1. 12 Aug, 2015 4 commits
    • 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
    • Kirill Smelkov's avatar
      bigfile/zodb: Monkey-patch for ZODB.Connection to support callback on .open() · 64d1f40b
      Kirill Smelkov authored
      ZODB.Connection has support for calling callbacks on .close() but not on
      .open() . We'll need to hook into both Connection open/close process in the
      next patch (for _ZBigFileH to stay in sync with Connection state).
      
      NOTE
      
      on-open  callbacks are setup once and fire many times on every open,
      on-close callbacks are setup once and fire only once on next close.
      
      The reason for this is that on-close callbacks are useful for scheduling
      current connection cleanup, after its processing is done. But on-open
      callback is for future connection usage, which is generally not related
      to current connection.
      
      /cc @jm, @vpelletier
      64d1f40b
    • Kirill Smelkov's avatar
    • Kirill Smelkov's avatar
      bigarray/zodb: Forgot to close DB in tests · 070aeaa9
      Kirill Smelkov authored
      ( without dbclose, next test will not be able to open database - will
        timeout on open on waiting for FileStorage lock )
      070aeaa9
  2. 09 Aug, 2015 1 commit
    • Kirill Smelkov's avatar
      bigfile/py: Teach storeblk() how to correctly propagate traceback on error · 6da5172e
      Kirill Smelkov authored
      Previously we were limited to printing traceback starting down from just
      storeblk() via explicit PyErr_PrintEx() - because pybuf was attached to
      memory which could go away right after return from C function - so we
      had to destroy that object for sure, not letting any traceback to hold a
      reference to it.
      
      This turned out to be too limiting and not showing full context where
      errors happen.
      
      So do the following trick: before returning, reattach pybuf to empty
      region at NULL, and this way we don't need to worry about pybuf pointing
      to memory which can go away -> thus instead of printing exception locally
      - just return it the usual way it is done with C api in Python.
      
      NOTE In contrast to PyMemoryViewObject, PyBufferObject definition is not
      public, so to support Python2 - had to copy its definition to PY2 compat
      header.
      
      NOTE2 loadblk() is not touched - the loading is done from sighandler
      context, which simulates as if it work in separate python thread, so it
      is leaved as is for now.
      6da5172e
  3. 06 Aug, 2015 8 commits
    • Kirill Smelkov's avatar
      bigfile/virtmem: Big Virtmem lock · d53271b9
      Kirill Smelkov authored
      At present several threads running can corrupt internal virtmem
      datastructures (e.g. ram->lru_list, fileh->pagemap, etc).
      
      This can happen even if we have zope instances only with 1 worker thread
      - because there are other "system" thread, and python garbage collection
      can trigger at any thread, so if a virtmem object, e.g. VMA or FileH was
      there sitting at GC queue to be collected, their collection, and thus
      e.g. vma_unmap() and fileh_close() will be called from
      different-from-worker thread.
      
      Because of that virtmem just has to be aware of threads not to allow
      internal datastructure corruption.
      
      On the other hand, the idea of introducing userspace virtual memory
      manager turned out to be not so good from performance and complexity
      point of view, and thus the plan is to try to move it back into the
      kernel. This way it does not make sense to do a well-optimised locking
      implementation for userspace version.
      
      So we do just a simple single "protect-all" big lock for virtmem.
      
      Of a particular note is interaction with Python's GIL - any long-lived
      lock has to be taken with GIL released, because else it can deadlock:
      
          t1  t2
      
          G
          V   G
         !G   V
          G
      
      so we introduce helpers to make sure the GIL is not taken, and to retake
      it back if we were holding it initially.
      
      Those helpers (py_gil_ensure_unlocked / py_gil_retake_if_waslocked) are
      symmetrical opposites to what Python provides to make sure the GIL is
      locked (via PyGILState_Ensure / PyGILState_Release).
      
      Otherwise, the patch is more-or-less straightforward application for
      one-big-lock to protect everything idea.
      d53271b9
    • Kirill Smelkov's avatar
      lib/utils: X- versions for pthread_mutex_{lock,unlock} · 78cbf2a0
      Kirill Smelkov authored
      Mutex lock/unlock should not fail if mutex was correctly initialized/used.
      78cbf2a0
    • Kirill Smelkov's avatar
      bigfile: Simple test that we can handle GC from-under sighandler · 786d418d
      Kirill Smelkov authored
      And specifically that GC'ed object __del__ calls into virtmem
      (vma_dealloc and fileh_dealloc) again.
      
      NOTE not sure it is a good idea to do GC from under sighandle, but
           currently it happens in practice, because we did not cared to
           protect against it.
      786d418d
    • Kirill Smelkov's avatar
      bigfile/virtmem: When restoring SIGSEGV, don't change procmask for other signals · d7c33cd7
      Kirill Smelkov authored
      We factored out SIGSEGV block/restore from fileh_dirty_writeout() to all
      functions in cb7a7055 (bigfile/virtmem: Block/restore SIGSEGV in
      non-pagefault-handling function). The restoration however just sets
      whole thread sigmask.
      
      It could be possible that between block/restore calls procmask for other
      signals could be changed, and this way - setting procmask directly - we
      will overwrite them.
      
      So be careful, and when restoring SIGSEGV mask, touch mask bit for only
      that signal.
      
      ( we need xsigismember helper to get this done, which is also introduced
        in this patch )
      d7c33cd7
    • Kirill Smelkov's avatar
      lib/utils: pthread_sigmask() returns error directly, not in errno · 8fa9af7f
      Kirill Smelkov authored
      The mistake was there from the beginning - from 3e5e78cd (lib/utils:
      Small C utilities we'll use).
      8fa9af7f
    • Kirill Smelkov's avatar
      lib/bug: BUGerr(err) - like BUGe() but takes error code explicitly · ec6ecd4e
      Kirill Smelkov authored
      We'll need this for function which return error not in errno - e.g.
      pthread_sigmask().
      ec6ecd4e
    • Kirill Smelkov's avatar
    • Kirill Smelkov's avatar
      bigfile/virtmem: Block/restore SIGSEGV in non-pagefault-handling function · cb7a7055
      Kirill Smelkov authored
      Non on-pagefault code should not access any not-mmapped memory.
      
      Here we just refactor the code we already had to block/restore
      SIGSEGV from fileh_dirty_writeout() and use it in all functions called
      from non-pagefaulting context, as promised.
      
      This way, if there is an error in virtmem implementation which
      incorrectly accesses prepared for BigFile maps memory, we'll just die
      with coredump instead of trying to incorrectly handle the pagefault.
      cb7a7055
  4. 27 Jul, 2015 1 commit
    • Kirill Smelkov's avatar
      bigarray: In-place .append() · 1245acc9
      Kirill Smelkov authored
      ca064f75 (bigarray: Support resizing in-place) added O(1) in-place
      BigArray.resize() which makes possible for users to append data to BigArray in
      O(δ) time.
      
      But it is easy for people to make off-by-one mistakes when calculating
      indices for append.
      
      So provide a convenient BigArray.append() which simplifies the following
      
          A                               # ZBigArray e.g. of shape       (N, 3)
          values                          # ndarray to append of shape    (δ, 3)
          n, δ = len(A), len(values)      # length of A's major index  =N
          A.resize((n+δ, A.shape[1:]))    # add δ new entries ; now len(A) =N+δ
          A[-δ:] = values                 # set data for last new δ entries
      
      into
      
          A.append(values)
      
      /cc @klaus
      1245acc9
  5. 24 Jul, 2015 1 commit
  6. 26 Jun, 2015 4 commits
  7. 25 Jun, 2015 4 commits
  8. 12 Jun, 2015 1 commit
  9. 02 Jun, 2015 9 commits
    • Kirill Smelkov's avatar
      bigfile/py: We cannot use memoryview for py2 even on 2.7.10 · a5511edf
      Kirill Smelkov authored
      Because numpy.ndarray does not accept it as buffer= argument
      
          https://github.com/numpy/numpy/issues/5935
      
      and our memcpy crashes.
      
      NOTE if we'll need to use memoryview, we can adapt our memcpy to use
      array() directly which works with memoryview, as outlined in the above
      numpy issue.
      a5511edf
    • Kirill Smelkov's avatar
      bigarray: Teach it how to automatically convert to ndarray (if enough address space is available) · 00db08d6
      Kirill Smelkov authored
      BigArrays can be big - up to 2^64 bytes, and thus in general it is not
      possible to represent whole BigArray as ndarray view, because address
      space is usually smaller on 64bit architectures.
      
      However users often try to pass BigArrays to numpy functions as-is, and
      numpy finds a way to convert, or start converting, BigArray to ndarray -
      via detecting it as a sequence, and extracting elements one-by-one.
      Which is slooooow.
      
      Because of the above, we provide users a well-defined service:
      - if virtual address space is available - we succeed at creating ndarray
        view for whole BigArray, without delay and copying.
      - if not - we report properly the error and give hint how BigArrays have
        to be processed in chunks.
      
      Verifying that big BigArrays cannot be converted to ndarray also tests
      for behaviour and issues fixed in last 5 patches.
      
      /cc @Tyagov
      /cc @klaus
      00db08d6
    • Kirill Smelkov's avatar
      *: It is not safe to use multiply.reduce() - it overflows · 73926487
      Kirill Smelkov authored
      e.g.
      
          In [1]: multiply.reduce((1<<30, 1<<30, 1<<30))
          Out[1]: 0
      
      instead of
      
          In [2]: (1<<30) * (1<<30) * (1<<30)
          Out[2]: 1237940039285380274899124224
      
          In [3]: 1<<90
          Out[3]: 1237940039285380274899124224
      
      also multiply.reduce returns int64, instead of python int:
      
          In [4]: type( multiply.reduce([1,2,3]) )
          Out[4]: numpy.int64
      
      which also leads to overflow-related problems if we further compute with
      this value and other integers and results exceeds int64 - it becomes
      float:
      
          In [5]: idx0_stop = 18446744073709551615
      
          In [6]: stride0   = numpy.int64(1)
      
          In [7]: byte0_stop = idx0_stop * stride0
      
          In [8]: byte0_stop
          Out[8]: 1.8446744073709552e+19
      
      and then it becomes a real problem for BigArray.__getitem__()
      
          wendelin.core/bigarray/__init__.py:326: RuntimeWarning: overflow encountered in long_scalars
            page0_min  = min(byte0_start, byte0_stop+byte0_stride) // pagesize # TODO -> fileh.pagesize
      
      and then
      
          >           vma0 = self._fileh.mmap(page0_min, page0_max-page0_min+1)
          E           TypeError: integer argument expected, got float
      
      ~~~~
      
      So just avoid multiple.reduce() and do our own mul() properly the same
      way sum() is builtin into python, and we avoid overflow-related
      problems.
      73926487
    • Kirill Smelkov's avatar
      3rdparty/ccan: Update for bitmap_alloc0() segfault fix · d59b15a3
      Kirill Smelkov authored
      We need this commit:
      
          http://git.ozlabs.org/?p=ccan;a=commitdiff;h=c38e11b508e52fb2921e67d1123b05d9bef90fd2
      
      or else we segfault on really big arrays allocation instead of getting
      ENOMEM and reporting it as MemoryError to python.
      d59b15a3
    • Kirill Smelkov's avatar
      bigfile/py: Fix crash in {pyvma,pyfileh}_dealloc() if deallocated object was not fully constructed · 7e6829c7
      Kirill Smelkov authored
      Consider e.g. this for pyvma:
      
          1. in pyfileh_mmap() pyvma is created
      
          2. next fileh_mmap(pyvma, pyfileh, ...) fails
      
          3. we need to deallocate pyvma which was not mapped
      
          4. in pyvma_dealloc() we unmap pyvma unconditionally -> boom.
      
      The same story goes for pyfileh dealloc vs not fully constructing it in
      pyfileh_open().
      7e6829c7
    • Kirill Smelkov's avatar
      bigarray: Translate OverflowError when computing slice indices to MemoryError · fcbb26e6
      Kirill Smelkov authored
      OverflowError when computing slice indices practically means we'll
      cannot allocate so much address space at next step:
      
          In [1]: s = slice(None)
      
          In [2]: s.indices(1<<62)
          Out[2]: (0, 4611686018427387904, 1)
      
          In [3]: s.indices(1<<63)
          ---------------------------------------------------------------------------
          OverflowError                             Traceback (most recent call last)
          <ipython-input-4-5aa549641bc6> in <module>()
          ----> 1 s.indices(1<<63)
      
          OverflowError: cannot fit 'long' into an index-sized integer
      
      So translate this OverflowError into MemoryError (preserving message
      details), because we'll need such "no so much address space" cases to
      show up as MemoryError in a sooner patch.
      fcbb26e6
    • Kirill Smelkov's avatar
      bigfile/py: Raise MemoryError for ENOMEM errno · 2cf9073f
      Kirill Smelkov authored
      Currently we always raise RuntimeError for problems, which is
      more-or-less ok for humans, but soon we'll need to distinguish "no
      memory" errors from other error conditions in upper layers in code.
      
      Introduce helper function for choosing appropriate exception type for an
      error - MemoryError for when errno=ENOMEM and RuntimeError otherwise,
      and use it where appropriate.
      
      ( Unfortunately Python does not provide such a helper... )
      2cf9073f
    • Kirill Smelkov's avatar
      setup: file_finders entry-point group registration is not a workaround · 6fdde936
      Kirill Smelkov authored
      As discussion in https://bitbucket.org/pypa/setuptools/issue/313
      unveiled, setuptools do not have to carry setuptools.file_finders
      entrypoint with it in order to support _other_ projects to use
      .file_finders entry point.
      
      In our case it means that is is normal that we have to make sure that
      the group we are going to register entry-point into, exists.
      
      Remove erroneous comments introduced in 11d130d1 (setup: Ensure
      setuptools.file_finders entry-point group is registered).
      6fdde936
    • Kirill Smelkov's avatar
      setup: Fix for py3 · 25dbf467
      Kirill Smelkov authored
      py2-only syntax introduced in acf7e91d (setup/runcmd: Properly report
      errors, if running command is missing):
      
            File "setup.py", line 168
              except Exception, e:
                              ^
          SyntaxError: invalid syntax
      25dbf467
  10. 01 Jun, 2015 4 commits
  11. 28 May, 2015 3 commits
    • Kirill Smelkov's avatar
      setup: Ensure setuptools.file_finders entry-point group is registered · 11d130d1
      Kirill Smelkov authored
      If setuptools.file_finders group is not registered,
      dist.get_entry_map('setuptools.file_finders') returns just {} not
      connected to entry map, and further modifications of this dict go
      nowhere (and thus, our git_lsfiles() is not hooked -> sdist fails to
      produce correct source archive).
      
      This is a workaround for setuptools 9.0 dropping
      `setuptools.file_finders` entrypoint group registration:
      
          https://bitbucket.org/pypa/setuptools/commits/f191c8a1225bd58a5fb5aa9abb31b06dc710f0b9#Lsetup.pyF175
          https://bitbucket.org/pypa/setuptools/issue/313
      
      issue reported back:
      
          https://bitbucket.org/pypa/setuptools/issue/313#comment-18430008
      11d130d1
    • Kirill Smelkov's avatar
      bigarray: Test that asarray(BigArray(...)) does not hang · 0e25b01c
      Kirill Smelkov authored
      It was hanging with NumPy-1.9 before 425dc5d1 (bigarray: Raise
      IndexError for out-of-bound element access), because of the following
      correct NumPy commit:
      
          https://github.com/numpy/numpy/commit/d36f8227
      
      and in particular
      
          https://github.com/numpy/numpy/commit/d36f8227#diff-6d326badc0872de91e025cbfb0be1aafR522
      
      That PySequence_Fast(obj)    (with obj being BigArray)
      
      creates iterator on top of obj and before our previous IndexError fix in
      425dc5d1, this was looping forever.
      
      Test explicitly with both NumPy 1.8 and NumPy 1.9, that this construct
      does not hang.
      
      /cc @Tyagov
      0e25b01c
    • Kirill Smelkov's avatar
      bigarray: Raise IndexError for out-of-bound element access · 425dc5d1
      Kirill Smelkov authored
      The way BigArray.__getitem__ works for element access is that for e.g.
      
          A[i]
      
      it translates the request to
      
          A[i:i+1]
      
      and remembers to lower the dimensionality at scalar index
      
          dim_adjust = (0,)
      
      so, in full, A[i] is computed this way:
      
          A[i] -> A[i:i+1](0,)
      
      ( it is done this way to unify code for scalar / slice access in
        __getitem__ - see 0c826d5c "BigArray: An ndarray-like on top of
        BigFile memory mappings" )
      
      The code for slice access also has a shortcut - if it sees that slice
      results in empty array (e.g. for out-of-bound slice), we can avoid
      spending time to create a file vma mapping only to create empty view on
      top of it.
      
      In 0c826d5c, that optimization, however forgot to apply the "lower the
      dimensionality" step on top of resulting empty view, and that turned out
      for not raising IndexError for out-of-bounds scalar access:
      
          A = BigArray((10,), uint8)
          In [1]: A[0]
          Out[1]: 0
      
          In [2]: A[1]
          Out[2]: 0
      
          In [3]: A[2]
          Out[3]: 0
      
          In [4]: A[9]
          Out[4]: 0
      
          In [5]: A[10]
          Out[5]: array([], dtype=uint8)
      
      NOTE that A[10] returns empty array instead of raising IndexError.
      
      So do not forget to apply the "reduce dimensionality" step for empty
      views, and this way we get proper IndexError (because for empty view,
      scalar access results in IndexError).
      
      NOTE:
      
      this bug was also preventing for e.g.
      
          list(A)
      
      to work, because list(A) internally works this way:
      
          l = []
          i = iter(A)
          for _ in i:
              l.append(_)
      
      but iterating would not stop after 10 elements - after array end, _ will
      be always array([], dtype=uint8), and thus the loop never finished and
      memory usage grow to infinity.
      
      /cc @Tyagov
      425dc5d1