Skip to content

Commit df67d73

Browse files
Mic92gitster
authored andcommitted
config: retry acquiring config.lock, configurable via core.configLockTimeout
Concurrent config writers race for the ".lock" file, which is taken with open(O_EXCL) and no retry, so the losers fail right away with "could not lock config file". This shows up with parallel "git worktree add -b" against the same repository: each one writes a couple of branch.* keys and the losers fail at random. Worse, "git worktree add" doesn't propagate that failure to its exit code, so the tracking config is silently dropped. (The swallowed error is a separate bug.) Retry instead of giving up on the first EEXIST. The lock is only held while rewriting a small file, so the loser only has to wait out the other writers. Same approach as 4ff0f01 (refs: retry acquiring reference locks for 100ms, 2017-08-21). On the semantics: the on-disk config is read only after the lock is taken, so writers touching different keys can't lose each other's change. Writers touching the same key still get last-writer-wins, but that is already the case today and would need a compare-and-swap config API to fix. The retry only turns hard failures into successes. Default to 1000ms, like core.packedRefsTimeout: same shape of problem, one shared file everyone serializes through. A larger timeout only costs anything when a stale lock is left behind by a crash, which is rare; a smaller one fails spuriously on slow filesystems (NTFS has been seen needing more than 100ms). Make it configurable as core.configLockTimeout. There is no chicken-and-egg problem: we read the config before we lock it. microsoft/git carries a similar patch (core.configWriteLockTimeoutMS, default off) for Scalar's tests. Defaulting to non-zero here because the worktree case fails silently. Helped-by: Patrick Steinhardt <[email protected]> Helped-by: Johannes Schindelin <[email protected]> Signed-off-by: Jörg Thalheim <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 94f0577 commit df67d73

5 files changed

Lines changed: 53 additions & 5 deletions

File tree

Documentation/config/core.adoc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,14 @@ core.packedRefsTimeout::
589589
all; -1 means to try indefinitely. Default is 1000 (i.e.,
590590
retry for 1 second).
591591

592+
core.configLockTimeout::
593+
The length of time, in milliseconds, to retry when trying to
594+
lock a configuration file for writing. Value 0 means not to
595+
retry at all; -1 means to try indefinitely. Default is 1000
596+
(i.e., retry for 1 second). This is read from the configuration
597+
that is already on disk before the lock is taken, so it can be
598+
set persistently like any other option.
599+
592600
core.pager::
593601
Text viewer for use by Git commands (e.g., 'less'). The value
594602
is meant to be interpreted by the shell. The order of preference

config.c

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2903,6 +2903,24 @@ char *git_config_prepare_comment_string(const char *comment)
29032903
return prepared;
29042904
}
29052905

2906+
/*
2907+
* How long to retry acquiring config.lock when another process holds
2908+
* it. Default matches core.packedRefsTimeout; override via
2909+
* core.configLockTimeout.
2910+
*/
2911+
static long config_lock_timeout_ms(struct repository *r)
2912+
{
2913+
static int configured;
2914+
static int timeout_ms = 1000;
2915+
2916+
if (!configured) {
2917+
repo_config_get_int(r, "core.configlocktimeout", &timeout_ms);
2918+
configured = 1;
2919+
}
2920+
2921+
return timeout_ms;
2922+
}
2923+
29062924
static void validate_comment_string(const char *comment)
29072925
{
29082926
size_t leading_blanks;
@@ -2986,7 +3004,8 @@ int repo_config_set_multivar_in_file_gently(struct repository *r,
29863004
* The lock serves a purpose in addition to locking: the new
29873005
* contents of .git/config will be written into it.
29883006
*/
2989-
fd = hold_lock_file_for_update(&lock, config_filename, 0);
3007+
fd = hold_lock_file_for_update_timeout(&lock, config_filename, 0,
3008+
config_lock_timeout_ms(r));
29903009
if (fd < 0) {
29913010
error_errno(_("could not lock config file %s"), config_filename);
29923011
ret = CONFIG_NO_LOCK;
@@ -3331,7 +3350,8 @@ static int repo_config_copy_or_rename_section_in_file(
33313350
if (!config_filename)
33323351
config_filename = filename_buf = repo_git_path(r, "config");
33333352

3334-
out_fd = hold_lock_file_for_update(&lock, config_filename, 0);
3353+
out_fd = hold_lock_file_for_update_timeout(&lock, config_filename, 0,
3354+
config_lock_timeout_ms(r));
33353355
if (out_fd < 0) {
33363356
ret = error(_("could not lock config file %s"), config_filename);
33373357
goto out;

t/t1300-config.sh

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2939,4 +2939,21 @@ test_expect_success 'writing value with trailing CR not stripped on read' '
29392939
test_cmp expect actual
29402940
'
29412941

2942+
test_expect_success 'writing config fails immediately with core.configLockTimeout=0' '
2943+
test_when_finished "rm -f .git/config.lock" &&
2944+
>.git/config.lock &&
2945+
test_must_fail git -c core.configLockTimeout=0 config foo.bar baz 2>err &&
2946+
test_grep "could not lock config file" err
2947+
'
2948+
2949+
test_expect_success 'writing config retries until lock is released' '
2950+
test_when_finished "rm -f .git/config.lock" &&
2951+
>.git/config.lock &&
2952+
{
2953+
( sleep 1 && rm -f .git/config.lock ) &
2954+
} &&
2955+
git -c core.configLockTimeout=5000 config retried.key value &&
2956+
test "$(git config retried.key)" = value
2957+
'
2958+
29422959
test_done

t/t3200-branch.sh

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1037,7 +1037,8 @@ test_expect_success '--set-upstream-to fails on locked config' '
10371037
test_when_finished "rm -f .git/config.lock" &&
10381038
>.git/config.lock &&
10391039
git branch locked &&
1040-
test_must_fail git branch --set-upstream-to locked 2>err &&
1040+
test_must_fail git -c core.configLockTimeout=0 \
1041+
branch --set-upstream-to locked 2>err &&
10411042
test_grep "could not lock config file .git/config" err
10421043
'
10431044

@@ -1068,7 +1069,8 @@ test_expect_success '--unset-upstream should fail if config is locked' '
10681069
test_when_finished "rm -f .git/config.lock" &&
10691070
git branch --set-upstream-to locked &&
10701071
>.git/config.lock &&
1071-
test_must_fail git branch --unset-upstream 2>err &&
1072+
test_must_fail git -c core.configLockTimeout=0 \
1073+
branch --unset-upstream 2>err &&
10721074
test_grep "could not lock config file .git/config" err
10731075
'
10741076

t/t5505-remote.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1327,7 +1327,8 @@ test_expect_success 'remote set-url with locked config' '
13271327
test_when_finished "rm -f .git/config.lock" &&
13281328
git config --get-all remote.someremote.url >expect &&
13291329
>.git/config.lock &&
1330-
test_must_fail git remote set-url someremote baz &&
1330+
test_must_fail git -c core.configLockTimeout=0 \
1331+
remote set-url someremote baz &&
13311332
git config --get-all remote.someremote.url >actual &&
13321333
cmp expect actual
13331334
'

0 commit comments

Comments
 (0)