| 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 |