返回 DeepSeek-Reasonix
service_ownership.go
根目录 / internal / session / service_ownership.go
1 package session
2
3 import (
4 "context"
5 "errors"
6 "fmt"
7 "sync"
8 "time"
9 )
10
11 // HostID returns the immutable host namespace used to validate SessionRef.
12 func (s *Service) HostID() string {
13 if s == nil {
14 return ""
15 }
16 return s.hostID
17 }
18
19 type prepareRuntime struct{ done chan struct{} }
20
21 // ClientBinding grants a caller access to one exact published runtime without
22 // granting authority to close the host-owned writer. Release only detaches this
23 // client; the host retires an idle runtime after its last binding disappears.
24 type ClientBinding struct {
25 service *Service
26 runtime *Runtime
27 once sync.Once
28 err error
29 }
30
31 func (b *ClientBinding) Runtime() *Runtime {
32 if b == nil {
33 return nil
34 }
35 return b.runtime
36 }
37
38 func (b *ClientBinding) Release(ctx context.Context) error {
39 if b == nil || b.service == nil || b.runtime == nil {
40 return nil
41 }
42 b.once.Do(func() { b.err = b.service.releaseBinding(ctx, b.runtime) })
43 return b.err
44 }
45
46 // PreparedRuntime owns a write handle that has been fully opened but is not
47 // yet visible through the host registry. Callers may build projections, seed
48 // initial events, and flush before atomically publishing the exact instance.
49 // A candidate must be either published or discarded.
50 type PreparedRuntime struct {
51 service *Service
52 runtime *Runtime
53 instance string
54
55 mu sync.Mutex
56 published bool
57 discarded bool
58 }
59
60 func (p *PreparedRuntime) Runtime() *Runtime {
61 if p == nil {
62 return nil
63 }
64 return p.runtime
65 }
66
67 func NewService(hostID string, persistence SessionPersistence) (*Service, error) {
68 if hostID == "" || persistence == nil {
69 return nil, errors.New("session: host id and persistence are required")
70 }
71 service := &Service{
72 hostID: hostID, persistence: persistence,
73 active: map[SessionRef]*Runtime{}, closed: map[SessionRef]error{}, preparing: map[SessionRef]*prepareRuntime{},
74 bindings: map[*Runtime]int{}, retiring: map[*Runtime]chan struct{}{}, retireIdle: map[*Runtime]bool{},
75 idleTimers: map[*Runtime]*time.Timer{}, idleWeight: map[*Runtime]int64{}, idleOrder: map[*Runtime]uint64{},
76 idleBudget: 256 << 20, idleTTL: 60 * time.Second,
77 }
78 service.query = newQuery(hostID, persistence, service)
79 return service, nil
80 }
81
82 func (s *Service) Create(ctx context.Context, options CreateOptions) (*Runtime, error) {
83 prepared, err := s.PrepareCreate(ctx, options)
84 if err != nil {
85 return nil, err
86 }
87 owner, err := s.Publish(prepared)
88 if err != nil {
89 _ = s.Discard(context.Background(), prepared)
90 return nil, err
91 }
92 return owner.Runtime(), nil
93 }
94
95 // PrepareCreate reserves the immutable session identity and its writer lease
96 // without publishing an attachable runtime. This is the DSH prepare phase:
97 // host/controller state remains untouched until Publish succeeds.
98 func (s *Service) PrepareCreate(ctx context.Context, options CreateOptions) (*PreparedRuntime, error) {
99 if err := ctx.Err(); err != nil {
100 return nil, err
101 }
102 session, err := s.persistence.Create(options)
103 if err != nil {
104 return nil, err
105 }
106 session.externalizeDurableHistory()
107 ref := SessionRef{HostID: s.hostID, SessionID: session.ID()}
108 candidate, err := newRuntime(ref, session)
109 if err != nil {
110 return nil, errors.Join(err, session.Close(context.Background()))
111 }
112 candidate.owner = s
113 return &PreparedRuntime{service: s, runtime: candidate, instance: randomID()}, nil
114 }
115
116 // Publish makes the exact prepared runtime visible and returns the host
117 // authority over that instance. It never replaces an existing instance with the
118 // same identity; the caller must resolve that ownership conflict explicitly.
119 //
120 // Only a RuntimeOwner can terminate a published runtime. Clients attach through
121 // ClientBinding and can only detach themselves.
122 func (s *Service) Publish(prepared *PreparedRuntime) (*RuntimeOwner, error) {
123 if prepared == nil || prepared.service != s || prepared.runtime == nil {
124 return nil, errors.New("session: invalid prepared runtime")
125 }
126 prepared.mu.Lock()
127 defer prepared.mu.Unlock()
128 if prepared.discarded {
129 return nil, errors.New("session: prepared runtime was discarded")
130 }
131 candidate := prepared.runtime
132 if prepared.published {
133 return &RuntimeOwner{service: s, runtime: candidate, instance: prepared.instance}, nil
134 }
135 ref := candidate.ref
136 s.mu.Lock()
137 if current := s.active[ref]; current != nil {
138 s.mu.Unlock()
139 return nil, fmt.Errorf("%w: %s", ErrSessionExists, ref.SessionID)
140 }
141 delete(s.closed, ref)
142 candidate.instance = prepared.instance
143 s.active[ref] = candidate
144 s.revision.Add(1)
145 s.mu.Unlock()
146 prepared.published = true
147 return &RuntimeOwner{service: s, runtime: candidate, instance: prepared.instance}, nil
148 }
149
150 // RuntimeOwner is the host-side authority over one exact published instance.
151 // Controller and client code must never hold one: they use ClientBinding so a
152 // failed attach can only undo its own bind, never dispose a shared runtime.
153 type RuntimeOwner struct {
154 service *Service
155 runtime *Runtime
156 instance string
157 }
158
159 // Runtime exposes the owned instance for host preparation work.
160 func (o *RuntimeOwner) Runtime() *Runtime {
161 if o == nil {
162 return nil
163 }
164 return o.runtime
165 }
166
167 // Bind attaches a client to this instance without transferring ownership.
168 func (o *RuntimeOwner) Bind() (*ClientBinding, error) {
169 if o == nil || o.service == nil || o.runtime == nil {
170 return nil, ErrSessionNotRunning
171 }
172 return o.service.Bind(o.runtime)
173 }
174
175 // Close terminates this exact instance. It refuses while any client is still
176 // bound, and it can never affect a same-ID successor published later.
177 func (o *RuntimeOwner) Close(ctx context.Context) error {
178 if o == nil || o.service == nil || o.runtime == nil {
179 return ErrSessionNotRunning
180 }
181 return o.service.closeOwned(ctx, o.runtime, o.instance)
182 }
183
184 // Owner returns the host authority for the exact active instance. It fails for
185 // an unknown or superseded instance, so a delayed caller can never acquire
186 // authority over a same-ID successor. It is used by the flows that publish a
187 // brand-new identity in the same call; attaching to an existing session must go
188 // through Open and its ClientBinding instead.
189 func (s *Service) Owner(runtime *Runtime) (*RuntimeOwner, error) {
190 if runtime == nil || runtime.owner != s {
191 return nil, ErrSessionNotRunning
192 }
193 s.mu.Lock()
194 defer s.mu.Unlock()
195 if runtime.instance == "" || s.active[runtime.ref] != runtime {
196 return nil, ErrSessionNotRunning
197 }
198 return &RuntimeOwner{service: s, runtime: runtime, instance: runtime.instance}, nil
199 }
200
201 // Discard closes an unpublished candidate and releases its writer lease.
202 // Published runtimes must be closed through Service.Close so exact-instance
203 // unregistering cannot be bypassed.
204 func (s *Service) Discard(ctx context.Context, prepared *PreparedRuntime) error {
205 if prepared == nil || prepared.service != s || prepared.runtime == nil {
206 return nil
207 }
208 prepared.mu.Lock()
209 defer prepared.mu.Unlock()
210 if prepared.published {
211 return errors.New("session: published runtime cannot be discarded")
212 }
213 if prepared.discarded {
214 return prepared.runtime.close(ctx)
215 }
216 prepared.discarded = true
217 return prepared.runtime.close(ctx)
218 }
219
220 // Open attaches a client to an existing session and returns a binding that can
221 // only detach this client. It never hands out authority to close a runtime that
222 // another client may already be using.
223 func (s *Service) Open(ctx context.Context, ref SessionRef) (*ClientBinding, error) {
224 return s.openBinding(ctx, ref)
225 }
226
227 // openRuntime resolves the exact published instance. It is internal so only the
228 // service's own prepare/publish flows can reach a runtime without a grant.
229 func (s *Service) openRuntime(ctx context.Context, ref SessionRef) (*Runtime, error) {
230 if err := ref.validate(s.hostID); err != nil {
231 return nil, err
232 }
233 for {
234 s.mu.Lock()
235 if current := s.active[ref]; current != nil {
236 s.mu.Unlock()
237 return current, nil
238 }
239 if pending := s.preparing[ref]; pending != nil {
240 done := pending.done
241 s.mu.Unlock()
242 select {
243 case <-done:
244 continue
245 case <-ctx.Done():
246 return nil, ctx.Err()
247 }
248 }
249 pending := &prepareRuntime{done: make(chan struct{})}
250 s.preparing[ref] = pending
251 s.mu.Unlock()
252 break
253 }
254
255 session, err := s.persistence.Open(ref.SessionID, ReadWrite)
256 if err != nil {
257 s.finishPrepare(ref)
258 return nil, err
259 }
260 if _, _, recoverErr := session.RecoverInterrupted(ctx); recoverErr != nil {
261 _ = session.Close(context.Background())
262 s.finishPrepare(ref)
263 return nil, fmt.Errorf("session: close interrupted runtime: %w", recoverErr)
264 }
265 session.externalizeDurableHistory()
266 candidate, err := newRuntime(ref, session)
267 if err != nil {
268 closeErr := session.Close(context.Background())
269 s.finishPrepare(ref)
270 return nil, errors.Join(err, closeErr)
271 }
272 candidate.owner = s
273 s.mu.Lock()
274 pending := s.preparing[ref]
275 delete(s.preparing, ref)
276 if current := s.active[ref]; current != nil {
277 if pending != nil {
278 close(pending.done)
279 }
280 s.mu.Unlock()
281 _ = candidate.close(context.Background())
282 return current, nil
283 }
284 delete(s.closed, ref)
285 // Stamp the publish grant so Owner can hand the host authority over exactly
286 // this instance and no same-ID successor.
287 candidate.instance = randomID()
288 s.active[ref] = candidate
289 s.revision.Add(1)
290 if pending != nil {
291 close(pending.done)
292 }
293 s.mu.Unlock()
294 return candidate, nil
295 }
296
297 func (s *Service) finishPrepare(ref SessionRef) {
298 s.mu.Lock()
299 if pending := s.preparing[ref]; pending != nil {
300 delete(s.preparing, ref)
301 close(pending.done)
302 }
303 s.mu.Unlock()
304 }
305
306 func (s *Service) Runtime(ref SessionRef) (*Runtime, bool) {
307 if ref.validate(s.hostID) != nil {
308 return nil, false
309 }
310 s.mu.Lock()
311 runtime := s.active[ref]
312 s.mu.Unlock()
313 return runtime, runtime != nil
314 }
315
316 // Bind attaches a client to an exact published runtime. The returned binding
317 // can only release its own reference; it cannot dispose the shared runtime.
318 func (s *Service) Bind(runtime *Runtime) (*ClientBinding, error) {
319 if runtime == nil || runtime.owner != s {
320 return nil, ErrSessionNotRunning
321 }
322 s.mu.Lock()
323 defer s.mu.Unlock()
324 if s.retiring[runtime] != nil {
325 return nil, ErrRuntimeRetiring
326 }
327 if s.active[runtime.ref] != runtime {
328 return nil, ErrSessionNotRunning
329 }
330 s.bindings[runtime]++
331 delete(s.retireIdle, runtime)
332 if timer := s.idleTimers[runtime]; timer != nil {
333 s.removeIdleCacheLocked(runtime, true)
334 }
335 return &ClientBinding{service: s, runtime: runtime}, nil
336 }
337
338 // OpenBinding opens or reuses a runtime and attaches a client capability.
339 func (s *Service) openBinding(ctx context.Context, ref SessionRef) (*ClientBinding, error) {
340 for {
341 runtime, err := s.openRuntime(ctx, ref)
342 if err != nil {
343 return nil, err
344 }
345 binding, err := s.Bind(runtime)
346 if err == nil {
347 return binding, err
348 }
349 if errors.Is(err, ErrRuntimeRetiring) {
350 s.mu.Lock()
351 done := s.retiring[runtime]
352 s.mu.Unlock()
353 if done != nil {
354 select {
355 case <-done:
356 continue
357 case <-ctx.Done():
358 return nil, ctx.Err()
359 }
360 }
361 continue
362 }
363 return nil, err
364 }
365 }
366
367 func (s *Service) releaseBinding(ctx context.Context, runtime *Runtime) error {
368 s.mu.Lock()
369 count := s.bindings[runtime]
370 if count <= 1 {
371 delete(s.bindings, runtime)
372 s.retireIdle[runtime] = true
373 } else {
374 s.bindings[runtime] = count - 1
375 }
376 s.mu.Unlock()
377 if count <= 1 {
378 var flushErr error
379 if !runtime.executionBusy() {
380 _, flushErr = runtime.session.Flush(ctx)
381 }
382 s.scheduleIdleRetirement(runtime)
383 return flushErr
384 }
385 return nil
386 }
387
388 func (s *Service) closeIfUnbound(ctx context.Context, runtime *Runtime) error {
389 if runtime == nil {
390 return nil
391 }
392 s.scheduleIdleRetirement(runtime)
393 return nil
394 }
395
396 func (s *Service) scheduleIdleRetirement(runtime *Runtime) {
397 if runtime == nil {
398 return
399 }
400 weight := runtime.session.cacheWeight()
401 s.mu.Lock()
402 if s.active[runtime.ref] != runtime || s.bindings[runtime] != 0 || !s.retireIdle[runtime] {
403 s.mu.Unlock()
404 return
405 }
406 if runtime.executionBusy() {
407 s.mu.Unlock()
408 return
409 }
410 if s.idleTimers[runtime] != nil {
411 s.mu.Unlock()
412 return
413 }
414 ttl := s.idleTTL
415 s.idleClock++
416 s.idleOrder[runtime] = s.idleClock
417 s.idleWeight[runtime] = weight
418 s.idleUsed += weight
419 if ttl > 0 {
420 s.idleTimers[runtime] = time.AfterFunc(ttl, func() {
421 _ = s.retireIfUnbound(context.Background(), runtime)
422 })
423 }
424 var victims []*Runtime
425 for s.idleBudget >= 0 && s.idleUsed > s.idleBudget && len(s.idleOrder) > 0 {
426 var oldest *Runtime
427 var order uint64
428 for candidate, candidateOrder := range s.idleOrder {
429 if oldest == nil || candidateOrder < order {
430 oldest, order = candidate, candidateOrder
431 }
432 }
433 if oldest == nil {
434 break
435 }
436 s.removeIdleCacheLocked(oldest, true)
437 victims = append(victims, oldest)
438 }
439 s.mu.Unlock()
440 for _, victim := range victims {
441 _ = s.retireIfUnbound(context.Background(), victim)
442 }
443 if ttl <= 0 && len(victims) == 0 {
444 _ = s.retireIfUnbound(context.Background(), runtime)
445 }
446 }
447
448 func (s *Service) removeIdleCacheLocked(runtime *Runtime, stop bool) {
449 if timer := s.idleTimers[runtime]; timer != nil && stop {
450 timer.Stop()
451 }
452 delete(s.idleTimers, runtime)
453 s.idleUsed -= s.idleWeight[runtime]
454 if s.idleUsed < 0 {
455 s.idleUsed = 0
456 }
457 delete(s.idleWeight, runtime)
458 delete(s.idleOrder, runtime)
459 }
460
461 func (s *Service) retireIfUnbound(ctx context.Context, runtime *Runtime) error {
462 s.mu.Lock()
463 s.removeIdleCacheLocked(runtime, false)
464 if s.active[runtime.ref] != runtime || s.bindings[runtime] != 0 || !s.retireIdle[runtime] || runtime.executionBusy() {
465 s.mu.Unlock()
466 return nil
467 }
468 if done := s.retiring[runtime]; done != nil {
469 s.mu.Unlock()
470 select {
471 case <-done:
472 return nil
473 case <-ctx.Done():
474 return ctx.Err()
475 }
476 }
477 done := make(chan struct{})
478 s.retiring[runtime] = done
479 s.mu.Unlock()
480
481 err := runtime.close(ctx)
482 s.mu.Lock()
483 delete(s.retiring, runtime)
484 if !errors.Is(err, ErrRuntimeBusy) && s.active[runtime.ref] == runtime && s.bindings[runtime] == 0 {
485 delete(s.active, runtime.ref)
486 delete(s.retireIdle, runtime)
487 s.removeIdleCacheLocked(runtime, true)
488 s.closed[runtime.ref] = err
489 s.revision.Add(1)
490 }
491 close(done)
492 s.mu.Unlock()
493 return err
494 }
495
495 lines GO