Browse Source

[326] cpp version for exclusive locking at creation time

Jelte Jansen 13 years ago
parent
commit
4579a2a9f4
1 changed files with 83 additions and 31 deletions
  1. 83 31
      src/lib/datasrc/sqlite3_datasrc.cc

+ 83 - 31
src/lib/datasrc/sqlite3_datasrc.cc

@@ -26,6 +26,8 @@
 #include <dns/rrset.h>
 #include <dns/rrsetlist.h>
 
+#define SQLITE_SCHEMA_VERSION 1
+
 using namespace std;
 using namespace isc::dns;
 using namespace isc::dns::rdata;
@@ -77,6 +79,8 @@ const char* const SCHEMA_LIST[] = {
     NULL
 };
 
+const char* const q_version_str = "SELECT version FROM schema_version";
+
 const char* const q_zone_str = "SELECT id FROM zones WHERE name=?1";
 
 const char* const q_record_str = "SELECT rdtype, ttl, sigtype, rdata "
@@ -254,7 +258,7 @@ Sqlite3DataSrc::findRecords(const Name& name, const RRType& rdtype,
         }
         break;
     }
-    
+
     sqlite3_reset(query);
     sqlite3_clear_bindings(query);
 
@@ -295,7 +299,7 @@ Sqlite3DataSrc::findRecords(const Name& name, const RRType& rdtype,
     //
     sqlite3_reset(dbparameters->q_count_);
     sqlite3_clear_bindings(dbparameters->q_count_);
-    
+
     rc = sqlite3_bind_int(dbparameters->q_count_, 1, zone_id);
     if (rc != SQLITE_OK) {
         isc_throw(Sqlite3Error, "Could not bind zone ID " << zone_id <<
@@ -653,42 +657,90 @@ prepare(sqlite3* const db, const char* const statement) {
     return (prepared);
 }
 
-void
-checkAndSetupSchema(Sqlite3Initializer* initializer) {
-    sqlite3* const db = initializer->params_.db_;
+// small function to sleep for 0.1 seconds, needed when waiting for
+// exclusive database locks (which should only occur on startup, and only
+// when the database has not been created yet)
+void do_sleep() {
+    struct timespec req;
+    req.tv_sec = 0;
+    req.tv_nsec = 100000000;
+    nanosleep(&req, NULL);
+}
 
+// returns the schema version if the schema version table exists
+// returns -1 if it does not
+int check_schema_version(sqlite3* db) {
     sqlite3_stmt* prepared = NULL;
-    if (sqlite3_prepare_v2(db, "SELECT version FROM schema_version", -1,
-                           &prepared, NULL) == SQLITE_OK &&
-        sqlite3_step(prepared) == SQLITE_ROW) {
-        initializer->params_.version_ = sqlite3_column_int(prepared, 0);
-        sqlite3_finalize(prepared);
-    } else {
-        logger.info(DATASRC_SQLITE_SETUP);
-        if (prepared != NULL) {
-            sqlite3_finalize(prepared);
+    // At this point in time, the database might be exclusively locked, in
+    // which case even prepare() will return BUSY, so we may need to try a
+    // few times
+    for (size_t i = 0; i < 50; ++i) {
+        int rc = sqlite3_prepare_v2(db, q_version_str, -1, &prepared, NULL);
+        if (rc == SQLITE_ERROR) {
+            // this is the error that is returned when the table does not
+            // exist
+            return -1;
+        } else if (rc == SQLITE_OK) {
+            break;
+        } else if (rc != SQLITE_BUSY || i == 50) {
+            isc_throw(Sqlite3Error, "Unable to prepare version query: "
+                        << rc << " " << sqlite3_errmsg(db));
         }
-        // We need to create the database. However, there is a potential
-        // race condition with another process that wants to do the same
-        // So we acquire an exclusive lock, and then check whether the
-        // tables have been created again. If not, we create them.
-        sqlite3_exec(db, "BEGIN EXCLUSIVE TRANSACTION", NULL, NULL, NULL);
-        if (sqlite3_prepare_v2(db, "SELECT version FROM schema_version", -1,
-                            &prepared, NULL) == SQLITE_OK &&
-            sqlite3_step(prepared) == SQLITE_ROW) {
-            initializer->params_.version_ = sqlite3_column_int(prepared, 0);
-            sqlite3_finalize(prepared);
-        } else {
-            for (int i = 0; SCHEMA_LIST[i] != NULL; ++i) {
-                if (sqlite3_exec(db, SCHEMA_LIST[i], NULL, NULL, NULL) !=
-                    SQLITE_OK) {
-                    isc_throw(Sqlite3Error,
-                            "Failed to set up schema " << SCHEMA_LIST[i]);
-                }
+        do_sleep();
+    }
+    if (sqlite3_step(prepared) != SQLITE_ROW) {
+        isc_throw(Sqlite3Error,
+                    "Unable to query version: " << sqlite3_errmsg(db));
+    }
+    int version = sqlite3_column_int(prepared, 0);
+    sqlite3_finalize(prepared);
+    return version;
+}
+
+// return db version
+int create_database(sqlite3* db) {
+    // try to get an exclusive lock. Once that is obtained, do the version
+    // check *again*, just in case this process was racing another
+    //
+    // try for 5 secs (50*0.1)
+    int rc;
+    logger.info(DATASRC_SQLITE_SETUP);
+    for (size_t i = 0; i < 50; ++i) {
+        rc = sqlite3_exec(db, "BEGIN EXCLUSIVE TRANSACTION", NULL, NULL,
+                            NULL);
+        if (rc == SQLITE_OK) {
+            break;
+        } else if (rc != SQLITE_BUSY || i == 50) {
+            isc_throw(Sqlite3Error, "Unable to acquire exclusive lock "
+                        "for database creation: " << sqlite3_errmsg(db));
+        }
+        do_sleep();
+    }
+    int schema_version = check_schema_version(db);
+    if (schema_version == -1) {
+        for (int i = 0; SCHEMA_LIST[i] != NULL; ++i) {
+            if (sqlite3_exec(db, SCHEMA_LIST[i], NULL, NULL, NULL) !=
+                SQLITE_OK) {
+                isc_throw(Sqlite3Error,
+                        "Failed to set up schema " << SCHEMA_LIST[i]);
             }
         }
         sqlite3_exec(db, "COMMIT TRANSACTION", NULL, NULL, NULL);
+        return SQLITE_SCHEMA_VERSION;
+    } else {
+        return schema_version;
+    }
+}
+
+void
+checkAndSetupSchema(Sqlite3Initializer* initializer) {
+    sqlite3* const db = initializer->params_.db_;
+
+    int schema_version = check_schema_version(db);
+    if (schema_version != SQLITE_SCHEMA_VERSION) {
+        schema_version = create_database(db);
     }
+    initializer->params_.version_ = schema_version;
 
     initializer->params_.q_zone_ = prepare(db, q_zone_str);
     initializer->params_.q_record_ = prepare(db, q_record_str);