@indigoai-us/hq-cloud 6.15.78 → 6.15.80
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/bin/area-ledger-migration.d.ts +13 -0
- package/dist/bin/area-ledger-migration.d.ts.map +1 -0
- package/dist/bin/area-ledger-migration.js +64 -0
- package/dist/bin/area-ledger-migration.js.map +1 -0
- package/dist/bin/area-ledger-migration.test.d.ts +2 -0
- package/dist/bin/area-ledger-migration.test.d.ts.map +1 -0
- package/dist/bin/area-ledger-migration.test.js +55 -0
- package/dist/bin/area-ledger-migration.test.js.map +1 -0
- package/dist/bin/sync-runner-company.d.ts +6 -0
- package/dist/bin/sync-runner-company.d.ts.map +1 -1
- package/dist/bin/sync-runner-company.js +6 -0
- package/dist/bin/sync-runner-company.js.map +1 -1
- package/dist/bin/sync-runner-company.test.js +47 -0
- package/dist/bin/sync-runner-company.test.js.map +1 -1
- package/dist/cli/share.d.ts +8 -0
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +184 -10
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +330 -7
- package/dist/cli/share.test.js.map +1 -1
- package/dist/cli/sync.d.ts +7 -0
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +11 -1
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +25 -0
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/index.test.js +11 -1
- package/dist/index.test.js.map +1 -1
- package/dist/journal-store.test.js +42 -1
- package/dist/journal-store.test.js.map +1 -1
- package/dist/journal.d.ts +13 -1
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +104 -3
- package/dist/journal.js.map +1 -1
- package/dist/sync/area-ledger-migration.d.ts +116 -0
- package/dist/sync/area-ledger-migration.d.ts.map +1 -0
- package/dist/sync/area-ledger-migration.js +505 -0
- package/dist/sync/area-ledger-migration.js.map +1 -0
- package/dist/sync/area-ledger-migration.test.d.ts +2 -0
- package/dist/sync/area-ledger-migration.test.d.ts.map +1 -0
- package/dist/sync/area-ledger-migration.test.js +274 -0
- package/dist/sync/area-ledger-migration.test.js.map +1 -0
- package/dist/sync/area-ledger.d.ts +93 -0
- package/dist/sync/area-ledger.d.ts.map +1 -0
- package/dist/sync/area-ledger.js +460 -0
- package/dist/sync/area-ledger.js.map +1 -0
- package/dist/sync/area-ledger.test.d.ts +2 -0
- package/dist/sync/area-ledger.test.d.ts.map +1 -0
- package/dist/sync/area-ledger.test.js +199 -0
- package/dist/sync/area-ledger.test.js.map +1 -0
- package/dist/sync/event-sync.d.ts +14 -4
- package/dist/sync/event-sync.d.ts.map +1 -1
- package/dist/sync/event-sync.js +177 -61
- package/dist/sync/event-sync.js.map +1 -1
- package/dist/sync/event-sync.test.js +140 -10
- package/dist/sync/event-sync.test.js.map +1 -1
- package/dist/sync/index.d.ts +2 -0
- package/dist/sync/index.d.ts.map +1 -1
- package/dist/sync/index.js +1 -0
- package/dist/sync/index.js.map +1 -1
- package/dist/sync/parallel-sync-coordinator.d.ts.map +1 -1
- package/dist/sync/parallel-sync-coordinator.js +22 -4
- package/dist/sync/parallel-sync-coordinator.js.map +1 -1
- package/dist/sync/parallel-sync-coordinator.test.js +45 -0
- package/dist/sync/parallel-sync-coordinator.test.js.map +1 -1
- package/dist/sync/reconcile-cursor.d.ts +97 -0
- package/dist/sync/reconcile-cursor.d.ts.map +1 -0
- package/dist/sync/reconcile-cursor.js +215 -0
- package/dist/sync/reconcile-cursor.js.map +1 -0
- package/dist/sync/reconcile-cursor.test.d.ts +2 -0
- package/dist/sync/reconcile-cursor.test.d.ts.map +1 -0
- package/dist/sync/reconcile-cursor.test.js +139 -0
- package/dist/sync/reconcile-cursor.test.js.map +1 -0
- package/dist/sync/state-store.d.ts +76 -0
- package/dist/sync/state-store.d.ts.map +1 -1
- package/dist/sync/state-store.js +317 -3
- package/dist/sync/state-store.js.map +1 -1
- package/dist/sync/state-store.test.js +39 -1
- package/dist/sync/state-store.test.js.map +1 -1
- package/dist/sync/sync-work.d.ts +2 -1
- package/dist/sync/sync-work.d.ts.map +1 -1
- package/dist/sync/sync-work.js.map +1 -1
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/vault-client.d.ts +9 -2
- package/dist/vault-client.d.ts.map +1 -1
- package/dist/vault-client.js +3 -2
- package/dist/vault-client.js.map +1 -1
- package/dist/vault-client.test.js +26 -0
- package/dist/vault-client.test.js.map +1 -1
- package/package.json +3 -2
|
@@ -20,6 +20,8 @@ export declare function setStateStorePositionalWriteForTest(write: ((fd: number,
|
|
|
20
20
|
export declare function setStateStoreRotationClaimWriteHookForTest(hook: (() => void) | undefined): void;
|
|
21
21
|
/** Test-only seam for proving same-process async lock waiters queue cooperatively. */
|
|
22
22
|
export declare function setStateStoreScopeLockAcquiredHookForTest(hook: (() => void | Promise<void>) | undefined): void;
|
|
23
|
+
/** Test-only seam for a failure after a batch lock file has been created. */
|
|
24
|
+
export declare function setStateStoreScopeLockWriteHookForTest(hook: (() => void) | undefined): void;
|
|
23
25
|
/** Test-only seam for exercising async lock deadlines without a minute-long wait. */
|
|
24
26
|
export declare function setStateStoreAsyncLockTimeoutForTest(timeoutMs: number | undefined): void;
|
|
25
27
|
/** Test-only seam for exercising synchronous lock contention without a five-second wait. */
|
|
@@ -55,12 +57,53 @@ export interface StateStoreRefreshResult {
|
|
|
55
57
|
export interface StateStoreAppendResult extends StateStoreRecord {
|
|
56
58
|
refresh: StateStoreRefreshResult;
|
|
57
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* One durable append participating in a short, ordered multi-scope commit.
|
|
62
|
+
*
|
|
63
|
+
* This is deliberately an internal-storage primitive rather than a general
|
|
64
|
+
* transaction manager: callers still own their recovery protocol. It gives
|
|
65
|
+
* them one important invariant that a sequence of `append()` calls cannot:
|
|
66
|
+
* every live lock is acquired in a stable order and a contender releases all
|
|
67
|
+
* acquired locks before waiting to retry.
|
|
68
|
+
*/
|
|
69
|
+
export interface StateStoreBatchAppend<T> {
|
|
70
|
+
/** Stable caller-owned order key. `@meta` is always acquired last. */
|
|
71
|
+
areaId: string;
|
|
72
|
+
store: StateStore<T>;
|
|
73
|
+
type: number;
|
|
74
|
+
payload: unknown;
|
|
75
|
+
flags?: number;
|
|
76
|
+
}
|
|
58
77
|
export interface StateStoreRecoveryStats {
|
|
59
78
|
/** Bytes in the selected generation's WAL, including its header. */
|
|
60
79
|
walBytes: number;
|
|
61
80
|
/** Largest single snapshot payload or WAL frame buffered during recovery. */
|
|
62
81
|
maxReadBytes: number;
|
|
63
82
|
}
|
|
83
|
+
/** A stable identity for one complete snapshot/WAL generation. */
|
|
84
|
+
export interface StateStoreGenerationIdentity {
|
|
85
|
+
generation: number;
|
|
86
|
+
lastRecordSequence: number;
|
|
87
|
+
walBytes: number;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* A durable lease on an immutable source generation.
|
|
91
|
+
*
|
|
92
|
+
* Rotation normally keeps only a short rollback history. Migration uses this
|
|
93
|
+
* lease to make an older source generation part of that history until the
|
|
94
|
+
* copy/catch-up transaction has finished (or has explicitly been abandoned).
|
|
95
|
+
*/
|
|
96
|
+
export interface StateStoreGenerationPin {
|
|
97
|
+
readonly identity: StateStoreGenerationIdentity;
|
|
98
|
+
/** Durable pin file, persisted by long-running migration control state. */
|
|
99
|
+
readonly path: string;
|
|
100
|
+
release(): void;
|
|
101
|
+
}
|
|
102
|
+
/** One bounded value yielded from a keyed state projection. */
|
|
103
|
+
export interface StateStoreStateEntry {
|
|
104
|
+
key: string;
|
|
105
|
+
value: unknown;
|
|
106
|
+
}
|
|
64
107
|
export interface StateStoreRepairResult {
|
|
65
108
|
recoveredGeneration: number;
|
|
66
109
|
recoveredWalBytes: number;
|
|
@@ -112,6 +155,17 @@ export declare class StateStore<T> {
|
|
|
112
155
|
static open<T>(options: StateStoreOptions<T>): StateStore<T>;
|
|
113
156
|
/** Yielding first-open variant for callback-originated projections. */
|
|
114
157
|
static openAsync<T>(options: StateStoreOptions<T>): Promise<StateStore<T>>;
|
|
158
|
+
/**
|
|
159
|
+
* Append one record to each independent store under lexical, all-or-none
|
|
160
|
+
* locks. The caller's `areaId` is the stable order key; hashed on-disk
|
|
161
|
+
* directories must never decide lock order. `@meta` intentionally follows
|
|
162
|
+
* all data areas so a watermark can only publish after its data is durable.
|
|
163
|
+
*
|
|
164
|
+
* A process death between records is still possible, by design. Consumers
|
|
165
|
+
* that need cross-area atomic visibility must use a data-first recovery
|
|
166
|
+
* protocol (AreaLedger does this with prepare records and a meta watermark).
|
|
167
|
+
*/
|
|
168
|
+
static appendBatch<T>(entries: readonly StateStoreBatchAppend<T>[]): StateStoreAppendResult[];
|
|
115
169
|
/**
|
|
116
170
|
* Repair an over-budget generation without making appenders wait for snapshot
|
|
117
171
|
* I/O. Recovery captures a stable generation while holding the scope lock,
|
|
@@ -123,6 +177,19 @@ export declare class StateStore<T> {
|
|
|
123
177
|
getGeneration(): number;
|
|
124
178
|
getLastRecordSequence(): number;
|
|
125
179
|
getRecoveryStats(): StateStoreRecoveryStats;
|
|
180
|
+
getGenerationIdentity(): StateStoreGenerationIdentity;
|
|
181
|
+
/**
|
|
182
|
+
* Persist a generation pin while holding append.lock. The pin is a tiny
|
|
183
|
+
* checksummed file in the store directory, so a concurrent compactor sees it
|
|
184
|
+
* before deciding which immutable generations may be pruned.
|
|
185
|
+
*/
|
|
186
|
+
pinCurrentGeneration(): StateStoreGenerationPin;
|
|
187
|
+
/**
|
|
188
|
+
* Iterate a keyed projection without exposing its mutable backing object.
|
|
189
|
+
* Each yielded row is cloned independently, allowing migration clients to
|
|
190
|
+
* keep projected WAL records bounded even when the source journal is huge.
|
|
191
|
+
*/
|
|
192
|
+
iterateStateEntries(path: readonly string[]): IterableIterator<StateStoreStateEntry>;
|
|
126
193
|
append(type: number, payload: unknown, flags?: number): StateStoreAppendResult;
|
|
127
194
|
/**
|
|
128
195
|
* Async lock acquisition for callback-originated work. The write itself
|
|
@@ -147,6 +214,15 @@ export declare class StateStore<T> {
|
|
|
147
214
|
* and retain the prior compact copy.
|
|
148
215
|
*/
|
|
149
216
|
replaceState(nextState: T): void;
|
|
217
|
+
/**
|
|
218
|
+
* Atomically derive a compact replacement from the newest durable state.
|
|
219
|
+
*
|
|
220
|
+
* Callers that fold a projection must not calculate their replacement before
|
|
221
|
+
* taking the append lock: another writer could otherwise be lost between the
|
|
222
|
+
* read and publication. The transform runs under that lock and publication
|
|
223
|
+
* still validates the source generation after the off-lock snapshot stage.
|
|
224
|
+
*/
|
|
225
|
+
replaceStateWith(transform: (state: T) => T): void;
|
|
150
226
|
private replaceStateAttempt;
|
|
151
227
|
/** Create G+1 without ever merging two generations during recovery. */
|
|
152
228
|
compact(): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state-store.d.ts","sourceRoot":"","sources":["../../src/sync/state-store.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"state-store.d.ts","sourceRoot":"","sources":["../../src/sync/state-store.ts"],"names":[],"mappings":"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"}
|
package/dist/sync/state-store.js
CHANGED
|
@@ -60,6 +60,7 @@ let reflinkCopyHookForTest;
|
|
|
60
60
|
let positionalWriteForTest;
|
|
61
61
|
let rotationClaimWriteHookForTest;
|
|
62
62
|
let scopeLockAcquiredHookForTest;
|
|
63
|
+
let scopeLockWriteHookForTest;
|
|
63
64
|
let asyncLockTimeoutMsForTest;
|
|
64
65
|
let syncLockTimeoutMsForTest;
|
|
65
66
|
export function resetStateStoreSnapshotRecoveryCountForTest() {
|
|
@@ -112,6 +113,10 @@ export function setStateStoreRotationClaimWriteHookForTest(hook) {
|
|
|
112
113
|
export function setStateStoreScopeLockAcquiredHookForTest(hook) {
|
|
113
114
|
scopeLockAcquiredHookForTest = hook;
|
|
114
115
|
}
|
|
116
|
+
/** Test-only seam for a failure after a batch lock file has been created. */
|
|
117
|
+
export function setStateStoreScopeLockWriteHookForTest(hook) {
|
|
118
|
+
scopeLockWriteHookForTest = hook;
|
|
119
|
+
}
|
|
115
120
|
/** Test-only seam for exercising async lock deadlines without a minute-long wait. */
|
|
116
121
|
export function setStateStoreAsyncLockTimeoutForTest(timeoutMs) {
|
|
117
122
|
asyncLockTimeoutMsForTest = timeoutMs;
|
|
@@ -245,6 +250,80 @@ export class StateStore {
|
|
|
245
250
|
return created;
|
|
246
251
|
}
|
|
247
252
|
}
|
|
253
|
+
/**
|
|
254
|
+
* Append one record to each independent store under lexical, all-or-none
|
|
255
|
+
* locks. The caller's `areaId` is the stable order key; hashed on-disk
|
|
256
|
+
* directories must never decide lock order. `@meta` intentionally follows
|
|
257
|
+
* all data areas so a watermark can only publish after its data is durable.
|
|
258
|
+
*
|
|
259
|
+
* A process death between records is still possible, by design. Consumers
|
|
260
|
+
* that need cross-area atomic visibility must use a data-first recovery
|
|
261
|
+
* protocol (AreaLedger does this with prepare records and a meta watermark).
|
|
262
|
+
*/
|
|
263
|
+
static appendBatch(entries) {
|
|
264
|
+
if (entries.length === 0)
|
|
265
|
+
return [];
|
|
266
|
+
const seenAreas = new Set();
|
|
267
|
+
const seenStores = new Set();
|
|
268
|
+
const ordered = entries.map((entry, index) => {
|
|
269
|
+
if (!entry.areaId)
|
|
270
|
+
throw new Error("state-store: batch areaId is required");
|
|
271
|
+
if (seenAreas.has(entry.areaId))
|
|
272
|
+
throw new Error(`state-store: duplicate batch area ${entry.areaId}`);
|
|
273
|
+
if (seenStores.has(entry.store.stateDir))
|
|
274
|
+
throw new Error(`state-store: duplicate batch state directory ${entry.store.stateDir}`);
|
|
275
|
+
seenAreas.add(entry.areaId);
|
|
276
|
+
seenStores.add(entry.store.stateDir);
|
|
277
|
+
assertUint16(entry.type, "record type");
|
|
278
|
+
assertUint16(entry.flags ?? 0, "record flags");
|
|
279
|
+
return { ...entry, index };
|
|
280
|
+
}).sort((left, right) => {
|
|
281
|
+
if (left.areaId === "@meta")
|
|
282
|
+
return right.areaId === "@meta" ? 0 : 1;
|
|
283
|
+
if (right.areaId === "@meta")
|
|
284
|
+
return -1;
|
|
285
|
+
return left.areaId.localeCompare(right.areaId);
|
|
286
|
+
});
|
|
287
|
+
let recovered = false;
|
|
288
|
+
while (true) {
|
|
289
|
+
const result = withScopeLocks(ordered.map((entry) => entry.store.stateDir), () => {
|
|
290
|
+
// Refresh every participant before appending any record. If a
|
|
291
|
+
// generation changed, release the full set and recover off-lock; a
|
|
292
|
+
// partial batch must never retry and duplicate an earlier append.
|
|
293
|
+
const refreshes = new Map();
|
|
294
|
+
for (const entry of ordered) {
|
|
295
|
+
const refresh = entry.store.refreshFromDisk();
|
|
296
|
+
if (!refresh)
|
|
297
|
+
return undefined;
|
|
298
|
+
refreshes.set(entry, refresh);
|
|
299
|
+
}
|
|
300
|
+
const appended = [];
|
|
301
|
+
for (const entry of ordered) {
|
|
302
|
+
appended.push({
|
|
303
|
+
entry,
|
|
304
|
+
result: entry.store.appendUnlocked(entry.type, entry.payload, entry.flags ?? 0, recovered ? { records: [], recovered: true } : refreshes.get(entry)),
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
return appended;
|
|
308
|
+
});
|
|
309
|
+
if (!result) {
|
|
310
|
+
for (const entry of ordered)
|
|
311
|
+
entry.store.recoverNewestGenerationOutsideLock();
|
|
312
|
+
recovered = true;
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
const compact = new Set();
|
|
316
|
+
for (const { entry } of result)
|
|
317
|
+
if (entry.store.needsCompaction())
|
|
318
|
+
compact.add(entry.store);
|
|
319
|
+
for (const store of compact)
|
|
320
|
+
store.compact();
|
|
321
|
+
const byIndex = [];
|
|
322
|
+
for (const appended of result)
|
|
323
|
+
byIndex[appended.entry.index] = appended.result;
|
|
324
|
+
return byIndex;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
248
327
|
/**
|
|
249
328
|
* Repair an over-budget generation without making appenders wait for snapshot
|
|
250
329
|
* I/O. Recovery captures a stable generation while holding the scope lock,
|
|
@@ -355,6 +434,70 @@ export class StateStore {
|
|
|
355
434
|
getRecoveryStats() {
|
|
356
435
|
return { walBytes: this.walBytes, maxReadBytes: this.maxRecoveryReadBytes };
|
|
357
436
|
}
|
|
437
|
+
getGenerationIdentity() {
|
|
438
|
+
return {
|
|
439
|
+
generation: this.generation,
|
|
440
|
+
lastRecordSequence: this.lastRecordSequence,
|
|
441
|
+
walBytes: this.walBytes,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Persist a generation pin while holding append.lock. The pin is a tiny
|
|
446
|
+
* checksummed file in the store directory, so a concurrent compactor sees it
|
|
447
|
+
* before deciding which immutable generations may be pruned.
|
|
448
|
+
*/
|
|
449
|
+
pinCurrentGeneration() {
|
|
450
|
+
while (true) {
|
|
451
|
+
const pin = withScopeLock(this.stateDir, () => {
|
|
452
|
+
const refresh = this.refreshFromDisk();
|
|
453
|
+
if (!refresh)
|
|
454
|
+
return undefined;
|
|
455
|
+
const identity = this.getGenerationIdentity();
|
|
456
|
+
const pinPath = writeGenerationPin(this.stateDir, this.scopeDigest, identity);
|
|
457
|
+
let released = false;
|
|
458
|
+
return {
|
|
459
|
+
identity,
|
|
460
|
+
path: pinPath,
|
|
461
|
+
release: () => {
|
|
462
|
+
if (released)
|
|
463
|
+
return;
|
|
464
|
+
released = true;
|
|
465
|
+
try {
|
|
466
|
+
fs.unlinkSync(pinPath);
|
|
467
|
+
fsyncDirectory(this.stateDir);
|
|
468
|
+
}
|
|
469
|
+
catch (error) {
|
|
470
|
+
if (!isMissing(error))
|
|
471
|
+
throw new Error(`state-store: cannot release generation pin ${pinPath}`, { cause: error });
|
|
472
|
+
}
|
|
473
|
+
},
|
|
474
|
+
};
|
|
475
|
+
});
|
|
476
|
+
if (pin)
|
|
477
|
+
return pin;
|
|
478
|
+
this.recoverNewestGenerationOutsideLock();
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Iterate a keyed projection without exposing its mutable backing object.
|
|
483
|
+
* Each yielded row is cloned independently, allowing migration clients to
|
|
484
|
+
* keep projected WAL records bounded even when the source journal is huge.
|
|
485
|
+
*/
|
|
486
|
+
*iterateStateEntries(path) {
|
|
487
|
+
let current = this.state;
|
|
488
|
+
for (const segment of path) {
|
|
489
|
+
if (typeof current !== "object" || current === null || Array.isArray(current) || !Object.hasOwn(current, segment)) {
|
|
490
|
+
throw new Error(`state-store: state path ${JSON.stringify(path)} is not an object map`);
|
|
491
|
+
}
|
|
492
|
+
current = current[segment];
|
|
493
|
+
}
|
|
494
|
+
if (typeof current !== "object" || current === null || Array.isArray(current)) {
|
|
495
|
+
throw new Error(`state-store: state path ${JSON.stringify(path)} is not an object map`);
|
|
496
|
+
}
|
|
497
|
+
for (const [key, value] of Object.entries(current)) {
|
|
498
|
+
yield { key, value: structuredClone(value) };
|
|
499
|
+
}
|
|
500
|
+
}
|
|
358
501
|
append(type, payload, flags = 0) {
|
|
359
502
|
assertUint16(type, "record type");
|
|
360
503
|
assertUint16(flags, "record flags");
|
|
@@ -494,16 +637,28 @@ export class StateStore {
|
|
|
494
637
|
* and retain the prior compact copy.
|
|
495
638
|
*/
|
|
496
639
|
replaceState(nextState) {
|
|
640
|
+
this.replaceStateWith(() => nextState);
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Atomically derive a compact replacement from the newest durable state.
|
|
644
|
+
*
|
|
645
|
+
* Callers that fold a projection must not calculate their replacement before
|
|
646
|
+
* taking the append lock: another writer could otherwise be lost between the
|
|
647
|
+
* read and publication. The transform runs under that lock and publication
|
|
648
|
+
* still validates the source generation after the off-lock snapshot stage.
|
|
649
|
+
*/
|
|
650
|
+
replaceStateWith(transform) {
|
|
497
651
|
for (let attempt = 0; attempt < REPLACE_STATE_MAX_ATTEMPTS; attempt++) {
|
|
498
|
-
if (this.replaceStateAttempt(
|
|
652
|
+
if (this.replaceStateAttempt(transform))
|
|
499
653
|
return;
|
|
500
654
|
}
|
|
501
655
|
throw new StateStoreLockError(`cannot replace state after ${REPLACE_STATE_MAX_ATTEMPTS} concurrent writes`);
|
|
502
656
|
}
|
|
503
|
-
replaceStateAttempt(
|
|
657
|
+
replaceStateAttempt(transform) {
|
|
504
658
|
const plan = withScopeLock(this.stateDir, () => {
|
|
505
659
|
if (!this.refreshFromDisk())
|
|
506
660
|
return undefined;
|
|
661
|
+
const nextState = transform(this.state);
|
|
507
662
|
const previousGeneration = this.generation;
|
|
508
663
|
const previousWalHasRecords = this.walBytes > WAL_HEADER_BYTES;
|
|
509
664
|
const first = {
|
|
@@ -1542,6 +1697,121 @@ function withScopeLock(dir, action) {
|
|
|
1542
1697
|
throw actionError;
|
|
1543
1698
|
return result;
|
|
1544
1699
|
}
|
|
1700
|
+
/**
|
|
1701
|
+
* Acquire every scope lock in caller-provided stable order, or acquire none.
|
|
1702
|
+
*
|
|
1703
|
+
* Waiting while holding an earlier lock would create the classic A→B/B→A
|
|
1704
|
+
* deadlock across independent StateStores. This optimistic loop instead
|
|
1705
|
+
* drops the partial set and retries after the ordinary bounded backoff. It is
|
|
1706
|
+
* only for tiny durable publication sections; staging, recovery, callbacks,
|
|
1707
|
+
* and network work remain outside these locks.
|
|
1708
|
+
*/
|
|
1709
|
+
function withScopeLocks(dirs, action) {
|
|
1710
|
+
const deadline = Date.now() + (syncLockTimeoutMsForTest ?? SYNC_LOCK_TIMEOUT_MS);
|
|
1711
|
+
const acquired = [];
|
|
1712
|
+
while (true) {
|
|
1713
|
+
let unavailable = false;
|
|
1714
|
+
try {
|
|
1715
|
+
for (const dir of dirs) {
|
|
1716
|
+
const held = tryAcquireScopeLock(dir);
|
|
1717
|
+
if (!held) {
|
|
1718
|
+
unavailable = true;
|
|
1719
|
+
break;
|
|
1720
|
+
}
|
|
1721
|
+
acquired.push(held);
|
|
1722
|
+
}
|
|
1723
|
+
if (!unavailable)
|
|
1724
|
+
break;
|
|
1725
|
+
}
|
|
1726
|
+
catch (error) {
|
|
1727
|
+
releaseScopeLocks(acquired);
|
|
1728
|
+
throw error;
|
|
1729
|
+
}
|
|
1730
|
+
releaseScopeLocks(acquired);
|
|
1731
|
+
if (Date.now() >= deadline) {
|
|
1732
|
+
throw new StateStoreLockError("timed out acquiring ordered scope locks");
|
|
1733
|
+
}
|
|
1734
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, LOCK_RETRY_MS);
|
|
1735
|
+
}
|
|
1736
|
+
let result;
|
|
1737
|
+
let actionError;
|
|
1738
|
+
try {
|
|
1739
|
+
result = action();
|
|
1740
|
+
}
|
|
1741
|
+
catch (error) {
|
|
1742
|
+
actionError = error;
|
|
1743
|
+
}
|
|
1744
|
+
releaseScopeLocks(acquired);
|
|
1745
|
+
if (actionError !== undefined)
|
|
1746
|
+
throw actionError;
|
|
1747
|
+
return result;
|
|
1748
|
+
}
|
|
1749
|
+
function tryAcquireScopeLock(dir) {
|
|
1750
|
+
const lockPath = path.join(dir, "append.lock");
|
|
1751
|
+
if (inProcessAsyncScopeLocks.has(path.resolve(dir))) {
|
|
1752
|
+
throw new StateStoreLockError(`in-process async holder for ${lockPath}`, {
|
|
1753
|
+
reason: "in-process-async-holder",
|
|
1754
|
+
});
|
|
1755
|
+
}
|
|
1756
|
+
let fd;
|
|
1757
|
+
try {
|
|
1758
|
+
fd = fs.openSync(lockPath, "wx");
|
|
1759
|
+
scopeLockWriteHookForTest?.();
|
|
1760
|
+
fs.writeFileSync(fd, String(process.pid));
|
|
1761
|
+
fs.fdatasyncSync(fd);
|
|
1762
|
+
return { lockPath, fd };
|
|
1763
|
+
}
|
|
1764
|
+
catch (error) {
|
|
1765
|
+
// `openSync(..., "wx")` has already made this process the lock owner.
|
|
1766
|
+
// A failed PID write or sync must not leave that live-PID lock behind,
|
|
1767
|
+
// because no stale breaker will remove it while this process remains up.
|
|
1768
|
+
if (fd !== undefined) {
|
|
1769
|
+
let cleanupError;
|
|
1770
|
+
try {
|
|
1771
|
+
fs.closeSync(fd);
|
|
1772
|
+
}
|
|
1773
|
+
catch (closeError) {
|
|
1774
|
+
cleanupError = closeError;
|
|
1775
|
+
}
|
|
1776
|
+
try {
|
|
1777
|
+
fs.unlinkSync(lockPath);
|
|
1778
|
+
}
|
|
1779
|
+
catch (unlinkError) {
|
|
1780
|
+
if (!isMissing(unlinkError))
|
|
1781
|
+
cleanupError ??= unlinkError;
|
|
1782
|
+
}
|
|
1783
|
+
if (cleanupError !== undefined)
|
|
1784
|
+
throw new StateStoreLockError(`cannot clean up ${lockPath}`, { cause: cleanupError });
|
|
1785
|
+
}
|
|
1786
|
+
if (!isAlreadyExists(error)) {
|
|
1787
|
+
throw new StateStoreLockError(`cannot acquire ${lockPath}`, { cause: error });
|
|
1788
|
+
}
|
|
1789
|
+
if (removeDeadLock(lockPath))
|
|
1790
|
+
return tryAcquireScopeLock(dir);
|
|
1791
|
+
return undefined;
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
function releaseScopeLocks(acquired) {
|
|
1795
|
+
let releaseError;
|
|
1796
|
+
while (acquired.length > 0) {
|
|
1797
|
+
const held = acquired.pop();
|
|
1798
|
+
try {
|
|
1799
|
+
fs.closeSync(held.fd);
|
|
1800
|
+
}
|
|
1801
|
+
catch (error) {
|
|
1802
|
+
releaseError ??= error;
|
|
1803
|
+
}
|
|
1804
|
+
try {
|
|
1805
|
+
fs.unlinkSync(held.lockPath);
|
|
1806
|
+
}
|
|
1807
|
+
catch (error) {
|
|
1808
|
+
releaseError ??= error;
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
if (releaseError !== undefined) {
|
|
1812
|
+
throw new StateStoreLockError("cannot release ordered scope locks", { cause: releaseError });
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1545
1815
|
/**
|
|
1546
1816
|
* A best-effort, non-blocking claim for off-lock snapshot materialisation.
|
|
1547
1817
|
* Appenders deliberately ignore it; it only prevents several stale handles
|
|
@@ -1966,7 +2236,7 @@ function retainedGenerationHistoryNeedsRepair(dir, newestGeneration, currentGene
|
|
|
1966
2236
|
return false;
|
|
1967
2237
|
}
|
|
1968
2238
|
function pruneGenerations(dir, retain) {
|
|
1969
|
-
const retained = new Set(retain);
|
|
2239
|
+
const retained = new Set([...retain, ...pinnedGenerations(dir)]);
|
|
1970
2240
|
for (const name of fs.readdirSync(dir)) {
|
|
1971
2241
|
const match = /^(?:snapshot|wal)-(\d+)\.bin$/.exec(name);
|
|
1972
2242
|
if (!match)
|
|
@@ -1983,6 +2253,50 @@ function pruneGenerations(dir, retain) {
|
|
|
1983
2253
|
}
|
|
1984
2254
|
fsyncDirectory(dir);
|
|
1985
2255
|
}
|
|
2256
|
+
function writeGenerationPin(dir, scopeDigest, identity) {
|
|
2257
|
+
const body = {
|
|
2258
|
+
generation: identity.generation,
|
|
2259
|
+
lastRecordSequence: identity.lastRecordSequence,
|
|
2260
|
+
walBytes: identity.walBytes,
|
|
2261
|
+
scopeDigest: scopeDigest.toString("hex"),
|
|
2262
|
+
};
|
|
2263
|
+
const checksum = sha256(Buffer.from(canonicalJson(body), "utf8")).toString("hex");
|
|
2264
|
+
const pinPath = path.join(dir, `generation-pin-${process.pid}-${crypto.randomBytes(8).toString("hex")}.json`);
|
|
2265
|
+
writeNewDurably(pinPath, Buffer.from(`${canonicalJson({ ...body, checksum })}\n`, "utf8"));
|
|
2266
|
+
fsyncDirectory(dir);
|
|
2267
|
+
return pinPath;
|
|
2268
|
+
}
|
|
2269
|
+
/** Invalid pins are retained conservatively: pruning is never allowed to win a crash race. */
|
|
2270
|
+
function pinnedGenerations(dir) {
|
|
2271
|
+
const pins = new Set();
|
|
2272
|
+
for (const name of fs.readdirSync(dir)) {
|
|
2273
|
+
if (!/^generation-pin-[A-Za-z0-9-]+\.json$/.test(name))
|
|
2274
|
+
continue;
|
|
2275
|
+
try {
|
|
2276
|
+
const raw = JSON.parse(fs.readFileSync(path.join(dir, name), "utf8"));
|
|
2277
|
+
const { checksum, ...body } = raw;
|
|
2278
|
+
if (typeof checksum !== "string" ||
|
|
2279
|
+
checksum !== sha256(Buffer.from(canonicalJson(body), "utf8")).toString("hex") ||
|
|
2280
|
+
!Number.isSafeInteger(body.generation) ||
|
|
2281
|
+
body.generation < 1) {
|
|
2282
|
+
// A torn pin represents an in-flight migration; retain every local
|
|
2283
|
+
// generation rather than guessing which source it meant to protect.
|
|
2284
|
+
return fs.readdirSync(dir)
|
|
2285
|
+
.map((candidate) => /^(?:snapshot|wal)-(\d+)\.bin$/.exec(candidate)?.[1])
|
|
2286
|
+
.filter((value) => value !== undefined)
|
|
2287
|
+
.map(Number);
|
|
2288
|
+
}
|
|
2289
|
+
pins.add(body.generation);
|
|
2290
|
+
}
|
|
2291
|
+
catch {
|
|
2292
|
+
return fs.readdirSync(dir)
|
|
2293
|
+
.map((candidate) => /^(?:snapshot|wal)-(\d+)\.bin$/.exec(candidate)?.[1])
|
|
2294
|
+
.filter((value) => value !== undefined)
|
|
2295
|
+
.map(Number);
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
return [...pins];
|
|
2299
|
+
}
|
|
1986
2300
|
function cleanupUnpublishedInitialGeneration(dir) {
|
|
1987
2301
|
if (fs.existsSync(path.join(dir, "current")))
|
|
1988
2302
|
return;
|