@serfab/cadre-core 1.9.0 → 1.11.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/dist/cadre-node.d.ts +148 -44
- package/dist/cadre-node.js +363 -139
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-authorization.d.ts +2 -2
- package/dist/control-authorization.js +1 -0
- package/dist/control-authorization.js.map +1 -1
- package/dist/control-cohort.d.ts +1 -1
- package/dist/control-cohort.js +1 -1
- package/dist/control-database.d.ts +105 -14
- package/dist/control-database.js +242 -73
- package/dist/control-database.js.map +1 -1
- package/dist/control-formation-recorder.d.ts +6 -6
- package/dist/control-formation-recorder.js +7 -8
- package/dist/control-formation-recorder.js.map +1 -1
- package/dist/control-schema.d.ts +1 -1
- package/dist/control-schema.js +1066 -979
- package/dist/control-schema.js.map +1 -1
- package/dist/control-write-retry.d.ts +21 -15
- package/dist/control-write-retry.js +21 -15
- package/dist/control-write-retry.js.map +1 -1
- package/dist/diagnostics/connection-path.d.ts +1 -18
- package/dist/diagnostics/connection-path.js +1 -20
- package/dist/diagnostics/connection-path.js.map +1 -1
- package/dist/identity-key.d.ts +0 -30
- package/dist/identity-key.js +7 -40
- package/dist/identity-key.js.map +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.js +12 -6
- package/dist/index.js.map +1 -1
- package/dist/invitation-bootstrap.d.ts +32 -0
- package/dist/invitation-bootstrap.js +30 -0
- package/dist/invitation-bootstrap.js.map +1 -0
- package/dist/link-budget.d.ts +204 -119
- package/dist/link-budget.js +208 -120
- package/dist/link-budget.js.map +1 -1
- package/dist/membership-connection-gater.d.ts +3 -3
- package/dist/membership-connection-gater.js +3 -3
- package/dist/peer-dial.d.ts +18 -14
- package/dist/peer-dial.js +18 -14
- package/dist/peer-dial.js.map +1 -1
- package/dist/peer-join-backfill.d.ts +3 -3
- package/dist/pending-join-runner.d.ts +186 -0
- package/dist/pending-join-runner.js +649 -0
- package/dist/pending-join-runner.js.map +1 -0
- package/dist/push-fanout.d.ts +1 -1
- package/dist/push-fanout.js +1 -1
- package/dist/relay-stun.d.ts +24 -0
- package/dist/relay-stun.js +51 -0
- package/dist/relay-stun.js.map +1 -0
- package/dist/seed-bootstrap.d.ts +2 -2
- package/dist/seed-bootstrap.js +5 -5
- package/dist/strand-addr-protocol.js +2 -2
- package/dist/strand-formation-deadlines.d.ts +5 -5
- package/dist/strand-formation-deadlines.js +5 -5
- package/dist/strand-formation-manager.d.ts +18 -4
- package/dist/strand-formation-manager.js +48 -33
- package/dist/strand-formation-manager.js.map +1 -1
- package/dist/strand-formation-protocol.d.ts +71 -14
- package/dist/strand-formation-protocol.js +222 -47
- package/dist/strand-formation-protocol.js.map +1 -1
- package/dist/strand-formation-rejection.d.ts +94 -0
- package/dist/strand-formation-rejection.js +77 -0
- package/dist/strand-formation-rejection.js.map +1 -0
- package/dist/strand-instance-manager.js +4 -1
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-membership-writer.js +32 -32
- package/dist/strand-solicitation.d.ts +11 -3
- package/dist/strand-solicitation.js +9 -0
- package/dist/strand-solicitation.js.map +1 -1
- package/dist/types.d.ts +103 -17
- package/dist/types.js +5 -9
- package/dist/types.js.map +1 -1
- package/package.json +22 -22
- package/src/cadre-node.ts +8654 -8400
- package/src/control-authorization.ts +2 -1
- package/src/control-cohort.ts +128 -128
- package/src/control-database.ts +303 -82
- package/src/control-formation-recorder.ts +353 -354
- package/src/control-read-retry.ts +216 -216
- package/src/control-schema.ts +1078 -991
- package/src/control-write-retry.ts +567 -561
- package/src/diagnostics/connection-path.ts +1 -29
- package/src/formation-approval.ts +610 -610
- package/src/hibernation-manager.ts +638 -638
- package/src/identity-key.ts +7 -62
- package/src/index.ts +644 -627
- package/src/invitation-bootstrap.ts +40 -0
- package/src/joined-strand-store.ts +498 -498
- package/src/link-budget.ts +684 -583
- package/src/membership-connection-gater.ts +570 -570
- package/src/node-local-snapshot.ts +298 -298
- package/src/peer-dial.ts +315 -311
- package/src/peer-join-backfill.ts +710 -710
- package/src/pending-join-runner.ts +735 -0
- package/src/push-fanout.ts +1 -1
- package/src/relay-reservation.ts +1121 -1121
- package/src/relay-server.ts +98 -98
- package/src/relay-stun.ts +58 -0
- package/src/seed-bootstrap.ts +1448 -1448
- package/src/strand-addr-protocol.ts +547 -547
- package/src/strand-database.ts +287 -287
- package/src/strand-first-sync-gate.ts +391 -391
- package/src/strand-formation-deadlines.ts +227 -227
- package/src/strand-formation-manager.ts +725 -700
- package/src/strand-formation-protocol.ts +1036 -802
- package/src/strand-formation-rejection.ts +129 -0
- package/src/strand-fret-addrs.ts +145 -145
- package/src/strand-instance-manager.ts +1789 -1786
- package/src/strand-membership-writer.ts +1974 -1974
- package/src/strand-network-state-file.ts +40 -40
- package/src/strand-network-state.ts +243 -243
- package/src/strand-revocation-enforcer.ts +654 -654
- package/src/strand-solicitation.ts +532 -519
- package/src/strand-wake-protocol.ts +401 -401
- package/src/types.ts +2222 -2140
- package/dist/diagnostics/webrtc-turn-tracker.d.ts +0 -50
- package/dist/diagnostics/webrtc-turn-tracker.js +0 -196
- package/dist/diagnostics/webrtc-turn-tracker.js.map +0 -1
- package/src/diagnostics/webrtc-turn-tracker.ts +0 -222
|
@@ -1,227 +1,227 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Strand-formation deadlines, derived from the declared link round trip.
|
|
3
|
-
*
|
|
4
|
-
* The wire protocol lives in `strand-formation-protocol.ts`; this module is only the
|
|
5
|
-
* arithmetic it runs on: the ladder of deadlines one declaration yields
|
|
6
|
-
* ({@link formationDeadlines}) and the clamping of a caller's configured provisioning budget
|
|
7
|
-
* under its session ({@link resolveProvisionTimeoutMs}, {@link splitProvisionBudget}). It
|
|
8
|
-
* reaches `link-budget.ts` for the counts and `formation-approval.ts` for the hook's flat
|
|
9
|
-
* timeout, and nothing in the protocol, so both roles and the manager can import it freely.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import debug from 'debug';
|
|
13
|
-
import { DEFAULT_APPROVAL_TIMEOUT_MS } from './formation-approval.js';
|
|
14
|
-
import {
|
|
15
|
-
CIRCUIT_REQUEST_ROUND_TRIPS,
|
|
16
|
-
cohortReadDeadlineMs,
|
|
17
|
-
commitBudgetMs,
|
|
18
|
-
relayedStreamOpenBudgetMs,
|
|
19
|
-
resolveLinkRoundTripMs
|
|
20
|
-
} from './link-budget.js';
|
|
21
|
-
|
|
22
|
-
const log = debug('sereus:cadre:formation-deadlines');
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Every deadline of one formation session, derived from one declared link round trip
|
|
26
|
-
* ({@link formationDeadlines}). Each waits on work that crosses the link between the two
|
|
27
|
-
* parties' machines, so each is a round-trip count times the declaration rather than a
|
|
28
|
-
* number chosen on a fast network; the initiator's and the responder's fields are derived
|
|
29
|
-
* together so the ordering between the roles holds by construction.
|
|
30
|
-
*/
|
|
31
|
-
export interface FormationDeadlines {
|
|
32
|
-
/** Initiator: open a possibly relayed connection and negotiate the formation protocol. */
|
|
33
|
-
dialMs: number;
|
|
34
|
-
/** Responder: from the handler starting to the contact frame arriving. */
|
|
35
|
-
awaitContactMs: number;
|
|
36
|
-
/**
|
|
37
|
-
* Responder: `validateToken` plus `validateDisclosure`, budgeted as two consulting control
|
|
38
|
-
* reads. Counted in the travel margin and the clamp room, not wrapped in a deadline of their
|
|
39
|
-
* own: the session bounds them.
|
|
40
|
-
*/
|
|
41
|
-
validationMs: number;
|
|
42
|
-
/** Responder: the provisioning hook's work budget, after which its signal aborts. */
|
|
43
|
-
provisionWorkMs: number;
|
|
44
|
-
/** Responder: the settle grace after the abort, which must contain one control commit. */
|
|
45
|
-
provisionGraceMs: number;
|
|
46
|
-
/**
|
|
47
|
-
* What the initiator's await-response holds over the responder's provisioning: the
|
|
48
|
-
* contact frame's trip out, the result frame's trip back, and the validation reads that
|
|
49
|
-
* run before the responder's provisioning clock starts.
|
|
50
|
-
*/
|
|
51
|
-
responseTravelMarginMs: number;
|
|
52
|
-
/** Initiator: the result frame read. */
|
|
53
|
-
initiatorAwaitResponseMs: number;
|
|
54
|
-
/** Both roles: the whole session. */
|
|
55
|
-
sessionMs: number;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Derive every formation deadline from the declared link round trip
|
|
60
|
-
* (`NetworkConfig.linkRoundTripMs`; the default when none is declared is
|
|
61
|
-
* `DECLARED_LINK_ROUND_TRIP_MS` in `link-budget.ts`). With `L` the declaration, `R` one
|
|
62
|
-
* cohort read at that link (`cohortReadDeadlineMs`, 2L: one request and its answer over an
|
|
63
|
-
* open circuit) and `K` one commit (`commitBudgetMs`, 20L, measured):
|
|
64
|
-
*
|
|
65
|
-
* dialMs = a relayed dial plus one protocol negotiation (
|
|
66
|
-
* awaitContactMs = one request over the open stream (2L)
|
|
67
|
-
* validationMs = 2R (4L)
|
|
68
|
-
* provisionWorkMs = approval hook (10 000, flat) + 2R + K (10 000 + 24L)
|
|
69
|
-
* provisionGraceMs = R + K (22L)
|
|
70
|
-
* responseTravelMarginMs = L + validationMs (5L)
|
|
71
|
-
* initiatorAwaitResponseMs = work + grace + margin (10 000 + 51L)
|
|
72
|
-
* sessionMs = dialMs + initiatorAwaitResponseMs (
|
|
73
|
-
*
|
|
74
|
-
* Why each term:
|
|
75
|
-
*
|
|
76
|
-
* - **awaitContact**: the listener's handler starts when it answers the protocol
|
|
77
|
-
* negotiation; the answer reaches the joiner and the contact frame comes back, one round
|
|
78
|
-
* trip, budgeted as a request over an open stream (2) for the same headroom as every other
|
|
79
|
-
* one-exchange read in `link-budget.ts`.
|
|
80
|
-
* - **validation** and the reads inside **provisionWork** are budgeted at two consulting
|
|
81
|
-
* reads each, not one per read the code issues. What actually runs: validation is three
|
|
82
|
-
* control reads (`isTokenValid` reads the invite; `isTokenUsed` reads it again and counts
|
|
83
|
-
* its usages) plus whatever the disclosure validator does; the bound provisioning path
|
|
84
|
-
* (`StrandFormationManager.provisionAsResponder`) reads the invite and the strand row to
|
|
85
|
-
* resolve the host strand, the invite again, the approver's `ValidationKey` and the seat
|
|
86
|
-
* count around the outside approval hook (an HTTP call, so its own
|
|
87
|
-
* `DEFAULT_APPROVAL_TIMEOUT_MS` is link-independent and added flat), the strand row and its
|
|
88
|
-
* `StrandPartyKey` to issue the joiner's membership invite, and the host strand's stamp
|
|
89
|
-
* before the `FormationUsage` write; then one strand-database commit issues the invite.
|
|
90
|
-
* Every one of those is a read of this party's own control database, which in steady state
|
|
91
|
-
* touches held blocks and consults nobody (`cadre-consistency.md` → "Deadlines Over
|
|
92
|
-
* Optimystic's Reads and Commits"), so `R` is charged as headroom for the ones that do
|
|
93
|
-
* consult, not per read. Waking a hibernating host strand inside the issue is NOT counted
|
|
94
|
-
* either: `wakeHostStrandForFormation` cuts it off by design with a retryable rejection
|
|
95
|
-
* and leaves the wake running, so the joiner's retry finds the strand live.
|
|
96
|
-
* NOTE: if formation is seen timing out while the responder's control reads are consulting
|
|
97
|
-
* the cohort (a cold responder, a control database not yet converged), charge one `R` per
|
|
98
|
-
* read here rather than raising the flat headroom; the joiner is told 'Formation
|
|
99
|
-
* provisioning timed out' with its invite unspent, so the cost is a retry, not a spent
|
|
100
|
-
* invite.
|
|
101
|
-
* - **provisionGrace**: the window it exists for opens when the `FormationUsage` insert
|
|
102
|
-
* attempt passes its abort check. `ControlDatabase.redeemInvitation` and
|
|
103
|
-
* `recordFormationUsage` check the signal once per attempt inside the write lock, then run
|
|
104
|
-
* the seat read (`assertSeatRemains`) and the transaction, so the grace contains one read
|
|
105
|
-
* plus one commit. A retry after a failed attempt re-checks the abort, so only one attempt
|
|
106
|
-
* needs containing.
|
|
107
|
-
* - **responseTravelMargin**: half a round trip out, half back, plus the validation reads,
|
|
108
|
-
* which run on the initiator's await-response clock but before the responder's
|
|
109
|
-
* provisioning clock starts.
|
|
110
|
-
* - **session**: the initiator's dial plus its await-response. The responder's whole path
|
|
111
|
-
* (awaitContact + validation + work + grace = 10 000 + 52L) is inside it at every L.
|
|
112
|
-
*
|
|
113
|
-
* Ordering, strict at every L > 0, and pinned by `strand-formation-deadlines.spec.ts`:
|
|
114
|
-
* approval hook < provisionWork < work + grace < initiatorAwaitResponse < session, and the
|
|
115
|
-
* responder's whole path < session. Each layer can therefore fail and report before the
|
|
116
|
-
* layer above it gives up, and the work budget always outlasts the hook's own timeout, so a
|
|
117
|
-
* dead hook reports 'Formation approval unavailable, retry' rather than racing a
|
|
118
|
-
* provisioning timeout.
|
|
119
|
-
*
|
|
120
|
-
* **What this costs.** At the default declaration (3 500 ms) a joiner whose responder
|
|
121
|
-
* accepts the contact and then hangs waits up to 188.5 s, about three minutes, before it is
|
|
122
|
-
* told, where the fixed ladder this replaced told it after 15 s; the whole session is
|
|
123
|
-
* bounded at
|
|
124
|
-
* that knows its party is on a fast link lowers `linkRoundTripMs` and gets roughly the old
|
|
125
|
-
* numbers back (at L = 100: dial
|
|
126
|
-
*
|
|
127
|
-
* above a 1.25-second round trip, and the fixed 2 s grace could not contain one commit at the
|
|
128
|
-
* supported link, so a joiner could be told 'timed out' over an invite that was in fact
|
|
129
|
-
* spent.
|
|
130
|
-
*/
|
|
131
|
-
export function formationDeadlines(linkRoundTripMs?: number): FormationDeadlines {
|
|
132
|
-
const link = resolveLinkRoundTripMs(linkRoundTripMs);
|
|
133
|
-
const readMs = cohortReadDeadlineMs(link);
|
|
134
|
-
const commitMs = commitBudgetMs(link);
|
|
135
|
-
const dialMs = relayedStreamOpenBudgetMs(link);
|
|
136
|
-
const validationMs = 2 * readMs;
|
|
137
|
-
const provisionWorkMs = DEFAULT_APPROVAL_TIMEOUT_MS + 2 * readMs + commitMs;
|
|
138
|
-
const provisionGraceMs = readMs + commitMs;
|
|
139
|
-
const responseTravelMarginMs = link + validationMs;
|
|
140
|
-
const initiatorAwaitResponseMs = provisionWorkMs + provisionGraceMs + responseTravelMarginMs;
|
|
141
|
-
return {
|
|
142
|
-
dialMs,
|
|
143
|
-
awaitContactMs: CIRCUIT_REQUEST_ROUND_TRIPS * link,
|
|
144
|
-
validationMs,
|
|
145
|
-
provisionWorkMs,
|
|
146
|
-
provisionGraceMs,
|
|
147
|
-
responseTravelMarginMs,
|
|
148
|
-
initiatorAwaitResponseMs,
|
|
149
|
-
sessionMs: dialMs + initiatorAwaitResponseMs
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Ceiling for a resolved provisioning budget.
|
|
155
|
-
*
|
|
156
|
-
* The session budget also has to cover what PRECEDES provisioning in the role
|
|
157
|
-
* (`precedingMs`: the initiator's dial; the responder's contact read plus its validation
|
|
158
|
-
* reads), so that whole span is held back rather than triggering only on a literal overrun.
|
|
159
|
-
*
|
|
160
|
-
* `reserveMs` is the EXTRA room only the responder holds back, so its own budget still
|
|
161
|
-
* lands strictly before the initiator's larger await-response budget even when both are
|
|
162
|
-
* clamped — without it, a configured budget large enough to clamp both roles puts the
|
|
163
|
-
* responder's reply on the wire at the moment the initiator's timeout fires. Capped at half
|
|
164
|
-
* the remaining room (like {@link splitProvisionBudget}) so a small session config still
|
|
165
|
-
* spends most of it working.
|
|
166
|
-
*/
|
|
167
|
-
function provisionCeilingMs(sessionTimeoutMs: number, precedingMs: number, reserveMs: number): number {
|
|
168
|
-
const roomMs = sessionTimeoutMs - precedingMs;
|
|
169
|
-
return Math.max(1, roomMs - Math.min(reserveMs, Math.floor(roomMs / 2)));
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* The extra room the responder's clamp holds back ({@link provisionCeilingMs}'s `reserveMs`).
|
|
174
|
-
*
|
|
175
|
-
* The initiator's clamped await-response is `session - dial`, and the responder's reply
|
|
176
|
-
* reaches it `responseTravelMarginMs` after provisioning ends, so provisioning must end by
|
|
177
|
-
* `session - dial - margin`. The responder's own room is `session - awaitContact -
|
|
178
|
-
* validation`; the difference between the two is what it must hold back on top. Computed
|
|
179
|
-
* from the responder's own derivation, since it cannot see the initiator's dial budget: the
|
|
180
|
-
* module doc of `link-budget.ts` says why both parties must declare the same link.
|
|
181
|
-
*/
|
|
182
|
-
export function responderClampReserveMs(deadlines: FormationDeadlines, awaitContactMs: number): number {
|
|
183
|
-
return Math.max(0, deadlines.dialMs + deadlines.responseTravelMarginMs - awaitContactMs - deadlines.validationMs);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Resolve a caller-supplied provisioning budget: `0`/negative means "unset" (use
|
|
188
|
-
* `defaultMs`). If the result would let provisioning outlive the session — no result
|
|
189
|
-
* frame is ever sent, exactly the failure this budget exists to prevent — clamp it to
|
|
190
|
-
* {@link provisionCeilingMs} and log a warning. A budget that exactly meets the ceiling is
|
|
191
|
-
* not clamped: the derived defaults sit there by construction.
|
|
192
|
-
*/
|
|
193
|
-
export function resolveProvisionTimeoutMs(
|
|
194
|
-
configured: number | undefined,
|
|
195
|
-
defaultMs: number,
|
|
196
|
-
sessionTimeoutMs: number,
|
|
197
|
-
precedingMs: number,
|
|
198
|
-
role: string,
|
|
199
|
-
reserveMs = 0
|
|
200
|
-
): number {
|
|
201
|
-
const requested = configured && configured > 0 ? configured : defaultMs;
|
|
202
|
-
const ceilingMs = provisionCeilingMs(sessionTimeoutMs, precedingMs, reserveMs);
|
|
203
|
-
if (requested > ceilingMs) {
|
|
204
|
-
log(
|
|
205
|
-
'%s provisionTimeoutMs %dms leaves no room under sessionTimeoutMs %dms (preceding %dms, reserve %dms); clamping to %dms',
|
|
206
|
-
role, requested, sessionTimeoutMs, precedingMs, reserveMs, ceilingMs
|
|
207
|
-
);
|
|
208
|
-
return ceilingMs;
|
|
209
|
-
}
|
|
210
|
-
return requested;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Split a resolved provisioning budget into the WORK budget and the trailing settle grace.
|
|
215
|
-
*
|
|
216
|
-
* The grace is carved OUT of the budget, never added on top, so the ladder in
|
|
217
|
-
* {@link formationDeadlines} is untouched: the listener aborts the provisioning hook when the
|
|
218
|
-
* work budget expires, then waits up to the grace for the work to settle anyway
|
|
219
|
-
* (`FormationListener.settleWithinGrace` in `strand-formation-protocol.ts`). `graceCeilingMs`
|
|
220
|
-
* is the derived grace, which contains one seat read plus one commit at the declared link; it
|
|
221
|
-
* is capped at half the budget so a small configured budget still spends at least half its
|
|
222
|
-
* time doing work.
|
|
223
|
-
*/
|
|
224
|
-
export function splitProvisionBudget(provisionTimeoutMs: number, graceCeilingMs: number): { workMs: number; graceMs: number } {
|
|
225
|
-
const graceMs = Math.min(graceCeilingMs, Math.floor(provisionTimeoutMs / 2));
|
|
226
|
-
return { workMs: provisionTimeoutMs - graceMs, graceMs };
|
|
227
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Strand-formation deadlines, derived from the declared link round trip.
|
|
3
|
+
*
|
|
4
|
+
* The wire protocol lives in `strand-formation-protocol.ts`; this module is only the
|
|
5
|
+
* arithmetic it runs on: the ladder of deadlines one declaration yields
|
|
6
|
+
* ({@link formationDeadlines}) and the clamping of a caller's configured provisioning budget
|
|
7
|
+
* under its session ({@link resolveProvisionTimeoutMs}, {@link splitProvisionBudget}). It
|
|
8
|
+
* reaches `link-budget.ts` for the counts and `formation-approval.ts` for the hook's flat
|
|
9
|
+
* timeout, and nothing in the protocol, so both roles and the manager can import it freely.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import debug from 'debug';
|
|
13
|
+
import { DEFAULT_APPROVAL_TIMEOUT_MS } from './formation-approval.js';
|
|
14
|
+
import {
|
|
15
|
+
CIRCUIT_REQUEST_ROUND_TRIPS,
|
|
16
|
+
cohortReadDeadlineMs,
|
|
17
|
+
commitBudgetMs,
|
|
18
|
+
relayedStreamOpenBudgetMs,
|
|
19
|
+
resolveLinkRoundTripMs
|
|
20
|
+
} from './link-budget.js';
|
|
21
|
+
|
|
22
|
+
const log = debug('sereus:cadre:formation-deadlines');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Every deadline of one formation session, derived from one declared link round trip
|
|
26
|
+
* ({@link formationDeadlines}). Each waits on work that crosses the link between the two
|
|
27
|
+
* parties' machines, so each is a round-trip count times the declaration rather than a
|
|
28
|
+
* number chosen on a fast network; the initiator's and the responder's fields are derived
|
|
29
|
+
* together so the ordering between the roles holds by construction.
|
|
30
|
+
*/
|
|
31
|
+
export interface FormationDeadlines {
|
|
32
|
+
/** Initiator: open a possibly relayed connection and negotiate the formation protocol. */
|
|
33
|
+
dialMs: number;
|
|
34
|
+
/** Responder: from the handler starting to the contact frame arriving. */
|
|
35
|
+
awaitContactMs: number;
|
|
36
|
+
/**
|
|
37
|
+
* Responder: `validateToken` plus `validateDisclosure`, budgeted as two consulting control
|
|
38
|
+
* reads. Counted in the travel margin and the clamp room, not wrapped in a deadline of their
|
|
39
|
+
* own: the session bounds them.
|
|
40
|
+
*/
|
|
41
|
+
validationMs: number;
|
|
42
|
+
/** Responder: the provisioning hook's work budget, after which its signal aborts. */
|
|
43
|
+
provisionWorkMs: number;
|
|
44
|
+
/** Responder: the settle grace after the abort, which must contain one control commit. */
|
|
45
|
+
provisionGraceMs: number;
|
|
46
|
+
/**
|
|
47
|
+
* What the initiator's await-response holds over the responder's provisioning: the
|
|
48
|
+
* contact frame's trip out, the result frame's trip back, and the validation reads that
|
|
49
|
+
* run before the responder's provisioning clock starts.
|
|
50
|
+
*/
|
|
51
|
+
responseTravelMarginMs: number;
|
|
52
|
+
/** Initiator: the result frame read. */
|
|
53
|
+
initiatorAwaitResponseMs: number;
|
|
54
|
+
/** Both roles: the whole session. */
|
|
55
|
+
sessionMs: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Derive every formation deadline from the declared link round trip
|
|
60
|
+
* (`NetworkConfig.linkRoundTripMs`; the default when none is declared is
|
|
61
|
+
* `DECLARED_LINK_ROUND_TRIP_MS` in `link-budget.ts`). With `L` the declaration, `R` one
|
|
62
|
+
* cohort read at that link (`cohortReadDeadlineMs`, 2L: one request and its answer over an
|
|
63
|
+
* open circuit) and `K` one commit (`commitBudgetMs`, 20L, measured):
|
|
64
|
+
*
|
|
65
|
+
* dialMs = a relayed dial plus one protocol negotiation (6L + 4 000)
|
|
66
|
+
* awaitContactMs = one request over the open stream (2L)
|
|
67
|
+
* validationMs = 2R (4L)
|
|
68
|
+
* provisionWorkMs = approval hook (10 000, flat) + 2R + K (10 000 + 24L)
|
|
69
|
+
* provisionGraceMs = R + K (22L)
|
|
70
|
+
* responseTravelMarginMs = L + validationMs (5L)
|
|
71
|
+
* initiatorAwaitResponseMs = work + grace + margin (10 000 + 51L)
|
|
72
|
+
* sessionMs = dialMs + initiatorAwaitResponseMs (14 000 + 57L)
|
|
73
|
+
*
|
|
74
|
+
* Why each term:
|
|
75
|
+
*
|
|
76
|
+
* - **awaitContact**: the listener's handler starts when it answers the protocol
|
|
77
|
+
* negotiation; the answer reaches the joiner and the contact frame comes back, one round
|
|
78
|
+
* trip, budgeted as a request over an open stream (2) for the same headroom as every other
|
|
79
|
+
* one-exchange read in `link-budget.ts`.
|
|
80
|
+
* - **validation** and the reads inside **provisionWork** are budgeted at two consulting
|
|
81
|
+
* reads each, not one per read the code issues. What actually runs: validation is three
|
|
82
|
+
* control reads (`isTokenValid` reads the invite; `isTokenUsed` reads it again and counts
|
|
83
|
+
* its usages) plus whatever the disclosure validator does; the bound provisioning path
|
|
84
|
+
* (`StrandFormationManager.provisionAsResponder`) reads the invite and the strand row to
|
|
85
|
+
* resolve the host strand, the invite again, the approver's `ValidationKey` and the seat
|
|
86
|
+
* count around the outside approval hook (an HTTP call, so its own
|
|
87
|
+
* `DEFAULT_APPROVAL_TIMEOUT_MS` is link-independent and added flat), the strand row and its
|
|
88
|
+
* `StrandPartyKey` to issue the joiner's membership invite, and the host strand's stamp
|
|
89
|
+
* before the `FormationUsage` write; then one strand-database commit issues the invite.
|
|
90
|
+
* Every one of those is a read of this party's own control database, which in steady state
|
|
91
|
+
* touches held blocks and consults nobody (`cadre-consistency.md` → "Deadlines Over
|
|
92
|
+
* Optimystic's Reads and Commits"), so `R` is charged as headroom for the ones that do
|
|
93
|
+
* consult, not per read. Waking a hibernating host strand inside the issue is NOT counted
|
|
94
|
+
* either: `wakeHostStrandForFormation` cuts it off by design with a retryable rejection
|
|
95
|
+
* and leaves the wake running, so the joiner's retry finds the strand live.
|
|
96
|
+
* NOTE: if formation is seen timing out while the responder's control reads are consulting
|
|
97
|
+
* the cohort (a cold responder, a control database not yet converged), charge one `R` per
|
|
98
|
+
* read here rather than raising the flat headroom; the joiner is told 'Formation
|
|
99
|
+
* provisioning timed out' with its invite unspent, so the cost is a retry, not a spent
|
|
100
|
+
* invite.
|
|
101
|
+
* - **provisionGrace**: the window it exists for opens when the `FormationUsage` insert
|
|
102
|
+
* attempt passes its abort check. `ControlDatabase.redeemInvitation` and
|
|
103
|
+
* `recordFormationUsage` check the signal once per attempt inside the write lock, then run
|
|
104
|
+
* the seat read (`assertSeatRemains`) and the transaction, so the grace contains one read
|
|
105
|
+
* plus one commit. A retry after a failed attempt re-checks the abort, so only one attempt
|
|
106
|
+
* needs containing.
|
|
107
|
+
* - **responseTravelMargin**: half a round trip out, half back, plus the validation reads,
|
|
108
|
+
* which run on the initiator's await-response clock but before the responder's
|
|
109
|
+
* provisioning clock starts.
|
|
110
|
+
* - **session**: the initiator's dial plus its await-response. The responder's whole path
|
|
111
|
+
* (awaitContact + validation + work + grace = 10 000 + 52L) is inside it at every L.
|
|
112
|
+
*
|
|
113
|
+
* Ordering, strict at every L > 0, and pinned by `strand-formation-deadlines.spec.ts`:
|
|
114
|
+
* approval hook < provisionWork < work + grace < initiatorAwaitResponse < session, and the
|
|
115
|
+
* responder's whole path < session. Each layer can therefore fail and report before the
|
|
116
|
+
* layer above it gives up, and the work budget always outlasts the hook's own timeout, so a
|
|
117
|
+
* dead hook reports 'Formation approval unavailable, retry' rather than racing a
|
|
118
|
+
* provisioning timeout.
|
|
119
|
+
*
|
|
120
|
+
* **What this costs.** At the default declaration (3 500 ms) a joiner whose responder
|
|
121
|
+
* accepts the contact and then hangs waits up to 188.5 s, about three minutes, before it is
|
|
122
|
+
* told, where the fixed ladder this replaced told it after 15 s; the whole session is
|
|
123
|
+
* bounded at 213.5 s. A responder that is unreachable still fails at the dial, 25 s. A host
|
|
124
|
+
* that knows its party is on a fast link lowers `linkRoundTripMs` and gets roughly the old
|
|
125
|
+
* numbers back (at L = 100: dial 4.6 s, provisioning 14.6 s, await-response 15.1 s, session
|
|
126
|
+
* 19.7 s). Before this derivation, the fixed 5 s dial could not open a relayed connection
|
|
127
|
+
* above a 1.25-second round trip, and the fixed 2 s grace could not contain one commit at the
|
|
128
|
+
* supported link, so a joiner could be told 'timed out' over an invite that was in fact
|
|
129
|
+
* spent.
|
|
130
|
+
*/
|
|
131
|
+
export function formationDeadlines(linkRoundTripMs?: number): FormationDeadlines {
|
|
132
|
+
const link = resolveLinkRoundTripMs(linkRoundTripMs);
|
|
133
|
+
const readMs = cohortReadDeadlineMs(link);
|
|
134
|
+
const commitMs = commitBudgetMs(link);
|
|
135
|
+
const dialMs = relayedStreamOpenBudgetMs(link);
|
|
136
|
+
const validationMs = 2 * readMs;
|
|
137
|
+
const provisionWorkMs = DEFAULT_APPROVAL_TIMEOUT_MS + 2 * readMs + commitMs;
|
|
138
|
+
const provisionGraceMs = readMs + commitMs;
|
|
139
|
+
const responseTravelMarginMs = link + validationMs;
|
|
140
|
+
const initiatorAwaitResponseMs = provisionWorkMs + provisionGraceMs + responseTravelMarginMs;
|
|
141
|
+
return {
|
|
142
|
+
dialMs,
|
|
143
|
+
awaitContactMs: CIRCUIT_REQUEST_ROUND_TRIPS * link,
|
|
144
|
+
validationMs,
|
|
145
|
+
provisionWorkMs,
|
|
146
|
+
provisionGraceMs,
|
|
147
|
+
responseTravelMarginMs,
|
|
148
|
+
initiatorAwaitResponseMs,
|
|
149
|
+
sessionMs: dialMs + initiatorAwaitResponseMs
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Ceiling for a resolved provisioning budget.
|
|
155
|
+
*
|
|
156
|
+
* The session budget also has to cover what PRECEDES provisioning in the role
|
|
157
|
+
* (`precedingMs`: the initiator's dial; the responder's contact read plus its validation
|
|
158
|
+
* reads), so that whole span is held back rather than triggering only on a literal overrun.
|
|
159
|
+
*
|
|
160
|
+
* `reserveMs` is the EXTRA room only the responder holds back, so its own budget still
|
|
161
|
+
* lands strictly before the initiator's larger await-response budget even when both are
|
|
162
|
+
* clamped — without it, a configured budget large enough to clamp both roles puts the
|
|
163
|
+
* responder's reply on the wire at the moment the initiator's timeout fires. Capped at half
|
|
164
|
+
* the remaining room (like {@link splitProvisionBudget}) so a small session config still
|
|
165
|
+
* spends most of it working.
|
|
166
|
+
*/
|
|
167
|
+
function provisionCeilingMs(sessionTimeoutMs: number, precedingMs: number, reserveMs: number): number {
|
|
168
|
+
const roomMs = sessionTimeoutMs - precedingMs;
|
|
169
|
+
return Math.max(1, roomMs - Math.min(reserveMs, Math.floor(roomMs / 2)));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* The extra room the responder's clamp holds back ({@link provisionCeilingMs}'s `reserveMs`).
|
|
174
|
+
*
|
|
175
|
+
* The initiator's clamped await-response is `session - dial`, and the responder's reply
|
|
176
|
+
* reaches it `responseTravelMarginMs` after provisioning ends, so provisioning must end by
|
|
177
|
+
* `session - dial - margin`. The responder's own room is `session - awaitContact -
|
|
178
|
+
* validation`; the difference between the two is what it must hold back on top. Computed
|
|
179
|
+
* from the responder's own derivation, since it cannot see the initiator's dial budget: the
|
|
180
|
+
* module doc of `link-budget.ts` says why both parties must declare the same link.
|
|
181
|
+
*/
|
|
182
|
+
export function responderClampReserveMs(deadlines: FormationDeadlines, awaitContactMs: number): number {
|
|
183
|
+
return Math.max(0, deadlines.dialMs + deadlines.responseTravelMarginMs - awaitContactMs - deadlines.validationMs);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Resolve a caller-supplied provisioning budget: `0`/negative means "unset" (use
|
|
188
|
+
* `defaultMs`). If the result would let provisioning outlive the session — no result
|
|
189
|
+
* frame is ever sent, exactly the failure this budget exists to prevent — clamp it to
|
|
190
|
+
* {@link provisionCeilingMs} and log a warning. A budget that exactly meets the ceiling is
|
|
191
|
+
* not clamped: the derived defaults sit there by construction.
|
|
192
|
+
*/
|
|
193
|
+
export function resolveProvisionTimeoutMs(
|
|
194
|
+
configured: number | undefined,
|
|
195
|
+
defaultMs: number,
|
|
196
|
+
sessionTimeoutMs: number,
|
|
197
|
+
precedingMs: number,
|
|
198
|
+
role: string,
|
|
199
|
+
reserveMs = 0
|
|
200
|
+
): number {
|
|
201
|
+
const requested = configured && configured > 0 ? configured : defaultMs;
|
|
202
|
+
const ceilingMs = provisionCeilingMs(sessionTimeoutMs, precedingMs, reserveMs);
|
|
203
|
+
if (requested > ceilingMs) {
|
|
204
|
+
log(
|
|
205
|
+
'%s provisionTimeoutMs %dms leaves no room under sessionTimeoutMs %dms (preceding %dms, reserve %dms); clamping to %dms',
|
|
206
|
+
role, requested, sessionTimeoutMs, precedingMs, reserveMs, ceilingMs
|
|
207
|
+
);
|
|
208
|
+
return ceilingMs;
|
|
209
|
+
}
|
|
210
|
+
return requested;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Split a resolved provisioning budget into the WORK budget and the trailing settle grace.
|
|
215
|
+
*
|
|
216
|
+
* The grace is carved OUT of the budget, never added on top, so the ladder in
|
|
217
|
+
* {@link formationDeadlines} is untouched: the listener aborts the provisioning hook when the
|
|
218
|
+
* work budget expires, then waits up to the grace for the work to settle anyway
|
|
219
|
+
* (`FormationListener.settleWithinGrace` in `strand-formation-protocol.ts`). `graceCeilingMs`
|
|
220
|
+
* is the derived grace, which contains one seat read plus one commit at the declared link; it
|
|
221
|
+
* is capped at half the budget so a small configured budget still spends at least half its
|
|
222
|
+
* time doing work.
|
|
223
|
+
*/
|
|
224
|
+
export function splitProvisionBudget(provisionTimeoutMs: number, graceCeilingMs: number): { workMs: number; graceMs: number } {
|
|
225
|
+
const graceMs = Math.min(graceCeilingMs, Math.floor(provisionTimeoutMs / 2));
|
|
226
|
+
return { workMs: provisionTimeoutMs - graceMs, graceMs };
|
|
227
|
+
}
|