package hostops import ( "context" "errors" "os" "slices" "github.com/carlosframework/platform/internal/hostclaim" ) func (c Controller) CensusPurge(ctx context.Context, account, app, id, appID string) error { j, tag, err := ReadPurgeJournal(ctx, c.Store, account, app, id, appID) if err != nil { return err } if j.Phase != "reserved" { return ErrPending } records, err := Records(ctx, c.Store, account, app) if err != nil { return err } for _, r := range records { obs, readErr := hostclaim.Read(ctx, c.Store, r.Holder.Host) if readErr != nil { return readErr } if obs.Absent { return ErrPending } if !matches(r, obs.Claim.Holder) { return ErrIdentity } } hosts, err := ListClaims(ctx, c.Store) if err != nil { return err } for _, host := range hosts { obs, err := hostclaim.Read(ctx, c.Store, host) if err != nil { return err } if obs.Absent || obs.Claim.Holder == nil { continue } h := obs.Claim.Holder if h.Account != account || h.App != app { continue } if obs.Claim.Op.Kind != hostclaim.KindServing || !obs.Claim.Touched.Complete { return ErrPending } if _, _, err = c.Authority.Inventory(ctx, obs); err != nil { return err } r, err := ReadRecord(ctx, c.Store, h.Key) if err != nil { return err } if !matches(r, h) { return ErrIdentity } progress := j.Hosts[host] holder := *h progress.Holder = &holder progress.RecordETag = r.ETag progress.ClaimETag = obs.ETag progress.ClaimGen = obs.Claim.Gen j.Hosts[host] = progress } j.Phase = "adopted" _, err = c.Control.SavePurgeJournal(ctx, j, tag) return err } func (c Controller) ClaimPurgeHosts(ctx context.Context, account, app, id, appID string) error { j, _, err := ReadPurgeJournal(ctx, c.Store, account, app, id, appID) if err != nil { return err } if j.Phase != "adopted" { return ErrPending } for host, p := range j.Hosts { if p.Holder == nil { continue } obs, err := hostclaim.Read(ctx, c.Store, host) if err != nil { return err } if obs.Absent || obs.Claim.Holder == nil || obs.Claim.Holder.Incarnation != p.Holder.Incarnation { return ErrIdentity } if obs.Claim.Op.Kind == hostclaim.KindPurging && obs.Claim.Op.ID == id { continue } if obs.ETag != p.ClaimETag || obs.Claim.Gen != p.ClaimGen { // A canceled unused hold invalidates its old observation. Re-save // only while this purge still owns the adopted phase and holder. fresh, tag, e := ReadPurgeJournal(ctx, c.Store, account, app, id, appID) if e != nil { return e } if fresh.Phase != "adopted" || obs.Claim.Op.Kind != hostclaim.KindServing || obs.Claim.Holder.Stamp != p.Holder.Stamp { return ErrPending } saved := fresh.Hosts[host] saved.ClaimETag = obs.ETag saved.ClaimGen = obs.Claim.Gen fresh.Hosts[host] = saved _, e = c.Control.SavePurgeJournal(ctx, fresh, tag) return e } authority, ok := c.Authority.(*ProductionAuthority) if !ok { return ErrContract } _, err = authority.StartPurgeClaim(ctx, obs, account, app, appID, id, j.RequestedBy) if err != nil { return err } } fresh, tag, err := ReadPurgeJournal(ctx, c.Store, account, app, id, appID) if err != nil { return err } if fresh.Phase != "adopted" { return ErrPending } fresh.Phase = "claimed" _, err = c.Control.SavePurgeJournal(ctx, fresh, tag) return err } func (c Controller) PurgeBarrier(ctx context.Context, account, app, id, appID string) error { j, _, err := ReadPurgeJournal(ctx, c.Store, account, app, id, appID) if err != nil { return err } if j.Phase != "claimed" { return ErrPending } for host, p := range j.Hosts { if p.Holder == nil { continue } obs, err := hostclaim.Read(ctx, c.Store, host) if err != nil { return err } if obs.Absent || obs.Claim.Holder == nil || obs.Claim.Holder.Incarnation != p.Holder.Incarnation || obs.Claim.Op.Kind != hostclaim.KindPurging || obs.Claim.Op.ID != id { return ErrPending } if err = c.Authority.FirstPhase(ctx, obs); err != nil { return err } } return nil } // ToParking records history before its chores finish. The old holder must // also be gone; FreeAfterPark is the only transition that removes it. func purgeHostFreed(claim hostclaim.Claim, p PurgeHost) bool { if p.Holder == nil || (claim.Holder != nil && claim.Holder.Incarnation == p.Holder.Incarnation) { return false } return slices.Contains(claim.RetiredIncarnations, p.Holder.Incarnation) || (p.FreeFromGen != 0 && claim.Gen > p.FreeFromGen) } func (c Controller) RetirePurgeHosts(ctx context.Context, account, app, id, appID string) error { j, _, err := ReadPurgeJournal(ctx, c.Store, account, app, id, appID) if err != nil { return err } if j.Phase != "torn-down" { return ErrPending } for host, p := range j.Hosts { if p.Holder == nil || p.Retired { continue } obs, err := hostclaim.Read(ctx, c.Store, host) if err != nil { return err } if obs.Absent { return ErrPending } if purgeHostFreed(*obs.Claim, p) { fresh, tag, err := ReadPurgeJournal(ctx, c.Store, account, app, id, appID) if err != nil { return err } saved := fresh.Hosts[host] saved.Retired = true fresh.Hosts[host] = saved if _, err = c.Control.SavePurgeJournal(ctx, fresh, tag); err != nil { return err } continue } if obs.Claim.Holder == nil || obs.Claim.Holder.Incarnation != p.Holder.Incarnation || obs.Claim.Op.ID != id { return ErrPending } if obs.Claim.Op.Kind == hostclaim.KindParking { if len(obs.Claim.Obligations) != 0 { return ErrPending } fresh, tag, err := ReadPurgeJournal(ctx, c.Store, account, app, id, appID) if err != nil { return err } saved := fresh.Hosts[host] saved.FreeFromGen = obs.Claim.Gen fresh.Hosts[host] = saved if _, err = c.Control.SavePurgeJournal(ctx, fresh, tag); err != nil { return err } if err = c.Free(ctx, host, id); err != nil { return err } return ErrPending } if obs.Claim.Op.Kind != hostclaim.KindPurging { return ErrPending } if obs.Claim.Op.Intent["etag"] == "" { r, err := ReadRecord(ctx, c.Store, p.Holder.Key) if errors.Is(err, os.ErrNotExist) { return ErrPending } if err != nil { return err } if !matches(r, p.Holder) { return ErrIdentity } // Save the exact observed record before the host intent, so a crash // cannot make later replay rediscover an unrelated version. fresh, tag, err := ReadPurgeJournal(ctx, c.Store, account, app, id, appID) if err != nil { return err } saved := fresh.Hosts[host] saved.RecordETag = r.ETag fresh.Hosts[host] = saved if _, err = c.Control.SavePurgeJournal(ctx, fresh, tag); err != nil { return err } if _, err = hostclaim.Update(ctx, c.Store, obs, PurgeRecordIntent(id, r.Holder.Key, r.ETag)); err != nil { return err } } if err = c.Retire(ctx, host, id); err != nil { return err } return ErrPending } fresh, tag, err := ReadPurgeJournal(ctx, c.Store, account, app, id, appID) if err != nil { return err } for _, p := range fresh.Hosts { if p.Holder != nil && !p.Retired { return ErrPending } } _ = tag return nil }