coverage~bigbes/sr-ht-spec3cb1c03dgitx/lock.go

Coverage
100.0% 21/21 statements
Δ
Blob
5e454d3
Uncovered nothing — every instrumented line ran
1 package gitx
2
3 import (
4 "context"
5 "fmt"
6 "sync"
7 )
8
9 // spaceLocks holds one buffered channel per space directory, used as a
10 // context-aware mutex. It is process-wide on purpose: a space may be opened
11 // many times (once per request is normal), and per-handle locking would not
12 // exclude anything. The key is the cleaned absolute repository directory, which
13 // Create and Open both guarantee.
14 //
15 // This guards only the daemon's own writes. Human pushes go through native
16 // receive-pack in another process and take git's own ref locks, whose
17 // interoperation with go-git's is not verified — which is why every ref move
18 // here is additionally a compare-and-swap that retries.
19 var spaceLocks = struct {
20 mu sync.Mutex
21 m map[string]chan struct{}
22 }{m: make(map[string]chan struct{})}
23
24 121 func spaceLock(dir string) chan struct{} {
25 121 spaceLocks.mu.Lock()
26 121 defer spaceLocks.mu.Unlock()
27 121 ch, ok := spaceLocks.m[dir]
28 121 if !ok {
29 42 ch = make(chan struct{}, 1)
30 42 spaceLocks.m[dir] = ch
31 42 }
32 121 return ch
33 }
34
35 // lock acquires the space's write lock, honouring ctx's deadline. The returned
36 // function releases it and must be called exactly once.
37 //
38 // Entries are never removed from the registry. A space is a long-lived object
39 // and the entry is one channel; reclaiming them would need a refcount whose
40 // only purpose is to free a few dozen bytes.
41 119 func (r *Repo) lock(ctx context.Context) (func(), error) {
42 119 ch := spaceLock(r.dir)
43 119 select {
44 118 case ch <- struct{}{}:
45 118 var once sync.Once
46 118 return func() { once.Do(func() { <-ch }) }, nil
47 1 case <-ctx.Done():
48 1 return nil, fmt.Errorf("gitx: acquiring the write lock for %s: %w", r.ref, ctx.Err())
49 }
50 }
51
52 // WithLock runs fn holding the space's write lock, so a caller that has to make
53 // several git writes look like one operation (a merge plus its bookkeeping, the
54 // reconciler repairing a branch) can do so without reaching into this package's
55 // internals.
56 //
57 // The lock is not reentrant: fn must not call an exported write method on the
58 // same space, which would deadlock.
59 2 func (r *Repo) WithLock(ctx context.Context, fn func(context.Context) error) error {
60 2 ctx, cancel := r.withTimeout(ctx)
61 2 defer cancel()
62 2
63 2 unlock, err := r.lock(ctx)
64 2 if err != nil {
65 1 return err
66 1 }
67 1 defer unlock()
68 1 return fn(ctx)
69 }