Commit 4357db3f authored by Kirill Smelkov's avatar Kirill Smelkov

go/zodb: LiveCache: Allow objects to be pinned / omitted from the cache

We already have LiveCacheControl and policy to keep object state in live
cache. However that state is currently kept only until the object is
present in the cache, and the object is there currently only until there
are live pointers to the object from anywhere. If all such pointers go
away, LiveCache currently discards the object.

Add a way (PCachePinObject) to tell live cache that it must retain an
object even if there are no other pointers to it. This is needed for
Wendelin.core which semantically relies on some objects to be present in
live cache, even if in ghost state, for propagating ZODB invalidations
into OS pagecache:

https://lab.nexedi.com/kirr/wendelin.core/blob/000bf16359/wcfs/wcfs.go#L245

For symmetry add a way (PCacheDropObject) to tell live cache that it
must not retain an object. This makes sense for objects that are read in
large only once to avoid needlessly evicting all other objects while
making the room for what won't be soon used again.

For completeness add also a way (PCacheDropState) to tell live cache
that the state of this object should not be cached. This can make sense
even for objects that are pinned: for example Wendelin.core pins ZBlk
objects to rely on volatile metadata it attaches to them, but have to
drop ZBlk state that was loaded from database: that state is propagated
to be cached in OS pagecache, and keeping it also in ZODB live cache
would just waste RAM.

Finally add test to cover LiveCache/LiveCacheControl functionality in
detail.
parent cb6fd5a8
...@@ -65,6 +65,13 @@ type Connection struct { ...@@ -65,6 +65,13 @@ type Connection struct {
// //
// Use .Lock() / .Unlock() to serialize access. // Use .Lock() / .Unlock() to serialize access.
type LiveCache struct { type LiveCache struct {
sync.Mutex
// pinned objects. may have referrers.
pinned map[Oid]IPersistent
// not pinned objects. may have referrers. cache keeps weak references to objects.
//
// rationale for using weakref: // rationale for using weakref:
// //
// on invalidations: we need to go oid -> obj and invalidate it. // on invalidations: we need to go oid -> obj and invalidate it.
...@@ -111,8 +118,6 @@ type LiveCache struct { ...@@ -111,8 +118,6 @@ type LiveCache struct {
// //
// NOTE2 finalizers don't run on when they are attached to an object in cycle. // NOTE2 finalizers don't run on when they are attached to an object in cycle.
// Hopefully we don't have cycles with BTree/Bucket. // Hopefully we don't have cycles with BTree/Bucket.
sync.Mutex
objtab map[Oid]*weak.Ref // oid -> weak.Ref(IPersistent) objtab map[Oid]*weak.Ref // oid -> weak.Ref(IPersistent)
// hooks for application to influence live caching decisions. // hooks for application to influence live caching decisions.
...@@ -135,14 +140,37 @@ type LiveCacheControl interface { ...@@ -135,14 +140,37 @@ type LiveCacheControl interface {
type PCachePolicy int type PCachePolicy int
const ( const (
// keep object pinned into cache, even if in ghost state.
//
// This allows to rely on object being never evicted from live cache.
//
// Note: object's state can still be discarded and the object can go
// into ghost state. Use PCacheKeepState to prevent such automatic
// state eviction until state discard is semantically required.
PCachePinObject PCachePolicy = 1 << iota
// don't keep object in cache.
//
// The object will be discarded from the cache completely as soon as it
// is semantically valid to do so.
PCacheDropObject
// keep object state in cache. // keep object state in cache.
// //
// This prevents object state to go away when !dirty object is no // This prevents object state to go away when !dirty object is no
// longer used. // longer used. However the object itself can go away unless it is
// pinned into cache via PCachePinObject.
// //
// Note: on invalidation, state of invalidated objects is discarded // Note: on invalidation, state of invalidated objects is discarded
// unconditionally. // unconditionally.
PCacheKeepState PCachePolicy = 1 << iota PCacheKeepState
// don't keep object state.
//
// Data access is likely non-temporal and object's state will be used
// once and then won't be used for a long time. Don't pollute cache
// with state of this object.
PCacheDropState
) )
// ---------------------------------------- // ----------------------------------------
...@@ -153,6 +181,7 @@ func newConnection(db *DB, at Tid) *Connection { ...@@ -153,6 +181,7 @@ func newConnection(db *DB, at Tid) *Connection {
db: db, db: db,
at: at, at: at,
cache: LiveCache{ cache: LiveCache{
pinned: make(map[Oid]IPersistent),
objtab: make(map[Oid]*weak.Ref), objtab: make(map[Oid]*weak.Ref),
}, },
} }
...@@ -182,13 +211,23 @@ func (e *wrongClassError) Error() string { ...@@ -182,13 +211,23 @@ func (e *wrongClassError) Error() string {
// //
// If object is found, it is guaranteed to stay in live cache while the caller keeps reference to it. // If object is found, it is guaranteed to stay in live cache while the caller keeps reference to it.
func (cache *LiveCache) Get(oid Oid) IPersistent { func (cache *LiveCache) Get(oid Oid) IPersistent {
// 1. lookup in pinned objects (likely hottest ones)
obj := cache.pinned[oid]
if obj != nil {
return obj
}
// 2. lookup in !pinned referenced object (they are likely to be loaded
// going from a referrer)
wobj := cache.objtab[oid] wobj := cache.objtab[oid]
var obj IPersistent
if wobj != nil { if wobj != nil {
if xobj := wobj.Get(); xobj != nil { if xobj := wobj.Get(); xobj != nil {
obj = xobj.(IPersistent) obj = xobj.(IPersistent)
} }
} }
// 3. TODO lookup in non-referenced LRU cache
return obj return obj
} }
...@@ -196,11 +235,24 @@ func (cache *LiveCache) Get(oid Oid) IPersistent { ...@@ -196,11 +235,24 @@ func (cache *LiveCache) Get(oid Oid) IPersistent {
// //
// The cache must not have entry for oid when setNew is called. // The cache must not have entry for oid when setNew is called.
func (cache *LiveCache) setNew(oid Oid, obj IPersistent) { func (cache *LiveCache) setNew(oid Oid, obj IPersistent) {
cache.objtab[oid] = weak.NewRef(obj) var cp PCachePolicy
if cc := cache.control; cc != nil {
cp = cache.control.PCacheClassify(obj)
}
if cp & PCachePinObject != 0 {
cache.pinned[oid] = obj
// XXX assert .objtab[oid] == nil ?
} else {
cache.objtab[oid] = weak.NewRef(obj)
// XXX asseer .pinned[oid] == nil ?
}
} }
// forEach calls f for all objects in the cache. // forEach calls f for all objects in the cache.
func (cache *LiveCache) forEach(f func(IPersistent)) { func (cache *LiveCache) forEach(f func(IPersistent)) {
for _, obj := range cache.pinned {
f(obj)
}
for _, wobj := range cache.objtab { for _, wobj := range cache.objtab {
if xobj := wobj.Get(); xobj != nil { if xobj := wobj.Get(); xobj != nil {
f(xobj.(IPersistent)) f(xobj.(IPersistent))
......
...@@ -276,6 +276,8 @@ func (obj *Persistent) PDeactivate() { ...@@ -276,6 +276,8 @@ func (obj *Persistent) PDeactivate() {
return return
} }
// TODO cp & PCacheDropState | PCacheDropObject -> drop unconditionally; otherwise -> LRU
obj.serial = InvalidTid obj.serial = InvalidTid
obj.istate().DropState() obj.istate().DropState()
obj.state = GHOST obj.state = GHOST
......
...@@ -25,6 +25,7 @@ import ( ...@@ -25,6 +25,7 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"reflect" "reflect"
"runtime"
"testing" "testing"
"lab.nexedi.com/kirr/neo/go/transaction" "lab.nexedi.com/kirr/neo/go/transaction"
...@@ -37,7 +38,8 @@ import ( ...@@ -37,7 +38,8 @@ import (
type MyObject struct { type MyObject struct {
Persistent Persistent
value string value string // persistent state
_v_cookie string // volatile in-RAM only state; not managed by persistency layer
} }
func NewMyObject(jar *Connection) *MyObject { func NewMyObject(jar *Connection) *MyObject {
...@@ -454,7 +456,8 @@ func (t *tConnection) fatalif(err error) { ...@@ -454,7 +456,8 @@ func (t *tConnection) fatalif(err error) {
// Persistent tests with storage. // Persistent tests with storage.
// //
// this test covers everything at application-level: Persistent, DB, Connection, LiveCache. // this test covers everything at application-level: Persistent, DB, Connection
// and somewhat LiveCache.
func TestPersistentDB(t *testing.T) { func TestPersistentDB(t *testing.T) {
// perform tests without and with raw data cache. // perform tests without and with raw data cache.
// (rawcache=y verifies how raw cache handles invalidations) // (rawcache=y verifies how raw cache handles invalidations)
...@@ -492,7 +495,7 @@ func testPersistentDB(t0 *testing.T, rawcache bool) { ...@@ -492,7 +495,7 @@ func testPersistentDB(t0 *testing.T, rawcache bool) {
// do not evict obj2 from live cache. obj1 is ok to be evicted. // do not evict obj2 from live cache. obj1 is ok to be evicted.
zcc := &zcacheControl{map[Oid]PCachePolicy{ zcc := &zcacheControl{map[Oid]PCachePolicy{
102: PCacheKeepState, 102: PCachePinObject | PCacheKeepState,
}} }}
zcache1 := t.conn.Cache() zcache1 := t.conn.Cache()
...@@ -713,6 +716,108 @@ func testPersistentDB(t0 *testing.T, rawcache bool) { ...@@ -713,6 +716,108 @@ func testPersistentDB(t0 *testing.T, rawcache bool) {
t.checkObj(robj2, 102, InvalidTid, GHOST, 0) t.checkObj(robj2, 102, InvalidTid, GHOST, 0)
} }
// Test details of how LiveCache handles live caching policy.
func TestLiveCache(t0 *testing.T) {
assert := assert.New(t0)
tdb := testdb(t0, /*rawcache=*/false)
defer tdb.Close()
tdb.Add(101, "мир")
tdb.Add(102, "труд")
tdb.Add(103, "май")
tdb.Add(104, "весна")
tdb.Commit()
at1 := tdb.head
zcc := &zcacheControl{map[Oid]PCachePolicy{
// obj1 - default (currently: don't pin and don't keep state)
// TODO: it should go into LRU on release, not dropped
101: 0,
// objPK - pin and keep state
102: PCachePinObject | PCacheKeepState,
// objPD - pin, but don't keep state
103: PCachePinObject | PCacheDropState,
// objDD - drop object and state
104: PCacheDropObject | PCacheDropState,
// XXX objDK ?
}}
t := tdb.Open(&ConnOptions{})
zcache := t.conn.Cache()
zcache.Lock()
zcache.SetControl(zcc)
zcache.Unlock()
// get objects
obj := t.Get(101)
objPK := t.Get(102)
objPD := t.Get(103)
objDD := t.Get(104)
t.checkObj(obj, 101, InvalidTid, GHOST, 0)
t.checkObj(objPK, 102, InvalidTid, GHOST, 0)
t.checkObj(objPD, 103, InvalidTid, GHOST, 0)
t.checkObj(objDD, 104, InvalidTid, GHOST, 0)
// activate * -> uptodate
t.PActivate(obj)
t.PActivate(objPK)
t.PActivate(objPD)
t.PActivate(objDD)
t.checkObj(obj, 101, at1, UPTODATE, 1, "мир")
t.checkObj(objPK, 102, at1, UPTODATE, 1, "труд")
t.checkObj(objPD, 103, at1, UPTODATE, 1, "май")
t.checkObj(objDD, 104, at1, UPTODATE, 1, "весна")
// deactivate obj{,PD,DD} drop state, objPK is kept
obj.PDeactivate()
objPK.PDeactivate()
objPD.PDeactivate()
objDD.PDeactivate()
t.checkObj(obj, 101, InvalidTid, GHOST, 0)
t.checkObj(objPK, 102, at1, UPTODATE, 0, "труд")
t.checkObj(objPD, 103, InvalidTid, GHOST, 0)
t.checkObj(objDD, 104, InvalidTid, GHOST, 0)
// live cache should keep pinned object present even if we drop all
// regular pointers to it and do GC.
obj._v_cookie = "peace"
objPK._v_cookie = "labour"
objPD._v_cookie = "may"
objDD._v_cookie = "spring"
obj = nil
objPK = nil
objPD = nil
objDD = nil
for i := 0; i < 10; i++ {
runtime.GC() // need only 2 runs since cache uses finalizers
}
xobj := zcache.Get(101)
xobjPK := zcache.Get(102)
xobjPD := zcache.Get(103)
xobjDD := zcache.Get(104)
assert.Equal(xobj, nil)
assert.NotEqual(xobjPK, nil)
assert.NotEqual(xobjPD, nil)
assert.Equal(xobjDD, nil)
objPK = xobjPK.(*MyObject)
objPD = xobjPD.(*MyObject)
t.checkObj(objPK, 102, at1, UPTODATE, 0, "труд")
t.checkObj(objPD, 103, InvalidTid, GHOST, 0)
assert.Equal(objPK._v_cookie, "labour")
assert.Equal(objPD._v_cookie, "may")
obj = t.Get(101)
objDD = t.Get(104)
t.checkObj(obj, 101, InvalidTid, GHOST, 0)
t.checkObj(objDD, 104, InvalidTid, GHOST, 0)
assert.Equal(obj._v_cookie, "")
assert.Equal(objDD._v_cookie, "")
}
// TODO Map & List tests. // TODO Map & List tests.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment