[ZODB-Dev] A source of POSKeyError

Tim Peters tim at zope.com
Tue Aug 24 19:58:20 EDT 2004


Zope Collector #789 leads to a way to produce POSKeyError in the 3.2 ZODB
line.  We'll fix it, but before then I thought the zodb-dev'ers would want
to be aware of it.  Here's a test case.  This path requires the combination
of a subtransaction, and closing the connection without finishing the
containing transaction explicitly (via abort() or commit() -- for reasons to
be explained, begin() later isn't enough).

"""
import ZODB
from ZODB.tests.MinPO import MinPO
from ZODB.FileStorage import FileStorage

st = FileStorage("temp.fs")
db = ZODB.DB(st )
cn = db.open()
rt = cn.root()

rt['obj'] = MinPO('obj')
get_transaction().commit()

obj = rt['obj']
obj.kid = MinPO('kid')
get_transaction().commit(1)
cn.close()

cn = db.open()
rt = cn.root()
# get_transaction().begin()
obj = rt['obj']

obj.kid._p_deactivate()

str(obj.kid)
"""

First it creates a persistent object, and saves it away.  Then it adds a
persistent subobject "kid" in a subtransaction, but closes the connection
without either aborting or committing the main transaction.  At this point
the obj->kid structure is still in cache, but not in the main database.

Next it reopens the connection.  Because Connection objects are reused from
a connection pool, we happen to get back the same Connection object, with
the same cache.  We also happen to be in the same thread, so this is about
the *simplest* variation possible.

The begin() on the next line is commented out.  Curiously, it makes no
difference if you uncomment it!  A begin() *normally* implies an abort(),
but in this case the only pending changes are in a subtransaction, and a
dubious optimization in begin() skips the abort() when no "top level"
changes are pending.  (BTW, getting rid of that optimization isn't a correct
way to fix this, as we *could* just as well have been in a different thread
by now, and if we were it wouldn't help for begin() to do an abort().)

Anyway, the cache is still intact from the transaction that never actually
ended, but the temporary storage holding the subtransaction data is gone.
So when kid is ghostified (via _p_deactivate), it leaves no way to restore
kid's state.  When the final line requires materializing kid's state again,
POSKeyError is the result.  In real life, it's unlikely this happens by an
explicit _p_deactivate() call in the app -- it's most likely to happen
later, after the cache goes through some cycles of ghostifying objects that
haven't been referenced recently.

This odd sequence of events is exacerbated by things Zope 2.7.2 does, as
detailed in the Collector report.

The fix we have in mind is to backstitch a simpler version of what ZODB 3.3
does, although ZODB 3.3 doesn't yet get this case entirely right either:  it
will be an error in the next ZODB 3.2 to close a connection when objects
from that connection still have pending modifications (doing either commit()
or abort() clears pending modifications).  The example above will raise an
exception on the cn.close() line then.  ZODB 3.3 already intended to do
this, but missed the case where all pending modifications are "hiding under"
subtransaction commits.



More information about the ZODB-Dev mailing list