package hostactors import ( "context" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostfloor" "github.com/carlosframework/platform/internal/release" "reflect" "strings" "time" ) // Start attaches the reservation in the same claim CAS as the start intent. func (d Directory) Start(ctx context.Context, o hostclaim.Observed, id string, tr func(*hostclaim.Claim) error) (hostclaim.Observed, error) { roster, err := d.Read(ctx) if err != nil { return hostclaim.Observed{}, err } h, ok := roster.Roster.Holds[id] if !ok || h.Host != o.Host || h.ExpectedClaimETag != o.ETag || roster.Roster.Publication != "published" || roster.Roster.FloorSHA256 != h.FloorSHA256 { return hostclaim.Observed{}, ErrRoster } floor, err := hostfloor.Read(ctx, d.Store, d.PublicKey, d.Bucket) if err != nil || floor.Digest != h.FloorSHA256 { return hostclaim.Observed{}, ErrRoster } for _, report := range h.Releases { if report.GeneratedAt.IsZero() || time.Since(report.GeneratedAt) > 15*time.Minute || report.GeneratedAt.After(time.Now().Add(5*time.Second)) { return hostclaim.Observed{}, ErrRoster } } // Reservations pin identity, but never preserve an actor's eligibility. for actor, enrollment := range h.Actors { ref, boxActor := strings.CutPrefix(actor, "box:") if !boxActor { id, consoleActor := strings.CutPrefix(actor, "console:") console, exists := roster.Roster.Consoles[id] if !consoleActor || !exists || console.Enrollment != enrollment { return hostclaim.Observed{}, ErrRoster } if console.Compute == nil { continue } ref, enrollment = console.Compute.Box, console.Compute.Enrollment } box, exists := roster.Roster.Boxes[ref] if !exists || box.Enrollment != enrollment || box.State != "active" { return hostclaim.Observed{}, ErrRoster } fenced, err := d.TerminationFence(ctx, roster, ref, time.Now()) if err != nil || fenced { return hostclaim.Observed{}, ErrRoster } } return hostclaim.Update(ctx, d.Store, o, func(c *hostclaim.Claim) error { if err := tr(c); err != nil { return err } if c.Op.ID != h.OpID || c.Op.Intent == nil { return ErrRoster } c.Op.Intent["actors_hold"] = id return nil }) } // CancelHold invalidates the claim ETag before releasing a never-started hold. // If a delayed start won its CAS, the reread leaves its reservation held. func (d Directory) CancelHold(ctx context.Context, sg release.Signer, id string) error { o, err := d.Read(ctx) if err != nil { return err } h, ok := o.Roster.Holds[id] if !ok { return nil } claim, err := hostclaim.Read(ctx, d.Store, h.Host) if err != nil { return err } if !claim.Absent && claim.Claim.Op.Intent["actors_hold"] == id { return ErrRoster } if claim.ETag == h.ExpectedClaimETag { _, err = hostclaim.Update(ctx, d.Store, claim, func(*hostclaim.Claim) error { return nil }) if err != nil { return err } } // Reread both records after the invalidating CAS. No timeout releases a hold. fresh, err := d.Read(ctx) if err != nil { return err } if !reflect.DeepEqual(fresh.Roster.Holds[id], h) { return ErrRoster } claim, err = hostclaim.Read(ctx, d.Store, h.Host) if err != nil { return err } if !claim.Absent && claim.Claim.Op.Intent["actors_hold"] == id { return ErrRoster } r := Clone(fresh.Roster) delete(r.Holds, id) _, err = d.Replace(ctx, sg, fresh, r) return err } func (d Directory) CheckHold(ctx context.Context, c *hostclaim.Claim, box, enrollment string) error { if c == nil { return ErrRoster } o, err := d.Read(ctx) if err != nil { return err } h, ok := o.Roster.Holds[c.Op.Intent["actors_hold"]] if !ok || h.Host != c.Host || h.OpID != c.Op.ID || h.Actors["box:"+box] != enrollment { return ErrRoster } return nil }