| 1 | package ledger |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "errors" |
| 7 | "os" |
| 8 | |
| 9 | "github.com/carlosframework/platform/internal/release" |
| 10 | ) |
| 11 | |
| 12 | func ReadManifest(ctx context.Context, st release.Store, sqid, app string) (Manifest, error) { |
| 13 | b, _, err := st.Get(ctx, ManifestKey(sqid, app)) |
| 14 | if err != nil { |
| 15 | return Manifest{}, err |
| 16 | } |
| 17 | var m Manifest |
| 18 | if err := json.Unmarshal(b, &m); err != nil { |
| 19 | return Manifest{}, err |
| 20 | } |
| 21 | return m, nil |
| 22 | } |
| 23 | |
| 24 | // WriteManifest replaces the whole manifest: read-etag-then-Put with one |
| 25 | // retry on a lost race, last-writer-wins (the putOverwrite pattern, |
| 26 | // internal/status/fleet.go). It is the right call for a caller that HAS the |
| 27 | // whole intended value — a seeded fixture, a box-side tool writing the set |
| 28 | // it just computed. A caller changing one ledger's publication wants |
| 29 | // UpdateManifest instead: replacing the whole value there unpublishes |
| 30 | // everything the caller didn't happen to name. |
| 31 | func WriteManifest(ctx context.Context, st release.Store, sqid, app string, m Manifest) error { |
| 32 | _, err := UpdateManifest(ctx, st, sqid, app, func(cur *Manifest) error { |
| 33 | *cur = m |
| 34 | return nil |
| 35 | }) |
| 36 | return err |
| 37 | } |
| 38 | |
| 39 | // manifestAttempts bounds UpdateManifest's CAS retry loop, matching |
| 40 | // appendAttempts: publish contention is two contributors publishing the same |
| 41 | // app in the same moment, and four attempts outlast that without spinning. |
| 42 | const manifestAttempts = 4 |
| 43 | |
| 44 | // UpdateManifest is the manifest's read-modify-write door: it reads the |
| 45 | // stored manifest (a missing one is the ZERO value, not an error — the first |
| 46 | // publish of an app creates it), hands it to apply, and CASes the result |
| 47 | // back on the etag it read. A lost race re-READS and re-APPLIES rather than |
| 48 | // re-putting a stale copy, which is the whole point: the manifest is a SET |
| 49 | // accrued across separate publish calls, so "carbon and prompts published at |
| 50 | // the same moment" must end with both, not with whichever writer was slower. |
| 51 | // |
| 52 | // Contention that outlasts the retries surfaces as release.ErrCASConflict |
| 53 | // (the console maps it to a retry-worthy 503) — never as a silent no-op. |
| 54 | // |
| 55 | // apply may REFUSE: an error from it aborts the attempt BEFORE the Put and is |
| 56 | // returned untouched, so nothing is written. That is what lets a caller |
| 57 | // enforce a rule the request alone cannot decide — a cap on the RESULTING set |
| 58 | // — on whatever manifest each attempt actually read, including the re-read |
| 59 | // after a lost race, rather than on a snapshot that a concurrent publisher |
| 60 | // has already moved past. |
| 61 | func claimRawUpdateManifest(ctx context.Context, st release.Store, sqid, app string, |
| 62 | apply func(*Manifest) error) (Manifest, error) { |
| 63 | key := ManifestKey(sqid, app) |
| 64 | var lastErr error |
| 65 | for range manifestAttempts { |
| 66 | var m Manifest |
| 67 | b, etag, err := st.Get(ctx, key) |
| 68 | ifMatch := etag |
| 69 | switch { |
| 70 | case errors.Is(err, os.ErrNotExist): |
| 71 | ifMatch = "" // create-only: no manifest yet |
| 72 | case err != nil: |
| 73 | return Manifest{}, err |
| 74 | default: |
| 75 | if err := json.Unmarshal(b, &m); err != nil { |
| 76 | return Manifest{}, err |
| 77 | } |
| 78 | } |
| 79 | |
| 80 | if err := apply(&m); err != nil { |
| 81 | return Manifest{}, err |
| 82 | } |
| 83 | m.Schema = Schema |
| 84 | body, err := json.Marshal(m) |
| 85 | if err != nil { |
| 86 | return Manifest{}, err |
| 87 | } |
| 88 | if _, err := st.Put(ctx, key, body, ifMatch); err != nil { |
| 89 | if errors.Is(err, release.ErrCASConflict) { |
| 90 | lastErr = err |
| 91 | continue |
| 92 | } |
| 93 | return Manifest{}, err |
| 94 | } |
| 95 | return m, nil |
| 96 | } |
| 97 | return Manifest{}, lastErr |
| 98 | } |
| 99 | |