diff --git a/gitutil/keyed_mutex_test.go b/gitutil/keyed_mutex_test.go new file mode 100644 index 000000000..ccab995e2 --- /dev/null +++ b/gitutil/keyed_mutex_test.go @@ -0,0 +1,52 @@ +package gitutil + +import ( + "fmt" + "testing" + "time" +) + +func (k *keyedMutex) entries() int { + k.mu.Lock() + defer k.mu.Unlock() + return len(k.m) +} + +func TestKeyedMutexForgetsKeysNobodyHolds(t *testing.T) { + var k keyedMutex + for i := range 100 { + k.lock(fmt.Sprintf("/repos/did:web:made-up-%d", i))() + } + if got := k.entries(); got != 0 { + t.Fatalf("%d entries left after every lock was released, want 0", got) + } + + unlock := k.lock("same") + entered := make(chan struct{}) + go func() { + k.lock("same")() + close(entered) + }() + select { + case <-entered: + t.Fatal("a second holder got in while the first still held the key") + case <-time.After(50 * time.Millisecond): + } + if got := k.entries(); got != 1 { + t.Fatalf("%d entries while one holds and one waits, want 1", got) + } + unlock() + select { + case <-entered: + case <-time.After(5 * time.Second): + t.Fatal("the waiter never got the key after the holder let go") + } + // the waiter drops its entry just after signaling, wait for that + deadline := time.Now().Add(5 * time.Second) + for k.entries() != 0 && time.Now().Before(deadline) { + time.Sleep(5 * time.Millisecond) + } + if got := k.entries(); got != 0 { + t.Fatalf("%d entries left after the waiter finished, want 0", got) + } +} diff --git a/gitutil/sync.go b/gitutil/sync.go index c9fbbc2fa..63f53e60f 100644 --- a/gitutil/sync.go +++ b/gitutil/sync.go @@ -15,25 +15,39 @@ import ( // don't let two requests mutate the same checkout at once var repoLocks keyedMutex +// entries drop once nobody holds them since callers pick the keys and nothing else bounds the map type keyedMutex struct { mu sync.Mutex - m map[string]*sync.Mutex + m map[string]*keyedLock +} + +type keyedLock struct { + mu sync.Mutex + users int } func (k *keyedMutex) lock(key string) func() { k.mu.Lock() if k.m == nil { - k.m = make(map[string]*sync.Mutex) + k.m = make(map[string]*keyedLock) } - mu, ok := k.m[key] + l, ok := k.m[key] if !ok { - mu = &sync.Mutex{} - k.m[key] = mu + l = &keyedLock{} + k.m[key] = l } + l.users++ k.mu.Unlock() - mu.Lock() - return mu.Unlock + l.mu.Lock() + return func() { + l.mu.Unlock() + k.mu.Lock() + defer k.mu.Unlock() + if l.users--; l.users == 0 { + delete(k.m, key) + } + } } func Version() (*version.Version, error) {