tdb: workaround starvation problem in locking entire database.
authorRusty Russell <rusty@rustcorp.com.au>
Mon, 16 Aug 2010 00:52:21 +0000 (10:22 +0930)
committerRusty Russell <rusty@rustcorp.com.au>
Mon, 16 Aug 2010 00:52:21 +0000 (10:22 +0930)
(Imported from SAMBA 11ab43084b10cf53b530cdc3a6036c898b79ca38)

We saw tdb_lockall() take 71 seconds under heavy load; this is because Linux
(at least) doesn't prevent new small locks being obtained while we're waiting
for a big log.

The workaround is to do divide and conquer using non-blocking chainlocks: if
we get down to a single chain we block.  Using a simple test program where
children did "hold lock for 100ms, sleep for 1 second" the time to do
tdb_lockall() dropped signifiantly.  There are ln(hashsize) locks taken in
the contended case, but that's slow anyway.

More analysis is given in my blog at http://rusty.ozlabs.org/?p=120

This may also help transactions, though in that case it's the initial
read lock which uses this gradual locking routine; the update-to-write-lock
code is separate and still tries to update in one go.

Even though ABI doesn't change, minor version bumped so behavior change
can be easily detected.

CQ:S1018154
Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
lib/tdb/common/lock.c
lib/tdb/configure.ac

index 285b7a34c3a12cb985b7799fbd9a2da8277ba1d7..803feeecbb0587d575206a7b72791cd40d413767 100644 (file)
@@ -152,14 +152,6 @@ int tdb_brlock(struct tdb_context *tdb,
                return -1;
        }
 
-       /* Sanity check */
-       if (tdb->transaction && offset >= lock_offset(-1) && len != 0) {
-               tdb->ecode = TDB_ERR_RDONLY;
-               TDB_LOG((tdb, TDB_DEBUG_TRACE, "tdb_brlock attempted in transaction at offset %d rw_type=%d flags=%d len=%d\n",
-                        offset, rw_type, flags, (int)len));
-               return -1;
-       }
-
        do {
                ret = fcntl_lock(tdb, rw_type, offset, len,
                                 flags & TDB_LOCK_WAIT);
@@ -486,11 +478,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) {
@@ -520,11 +510,73 @@ int tdb_allrecord_lock(struct tdb_context *tdb, int ltype,
                tdb->ecode = TDB_ERR_LOCK;
                return -1;
        }
+       return 1;
+}
 
-       if (tdb_brlock(tdb, ltype, FREELIST_TOP, 0, flags)) {
-               if (flags & TDB_LOCK_WAIT) {
-                       TDB_LOG((tdb, TDB_DEBUG_ERROR, "tdb_lockall failed (%s)\n", strerror(errno)));
-               }
+/* 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,
+                                int ltype, enum tdb_lock_flags flags,
+                                size_t off, size_t len)
+{
+       int ret;
+       enum tdb_lock_flags nb_flags = (flags & ~TDB_LOCK_WAIT);
+
+       if (len <= 4) {
+               /* Single record.  Just do blocking lock. */
+               return tdb_brlock(tdb, ltype, off, len, flags);
+       }
+
+       /* First we try non-blocking. */
+       ret = tdb_brlock(tdb, ltype, off, len, nb_flags);
+       if (ret == 0) {
+               return 0;
+       }
+
+       /* Try locking first half, then second. */
+       ret = tdb_chainlock_gradual(tdb, ltype, flags, off, len / 2);
+       if (ret == -1)
+               return -1;
+
+       ret = tdb_chainlock_gradual(tdb, ltype, flags,
+                                   off + len / 2, len - len / 2);
+       if (ret == -1) {
+               tdb_brunlock(tdb, ltype, off, len / 2);
+               return -1;
+       }
+       return 0;
+}
+
+/* lock/unlock entire database.  It can only be upgradable if you have some
+ * other way of guaranteeing exclusivity (ie. transaction write lock).
+ * We do the locking gradually to avoid being starved by smaller locks. */
+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;
+       }
+
+       /* 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, ltype, flags, FREELIST_TOP,
+                                 tdb->header.hash_size * 4) == -1) {
+               return -1;
+       }
+
+       /* Grab individual record locks. */
+       if (tdb_brlock(tdb, ltype, lock_offset(tdb->header.hash_size), 0,
+                      flags) == -1) {
+               tdb_brunlock(tdb, ltype, FREELIST_TOP,
+                            tdb->header.hash_size * 4);
                return -1;
        }
 
index 686b0a6763379ed35969d81c04dd2e546ae7a8e3..9b87227f3562a8215a84f83181f3a61c2e7a3a41 100644 (file)
@@ -2,7 +2,7 @@ AC_PREREQ(2.50)
 AC_DEFUN([SMB_MODULE_DEFAULT], [echo -n ""])
 AC_DEFUN([SMB_LIBRARY_ENABLE], [echo -n ""])
 AC_DEFUN([SMB_ENABLE], [echo -n ""])
-AC_INIT(tdb, 1.2.2)
+AC_INIT(tdb, 1.2.3)
 AC_CONFIG_SRCDIR([common/tdb.c])
 AC_CONFIG_HEADER(include/config.h)
 AC_LIBREPLACE_ALL_CHECKS