Browse Source

[326] do table creation in an exclusive transaction (python)

Jelte Jansen 13 years ago
parent
commit
a9b769b8bf

+ 49 - 29
src/lib/python/isc/datasrc/sqlite3_ds.py

@@ -33,31 +33,48 @@ def create(cur):
     Arguments:
     Arguments:
         cur - sqlite3 cursor.
         cur - sqlite3 cursor.
     """
     """
-    cur.execute("CREATE TABLE schema_version (version INTEGER NOT NULL)")
-    cur.execute("INSERT INTO schema_version VALUES (1)")
-    cur.execute("""CREATE TABLE zones (id INTEGER PRIMARY KEY,
-                   name STRING NOT NULL COLLATE NOCASE,
-                   rdclass STRING NOT NULL COLLATE NOCASE DEFAULT 'IN',
-                   dnssec BOOLEAN NOT NULL DEFAULT 0)""")
-    cur.execute("CREATE INDEX zones_byname ON zones (name)")
-    cur.execute("""CREATE TABLE records (id INTEGER PRIMARY KEY,
-                   zone_id INTEGER NOT NULL,
-                   name STRING NOT NULL COLLATE NOCASE,
-                   rname STRING NOT NULL COLLATE NOCASE,
-                   ttl INTEGER NOT NULL,
-                   rdtype STRING NOT NULL COLLATE NOCASE,
-                   sigtype STRING COLLATE NOCASE,
-                   rdata STRING NOT NULL)""")
-    cur.execute("CREATE INDEX records_byname ON records (name)")
-    cur.execute("CREATE INDEX records_byrname ON records (rname)")
-    cur.execute("""CREATE TABLE nsec3 (id INTEGER PRIMARY KEY,
-                   zone_id INTEGER NOT NULL,
-                   hash STRING NOT NULL COLLATE NOCASE,
-                   owner STRING NOT NULL COLLATE NOCASE,
-                   ttl INTEGER NOT NULL,
-                   rdtype STRING NOT NULL COLLATE NOCASE,
-                   rdata STRING NOT NULL)""")
-    cur.execute("CREATE INDEX nsec3_byhash ON nsec3 (hash)")
+    # We are creating the database because it apparently had not been at
+    # the time we tried to read from it. However, another process may have
+    # had the same idea, resulting in a potential race condition.
+    # Therefore, we obtain an exclusive lock before we create anything
+    # When we have it, we check *again* whether the database has been
+    # initialized. If not, we do so.
+
+    # If the database is perpetually locked, it'll time out automatically
+    # and we just let it fail.
+    cur.execute("BEGIN EXCLUSIVE TRANSACTION")
+    try:
+        cur.execute("SELECT version FROM schema_version")
+        row = cur.fetchone()
+    except sqlite3.OperationalError:
+        cur.execute("CREATE TABLE schema_version (version INTEGER NOT NULL)")
+        cur.execute("INSERT INTO schema_version VALUES (1)")
+        cur.execute("""CREATE TABLE zones (id INTEGER PRIMARY KEY,
+                    name STRING NOT NULL COLLATE NOCASE,
+                    rdclass STRING NOT NULL COLLATE NOCASE DEFAULT 'IN',
+                    dnssec BOOLEAN NOT NULL DEFAULT 0)""")
+        cur.execute("CREATE INDEX zones_byname ON zones (name)")
+        cur.execute("""CREATE TABLE records (id INTEGER PRIMARY KEY,
+                    zone_id INTEGER NOT NULL,
+                    name STRING NOT NULL COLLATE NOCASE,
+                    rname STRING NOT NULL COLLATE NOCASE,
+                    ttl INTEGER NOT NULL,
+                    rdtype STRING NOT NULL COLLATE NOCASE,
+                    sigtype STRING COLLATE NOCASE,
+                    rdata STRING NOT NULL)""")
+        cur.execute("CREATE INDEX records_byname ON records (name)")
+        cur.execute("CREATE INDEX records_byrname ON records (rname)")
+        cur.execute("""CREATE TABLE nsec3 (id INTEGER PRIMARY KEY,
+                    zone_id INTEGER NOT NULL,
+                    hash STRING NOT NULL COLLATE NOCASE,
+                    owner STRING NOT NULL COLLATE NOCASE,
+                    ttl INTEGER NOT NULL,
+                    rdtype STRING NOT NULL COLLATE NOCASE,
+                    rdata STRING NOT NULL)""")
+        cur.execute("CREATE INDEX nsec3_byhash ON nsec3 (hash)")
+        row = [1]
+    cur.execute("COMMIT TRANSACTION")
+    return row
 
 
 def open(dbfile):
 def open(dbfile):
     """ Open a database, if the database is not yet set up, call create
     """ Open a database, if the database is not yet set up, call create
@@ -80,10 +97,13 @@ def open(dbfile):
     try:
     try:
         cur.execute("SELECT version FROM schema_version")
         cur.execute("SELECT version FROM schema_version")
         row = cur.fetchone()
         row = cur.fetchone()
-    except:
-        create(cur)
-        conn.commit()
-        row = [1]
+    except sqlite3.OperationalError:
+        # temporarily disable automatic transactions so
+        # we can do our own
+        iso_lvl = conn.isolation_level
+        conn.isolation_level = None
+        row = create(cur)
+        conn.isolation_level = iso_lvl
 
 
     if row == None or row[0] != 1:
     if row == None or row[0] != 1:
         raise Sqlite3DSError("Bad database schema version")
         raise Sqlite3DSError("Bad database schema version")

+ 38 - 1
src/lib/python/isc/datasrc/tests/sqlite3_ds_test.py

@@ -23,8 +23,9 @@ TESTDATA_PATH = os.environ['TESTDATA_PATH'] + os.sep
 TESTDATA_WRITE_PATH = os.environ['TESTDATA_WRITE_PATH'] + os.sep
 TESTDATA_WRITE_PATH = os.environ['TESTDATA_WRITE_PATH'] + os.sep
 
 
 READ_ZONE_DB_FILE = TESTDATA_PATH + "example.com.sqlite3"
 READ_ZONE_DB_FILE = TESTDATA_PATH + "example.com.sqlite3"
-WRITE_ZONE_DB_FILE = TESTDATA_WRITE_PATH + "example.com.out.sqlite3"
 BROKEN_DB_FILE = TESTDATA_PATH + "brokendb.sqlite3"
 BROKEN_DB_FILE = TESTDATA_PATH + "brokendb.sqlite3"
+WRITE_ZONE_DB_FILE = TESTDATA_WRITE_PATH + "example.com.out.sqlite3"
+NEW_DB_FILE = TESTDATA_WRITE_PATH + "new_db.sqlite3"
 
 
 def example_reader():
 def example_reader():
     my_zone = [
     my_zone = [
@@ -91,5 +92,41 @@ class TestSqlite3_ds(unittest.TestCase):
         # and make sure lock does not stay
         # and make sure lock does not stay
         sqlite3_ds.load(WRITE_ZONE_DB_FILE, ".", example_reader)
         sqlite3_ds.load(WRITE_ZONE_DB_FILE, ".", example_reader)
 
 
+class NewDBFile(unittest.TestCase):
+    def tearDown(self):
+        # remove the created database after every test
+        if (os.path.exists(NEW_DB_FILE)):
+            os.remove(NEW_DB_FILE)
+
+    def setUp(self):
+        # remove the created database before every test too, just
+        # in case a test got aborted half-way, and cleanup didn't occur
+        if (os.path.exists(NEW_DB_FILE)):
+            os.remove(NEW_DB_FILE)
+
+    def test_new_db(self):
+        self.assertFalse(os.path.exists(NEW_DB_FILE))
+        sqlite3_ds.load(NEW_DB_FILE, ".", example_reader)
+        self.assertTrue(os.path.exists(NEW_DB_FILE))
+
+    def test_new_db_locked(self):
+        self.assertFalse(os.path.exists(NEW_DB_FILE))
+        con = sqlite3.connect(NEW_DB_FILE);
+        cur = con.cursor()
+        con.isolation_level = None
+        cur.execute("BEGIN EXCLUSIVE TRANSACTION")
+
+        # load should now fail, since the database is locked
+        self.assertRaises(sqlite3.OperationalError,
+                          sqlite3_ds.load, NEW_DB_FILE, ".", example_reader)
+
+        con.rollback()
+        cur.close()
+        con.close()
+        self.assertTrue(os.path.exists(NEW_DB_FILE))
+
+        # now that we closed our connection, load should work again
+        sqlite3_ds.load(NEW_DB_FILE, ".", example_reader)
+
 if __name__ == '__main__':
 if __name__ == '__main__':
     unittest.main()
     unittest.main()