| 1 | package session |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | ) |
| 6 | |
| 7 | // ExecutionControl is the session-scoped turn-loop bound to a Runtime. |
| 8 | // The loop is the execution authority; Runtime only forwards Cancel and |
| 9 | // caches the last NoteExecution from the bound generation. |
| 10 | type ExecutionControl interface { |
| 11 | Snapshot() RuntimeSnapshot |
| 12 | Cancel() bool |
| 13 | } |
| 14 | |
| 15 | type executionBinding struct { |
| 16 | generation uint64 |
| 17 | control ExecutionControl |
| 18 | } |
| 19 | |
| 20 | // BindExecution installs the first generation-scoped turn-loop. Replacing an |
| 21 | // existing owner is always explicit through ReplaceExecution so constructing a |
| 22 | // candidate controller cannot steal Stop or mutation authority. |
| 23 | func (r *Runtime) BindExecution(control ExecutionControl) uint64 { |
| 24 | if r == nil || control == nil { |
| 25 | return 0 |
| 26 | } |
| 27 | r.mu.Lock() |
| 28 | defer r.mu.Unlock() |
| 29 | if r.phase == RuntimeClosed || r.phase == RuntimeRecoveryRequired || r.execution.Load() != nil { |
| 30 | return 0 |
| 31 | } |
| 32 | gen := r.bindGen.Add(1) |
| 33 | r.execution.Store(&executionBinding{generation: gen, control: control}) |
| 34 | r.revision.Add(1) |
| 35 | return gen |
| 36 | } |
| 37 | |
| 38 | // ReplaceExecution transfers an idle runtime from the exact expected |
| 39 | // generation to a replacement turn-loop. The lock acquisition is the |
| 40 | // linearization point: generations are allocated only after ownership and |
| 41 | // phase validation, so they can never be published out of order. |
| 42 | func (r *Runtime) ReplaceExecution(expectedGeneration uint64, control ExecutionControl) uint64 { |
| 43 | if r == nil || expectedGeneration == 0 || control == nil { |
| 44 | return 0 |
| 45 | } |
| 46 | r.mu.Lock() |
| 47 | defer r.mu.Unlock() |
| 48 | cur := r.execution.Load() |
| 49 | if cur == nil || cur.generation != expectedGeneration || r.phase != RuntimeIdle { |
| 50 | return 0 |
| 51 | } |
| 52 | gen := r.bindGen.Add(1) |
| 53 | r.execution.Store(&executionBinding{generation: gen, control: control}) |
| 54 | r.revision.Add(1) |
| 55 | return gen |
| 56 | } |
| 57 | |
| 58 | // ReplaceExecutionAndCommit atomically accepts a candidate's prevalidated |
| 59 | // session mutation and transfers an idle Runtime to that candidate. A failed |
| 60 | // commit leaves the outgoing execution owner untouched. |
| 61 | func (r *Runtime) ReplaceExecutionAndCommit(expectedGeneration uint64, control ExecutionControl, prepared PreparedBatch) (uint64, Commit, error) { |
| 62 | if r == nil || expectedGeneration == 0 || control == nil { |
| 63 | prepared.Release() |
| 64 | return 0, Commit{}, ErrStaleExecution |
| 65 | } |
| 66 | r.mu.Lock() |
| 67 | defer r.mu.Unlock() |
| 68 | cur := r.execution.Load() |
| 69 | if cur == nil || cur.generation != expectedGeneration || r.phase != RuntimeIdle { |
| 70 | prepared.Release() |
| 71 | return 0, Commit{}, ErrStaleExecution |
| 72 | } |
| 73 | commit, err := r.session.CommitPrepared(prepared) |
| 74 | if err != nil { |
| 75 | return 0, Commit{}, err |
| 76 | } |
| 77 | gen := r.bindGen.Add(1) |
| 78 | r.execution.Store(&executionBinding{generation: gen, control: control}) |
| 79 | r.revision.Add(1) |
| 80 | return gen, commit, nil |
| 81 | } |
| 82 | |
| 83 | // OwnsExecution reports whether generation is the currently bound turn-loop. |
| 84 | // It is a snapshot only; mutations that require fencing must validate again |
| 85 | // while holding r.mu. |
| 86 | func (r *Runtime) OwnsExecution(generation uint64) bool { |
| 87 | if r == nil || generation == 0 { |
| 88 | return false |
| 89 | } |
| 90 | cur := r.execution.Load() |
| 91 | return cur != nil && cur.generation == generation |
| 92 | } |
| 93 | |
| 94 | // BeginExecution atomically admits a turn for the exact execution owner. A |
| 95 | // queued turn may move directly from finalizing to running; no observer sees a |
| 96 | // false idle boundary between turns. |
| 97 | func (r *Runtime) BeginExecution(generation uint64, activity string) bool { |
| 98 | if r == nil || generation == 0 { |
| 99 | return false |
| 100 | } |
| 101 | r.mu.Lock() |
| 102 | defer r.mu.Unlock() |
| 103 | cur := r.execution.Load() |
| 104 | if cur == nil || cur.generation != generation { |
| 105 | return false |
| 106 | } |
| 107 | if r.phase != RuntimeIdle && r.phase != RuntimeFinalizing { |
| 108 | return false |
| 109 | } |
| 110 | r.phase = RuntimeRunning |
| 111 | r.activity = activity |
| 112 | r.canceling.Store(false) |
| 113 | r.revision.Add(1) |
| 114 | return true |
| 115 | } |
| 116 | |
| 117 | // CommitPreparedForExecution accepts a prepared batch only while generation |
| 118 | // is the exact execution owner. Preparation remains outside the Runtime lock; |
| 119 | // the short ownership check and in-memory Session acceptance form one commit |
| 120 | // boundary so a controller cutover cannot race a stale writer into the log. |
| 121 | func (r *Runtime) CommitPreparedForExecution(generation uint64, prepared PreparedBatch) (Commit, error) { |
| 122 | if r == nil || generation == 0 { |
| 123 | prepared.Release() |
| 124 | return Commit{}, ErrStaleExecution |
| 125 | } |
| 126 | r.mu.Lock() |
| 127 | defer r.mu.Unlock() |
| 128 | cur := r.execution.Load() |
| 129 | if cur == nil || cur.generation != generation { |
| 130 | prepared.Release() |
| 131 | return Commit{}, ErrStaleExecution |
| 132 | } |
| 133 | return r.session.CommitPrepared(prepared) |
| 134 | } |
| 135 | |
| 136 | // CommitPreparedForTurn makes the owner/turn check and terminal acceptance one |
| 137 | // critical section. A prepared completion cannot close a successor's turn. |
| 138 | func (r *Runtime) CommitPreparedForTurn(generation uint64, turnID string, prepared PreparedBatch) (Commit, error) { |
| 139 | if r == nil || generation == 0 { |
| 140 | prepared.Release() |
| 141 | return Commit{}, ErrStaleExecution |
| 142 | } |
| 143 | r.mu.Lock() |
| 144 | defer r.mu.Unlock() |
| 145 | cur := r.execution.Load() |
| 146 | if cur == nil || cur.generation != generation || r.session.StateSnapshot().Projection.TurnID != turnID { |
| 147 | prepared.Release() |
| 148 | return Commit{}, ErrStaleExecution |
| 149 | } |
| 150 | return r.session.CommitPrepared(prepared) |
| 151 | } |
| 152 | |
| 153 | // UnbindExecution releases only the exact generation. A superseded controller |
| 154 | // cannot clear the replacement's control binding. |
| 155 | func (r *Runtime) UnbindExecution(generation uint64) { |
| 156 | if r == nil || generation == 0 { |
| 157 | return |
| 158 | } |
| 159 | r.mu.Lock() |
| 160 | defer r.mu.Unlock() |
| 161 | cur := r.execution.Load() |
| 162 | if cur == nil || cur.generation != generation || (r.phase.busy() && r.phase != RuntimeRecoveryRequired) { |
| 163 | return |
| 164 | } |
| 165 | r.execution.Store(nil) |
| 166 | r.revision.Add(1) |
| 167 | } |
| 168 | |
| 169 | // NoteExecution records a phase transition from the exact bound generation. |
| 170 | // Validation and mutation share r.mu so a replacement cannot land between |
| 171 | // them and let an older controller alter the new owner's phase. |
| 172 | func (r *Runtime) NoteExecution(generation uint64, phase RuntimePhase, activity string) { |
| 173 | if r == nil || generation == 0 { |
| 174 | return |
| 175 | } |
| 176 | r.mu.Lock() |
| 177 | cur := r.execution.Load() |
| 178 | if cur == nil || cur.generation != generation { |
| 179 | r.mu.Unlock() |
| 180 | return |
| 181 | } |
| 182 | if r.phase == RuntimeClosed { |
| 183 | r.mu.Unlock() |
| 184 | return |
| 185 | } |
| 186 | if r.phase == RuntimeRecoveryRequired && phase != RuntimeRecoveryRequired && phase != RuntimeClosed { |
| 187 | r.mu.Unlock() |
| 188 | return |
| 189 | } |
| 190 | if r.phase == phase && r.activity == activity { |
| 191 | r.mu.Unlock() |
| 192 | return |
| 193 | } |
| 194 | r.phase = phase |
| 195 | r.activity = activity |
| 196 | r.revision.Add(1) |
| 197 | if phase == RuntimeCancelling || phase == RuntimeRecoveryRequired { |
| 198 | r.canceling.Store(true) |
| 199 | } else { |
| 200 | r.canceling.Store(false) |
| 201 | } |
| 202 | owner := r.owner |
| 203 | r.mu.Unlock() |
| 204 | if phase == RuntimeIdle && owner != nil { |
| 205 | _ = owner.closeIfUnbound(context.Background(), r) |
| 206 | } |
| 207 | } |
| 208 | |
| 209 | func (r *Runtime) loadExecution() *executionBinding { |
| 210 | if r == nil { |
| 211 | return nil |
| 212 | } |
| 213 | return r.execution.Load() |
| 214 | } |
| 215 | |
| 216 | // Busy reports whether a phase still owns live execution or finalization |
| 217 | // work. Hosts use this shared definition so finalizing sessions cannot vanish |
| 218 | // from running lists before their terminal commit completes. |
| 219 | func (p RuntimePhase) Busy() bool { |
| 220 | switch p { |
| 221 | case RuntimeRunning, RuntimeCancelling, RuntimeFinalizing, RuntimeRecoveryRequired: |
| 222 | return true |
| 223 | default: |
| 224 | return false |
| 225 | } |
| 226 | } |
| 227 | |
| 228 | func (p RuntimePhase) busy() bool { return p.Busy() } |
| 229 | |
| 230 | func (r *Runtime) executionBusy() bool { |
| 231 | if r == nil { |
| 232 | return false |
| 233 | } |
| 234 | r.mu.Lock() |
| 235 | phase := r.phase |
| 236 | bound := r.execution.Load() != nil |
| 237 | r.mu.Unlock() |
| 238 | return phase.busy() && bound |
| 239 | } |
| 240 |