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 "crypto/rand"
6 "database/sql"
7 "database/sql/driver"
8 "encoding/hex"
9 "encoding/json"
10 "errors"
11 "fmt"
12 "github.com/carlosframework/platform/internal/hostops"
13 "log"
14 "time"
15)
16
17// These tables exist only in the claim-on console's existing durable DB.
18// The lexer blocks all ordinary handles from naming either table.
19func initSQLReceipts(db *sql.DB, instance string) error {
20 _, err := db.Exec(`CREATE TABLE IF NOT EXISTS claim_sql_identity (id TEXT PRIMARY KEY, instance TEXT NOT NULL DEFAULT '');
21 CREATE TABLE IF NOT EXISTS claim_sql_receipts (effect TEXT PRIMARY KEY, scope TEXT NOT NULL, state TEXT NOT NULL CHECK(state IN ('committed','fenced')));`)
22 if err != nil {
23 return err
24 }
25 var hasInstance int
26 if err = db.QueryRow(`SELECT count(*) FROM pragma_table_info('claim_sql_identity') WHERE name = 'instance'`).Scan(&hasInstance); err != nil {
27 return err
28 }
29 if hasInstance == 0 {
30 if _, err = db.Exec(`ALTER TABLE claim_sql_identity ADD COLUMN instance TEXT NOT NULL DEFAULT ''`); err != nil {
31 return err
32 }
33 }
34 var nonce [24]byte
35 if _, err = rand.Read(nonce[:]); err != nil {
36 return err
37 }
38 if _, err = db.Exec(`INSERT INTO claim_sql_identity(id, instance) SELECT ?, ? WHERE NOT EXISTS(SELECT 1 FROM claim_sql_identity)`, hex.EncodeToString(nonce[:]), instance); err != nil {
39 return err
40 }
41 // A row from before the instance was bound takes this instance. That is
42 // safe because the instance is part of the database ID: effects bound to
43 // the old ID stay unsettled here, and a clone adopting the same row gets
44 // an ID of its own.
45 if _, err = db.Exec(`UPDATE claim_sql_identity SET instance = ? WHERE instance = ''`, instance); err != nil {
46 return err
47 }
48 var rows int
49 var stored string
50 if err = db.QueryRow(`SELECT count(*), COALESCE(MIN(instance), '') FROM claim_sql_identity`).Scan(&rows, &stored); err != nil {
51 return err
52 }
53 if rows != 1 {
54 return fmt.Errorf("claim SQL receipt database has %d identity rows: %w", rows, hostops.ErrIdentity)
55 }
56 if stored == instance {
57 return nil
58 }
59 // Restored onto another instance (review I2-2): adopt it under a fresh
60 // nonce, so this copy has a database ID no other copy has had, even one
61 // moved back to an instance it lived on before. Copied receipts still
62 // cannot settle the original's effects, because every settlement compares
63 // the effect's database ID first; those effects stay unsettled and are
64 // alerted past sql_recover_after.
65 if _, err = rand.Read(nonce[:]); err != nil {
66 return err
67 }
68 if _, err = db.Exec(`UPDATE claim_sql_identity SET id = ?, instance = ?`, hex.EncodeToString(nonce[:]), instance); err != nil {
69 return err
70 }
71 log.Printf("console: claim SQL receipt database moved from instance %s to %s; it has a new database ID, and effects bound to the old one stay unsettled", stored, instance)
72 return nil
73}
74
75// databaseID is the receipt database's full identity: the stored nonce, the
76// instance verified at open, and the physical file, re-read on every call so
77// a database replaced under the running process is noticed.
78func (g *claimGate) databaseID(nonce string) (string, error) {
79 physical, err := sqlPhysicalIdentity(g.path)
80 if err != nil {
81 return "", err
82 }
83 if physical != g.physical || g.instance == "" {
84 return "", hostops.ErrIdentity
85 }
86 return nonce + ":" + g.instance + ":" + physical, nil
87}
88func cloneSQLReceiptTables(in map[string]claimSQLTable) map[string]claimSQLTable {
89 out := make(map[string]claimSQLTable, len(in)+2)
90 for k, v := range in {
91 out[k] = v
92 }
93 out["claim_sql_identity"] = claimSQLTable{NonApp: "private SQL receipt database identity; ordinary SQL handles refuse access"}
94 out["claim_sql_receipts"] = claimSQLTable{NonApp: "private effect receipts and fences; ordinary SQL handles refuse access"}
95 return out
96}
97func (s *Store) sqlDatabaseID() (string, error) {
98 if s == nil || s.gate == nil || s.rawBoot == nil {
99 return "", hostops.ErrContract
100 }
101 var id string
102 err := s.rawBoot.QueryRow(`SELECT id FROM claim_sql_identity`).Scan(&id)
103 if err != nil {
104 return "", err
105 }
106 return s.gate.databaseID(id)
107}
108
109// FenceSQL serializes with the original transaction on SQLite's writer lock.
110// Inserting the durable fence even when a receipt is absent closes the
111// issued-before-BEGIN crash window. A delayed BEGIN can only refuse it.
112func (s *Store) FenceSQL(ctx context.Context, sc hostops.SQLReceiptScope) error {
113 _, err := s.FenceSQLOutcome(ctx, sc)
114 return err
115}
116
117// FenceSQLOutcome is FenceSQL that also reports whether the original
118// transaction's committed receipt was found, so recovery records what it
119// proved rather than calling every outcome a fence.
120func (s *Store) FenceSQLOutcome(ctx context.Context, sc hostops.SQLReceiptScope) (committed bool, err error) {
121 if s.gate == nil || sc.Effect == "" || sc.Operation == "" || sc.AppID == "" || sc.Epoch == 0 {
122 return false, hostops.ErrIdentity
123 }
124 id, err := s.sqlDatabaseID()
125 if err != nil {
126 return false, err
127 }
128 if id != sc.Database {
129 return false, hostops.ErrIdentity
130 }
131 body, err := json.Marshal(sc)
132 if err != nil {
133 return false, err
134 }
135 conn, err := s.rawBoot.Conn(ctx)
136 if err != nil {
137 return false, err
138 }
139 defer conn.Close()
140 if _, err = conn.ExecContext(ctx, "BEGIN IMMEDIATE"); err != nil {
141 return false, err
142 }
143 defer conn.ExecContext(context.Background(), "ROLLBACK")
144 var have, state string
145 err = conn.QueryRowContext(ctx, `SELECT scope, state FROM claim_sql_receipts WHERE effect=?`, sc.Effect).Scan(&have, &state)
146 if err != nil && !errors.Is(err, sql.ErrNoRows) {
147 return false, err
148 }
149 if err == nil && have != string(body) {
150 return false, hostops.ErrIdentity
151 }
152 if _, err = conn.ExecContext(ctx, `INSERT INTO claim_sql_receipts(effect,scope,state) VALUES(?,?,'fenced') ON CONFLICT(effect) DO NOTHING`, sc.Effect, string(body)); err != nil {
153 return false, err
154 }
155 if _, err = conn.ExecContext(ctx, "COMMIT"); err != nil {
156 return false, err
157 }
158 return state == "committed", nil
159}
160
161type claimSavepoint struct{ c *claimConn }
162
163func (t *claimSavepoint) Commit() error {
164 return t.c.innerExec(context.Background(), "RELEASE claim_batch_statement")
165}
166func (t *claimSavepoint) Rollback() error {
167 if err := t.c.innerExec(context.Background(), "ROLLBACK TO claim_batch_statement"); err != nil {
168 return err
169 }
170 return t.c.innerExec(context.Background(), "RELEASE claim_batch_statement")
171}
172func (c *claimConn) beginBatch(ctx context.Context, b *claimBinding) error {
173 // IMMEDIATE is required: a paused deferred read transaction cannot upgrade
174 // after recovery inserted a fence using another connection.
175 if err := c.innerExec(ctx, "BEGIN IMMEDIATE"); err != nil {
176 return err
177 }
178 b.outer = &claimBatchTx{c: c}
179 if len(b.receipts) == 0 {
180 return nil
181 }
182 for _, sc := range b.receipts {
183 rows, err := c.inner.QueryContext(ctx, `SELECT id FROM claim_sql_identity`, nil)
184 if err != nil {
185 return err
186 }
187 vals := make([]driver.Value, 1)
188 err = rows.Next(vals)
189 rows.Close()
190 if err != nil {
191 return err
192 }
193 nonce, ok := vals[0].(string)
194 if !ok {
195 return hostops.ErrIdentity
196 }
197 id, err := c.g.databaseID(nonce)
198 if err != nil {
199 return err
200 }
201 if id != sc.Database {
202 return hostops.ErrIdentity
203 }
204 // Any prior terminal row makes replay impossible, including a recovery
205 // fence written before the first SQL call reached this connection.
206 rows, err = c.inner.QueryContext(ctx, `SELECT count(*) FROM claim_sql_receipts WHERE effect=?`, []driver.NamedValue{{Ordinal: 1, Value: sc.Effect}})
207 if err != nil {
208 return err
209 }
210 vals = make([]driver.Value, 1)
211 err = rows.Next(vals)
212 rows.Close()
213 n, _ := vals[0].(int64)
214 if err != nil {
215 return err
216 }
217 if n != 0 {
218 return hostops.ErrPending
219 }
220 }
221 return nil
222}
223
224type claimBatchTx struct{ c *claimConn }
225
226func (t *claimBatchTx) Commit() error { return t.c.innerExec(context.Background(), "COMMIT") }
227func (t *claimBatchTx) Rollback() error { return t.c.innerExec(context.Background(), "ROLLBACK") }
228func (c *claimConn) endBatch(b *claimBinding, commit bool) error {
229 if b.outer == nil {
230 return hostops.ErrContract
231 }
232 // The caller sealed every public handle; this runs under the driver lock,
233 // so nothing else is in flight. Finish what the handles left behind.
234 b.sealed.Store(true)
235 c.closeRows()
236 if c.tx != nil {
237 c.tx.abort()
238 if commit {
239 // A transaction left open means the requested commit cannot be
240 // honoured: roll back and say so, never report success.
241 err := fmt.Errorf("%w: transaction still open at batch commit", ErrClaimSQLScope)
242 if rerr := b.outer.Rollback(); rerr != nil {
243 c.bad = true
244 err = errors.Join(err, rerr)
245 }
246 b.outer = nil
247 return err
248 }
249 }
250 if commit {
251 err := b.lostErr()
252 if err == nil {
253 err = b.authorizeCommit(b.ctx, c)
254 }
255 if err != nil {
256 if rerr := b.outer.Rollback(); rerr != nil {
257 c.bad = true
258 err = errors.Join(err, rerr)
259 }
260 b.outer = nil
261 return err
262 }
263 }
264 if commit {
265 for _, receipt := range b.receipts {
266 body, err := json.Marshal(receipt)
267 if err != nil {
268 return err
269 }
270 if err = c.innerExec(b.ctx, `INSERT INTO claim_sql_receipts(effect,scope,state) VALUES(?,?,'committed')`, receipt.Effect, string(body)); err != nil {
271 return err
272 }
273 }
274 }
275 var err error
276 if commit {
277 err = b.outer.Commit()
278 } else {
279 err = b.outer.Rollback()
280 }
281 b.outer = nil
282 if err != nil {
283 c.bad = true
284 }
285 return err
286}
287
288// A returned SQL error is not necessarily ambiguous: after handles joined,
289// the same positive local fence decides it immediately. Keep the callback's
290// error for its caller while settling authority only after this proof.
291func (s *Store) finishPreparedSQL(ctx context.Context, p *hostops.PreparedSQLPermit, callErr error) error {
292 if callErr != nil {
293 proofCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 20*time.Second)
294 defer cancel()
295 if err := s.FenceSQL(proofCtx, p.Receipt()); err != nil {
296 return errors.Join(err, p.FinishTransaction(callErr))
297 }
298 }
299 return p.FinishTransaction(nil)
300}
301