返回 DeepSeek-Reasonix
session_writer.go
根目录 / internal / agent / session_writer.go
1 package agent
2
3 import (
4 "fmt"
5 "strings"
6 "sync"
7 )
8
9 // SessionWriter is the cross-process write identity for one session file:
10 // lease, owner metadata, generation, save serialization, and event-log baseline.
11 // Production saves go through a SessionWriter; one-shot callers use
12 // SaveWithEphemeralWriter.
13 type SessionWriter struct {
14 lease *SessionLease
15 info SessionLeaseInfo
16
17 // saveMu serializes save cycles issued through this writer. It is held
18 // from authority BeginSave until the save's release runs.
19 saveMu sync.Mutex
20
21 mu sync.Mutex
22 // baseline is the event-log state this writer last persisted or adopted
23 // for baselinePath. A zero revisionKnown marks "not yet learned".
24 baselinePath string
25 baselineRev int64
26 baselineDigest string
27 revisionKnown bool
28 logTail int64
29 }
30
31 // AcquireSessionWriter takes path's session lease and returns the writer that
32 // owns it. It fails with *SessionLeaseError when another runtime holds the
33 // lease.
34 func AcquireSessionWriter(path string) (*SessionWriter, error) {
35 lease, err := TryAcquireSessionLease(path)
36 if err != nil {
37 return nil, err
38 }
39 return lease.Writer(), nil
40 }
41
42 // WriterForSessionLease returns the writer facade for an already-held lease
43 // (keeper and desktop-tab handoff paths keep their own acquire/rebind
44 // choreography). The facade is cached on the lease so rebinds share one
45 // save-serialization domain.
46 func WriterForSessionLease(lease *SessionLease) (*SessionWriter, error) {
47 if lease == nil {
48 return nil, fmt.Errorf("session lease is nil")
49 }
50 return lease.Writer(), nil
51 }
52
53 // Lease returns the underlying lease. Callers must not Release it; release
54 // the writer instead.
55 func (w *SessionWriter) Lease() *SessionLease {
56 if w == nil {
57 return nil
58 }
59 return w.lease
60 }
61
62 // Info returns the writer identity published with the lease.
63 func (w *SessionWriter) Info() SessionLeaseInfo {
64 if w == nil {
65 return SessionLeaseInfo{}
66 }
67 return w.info
68 }
69
70 // WriterID returns this writer's stable identity string.
71 func (w *SessionWriter) WriterID() string {
72 if w == nil {
73 return ""
74 }
75 return w.info.WriterID
76 }
77
78 // PID returns the holding process id.
79 func (w *SessionWriter) PID() int {
80 if w == nil {
81 return 0
82 }
83 return w.info.PID
84 }
85
86 // Hostname returns the holding machine name, when known.
87 func (w *SessionWriter) Hostname() string {
88 if w == nil {
89 return ""
90 }
91 return w.info.Hostname
92 }
93
94 // Path returns the canonical session path this writer owns.
95 func (w *SessionWriter) Path() string {
96 if w == nil {
97 return ""
98 }
99 return w.lease.Path()
100 }
101
102 // Release drops the lease and invalidates every authority minted through
103 // this writer. It waits for in-flight authority-guarded saves first.
104 func (w *SessionWriter) Release() {
105 if w == nil {
106 return
107 }
108 w.lease.Release()
109 }
110
111 // IssueWriteAuthority mints a generation-bound authority for generation and
112 // attaches it to this writer, so saves guarded by the authority serialize
113 // through the writer and update its baseline.
114 func (w *SessionWriter) IssueWriteAuthority(generation uint64) (*SessionWriteAuthority, error) {
115 if w == nil {
116 return nil, ErrSessionWriteAuthorityMissing
117 }
118 auth, err := w.lease.IssueWriteAuthority(generation)
119 if err != nil {
120 return nil, err
121 }
122 auth.writer = w
123 return auth, nil
124 }
125
126 // Bind issues a generation-bound authority from this writer and binds it to
127 // sess, putting sess on the production fail-closed save path. Controllers and
128 // keepers call this after acquiring the writer.
129 func (w *SessionWriter) Bind(sess *Session, generation uint64) error {
130 if w == nil {
131 return ErrSessionWriteAuthorityMissing
132 }
133 if sess == nil {
134 return fmt.Errorf("bind session writer: session is nil")
135 }
136 sess.RequireWriteAuthority()
137 auth, err := w.IssueWriteAuthority(generation)
138 if err != nil {
139 sess.ClearWriteAuthority()
140 return err
141 }
142 sess.BindWriteAuthority(auth)
143 sess.syncWriterBaseline(w.lease.accessPath)
144 return nil
145 }
146
147 // Writer returns the writer this authority was minted through, if any.
148 // Authorities minted directly from a lease (legacy paths) have none.
149 func (a *SessionWriteAuthority) Writer() *SessionWriter {
150 if a == nil {
151 return nil
152 }
153 return a.writer
154 }
155
156 // RecordBaseline stores the event-log state a completed save left behind.
157 // Only the writer covering path records; other paths are recorded on the
158 // writer that owns them (a session moved by recovery rebinds writers).
159 func (w *SessionWriter) RecordBaseline(path string, revision int64, digest string, known bool, logTail int64) {
160 if w == nil {
161 return
162 }
163 w.mu.Lock()
164 defer w.mu.Unlock()
165 w.baselinePath = canonicalSessionSavePath(path)
166 w.baselineRev = revision
167 w.baselineDigest = digest
168 w.revisionKnown = known
169 w.logTail = logTail
170 }
171
172 // WriterBaseline is a snapshot of the writer's event-log baseline.
173 type WriterBaseline struct {
174 Path string
175 Revision int64
176 ContentDigest string
177 RevisionKnown bool
178 LogTail int64
179 }
180
181 // Baseline returns the recorded baseline when the writer covers path.
182 func (w *SessionWriter) Baseline(path string) (WriterBaseline, bool) {
183 if w == nil {
184 return WriterBaseline{}, false
185 }
186 canonical := canonicalSessionSavePath(path)
187 w.mu.Lock()
188 defer w.mu.Unlock()
189 if w.baselinePath != canonical {
190 return WriterBaseline{}, false
191 }
192 return WriterBaseline{
193 Path: w.baselinePath,
194 Revision: w.baselineRev,
195 ContentDigest: w.baselineDigest,
196 RevisionKnown: w.revisionKnown,
197 LogTail: w.logTail,
198 }, true
199 }
200
201 // SaveWithEphemeralWriter acquires path's session lease, saves, then releases.
202 // It leaves any existing authority binding untouched so fork copies stay adoptable.
203 func (s *Session) SaveWithEphemeralWriter(path string, save func(target string) error) error {
204 if strings.TrimSpace(path) == "" {
205 return fmt.Errorf("empty session path")
206 }
207 if save == nil {
208 save = func(target string) error { return s.SaveSnapshot(target) }
209 }
210 lease, err := TryAcquireSessionLease(path)
211 if err != nil {
212 return err
213 }
214 defer lease.Release()
215 return save(path)
216 }
217
217 lines GO