| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "fmt" |
| 7 | "github.com/carlosframework/platform/internal/moves" |
| 8 | "os" |
| 9 | "path/filepath" |
| 10 | "strconv" |
| 11 | |
| 12 | "github.com/carlosframework/platform/internal/hostbox" |
| 13 | "github.com/carlosframework/platform/internal/hostclaim" |
| 14 | "github.com/carlosframework/platform/internal/hostguard" |
| 15 | "github.com/carlosframework/platform/internal/park" |
| 16 | ) |
| 17 | |
| 18 | // claimArtifactRetirement parks a superseded database and marker under the |
| 19 | // admitted installation's descriptor guard. Landing probes, restoring and |
| 20 | // reverting share the same exact inventory checks. Initial source recovery |
| 21 | // uses the source cut's journal and report; later sessions require root's |
| 22 | // completed writer-stop receipt. Files and environment paths are unaffected. |
| 23 | // Continue is bound only by the moves C1 integration; nil refuses. |
| 24 | type claimArtifactRetirement struct { |
| 25 | Local park.Local |
| 26 | Continue func(kind, from, to string) bool |
| 27 | } |
| 28 | |
| 29 | func (r claimArtifactRetirement) Retire(ctx context.Context, admitted hostclaim.Observed, in ClaimLaunchInputs, generation string) error { |
| 30 | box := r.Local.Box |
| 31 | if r.Continue == nil || r.Local.Claims == nil || box == "" || !filepath.IsAbs(r.Local.DataDir) || admitted.Claim == nil || admitted.Claim.Holder == nil || !claimArtifactKind(admitted.Claim.Op.Kind) { |
| 32 | return hostbox.ErrChanged |
| 33 | } |
| 34 | h := admitted.Claim.Holder |
| 35 | if generation == "" || generation == h.Generation || !filepath.IsAbs(in.DBPath) { |
| 36 | return hostbox.ErrChanged |
| 37 | } |
| 38 | marker, err := filepath.Rel(r.Local.DataDir, in.DBPath+".gen") |
| 39 | if err != nil || !filepath.IsLocal(marker) { |
| 40 | return hostbox.ErrChanged |
| 41 | } |
| 42 | database, err := filepath.Rel(r.Local.DataDir, in.DBPath) |
| 43 | if err != nil || !filepath.IsLocal(database) { |
| 44 | return hostbox.ErrChanged |
| 45 | } |
| 46 | marker, database = filepath.ToSlash(marker), filepath.ToSlash(database) |
| 47 | fresh, err := hostclaim.Read(ctx, r.Local.Claims, admitted.Host) |
| 48 | if err != nil { |
| 49 | return err |
| 50 | } |
| 51 | if !claimSameWriter(admitted, fresh, r.Continue) { |
| 52 | return hostbox.ErrChanged |
| 53 | } |
| 54 | c := fresh.Claim |
| 55 | if _, ok := claimArtifactWriter(c, box); !ok { |
| 56 | return hostbox.ErrChanged |
| 57 | } |
| 58 | // The superseded database is this op's: its prepared generation, named by |
| 59 | // the op id, or a generation an earlier session on this box was admitted |
| 60 | // into under it. |
| 61 | known := c.Op.Kind == hostclaim.KindLanding && generation == c.Op.ID |
| 62 | if claimRecoveryKind(c.Op.Kind) && generation == c.Op.Journal[hostclaim.JournalSrcGen] { |
| 63 | known = true |
| 64 | } |
| 65 | for _, a := range c.Holder.Admissions { |
| 66 | if a.Generation == generation && a.Box == box && a.Session < c.Holder.Session { |
| 67 | known = true |
| 68 | } |
| 69 | } |
| 70 | if !known { |
| 71 | return hostbox.ErrChanged |
| 72 | } |
| 73 | paths := c.Inventory[box].Paths |
| 74 | stamp := paths[marker] |
| 75 | if stamp == 0 || stamp >= c.Holder.Stamp || paths[database] != stamp { |
| 76 | return hostbox.ErrChanged |
| 77 | } |
| 78 | if err := r.artifactsStopped(ctx, admitted.Host, c, box, stamp, generation); err != nil { |
| 79 | return err |
| 80 | } |
| 81 | check := func(now *hostclaim.Claim) error { |
| 82 | if !claimSameWriter(fresh, hostclaim.Observed{Host: admitted.Host, Claim: now}, r.Continue) { |
| 83 | return hostbox.ErrChanged |
| 84 | } |
| 85 | if _, ok := claimArtifactWriter(now, box); !ok { |
| 86 | return hostbox.ErrChanged |
| 87 | } |
| 88 | return nil |
| 89 | } |
| 90 | retirement := fmt.Sprintf("superseded-%d-%d", c.Holder.Session, stamp) |
| 91 | before, after, err := r.Local.RetireSuperseded(ctx, admitted.Host, hostguard.Captured{Incarnation: h.Incarnation, Stamp: h.Stamp}, c.Op.ID, retirement, stamp, []string{database, marker}, check) |
| 92 | if err != nil { |
| 93 | return err |
| 94 | } |
| 95 | return in.observeClaimWrite(before, after) |
| 96 | } |
| 97 | |
| 98 | // stoppedAt requires root's completed full stop of the writer session that ran |
| 99 | // installation stamp on box under the op: every writer-session receipt the |
| 100 | // guard journalled for that installation names one session, verifies byte for |
| 101 | // byte, and that session was admitted on box before the current one. At most |
| 102 | // one session runs a stamp: a cold admission takes its own stamp unless it is |
| 103 | // the first writer since the stamp was issued. No receipt refuses. |
| 104 | func (r claimArtifactRetirement) stoppedAt(ctx context.Context, host string, c *hostclaim.Claim, box string, stamp uint64) error { |
| 105 | receipts, err := r.Local.Guard.CompletedWriterStops(ctx, host, hostguard.Captured{Incarnation: c.Holder.Incarnation, Stamp: stamp}, c.Op.ID, box) |
| 106 | if err != nil { |
| 107 | return err |
| 108 | } |
| 109 | if len(receipts) == 0 { |
| 110 | return hostguard.ErrNoFenceReceipt |
| 111 | } |
| 112 | session := receipts[0].Target.Session |
| 113 | for _, receipt := range receipts { |
| 114 | if receipt.Target.Session != session || verifyRetirementReceipt(ctx, receipt) != nil { |
| 115 | return ErrClaimStopReceipt |
| 116 | } |
| 117 | } |
| 118 | for _, a := range c.Holder.Admissions { |
| 119 | if a.Session == session && a.Box == box && session < c.Holder.Session { |
| 120 | return nil |
| 121 | } |
| 122 | } |
| 123 | return hostbox.ErrChanged |
| 124 | } |
| 125 | |
| 126 | // Recover re-drives retirement before any restore/materialization when a |
| 127 | // failed inventory CAS left both database and marker parked. The absent |
| 128 | // marker cannot be the only retry trigger: ownership must be recorded before |
| 129 | // the successor creates new bytes at those paths. Current tags mean the |
| 130 | // completed retirement already recorded that ownership. |
| 131 | func (r claimArtifactRetirement) Recover(ctx context.Context, admitted hostclaim.Observed, in ClaimLaunchInputs) error { |
| 132 | if r.Continue == nil || r.Local.Claims == nil || admitted.Claim == nil || admitted.Claim.Holder == nil || !filepath.IsAbs(in.DBPath) { |
| 133 | return hostbox.ErrChanged |
| 134 | } |
| 135 | if !claimArtifactKind(admitted.Claim.Op.Kind) { |
| 136 | return nil |
| 137 | } |
| 138 | if _, err := os.Lstat(in.DBPath + ".gen"); err == nil { |
| 139 | return nil |
| 140 | } else if !errors.Is(err, os.ErrNotExist) { |
| 141 | return err |
| 142 | } |
| 143 | fresh, err := hostclaim.Read(ctx, r.Local.Claims, admitted.Host) |
| 144 | if err != nil { |
| 145 | return err |
| 146 | } |
| 147 | if !claimSameWriter(admitted, fresh, r.Continue) { |
| 148 | return hostbox.ErrChanged |
| 149 | } |
| 150 | c, box := fresh.Claim, r.Local.Box |
| 151 | if _, ok := claimArtifactWriter(c, box); !ok { |
| 152 | return hostbox.ErrChanged |
| 153 | } |
| 154 | db, err := filepath.Rel(r.Local.DataDir, in.DBPath) |
| 155 | if err != nil || !filepath.IsLocal(db) { |
| 156 | return hostbox.ErrChanged |
| 157 | } |
| 158 | db = filepath.ToSlash(db) |
| 159 | paths := c.Inventory[box].Paths |
| 160 | stamp := paths[db+".gen"] |
| 161 | if stamp == 0 || paths[db] != stamp { |
| 162 | return hostbox.ErrChanged |
| 163 | } |
| 164 | if stamp == c.Holder.Stamp { |
| 165 | return nil |
| 166 | } |
| 167 | if stamp > c.Holder.Stamp { |
| 168 | return hostbox.ErrChanged |
| 169 | } |
| 170 | if claimRecoveryKind(c.Op.Kind) { |
| 171 | sourceStamp, e := strconv.ParseUint(c.Op.Journal[hostclaim.JournalSourceStamp], 10, 64) |
| 172 | if e == nil && stamp == sourceStamp { |
| 173 | return r.Retire(ctx, admitted, in, c.Op.Journal[hostclaim.JournalSrcGen]) |
| 174 | } |
| 175 | } |
| 176 | receipts, err := r.Local.Guard.CompletedWriterStops(ctx, admitted.Host, hostguard.Captured{Incarnation: c.Holder.Incarnation, Stamp: stamp}, c.Op.ID, box) |
| 177 | if err != nil { |
| 178 | return err |
| 179 | } |
| 180 | if len(receipts) == 0 { |
| 181 | return hostguard.ErrNoFenceReceipt |
| 182 | } |
| 183 | session := receipts[0].Target.Session |
| 184 | for _, a := range c.Holder.Admissions { |
| 185 | if a.Session == session && a.Box == box && a.Session < c.Holder.Session { |
| 186 | // Retire repeats all receipt, generation and guarded inventory checks; |
| 187 | // park.Local resumes the deterministic journal, even with both paths absent. |
| 188 | return r.Retire(ctx, admitted, in, a.Generation) |
| 189 | } |
| 190 | } |
| 191 | return hostbox.ErrChanged |
| 192 | } |
| 193 | |
| 194 | func claimArtifactKind(kind string) bool { |
| 195 | return kind == hostclaim.KindLanding || claimRecoveryKind(kind) |
| 196 | } |
| 197 | func claimArtifactWriter(c *hostclaim.Claim, box string) (hostclaim.AdmissionRecord, bool) { |
| 198 | if c != nil && claimRecoveryKind(c.Op.Kind) { |
| 199 | return hostguard.RecoveryWriter(c, box) |
| 200 | } |
| 201 | return hostguard.LandingWriter(c, box) |
| 202 | } |
| 203 | func (r claimArtifactRetirement) artifactsStopped(ctx context.Context, host string, c *hostclaim.Claim, box string, stamp uint64, generation string) error { |
| 204 | sourceStamp, err := strconv.ParseUint(c.Op.Journal[hostclaim.JournalSourceStamp], 10, 64) |
| 205 | if !claimRecoveryKind(c.Op.Kind) || err != nil || stamp != sourceStamp { |
| 206 | return r.stoppedAt(ctx, host, c, box, stamp) |
| 207 | } |
| 208 | if c.Op.Intent["source"] != box || generation != c.Op.Journal[hostclaim.JournalSrcGen] { |
| 209 | return hostbox.ErrChanged |
| 210 | } |
| 211 | session, err := strconv.ParseUint(c.Op.Journal[hostclaim.JournalCutSession], 10, 64) |
| 212 | if err != nil || session >= c.Holder.Session { |
| 213 | return hostbox.ErrChanged |
| 214 | } |
| 215 | key := moves.StoppedKey(moves.ReportPrefix(c.Holder.Account, c.Holder.App, host, c.Op.ID), "source") |
| 216 | if c.Op.Journal[hostclaim.JournalEvidenceSource] != key { |
| 217 | return hostbox.ErrChanged |
| 218 | } |
| 219 | report, err := readStoppedReport(ctx, r.Local.Claims, key) |
| 220 | if err != nil { |
| 221 | return err |
| 222 | } |
| 223 | if report.Box != box || report.Role != "source" || report.Host != host || report.OpID != c.Op.ID || report.Incarnation != c.Holder.Incarnation { |
| 224 | return hostbox.ErrChanged |
| 225 | } |
| 226 | return nil |
| 227 | } |
| 228 | |