| 1 | package hostclaim |
| 2 | |
| 3 | import ( |
| 4 | "encoding/json" |
| 5 | "errors" |
| 6 | "fmt" |
| 7 | "os" |
| 8 | "time" |
| 9 | ) |
| 10 | |
| 11 | var ErrRetiredIncarnation = errors.New("hostclaim: incarnation was retired on this host") |
| 12 | |
| 13 | type NewHolder struct { |
| 14 | Acquisition string |
| 15 | Family, Key, Account, App, Name, Host string |
| 16 | RecordRegion string // Region encoded in the record; empty means home region. |
| 17 | Region string // Resolved storage region; must be non-empty. |
| 18 | CreatedAt time.Time |
| 19 | } |
| 20 | |
| 21 | func keyMatches(nh NewHolder) bool { |
| 22 | return holderKeyMatches(nh.Family, nh.Key, nh.Account, nh.App, nh.Name, nh.Host) |
| 23 | } |
| 24 | |
| 25 | // recordMatches checks the record body that create persists as its |
| 26 | // reconstruction intent is this holder's record: valid JSON naming the |
| 27 | // same host, account, app, sidecar name (empty for instances) and region, |
| 28 | // and the same CreatedAt. |
| 29 | func recordMatches(nh NewHolder, body []byte) bool { |
| 30 | var r struct { |
| 31 | Name string `json:"name"` // sidecar records only |
| 32 | Host string `json:"host"` |
| 33 | Account string `json:"account"` |
| 34 | App string `json:"app"` |
| 35 | Region string `json:"region"` |
| 36 | CreatedAt time.Time `json:"created_at"` |
| 37 | } |
| 38 | if err := json.Unmarshal(body, &r); err != nil { |
| 39 | return false |
| 40 | } |
| 41 | return r.Host == nh.Host && r.Account == nh.Account && r.App == nh.App && r.Name == nh.Name && |
| 42 | r.Region == nh.RecordRegion && r.CreatedAt.Equal(nh.CreatedAt) |
| 43 | } |
| 44 | |
| 45 | func retire(c *Claim, inc string) { |
| 46 | if os.Getenv("CARLOS_HOST_CLAIM_RETIRED_LOCATIONS") == "1" && c.Holder != nil && c.Holder.Incarnation == inc && isHex64(inc) { |
| 47 | if c.RetiredLocations == nil { |
| 48 | c.RetiredLocations = map[string]string{} |
| 49 | } |
| 50 | c.RetiredLocations[inc] = c.Holder.Location |
| 51 | } |
| 52 | c.RetiredIncarnations = append([]string{inc}, c.RetiredIncarnations...) |
| 53 | if len(c.RetiredIncarnations) > retiredCap { |
| 54 | c.RetiredIncarnations = c.RetiredIncarnations[:retiredCap] |
| 55 | for retired := range c.RetiredLocations { |
| 56 | if !stringPresent(c.RetiredIncarnations, retired) { |
| 57 | delete(c.RetiredLocations, retired) |
| 58 | } |
| 59 | } |
| 60 | } |
| 61 | } |
| 62 | |
| 63 | func BeginCreate(nh NewHolder, recordBody []byte, opID, by string, now time.Time) func(*Claim) error { |
| 64 | return func(c *Claim) error { |
| 65 | if nh.Host != c.Host || !keyMatches(nh) || nh.Region == "" { |
| 66 | return fmt.Errorf("%w: create inputs are not this host's", ErrWrongState) |
| 67 | } |
| 68 | if !c.Free() { |
| 69 | return fmt.Errorf("%w: host is held", ErrWrongState) |
| 70 | } |
| 71 | if nh.CreatedAt.IsZero() || opID == "" { |
| 72 | return fmt.Errorf("%w: create needs CreatedAt and an op id", ErrMalformed) |
| 73 | } |
| 74 | if !recordMatches(nh, recordBody) { |
| 75 | return fmt.Errorf("%w: the record body is not this holder's record", ErrWrongState) |
| 76 | } |
| 77 | inc, err := Incarnation(nh.Account, nh.App, nh.Host, nh.CreatedAt) |
| 78 | if err != nil { |
| 79 | return err |
| 80 | } |
| 81 | for _, r := range c.RetiredIncarnations { |
| 82 | if r == inc { |
| 83 | return ErrRetiredIncarnation |
| 84 | } |
| 85 | } |
| 86 | c.Holder = &Holder{Acquisition: nh.Acquisition, Family: nh.Family, Key: nh.Key, Account: nh.Account, App: nh.App, Name: nh.Name, |
| 87 | Incarnation: inc, CreatedAt: nh.CreatedAt, Location: "instances/" + nh.Host + "/" + inc} |
| 88 | c.Op = Op{Kind: KindCreating, ID: opID, By: by, StartedAt: now.UTC(), |
| 89 | Intent: map[string]string{"key": nh.Key, "record": string(recordBody)}} |
| 90 | c.Touched = Touched{Complete: true, Regions: []string{nh.Region}} |
| 91 | return nil |
| 92 | } |
| 93 | } |
| 94 | |
| 95 | // ExecutorSelection is the box create selects once, with |
| 96 | // boxchoice.Choose over the eligible set (claim P5-D2, region HA §5.2, |
| 97 | // H12), and its roster enrollment. |
| 98 | type ExecutorSelection struct { |
| 99 | Box, Enrollment string |
| 100 | } |
| 101 | |
| 102 | // BeginCreateOn is BeginCreate recording the selected executor in the |
| 103 | // create intent, in the same step 2 CAS, before any effect (claim §5.1, |
| 104 | // P5-D6 `selected_executor`, `executor_enrollment`). CompleteCreate then |
| 105 | // sets op.exec to exactly that box; recovery never substitutes another. |
| 106 | func BeginCreateOn(nh NewHolder, recordBody []byte, opID, by string, now time.Time, sel ExecutorSelection) func(*Claim) error { |
| 107 | begin := BeginCreate(nh, recordBody, opID, by, now) |
| 108 | return func(c *Claim) error { |
| 109 | if !validBoxReference(sel.Box) || sel.Box == "none" || sel.Enrollment == "" { |
| 110 | return fmt.Errorf("%w: create needs a selected executor", ErrMalformed) |
| 111 | } |
| 112 | if err := begin(c); err != nil { |
| 113 | return err |
| 114 | } |
| 115 | c.Op.Intent["selected_executor"] = sel.Box |
| 116 | c.Op.Intent["executor_enrollment"] = sel.Enrollment |
| 117 | return nil |
| 118 | } |
| 119 | } |
| 120 | |
| 121 | func CompleteCreate(opID, execBox string) func(*Claim) error { |
| 122 | return func(c *Claim) error { |
| 123 | if err := RequireOp(opID)(c); err != nil { |
| 124 | return err |
| 125 | } |
| 126 | if c.Op.Kind != KindCreating || c.Op.Phase == "cancelling" || execBox == "" || (c.Op.Intent["selected_executor"] != "" && execBox != c.Op.Intent["selected_executor"]) { |
| 127 | return fmt.Errorf("%w: kind %q", ErrWrongState, c.Op.Kind) |
| 128 | } |
| 129 | if err := requireNoCreateAuthority(c); err != nil { |
| 130 | return err |
| 131 | } |
| 132 | c.Op = Op{Kind: KindServing, Exec: execBox, Participants: []string{execBox}} |
| 133 | c.Holder.Stamp = c.Gen + 1 // the resulting gen: Update sets gen = observed+1 |
| 134 | if !set(c.Touched.Boxes)[execBox] { // the executor will hold local state (§2 touched) |
| 135 | c.Touched.Boxes = append(c.Touched.Boxes, execBox) |
| 136 | } |
| 137 | return nil |
| 138 | } |
| 139 | } |
| 140 | |
| 141 | func CancelCreate(opID string, now time.Time) func(*Claim) error { |
| 142 | return func(c *Claim) error { |
| 143 | if err := RequireOp(opID)(c); err != nil { |
| 144 | return err |
| 145 | } |
| 146 | if c.Op.Kind != KindCreating { |
| 147 | return fmt.Errorf("%w: kind %q", ErrWrongState, c.Op.Kind) |
| 148 | } |
| 149 | if err := requireNoCreateAuthority(c); err != nil { |
| 150 | return err |
| 151 | } |
| 152 | if err := RequireLossEvidenceArchived(c); err != nil { |
| 153 | return err |
| 154 | } |
| 155 | inc, key := c.Holder.Incarnation, c.Holder.Key |
| 156 | retire(c, inc) |
| 157 | c.CancelledCreates = append(c.CancelledCreates, CancelledCreate{Key: key, Incarnation: inc, Until: now.UTC().Add(time.Hour)}) |
| 158 | c.Holder, c.Op, c.Touched = nil, Op{}, Touched{} |
| 159 | return nil |
| 160 | } |
| 161 | } |
| 162 | |
| 163 | func requireNoCreateAuthority(c *Claim) error { |
| 164 | selected := c.Op.Intent["selected_executor"] |
| 165 | selectedOnly := len(c.Op.Participants) == 1 && selected != "" && c.Op.Participants[0] == selected && c.Op.Intent["executor_enrollment"] != "" |
| 166 | if c.Op.Exec != "" || (len(c.Op.Participants) > 0 && !selectedOnly) || c.Holder.Stamp != 0 { |
| 167 | return fmt.Errorf("%w: create carries execution authority", ErrWrongState) |
| 168 | } |
| 169 | return nil |
| 170 | } |
| 171 | |