Fix transactions once again

This commit is contained in:
Markus Unterwaditzer 2017-03-28 21:14:33 +02:00
parent 07f6c3af12
commit fbe3f9910d

View file

@ -25,6 +25,18 @@ from .utils import uniq
sync_logger = logging.getLogger(__name__) sync_logger = logging.getLogger(__name__)
@contextlib.contextmanager
def _exclusive_transaction(conn):
try:
c = conn.execute('BEGIN EXCLUSIVE TRANSACTION')
yield c
c.execute('COMMIT')
except BaseException:
_, e, tb = sys.exc_info()
c.execute('ROLLBACK')
raise e.with_traceback(tb)
class SyncError(exceptions.Error): class SyncError(exceptions.Error):
'''Errors related to synchronization.''' '''Errors related to synchronization.'''
@ -208,16 +220,14 @@ class SqliteStatus(_StatusBase):
# If we ever bump the schema version, we will need a way to migrate # If we ever bump the schema version, we will need a way to migrate
# data. # data.
with _exclusive_transaction(self._c) as c:
self._c.execute('''CREATE TABLE meta ( c.execute('CREATE TABLE meta ( "version" INTEGER PRIMARY KEY )')
"version" INTEGER PRIMARY KEY c.execute('INSERT INTO meta (version) VALUES (?)',
); ''')
self._c.execute('INSERT INTO meta (version) VALUES (?)',
(self.SCHEMA_VERSION,)) (self.SCHEMA_VERSION,))
# I know that this is a bad schema, but right there is just too little # I know that this is a bad schema, but right there is just too
# gain in deduplicating the .._a and .._b columns. # little gain in deduplicating the .._a and .._b columns.
self._c.execute('''CREATE TABLE status ( c.execute('''CREATE TABLE status (
"ident" TEXT PRIMARY KEY NOT NULL, "ident" TEXT PRIMARY KEY NOT NULL,
"href_a" TEXT, "href_a" TEXT,
"href_b" TEXT, "href_b" TEXT,
@ -226,22 +236,23 @@ class SqliteStatus(_StatusBase):
"etag_a" TEXT, "etag_a" TEXT,
"etag_b" TEXT "etag_b" TEXT
); ''') ); ''')
self._c.execute('CREATE UNIQUE INDEX by_href_a ON status(href_a)') c.execute('CREATE UNIQUE INDEX by_href_a ON status(href_a)')
self._c.execute('CREATE UNIQUE INDEX by_href_b ON status(href_b)') c.execute('CREATE UNIQUE INDEX by_href_b ON status(href_b)')
# We cannot add NOT NULL here because data is first fetched for the # We cannot add NOT NULL here because data is first fetched for the
# storage a, then storage b. Inbetween the `_b`-columns are filled with # storage a, then storage b. Inbetween the `_b`-columns are filled
# NULL. # with NULL.
# #
# In an ideal world we would be able to start a transaction with one # In an ideal world we would be able to start a transaction with
# cursor, write our new data into status and simultaneously query the # one cursor, write our new data into status and simultaneously
# old status data using a different cursor. Unfortunately sqlite # query the old status data using a different cursor.
# enforces NOT NULL constraints immediately, not just at commit. Since # Unfortunately sqlite enforces NOT NULL constraints immediately,
# there is also no way to alter constraints on a table (disable # not just at commit. Since there is also no way to alter
# constraints on start of transaction and reenable on end), it's a # constraints on a table (disable constraints on start of
# separate table now that just gets copied over before we commit. # transaction and reenable on end), it's a separate table now that
# That's a lot of copying, sadly. # just gets copied over before we commit. That's a lot of copying,
self._c.execute('''CREATE TABLE new_status ( # sadly.
c.execute('''CREATE TABLE new_status (
"ident" TEXT PRIMARY KEY NOT NULL, "ident" TEXT PRIMARY KEY NOT NULL,
"href_a" TEXT, "href_a" TEXT,
"href_b" TEXT, "href_b" TEXT,
@ -262,19 +273,15 @@ class SqliteStatus(_StatusBase):
@contextlib.contextmanager @contextlib.contextmanager
def transaction(self): def transaction(self):
try:
old_c = self._c old_c = self._c
self._c = self._c.execute('BEGIN EXCLUSIVE TRANSACTION') try:
with _exclusive_transaction(self._c) as new_c:
self._c = new_c
yield yield
self._c.execute('DELETE FROM status') self._c.execute('DELETE FROM status')
self._c.execute('INSERT INTO status ' self._c.execute('INSERT INTO status '
'SELECT * FROM new_status') 'SELECT * FROM new_status')
self._c.execute('DELETE FROM new_status') self._c.execute('DELETE FROM new_status')
self._c.execute('COMMIT')
except BaseException:
_, e, tb = sys.exc_info()
self._c.execute('ROLLBACK')
raise e.with_traceback(tb)
finally: finally:
self._c = old_c self._c = old_c