| 1 | package hostactors |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "github.com/carlosframework/platform/internal/hostclaim" |
| 6 | "github.com/carlosframework/platform/internal/hostfloor" |
| 7 | "github.com/carlosframework/platform/internal/release" |
| 8 | "reflect" |
| 9 | "strings" |
| 10 | "time" |
| 11 | ) |
| 12 | |
| 13 | // Start attaches the reservation in the same claim CAS as the start intent. |
| 14 | func (d Directory) Start(ctx context.Context, o hostclaim.Observed, id string, tr func(*hostclaim.Claim) error) (hostclaim.Observed, error) { |
| 15 | roster, err := d.Read(ctx) |
| 16 | if err != nil { |
| 17 | return hostclaim.Observed{}, err |
| 18 | } |
| 19 | h, ok := roster.Roster.Holds[id] |
| 20 | if !ok || h.Host != o.Host || h.ExpectedClaimETag != o.ETag || roster.Roster.Publication != "published" || roster.Roster.FloorSHA256 != h.FloorSHA256 { |
| 21 | return hostclaim.Observed{}, ErrRoster |
| 22 | } |
| 23 | floor, err := hostfloor.Read(ctx, d.Store, d.PublicKey, d.Bucket) |
| 24 | if err != nil || floor.Digest != h.FloorSHA256 { |
| 25 | return hostclaim.Observed{}, ErrRoster |
| 26 | } |
| 27 | for _, report := range h.Releases { |
| 28 | if report.GeneratedAt.IsZero() || time.Since(report.GeneratedAt) > 15*time.Minute || report.GeneratedAt.After(time.Now().Add(5*time.Second)) { |
| 29 | return hostclaim.Observed{}, ErrRoster |
| 30 | } |
| 31 | } |
| 32 | // Reservations pin identity, but never preserve an actor's eligibility. |
| 33 | for actor, enrollment := range h.Actors { |
| 34 | ref, boxActor := strings.CutPrefix(actor, "box:") |
| 35 | if !boxActor { |
| 36 | id, consoleActor := strings.CutPrefix(actor, "console:") |
| 37 | console, exists := roster.Roster.Consoles[id] |
| 38 | if !consoleActor || !exists || console.Enrollment != enrollment { |
| 39 | return hostclaim.Observed{}, ErrRoster |
| 40 | } |
| 41 | if console.Compute == nil { |
| 42 | continue |
| 43 | } |
| 44 | ref, enrollment = console.Compute.Box, console.Compute.Enrollment |
| 45 | } |
| 46 | box, exists := roster.Roster.Boxes[ref] |
| 47 | if !exists || box.Enrollment != enrollment || box.State != "active" { |
| 48 | return hostclaim.Observed{}, ErrRoster |
| 49 | } |
| 50 | fenced, err := d.TerminationFence(ctx, roster, ref, time.Now()) |
| 51 | if err != nil || fenced { |
| 52 | return hostclaim.Observed{}, ErrRoster |
| 53 | } |
| 54 | } |
| 55 | return hostclaim.Update(ctx, d.Store, o, func(c *hostclaim.Claim) error { |
| 56 | if err := tr(c); err != nil { |
| 57 | return err |
| 58 | } |
| 59 | if c.Op.ID != h.OpID || c.Op.Intent == nil { |
| 60 | return ErrRoster |
| 61 | } |
| 62 | c.Op.Intent["actors_hold"] = id |
| 63 | return nil |
| 64 | }) |
| 65 | } |
| 66 | |
| 67 | // CancelHold invalidates the claim ETag before releasing a never-started hold. |
| 68 | // If a delayed start won its CAS, the reread leaves its reservation held. |
| 69 | func (d Directory) CancelHold(ctx context.Context, sg release.Signer, id string) error { |
| 70 | o, err := d.Read(ctx) |
| 71 | if err != nil { |
| 72 | return err |
| 73 | } |
| 74 | h, ok := o.Roster.Holds[id] |
| 75 | if !ok { |
| 76 | return nil |
| 77 | } |
| 78 | claim, err := hostclaim.Read(ctx, d.Store, h.Host) |
| 79 | if err != nil { |
| 80 | return err |
| 81 | } |
| 82 | if !claim.Absent && claim.Claim.Op.Intent["actors_hold"] == id { |
| 83 | return ErrRoster |
| 84 | } |
| 85 | if claim.ETag == h.ExpectedClaimETag { |
| 86 | _, err = hostclaim.Update(ctx, d.Store, claim, func(*hostclaim.Claim) error { return nil }) |
| 87 | if err != nil { |
| 88 | return err |
| 89 | } |
| 90 | } |
| 91 | // Reread both records after the invalidating CAS. No timeout releases a hold. |
| 92 | fresh, err := d.Read(ctx) |
| 93 | if err != nil { |
| 94 | return err |
| 95 | } |
| 96 | if !reflect.DeepEqual(fresh.Roster.Holds[id], h) { |
| 97 | return ErrRoster |
| 98 | } |
| 99 | claim, err = hostclaim.Read(ctx, d.Store, h.Host) |
| 100 | if err != nil { |
| 101 | return err |
| 102 | } |
| 103 | if !claim.Absent && claim.Claim.Op.Intent["actors_hold"] == id { |
| 104 | return ErrRoster |
| 105 | } |
| 106 | r := Clone(fresh.Roster) |
| 107 | delete(r.Holds, id) |
| 108 | _, err = d.Replace(ctx, sg, fresh, r) |
| 109 | return err |
| 110 | } |
| 111 | func (d Directory) CheckHold(ctx context.Context, c *hostclaim.Claim, box, enrollment string) error { |
| 112 | if c == nil { |
| 113 | return ErrRoster |
| 114 | } |
| 115 | o, err := d.Read(ctx) |
| 116 | if err != nil { |
| 117 | return err |
| 118 | } |
| 119 | h, ok := o.Roster.Holds[c.Op.Intent["actors_hold"]] |
| 120 | if !ok || h.Host != c.Host || h.OpID != c.Op.ID || h.Actors["box:"+box] != enrollment { |
| 121 | return ErrRoster |
| 122 | } |
| 123 | return nil |
| 124 | } |
| 125 | |