move_lifecycle.go RawHistoryFolder
1package daemon
2
3import (
4 "cmp"
5 "errors"
6 "slices"
7 "sync"
8)
9
10var errMoveLifecycleUnavailable = errors.New("move lifecycle is no longer available")
11
12// moveLifecycleReservation pins a unique, canonically ordered set of sessions
13// plus the daemon move gate. Release is idempotent so every abort path can use
14// defer without coordinating ownership with its caller.
15type moveLifecycleReservation struct {
16 daemon *Daemon
17 sessions []*session
18 once sync.Once
19}
20
21// reserveMoveLifecycles admits a move at the daemon gate before touching any
22// per-session lifecycle state. All session counts publish atomically while the
23// complete ordered teardown lock set is held.
24func (d *Daemon) reserveMoveLifecycles(source, destination *session) (*moveLifecycleReservation, error) {
25 if d == nil || source == nil || destination == nil {
26 return nil, errMoveLifecycleUnavailable
27 }
28
29 d.moveLifecycleMu.Lock()
30 if d.moveLifecycleClosing {
31 d.moveLifecycleMu.Unlock()
32 return nil, errMoveLifecycleUnavailable
33 }
34 d.mu.Lock()
35 closing := d.closing
36 purgeClosing := d.purgeAdmissionClosing
37 if !closing && !purgeClosing {
38 // A move transfers live state across the exact lifecycle set a KillAll
39 // purge captured, so it reserves the purge admission gate too: the purge
40 // drains this reservation before it snapshots, and a move arriving during
41 // the purge is rejected instead of racing the set.
42 d.purgeAdmissionActive++
43 }
44 d.mu.Unlock()
45 if closing {
46 d.moveLifecycleMu.Unlock()
47 return nil, errMoveLifecycleUnavailable
48 }
49 if purgeClosing {
50 // A transient KillAll purge owns the exact lifecycle set, so no move can
51 // reserve it. Unlike a shutdown this is retry state, so report the same
52 // typed admission outcome as the other command paths instead of a
53 // permanent "target gone" verdict.
54 d.moveLifecycleMu.Unlock()
55 return nil, errPurgeAdmissionClosed
56 }
57 d.moveLifecycleActive++
58 d.signalMoveLifecycleChangedLocked()
59 d.moveLifecycleMu.Unlock()
60 if d.afterMoveLifecycleGateBeforeTeardownLocks != nil {
61 d.afterMoveLifecycleGateBeforeTeardownLocks()
62 }
63
64 sessions := uniqueMoveSessions(source, destination)
65 for _, sess := range sessions {
66 sess.teardownMu.Lock()
67 }
68 for _, sess := range sessions {
69 if sess.teardownActive {
70 unlockMoveSessions(sessions)
71 d.moveLifecycleMu.Lock()
72 d.releaseMoveAdmissionLocked()
73 d.moveLifecycleMu.Unlock()
74 return nil, errMoveLifecycleUnavailable
75 }
76 }
77 for _, sess := range sessions {
78 sess.moveReservations++
79 sess.signalTeardownChangedLocked()
80 }
81 unlockMoveSessions(sessions)
82
83 return &moveLifecycleReservation{daemon: d, sessions: sessions}, nil
84}
85
86func uniqueMoveSessions(source, destination *session) []*session {
87 sessions := []*session{source}
88 if destination != source {
89 sessions = append(sessions, destination)
90 }
91 slices.SortFunc(sessions, func(left, right *session) int {
92 return cmp.Compare(left.id, right.id)
93 })
94 return sessions
95}
96
97func unlockMoveSessions(sessions []*session) {
98 for i := len(sessions) - 1; i >= 0; i-- {
99 sessions[i].teardownMu.Unlock()
100 }
101}
102
103func (r *moveLifecycleReservation) Release() {
104 if r == nil {
105 return
106 }
107 r.once.Do(func() {
108 r.daemon.moveLifecycleMu.Lock()
109 for _, sess := range r.sessions {
110 sess.teardownMu.Lock()
111 }
112 for _, sess := range r.sessions {
113 sess.moveReservations--
114 sess.signalTeardownChangedLocked()
115 }
116 unlockMoveSessions(r.sessions)
117 r.daemon.releaseMoveAdmissionLocked()
118 r.daemon.moveLifecycleMu.Unlock()
119 })
120}
121
122// closeMoveLifecycles rejects new moves, publishes daemon shutdown to ordinary
123// routing, then drains reservations without holding either daemon mutex. Pane
124// process lifetime is cancelled only after the final reservation releases.
125func (d *Daemon) closeMoveLifecycles() {
126 if d == nil {
127 return
128 }
129 d.moveLifecycleMu.Lock()
130 if !d.moveLifecycleClosing {
131 d.moveLifecycleClosing = true
132 d.signalMoveLifecycleChangedLocked()
133 }
134 d.moveLifecycleMu.Unlock()
135
136 // Publish ordinary shutdown admission separately: no move gate mutex is held
137 // while taking the architecture registry lock.
138 d.mu.Lock()
139 d.closing = true
140 d.mu.Unlock()
141
142 d.moveLifecycleMu.Lock()
143 for d.moveLifecycleActive != 0 {
144 changed := d.moveLifecycleChangeLocked()
145 d.moveLifecycleMu.Unlock()
146 <-changed
147 d.moveLifecycleMu.Lock()
148 }
149 cancel := d.paneProcessCancel
150 d.moveLifecycleMu.Unlock()
151 if cancel != nil {
152 cancel()
153 }
154}
155
156// releaseMoveAdmissionLocked drops one move's move-gate and KillAll purge-gate
157// reservations. Caller holds moveLifecycleMu; the purge gate is guarded by mu.
158func (d *Daemon) releaseMoveAdmissionLocked() {
159 d.moveLifecycleActive--
160 d.signalMoveLifecycleChangedLocked()
161 d.mu.Lock()
162 d.purgeAdmissionActive--
163 d.signalPurgeAdmissionChangedLocked()
164 d.mu.Unlock()
165}
166
167func (d *Daemon) moveLifecycleChangeLocked() chan struct{} {
168 if d.moveLifecycleChanged == nil {
169 d.moveLifecycleChanged = make(chan struct{})
170 }
171 return d.moveLifecycleChanged
172}
173
174func (d *Daemon) signalMoveLifecycleChangedLocked() {
175 close(d.moveLifecycleChangeLocked())
176 d.moveLifecycleChanged = make(chan struct{})
177}