返回 DeepSeek-Reasonix
execution.go
根目录 / internal / session / execution.go
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
240 lines GO