1. 02 Sep, 2015 2 commits
  2. 18 Aug, 2015 2 commits
    • Kirill Smelkov's avatar
      bigfile/zodb: ZBlk._p_invalidate() can be called more than once, in particular in case of conflicts · 800c14a9
      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.
      800c14a9
    • Kirill Smelkov's avatar
      bigfile/zodb: Note that even LivePersistent goes to GHOST state on invalidation · 48b2bb74
      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.
      48b2bb74
  3. 17 Aug, 2015 3 commits
    • Kirill Smelkov's avatar
      bigfile/zodb: Do not hold reference to ZBigFileH indefinitely in Connection.onOpenCallback · 059c71e1
      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.
      059c71e1
    • Kirill Smelkov's avatar
      Drop support for ZODB < 3.10 · 105ab1c7
      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.
      105ab1c7
    • Kirill Smelkov's avatar
      bigfile: ZODB -> BigFileH invalidate propagation · 92bfd03e
      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).
      92bfd03e
  4. 12 Aug, 2015 1 commit
    • Kirill Smelkov's avatar
      bigfile/zodb: ZODB.Connection can migrate between threads on close/open and we have to care · c7c01ce4
      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
      c7c01ce4
  5. 26 Jun, 2015 1 commit
    • Kirill Smelkov's avatar
      tests: Allow to test with ZEO & NEO ZODB storages · 7fc4ec66
      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
      7fc4ec66
  6. 25 Jun, 2015 1 commit
    • Kirill Smelkov's avatar
      Move dbopen(), dbclose() to wendelin.lib.zodb · 72685306
      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.
      72685306
  7. 03 Apr, 2015 1 commit
    • Kirill Smelkov's avatar
      bigfile: BigFile backend to store data in ZODB · 4174b84a
      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/
      4174b84a