返回 DeepSeek-Reasonix
session_removal.go
根目录 / internal / agent / session_removal.go
1 package agent
2
3 import (
4 "errors"
5 "os"
6
7 "reasonix/internal/store"
8 )
9
10 // SessionRemovalGuard holds a session's save lock and lease lock for the
11 // duration of a destructive operation (trash, purge, permanent delete). While
12 // held, no other runtime can acquire the session lease and no saver can write
13 // the transcript, so artifacts can be moved or deleted without racing a live
14 // owner; the lock files themselves are then deleted atomically with the
15 // release (unlink-under-flock on Unix, delete-disposition on Windows), so a
16 // later acquirer can never lock an inode that survived the deletion.
17 //
18 // This closes the probe-then-delete window: a one-shot busy check followed by
19 // a plain RemoveAll lets another process acquire the lease between the two
20 // steps and then loses its lock file, breaking cross-process mutual exclusion.
21 type SessionRemovalGuard struct {
22 path string
23 saveLock *sessionLockFile
24 leaseLock *sessionLockFile
25 }
26
27 // TryAcquireSessionRemovalGuard takes both locks without blocking. A live
28 // holder of either — including a lease held elsewhere in this process —
29 // surfaces as ErrSessionLeaseHeld so callers report the session as busy
30 // instead of deleting files out from under a running owner.
31 func TryAcquireSessionRemovalGuard(path string) (*SessionRemovalGuard, error) {
32 path = canonicalSessionSavePath(path)
33 if sessionLeaseHeldLocally(path) {
34 info, _ := LoadSessionLeaseInfo(path)
35 return nil, &SessionLeaseError{Path: path, Info: info}
36 }
37 leaseLock, err := tryTakeSessionLockFile(store.SessionLeaseLock(path))
38 if err != nil {
39 if errors.Is(err, ErrSessionFileLockHeld) {
40 info, _ := LoadSessionLeaseInfo(path)
41 return nil, &SessionLeaseError{Path: path, Info: info}
42 }
43 return nil, err
44 }
45 saveLock, err := tryTakeSessionLockFile(store.SessionLockFile(path))
46 if err != nil {
47 leaseLock.Unlock()
48 if errors.Is(err, ErrSessionFileLockHeld) {
49 // A save is in flight; deleting mid-write would race it.
50 return nil, &SessionLeaseError{Path: path}
51 }
52 return nil, err
53 }
54 return &SessionRemovalGuard{path: path, saveLock: saveLock, leaseLock: leaseLock}, nil
55 }
56
57 // Release ends the guard without deleting the lock files — the abort path
58 // when the destructive operation did not happen. Safe to call after
59 // RemoveSidecarsAndRelease (it becomes a no-op).
60 func (g *SessionRemovalGuard) Release() {
61 if g == nil {
62 return
63 }
64 if g.saveLock != nil {
65 g.saveLock.Unlock()
66 g.saveLock = nil
67 }
68 if g.leaseLock != nil {
69 g.leaseLock.Unlock()
70 g.leaseLock = nil
71 }
72 }
73
74 // RemoveSidecarsAndRelease deletes the lease info and both lock files
75 // atomically with the release, then ends the guard. The lease info goes first,
76 // while the lease lock is still held, so no probe can adopt it mid-removal.
77 func (g *SessionRemovalGuard) RemoveSidecarsAndRelease() error {
78 if g == nil {
79 return nil
80 }
81 var errs []error
82 if err := os.Remove(store.SessionLeaseInfo(g.path)); err != nil && !os.IsNotExist(err) {
83 errs = append(errs, err)
84 }
85 if g.saveLock != nil {
86 if err := g.saveLock.RemoveAndUnlock(); err != nil {
87 errs = append(errs, err)
88 }
89 g.saveLock = nil
90 }
91 if g.leaseLock != nil {
92 if err := g.leaseLock.RemoveAndUnlock(); err != nil {
93 errs = append(errs, err)
94 }
95 g.leaseLock = nil
96 }
97 return errors.Join(errs...)
98 }
99
99 lines GO