git/list[1] front-page[2] threads[3] people[4] search[5] about
 

[PATCH v2 00/17] Fix some mkdir/rmdir races

From
Michael Haggerty <mhagger@alum.mit.edu>
Date
Jan 6, 2014, 13:45 UTC
Message-ID
<1389015935-21936-1-git-send-email-mhagger@alum.mit.edu>

This is v2 of changes [1] to remove some mkdir/rmdir races around safe_create_leading_directories() and a couple of its callers. Thanks to Jonathan Nieder for his thorough review of v1 and to Ramsay Jones for pointing out a typo. I think I have addressed all of their suggestions.

This patch series has a bigger scope than v1. The main differences is that I took Jonathan's (implicit?) suggestion to move the retrying to a level higher, where files are actually created in the directories and therefore the directories are definitely no longer subject to pruning.

Differences from v1:
* Improve some commit messages
* Break up some changes to safe_create_leading_directories() into
  smaller steps.
* Fix a problem in safe_create_leading_directories() when handling
  paths with multiple slashes (e.g., "foo//bar").  (I noticed this
  pre-existing problem while making the other changes.)
* Change how retrying is done:
  * safe_create_leading_directories() doesn't retry internally;
    instead, its return value is turned into an enum with a new value,
    SCLD_VANISHED, that indicates that a directory along the path
    vanished while it was working.  This return value is an indication
    that its caller might want to try calling the function again.
  * Change rename_ref() to retry if either
    safe_create_leading_directories() returns SCLD_VANISHED or if its
    own call to rename() returns ENOENT.
* Fix a similar mkdir/rmdir race in lock_ref_sha1_basic().  This one
  is actually more interesting than the one in rename_ref() because it
  can be triggered internal to git.
* Fix a race in remove_dir_recurse(): if somebody else deletes a file
  or directory that the function wanted to delete anyway, don't treat
  it as an error.

The main git-internal race that I know to be fixed by these changes is when pack-refs is trying to delete empty directories at the same time as another process is trying to create a new reference. It can be reproduced by this script:

    BRANCHES="foo/bar/xyzzy foo/bar/plugh"
    HEADS="h1 h2"
    rm -rf test-repo
    $GIT init test-repo
    cd test-repo
    $GIT config user.name 'Lou User'
    $GIT config user.email 'luser@example.com'
    for h in $HEADS
    do
        $GIT commit --allow-empty -m $h
        $GIT branch $h
    done
    (
        while true
        do
    	for b in $BRANCHES
    	do
    	    for h in $HEADS
    	    do
    		$GIT update-ref refs/heads/$b $h
    	    done
    	done
        done
    ) &
    pid1=$!
    (
        while true
        do
    	$GIT pack-refs --all
        done
    ) &
    pid2=$!

The script has to fail if update-ref and pack-refs try to lock the same reference at the same time, because we don't handle lock contention:

    fatal: Unable to create '/home/mhagger/tmp/test-repo/.git/refs/heads/foo/bar/plugh.lock': File exists.
    If no other git process is currently running, this probably means a
    git process crashed in this repository earlier. Make sure no other git
    process is running and remove the file manually to continue.

Also, if update-ref changes the value of a reference while pack-refs is running, then pack-refs emits a warning and leaves the new value of the loose reference in place:

    error: Ref refs/heads/foo/bar/xyzzy is at 003a9cedc3ec79b8f589b158ffe91177f0a611b3 but expected 34bf1e34d27a639732a01fef0a791e58c2c0882f

But it used to have other unnecessary failures, too, related to mkdir/rmdir races between the two programs:

    error: unable to create directory for .git/refs/heads/foo/bar/plugh
and
    fatal: Unable to create '/home/mhagger/tmp/test-repo/.git/refs/heads/foo/bar/plugh.lock': No such file or directory

This patch series eliminates the last two types of failures. Here are tallies of failures from running the above script for 60 seconds before and after this change. Before:

    Total updates: 46900
    Unable to create '*.lock': File exists: 1992
    * is at * but expected *: 940
    unable to create directory: 187
    Unable to create '*.lock': No such file or directory: 197
After:
    Total updates: 47892
    Unable to create '*.lock': File exists: 2105
    * is at * but expected *: 835
    unable to create directory: 0
    Unable to create '*.lock': No such file or directory: 0

Clearly, more work is still needed in this area. For example, the "* is at * but expected *" errors are not really errors at all and should not be reported as such. And we might want to consider adding a short delay and then a retry when acquiring locks. If I run the same test script with the following patch, then most or all of the "Unable to create '*.lock': File exists" errors go away too.

======================================================================
diff --git a/refs.c b/refs.c
index 810f802..b3ff1f5 100644
--- a/refs.c
+++ b/refs.c
@@ -2117,7 +2117,10 @@ static struct ref_lock *lock_ref_sha1_basic(const char *refname,
                         * again:
                         */
                        goto retry;
-               else
+               else if (errno == EEXIST && --attempts > 0) {
+                       usleep(1000);
+                       goto retry;
+               } else
                        unable_to_lock_index_die(ref_file, errno);
        }
        return old_sha1 ? verify_lock(lock, old_sha1, mustexist) : lock;
======================================================================

I hope to do some more work here in the near future.

[1] Version 1: http://thread.gmane.org/gmane.comp.version-control.git/239638

Michael Haggerty (17):
  safe_create_leading_directories(): fix format of "if" chaining
  safe_create_leading_directories(): reduce scope of local variable
  safe_create_leading_directories(): add explicit "slash" pointer
  safe_create_leading_directories(): rename local variable
  safe_create_leading_directories(): split on first of multiple slashes
  safe_create_leading_directories(): always restore slash at end of loop
  safe_create_leading_directories(): introduce enum for return values
  cmd_init_db(): when creating directories, handle errors conservatively
  safe_create_leading_directories(): add new error value SCLD_VANISHED
  lock_ref_sha1_basic(): on SCLD_VANISHED, retry
  lock_ref_sha1_basic(): if locking fails with ENOENT, retry
  remove_dir_recurse(): tighten condition for removing unreadable dir
  remove_dir_recurse(): handle disappearing files and directories
  rename_ref(): extract function rename_tmp_log()
  rename_tmp_log(): handle a possible mkdir/rmdir race
  rename_tmp_log(): limit the number of remote_empty_directories()
    attempts
  rename_tmp_log(): on SCLD_VANISHED, retry

 builtin/init-db.c |  9 +++---
 cache.h           | 25 +++++++++++++--
 dir.c             | 27 +++++++++++-----
 merge-recursive.c |  2 +-
 refs.c            | 92 ++++++++++++++++++++++++++++++++++++++++---------------
 sha1_file.c       | 67 ++++++++++++++++++++++------------------
 6 files changed, 155 insertions(+), 67 deletions(-)
-- 
1.8.5.2
Next: Michael Haggerty
Message 1 of 29 in “Fix some mkdir/rmdir races”
  1. 00/17 Fix some mkdir/rmdir racesMichael Haggerty, Jan 6, 2014
  2. 01/17 safe_create_leading_directories(): fix format of "if" chainingMichael Haggerty, Jan 6, 2014
  3. 02/17 safe_create_leading_directories(): reduce scope of local variableMichael Haggerty, Jan 6, 2014
  4. 03/17 safe_create_leading_directories(): add explicit "slash" pointerMichael Haggerty, Jan 6, 2014
  5. Junio C HamanoJan 6, 2014
  6. Michael HaggertyJan 7, 2014
  7. Junio C HamanoJan 7, 2014
  8. Sebastian SchuberthJan 19, 2014
  9. 04/17 safe_create_leading_directories(): rename local variableMichael Haggerty, Jan 6, 2014
  10. 05/17 safe_create_leading_directories(): split on first of multiple slashesMichael Haggerty, Jan 6, 2014
  11. 06/17 safe_create_leading_directories(): always restore slash at end of loopMichael Haggerty, Jan 6, 2014
  12. 07/17 safe_create_leading_directories(): introduce enum for return valuesMichael Haggerty, Jan 6, 2014
  13. 08/17 cmd_init_db(): when creating directories, handle errors conservativelyMichael Haggerty, Jan 6, 2014
  14. 09/17 safe_create_leading_directories(): add new error value SCLD_VANISHEDMichael Haggerty, Jan 6, 2014
  15. 10/17 lock_ref_sha1_basic(): on SCLD_VANISHED, retryMichael Haggerty, Jan 6, 2014
  16. Junio C HamanoJan 6, 2014
  17. Michael HaggertyJan 7, 2014
  18. 11/17 lock_ref_sha1_basic(): if locking fails with ENOENT, retryMichael Haggerty, Jan 6, 2014
  19. 12/17 remove_dir_recurse(): tighten condition for removing unreadable dirMichael Haggerty, Jan 6, 2014
  20. 13/17 remove_dir_recurse(): handle disappearing files and directoriesMichael Haggerty, Jan 6, 2014
  21. Junio C HamanoJan 6, 2014
  22. Michael HaggertyJan 7, 2014
  23. Junio C HamanoJan 7, 2014
  24. 14/17 rename_ref(): extract function rename_tmp_log()Michael Haggerty, Jan 6, 2014
  25. 15/17 rename_tmp_log(): handle a possible mkdir/rmdir raceMichael Haggerty, Jan 6, 2014
  26. 16/17 rename_tmp_log(): limit the number of remote_empty_directories() attemptsMichael Haggerty, Jan 6, 2014
  27. 17/17 rename_tmp_log(): on SCLD_VANISHED, retryMichael Haggerty, Jan 6, 2014
  28. Junio C HamanoJan 6, 2014
  29. Michael HaggertyJan 7, 2014

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.