package hostclaim import ( "encoding/json" "errors" "fmt" "os" "time" ) var ErrRetiredIncarnation = errors.New("hostclaim: incarnation was retired on this host") type NewHolder struct { Acquisition string Family, Key, Account, App, Name, Host string RecordRegion string // Region encoded in the record; empty means home region. Region string // Resolved storage region; must be non-empty. CreatedAt time.Time } func keyMatches(nh NewHolder) bool { return holderKeyMatches(nh.Family, nh.Key, nh.Account, nh.App, nh.Name, nh.Host) } // recordMatches checks the record body that create persists as its // reconstruction intent is this holder's record: valid JSON naming the // same host, account, app, sidecar name (empty for instances) and region, // and the same CreatedAt. func recordMatches(nh NewHolder, body []byte) bool { var r struct { Name string `json:"name"` // sidecar records only Host string `json:"host"` Account string `json:"account"` App string `json:"app"` Region string `json:"region"` CreatedAt time.Time `json:"created_at"` } if err := json.Unmarshal(body, &r); err != nil { return false } return r.Host == nh.Host && r.Account == nh.Account && r.App == nh.App && r.Name == nh.Name && r.Region == nh.RecordRegion && r.CreatedAt.Equal(nh.CreatedAt) } func retire(c *Claim, inc string) { if os.Getenv("CARLOS_HOST_CLAIM_RETIRED_LOCATIONS") == "1" && c.Holder != nil && c.Holder.Incarnation == inc && isHex64(inc) { if c.RetiredLocations == nil { c.RetiredLocations = map[string]string{} } c.RetiredLocations[inc] = c.Holder.Location } c.RetiredIncarnations = append([]string{inc}, c.RetiredIncarnations...) if len(c.RetiredIncarnations) > retiredCap { c.RetiredIncarnations = c.RetiredIncarnations[:retiredCap] for retired := range c.RetiredLocations { if !stringPresent(c.RetiredIncarnations, retired) { delete(c.RetiredLocations, retired) } } } } func BeginCreate(nh NewHolder, recordBody []byte, opID, by string, now time.Time) func(*Claim) error { return func(c *Claim) error { if nh.Host != c.Host || !keyMatches(nh) || nh.Region == "" { return fmt.Errorf("%w: create inputs are not this host's", ErrWrongState) } if !c.Free() { return fmt.Errorf("%w: host is held", ErrWrongState) } if nh.CreatedAt.IsZero() || opID == "" { return fmt.Errorf("%w: create needs CreatedAt and an op id", ErrMalformed) } if !recordMatches(nh, recordBody) { return fmt.Errorf("%w: the record body is not this holder's record", ErrWrongState) } inc, err := Incarnation(nh.Account, nh.App, nh.Host, nh.CreatedAt) if err != nil { return err } for _, r := range c.RetiredIncarnations { if r == inc { return ErrRetiredIncarnation } } c.Holder = &Holder{Acquisition: nh.Acquisition, Family: nh.Family, Key: nh.Key, Account: nh.Account, App: nh.App, Name: nh.Name, Incarnation: inc, CreatedAt: nh.CreatedAt, Location: "instances/" + nh.Host + "/" + inc} c.Op = Op{Kind: KindCreating, ID: opID, By: by, StartedAt: now.UTC(), Intent: map[string]string{"key": nh.Key, "record": string(recordBody)}} c.Touched = Touched{Complete: true, Regions: []string{nh.Region}} return nil } } // ExecutorSelection is the box create selects once, with // boxchoice.Choose over the eligible set (claim P5-D2, region HA §5.2, // H12), and its roster enrollment. type ExecutorSelection struct { Box, Enrollment string } // BeginCreateOn is BeginCreate recording the selected executor in the // create intent, in the same step 2 CAS, before any effect (claim §5.1, // P5-D6 `selected_executor`, `executor_enrollment`). CompleteCreate then // sets op.exec to exactly that box; recovery never substitutes another. func BeginCreateOn(nh NewHolder, recordBody []byte, opID, by string, now time.Time, sel ExecutorSelection) func(*Claim) error { begin := BeginCreate(nh, recordBody, opID, by, now) return func(c *Claim) error { if !validBoxReference(sel.Box) || sel.Box == "none" || sel.Enrollment == "" { return fmt.Errorf("%w: create needs a selected executor", ErrMalformed) } if err := begin(c); err != nil { return err } c.Op.Intent["selected_executor"] = sel.Box c.Op.Intent["executor_enrollment"] = sel.Enrollment return nil } } func CompleteCreate(opID, execBox string) func(*Claim) error { return func(c *Claim) error { if err := RequireOp(opID)(c); err != nil { return err } if c.Op.Kind != KindCreating || c.Op.Phase == "cancelling" || execBox == "" || (c.Op.Intent["selected_executor"] != "" && execBox != c.Op.Intent["selected_executor"]) { return fmt.Errorf("%w: kind %q", ErrWrongState, c.Op.Kind) } if err := requireNoCreateAuthority(c); err != nil { return err } c.Op = Op{Kind: KindServing, Exec: execBox, Participants: []string{execBox}} c.Holder.Stamp = c.Gen + 1 // the resulting gen: Update sets gen = observed+1 if !set(c.Touched.Boxes)[execBox] { // the executor will hold local state (§2 touched) c.Touched.Boxes = append(c.Touched.Boxes, execBox) } return nil } } func CancelCreate(opID string, now time.Time) func(*Claim) error { return func(c *Claim) error { if err := RequireOp(opID)(c); err != nil { return err } if c.Op.Kind != KindCreating { return fmt.Errorf("%w: kind %q", ErrWrongState, c.Op.Kind) } if err := requireNoCreateAuthority(c); err != nil { return err } if err := RequireLossEvidenceArchived(c); err != nil { return err } inc, key := c.Holder.Incarnation, c.Holder.Key retire(c, inc) c.CancelledCreates = append(c.CancelledCreates, CancelledCreate{Key: key, Incarnation: inc, Until: now.UTC().Add(time.Hour)}) c.Holder, c.Op, c.Touched = nil, Op{}, Touched{} return nil } } func requireNoCreateAuthority(c *Claim) error { selected := c.Op.Intent["selected_executor"] selectedOnly := len(c.Op.Participants) == 1 && selected != "" && c.Op.Participants[0] == selected && c.Op.Intent["executor_enrollment"] != "" if c.Op.Exec != "" || (len(c.Op.Participants) > 0 && !selectedOnly) || c.Holder.Stamp != 0 { return fmt.Errorf("%w: create carries execution authority", ErrWrongState) } return nil }