@serfab/cadre-core 1.17.0 → 1.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/cadre-invite-protocol.d.ts +35 -13
- package/dist/cadre-invite-protocol.js +90 -25
- package/dist/cadre-invite-protocol.js.map +1 -1
- package/dist/cadre-node.d.ts +586 -255
- package/dist/cadre-node.js +1182 -566
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-cohort.d.ts +66 -9
- package/dist/control-cohort.js +67 -9
- package/dist/control-cohort.js.map +1 -1
- package/dist/control-database.d.ts +451 -193
- package/dist/control-database.js +701 -359
- package/dist/control-database.js.map +1 -1
- package/dist/control-formation-recorder.d.ts +46 -43
- package/dist/control-formation-recorder.js +77 -78
- package/dist/control-formation-recorder.js.map +1 -1
- package/dist/control-protocol-guard.d.ts +6 -0
- package/dist/control-protocol-guard.js +14 -2
- package/dist/control-protocol-guard.js.map +1 -1
- package/dist/control-schema-version.d.ts +48 -31
- package/dist/control-schema-version.js +67 -45
- package/dist/control-schema-version.js.map +1 -1
- package/dist/control-schema.d.ts +8 -8
- package/dist/control-schema.js +16 -16
- package/dist/control-schema.js.map +1 -1
- package/dist/control-write-retry.d.ts +42 -7
- package/dist/control-write-retry.js +87 -19
- package/dist/control-write-retry.js.map +1 -1
- package/dist/enrolled-machine-store.d.ts +3 -3
- package/dist/enrolled-machine-store.js +3 -3
- package/dist/formation-token.d.ts +48 -0
- package/dist/formation-token.js +26 -0
- package/dist/formation-token.js.map +1 -0
- package/dist/index.d.ts +15 -14
- package/dist/index.js +17 -14
- package/dist/index.js.map +1 -1
- package/dist/joined-strand-store.d.ts +26 -9
- package/dist/joined-strand-store.js +63 -24
- package/dist/joined-strand-store.js.map +1 -1
- package/dist/link-budget.d.ts +9 -7
- package/dist/link-budget.js +9 -7
- package/dist/link-budget.js.map +1 -1
- package/dist/member-redemption.d.ts +15 -9
- package/dist/member-redemption.js +13 -8
- package/dist/member-redemption.js.map +1 -1
- package/dist/membership-view.d.ts +91 -0
- package/dist/membership-view.js +110 -0
- package/dist/membership-view.js.map +1 -0
- package/dist/owner-grant-protocol.d.ts +11 -7
- package/dist/owner-grant-protocol.js +31 -17
- package/dist/owner-grant-protocol.js.map +1 -1
- package/dist/peer-addr-book.d.ts +10 -1
- package/dist/peer-addr-book.js +4 -3
- package/dist/peer-addr-book.js.map +1 -1
- package/dist/peer-join-backfill.d.ts +96 -30
- package/dist/peer-join-backfill.js +272 -128
- package/dist/peer-join-backfill.js.map +1 -1
- package/dist/pending-join-runner.d.ts +48 -18
- package/dist/pending-join-runner.js +74 -56
- package/dist/pending-join-runner.js.map +1 -1
- package/dist/seed-bootstrap.d.ts +47 -40
- package/dist/seed-bootstrap.js +66 -61
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/self-address-update.d.ts +13 -0
- package/dist/self-address-update.js +19 -0
- package/dist/self-address-update.js.map +1 -0
- package/dist/strand-database.d.ts +23 -2
- package/dist/strand-database.js +26 -6
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-first-sync-gate.d.ts +176 -89
- package/dist/strand-first-sync-gate.js +288 -122
- package/dist/strand-first-sync-gate.js.map +1 -1
- package/dist/strand-formation-deadlines.d.ts +9 -9
- package/dist/strand-formation-deadlines.js +9 -9
- package/dist/strand-formation-manager.d.ts +23 -17
- package/dist/strand-formation-manager.js +46 -44
- package/dist/strand-formation-manager.js.map +1 -1
- package/dist/strand-formation-protocol.d.ts +5 -8
- package/dist/strand-formation-protocol.js +101 -16
- package/dist/strand-formation-protocol.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +26 -17
- package/dist/strand-instance-manager.js +119 -50
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-membership-reconciler.d.ts +16 -13
- package/dist/strand-membership-reconciler.js +28 -22
- package/dist/strand-membership-reconciler.js.map +1 -1
- package/dist/strand-membership-writer.d.ts +40 -28
- package/dist/strand-membership-writer.js +212 -153
- package/dist/strand-membership-writer.js.map +1 -1
- package/dist/strand-move.js +5 -2
- package/dist/strand-move.js.map +1 -1
- package/dist/strand-revocation-enforcer.d.ts +134 -60
- package/dist/strand-revocation-enforcer.js +144 -71
- package/dist/strand-revocation-enforcer.js.map +1 -1
- package/dist/strand-solicitation.d.ts +50 -39
- package/dist/strand-solicitation.js +10 -3
- package/dist/strand-solicitation.js.map +1 -1
- package/dist/strand-watcher.d.ts +80 -13
- package/dist/strand-watcher.js +152 -48
- package/dist/strand-watcher.js.map +1 -1
- package/dist/table-change-watch.d.ts +63 -0
- package/dist/table-change-watch.js +209 -0
- package/dist/table-change-watch.js.map +1 -0
- package/dist/timeout-scheduler.d.ts +4 -3
- package/dist/timeout-scheduler.js +4 -3
- package/dist/timeout-scheduler.js.map +1 -1
- package/dist/types.d.ts +90 -49
- package/dist/types.js +4 -4
- package/dist/types.js.map +1 -1
- package/package.json +8 -8
- package/src/cadre-invite-protocol.ts +119 -31
- package/src/cadre-node.ts +1289 -588
- package/src/control-cohort.ts +86 -9
- package/src/control-database.ts +848 -403
- package/src/control-formation-recorder.ts +359 -353
- package/src/control-protocol-guard.ts +16 -2
- package/src/control-schema-version.ts +101 -51
- package/src/control-schema.ts +16 -16
- package/src/control-write-retry.ts +91 -19
- package/src/delegate-admission.ts +351 -351
- package/src/enrolled-machine-store.ts +3 -3
- package/src/formation-token.ts +63 -0
- package/src/index.ts +30 -18
- package/src/joined-strand-store.ts +69 -24
- package/src/link-budget.ts +9 -7
- package/src/member-redemption.ts +20 -13
- package/src/membership-view.ts +144 -0
- package/src/owner-grant-protocol.ts +33 -18
- package/src/peer-addr-book.ts +4 -3
- package/src/peer-join-backfill.ts +961 -761
- package/src/pending-join-runner.ts +76 -53
- package/src/seed-bootstrap.ts +72 -67
- package/src/self-address-update.ts +19 -0
- package/src/strand-database.ts +352 -322
- package/src/strand-first-sync-gate.ts +334 -134
- package/src/strand-formation-deadlines.ts +227 -227
- package/src/strand-formation-manager.ts +731 -725
- package/src/strand-formation-protocol.ts +122 -24
- package/src/strand-instance-manager.ts +2092 -2026
- package/src/strand-membership-reconciler.ts +31 -25
- package/src/strand-membership-writer.ts +2093 -2019
- package/src/strand-move.ts +5 -2
- package/src/strand-revocation-enforcer.ts +740 -654
- package/src/strand-solicitation.ts +61 -34
- package/src/strand-watcher.ts +175 -56
- package/src/table-change-watch.ts +285 -0
- package/src/timeout-scheduler.ts +29 -28
- package/src/types.ts +89 -51
|
@@ -135,6 +135,21 @@ export function controlNetworkName(partyId: string): string {
|
|
|
135
135
|
return `control-${partyId}`;
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
/** The control database's two storage protocols, `repo` and `cluster`. */
|
|
139
|
+
function controlDatabaseProtocols(partyId: string): readonly string[] {
|
|
140
|
+
const prefix = `/optimystic/${controlNetworkName(partyId)}`;
|
|
141
|
+
return [`${prefix}/repo/1.0.0`, `${prefix}/cluster/1.0.0`];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Whether a peer whose protocol list is `protocols` serves the control database. Optimystic
|
|
146
|
+
* counts a peer into a control block's cohort by the same test (`servesThisNetwork` in db-p2p's
|
|
147
|
+
* `libp2p-key-network.ts`), before its membership check.
|
|
148
|
+
*/
|
|
149
|
+
export function servesControlDatabase(partyId: string, protocols: readonly string[]): boolean {
|
|
150
|
+
return controlDatabaseProtocols(partyId).some((protocol) => protocols.includes(protocol));
|
|
151
|
+
}
|
|
152
|
+
|
|
138
153
|
/**
|
|
139
154
|
* The class of every protocol a control node serves, by exact protocol id: the single
|
|
140
155
|
* declaration the guard enforces. See the module doc for what each class admits and why each
|
|
@@ -152,8 +167,7 @@ export function controlProtocolClasses(partyId: string): ReadonlyMap<string, Con
|
|
|
152
167
|
[WAKE_PROTOCOL, 'members'],
|
|
153
168
|
[STRAND_ADDR_PROTOCOL, 'members'],
|
|
154
169
|
|
|
155
|
-
|
|
156
|
-
[`${prefix}/cluster/1.0.0`, 'members-snapshot'],
|
|
170
|
+
...controlDatabaseProtocols(partyId).map((protocol): [string, ControlProtocolClass] => [protocol, 'members-snapshot']),
|
|
157
171
|
[`${prefix}/db-p2p/sync/1.0.0`, 'members-snapshot'],
|
|
158
172
|
[`${prefix}/db-p2p/block-transfer/1.0.0`, 'members-snapshot'],
|
|
159
173
|
[`${fret}/neighbors`, 'members-snapshot'],
|
|
@@ -3,40 +3,53 @@
|
|
|
3
3
|
* store after declaring the control schema and before applying it.
|
|
4
4
|
*
|
|
5
5
|
* `apply schema CadreControl` runs whatever DDL makes the hydrated catalog equal the declared
|
|
6
|
-
* schema
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* schema, and that apply is how a store written under an older control schema version is
|
|
7
|
+
* migrated in place: between consecutive versions the difference is limited to statements the
|
|
8
|
+
* optimystic module applies to a table's catalog record without rewriting a row, so running the
|
|
9
|
+
* diff from a store at version K to this build's declaration runs each version's changes from
|
|
10
|
+
* K+1 up to the build's, in order. This module decides, from the stored version and the pending
|
|
11
|
+
* DDL and before any of it runs, whether the apply may go ahead, and throws one typed error
|
|
12
|
+
* naming the reason when it may not.
|
|
11
13
|
*
|
|
12
14
|
* The same change classifier ({@link classifyControlSchemaChange}) is what
|
|
13
15
|
* `control-schema-versions.spec.ts` runs over each pair of consecutive control schema versions,
|
|
14
16
|
* so a schema edit that no store could be migrated across fails when it is made. Where the
|
|
15
17
|
* version lives, when it changes and what a new version may change: docs/architecture.md →
|
|
16
|
-
* "Control schema
|
|
18
|
+
* "Control schema versions and migrations".
|
|
17
19
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* (control-store-migrates-on-start) needs the version a store was upgraded to, so it needs that
|
|
25
|
-
* persisted upstream (optimystic-catalog-only-alter-table).
|
|
20
|
+
* The stored tag is the version of the last start that applied the schema to the store: the
|
|
21
|
+
* apply's `alter table OwnerKey set tags (…)` persists since Optimystic 1.16.0, so after a
|
|
22
|
+
* migrating start the store records this build's version. A store additively upgraded by a build
|
|
23
|
+
* from before that still reads the version it was created under, which is harmless: the diff is
|
|
24
|
+
* computed from the catalog, not from the number, and the number only has to tell a store that
|
|
25
|
+
* a newer release migrated from one this build can migrate.
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
28
|
import { Parser, type Database, type SchemaManager, type SqlValue, type TableSchema } from '@quereus/quereus';
|
|
29
29
|
import type { AlterTableStmt, ColumnDef, Statement } from '@quereus/quereus/parser';
|
|
30
|
-
import { CONTROL_SCHEMA_NAME, CONTROL_SCHEMA_VERSION_TAG } from './control-schema.js';
|
|
30
|
+
import { CONTROL_SCHEMA_BASELINE_VERSION, CONTROL_SCHEMA_NAME, CONTROL_SCHEMA_VERSION_TAG } from './control-schema.js';
|
|
31
31
|
import { controlStorageScope } from './storage-scope.js';
|
|
32
32
|
|
|
33
33
|
/** The control table whose tags carry {@link CONTROL_SCHEMA_VERSION_TAG}. */
|
|
34
34
|
const VERSION_TABLE = 'OwnerKey';
|
|
35
35
|
|
|
36
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* Why a warm control store is refused, in the order the check decides them:
|
|
38
|
+
* - `newer-release`: the store records a version above this build's, so a newer release has
|
|
39
|
+
* migrated it (this machine before a downgrade, or another machine sharing its catalog
|
|
40
|
+
* lineage); this machine is the one to update.
|
|
41
|
+
* - `before-migrations`: the store records a version below {@link CONTROL_SCHEMA_BASELINE_VERSION}
|
|
42
|
+
* (written by 1.16.0 or earlier); it cannot be migrated and must be recreated.
|
|
43
|
+
* - `not-migratable`: a pending statement would rewrite or discard rows. Reachable only from a
|
|
44
|
+
* development or hand-made store, since the edit-time check refuses such a version.
|
|
45
|
+
*/
|
|
46
|
+
export type ControlStoreIncompatibility = 'newer-release' | 'before-migrations' | 'not-migratable';
|
|
47
|
+
|
|
48
|
+
/** A warm control store this build cannot apply its control schema to. */
|
|
37
49
|
export class ControlStoreIncompatibleError extends Error {
|
|
50
|
+
readonly reason: ControlStoreIncompatibility;
|
|
38
51
|
readonly partyId: string;
|
|
39
|
-
/** The version the store records: the one
|
|
52
|
+
/** The version the store records: the one its last migrating start declared; 0 = before versions were recorded. */
|
|
40
53
|
readonly storeVersion: number;
|
|
41
54
|
/** The version this build declares. */
|
|
42
55
|
readonly buildVersion: number;
|
|
@@ -45,16 +58,17 @@ export class ControlStoreIncompatibleError extends Error {
|
|
|
45
58
|
/** `controlStorageScope(partyId)`: the storage the embedder deletes to recreate (docs/architecture.md). */
|
|
46
59
|
readonly storageScope: string;
|
|
47
60
|
|
|
48
|
-
constructor(
|
|
61
|
+
constructor(
|
|
62
|
+
reason: ControlStoreIncompatibility,
|
|
63
|
+
partyId: string,
|
|
64
|
+
storeVersion: number,
|
|
65
|
+
buildVersion: number,
|
|
66
|
+
pendingDdl: readonly string[]
|
|
67
|
+
) {
|
|
49
68
|
const storageScope = controlStorageScope(partyId);
|
|
50
|
-
super(
|
|
51
|
-
`The control store for party ${JSON.stringify(partyId)} ${provenance(storeVersion, buildVersion)} ` +
|
|
52
|
-
`(it records control schema version ${storeVersion}; this build declares ${buildVersion}), and this build ` +
|
|
53
|
-
'cannot apply its control schema to it without losing data. Run a release whose control schema ' +
|
|
54
|
-
`matches the store, or recreate the control store (storage scope ${storageScope}) as ` +
|
|
55
|
-
'docs/architecture.md describes.'
|
|
56
|
-
);
|
|
69
|
+
super(refusalMessage(reason, partyId, storeVersion, buildVersion, storageScope));
|
|
57
70
|
this.name = 'ControlStoreIncompatibleError';
|
|
71
|
+
this.reason = reason;
|
|
58
72
|
this.partyId = partyId;
|
|
59
73
|
this.storeVersion = storeVersion;
|
|
60
74
|
this.buildVersion = buildVersion;
|
|
@@ -64,33 +78,55 @@ export class ControlStoreIncompatibleError extends Error {
|
|
|
64
78
|
}
|
|
65
79
|
|
|
66
80
|
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
81
|
+
* The message for each {@link ControlStoreIncompatibility}. Only the two recreate cases advise
|
|
82
|
+
* recreating: a store a newer release migrated holds rows written since, and recreating it
|
|
83
|
+
* would throw them away.
|
|
69
84
|
*/
|
|
70
|
-
function
|
|
71
|
-
|
|
72
|
-
|
|
85
|
+
function refusalMessage(
|
|
86
|
+
reason: ControlStoreIncompatibility,
|
|
87
|
+
partyId: string,
|
|
88
|
+
storeVersion: number,
|
|
89
|
+
buildVersion: number,
|
|
90
|
+
storageScope: string
|
|
91
|
+
): string {
|
|
92
|
+
const store = `The control store for party ${JSON.stringify(partyId)}`;
|
|
93
|
+
const versions = `it records control schema version ${storeVersion}; this build declares ${buildVersion}`;
|
|
94
|
+
const recreate = 'Run a release whose control schema matches the store, or recreate the control store ' +
|
|
95
|
+
`(storage scope ${storageScope}) as docs/architecture.md describes.`;
|
|
96
|
+
switch (reason) {
|
|
97
|
+
case 'newer-release':
|
|
98
|
+
return `${store} was migrated by a newer release (${versions}). Update this app or node to that ` +
|
|
99
|
+
'release or a later one; the store and its rows are intact.';
|
|
100
|
+
case 'before-migrations':
|
|
101
|
+
return `${store} was written before control schema migrations (${versions}; migrations start at ` +
|
|
102
|
+
`version ${CONTROL_SCHEMA_BASELINE_VERSION}, and docs/architecture.md says why earlier stores are not ` +
|
|
103
|
+
`migrated), so this build cannot upgrade it in place. ${recreate}`;
|
|
104
|
+
case 'not-migratable':
|
|
105
|
+
return `${store} cannot be migrated to this build's control schema (${versions}): applying it would ` +
|
|
106
|
+
`rewrite or discard stored rows. ${recreate}`;
|
|
73
107
|
}
|
|
74
|
-
return storeVersion < buildVersion
|
|
75
|
-
? 'was created by an older release'
|
|
76
|
-
: 'was created under the same control schema version';
|
|
77
108
|
}
|
|
78
109
|
|
|
79
110
|
/**
|
|
80
|
-
* Throw {@link ControlStoreIncompatibleError} when
|
|
81
|
-
* hydrated store
|
|
82
|
-
* `declare schema CadreControl` to have run, and runs only `diff schema`, which
|
|
111
|
+
* Throw {@link ControlStoreIncompatibleError} when the declared control schema must not be applied
|
|
112
|
+
* to the hydrated store; return having run nothing otherwise, and the apply then migrates the
|
|
113
|
+
* store. Needs `declare schema CadreControl` to have run, and runs only `diff schema`, which
|
|
114
|
+
* executes no DDL.
|
|
83
115
|
*
|
|
84
116
|
* A cold store (the hydrated catalog lists no control table) passes without a diff: the apply
|
|
85
|
-
* creates everything. A warm store is refused
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
117
|
+
* creates everything. A warm store is refused for the first of these that holds
|
|
118
|
+
* ({@link ControlStoreIncompatibility}):
|
|
119
|
+
* - its version is above the build's: even a diff that only adds (the newer release no longer
|
|
120
|
+
* declares something this build does) would put back what that release removed, and the newer
|
|
121
|
+
* release would then refuse its own store;
|
|
122
|
+
* - its version is below the baseline, whatever the diff: a store written by 1.16.0 or earlier,
|
|
123
|
+
* which the baseline paragraph of docs/architecture.md explains cannot be migrated;
|
|
124
|
+
* - any pending statement is `unmigratable` ({@link classifyControlSchemaChange}).
|
|
91
125
|
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
126
|
+
* Every `additive` and `catalog-only` change passes, so a store from an older migratable
|
|
127
|
+
* release warm-starts and the apply brings it to this build's version. A store that is both
|
|
128
|
+
* older than the baseline and not migratable reports `before-migrations`, whose message gives the
|
|
129
|
+
* recreate advice and points at the explanation.
|
|
94
130
|
*/
|
|
95
131
|
export async function refuseIncompatibleControlStore(db: Database, partyId: string): Promise<void> {
|
|
96
132
|
if (!isWarmControlStore(db)) {
|
|
@@ -99,11 +135,25 @@ export async function refuseIncompatibleControlStore(db: Database, partyId: stri
|
|
|
99
135
|
const storeVersion = schemaVersionOf(db.schemaManager.getTable(CONTROL_SCHEMA_NAME, VERSION_TABLE)?.tags);
|
|
100
136
|
const buildVersion = declaredSchemaVersion(db);
|
|
101
137
|
const pending = await pendingControlSchemaChanges(db);
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
138
|
+
const reason = incompatibilityOf(storeVersion, buildVersion, pending);
|
|
139
|
+
if (reason !== undefined) {
|
|
140
|
+
throw new ControlStoreIncompatibleError(reason, partyId, storeVersion, buildVersion, pending.map((change) => change.ddl));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** The first {@link ControlStoreIncompatibility} that holds for a warm store, or undefined when the apply may migrate it. */
|
|
145
|
+
function incompatibilityOf(
|
|
146
|
+
storeVersion: number,
|
|
147
|
+
buildVersion: number,
|
|
148
|
+
pending: readonly PendingControlSchemaChange[]
|
|
149
|
+
): ControlStoreIncompatibility | undefined {
|
|
150
|
+
if (storeVersion > buildVersion) {
|
|
151
|
+
return 'newer-release';
|
|
152
|
+
}
|
|
153
|
+
if (storeVersion < CONTROL_SCHEMA_BASELINE_VERSION) {
|
|
154
|
+
return 'before-migrations';
|
|
106
155
|
}
|
|
156
|
+
return pending.some((change) => change.kind === 'unmigratable') ? 'not-migratable' : undefined;
|
|
107
157
|
}
|
|
108
158
|
|
|
109
159
|
/** Whether the hydrated catalog lists at least one control table, i.e. the store was written before. */
|
|
@@ -139,11 +189,11 @@ function schemaVersionOf(tags: Readonly<Record<string, SqlValue>> | undefined):
|
|
|
139
189
|
|
|
140
190
|
/**
|
|
141
191
|
* How applying one `diff schema` statement to a warm store treats its rows, from most to least
|
|
142
|
-
* migratable (docs/architecture.md → "Control schema
|
|
192
|
+
* migratable (docs/architecture.md → "Control schema versions and migrations"):
|
|
143
193
|
* - `additive`: a new table, a new index, or a table tag change, which the optimystic module
|
|
144
|
-
* applies
|
|
194
|
+
* applies without touching a row;
|
|
145
195
|
* - `catalog-only`: a change the optimystic module applies to the table's catalog record without
|
|
146
|
-
* rewriting a row
|
|
196
|
+
* rewriting a row (since Optimystic 1.16.0);
|
|
147
197
|
* - `unmigratable`: everything else, which rewrites or discards rows.
|
|
148
198
|
*/
|
|
149
199
|
export type ControlSchemaChangeKind = 'additive' | 'catalog-only' | 'unmigratable';
|
package/src/control-schema.ts
CHANGED
|
@@ -3,20 +3,20 @@ import { Parser } from '@quereus/quereus';
|
|
|
3
3
|
/**
|
|
4
4
|
* The version of the declared control schema, carried in the schema text as the
|
|
5
5
|
* {@link CONTROL_SCHEMA_VERSION_TAG} tag on the `OwnerKey` table. A warm start compares
|
|
6
|
-
* it with the version the store records before applying anything
|
|
7
|
-
* (`control-schema-version.ts
|
|
8
|
-
* created under).
|
|
6
|
+
* it with the version the store records before applying anything, and the apply then
|
|
7
|
+
* migrates the store to this version and records it (`control-schema-version.ts`).
|
|
9
8
|
*
|
|
10
9
|
* When it changes, what a new version may change, and the fixture each version keeps:
|
|
11
|
-
* docs/architecture.md → "Control schema
|
|
10
|
+
* docs/architecture.md → "Control schema versions and migrations", enforced by
|
|
12
11
|
* `control-schema-versions.spec.ts`.
|
|
13
12
|
*/
|
|
14
13
|
export const CONTROL_SCHEMA_VERSION = 3;
|
|
15
14
|
|
|
16
15
|
/**
|
|
17
16
|
* The first control schema version whose fixture is kept, and the oldest a store can be
|
|
18
|
-
* migrated from.
|
|
19
|
-
*
|
|
17
|
+
* migrated from. A store that records a version below it (written by 1.16.0 and earlier,
|
|
18
|
+
* which recorded none) is refused with reason `before-migrations`; docs/architecture.md
|
|
19
|
+
* says why it is not migrated.
|
|
20
20
|
*/
|
|
21
21
|
export const CONTROL_SCHEMA_BASELINE_VERSION = 3;
|
|
22
22
|
|
|
@@ -667,7 +667,7 @@ declare schema CadreControl {
|
|
|
667
667
|
) with context (Now datetime, ValidationKey text null, ValidationSignature text null);
|
|
668
668
|
|
|
669
669
|
-- Serves the per-token reads (the cap count above; cadre-core's countFormationUsage,
|
|
670
|
-
--
|
|
670
|
+
-- readToken and hasOutstandingFormationInvite) on an append-only table. Its cross-machine
|
|
671
671
|
-- convergence is guarded by the integration scenario strand-formation-concurrent-redemption.
|
|
672
672
|
index FormationUsageByToken on FormationUsage (Token);
|
|
673
673
|
|
|
@@ -903,16 +903,15 @@ declare schema CadreControl {
|
|
|
903
903
|
|
|
904
904
|
-- Append-only retirement record for the stamps of removed rows of every guarded table, with
|
|
905
905
|
-- the removed row's key. Every append is owner-signed, so the growth surface is the owner
|
|
906
|
-
-- keys.
|
|
907
|
-
--
|
|
908
|
-
-- table
|
|
909
|
-
-- for on every guarded insert and membership lookup. Readers filter on their own guarded
|
|
906
|
+
-- keys. A party founded before genesis created every control collection may hold one marker
|
|
907
|
+
-- row, ('Revocation', 'ledger', 'opened'), which retires nothing: an owner filed it so this
|
|
908
|
+
-- table had a collection. Nothing files it now. Readers filter on their own guarded
|
|
910
909
|
-- TableName, and queryRevocations skips it. A 'CadreInvite' or 'OwnerGrant' row withdraws
|
|
911
910
|
-- an invitation or a grant whose row stays (the table comments).
|
|
912
911
|
table Revocation (
|
|
913
912
|
TableName text, -- 'OwnerKey' | 'ValidationKey' | 'Strand' | 'StrandPartyKey' | 'JoinedStrand' |
|
|
914
913
|
-- 'JoinRequest' | 'CadrePeer' | 'DeviceToken' | 'CadreInvite' | 'OwnerGrant', or
|
|
915
|
-
-- 'Revocation' for
|
|
914
|
+
-- 'Revocation' for an older party's marker
|
|
916
915
|
RowKey text, -- primary key of the removed row
|
|
917
916
|
StampId text, -- the retired stamp
|
|
918
917
|
ReissuedAt integer default 0, -- bumped by an owner-signed re-issue so a tombstone committed while the
|
|
@@ -942,7 +941,9 @@ declare schema CadreControl {
|
|
|
942
941
|
-- never existed is harmless: stamps carry 128 random bits (generateStampId). For
|
|
943
942
|
-- 'CadreInvite' and 'OwnerGrant' there is no row-gone test: a tombstone there is a
|
|
944
943
|
-- withdrawal, and the row stays (the table comments). The last branch admits the marker
|
|
945
|
-
-- row and nothing else under 'Revocation'.
|
|
944
|
+
-- row an older party may hold, and nothing else under 'Revocation'; nothing files it now.
|
|
945
|
+
-- NOTE: drop this branch, REVOCATION_LEDGER_MARKER and the readers' skip together with the
|
|
946
|
+
-- next CONTROL_SCHEMA_VERSION change that already needs a migration.
|
|
946
947
|
constraint RowIsGone check on insert (
|
|
947
948
|
(new.TableName = 'OwnerKey' and not exists (select 1 from OwnerKey K where K.StampId = new.StampId))
|
|
948
949
|
or (new.TableName = 'CadrePeer' and not exists (select 1 from CadrePeer P where P.StampId = new.StampId))
|
|
@@ -961,9 +962,8 @@ declare schema CadreControl {
|
|
|
961
962
|
-- writer could evict a peer party-wide and block its re-admission. RowKey is not checked
|
|
962
963
|
-- against the retired row here (that would reject a tombstone re-issued later, or filed on
|
|
963
964
|
-- a node that never held the row); each guarded table's RevocationRecorded binds it
|
|
964
|
-
-- instead. Reads live OwnerKey like the sibling tables.
|
|
965
|
-
-- rule too
|
|
966
|
-
-- pair verified here.
|
|
965
|
+
-- instead. Reads live OwnerKey like the sibling tables. An older party's marker was signed
|
|
966
|
+
-- under this rule too. The stored signer pair must be the pair verified here.
|
|
967
967
|
constraint Authorized check on insert (
|
|
968
968
|
exists (select 1 from OwnerKey A where A.Key = context.OwnerKey and not exists (select 1 from Revocation R where R.TableName = 'OwnerKey' and R.StampId = A.StampId) and verify(digest('CadreControl.Revocation', 'remove', party_id(), new.TableName, new.RowKey, new.StampId), context.Signature, A.Key, 'ed25519'))
|
|
969
969
|
and coalesce(new.SignerKey, '') = context.OwnerKey
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CoordinatorPartialCommitError, SyncRetryExhaustedError, TornActionError } from '@optimystic/db-core';
|
|
2
|
-
import { PartialCommitError } from '@optimystic/quereus-plugin-optimystic';
|
|
2
|
+
import { ConcurrentModificationError, PartialCommitError } from '@optimystic/quereus-plugin-optimystic';
|
|
3
3
|
import { causeChain, chainMessages, retryControlOperation } from './control-retry.js';
|
|
4
4
|
import type { ControlRetryOptions } from './control-retry.js';
|
|
5
5
|
|
|
@@ -32,12 +32,13 @@ import type { ControlRetryOptions } from './control-retry.js';
|
|
|
32
32
|
* before any matcher runs. The one half-landed write that IS re-presented is a torn write the
|
|
33
33
|
* library marks final — not saved and unable to land ({@link isFinalTornWrite}).
|
|
34
34
|
*
|
|
35
|
-
*
|
|
35
|
+
* THREE policies ship from here, and the split is deliberate: the default
|
|
36
36
|
* ({@link isRetriableControlWriteFailure}, {@link CONTROL_WRITE_ATTEMPTS}) sits under every
|
|
37
|
-
* control write
|
|
38
|
-
* `ControlDatabase.applyControlSchema` alone
|
|
39
|
-
*
|
|
40
|
-
*
|
|
37
|
+
* control write; {@link SCHEMA_INIT_RETRY_POLICY} is opted into by
|
|
38
|
+
* `ControlDatabase.applyControlSchema` alone; {@link LOST_UPDATE_RETRY_POLICY} is opted into by
|
|
39
|
+
* the `CadrePeer` writers whose bodies re-read the row and decide again. Each widened one absorbs
|
|
40
|
+
* one extra failure class whose re-run safety holds for its opted-in bodies and NOT for writes in
|
|
41
|
+
* general; anything added to the default list must be safe for all ~19 callers.
|
|
41
42
|
*
|
|
42
43
|
* Lives outside `control-database.ts` so a spec can drive the classifier against real engine
|
|
43
44
|
* errors without importing the whole database class. The loop itself lives in
|
|
@@ -343,6 +344,34 @@ function isUnansweredSuperMajorityShortfall(message: string): boolean {
|
|
|
343
344
|
return SUPER_MAJORITY_SHORTFALL_UNANSWERED.test(message);
|
|
344
345
|
}
|
|
345
346
|
|
|
347
|
+
/**
|
|
348
|
+
* A LOST UPDATE: the optimystic bridge refusing a commit because another writer changed or
|
|
349
|
+
* removed a row this statement read (`TransactionBridge.mapCommitRefusal` over a
|
|
350
|
+
* `TreeEntryChangedError`, `quereus-plugin-optimystic/src/optimystic-adapter/txn-bridge.ts`).
|
|
351
|
+
* Claimed by TYPE for {@link LOST_UPDATE_RETRY_POLICY} only.
|
|
352
|
+
*
|
|
353
|
+
* Why it is a proven non-commit: the refusal is raised inside the action's atomic handler, so
|
|
354
|
+
* none of that action's entries land. A refusal after a sibling tree or collection of the same
|
|
355
|
+
* transaction already synced is not delivered bare — the per-tree sweep wraps it in
|
|
356
|
+
* `PartialCommitError`, the coordinator path in `CoordinatorPartialCommitError` — and
|
|
357
|
+
* {@link reportsPossiblyStoredWrite} vetoes both by type before this predicate runs. A bare chain
|
|
358
|
+
* carrying this class therefore means nothing of the write is durable.
|
|
359
|
+
*
|
|
360
|
+
* Why it is not in the default list: the rejection is of the write's READ SET, not of its
|
|
361
|
+
* signature, so re-presenting the same statement is only right when the body re-reads the row and
|
|
362
|
+
* decides again over what it finds. That is a property of each opted-in body (see the policy's
|
|
363
|
+
* `NOTE:`), not of control writes in general.
|
|
364
|
+
*
|
|
365
|
+
* Not matched by text: `ConcurrentModificationError` is exported from both the plugin's root and
|
|
366
|
+
* `/plugin` entries over one shared chunk, as `PartialCommitError` is, so the one `instanceof`
|
|
367
|
+
* covers both (the `NOTE:` on {@link reportsPossiblyStoredWrite} applies to it too). A second
|
|
368
|
+
* loaded copy of the plugin would fail the `instanceof` and the lost update would then simply not
|
|
369
|
+
* be retried — the safe side.
|
|
370
|
+
*/
|
|
371
|
+
function isLostUpdate(link: Error): boolean {
|
|
372
|
+
return link instanceof ConcurrentModificationError;
|
|
373
|
+
}
|
|
374
|
+
|
|
346
375
|
/**
|
|
347
376
|
* Optimystic refusing to let a node elect ITSELF coordinator because its last connection dropped
|
|
348
377
|
* less than the guard's grace period ago (`Libp2pKeyPeerNetwork.shouldAllowSelfCoordination` →
|
|
@@ -411,9 +440,9 @@ const RETRIABLE_CONTROL_WRITE_MATCHERS: readonly ((message: string) => boolean)[
|
|
|
411
440
|
* does not complete — an undo statement threw, the post-unwind catalog did not match the pre-apply
|
|
412
441
|
* fingerprint, that catalog could not be re-collected to check, or a step the differ marks
|
|
413
442
|
* irreversible because it discards data (dropping a table or a column, narrowing a column's type —
|
|
414
|
-
*
|
|
415
|
-
* `refuseIncompatibleControlStore` in `control-schema-version.ts`
|
|
416
|
-
*
|
|
443
|
+
* of which only dropping a column reaches a `CadreControl` apply, as the catalog-only migration
|
|
444
|
+
* step `refuseIncompatibleControlStore` in `control-schema-version.ts` lets through; it discards
|
|
445
|
+
* nothing there, since rows keep the field) poisoned the journal before it
|
|
417
446
|
* ran, leaving nothing unwound — Quereus keeps the failing step's own message, APPENDS the reason the
|
|
418
447
|
* schema could not be restored, and carries the original as `cause`. So a transient cause
|
|
419
448
|
* underneath still matches here, and neither veto ({@link reportsPossiblyStoredWrite},
|
|
@@ -469,16 +498,30 @@ export function isRetriableSchemaInitFailure(error: unknown): boolean {
|
|
|
469
498
|
}
|
|
470
499
|
|
|
471
500
|
/**
|
|
472
|
-
*
|
|
473
|
-
*
|
|
474
|
-
*
|
|
475
|
-
*
|
|
476
|
-
*
|
|
477
|
-
|
|
501
|
+
* {@link isRetriableControlWriteFailure} widened by exactly one class — a lost update
|
|
502
|
+
* ({@link isLostUpdate}), claimed by type — for the `CadrePeer` writers that re-read the row inside
|
|
503
|
+
* their locked body. Opt-in per call site, via {@link LOST_UPDATE_RETRY_POLICY}; both vetoes still
|
|
504
|
+
* run first, so a lost update carried inside a partial commit, a spent sync budget or a non-final
|
|
505
|
+
* torn write stays declined.
|
|
506
|
+
*/
|
|
507
|
+
export function isRetriableLostUpdateFailure(error: unknown): boolean {
|
|
508
|
+
return matchesRetriableFailure(error, RETRIABLE_CONTROL_WRITE_MATCHERS, isLostUpdate);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Shared body of the three classifiers: walk the `cause` chain, let
|
|
513
|
+
* {@link reportsPossiblyStoredWrite} and then {@link reportsIndeterminateCommit} veto it, then claim
|
|
514
|
+
* a final torn write ({@link isFinalTornWrite}) or any link `claimsLink` names, then ask `matchers`.
|
|
515
|
+
* The vetoes run BEFORE any claim for every policy — a chain reporting a possibly-stored write or a
|
|
516
|
+
* commit-phase batch is never retried, however transient some other level looks, and a final torn
|
|
517
|
+
* write or a lost update carried by a partial commit is still declined because a sibling collection
|
|
518
|
+
* landed. `claimsLink` is the one typed claim a policy may add; it sits here beside the torn-write
|
|
519
|
+
* claim so the vetoes stay in one place above both.
|
|
478
520
|
*/
|
|
479
521
|
function matchesRetriableFailure(
|
|
480
522
|
error: unknown,
|
|
481
|
-
matchers: readonly ((message: string) => boolean)[]
|
|
523
|
+
matchers: readonly ((message: string) => boolean)[],
|
|
524
|
+
claimsLink?: (link: Error) => boolean
|
|
482
525
|
): boolean {
|
|
483
526
|
if (!(error instanceof Error)) {
|
|
484
527
|
return false;
|
|
@@ -491,7 +534,7 @@ function matchesRetriableFailure(
|
|
|
491
534
|
if (reportsIndeterminateCommit(messages)) {
|
|
492
535
|
return false;
|
|
493
536
|
}
|
|
494
|
-
if (links.some(isFinalTornWrite)) {
|
|
537
|
+
if (links.some(link => isFinalTornWrite(link) || claimsLink?.(link) === true)) {
|
|
495
538
|
return true;
|
|
496
539
|
}
|
|
497
540
|
return messages.some(message => matchers.some(matches => matches(message)));
|
|
@@ -510,7 +553,7 @@ function matchesRetriableFailure(
|
|
|
510
553
|
export type ControlWriteRetryOptions = ControlRetryOptions;
|
|
511
554
|
|
|
512
555
|
/**
|
|
513
|
-
* The
|
|
556
|
+
* The schema-init policy: `ControlDatabase.applyControlSchema`'s distributed DDL, which
|
|
514
557
|
* additionally absorbs optimystic's self-coordination grace refusal and gets more attempts over a
|
|
515
558
|
* longer backoff to do it (see {@link SCHEMA_INIT_ATTEMPTS} and
|
|
516
559
|
* {@link SCHEMA_INIT_RETRY_DELAYS_MS}). Still bounded by the shared
|
|
@@ -521,7 +564,8 @@ export type ControlWriteRetryOptions = ControlRetryOptions;
|
|
|
521
564
|
* and verified against the pre-apply catalog (full argument at the `applyControlSchema` call site
|
|
522
565
|
* in `control-database.ts`). A second opt-in must re-derive that argument for its own write body
|
|
523
566
|
* first; if this policy ever grows a third consumer, rename it for what the callers share rather
|
|
524
|
-
* than widening it by default.
|
|
567
|
+
* than widening it by default. {@link LOST_UPDATE_RETRY_POLICY} is a separate policy for a
|
|
568
|
+
* separate class, not a second consumer of this one.
|
|
525
569
|
*/
|
|
526
570
|
export const SCHEMA_INIT_RETRY_POLICY: Readonly<ControlWriteRetryOptions> = {
|
|
527
571
|
attempts: SCHEMA_INIT_ATTEMPTS,
|
|
@@ -529,6 +573,34 @@ export const SCHEMA_INIT_RETRY_POLICY: Readonly<ControlWriteRetryOptions> = {
|
|
|
529
573
|
isRetriable: isRetriableSchemaInitFailure,
|
|
530
574
|
};
|
|
531
575
|
|
|
576
|
+
/**
|
|
577
|
+
* The lost-update policy: the default classifier plus a lost update claimed by type
|
|
578
|
+
* ({@link isRetriableLostUpdateFailure}), over the default attempts, backoff and budget. A re-run
|
|
579
|
+
* here is a RE-DECISION, not a re-presentation: the body reads the row again under the lock and
|
|
580
|
+
* signs over what it finds, so the write that lost to a rival is replaced by one made over the
|
|
581
|
+
* rival's result. The budget bounds it as it bounds every write — a losing attempt whose read
|
|
582
|
+
* phase waited out one per-peer read deadline (~7.5 s measured in the degraded-cohort scenario's
|
|
583
|
+
* trio boot) gets exactly one retry, which is the ceiling to expect; one that waited out two
|
|
584
|
+
* (14.7 s, measured once in 18 of that scenario's boots, on the member that cannot reach one
|
|
585
|
+
* sibling) gets none and is abandoned on the budget, and the lost publish is then the reconcile
|
|
586
|
+
* pass's or the heartbeat's to redo, as every abandoned self-publish is.
|
|
587
|
+
*
|
|
588
|
+
* NOTE: the re-run safety this policy assumes is each opted-in body's, not a general property. A
|
|
589
|
+
* body may opt in only when its read of the row, its decision and its signature ALL run inside
|
|
590
|
+
* the locked body, so that the re-run presents a write made over the row as it now stands rather
|
|
591
|
+
* than the first attempt's signed record (which the rival may have stamped past, or which may
|
|
592
|
+
* overwrite what the rival wrote). Three `CadrePeer` writers opt in, through
|
|
593
|
+
* `ControlDatabase.mutateCadrePeer`'s policy parameter or `lockedWithRetry` directly:
|
|
594
|
+
* `updateSelfPeerRecord` (re-reads and re-signs the node's own record), `reauthorizeCadrePeer`
|
|
595
|
+
* (re-reads the stamp and decides again whether the row is still the owner's to re-touch) and
|
|
596
|
+
* the two removals `deleteCadrePeer` / `removeMachine` (stamp read and signature already inside
|
|
597
|
+
* the body, and a self-publish never changes the stamp the `'remove'` digest binds). A new opt-in
|
|
598
|
+
* must re-derive that argument for its own body first.
|
|
599
|
+
*/
|
|
600
|
+
export const LOST_UPDATE_RETRY_POLICY: Readonly<ControlWriteRetryOptions> = {
|
|
601
|
+
isRetriable: isRetriableLostUpdateFailure,
|
|
602
|
+
};
|
|
603
|
+
|
|
532
604
|
/**
|
|
533
605
|
* Run `attempt` up to {@link CONTROL_WRITE_ATTEMPTS} times (or `options.attempts`), retrying only
|
|
534
606
|
* failures the policy's classifier — {@link isRetriableControlWriteFailure} by default —
|