]> git.ozlabs.org Git - ccan/blobdiff - ccan/tdb/lock.c
tdb: test and resolultion for tdb_lockall starvation.
[ccan] / ccan / tdb / lock.c
index fd451e58cd314b54167b476e8a74890aa8e23581..977091597f7d5d1247e815292e15605be6bab4bc 100644 (file)
@@ -309,10 +309,44 @@ int tdb_nest_lock(struct tdb_context *tdb, uint32_t offset, int ltype,
        return 0;
 }
 
+static int tdb_lock_and_recover(struct tdb_context *tdb)
+{
+       int ret;
+
+       /* We need to match locking order in transaction commit. */
+       if (tdb_brlock(tdb, F_WRLCK, FREELIST_TOP, 0, TDB_LOCK_WAIT)) {
+               return -1;
+       }
+
+       if (tdb_brlock(tdb, F_WRLCK, OPEN_LOCK, 1, TDB_LOCK_WAIT)) {
+               tdb_brunlock(tdb, F_WRLCK, FREELIST_TOP, 0);
+               return -1;
+       }
+
+       ret = tdb_transaction_recover(tdb);
+
+       tdb_brunlock(tdb, F_WRLCK, OPEN_LOCK, 1);
+       tdb_brunlock(tdb, F_WRLCK, FREELIST_TOP, 0);
+
+       return ret;
+}
+
+static bool have_data_locks(const struct tdb_context *tdb)
+{
+       unsigned int i;
+
+       for (i = 0; i < tdb->num_lockrecs; i++) {
+               if (tdb->lockrecs[i].off >= lock_offset(-1))
+                       return true;
+       }
+       return false;
+}
+
 static int tdb_lock_list(struct tdb_context *tdb, int list, int ltype,
                         enum tdb_lock_flags waitflag)
 {
        int ret;
+       bool check = false;
 
        /* a allrecord lock allows us to avoid per chain locks */
        if (tdb->allrecord_lock.count &&
@@ -324,7 +358,18 @@ static int tdb_lock_list(struct tdb_context *tdb, int list, int ltype,
                tdb->ecode = TDB_ERR_LOCK;
                ret = -1;
        } else {
+               /* Only check when we grab first data lock. */
+               check = !have_data_locks(tdb);
                ret = tdb_nest_lock(tdb, lock_offset(list), ltype, waitflag);
+
+               if (ret == 0 && check && tdb_needs_recovery(tdb)) {
+                       tdb_nest_unlock(tdb, lock_offset(list), ltype, false);
+
+                       if (tdb_lock_and_recover(tdb) == -1) {
+                               return -1;
+                       }
+                       return tdb_lock_list(tdb, list, ltype, waitflag);
+               }
        }
        return ret;
 }
@@ -440,11 +485,9 @@ int tdb_transaction_unlock(struct tdb_context *tdb, int ltype)
        return tdb_nest_unlock(tdb, TRANSACTION_LOCK, ltype, false);
 }
 
-
-/* lock/unlock entire database.  It can only be upgradable if you have some
- * other way of guaranteeing exclusivity (ie. transaction write lock). */
-int tdb_allrecord_lock(struct tdb_context *tdb, int ltype,
-                      enum tdb_lock_flags flags, bool upgradable)
+/* Returns 0 if all done, -1 if error, 1 if ok. */
+static int tdb_allrecord_check(struct tdb_context *tdb, int ltype,
+                              enum tdb_lock_flags flags, bool upgradable)
 {
        /* There are no locks on read-only dbs */
        if (tdb->read_only || tdb->traverse_read) {
@@ -474,6 +517,20 @@ int tdb_allrecord_lock(struct tdb_context *tdb, int ltype,
                tdb->ecode = TDB_ERR_LOCK;
                return -1;
        }
+       return 1;
+}
+
+/* lock/unlock entire database.  It can only be upgradable if you have some
+ * other way of guaranteeing exclusivity (ie. transaction write lock). */
+int tdb_allrecord_lock(struct tdb_context *tdb, int ltype,
+                      enum tdb_lock_flags flags, bool upgradable)
+{
+       switch (tdb_allrecord_check(tdb, ltype, flags, upgradable)) {
+       case -1:
+               return -1;
+       case 0:
+               return 0;
+       }
 
        if (tdb_brlock(tdb, ltype, FREELIST_TOP, 0, flags)) {
                if (flags & TDB_LOCK_WAIT) {
@@ -488,6 +545,21 @@ int tdb_allrecord_lock(struct tdb_context *tdb, int ltype,
        tdb->allrecord_lock.ltype = upgradable ? F_WRLCK : ltype;
        tdb->allrecord_lock.off = upgradable;
 
+       if (tdb_needs_recovery(tdb)) {
+               bool mark = flags & TDB_LOCK_MARK_ONLY;
+               tdb_allrecord_unlock(tdb, ltype, mark);
+               if (mark) {
+                       tdb->ecode = TDB_ERR_LOCK;
+                       TDB_LOG((tdb, TDB_DEBUG_ERROR,
+                                "tdb_lockall_mark cannot do recovery\n"));
+                       return -1;
+               }
+               if (tdb_lock_and_recover(tdb) == -1) {
+                       return -1;
+               }
+               return tdb_allrecord_lock(tdb, ltype, flags, upgradable);
+       }
+
        return 0;
 }
 
@@ -588,6 +660,85 @@ int tdb_unlockall_read(struct tdb_context *tdb)
        return tdb_allrecord_unlock(tdb, F_RDLCK, false);
 }
 
+/* We only need to lock individual bytes, but Linux merges consecutive locks
+ * so we lock in contiguous ranges. */
+static int tdb_chainlock_gradual(struct tdb_context *tdb,
+                                size_t off, size_t len)
+{
+       int ret;
+
+       if (len <= 4) {
+               /* Single record.  Just do blocking lock. */
+               return tdb_brlock(tdb, F_WRLCK, off, len, TDB_LOCK_WAIT);
+       }
+
+       /* First we try non-blocking. */
+       ret = tdb_brlock(tdb, F_WRLCK, off, len, TDB_LOCK_NOWAIT);
+       if (ret == 0) {
+               return 0;
+       }
+
+       /* Try locking first half, then second. */
+       ret = tdb_chainlock_gradual(tdb, off, len / 2);
+       if (ret == -1)
+               return -1;
+
+       ret = tdb_chainlock_gradual(tdb, off + len / 2, len - len / 2);
+       if (ret == -1) {
+               tdb_brunlock(tdb, F_WRLCK, off, len / 2);
+               return -1;
+       }
+       return 0;
+}
+
+/* We do the locking gradually to avoid being starved by smaller locks. */
+int tdb_lockall_gradual(struct tdb_context *tdb)
+{
+       int ret;
+
+       /* This checks for other locks, nesting. */
+       ret = tdb_allrecord_check(tdb, F_WRLCK, TDB_LOCK_WAIT, false);
+       if (ret == -1 || ret == 0)
+               return ret;
+
+       /* We cover two kinds of locks:
+        * 1) Normal chain locks.  Taken for almost all operations.
+        * 3) Individual records locks.  Taken after normal or free
+        *    chain locks.
+        *
+        * It is (1) which cause the starvation problem, so we're only
+        * gradual for that. */
+       if (tdb_chainlock_gradual(tdb, FREELIST_TOP,
+                                 tdb->header.hash_size * 4) == -1) {
+               return -1;
+       }
+
+       /* Grab individual record locks. */
+       if (tdb_brlock(tdb, F_WRLCK, lock_offset(tdb->header.hash_size), 0,
+                      TDB_LOCK_WAIT) == -1) {
+               tdb_brunlock(tdb, F_WRLCK, FREELIST_TOP,
+                            tdb->header.hash_size * 4);
+               return -1;
+       }
+
+       /* That adds up to an allrecord lock. */
+       tdb->allrecord_lock.count = 1;
+       tdb->allrecord_lock.ltype = F_WRLCK;
+       tdb->allrecord_lock.off = false;
+
+       /* Just check we don't need recovery... */
+       if (tdb_needs_recovery(tdb)) {
+               tdb_allrecord_unlock(tdb, F_WRLCK, false);
+               if (tdb_lock_and_recover(tdb) == -1) {
+                       return -1;
+               }
+               /* Try again. */
+               return tdb_lockall_gradual(tdb);
+       }
+
+       return 0;
+}
+
 /* lock/unlock one hash chain. This is meant to be used to reduce
    contention - it cannot guarantee how many records will be locked */
 int tdb_chainlock(struct tdb_context *tdb, TDB_DATA key)