返回 DeepSeek-Reasonix
directory_ownership.go
根目录 / internal / session / directory_ownership.go
1 package session
2
3 import (
4 "errors"
5 "os"
6 "path/filepath"
7 "strings"
8 "sync"
9 "time"
10
11 filelock "reasonix/internal/identitylock"
12 )
13
14 func directoryOwnershipPath(dir string) string {
15 return filepath.Join(filepath.Dir(dir), "."+filepath.Base(dir)+".ownership.lock")
16 }
17
18 // writerOwnership is this process's view of the writer leases it holds. The
19 // probe answers from it without touching the lock file, and its mutex makes an
20 // in-process probe and an in-process acquire mutually exclusive: the probe's
21 // transient exclusive hold can therefore never be what an acquirer collides
22 // with inside this process.
23 var writerOwnership = struct {
24 mu sync.Mutex
25 held map[string]int
26 }{held: map[string]int{}}
27
28 // A probe in another process holds writer.lock for microseconds. An acquirer
29 // that lands in that window sees ErrHeld exactly as it would for a real owner,
30 // so it retries a bounded number of times before reporting the lock as owned.
31 // The bound is small enough that a genuine owner is still reported promptly.
32 const writerProbeCollisionRetries = 5
33
34 var writerProbeCollisionBackoff = 10 * time.Millisecond
35
36 // Test seams for the collision protocol: the probe hook runs while the probe
37 // holds the transient lock and the ownership mutex, the acquire hooks run
38 // before the acquirer first contends for that mutex and after each ErrHeld it
39 // decides to retry.
40 var (
41 writerProbeHoldHookForTest func()
42 writerAcquireEnterHookForTest func()
43 writerAcquireRetryHookForTest func(attempt int)
44 )
45
46 func writerOwnershipKey(dir string) string {
47 dir = strings.TrimSpace(dir)
48 abs, err := filepath.Abs(dir)
49 if err != nil {
50 return filepath.Clean(dir)
51 }
52 return filepath.Clean(abs)
53 }
54
55 func writerHeldLocally(key string) bool {
56 writerOwnership.mu.Lock()
57 defer writerOwnership.mu.Unlock()
58 return writerOwnership.held[key] > 0
59 }
60
61 // Keep both claims for a writer's lifetime: the inner claim excludes existing
62 // writers and the outer claim remains usable while the directory is moved.
63 func acquireSessionWriter(dir string) (func(), error) {
64 if writerAcquireEnterHookForTest != nil {
65 writerAcquireEnterHookForTest()
66 }
67 key := writerOwnershipKey(dir)
68 if writerHeldLocally(key) {
69 return nil, filelock.ErrHeld
70 }
71 releaseDirectory, err := filelock.TryAcquire(directoryOwnershipPath(dir))
72 if err != nil {
73 return nil, err
74 }
75 releaseWriter, err := acquireWriterLock(dir, key)
76 if err != nil {
77 releaseDirectory()
78 return nil, err
79 }
80 return func() {
81 releaseWriter()
82 releaseDirectory()
83 }, nil
84 }
85
86 // acquireWriterLock takes writer.lock under the ownership mutex and records the
87 // hold. Only the inner lock is retried: the probe never touches the directory
88 // claim, so ErrHeld there is always a real owner.
89 func acquireWriterLock(dir, key string) (func(), error) {
90 lockPath := filepath.Join(dir, "writer.lock")
91 for attempt := 0; ; attempt++ {
92 writerOwnership.mu.Lock()
93 release, err := filelock.TryAcquire(lockPath)
94 if err == nil {
95 writerOwnership.held[key]++
96 writerOwnership.mu.Unlock()
97 var once sync.Once
98 return func() {
99 once.Do(func() {
100 writerOwnership.mu.Lock()
101 release()
102 if writerOwnership.held[key] <= 1 {
103 delete(writerOwnership.held, key)
104 } else {
105 writerOwnership.held[key]--
106 }
107 writerOwnership.mu.Unlock()
108 })
109 }, nil
110 }
111 writerOwnership.mu.Unlock()
112 if !errors.Is(err, filelock.ErrHeld) || attempt >= writerProbeCollisionRetries {
113 return nil, err
114 }
115 if writerAcquireRetryHookForTest != nil {
116 writerAcquireRetryHookForTest(attempt)
117 }
118 time.Sleep(writerProbeCollisionBackoff)
119 }
120 }
121
122 // ProbeWriterHeld reports whether some runtime currently owns the session's
123 // writer lease. It is the occupancy oracle for final-format identities: the
124 // takeover protocol asks the holder to stand down and then watches this probe
125 // turn false before re-acquiring. A lease this process holds is answered from
126 // the ownership registry; only a foreign hold needs the lock file, and a
127 // session whose writer.lock was never created counts as free without creating
128 // it. A missing session directory counts as free.
129 func ProbeWriterHeld(dir string) bool {
130 dir = strings.TrimSpace(dir)
131 if dir == "" {
132 return false
133 }
134 if writerHeldLocally(writerOwnershipKey(dir)) {
135 return true
136 }
137 if _, err := os.Stat(filepath.Join(dir, "manifest.json")); err != nil {
138 return false
139 }
140 lockPath := filepath.Join(dir, "writer.lock")
141 if _, err := os.Stat(lockPath); err != nil {
142 return false
143 }
144 writerOwnership.mu.Lock()
145 defer writerOwnership.mu.Unlock()
146 release, err := filelock.TryAcquire(lockPath)
147 if err != nil {
148 return true
149 }
150 if writerProbeHoldHookForTest != nil {
151 writerProbeHoldHookForTest()
152 }
153 release()
154 return false
155 }
156
156 lines GO