carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package registry
2
3import (
4 "bytes"
5 "context"
6 "database/sql"
7 "encoding/json"
8 "errors"
9 "fmt"
10 "strings"
11)
12
13var ErrClaimOwnership = errors.New("registry: claim row ownership changed or unknown")
14
15type ClaimOwnership struct {
16 ClaimIncarnation string
17 ClaimStamp uint64
18 ClaimOwner string
19 ClaimRowID string
20}
21
22func (o ClaimOwnership) Valid() bool {
23 return o.ClaimIncarnation != "" && o.ClaimStamp != 0 && o.ClaimOwner != "" && o.ClaimRowID != ""
24}
25
26type ClaimRow struct {
27 AllowAbsent bool `json:",omitempty"`
28 Kind string
29 Host string
30 Name string
31 Registration string
32 Ownership ClaimOwnership
33}
34type ClaimRowArchive struct {
35 Row ClaimRow
36 Contents json.RawMessage
37}
38
39func (r *Registry) migrateClaimOwnership() error {
40 for _, table := range []string{"routes", "timers"} {
41 for _, column := range []string{"claim_incarnation TEXT NOT NULL DEFAULT ''", "claim_stamp INTEGER NOT NULL DEFAULT 0", "claim_owner TEXT NOT NULL DEFAULT ''", "claim_row_id TEXT NOT NULL DEFAULT ''"} {
42 if _, err := r.sql.Exec("ALTER TABLE " + table + " ADD COLUMN " + column); err != nil && !strings.Contains(err.Error(), "duplicate column") {
43 return err
44 }
45 }
46 }
47 _, err := r.sql.Exec(`CREATE TABLE IF NOT EXISTS claim_row_retirements (
48 journal TEXT NOT NULL, kind TEXT NOT NULL, host TEXT NOT NULL, name TEXT NOT NULL,
49 row_id TEXT NOT NULL, archive BLOB NOT NULL,
50 PRIMARY KEY(journal,kind,host,name,row_id))`)
51 return err
52}
53
54// AdoptClaimRoute is called only by the complete-census backfill coordinator.
55// The full observed row is compared inside the write transaction.
56func (r *Registry) AdoptClaimRoute(ctx context.Context, observed Route, owner ClaimOwnership) error {
57 if r.remote != nil {
58 _, err := r.remote.call(ctx, remoteRequest{Method: "AdoptClaimRoute", Observed: observed, Owner: owner}, nil)
59 return err
60 }
61
62 if !owner.Valid() || observed.ClaimOwnership != (ClaimOwnership{}) {
63 return ErrClaimOwnership
64 }
65 tx, err := r.beginClaimTransaction(ctx, true)
66 if err != nil {
67 return err
68 }
69 defer tx.Rollback()
70 current, err := scanRoute(tx.QueryRowContext(ctx, `SELECT `+routeCols+` FROM routes WHERE host=?`, observed.Host).Scan)
71 if err != nil {
72 return err
73 }
74 a, _ := json.Marshal(current)
75 b, _ := json.Marshal(observed)
76 if !bytes.Equal(a, b) {
77 return ErrClaimOwnership
78 }
79 res, err := tx.ExecContext(ctx, `UPDATE routes SET claim_incarnation=?,claim_stamp=?,claim_owner=?,claim_row_id=? WHERE host=? AND claim_owner='' AND claim_stamp=0 AND claim_incarnation='' AND claim_row_id=''`, owner.ClaimIncarnation, owner.ClaimStamp, owner.ClaimOwner, owner.ClaimRowID, observed.Host)
80 if err != nil {
81 return err
82 }
83 n, err := res.RowsAffected()
84 if err != nil {
85 return err
86 }
87 if n != 1 {
88 return ErrClaimOwnership
89 }
90 return tx.Commit()
91}
92
93// RetireClaimRows archives the exact transaction snapshot before deletion.
94// A missing or replacement row is successful only when this journal's committed
95// archive proves the predecessor was already removed by an earlier attempt.
96func (r *Registry) RetireClaimRows(ctx context.Context, journal string, targets []ClaimRow, archive func([]ClaimRowArchive) error) error {
97 if archive == nil {
98 return ErrClaimOwnership
99 }
100 if r.remote != nil {
101 _, err := r.remote.call(ctx, remoteRequest{Method: "RetireClaimRows", Journal: journal, Targets: targets}, func(out remoteResponse) error { return archive(out.Archives) })
102 return err
103 }
104
105 if journal == "" || len(targets) == 0 || archive == nil {
106 return ErrClaimOwnership
107 }
108 tx, err := r.beginClaimTransaction(ctx, true)
109 if err != nil {
110 return err
111 }
112 defer tx.Rollback()
113 archives := make([]ClaimRowArchive, 0, len(targets))
114 pending := make([]bool, len(targets))
115 seen := map[string]bool{}
116 for i, t := range targets {
117 if t.AllowAbsent && t.Kind != "timer" {
118 return ErrClaimOwnership
119 }
120 if !t.Ownership.Valid() || t.Host == "" || (t.Kind != "route" && t.Kind != "timer") || (t.Kind == "timer" && (t.Name == "" || t.Registration == "")) || (t.Kind == "route" && (t.Name != "" || t.Registration != "")) {
121 return ErrClaimOwnership
122 }
123 key := t.Kind + "\x00" + t.Host + "\x00" + t.Name
124 if seen[key] {
125 return ErrClaimOwnership
126 }
127 seen[key] = true
128 var saved []byte
129 err = tx.QueryRowContext(ctx, `SELECT archive FROM claim_row_retirements WHERE journal=? AND kind=? AND host=? AND name=? AND row_id=?`, journal, t.Kind, t.Host, t.Name, t.Ownership.ClaimRowID).Scan(&saved)
130 if err == nil {
131 var a ClaimRowArchive
132 if json.Unmarshal(saved, &a) != nil || a.Row != t || len(a.Contents) == 0 {
133 return ErrClaimOwnership
134 }
135 archives = append(archives, a)
136 continue
137 }
138 if !errors.Is(err, sql.ErrNoRows) {
139 return err
140 }
141 var content []byte
142 switch t.Kind {
143 case "route":
144 row, e := scanRoute(tx.QueryRowContext(ctx, `SELECT `+routeCols+` FROM routes WHERE host=?`, t.Host).Scan)
145 if e != nil {
146 return fmt.Errorf("%w: %v", ErrClaimOwnership, e)
147 }
148 if row.ClaimOwnership != t.Ownership {
149 return ErrClaimOwnership
150 }
151 content, err = json.Marshal(row)
152 case "timer":
153 row, e := scanTimer(tx.QueryRowContext(ctx, `SELECT `+timerCols+` FROM timers WHERE host=? AND name=?`, t.Host, t.Name))
154 if errors.Is(e, sql.ErrNoRows) && t.AllowAbsent {
155 content, err = []byte("null"), nil
156 break
157 }
158 if e != nil {
159 return fmt.Errorf("%w: %v", ErrClaimOwnership, e)
160 }
161 if row.ClaimOwnership != t.Ownership || row.Registration != t.Registration {
162 return ErrClaimOwnership
163 }
164 content, err = json.Marshal(struct {
165 Timer Timer
166 Registration string
167 }{row, row.Registration})
168 }
169 if err != nil {
170 return err
171 }
172 archives = append(archives, ClaimRowArchive{Row: t, Contents: content})
173 pending[i] = true
174 }
175 if err = archive(archives); err != nil {
176 return err
177 }
178 for i, t := range targets {
179 if !pending[i] {
180 continue
181 }
182 a, err := json.Marshal(archives[i])
183 if err != nil {
184 return err
185 }
186 if _, err = tx.ExecContext(ctx, `INSERT INTO claim_row_retirements (journal,kind,host,name,row_id,archive) VALUES (?,?,?,?,?,?)`, journal, t.Kind, t.Host, t.Name, t.Ownership.ClaimRowID, a); err != nil {
187 return err
188 }
189 table := "routes"
190 q := ""
191 args := []any{t.Host, t.Ownership.ClaimIncarnation, t.Ownership.ClaimStamp, t.Ownership.ClaimOwner, t.Ownership.ClaimRowID}
192 if t.Kind == "timer" {
193 table = "timers"
194 q = " AND name=? AND registration=?"
195 args = append(args, t.Name, t.Registration)
196 }
197 res, e := tx.ExecContext(ctx, `DELETE FROM `+table+` WHERE host=? AND claim_incarnation=? AND claim_stamp=? AND claim_owner=? AND claim_row_id=?`+q, args...)
198 if e != nil {
199 return e
200 }
201 n, e := res.RowsAffected()
202 if e != nil {
203 return e
204 }
205 if n != 1 && !(n == 0 && t.AllowAbsent && string(archives[i].Contents) == "null") {
206 return ErrClaimOwnership
207 }
208 }
209 return tx.Commit()
210}
211
212func (r *Registry) AdoptClaimTimer(ctx context.Context, observed Timer, owner ClaimOwnership) error {
213 if r.remote != nil {
214 _, err := r.remote.call(ctx, remoteRequest{Method: "AdoptClaimTimer", ObservedValue: observed, Owner: owner}, nil)
215 return err
216 }
217
218 if !owner.Valid() || observed.ClaimOwnership != (ClaimOwnership{}) || observed.Registration == "" || owner.ClaimRowID != observed.Registration {
219 return ErrClaimOwnership
220 }
221 tx, err := r.beginClaimTransaction(ctx, true)
222 if err != nil {
223 return err
224 }
225 defer tx.Rollback()
226 current, err := scanTimer(tx.QueryRowContext(ctx, `SELECT `+timerCols+` FROM timers WHERE host=? AND name=?`, observed.Host, observed.Name))
227 if err != nil {
228 return err
229 }
230 if current != observed {
231 return ErrClaimOwnership
232 }
233 res, err := tx.ExecContext(ctx, `UPDATE timers SET claim_incarnation=?,claim_stamp=?,claim_owner=?,claim_row_id=? WHERE host=? AND name=? AND registration=? AND claim_owner='' AND claim_stamp=0 AND claim_incarnation='' AND claim_row_id=''`, owner.ClaimIncarnation, owner.ClaimStamp, owner.ClaimOwner, owner.ClaimRowID, observed.Host, observed.Name, observed.Registration)
234 if err != nil {
235 return err
236 }
237 n, err := res.RowsAffected()
238 if err != nil {
239 return err
240 }
241 if n != 1 {
242 return ErrClaimOwnership
243 }
244 return tx.Commit()
245}
246