返回 DeepSeek-Reasonix
session_removal.go
根目录 / internal / agent / session_removal.go
1 package agent
2
3 import (
4 "errors"
5 "fmt"
6 "os"
7
8 "reasonix/internal/pathidentity"
9 "reasonix/internal/store"
10 )
11
12 // SessionRemovalGuard holds a session's save lock and lease lock for the
13 // duration of a destructive operation (trash, purge, permanent delete). While
14 // held, no other runtime can acquire the session lease and no saver can write
15 // the transcript, so artifacts can be moved or deleted without racing a live
16 // owner; the lock files themselves are then deleted atomically with the
17 // release (unlink-under-flock on Unix, delete-disposition on Windows), so a
18 // later acquirer can never lock an inode that survived the deletion.
19 //
20 // This closes the probe-then-delete window: a one-shot busy check followed by
21 // a plain RemoveAll lets another process acquire the lease between the two
22 // steps and then loses its lock file, breaking cross-process mutual exclusion.
23 type SessionRemovalGuard struct {
24 path string
25 key string
26 saveLock *sessionLockFile
27 leaseLock *sessionLockFile
28 legacyLeaseLock *sessionLockFile
29 legacyPath string
30 restoreOwner uint64
31 }
32
33 func tryTakeSessionLeaseLock(path string) (*sessionLockFile, error) {
34 lock, err := tryTakeSessionLeaseLockFile(store.SessionLeaseLock(path))
35 if errors.Is(err, ErrSessionFileLockHeld) {
36 return nil, ErrSessionLeaseHeld
37 }
38 return lock, err
39 }
40
41 // TryAcquireSessionRemovalGuard takes both locks without blocking. A live
42 // holder of either — including a lease held elsewhere in this process —
43 // surfaces as ErrSessionLeaseHeld so callers report the session as busy
44 // instead of deleting files out from under a running owner.
45 func TryAcquireSessionRemovalGuard(path string) (*SessionRemovalGuard, error) {
46 identity, err := resolveSessionPathIdentity(path)
47 if err != nil {
48 return nil, err
49 }
50 path, key := identity.PhysicalPath, identity.Key
51 legacyPath := pathidentity.Canonical(identity.AccessPath)
52 if sessionLeaseHeldLocally(key) {
53 info, _ := LoadSessionLeaseInfo(path)
54 return nil, &SessionLeaseError{Path: key, Info: info}
55 }
56 leaseLock, legacyLeaseLock, err := tryTakeCompatibleSessionLeaseLocks(path, legacyPath)
57 if err != nil {
58 if errors.Is(err, ErrSessionLeaseHeld) {
59 info, _ := LoadSessionLeaseInfo(path)
60 return nil, &SessionLeaseError{Path: key, Info: info}
61 }
62 return nil, err
63 }
64 saveLock, err := tryTakeSessionLockFile(store.SessionLockFile(path))
65 if err != nil {
66 unlockSessionLeaseLocks(leaseLock, legacyLeaseLock)
67 if errors.Is(err, ErrSessionFileLockHeld) {
68 // A save is in flight; deleting mid-write would race it.
69 return nil, &SessionLeaseError{Path: key}
70 }
71 return nil, err
72 }
73 return &SessionRemovalGuard{
74 path: path, key: key, legacyPath: legacyPath,
75 saveLock: saveLock, leaseLock: leaseLock, legacyLeaseLock: legacyLeaseLock,
76 }, nil
77 }
78
79 // TryConvertToRemovalGuard transfers a live lease into destructive ownership
80 // without ever releasing its lease lock. The save lock is acquired first, so a
81 // failed conversion leaves the original lease fully active and reusable.
82 func (l *SessionLease) TryConvertToRemovalGuard() (*SessionRemovalGuard, error) {
83 if l == nil {
84 return nil, fmt.Errorf("nil session lease")
85 }
86 l.mu.Lock()
87 defer l.mu.Unlock()
88 if l.released || l.leaseLock == nil {
89 return nil, &SessionLeaseError{Path: l.path}
90 }
91 saveLock, err := tryTakeSessionLockFile(store.SessionLockFile(l.accessPath))
92 if err != nil {
93 if errors.Is(err, ErrSessionFileLockHeld) {
94 return nil, &SessionLeaseError{Path: l.path}
95 }
96 return nil, err
97 }
98 // Keep the OS lock and original process-local reservation while revoking
99 // active runtime ownership. Both block competing acquisition and make
100 // rollback immune to an in-process lease race.
101 sessionLeaseActiveOwners.CompareAndDelete(l.path, l.ownerID)
102 leaseLock := l.leaseLock
103 legacyLeaseLock := l.legacyLeaseLock
104 l.leaseLock = nil
105 l.legacyLeaseLock = nil
106 l.released = true
107 return &SessionRemovalGuard{
108 path: l.accessPath,
109 key: l.path,
110 saveLock: saveLock,
111 leaseLock: leaseLock,
112 legacyLeaseLock: legacyLeaseLock,
113 legacyPath: l.legacyAccessPath,
114 restoreOwner: l.ownerID,
115 }, nil
116 }
117
118 // RestoreSessionLease aborts a converted removal guard before the destructive
119 // commit point. The original lease metadata remains untouched and both locks
120 // stay held until its owner generation is republished, so rollback cannot fail
121 // on a second disk write and has no writer gap.
122 func (g *SessionRemovalGuard) RestoreSessionLease() (*SessionLease, error) {
123 if g == nil || g.restoreOwner == 0 || g.leaseLock == nil || g.saveLock == nil {
124 return nil, fmt.Errorf("removal guard cannot restore a session lease")
125 }
126 ownerID := g.restoreOwner
127 current, ok := sessionLeaseOwners.Load(g.key)
128 if !ok || current != ownerID {
129 return nil, &SessionLeaseError{Path: g.path}
130 }
131 sessionLeaseActiveOwners.Delete(g.key)
132 sessionLeaseActiveOwners.Store(g.key, ownerID)
133 lease := &SessionLease{
134 path: g.key, accessPath: g.path, legacyAccessPath: g.legacyPath,
135 ownerID: ownerID, leaseLock: g.leaseLock, legacyLeaseLock: g.legacyLeaseLock,
136 }
137 g.leaseLock = nil
138 g.legacyLeaseLock = nil
139 g.saveLock.Unlock()
140 g.saveLock = nil
141 g.restoreOwner = 0
142 return lease, nil
143 }
144
145 // Release ends the guard without deleting the lock files — the abort path
146 // when the destructive operation did not happen. Safe to call after
147 // RemoveSidecarsAndRelease (it becomes a no-op).
148 func (g *SessionRemovalGuard) Release() {
149 if g == nil {
150 return
151 }
152 if g.restoreOwner != 0 {
153 // A converted lease no longer has an active owner. Do not leave its
154 // identity sidecar naming a writer after an abort that chose not to
155 // restore the runtime lease.
156 sessionLeaseActiveOwners.CompareAndDelete(g.key, g.restoreOwner)
157 sessionLeaseOwners.CompareAndDelete(g.key, g.restoreOwner)
158 _ = os.Remove(store.SessionLeaseInfo(g.path))
159 g.restoreOwner = 0
160 }
161 if g.saveLock != nil {
162 g.saveLock.Unlock()
163 g.saveLock = nil
164 }
165 if g.leaseLock != nil {
166 g.leaseLock.Unlock()
167 g.leaseLock = nil
168 }
169 if g.legacyLeaseLock != nil {
170 g.legacyLeaseLock.Unlock()
171 g.legacyLeaseLock = nil
172 }
173 }
174
175 // RemoveSidecarsAndRelease deletes the lease info and both lock files
176 // atomically with the release, then ends the guard. The lease info goes first,
177 // while the lease lock is still held, so no probe can adopt it mid-removal.
178 func (g *SessionRemovalGuard) RemoveSidecarsAndRelease() error {
179 if g == nil {
180 return nil
181 }
182 var errs []error
183 if g.restoreOwner != 0 {
184 sessionLeaseActiveOwners.CompareAndDelete(g.key, g.restoreOwner)
185 sessionLeaseOwners.CompareAndDelete(g.key, g.restoreOwner)
186 }
187 if err := os.Remove(store.SessionLeaseInfo(g.path)); err != nil && !os.IsNotExist(err) {
188 errs = append(errs, err)
189 }
190 if g.saveLock != nil {
191 if err := g.saveLock.RemoveAndUnlock(); err != nil {
192 errs = append(errs, err)
193 }
194 g.saveLock = nil
195 }
196 if g.leaseLock != nil {
197 if err := g.leaseLock.RemoveAndUnlock(); err != nil {
198 errs = append(errs, err)
199 }
200 g.leaseLock = nil
201 }
202 if g.legacyLeaseLock != nil {
203 if err := g.legacyLeaseLock.RemoveAndUnlock(); err != nil {
204 errs = append(errs, err)
205 }
206 g.legacyLeaseLock = nil
207 }
208 g.restoreOwner = 0
209 return errors.Join(errs...)
210 }
211
211 lines GO