carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "slices"
8 "time"
9
10 "github.com/carlosframework/platform/internal/hostbox"
11 "github.com/carlosframework/platform/internal/hostclaim"
12 "github.com/carlosframework/platform/internal/hostguard"
13 "github.com/carlosframework/platform/internal/hostwrite"
14 "github.com/carlosframework/platform/internal/stopevidence"
15)
16
17// ErrClaimWritersStopped refuses new writer work on an attempt whose
18// writers StopWriters has stopped.
19var ErrClaimWritersStopped = errors.New("host claims: the session's writers are stopped")
20
21// The producers a writers-only stop keeps running: the session's
22// replicator, its generation marker publisher and region HA's files
23// publisher (claim §6.2a, L13). Every other producer is a writer: a role
24// launch, or a timer delivery ("timer-" ids, ClaimTimerDelivery).
25const (
26 ClaimProducerReplicator = "replicator"
27 ClaimProducerPublisher = "publisher"
28 ClaimProducerFiles = "files"
29)
30
31func claimRetainedProducer(id string) bool {
32 return id == ClaimProducerReplicator || id == ClaimProducerPublisher || id == ClaimProducerFiles
33}
34
35// ClaimWritersDrained is StopWriters' witness for one exact writer session
36// (claim Plan 11, R2). It is no stop evidence: nothing is fenced, signed
37// or published, and the claim is untouched. Root's writers-stopped
38// observation (R3) and root's full stop still follow.
39type ClaimWritersDrained struct {
40 // Target is the session's launch tuple: host, incarnation, stamp,
41 // writer session, box, enrollment, boot and controller.
42 Target hostguard.FenceTarget
43 Generation string
44 WriterBox string
45 // FenceClosed: the attempt and its producers admit no new writer.
46 FenceClosed bool
47 // LaunchJoined: every role launch producer was cancelled and joined.
48 LaunchJoined bool
49 // WorkDrained: every in-flight timer delivery was cancelled and joined.
50 WorkDrained bool
51 // SpawnsSettled: the launch journal holds no unsettled reservation.
52 SpawnsSettled bool
53 // CgroupEmpty: the session's containment was emptied and reads empty.
54 CgroupEmpty bool
55 // Cgroup is the containment's path relative to the cgroup root.
56 Cgroup string
57 DBPaths []string
58 // Drained and Retained are the producer ids joined and still running.
59 Drained, Retained []string
60 CheckedAt time.Time
61}
62
63// Valid reports a complete witness.
64func (d ClaimWritersDrained) Valid() bool {
65 t := d.Target
66 return t.Host != "" && t.Incarnation != "" && t.Stamp != 0 && t.Session != 0 && t.Box != "" && t.Enrollment != "" && t.BootID != "" && t.ControllerID != "" &&
67 d.FenceClosed && d.LaunchJoined && d.WorkDrained && d.SpawnsSettled && d.CgroupEmpty && d.Cgroup != "" && len(d.DBPaths) != 0 && !d.CheckedAt.IsZero()
68}
69
70// For checks the witness still names o's writer session: the same host,
71// holder, stamp, session, generation and writer box. Any other session,
72// including a later one on this box, gets nothing from it.
73func (d ClaimWritersDrained) For(o hostclaim.Observed) error {
74 if !d.Valid() || o.Claim == nil || o.Claim.Holder == nil {
75 return hostbox.ErrChanged
76 }
77 h := o.Claim.Holder
78 if o.Host != d.Target.Host || h.Incarnation != d.Target.Incarnation || h.Stamp != d.Target.Stamp || h.Session != d.Target.Session || h.Generation != d.Generation || h.WriterBox != d.WriterBox {
79 return hostbox.ErrChanged
80 }
81 return nil
82}
83
84// claimWritersJournal is the launch journal as StopWriters uses it.
85type claimWritersJournal interface {
86 CloseWriters(context.Context, hostguard.FenceTarget) error
87 Read(context.Context, hostguard.FenceTarget) (hostbox.LaunchInventory, error)
88 Settled(context.Context, hostbox.LaunchReservation) error
89}
90
91// claimWritersCgroup is the session's containment (the launch's parent).
92type claimWritersCgroup interface {
93 Populated() (bool, error)
94 KillAndEmpty(context.Context) error
95}
96
97// admitsWriters refuses once the attempt's writers are stopped. Every new
98// launch, reservation, restore and producer of the attempt asks it.
99func (a *claimWakeAttempt) admitsWriters() error {
100 a.mu.Lock()
101 defer a.mu.Unlock()
102 if a.writersClosed {
103 return ErrClaimWritersStopped
104 }
105 return nil
106}
107
108// StopWriters stops the attempt's writers and keeps its replicator (claim
109// Plan 11, R2). In order: close admission; cancel and join the role launch
110// and the schedule runner's in-flight deliveries for the host (both are
111// producers of the session); empty the session's containment; settle every
112// spawn reservation the journal still holds; check the containment reads
113// empty. The replicator, the generation publisher and the files publisher
114// keep running, and the lease stays held. It never releases the claim or
115// publishes evidence; root's stop does that. Once it has succeeded it
116// returns the same witness again without repeating any effect; after a
117// failure the next call resumes, each step being idempotent.
118func (a *claimWakeAttempt) StopWriters(ctx context.Context) (ClaimWritersDrained, error) {
119 if a == nil || ctx == nil {
120 return ClaimWritersDrained{}, hostwrite.ErrUnavailable
121 }
122 a.writersMu.Lock()
123 defer a.writersMu.Unlock()
124 a.mu.Lock()
125 if a.writers != nil {
126 done := a.writers.clone()
127 a.mu.Unlock()
128 return done, nil
129 }
130 observed, in, owner := a.observed, a.inputs, a.owner
131 launch := a.launch
132 if launch == nil {
133 launch = a.prelaunch
134 }
135 journal, group := a.writersJournal, a.writersCgroup
136 a.mu.Unlock()
137 if launch == nil || owner == nil || owner.Producers == nil || journal == nil || group == nil || in.DBPath == "" {
138 return ClaimWritersDrained{}, hostwrite.ErrUnavailable
139 }
140 target := launch.Target
141 if observed.Claim == nil || observed.Claim.Holder == nil {
142 return ClaimWritersDrained{}, hostbox.ErrChanged
143 }
144 h := observed.Claim.Holder
145 if observed.Host != target.Host || h.Incarnation != target.Incarnation || h.Stamp != target.Stamp || h.Session != target.Session || target.Session == 0 {
146 return ClaimWritersDrained{}, hostbox.ErrChanged
147 }
148 // 1. Admission: the attempt refuses new launches and restores, and the
149 // session's producers refuse every writer from here on.
150 a.mu.Lock()
151 a.writersClosed = true
152 a.mu.Unlock()
153 producers, err := owner.Producers(ctx, observed)
154 if err != nil {
155 return ClaimWritersDrained{}, err
156 }
157 if err := journal.CloseWriters(ctx, target); err != nil {
158 return ClaimWritersDrained{}, err
159 }
160 // 2. The role launch and the timer deliveries, joined.
161 drained, err := producers.StopWriters(ctx, target)
162 if err != nil {
163 return ClaimWritersDrained{}, err
164 }
165 // 3. The containment: whatever the roles left is killed and waited for.
166 if err := group.KillAndEmpty(ctx); err != nil {
167 return ClaimWritersDrained{}, err
168 }
169 // 4. Spawn reservations: with the containment empty, nothing reserved
170 // or started can still run. An intended role is root's to resolve.
171 inventory, err := journal.Read(ctx, target)
172 if err != nil {
173 return ClaimWritersDrained{}, err
174 }
175 for _, r := range inventory.Reservations {
176 switch r.State {
177 case "settled":
178 case "reserved", "started":
179 if err := journal.Settled(ctx, r); err != nil {
180 return ClaimWritersDrained{}, err
181 }
182 default:
183 return ClaimWritersDrained{}, fmt.Errorf("%w: reservation %s is %s", ErrClaimStopInventory, r.ID, r.State)
184 }
185 }
186 settled, err := journal.Read(ctx, target)
187 if err != nil {
188 return ClaimWritersDrained{}, err
189 }
190 for _, r := range settled.Reservations {
191 if r.State != "settled" {
192 return ClaimWritersDrained{}, fmt.Errorf("%w: reservation %s is %s", ErrClaimStopInventory, r.ID, r.State)
193 }
194 }
195 // 5. The containment reads empty after the journal is settled.
196 populated, err := group.Populated()
197 if err != nil {
198 return ClaimWritersDrained{}, err
199 }
200 if populated {
201 return ClaimWritersDrained{}, fmt.Errorf("%w: the session's cgroup is still populated", ErrClaimStopInventory)
202 }
203 var retained []string
204 for _, id := range producers.Running() {
205 if !claimRetainedProducer(id) {
206 return ClaimWritersDrained{}, fmt.Errorf("%w: producer %s still runs", ErrClaimStopIdentity, id)
207 }
208 retained = append(retained, id)
209 }
210 // Every producer but the retained ones was joined in step 2: the role
211 // launch's and the timer deliveries' ("timer-" ids) alike.
212 done := ClaimWritersDrained{Target: target, Generation: h.Generation, WriterBox: h.WriterBox, FenceClosed: true, LaunchJoined: true, WorkDrained: true, SpawnsSettled: true, CgroupEmpty: true,
213 Cgroup: launch.ParentPath, DBPaths: []string{in.DBPath}, Drained: slices.Clip(drained), Retained: retained, CheckedAt: time.Now().UTC()}
214 if !done.Valid() {
215 return ClaimWritersDrained{}, hostwrite.ErrUnavailable
216 }
217 a.mu.Lock()
218 cached := done.clone()
219 a.writers = &cached
220 a.mu.Unlock()
221 return done, nil
222}
223
224// recordReplicatorHints takes this process's own descriptors on the
225// database files immediately before and after open, and keeps those that
226// appeared: the replicator's descriptors (R3, the agent half). They are a
227// narrowing hint for root, never evidence.
228func recordReplicatorHints(ctx context.Context, procRoot string, pid int, db string, open func() error) ([]stopevidence.DescriptorHint, error) {
229 paths := []string{db, db + "-wal", db + "-shm"}
230 before, beforeErr := stopevidence.OwnDescriptors(ctx, procRoot, pid, paths)
231 if err := open(); err != nil {
232 return nil, err
233 }
234 if beforeErr != nil {
235 return nil, beforeErr
236 }
237 after, err := stopevidence.OwnDescriptors(ctx, procRoot, pid, paths)
238 if err != nil {
239 return nil, err
240 }
241 var hints []stopevidence.DescriptorHint
242 for _, d := range after {
243 if !slices.Contains(before, d) {
244 hints = append(hints, d)
245 }
246 }
247 return hints, nil
248}
249
250func (d ClaimWritersDrained) clone() ClaimWritersDrained {
251 d.DBPaths = slices.Clone(d.DBPaths)
252 d.Drained = slices.Clone(d.Drained)
253 d.Retained = slices.Clone(d.Retained)
254 return d
255}
256