// Copyright (C) 2018-2019 Nexedi SA and Contributors. // Kirill Smelkov <kirr@nexedi.com> // // This program is free software: you can Use, Study, Modify and Redistribute // it under the terms of the GNU General Public License version 3, or (at your // option) any later version, as published by the Free Software Foundation. // // You can also Link and Combine this program with other software covered by // the terms of any of the Free Software licenses or any of the Open Source // Initiative approved licenses and Convey the resulting work. Corresponding // source of such a combination shall include the source code for all other // software used. // // This program is distributed WITHOUT ANY WARRANTY; without even the implied // warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // // See COPYING file for full licensing terms. // See https://www.nexedi.com/licensing for rationale and options. package zodb // application-level database connection. import ( "context" "fmt" "sync" "lab.nexedi.com/kirr/go123/mem" "lab.nexedi.com/kirr/go123/xerr" "lab.nexedi.com/kirr/neo/go/transaction" "lab.nexedi.com/kirr/neo/go/zodb/internal/weak" ) // Connection represents application-level view of a ZODB database. // // The view is represented by IPersistent objects associated with the connection. // Connection changes are private and are isolated from changes in other Connections. // Connection's view corresponds to particular database state and is thus // isolated from further database transactions. // // Connection is safe to access from multiple goroutines simultaneously. // // Connection and objects obtained from it must be used by application only // inside transaction where Connection was opened. // // Use DB.Open to open a connection. type Connection struct { stor IStorage // underlying storage db *DB // Connection is part of this DB txn transaction.Transaction // opened under this txn; nil if idle in DB pool. at Tid // current view of database; stable inside a transaction. cache LiveCache // cache of connection's in-RAM objects } // LiveCache keeps registry of live in-RAM objects for a Connection. // // It semantically consists of // // {} oid -> obj // // but does not hold strong reference to cached objects. // // LiveCache is not safe to use from multiple goroutines simultaneously. // // Use .Lock() / .Unlock() to serialize access. type LiveCache struct { // rationale for using weakref: // // on invalidations: we need to go oid -> obj and invalidate it. // -> Connection need to keep {} oid -> obj. // -> we can use that {} when loading a persistent Ref twice to get to the same object. // // however: if Connection keeps strong link to obj, just // obj.PDeactivate will not fully release obj if there are no // references to it from other objects: // // - deactivate will release obj state (ok) // - but there will be still reference from connection `oid -> obj` map to this object, // which means the object won't be garbage-collected. // // -> we can solve it by using "weak" pointers in the map. // // NOTE we cannot use regular map and arbitrarily manually "gc" entries // there periodically: since for an obj we don't know whether other // objects are referencing it, we can't just remove obj's oid from // the map - if we do so and there are other live objects that // reference obj, user code can still reach obj via those // references. On the other hand, if another, not yet loaded, object // also references obj and gets loaded, traversing reference from // that loaded object will load second copy of obj, thus breaking 1 // object in db <-> 1 live object invariant: // // A → B → C // ↓ | // D <--------- - - -> D2 (wrong) // // - A activate // - D activate // - B activate // - D gc, A still keeps link on D // - C activate -> it needs to get to D, but D was removed from objtab // -> new D2 is wrongly created // // that's why we have to depend on Go's GC to know whether there are // still live references left or not. And that in turn means finalizers // and thus weak references. // // some link on the subject: // https://groups.google.com/forum/#!topic/golang-nuts/PYWxjT2v6ps // // NOTE2 finalizers don't run on when they are attached to an object in cycle. // Hopefully we don't have cycles with BTree/Bucket. sync.Mutex objtab map[Oid]*weak.Ref // oid -> weak.Ref(IPersistent) // hooks for application to influence live caching decisions. control LiveCacheControl } // LiveCacheControl is the interface that allows applications to influence // Connection's decisions with respect to Connection's live cache. type LiveCacheControl interface { // WantEvict is called when object is going to be evicted from live // cache on deactivation and made ghost. // // If !ok the object will remain live. // // NOTE on invalidation invalidated objects are evicted from live cache // unconditionally. WantEvict(obj IPersistent) (ok bool) } // ---------------------------------------- // newConnection creates new Connection associated with db. func newConnection(db *DB, at Tid) *Connection { return &Connection{ stor: db.stor, db: db, at: at, cache: LiveCache{ objtab: make(map[Oid]*weak.Ref), }, } } // At returns database state corresponding to the connection. func (conn *Connection) At() Tid { conn.checkLive("at") return conn.at } // Cache returns connection's cache of live objects. func (conn *Connection) Cache() *LiveCache { return &conn.cache } // wrongClassError is the error cause returned when ZODB object's class is not what was expected. type wrongClassError struct { want, have string } func (e *wrongClassError) Error() string { return fmt.Sprintf("wrong class: want %q; have %q", e.want, e.have) } // Get lookups object corresponding to oid in the cache. // // 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 { wobj := cache.objtab[oid] var obj IPersistent if wobj != nil { if xobj := wobj.Get(); xobj != nil { obj = xobj.(IPersistent) } } return obj } // SetControl installs c to handle cache decisions. // // Any previously installed cache control is uninstalled. // Passing nil sets the cache to have no control installed at all. // // It is not safe to call SetControl simultaneously to other cache operations. func (cache *LiveCache) SetControl(c LiveCacheControl) { cache.control = c } // get is like Get, but used when we already know object class. // // Use-case: in ZODB references are (pyclass, oid), so new ghost is created // without further loading anything. func (conn *Connection) get(class string, oid Oid) (IPersistent, error) { checkClass := true conn.cache.Lock() // XXX -> rlock? obj := conn.cache.Get(oid) if obj == nil { obj = newGhost(class, oid, conn) conn.cache.objtab[oid] = weak.NewRef(obj) checkClass = false } conn.cache.Unlock() if checkClass { if cls := ClassOf(obj); class != cls { var err error = &wrongClassError{class, cls} xerr.Contextf(&err, "get %s", Xid{conn.at, oid}) return nil, err } } return obj, nil } // Get returns in-RAM object corresponding to specified ZODB object according to current database view. // // If there is already in-RAM object that corresponds to oid, that in-RAM object is returned. // Otherwise new in-RAM object is created and filled with object's class loaded from the database. // // The scope of the object returned is the Connection. // // The object's data is not necessarily loaded after Get returns. Use // PActivate to make sure the object is fully loaded. func (conn *Connection) Get(ctx context.Context, oid Oid) (_ IPersistent, err error) { conn.checkTxnCtx(ctx, "Get") defer xerr.Contextf(&err, "Get %s", oid) conn.cache.Lock() // XXX -> rlock? obj := conn.cache.Get(oid) conn.cache.Unlock() // object was already there in cache. if obj != nil { return obj, nil } // object is not in cache - raw load it, get its class -> get(pyclass, oid) // XXX "py always" hardcoded class, pystate, serial, err := conn.loadpy(ctx, oid) if err != nil { xerr.Contextf(&err, "Get %s", Xid{conn.at, oid}) return nil, err } obj, err = conn.get(class, oid) if err != nil { return nil, err } // XXX we are dropping just loaded pystate. Usually Get should be used // to only load root object, so maybe that is ok. // // TODO -> use (pystate, serial) to activate. _, _ = pystate, serial return obj, nil } // load loads object specified by oid. func (conn *Connection) load(ctx context.Context, oid Oid) (_ *mem.Buf, serial Tid, _ error) { conn.checkTxnCtx(ctx, "load") return conn.stor.Load(ctx, Xid{Oid: oid, At: conn.at}) } // ---------------------------------------- // checkTxnCtx asserts that current transaction is the same as conn.txn . func (conn *Connection) checkTxnCtx(ctx context.Context, who string) { conn.checkTxn(transaction.Current(ctx), who) } // checkTxn asserts that specified "current" transaction is the same as conn.txn . func (conn *Connection) checkTxn(txn transaction.Transaction, who string) { if txn != conn.txn { panic("connection: " + who + ": current transaction is different from connection transaction") } } // checkLive asserts that the connection is alive - the transaction under which // it has been opened is not yet complete. func (conn *Connection) checkLive(who string) { if conn.txn == nil { panic("connection: " + who + ": connection is not live") } }