package moves import ( "errors" "github.com/carlosframework/platform/internal/genselect" "github.com/carlosframework/platform/internal/hostclaim" ) // The start check (spec §5.1, INV-4, J2-R2-8): before every spawn the three // markers a generation leaves on a box are compared with the host claim. A // marker that merely exists proves nothing; the claim says which generation // this box may run. // // The three markers have three formats (Paul's ruling on H11): // // - generation.json is genselect.Marker, read from the box's own location; // absent is the origin. // - .files-gen is the JSON {op, incarnation} (FilesGen); absent is "no // files directory yet". // - .gen is the plain generation name hostbox.WriteGenerationMarker // writes and ColdRestore rewrites; "" is absent. It carries no incarnation // and no location: the claim's holder and the restore vouch for those. // // Refusals are typed and carry member-safe reasons, never marker contents. var ( // ErrStartMarker: generation.json does not name a generation the claim // expects, or names another incarnation. ErrStartMarker = errors.New("this box holds another generation of the instance") // ErrStartFiles: .files-gen does not name a generation and incarnation // the claim expects. ErrStartFiles = errors.New("the instance's files belong to another generation") // ErrStartDB: the database's marker does not name the claim's generation. ErrStartDB = errors.New("the instance's database belongs to another generation") ) // FilesGen is the .files-gen body (§5.1): the op and the incarnation. type FilesGen struct { Op string `json:"op"` Incarnation string `json:"incarnation"` } // originOr names the origin generation the way the claim's admissions do. func originOr(g string) string { if g == "" { return hostclaim.RootGeneration } return g } // coldAdmission is the newest admission into generation g that is not a warm // self-link (F from F, the warm keep genselect's unfinished skips). Its // predecessor is where g came from. func coldAdmission(h *hostclaim.Holder, g string) (hostclaim.AdmissionRecord, bool) { for i := len(h.Admissions) - 1; i >= 0; i-- { a := h.Admissions[i] if originOr(a.Generation) != g || a.Mode == "warm" { continue } return a, true } return hostclaim.AdmissionRecord{}, false } // markerGenerations is the set of generations generation.json may name for // this holder: holder.generation, as the admission's CAS left it, and the // lagging ones. A marker is published once per admission, at Replicate, so // after a restarted attempt (a fresh generation restored from the prepared // one) or a run of admissions that published nothing, the marker still names // an earlier generation. Walking holder.admissions from the newest, skipping // warm self-links, each cold admission into an accepted generation adds its // predecessor. The origin is hostclaim.RootGeneration here. func markerGenerations(h *hostclaim.Holder) map[string]bool { cur := originOr(h.Generation) ok := map[string]bool{cur: true} for { a, found := coldAdmission(h, cur) if !found { return ok } next := originOr(a.Predecessor) if ok[next] { // damaged record: predecessors that loop return ok } ok[next] = true cur = next } } // filesGenerationOK reports whether .files-gen may name g: exactly // holder.generation, or the cold predecessor of holder.generation (the // prepared generation a restarted attempt was restored from, whose files // are still the ones installed). Nothing further back counts: files from // before the preparation are stale. func filesGenerationOK(h *hostclaim.Holder, g string) bool { if g == "" || g == hostclaim.RootGeneration { return false } if g == h.Generation { return true } a, found := coldAdmission(h, originOr(h.Generation)) return found && a.Predecessor == g } // StartCheck is the §5.1 start check against the claim, run after the // admission CAS and before every spawn. // // 1. marker (generation.json; nil is the origin) names a generation the // claim expects (holder.generation or, lagging, an earlier link of the // admission chain) and the holder's incarnation. // 2. filesGen, when present, names holder.generation or its cold // predecessor (a restarted attempt's files still name the prepared // generation; nothing older) and the holder's incarnation. // 3. dbGen, when not "", is exactly holder.generation: the restore rewrites // it, so it is current after every restore. A warm database that fails // here is from an earlier stay; the caller parks it and restores cold. // // A claim with no holder expects no generation, so every start is refused // with ErrStartMarker. func StartCheck(c *hostclaim.Claim, marker *genselect.Marker, filesGen *FilesGen, dbGen string) error { if c == nil || c.Holder == nil { return ErrStartMarker } h := c.Holder want := markerGenerations(h) if marker == nil { if !want[hostclaim.RootGeneration] { return ErrStartMarker } } else if marker.Op == "" || marker.Op == hostclaim.RootGeneration || !want[marker.Op] || marker.Incarnation != h.Incarnation { return ErrStartMarker } if filesGen != nil { if !filesGenerationOK(h, filesGen.Op) || filesGen.Incarnation != h.Incarnation { return ErrStartFiles } } if dbGen != "" && dbGen != h.Generation { return ErrStartDB } return nil } // PreparedCheck validates a landing probe's inputs before admission (Plan 3 // Task 5 step 3): the files marker and the database marker both exist and // name the prepared generation (the move id), and the files marker names the // holder's incarnation. The database marker is the plain name (H11); the // location is vouched for by the holder. func PreparedCheck(prepared string, holder hostclaim.Holder, filesGen *FilesGen, dbGen string) error { if prepared == "" || prepared == hostclaim.RootGeneration { return ErrStartFiles } if filesGen == nil || filesGen.Op != prepared || filesGen.Incarnation != holder.Incarnation { return ErrStartFiles } if dbGen != prepared { return ErrStartDB } return nil }