carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3import (
4 "context"
5 "database/sql"
6 "errors"
7 "os"
8 "time"
9
10 "github.com/carlosframework/platform/internal/hostops"
11 "github.com/carlosframework/platform/internal/release"
12)
13
14// materializeClaimApp is a claim-enabled console's projection of one app's
15// canonical state into its own SQL row. Flag-off materialization never calls
16// it. It reads canonical admission and the marker, and writes only through
17// Store.WithAppMutation with a grant derived from that canonical evidence:
18//
19// in-flight purge journal -> hold
20// last_purge retires this row's app_id -> replay that purge's SQL cleanup
21// (completed retirement, or a successor incarnation that replaced it)
22// closed by any operation -> hold (the operation owns the app)
23// another incarnation, no evidence -> hold
24// exact marker, row live -> trash with the marker's clock
25// exact marker, row restoring -> release the stale restoring slot
26// no marker, completed receipt -> replay that receipt's restore
27// no marker, no receipt -> hold: absence alone never restores
28//
29// Restore and trash projection acquire a short exclusive app mutation before
30// checking evidence and writing SQL. The receipt alone cannot exclude a new
31// operation on another console. Purge replay is narrower: positive retirement
32// evidence grants deletion of only the retired incarnation's local rows.
33// The admission sweep finishes interrupted restore effects. A missing marker
34// without a receipt never restores a row.
35func materializeClaimApp(ctx context.Context, bucket release.Store, st *Store, accountID int64, account, app string, now time.Time) (bool, error) {
36 a, err := hostops.ReadCanonicalAdmission(ctx, bucket, account, app)
37 if errors.Is(err, os.ErrNotExist) {
38 return true, nil
39 }
40 if err != nil {
41 return true, err
42 }
43 id, err := st.ClaimAppID(accountID, app)
44 if errors.Is(err, sql.ErrNoRows) {
45 return true, nil
46 }
47 if err != nil {
48 return true, err
49 }
50 if a.Value.LastPurge != nil && purgeRetired(a.Value, id, a.Value.LastPurge.ID) {
51 return projectClaimApp(ctx, bucket, st, accountID, account, app, now)
52 }
53 if a.Value.AppID != id || a.Value.State != "open" || a.Value.Op != nil {
54 return true, nil
55 }
56 err = release.WithAppMutation(ctx, bucket, account, app, "projection", func(ctx context.Context) error {
57 _, err := projectClaimApp(ctx, bucket, st, accountID, account, app, now)
58 return err
59 })
60 if errors.Is(err, hostops.ErrPending) {
61 return true, nil
62 }
63 return true, err
64}
65func projectClaimApp(ctx context.Context, bucket release.Store, st *Store, accountID int64, account, app string, now time.Time) (bool, error) {
66 if err := purgeJournalAbsent(ctx, bucket, account, app); err != nil {
67 if errors.Is(err, hostops.ErrPending) {
68 return true, nil
69 }
70 return true, err
71 }
72 a, err := hostops.ReadCanonicalAdmission(ctx, bucket, account, app)
73 if errors.Is(err, os.ErrNotExist) {
74 // No canonical identity: nothing here may be written until the signed
75 // backfill or enrollment binds one.
76 return true, nil
77 }
78 if err != nil {
79 return true, err
80 }
81 id, err := st.ClaimAppID(accountID, app)
82 if errors.Is(err, sql.ErrNoRows) {
83 return true, nil
84 }
85 if err != nil {
86 return true, err // an unbound row waits for explicit identity binding
87 }
88 scope := claimSQLScope{AccountID: accountID, App: app, AppID: id}
89 if lp := a.Value.LastPurge; lp != nil && purgeRetired(a.Value, id, lp.ID) {
90 // The retired incarnation's terminal replay, keyed on its own app_id,
91 // so it still runs on a console that lagged past a recreation. The
92 // revision is the canonical object's, which only grows across
93 // recreation.
94 op := lp.ID
95 return true, st.WithAppMutation(ctx, []sqlGrant{purgeReceiptGrant(bucket, account, scope, op)}, func(v *Store) error {
96 if err := v.BindClaimAppOperation(accountID, app, id, op, a.Value.Revision); err != nil {
97 return err
98 }
99 if err := v.MarkClaimAppRetired(accountID, app, id, now); err != nil {
100 return err
101 }
102 return v.PurgeClaimAppRow(ctx, accountID, app, id, op)
103 })
104 }
105 if id != a.Value.AppID || hostops.RequireAppMutation(ctx, bucket, account, app, id) != nil {
106 // An exclusive operation owns the app, a retirement names some other
107 // incarnation, or an open successor must never be projected onto an
108 // old incarnation's row without last_purge naming it.
109 return true, nil
110 }
111 life, err := st.AppLife(accountID, app)
112 if err != nil || life == nil {
113 return true, err
114 }
115 tomb, tag, err := readTombstoneVersion(ctx, bucket, account, app)
116 if err != nil {
117 return true, err
118 }
119 if tomb != nil {
120 switch {
121 case !life.Trashed():
122 return true, st.WithAppMutation(ctx, []sqlGrant{tombstoneGrant(ctx, bucket, account, scope, tag)}, func(v *Store) error {
123 _, err := v.TrashClaimAppRow(accountID, app, id, tomb.DeletedAt)
124 return err
125 })
126 case life.Restoring:
127 return true, st.WithAppMutation(ctx, []sqlGrant{tombstoneGrant(ctx, bucket, account, scope, tag)}, func(v *Store) error {
128 _, err := v.SetClaimRestoring(accountID, app, id, false)
129 return err
130 })
131 }
132 return true, nil
133 }
134 if !life.Trashed() {
135 return true, nil
136 }
137 receipt := a.Value.LastRestore
138 if receipt == nil || !restoreReceiptCurrent(a.Value, *receipt) || receipt.AppID != id || life.DeletedAt != receipt.DeletedAt {
139 // No completed receipt for this incarnation, or the row's trash is
140 // not the one the receipt removed (a later trash): hold.
141 return true, nil
142 }
143 r := *receipt
144 return true, st.WithAppMutation(ctx, []sqlGrant{restoreReceiptGrant(ctx, bucket, account, scope, r)}, func(v *Store) error {
145 _, err := v.FinishClaimRestoreRow(accountID, app, id, r.DeletedAt, now)
146 return err
147 })
148}
149