返回 DeepSeek-Reasonix
session_write_authority.go
根目录 / internal / agent / session_write_authority.go
1 package agent
2
3 import (
4 "errors"
5 "fmt"
6 "strings"
7 "sync"
8 "sync/atomic"
9 )
10
11 // ErrSessionWriteAuthorityMissing is returned when a save or ownership-
12 // sensitive rewrite runs without a write authority bound for the target path.
13 // Callers must re-acquire a lease and rebind rather than forking recovery.
14 var ErrSessionWriteAuthorityMissing = errors.New("session write authority missing")
15
16 // ErrSessionWriteAuthorityStale is returned when the bound authority no longer
17 // matches the live lease generation (rebind, release, or controller replace).
18 // The in-memory transcript is preserved; callers rebind or fall back to an
19 // emergency isolated copy only on shutdown.
20 var ErrSessionWriteAuthorityStale = errors.New("session write authority stale")
21
22 // sessionWriteGeneration is a process-wide counter used by controllers when
23 // they issue a new authority generation. Each bind/rebind/replace must use a
24 // fresh value so any previous Controller's authority becomes stale immediately.
25 var sessionWriteGeneration atomic.Uint64
26
27 // NextSessionWriteGeneration allocates a new generation token. Controllers call
28 // this on create, restore, path change, and re-issue.
29 func NextSessionWriteGeneration() uint64 {
30 return sessionWriteGeneration.Add(1)
31 }
32
33 // SessionWriteAuthority is an unforgeable in-memory write permit for one
34 // session path. Only a live SessionLease can issue it; the token carries the
35 // lease owner id and a controller-bound generation that is invalidated when the
36 // controller is replaced or rebound. Tokens never leave process memory and
37 // cannot be reconstructed from disk metadata.
38 type SessionWriteAuthority struct {
39 path string
40 ownerID uint64
41 generation uint64
42 lease *SessionLease
43 // writer is the SessionWriter this authority was minted through, if any.
44 // Saves guarded by a writer-bound authority serialize through the
45 // writer's saveMu and update its event-log baseline.
46 writer *SessionWriter
47 }
48
49 // IssueWriteAuthority mints a path-bound authority for generation. The lease
50 // must still be held; a released lease returns a stale error so callers cannot
51 // mint after release.
52 func (l *SessionLease) IssueWriteAuthority(generation uint64) (*SessionWriteAuthority, error) {
53 if l == nil {
54 return nil, ErrSessionWriteAuthorityMissing
55 }
56 if generation == 0 {
57 return nil, fmt.Errorf("%w: zero generation", ErrSessionWriteAuthorityMissing)
58 }
59 l.mu.Lock()
60 defer l.mu.Unlock()
61 if l.released || l.leaseLock == nil {
62 return nil, ErrSessionWriteAuthorityStale
63 }
64 l.writeGeneration = generation
65 return &SessionWriteAuthority{
66 path: l.path,
67 ownerID: l.ownerID,
68 generation: generation,
69 lease: l,
70 }, nil
71 }
72
73 // Path is the canonical session path this authority covers.
74 func (a *SessionWriteAuthority) Path() string {
75 if a == nil {
76 return ""
77 }
78 return a.path
79 }
80
81 // Generation is the controller-bound generation this authority was issued for.
82 func (a *SessionWriteAuthority) Generation() uint64 {
83 if a == nil {
84 return 0
85 }
86 return a.generation
87 }
88
89 // Valid reports whether the authority still matches a live lease for its path
90 // and generation. A nil authority is never valid.
91 func (a *SessionWriteAuthority) Valid() bool {
92 if a == nil || a.lease == nil || a.generation == 0 {
93 return false
94 }
95 a.lease.mu.Lock()
96 defer a.lease.mu.Unlock()
97 return a.validLeaseLocked()
98 }
99
100 func (a *SessionWriteAuthority) validLeaseLocked() bool {
101 if a.lease.released || a.lease.leaseLock == nil {
102 return false
103 }
104 if a.lease.path != a.path || a.lease.ownerID != a.ownerID || a.lease.writeGeneration != a.generation {
105 return false
106 }
107 // Active-owner registry must still name this generation's lease. A reclaim
108 // that replaced the owner id makes prior authorities stale even if the
109 // lease object has not been released yet.
110 owner, ok := sessionLeaseActiveOwners.Load(a.path)
111 if !ok {
112 return false
113 }
114 id, ok := owner.(uint64)
115 return ok && id == a.ownerID
116 }
117
118 // lockCurrentLease fences generation replacement through a metadata commit.
119 // Callers acquire their save/file/meta locks first: a lease operation must
120 // never hold this mutex while waiting to acquire those locks.
121 func (a *SessionWriteAuthority) lockCurrentLease(path string) (func(), error) {
122 if a == nil || a.lease == nil || a.generation == 0 {
123 return nil, ErrSessionWriteAuthorityMissing
124 }
125 canonical := CanonicalSessionPath(path)
126 a.lease.mu.Lock()
127 if a.path != canonical || !a.validLeaseLocked() {
128 a.lease.mu.Unlock()
129 return nil, ErrSessionWriteAuthorityStale
130 }
131 return a.lease.mu.Unlock, nil
132 }
133
134 // Covers reports whether a is valid for path (canonical comparison).
135 func (a *SessionWriteAuthority) Covers(path string) bool {
136 if !a.Valid() {
137 return false
138 }
139 return a.path == CanonicalSessionPath(path)
140 }
141
142 // BeginSave registers an in-flight save so Release waits for it. Writer-minted
143 // authorities also hold saveMu for the whole cycle. The returned release runs
144 // once; missing or stale authorities return a typed error, not recovery.
145 func (a *SessionWriteAuthority) BeginSave(path string) (func(), error) {
146 if a == nil {
147 return nil, ErrSessionWriteAuthorityMissing
148 }
149 if a.lease == nil || a.generation == 0 {
150 return nil, ErrSessionWriteAuthorityMissing
151 }
152 if a.writer != nil {
153 a.writer.saveMu.Lock()
154 }
155 release, err := a.beginLeaseSave(path)
156 if err != nil {
157 if a.writer != nil {
158 a.writer.saveMu.Unlock()
159 }
160 return nil, err
161 }
162 writer := a.writer
163 return func() {
164 release()
165 if writer != nil {
166 writer.saveMu.Unlock()
167 }
168 }, nil
169 }
170
171 // beginLeaseSave registers the save against the lease's active-save count and
172 // revalidates the authority under the lease lock.
173 func (a *SessionWriteAuthority) beginLeaseSave(path string) (func(), error) {
174 a.lease.mu.Lock()
175 defer a.lease.mu.Unlock()
176 if a.path != CanonicalSessionPath(path) ||
177 a.lease.released || a.lease.leaseLock == nil ||
178 a.lease.path != a.path || a.lease.ownerID != a.ownerID ||
179 a.lease.writeGeneration != a.generation {
180 return nil, ErrSessionWriteAuthorityStale
181 }
182 owner, ok := sessionLeaseActiveOwners.Load(a.path)
183 id, okID := owner.(uint64)
184 if !ok || !okID || id != a.ownerID {
185 return nil, ErrSessionWriteAuthorityStale
186 }
187 a.lease.activeSaves++
188 var once sync.Once
189 return func() {
190 once.Do(func() {
191 a.lease.mu.Lock()
192 if a.lease.activeSaves > 0 {
193 a.lease.activeSaves--
194 }
195 if a.lease.activeSaves == 0 && a.lease.releaseWait != nil {
196 // Wake any Release waiters that parked for in-flight saves.
197 close(a.lease.releaseWait)
198 a.lease.releaseWait = nil
199 }
200 a.lease.mu.Unlock()
201 })
202 }, nil
203 }
204
205 // bindWriteAuthority stores auth on the session. A nil auth clears the binding.
206 // The session only consults authority for ownership-sensitive decisions when a
207 // non-nil auth has been bound at least once (authRequired).
208 func (s *Session) BindWriteAuthority(auth *SessionWriteAuthority) {
209 if s == nil {
210 return
211 }
212 s.mu.Lock()
213 defer s.mu.Unlock()
214 s.writeAuth = auth
215 if auth != nil {
216 s.authRequired = true
217 }
218 }
219
220 // RequireWriteAuthority permanently puts this Session on the production
221 // fail-closed path. Controllers call it before attempting lease issuance so a
222 // failed or interrupted bind cannot leave a persisted session writable through
223 // the legacy unbound test path.
224 func (s *Session) RequireWriteAuthority() {
225 if s == nil {
226 return
227 }
228 s.mu.Lock()
229 s.authRequired = true
230 s.mu.Unlock()
231 }
232
233 // WriteAuthorityRequired reports whether production admission and saves must
234 // present a live path-bound authority.
235 func (s *Session) WriteAuthorityRequired() bool {
236 if s == nil {
237 return false
238 }
239 s.mu.RLock()
240 defer s.mu.RUnlock()
241 return s.authRequired
242 }
243
244 // WriteAuthority returns the currently bound authority, if any.
245 func (s *Session) WriteAuthority() *SessionWriteAuthority {
246 if s == nil {
247 return nil
248 }
249 s.mu.RLock()
250 defer s.mu.RUnlock()
251 return s.writeAuth
252 }
253
254 // ClearWriteAuthority drops the bound authority without clearing authRequired,
255 // so subsequent saves fail closed until a fresh authority is bound.
256 func (s *Session) ClearWriteAuthority() {
257 if s == nil {
258 return
259 }
260 s.mu.Lock()
261 defer s.mu.Unlock()
262 s.writeAuth = nil
263 }
264
265 // requireWriteAuthorityForSave enforces the production write path. Low-level
266 // unit tests that never bind an authority keep the legacy unbound path so they
267 // can exercise pure CAS mechanics. Once a controller has bound an authority,
268 // every save must present a live one for the target path.
269 func (s *Session) requireWriteAuthorityForSave(path string) (func(), error) {
270 if s == nil {
271 return nil, ErrSessionWriteAuthorityMissing
272 }
273 s.mu.RLock()
274 auth := s.writeAuth
275 required := s.authRequired
276 s.mu.RUnlock()
277 if !required {
278 return func() {}, nil
279 }
280 if auth == nil {
281 return nil, ErrSessionWriteAuthorityMissing
282 }
283 return auth.BeginSave(path)
284 }
285
286 // hasValidWriteAuthority reports whether the session currently holds a live
287 // authority covering path. Used by conflict classification for owned rewrite.
288 func (s *Session) hasValidWriteAuthority(path string) bool {
289 if s == nil {
290 return false
291 }
292 s.mu.RLock()
293 auth := s.writeAuth
294 s.mu.RUnlock()
295 return auth.Covers(path)
296 }
297
298 // authorityErrorForPath returns a typed authority error when the session has
299 // bound (or required) authority that no longer covers path. A never-bound
300 // session returns nil so low-level CAS tests keep their existing conflict path.
301 func (s *Session) authorityErrorForPath(path string) error {
302 if s == nil {
303 return nil
304 }
305 s.mu.RLock()
306 auth := s.writeAuth
307 required := s.authRequired
308 s.mu.RUnlock()
309 if !required {
310 return nil
311 }
312 if auth == nil {
313 return ErrSessionWriteAuthorityMissing
314 }
315 if auth.Covers(path) {
316 return nil
317 }
318 if strings.TrimSpace(auth.path) == "" || auth.generation == 0 {
319 return ErrSessionWriteAuthorityMissing
320 }
321 return ErrSessionWriteAuthorityStale
322 }
323
323 lines GO