| 1 | package moves |
| 2 | |
| 3 | import ( |
| 4 | "errors" |
| 5 | |
| 6 | "github.com/carlosframework/platform/internal/genselect" |
| 7 | "github.com/carlosframework/platform/internal/hostclaim" |
| 8 | ) |
| 9 | |
| 10 | // The start check (spec §5.1, INV-4, J2-R2-8): before every spawn the three |
| 11 | // markers a generation leaves on a box are compared with the host claim. A |
| 12 | // marker that merely exists proves nothing; the claim says which generation |
| 13 | // this box may run. |
| 14 | // |
| 15 | // The three markers have three formats (Paul's ruling on H11): |
| 16 | // |
| 17 | // - generation.json is genselect.Marker, read from the box's own location; |
| 18 | // absent is the origin. |
| 19 | // - .files-gen is the JSON {op, incarnation} (FilesGen); absent is "no |
| 20 | // files directory yet". |
| 21 | // - <dbPath>.gen is the plain generation name hostbox.WriteGenerationMarker |
| 22 | // writes and ColdRestore rewrites; "" is absent. It carries no incarnation |
| 23 | // and no location: the claim's holder and the restore vouch for those. |
| 24 | // |
| 25 | // Refusals are typed and carry member-safe reasons, never marker contents. |
| 26 | var ( |
| 27 | // ErrStartMarker: generation.json does not name a generation the claim |
| 28 | // expects, or names another incarnation. |
| 29 | ErrStartMarker = errors.New("this box holds another generation of the instance") |
| 30 | // ErrStartFiles: .files-gen does not name a generation and incarnation |
| 31 | // the claim expects. |
| 32 | ErrStartFiles = errors.New("the instance's files belong to another generation") |
| 33 | // ErrStartDB: the database's marker does not name the claim's generation. |
| 34 | ErrStartDB = errors.New("the instance's database belongs to another generation") |
| 35 | ) |
| 36 | |
| 37 | // FilesGen is the .files-gen body (§5.1): the op and the incarnation. |
| 38 | type FilesGen struct { |
| 39 | Op string `json:"op"` |
| 40 | Incarnation string `json:"incarnation"` |
| 41 | } |
| 42 | |
| 43 | // originOr names the origin generation the way the claim's admissions do. |
| 44 | func originOr(g string) string { |
| 45 | if g == "" { |
| 46 | return hostclaim.RootGeneration |
| 47 | } |
| 48 | return g |
| 49 | } |
| 50 | |
| 51 | // coldAdmission is the newest admission into generation g that is not a warm |
| 52 | // self-link (F from F, the warm keep genselect's unfinished skips). Its |
| 53 | // predecessor is where g came from. |
| 54 | func coldAdmission(h *hostclaim.Holder, g string) (hostclaim.AdmissionRecord, bool) { |
| 55 | for i := len(h.Admissions) - 1; i >= 0; i-- { |
| 56 | a := h.Admissions[i] |
| 57 | if originOr(a.Generation) != g || a.Mode == "warm" { |
| 58 | continue |
| 59 | } |
| 60 | return a, true |
| 61 | } |
| 62 | return hostclaim.AdmissionRecord{}, false |
| 63 | } |
| 64 | |
| 65 | // markerGenerations is the set of generations generation.json may name for |
| 66 | // this holder: holder.generation, as the admission's CAS left it, and the |
| 67 | // lagging ones. A marker is published once per admission, at Replicate, so |
| 68 | // after a restarted attempt (a fresh generation restored from the prepared |
| 69 | // one) or a run of admissions that published nothing, the marker still names |
| 70 | // an earlier generation. Walking holder.admissions from the newest, skipping |
| 71 | // warm self-links, each cold admission into an accepted generation adds its |
| 72 | // predecessor. The origin is hostclaim.RootGeneration here. |
| 73 | func markerGenerations(h *hostclaim.Holder) map[string]bool { |
| 74 | cur := originOr(h.Generation) |
| 75 | ok := map[string]bool{cur: true} |
| 76 | for { |
| 77 | a, found := coldAdmission(h, cur) |
| 78 | if !found { |
| 79 | return ok |
| 80 | } |
| 81 | next := originOr(a.Predecessor) |
| 82 | if ok[next] { // damaged record: predecessors that loop |
| 83 | return ok |
| 84 | } |
| 85 | ok[next] = true |
| 86 | cur = next |
| 87 | } |
| 88 | } |
| 89 | |
| 90 | // filesGenerationOK reports whether .files-gen may name g: exactly |
| 91 | // holder.generation, or the cold predecessor of holder.generation (the |
| 92 | // prepared generation a restarted attempt was restored from, whose files |
| 93 | // are still the ones installed). Nothing further back counts: files from |
| 94 | // before the preparation are stale. |
| 95 | func filesGenerationOK(h *hostclaim.Holder, g string) bool { |
| 96 | if g == "" || g == hostclaim.RootGeneration { |
| 97 | return false |
| 98 | } |
| 99 | if g == h.Generation { |
| 100 | return true |
| 101 | } |
| 102 | a, found := coldAdmission(h, originOr(h.Generation)) |
| 103 | return found && a.Predecessor == g |
| 104 | } |
| 105 | |
| 106 | // StartCheck is the §5.1 start check against the claim, run after the |
| 107 | // admission CAS and before every spawn. |
| 108 | // |
| 109 | // 1. marker (generation.json; nil is the origin) names a generation the |
| 110 | // claim expects (holder.generation or, lagging, an earlier link of the |
| 111 | // admission chain) and the holder's incarnation. |
| 112 | // 2. filesGen, when present, names holder.generation or its cold |
| 113 | // predecessor (a restarted attempt's files still name the prepared |
| 114 | // generation; nothing older) and the holder's incarnation. |
| 115 | // 3. dbGen, when not "", is exactly holder.generation: the restore rewrites |
| 116 | // it, so it is current after every restore. A warm database that fails |
| 117 | // here is from an earlier stay; the caller parks it and restores cold. |
| 118 | // |
| 119 | // A claim with no holder expects no generation, so every start is refused |
| 120 | // with ErrStartMarker. |
| 121 | func StartCheck(c *hostclaim.Claim, marker *genselect.Marker, filesGen *FilesGen, dbGen string) error { |
| 122 | if c == nil || c.Holder == nil { |
| 123 | return ErrStartMarker |
| 124 | } |
| 125 | h := c.Holder |
| 126 | want := markerGenerations(h) |
| 127 | if marker == nil { |
| 128 | if !want[hostclaim.RootGeneration] { |
| 129 | return ErrStartMarker |
| 130 | } |
| 131 | } else if marker.Op == "" || marker.Op == hostclaim.RootGeneration || |
| 132 | !want[marker.Op] || marker.Incarnation != h.Incarnation { |
| 133 | return ErrStartMarker |
| 134 | } |
| 135 | if filesGen != nil { |
| 136 | if !filesGenerationOK(h, filesGen.Op) || filesGen.Incarnation != h.Incarnation { |
| 137 | return ErrStartFiles |
| 138 | } |
| 139 | } |
| 140 | if dbGen != "" && dbGen != h.Generation { |
| 141 | return ErrStartDB |
| 142 | } |
| 143 | return nil |
| 144 | } |
| 145 | |
| 146 | // PreparedCheck validates a landing probe's inputs before admission (Plan 3 |
| 147 | // Task 5 step 3): the files marker and the database marker both exist and |
| 148 | // name the prepared generation (the move id), and the files marker names the |
| 149 | // holder's incarnation. The database marker is the plain name (H11); the |
| 150 | // location is vouched for by the holder. |
| 151 | func PreparedCheck(prepared string, holder hostclaim.Holder, filesGen *FilesGen, dbGen string) error { |
| 152 | if prepared == "" || prepared == hostclaim.RootGeneration { |
| 153 | return ErrStartFiles |
| 154 | } |
| 155 | if filesGen == nil || filesGen.Op != prepared || filesGen.Incarnation != holder.Incarnation { |
| 156 | return ErrStartFiles |
| 157 | } |
| 158 | if dbGen != prepared { |
| 159 | return ErrStartDB |
| 160 | } |
| 161 | return nil |
| 162 | } |
| 163 | |