- 02 Sep, 2015 2 commits
-
-
Kirill Smelkov authored
bigfile/zodb/tests: Make sure _p_invalidate() in Zblk.loadblk() does not lead to reloading data updated Thanks to ZODB being MVCC this does not happen, but we better test explicitly.
-
Kirill Smelkov authored
We'll need it in other places in the next patch.
-
- 18 Aug, 2015 2 commits
-
-
Kirill Smelkov authored
When there is a conflict (on any object, but on ZBlk in particular) ZODB machinery calls its ._p_invalidate() twice: File ".../wendelin.core/bigfile/tests/test_filezodb.py", line 661, in test_bigfile_filezodb_vs_conflicts tm2.commit() # this should raise ConflictError and stay at 11 state File ".../transaction/_manager.py", line 111, in commit return self.get().commit() File ".../transaction/_transaction.py", line 271, in commit self._commitResources() File ".../transaction/_transaction.py", line 414, in _commitResources self._cleanup(L) File ".../transaction/_transaction.py", line 426, in _cleanup rm.abort(self) File ".../ZODB/Connection.py", line 436, in abort self._abort() File ".../ZODB/Connection.py", line 479, in _abort self._cache.invalidate(oid) File ".../wendelin.core/bigfile/file_zodb.py", line 148, in _p_invalidate traceback.print_stack() and File ".../wendelin.core/bigfile/tests/test_filezodb.py", line 661, in test_bigfile_filezodb_vs_conflicts tm2.commit() # this should raise ConflictError and stay at 11 state File ".../transaction/_manager.py", line 111, in commit return self.get().commit() File ".../transaction/_transaction.py", line 271, in commit self._commitResources() File ".../transaction/_transaction.py", line 416, in _commitResources self._synchronizers.map(lambda s: s.afterCompletion(self)) File ".../transaction/weakset.py", line 59, in map f(elt) File ".../transaction/_transaction.py", line 416, in <lambda> self._synchronizers.map(lambda s: s.afterCompletion(self)) File ".../ZODB/Connection.py", line 831, in _storage_sync self._flush_invalidations() File ".../ZODB/Connection.py", line 539, in _flush_invalidations self._cache.invalidate(invalidated) File ".../wendelin.core/bigfile/file_zodb.py", line 148, in _p_invalidate traceback.print_stack() i.e. first invalidation is done by commit cleanup: https://github.com/zopefoundation/transaction/blob/1.4.4/transaction/_transaction.py#L414 https://github.com/zopefoundation/ZODB/blob/3.10/src/ZODB/Connection.py#L479 and then Connection.afterCompletion() flushes invalidation again: https://github.com/zopefoundation/transaction/blob/1.4.4/transaction/_transaction.py#L416 https://github.com/zopefoundation/ZODB/blob/3.10/src/ZODB/Connection.py#L833 https://github.com/zopefoundation/ZODB/blob/3.10/src/ZODB/Connection.py#L539 If there was no conflict - there will be no ConflictError raised and thus no Transaction._cleanup() done in its ._commitResources() -> invalidation called only once. But with ConflictError - it is twice. Adjust ZBlk._p_invalidate() not to delve into real invalidation more than once - else we will fail, as ZBlk._v_zfile becomes unbound after invalidation done the first time.
-
Kirill Smelkov authored
LivePersistent can go to ghost state, because invalidation cannot be ignored, i.e. they indicate the object has been changed externally. This does not break our logic for ZBigFile and ZBigArray as invalidations can happen only at transaction boundary, so during the course of transaction those classes are guaranteed to stay uptodate and thus not loose ._v_file and ._v_fileh (which is the reason they inherit from LivePersistent). it is ok to loose ._v_file and ._v_fileh at transaction boundary and become ghost - those objects will be recreated upon going back uptodate and will stay alive again during the whole transaction window. We care only not to loose e.g. ._v_fileh inside transaction, because loosing that data manager and thus data it manages inside transaction can break synchronization logic and forget changed-through-mmap data.
-
- 17 Aug, 2015 3 commits
-
-
Kirill Smelkov authored
If we do - ZBigFileH objects just don't get garbage collected, and sooner or later this way it leaks enough filedescriptors so that main zope loop breaks: Traceback (most recent call last): File ".../bin/runzope", line 194, in <module> sys.exit(Zope2.Startup.run.run()) File ".../eggs/Zope2-2.13.22-py2.7.egg/Zope2/Startup/run.py", line 26, in run starter.run() File ".../eggs/Zope2-2.13.22-py2.7.egg/Zope2/Startup/__init__.py", line 105, in run Lifetime.loop() File ".../eggs/Zope2-2.13.22-py2.7.egg/Lifetime/__init__.py", line 43, in loop lifetime_loop() File ".../eggs/Zope2-2.13.22-py2.7.egg/Lifetime/__init__.py", line 53, in lifetime_loop asyncore.poll(timeout, map) File ".../parts/python2.7/lib/python2.7/asyncore.py", line 145, in poll r, w, e = select.select(r, w, e, timeout) ValueError: filedescriptor out of range in select() $ lsof -p <runzope-pid> |grep ramh | wc -l 950 So continuing 64d1f40b (bigfile/zodb: Monkey-patch for ZODB.Connection to support callback on .open()) let's change the implementation to use WeakSet for callbacks list. Yes, because weakref to bound methods release immediately, we give up flexibility to subscribe to arbitrary callbacks. If it become an issue, something like WeakMethod from py3 or recipes from the net how to do it are there.
-
Kirill Smelkov authored
ZODB 3.10.4 was released almost 4 years ago, and contains significant change how ghost objects coming from DB are initially setup.
-
Kirill Smelkov authored
Continuing theme from the previous patch, here is propagation of invalidation messages from ZODB to BigFileH memory. The use-case here is that e.g. one fileh mapping was created in one connection, another in another, and after doing changes in second connection and committing there, the first fileh has to invalidate appropriate already-loaded pages, so its next transaction won't work with stale data. To do it, we hook into ZBlk._p_invalidate() and propagate the invalidation message to ZBigFile which then notifies all opened-through-it ZBigFileH to invalidate a page. ZBlk -> ZBigFile lookup is done without storing backpointer in ZODB - instead, every time ZBigFile touches ZBlk object (and thus potentially does GHOST -> Live transition to it), we (re-)bind it back to ZBigFile. Since ZBigFile is the only class that works with ZBlk objects it is safe to do so. For ZBigFile to notify "all-opened-through-it" ZBigFileH, a weakset is introduced to track them. Otherwise the real page invalidation work is done by virtmem (see previous patch).
-
- 12 Aug, 2015 1 commit
-
-
Kirill Smelkov authored
Intro ----- ZODB maintains pool of opened-to-DB connections. For each request Zope opens 1 connection and, after request handling is done, returns the connection back to ZODB pool (via Connection.close()). The same connection will be opened again for handling some future next request at some future time. This next open can happen in different-from-first request worker thread. TransactionManager (as accessed by transaction.{get,commit,abort,...}) is thread-local, that is e.g. transaction.get() returns different transaction for threads T1 and T2. When _ZBigFileH hooks into txn_manager to get a chance to run its .beforeCompletion() when transaction.commit() is run, it hooks into _current_ _thread_ transaction manager. Without unhooking on connection close, and circumstances where connection migrates to different thread this can lead to dissynchronization between ZBigFileH managing fileh pages and Connection with ZODB objects. And even to data corruption, e.g. T1 T2 open zarray[0] = 11 commit close open # opens connection as closed in T1 open zarray[0] = 21 commit abort close close Here zarray[0]=21 _will_ be committed by T1 as part of T1 transaction - because when T1 does commit .beforeCompletion() for zarray is invoked, sees there is dirty data and propagate changes to zodb objects in connection for T2, joins connection for T2 into txn for T1, and then txn for t1 when doing two-phase-commit stores modified objects to DB -> oops. ---------------------------------------- To prevent such dissynchronization _ZBigFileH needs to be a DataManager which works in sync with the connection it was initially created under - on connection close, unregister from transaction_manager, and on connection open, register to transaction manager in current, possibly different, thread context. Then there won't be incorrect beforeCompletion() notification and corruption. This issue, besides possible data corruption, was probably also exposing itself via following ways we've seen in production (everywhere connection was migrated from T1 to T2): 1. Exception ZODB.POSException.ConnectionStateError: ConnectionStateError('Cannot close a connection joined to a transaction',) in <bound method Cleanup.__del__ of <App.ZApplication.Cleanup instance at 0x7f10f4bab050>> ignored T1 T2 modify zarray commit/abort # does not join zarray to T2.txn, # because .beforeCompletion() is # registered in T1.txn_manager commit # T1 invokes .beforeCompletion() ... # beforeCompletion() joins ZBigFileH and zarray._p_jar (=T2.conn) to T1.txn ... # commit is going on in progress ... ... close # T2 thinks request handling is done and ... # and closes connection. But T2.conn is ... # still joined to T1.txn 2. Traceback (most recent call last): File ".../wendelin/bigfile/file_zodb.py", line 121, in storeblk def storeblk(self, blk, buf): return self.zself.storeblk(blk, buf) File ".../wendelin/bigfile/file_zodb.py", line 220, in storeblk zblk._v_blkdata = bytes(buf) # FIXME does memcpy File ".../ZODB/Connection.py", line 857, in setstate raise ConnectionStateError(msg) ZODB.POSException.ConnectionStateError: Shouldn't load state for 0x1f23a5 when the connection is closed Similar to "1", but close in T2 happens sooner, so that when T1 does the commit and tries to store object to database, Connection refuses to do the store: T1 T2 modify zarray commit/abort commit ... close ... ... . obj.store() ... ... 3. Traceback (most recent call last): File ".../wendelin/bigfile/file_zodb.py", line 121, in storeblk def storeblk(self, blk, buf): return self.zself.storeblk(blk, buf) File ".../wendelin/bigfile/file_zodb.py", line 221, in storeblk zblk._p_changed = True # if zblk was already in DB: _p_state -> CHANGED File ".../ZODB/Connection.py", line 979, in register self._register(obj) File ".../ZODB/Connection.py", line 989, in _register self.transaction_manager.get().join(self) File ".../transaction/_transaction.py", line 220, in join Status.ACTIVE, Status.DOOMED, self.status)) ValueError: expected txn status 'Active' or 'Doomed', but it's 'Committing' ( storeblk() does zblk._p_changed -> Connection.register(zblk) -> txn.join() but txn is already committing IOW storeblk() was invoked with txn.state being already 'Committing' ) T1 T2 modify obj # this way T2.conn joins T2.txn modify zarray commit # T1 invokes .beforeCompletion() ... # beforeCompletion() joins only _ZBigFileH to T1.txn ... # (because T2.conn is already marked as joined) ... ... commit/abort # T2 does commit/abort - this touches only T2.conn, not ZBigFileH ... # in particular T2.conn is now reset to be not joined ... . tpc_begin # actual active commit phase of T1 was somehow delayed a bit . tpc_commit # when changes from RAM propagate to ZODB objects associated . storeblk # connection (= T2.conn !) is notified again, . zblk = ... # wants to join txn for it thinks its transaction_manager, # which when called from under T1 returns *T1* transaction manager for # which T1.txn is already in state='Committing' 4. Empty transaction committed to NEO ( different from doing just transaction.commit() without changing any data - a connection was joined to txn, but set of modified object turned out to be empty ) This is probably a race in Connection._register when both T1 and T2 go to it at the same time: https://github.com/zopefoundation/ZODB/blob/3.10/src/ZODB/Connection.py#L988 def _register(self, obj=None): if self._needs_to_join: self.transaction_manager.get().join(self) self._needs_to_join = False T1 T2 modify zarray commit ... .beforeCompletion modify obj . if T2.conn.needs_join if T2.conn.needs_join # race here . T2.conn.join(T1.txn) T2.conn.join(T2.txn) # as a result T2.conn joins both T1.txn and T2.txn . commit finishes # T2.conn registered-for-commit object list is now empty commit tpc_begin storage.tpc_begin tpc_commit # no object stored, because for-commit-list is empty /cc @jm, @klaus, @Tyagov, @vpelletier
-
- 26 Jun, 2015 1 commit
-
-
Kirill Smelkov authored
Previously we were always testing with DBs backed up by FileStorage. Now we provide a way to run the testsuite with user selected storage backend: $ WENDELIN_CORE_TEST_DB="<fs>" make test.py # test with temporary db with FileStorage $ WENDELIN_CORE_TEST_DB="<zeo>" make test.py # ----------//---------- with ZEO $ WENDELIN_CORE_TEST_DB="<neo>" make test.py # ----------//---------- with NEO $ WENDELIN_CORE_TEST_DB=neo://db@master make test.py # test with externally provided DB Default is still to run tests with FileStorage. /cc @jm
-
- 25 Jun, 2015 1 commit
-
-
Kirill Smelkov authored
Factor out those routines to open a ZODB database to common place. The reason for doing so is that we'll soon teach dbopen to automatically recognize several protocols, e.g. neo:// and zeo:// and this way, clients who use dbopen() could automatically access storages besides FileStorage.
-
- 03 Apr, 2015 1 commit
-
-
Kirill Smelkov authored
This adds transactionality and with e.g. NEO[1] allows to distribute objects to nodes into cluster. We hook into ZODB two-phase commit process as a separate data manager, and synchronize changes to memory, to changes to object only at that time. Alternative would be to get notified on every page change, and mark appropriate object as dirty right at that moment. But I wanted to stay close to filesystem design (we don't get notification for every file change from kernel) - that's why it is done the first way. [1] http://www.neoppod.org/
-