From b487001c9bd4537dcd2e12c95f3105886676b1b7 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Fri, 8 Jun 2012 11:34:54 -0700 Subject: [PATCH] exclusive database access (GC-399) This is the crux of the matter. This new lock synchronizes accesses to the database itself, allowing only one thread to have transactions active at a time. In effect, this makes sure that beets only has one SQLite transaction active at a time (and is very conservative in this effort; no read sharing is allowed either). This prevents all contention in SQLite and hides the pathological HAVE_SLEEP=0 behavior. --- beets/library.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/beets/library.py b/beets/library.py index 755f74bc7..47456fe9c 100644 --- a/beets/library.py +++ b/beets/library.py @@ -912,7 +912,12 @@ class Transaction(object): another is active in a different thread. """ with self.lib._tx_stack() as stack: + first = not stack stack.append(self) + if first: + # Beginning a "root" transaction, which corresponds to an + # SQLite transaction. + self.lib._db_lock.acquire() return self def __exit__(self, exc_type, exc_value, traceback): @@ -924,7 +929,9 @@ class Transaction(object): assert stack.pop() is self empty = not stack if empty: + # Ending a "root" transaction. End the SQLite transaction. self.lib._connection().commit() + self.lib._db_lock.release() def query(self, statement, subvals=()): """Execute an SQL statement with substitution values and return @@ -972,6 +979,15 @@ class Library(BaseLibrary): # A lock to protect the _connections and _tx_stacks maps, which # both map thread IDs to private resources. self._shared_map_lock = threading.Lock() + # A lock to protect access to the database itself. SQLite does + # allow multiple threads to access the database at the same + # time, but many users were experiencing crashes related to this + # capability: where SQLite was compiled without HAVE_USLEEP, its + # backoff algorithm in the case of contention was causing + # whole-second sleeps (!) that would trigger its internal + # timeout. Using this lock ensures only one SQLite transaction + # is active at a time. + self._db_lock = threading.Lock() # Set up database schema. self._make_table('items', item_fields)