Merge branch 'mh/lockfile-retry'
authorJunio C Hamano <gitster@pobox.com>
Fri, 22 May 2015 19:41:55 +0000 (12:41 -0700)
committerJunio C Hamano <gitster@pobox.com>
Fri, 22 May 2015 19:41:55 +0000 (12:41 -0700)
Instead of dying immediately upon failing to obtain a lock, retry
after a short while with backoff.

* mh/lockfile-retry:
lock_packed_refs(): allow retries when acquiring the packed-refs lock
lockfile: allow file locking to be retried with a timeout

Documentation/config.txt
lockfile.c
lockfile.h
refs.c
t/t3210-pack-refs.sh
index 54cbf6b7ea8c6e21a4ffc5c767fc15c630ccd33c..0f668bbe11a5b439fca53ca7aa52a43782e30b79 100644 (file)
@@ -624,6 +624,12 @@ core.commentChar::
 If set to "auto", `git-commit` would select a character that is not
 the beginning character of any line in existing commit messages.
 
+core.packedRefsTimeout::
+       The length of time, in milliseconds, to retry when trying to
+       lock the `packed-refs` file. Value 0 means not to retry at
+       all; -1 means to try indefinitely. Default is 1000 (i.e.,
+       retry for 1 second).
+
 sequence.editor::
        Text editor used by `git rebase -i` for editing the rebase instruction file.
        The value is meant to be interpreted by the shell when it is used.
index 988927775169487df7677a17465b512a567488dd..30e65e9d22566ada51cffb36642e0a27ea4507dc 100644 (file)
@@ -157,6 +157,80 @@ static int lock_file(struct lock_file *lk, const char *path, int flags)
        return lk->fd;
 }
 
+static int sleep_microseconds(long us)
+{
+       struct timeval tv;
+       tv.tv_sec = 0;
+       tv.tv_usec = us;
+       return select(0, NULL, NULL, NULL, &tv);
+}
+
+/*
+ * Constants defining the gaps between attempts to lock a file. The
+ * first backoff period is approximately INITIAL_BACKOFF_MS
+ * milliseconds. The longest backoff period is approximately
+ * (BACKOFF_MAX_MULTIPLIER * INITIAL_BACKOFF_MS) milliseconds.
+ */
+#define INITIAL_BACKOFF_MS 1L
+#define BACKOFF_MAX_MULTIPLIER 1000
+
+/*
+ * Try locking path, retrying with quadratic backoff for at least
+ * timeout_ms milliseconds. If timeout_ms is 0, try locking the file
+ * exactly once. If timeout_ms is -1, try indefinitely.
+ */
+static int lock_file_timeout(struct lock_file *lk, const char *path,
+                            int flags, long timeout_ms)
+{
+       int n = 1;
+       int multiplier = 1;
+       long remaining_us = 0;
+       static int random_initialized = 0;
+
+       if (timeout_ms == 0)
+               return lock_file(lk, path, flags);
+
+       if (!random_initialized) {
+               srandom((unsigned int)getpid());
+               random_initialized = 1;
+       }
+
+       if (timeout_ms > 0) {
+               /* avoid overflow */
+               if (timeout_ms <= LONG_MAX / 1000)
+                       remaining_us = timeout_ms * 1000;
+               else
+                       remaining_us = LONG_MAX;
+       }
+
+       while (1) {
+               long backoff_ms, wait_us;
+               int fd;
+
+               fd = lock_file(lk, path, flags);
+
+               if (fd >= 0)
+                       return fd; /* success */
+               else if (errno != EEXIST)
+                       return -1; /* failure other than lock held */
+               else if (timeout_ms > 0 && remaining_us <= 0)
+                       return -1; /* failure due to timeout */
+
+               backoff_ms = multiplier * INITIAL_BACKOFF_MS;
+               /* back off for between 0.75*backoff_ms and 1.25*backoff_ms */
+               wait_us = (750 + random() % 500) * backoff_ms;
+               sleep_microseconds(wait_us);
+               remaining_us -= wait_us;
+
+               /* Recursion: (n+1)^2 = n^2 + 2n + 1 */
+               multiplier += 2*n + 1;
+               if (multiplier > BACKOFF_MAX_MULTIPLIER)
+                       multiplier = BACKOFF_MAX_MULTIPLIER;
+               else
+                       n++;
+       }
+}
+
 void unable_to_lock_message(const char *path, int err, struct strbuf *buf)
 {
        if (err == EEXIST) {
@@ -179,9 +253,10 @@ NORETURN void unable_to_lock_die(const char *path, int err)
 }
 
 /* This should return a meaningful errno on failure */
-int hold_lock_file_for_update(struct lock_file *lk, const char *path, int flags)
+int hold_lock_file_for_update_timeout(struct lock_file *lk, const char *path,
+                                     int flags, long timeout_ms)
 {
-       int fd = lock_file(lk, path, flags);
+       int fd = lock_file_timeout(lk, path, flags, timeout_ms);
        if (fd < 0 && (flags & LOCK_DIE_ON_ERROR))
                unable_to_lock_die(path, errno);
        return fd;
index cd2ec95d3003be5ff28f84e9e5b33cc85d9990c3..b4abc61c008b5199342c9e5eb12886039d2c0cc4 100644 (file)
@@ -74,8 +74,20 @@ struct lock_file {
 extern void unable_to_lock_message(const char *path, int err,
                                   struct strbuf *buf);
 extern NORETURN void unable_to_lock_die(const char *path, int err);
-extern int hold_lock_file_for_update(struct lock_file *, const char *path, int);
-extern int hold_lock_file_for_append(struct lock_file *, const char *path, int);
+extern int hold_lock_file_for_update_timeout(
+               struct lock_file *lk, const char *path,
+               int flags, long timeout_ms);
+
+static inline int hold_lock_file_for_update(
+               struct lock_file *lk, const char *path,
+               int flags)
+{
+       return hold_lock_file_for_update_timeout(lk, path, flags, 0);
+}
+
+extern int hold_lock_file_for_append(struct lock_file *lk, const char *path,
+                                    int flags);
+
 extern FILE *fdopen_lock_file(struct lock_file *, const char *mode);
 extern char *get_locked_file_path(struct lock_file *);
 extern int commit_lock_file_to(struct lock_file *, const char *path);
diff --git a/refs.c b/refs.c
index f704ee285cdff46e94d9bd47de5f39a7cc6927ee..8480d8dbf5c78c28d895c9c1a2ac7aa5305f4e5e 100644 (file)
--- a/refs.c
+++ b/refs.c
@@ -2505,9 +2505,19 @@ static int write_packed_entry_fn(struct ref_entry *entry, void *cb_data)
 /* This should return a meaningful errno on failure */
 int lock_packed_refs(int flags)
 {
+       static int timeout_configured = 0;
+       static int timeout_value = 1000;
+
        struct packed_ref_cache *packed_ref_cache;
 
-       if (hold_lock_file_for_update(&packlock, git_path("packed-refs"), flags) < 0)
+       if (!timeout_configured) {
+               git_config_get_int("core.packedrefstimeout", &timeout_value);
+               timeout_configured = 1;
+       }
+
+       if (hold_lock_file_for_update_timeout(
+                           &packlock, git_path("packed-refs"),
+                           flags, timeout_value) < 0)
                return -1;
        /*
         * Get the current packed-refs while holding the lock.  If the
index aa9eb3a3e5ef0637615c6d958a29dbd9bbbf0895..8aae98d482aa572b3a9a55f9b38280ad716c7baf 100755 (executable)
@@ -187,4 +187,21 @@ test_expect_success 'notice d/f conflict with existing ref' '
        test_must_fail git branch foo/bar/baz/lots/of/extra/components
 '
 
+test_expect_success 'timeout if packed-refs.lock exists' '
+       LOCK=.git/packed-refs.lock &&
+       >"$LOCK" &&
+       test_when_finished "rm -f $LOCK" &&
+       test_must_fail git pack-refs --all --prune
+'
+
+test_expect_success 'retry acquiring packed-refs.lock' '
+       LOCK=.git/packed-refs.lock &&
+       >"$LOCK" &&
+       test_when_finished "wait; rm -f $LOCK" &&
+       {
+               ( sleep 1 ; rm -f $LOCK ) &
+       } &&
+       git -c core.packedrefstimeout=3000 pack-refs --all --prune
+'
+
 test_done