Commit dc113a8a authored by Gregory P. Smith's avatar Gregory P. Smith

* Fix the singlethreaded deadlocks occurring in the simple bsddb interface.

* Add support for multiple iterator/generator objects at once on the simple
  bsddb _DBWithCursor interface.
parent e2767171
...@@ -70,20 +70,74 @@ import UserDict ...@@ -70,20 +70,74 @@ import UserDict
class _iter_mixin(UserDict.DictMixin): class _iter_mixin(UserDict.DictMixin):
def __iter__(self): def __iter__(self):
try: try:
yield self.first()[0] cur = self.db.cursor()
next = self.next self._iter_cursors[str(cur)] = cur
# since we're only returning keys, we call the cursor
# methods with flags=0, dlen=0, dofs=0
curkey = cur.first(0,0,0)[0]
yield curkey
next = cur.next
while 1: while 1:
yield next()[0] try:
curkey = next(0,0,0)[0]
yield curkey
except _bsddb.DBCursorClosedError:
# our cursor object was closed since we last yielded
# create a new one and attempt to reposition to the
# right place
cur = self.db.cursor()
self._iter_cursors[str(cur)] = cur
# FIXME-20031101-greg: race condition. cursor could
# be closed by another thread before this set call.
try:
cur.set(curkey,0,0,0)
except _bsddb.DBCursorClosedError:
# halt iteration on race condition...
raise _bsddb.DBNotFoundError
next = cur.next
except _bsddb.DBNotFoundError: except _bsddb.DBNotFoundError:
try:
del self._iter_cursors[str(cur)]
except KeyError:
pass
return return
def iteritems(self): def iteritems(self):
try: try:
yield self.first() cur = self.db.cursor()
next = self.next self._iter_cursors[str(cur)] = cur
kv = cur.first()
curkey = kv[0]
yield kv
next = cur.next
while 1: while 1:
yield next() try:
kv = next()
curkey = kv[0]
yield kv
except _bsddb.DBCursorClosedError:
# our cursor object was closed since we last yielded
# create a new one and attempt to reposition to the
# right place
cur = self.db.cursor()
self._iter_cursors[str(cur)] = cur
# FIXME-20031101-greg: race condition. cursor could
# be closed by another thread before this set call.
try:
cur.set(curkey,0,0,0)
except _bsddb.DBCursorClosedError:
# halt iteration on race condition...
raise _bsddb.DBNotFoundError
next = cur.next
except _bsddb.DBNotFoundError: except _bsddb.DBNotFoundError:
try:
del self._iter_cursors[str(cur)]
except KeyError:
pass
return return
""" """
else: else:
...@@ -97,15 +151,53 @@ class _DBWithCursor(_iter_mixin): ...@@ -97,15 +151,53 @@ class _DBWithCursor(_iter_mixin):
""" """
def __init__(self, db): def __init__(self, db):
self.db = db self.db = db
self.dbc = None
self.db.set_get_returns_none(0) self.db.set_get_returns_none(0)
# FIXME-20031101-greg: I believe there is still the potential
# for deadlocks in a multithreaded environment if someone
# attempts to use the any of the cursor interfaces in one
# thread while doing a put or delete in another thread. The
# reason is that _checkCursor and _closeCursors are not atomic
# operations. Doing our own locking around self.dbc,
# self.saved_dbc_key and self._iter_cursors could prevent this.
# TODO: A test case demonstrating the problem needs to be written.
# self.dbc is a DBCursor object used to implement the
# first/next/previous/last/set_location methods.
self.dbc = None
self.saved_dbc_key = None
# a collection of all DBCursor objects currently allocated
# by the _iter_mixin interface.
self._iter_cursors = {}
def __del__(self): def __del__(self):
self.close() self.close()
def _get_dbc(self):
return self.dbc
def _checkCursor(self): def _checkCursor(self):
if self.dbc is None: if self.dbc is None:
self.dbc = self.db.cursor() self.dbc = self.db.cursor()
if self.saved_dbc_key is not None:
self.dbc.set(self.saved_dbc_key)
self.saved_dbc_key = None
# This method is needed for all non-cursor DB calls to avoid
# BerkeleyDB deadlocks (due to being opened with DB_INIT_LOCK
# and DB_THREAD to be thread safe) when intermixing database
# operations that use the cursor internally with those that don't.
def _closeCursors(self, save=True):
if self.dbc:
c = self.dbc
self.dbc = None
if save:
self.saved_dbc_key = c.current(0,0,0)[0]
c.close()
del c
map(lambda c: c.close(), self._iter_cursors.values())
def _checkOpen(self): def _checkOpen(self):
if self.db is None: if self.db is None:
...@@ -124,13 +216,16 @@ class _DBWithCursor(_iter_mixin): ...@@ -124,13 +216,16 @@ class _DBWithCursor(_iter_mixin):
def __setitem__(self, key, value): def __setitem__(self, key, value):
self._checkOpen() self._checkOpen()
self._closeCursors()
self.db[key] = value self.db[key] = value
def __delitem__(self, key): def __delitem__(self, key):
self._checkOpen() self._checkOpen()
self._closeCursors()
del self.db[key] del self.db[key]
def close(self): def close(self):
self._closeCursors(save=False)
if self.dbc is not None: if self.dbc is not None:
self.dbc.close() self.dbc.close()
v = 0 v = 0
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
"""Test script for the bsddb C module by Roger E. Masse """Test script for the bsddb C module by Roger E. Masse
Adapted to unittest format and expanded scope by Raymond Hettinger Adapted to unittest format and expanded scope by Raymond Hettinger
""" """
import os import os, sys
import bsddb import bsddb
import dbhash # Just so we know it's imported import dbhash # Just so we know it's imported
import unittest import unittest
...@@ -93,6 +93,57 @@ class TestBSDDB(unittest.TestCase): ...@@ -93,6 +93,57 @@ class TestBSDDB(unittest.TestCase):
self.f.clear() self.f.clear()
self.assertEqual(len(self.f), 0) self.assertEqual(len(self.f), 0)
def test__no_deadlock_first(self, debug=0):
# do this so that testers can see what function we're in in
# verbose mode when we deadlock.
sys.stdout.flush()
# in pybsddb's _DBWithCursor this causes an internal DBCursor
# object is created. Other test_ methods in this class could
# inadvertently cause the deadlock but an explicit test is needed.
if debug: print "A"
k,v = self.f.first()
if debug: print "B", k
self.f[k] = "deadlock. do not pass go. do not collect $200."
if debug: print "C"
# if the bsddb implementation leaves the DBCursor open during
# the database write and locking+threading support is enabled
# the cursor's read lock will deadlock the write lock request..
# test the iterator interface (if present)
if hasattr(self, 'iteritems'):
if debug: print "D"
k,v = self.f.iteritems()
if debug: print "E"
self.f[k] = "please don't deadlock"
if debug: print "F"
while 1:
try:
k,v = self.f.iteritems()
except StopIteration:
break
if debug: print "F2"
i = iter(self.f)
if debug: print "G"
while i:
try:
if debug: print "H"
k = i.next()
if debug: print "I"
self.f[k] = "deadlocks-r-us"
if debug: print "J"
except StopIteration:
i = None
if debug: print "K"
# test the legacy cursor interface mixed with writes
self.assert_(self.f.first()[0] in self.d)
k = self.f.next()[0]
self.assert_(k in self.d)
self.f[k] = "be gone with ye deadlocks"
self.assert_(self.f[k], "be gone with ye deadlocks")
def test_popitem(self): def test_popitem(self):
k, v = self.f.popitem() k, v = self.f.popitem()
self.assert_(k in self.d) self.assert_(k in self.d)
......
...@@ -93,7 +93,7 @@ ...@@ -93,7 +93,7 @@
/* 40 = 4.0, 33 = 3.3; this will break if the second number is > 9 */ /* 40 = 4.0, 33 = 3.3; this will break if the second number is > 9 */
#define DBVER (DB_VERSION_MAJOR * 10 + DB_VERSION_MINOR) #define DBVER (DB_VERSION_MAJOR * 10 + DB_VERSION_MINOR)
#define PY_BSDDB_VERSION "4.2.2" #define PY_BSDDB_VERSION "4.2.3"
static char *rcs_id = "$Id$"; static char *rcs_id = "$Id$";
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment