@indigoai-us/hq-cloud 6.18.43 → 6.18.44-alpha.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/bin/sync-runner-watch-loop.d.ts +39 -0
- package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-loop.js +134 -14
- package/dist/bin/sync-runner-watch-loop.js.map +1 -1
- package/dist/bin/sync-runner.test.js +606 -1
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +6 -1
- package/dist/cli/sync.js.map +1 -1
- package/dist/file-mode-portability.d.ts +33 -0
- package/dist/file-mode-portability.d.ts.map +1 -0
- package/dist/file-mode-portability.js +41 -0
- package/dist/file-mode-portability.js.map +1 -0
- package/dist/journal-snapshot-memory.test.js +22 -7
- package/dist/journal-snapshot-memory.test.js.map +1 -1
- package/dist/private-folder-transport.d.ts.map +1 -1
- package/dist/private-folder-transport.js +5 -1
- package/dist/private-folder-transport.js.map +1 -1
- package/dist/s3.d.ts +7 -1
- package/dist/s3.d.ts.map +1 -1
- package/dist/s3.js +82 -4
- package/dist/s3.js.map +1 -1
- package/dist/s3.windows-portability.test.d.ts +12 -0
- package/dist/s3.windows-portability.test.d.ts.map +1 -0
- package/dist/s3.windows-portability.test.js +223 -0
- package/dist/s3.windows-portability.test.js.map +1 -0
- package/dist/script-eol.d.ts +34 -0
- package/dist/script-eol.d.ts.map +1 -0
- package/dist/script-eol.js +60 -0
- package/dist/script-eol.js.map +1 -0
- package/dist/sync/event-sync.d.ts.map +1 -1
- package/dist/sync/event-sync.js +4 -1
- package/dist/sync/event-sync.js.map +1 -1
- package/dist/sync/event-sync.test.js +44 -0
- package/dist/sync/event-sync.test.js.map +1 -1
- package/dist/sync/push-receiver.d.ts.map +1 -1
- package/dist/sync/push-receiver.js +11 -6
- package/dist/sync/push-receiver.js.map +1 -1
- package/dist/sync/push-receiver.test.js +56 -1
- package/dist/sync/push-receiver.test.js.map +1 -1
- package/package.json +1 -1
|
@@ -21,7 +21,7 @@ import { PresignObjectIO, S3SdkObjectIO } from "../object-io.js";
|
|
|
21
21
|
import { BandwidthGovernor, setBandwidthGovernorForTesting, } from "../bandwidth.js";
|
|
22
22
|
import { POST_SYNC_FLAG_KEYS, } from "./sync-runner-post-sync-flags.js";
|
|
23
23
|
import { FakeClock, TreeWatcher, createWatchPathFilter, } from "../watcher.js";
|
|
24
|
-
import { adaptivePollMs, advanceMonotonicDeadline, DEFAULT_EVENT_BATCH_LIMIT, DEFAULT_FAST_LANE_MAX_BYTES, DEFAULT_FAST_LANE_MAX_FILE_BYTES, DEFAULT_FAST_LANE_MAX_FILES, DEFAULT_FULL_RECONCILE_MS, EVENT_BATCH_LIMIT_ENV, FAST_LANE_MAX_BYTES_ENV, FAST_LANE_MAX_FILES_ENV, FULL_RECONCILE_MS_ENV, isFastLocalBatch, isLargeLocalUpload, localUploadSizeClass, isChoreLocalPush, isSessionLogsPushOnlyPath, isFastReceiverBatch, partitionLocalBatch, journalEntriesForWatcherDelete, resolveEventBatchLimit, resolveFullReconcileMs, resolveWatchPollMs, CHORE_STARVATION_CAP_MS, CooperativePassScheduler, allowConcurrentHandoffForGuardedWork, REALTIME_ADMISSION_DEADLINE_MS, REALTIME_DRAIN_BURST_MS, REALTIME_WORKER_OCCUPANCY_CAP, REALTIME_LARGE_UPLOAD_THRESHOLD_BYTES, RealtimeAdmissionTimeoutError, setRealtimeAdmissionDeadlineMsForTesting, setRealtimeDrainBurstMsForTesting, normalizeCoordinatorPaths, shouldQuarantineWatcherDeleteIntents, shouldUseAreaStorage, WATCHER_REDISCOVERY_RETRY_MS, WATCHER_REDISCOVERY_RETRY_MAX_MS, EVENT_SYNC_STARTUP_SHUTDOWN_WAIT_MS, } from "./sync-runner-watch-loop.js";
|
|
24
|
+
import { adaptivePollMs, advanceMonotonicDeadline, DEFAULT_EVENT_BATCH_LIMIT, DEFAULT_FAST_LANE_MAX_BYTES, DEFAULT_FAST_LANE_MAX_FILE_BYTES, DEFAULT_FAST_LANE_MAX_FILES, DEFAULT_FULL_RECONCILE_MS, EVENT_BATCH_LIMIT_ENV, FAST_LANE_MAX_BYTES_ENV, FAST_LANE_MAX_FILES_ENV, FULL_RECONCILE_MS_ENV, isFastLocalBatch, isLargeLocalUpload, localUploadSizeClass, isChoreLocalPush, isSessionLogsPushOnlyPath, isFastReceiverBatch, partitionLocalBatch, journalEntriesForWatcherDelete, resolveEventBatchLimit, resolveFullReconcileMs, resolveWatchPollMs, CHORE_STARVATION_CAP_MS, CooperativePassScheduler, allowConcurrentHandoffForGuardedWork, REALTIME_ADMISSION_DEADLINE_MS, REALTIME_DRAIN_BURST_MS, REALTIME_WORKER_OCCUPANCY_CAP, REALTIME_LARGE_UPLOAD_THRESHOLD_BYTES, RealtimeAdmissionTimeoutError, REALTIME_ADMISSION_RETRY_MS, setRealtimeAdmissionDeadlineMsForTesting, setRealtimeDrainBurstMsForTesting, normalizeCoordinatorPaths, shouldQuarantineWatcherDeleteIntents, shouldUseAreaStorage, WATCHER_REDISCOVERY_RETRY_MS, WATCHER_REDISCOVERY_RETRY_MAX_MS, EVENT_SYNC_STARTUP_SHUTDOWN_WAIT_MS, } from "./sync-runner-watch-loop.js";
|
|
25
25
|
import { FullPassCapController, resetFullPassCapLogForTest, } from "../sync/full-pass-caps.js";
|
|
26
26
|
import { AreaLedgerMigration, JournalStore, getJournalPath, openJournalStateStoreForMigration, PERSONAL_VAULT_JOURNAL_SLUG, readJournal, readJournalForPass, readJournalScoped, readJournalScopedAsync, writeJournal, } from "../journal.js";
|
|
27
27
|
import { AreaLedger } from "../sync/area-ledger.js";
|
|
@@ -11234,6 +11234,310 @@ describe("CooperativePassScheduler", () => {
|
|
|
11234
11234
|
releaseChore();
|
|
11235
11235
|
await chore;
|
|
11236
11236
|
});
|
|
11237
|
+
/**
|
|
11238
|
+
* Settles to "rejected:<name>" or "resolved", or "pending" if neither
|
|
11239
|
+
* happened within `withinMs`. Never leaves a test hanging on an unbounded op.
|
|
11240
|
+
*/
|
|
11241
|
+
async function settledWithin(op, withinMs) {
|
|
11242
|
+
return Promise.race([
|
|
11243
|
+
op.then(() => "resolved", (error) => `rejected:${error.name}`),
|
|
11244
|
+
new Promise((resolve) => setTimeout(() => resolve("pending"), withinMs)),
|
|
11245
|
+
]);
|
|
11246
|
+
}
|
|
11247
|
+
it("REGRESSION: bounds a realtime op queued during a checkpoint hand-off that closes without admitting it", async () => {
|
|
11248
|
+
// alpha.1 round 20261003T134653Z: a receiver op queued while the startup
|
|
11249
|
+
// full reconcile had lent its slot waited 600 s. The hand-off left
|
|
11250
|
+
// `active` null, so the op was queued with no admission timer, and the
|
|
11251
|
+
// chore took the slot back without yielding again.
|
|
11252
|
+
setRealtimeAdmissionDeadlineMsForTesting(80);
|
|
11253
|
+
setRealtimeDrainBurstMsForTesting(20);
|
|
11254
|
+
const scheduler = new CooperativePassScheduler(() => Date.now());
|
|
11255
|
+
let releaseChore = () => { };
|
|
11256
|
+
const choreHold = new Promise((resolve) => {
|
|
11257
|
+
releaseChore = resolve;
|
|
11258
|
+
});
|
|
11259
|
+
let firstStarted = () => { };
|
|
11260
|
+
const firstRunning = new Promise((resolve) => {
|
|
11261
|
+
firstStarted = resolve;
|
|
11262
|
+
});
|
|
11263
|
+
let openCheckpoint = () => { };
|
|
11264
|
+
const checkpointGate = new Promise((resolve) => {
|
|
11265
|
+
openCheckpoint = resolve;
|
|
11266
|
+
});
|
|
11267
|
+
const chore = scheduler.submit(async ({ checkpoint }) => {
|
|
11268
|
+
await checkpointGate;
|
|
11269
|
+
await checkpoint();
|
|
11270
|
+
await choreHold; // never yields again
|
|
11271
|
+
return 0;
|
|
11272
|
+
}, "chore");
|
|
11273
|
+
const first = scheduler.submit(async () => {
|
|
11274
|
+
firstStarted();
|
|
11275
|
+
await new Promise((resolve) => setTimeout(resolve, 60)); // outlives the burst window
|
|
11276
|
+
return 0;
|
|
11277
|
+
}, "realtime");
|
|
11278
|
+
openCheckpoint();
|
|
11279
|
+
await firstRunning;
|
|
11280
|
+
const started = Date.now();
|
|
11281
|
+
const queued = scheduler.submit(async () => 0, "realtime");
|
|
11282
|
+
expect(await settledWithin(queued, 400)).toBe("rejected:RealtimeAdmissionTimeout");
|
|
11283
|
+
expect(Date.now() - started).toBeLessThan(300);
|
|
11284
|
+
await first;
|
|
11285
|
+
releaseChore();
|
|
11286
|
+
await chore;
|
|
11287
|
+
});
|
|
11288
|
+
it("REGRESSION: bounds a realtime op queued during a network-transfer hand-off that closes without admitting it", async () => {
|
|
11289
|
+
setRealtimeAdmissionDeadlineMsForTesting(80);
|
|
11290
|
+
setRealtimeDrainBurstMsForTesting(20);
|
|
11291
|
+
const scheduler = new CooperativePassScheduler(() => Date.now());
|
|
11292
|
+
let releaseChore = () => { };
|
|
11293
|
+
const choreHold = new Promise((resolve) => {
|
|
11294
|
+
releaseChore = resolve;
|
|
11295
|
+
});
|
|
11296
|
+
let transferStarted = () => { };
|
|
11297
|
+
const transferRunning = new Promise((resolve) => {
|
|
11298
|
+
transferStarted = resolve;
|
|
11299
|
+
});
|
|
11300
|
+
const chore = scheduler.submit(async ({ checkpoint }) => {
|
|
11301
|
+
const transfer = await checkpoint.runChoreNetworkTransfer(async () => {
|
|
11302
|
+
transferStarted();
|
|
11303
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
11304
|
+
return 0;
|
|
11305
|
+
});
|
|
11306
|
+
transfer.complete();
|
|
11307
|
+
await choreHold; // never yields again
|
|
11308
|
+
return 0;
|
|
11309
|
+
}, "chore");
|
|
11310
|
+
await transferRunning;
|
|
11311
|
+
// The first op takes the single backlog slot and outlives the fresh
|
|
11312
|
+
// post-transfer window; the second is left queued behind it.
|
|
11313
|
+
const first = scheduler.submit(async () => {
|
|
11314
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
11315
|
+
return 0;
|
|
11316
|
+
}, "realtime");
|
|
11317
|
+
const started = Date.now();
|
|
11318
|
+
const queued = scheduler.submit(async () => 0, "realtime");
|
|
11319
|
+
expect(await settledWithin(queued, 400)).toBe("rejected:RealtimeAdmissionTimeout");
|
|
11320
|
+
expect(Date.now() - started).toBeLessThan(300);
|
|
11321
|
+
await first;
|
|
11322
|
+
releaseChore();
|
|
11323
|
+
await chore;
|
|
11324
|
+
});
|
|
11325
|
+
it("REGRESSION: bounds realtime queued behind realtime when a starved chore takes the slot", async () => {
|
|
11326
|
+
setRealtimeAdmissionDeadlineMsForTesting(80);
|
|
11327
|
+
let now = 0;
|
|
11328
|
+
const scheduler = new CooperativePassScheduler(() => now, 1_000);
|
|
11329
|
+
let releaseRealtime = () => { };
|
|
11330
|
+
const realtimeHold = new Promise((resolve) => {
|
|
11331
|
+
releaseRealtime = resolve;
|
|
11332
|
+
});
|
|
11333
|
+
let releaseChore = () => { };
|
|
11334
|
+
const choreHold = new Promise((resolve) => {
|
|
11335
|
+
releaseChore = resolve;
|
|
11336
|
+
});
|
|
11337
|
+
const running = scheduler.submit(async () => {
|
|
11338
|
+
await realtimeHold;
|
|
11339
|
+
return 0;
|
|
11340
|
+
}, "realtime");
|
|
11341
|
+
const chore = scheduler.submit(async () => {
|
|
11342
|
+
await choreHold; // never yields
|
|
11343
|
+
return 0;
|
|
11344
|
+
}, "chore");
|
|
11345
|
+
// Queued while realtime (not a chore) holds the slot: no timer yet.
|
|
11346
|
+
const queued = scheduler.submit(async () => 0, "realtime");
|
|
11347
|
+
now = 1_000; // the chore has waited past the starvation cap
|
|
11348
|
+
releaseRealtime();
|
|
11349
|
+
await running;
|
|
11350
|
+
// The deadline runs from when the chore took the slot, not from enqueue:
|
|
11351
|
+
// time spent behind other realtime work is not charged to it.
|
|
11352
|
+
const choreTookSlot = Date.now();
|
|
11353
|
+
expect(await settledWithin(queued, 400)).toBe("rejected:RealtimeAdmissionTimeout");
|
|
11354
|
+
expect(Date.now() - choreTookSlot).toBeGreaterThanOrEqual(70);
|
|
11355
|
+
releaseChore();
|
|
11356
|
+
await chore;
|
|
11357
|
+
});
|
|
11358
|
+
it("never leaves realtime queued past the admission deadline across randomized hand-offs", async () => {
|
|
11359
|
+
// Seeded so a failure names a reproducible case.
|
|
11360
|
+
let seed = 0x5eed;
|
|
11361
|
+
const rand = () => {
|
|
11362
|
+
seed = (seed * 1_103_515_245 + 12_345) & 0x7fffffff;
|
|
11363
|
+
return seed / 0x7fffffff;
|
|
11364
|
+
};
|
|
11365
|
+
const deadlineMs = 60;
|
|
11366
|
+
const slackMs = 150;
|
|
11367
|
+
setRealtimeAdmissionDeadlineMsForTesting(deadlineMs);
|
|
11368
|
+
const outcomes = new Set();
|
|
11369
|
+
for (let iteration = 0; iteration < 60; iteration += 1) {
|
|
11370
|
+
const burstMs = 5 + Math.floor(rand() * 30);
|
|
11371
|
+
const firstMs = 5 + Math.floor(rand() * 80);
|
|
11372
|
+
const arrivalMs = Math.floor(rand() * 20);
|
|
11373
|
+
const network = rand() < 0.5;
|
|
11374
|
+
setRealtimeDrainBurstMsForTesting(burstMs);
|
|
11375
|
+
const scheduler = new CooperativePassScheduler(() => Date.now());
|
|
11376
|
+
let releaseChore = () => { };
|
|
11377
|
+
const choreHold = new Promise((resolve) => {
|
|
11378
|
+
releaseChore = resolve;
|
|
11379
|
+
});
|
|
11380
|
+
let lent = () => { };
|
|
11381
|
+
const slotLent = new Promise((resolve) => {
|
|
11382
|
+
lent = resolve;
|
|
11383
|
+
});
|
|
11384
|
+
let go = () => { };
|
|
11385
|
+
const choreGate = new Promise((resolve) => {
|
|
11386
|
+
go = resolve;
|
|
11387
|
+
});
|
|
11388
|
+
const chore = scheduler.submit(async ({ checkpoint }) => {
|
|
11389
|
+
await choreGate;
|
|
11390
|
+
if (network) {
|
|
11391
|
+
const transfer = await checkpoint.runChoreNetworkTransfer(async () => {
|
|
11392
|
+
lent();
|
|
11393
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
11394
|
+
return 0;
|
|
11395
|
+
});
|
|
11396
|
+
transfer.complete();
|
|
11397
|
+
}
|
|
11398
|
+
else {
|
|
11399
|
+
lent();
|
|
11400
|
+
await checkpoint();
|
|
11401
|
+
}
|
|
11402
|
+
await choreHold;
|
|
11403
|
+
return 0;
|
|
11404
|
+
}, "chore");
|
|
11405
|
+
const first = scheduler.submit(async () => {
|
|
11406
|
+
await new Promise((resolve) => setTimeout(resolve, firstMs));
|
|
11407
|
+
return 0;
|
|
11408
|
+
}, "realtime");
|
|
11409
|
+
go();
|
|
11410
|
+
await slotLent;
|
|
11411
|
+
await new Promise((resolve) => setTimeout(resolve, arrivalMs));
|
|
11412
|
+
const queuedAt = Date.now();
|
|
11413
|
+
const queued = scheduler.submit(async () => 0, "realtime");
|
|
11414
|
+
const outcome = await settledWithin(queued, deadlineMs + slackMs);
|
|
11415
|
+
const label = `iteration ${iteration} network=${network} burst=${burstMs} first=${firstMs} arrival=${arrivalMs}`;
|
|
11416
|
+
expect(["resolved", "rejected:RealtimeAdmissionTimeout"], label).toContain(outcome);
|
|
11417
|
+
expect(Date.now() - queuedAt, label).toBeLessThanOrEqual(deadlineMs + slackMs);
|
|
11418
|
+
outcomes.add(outcome);
|
|
11419
|
+
await first.catch(() => 0);
|
|
11420
|
+
releaseChore();
|
|
11421
|
+
await chore;
|
|
11422
|
+
}
|
|
11423
|
+
// The seeded cases must cover both sides: some ops admitted by a
|
|
11424
|
+
// hand-off, some refused at the deadline.
|
|
11425
|
+
expect([...outcomes].sort()).toEqual(["rejected:RealtimeAdmissionTimeout", "resolved"]);
|
|
11426
|
+
});
|
|
11427
|
+
it("admits queued realtime when the chore checkpoints again inside the deadline after a hand-off closed without it", async () => {
|
|
11428
|
+
// The deadline must not refuse an op the chore would admit in time.
|
|
11429
|
+
setRealtimeAdmissionDeadlineMsForTesting(300);
|
|
11430
|
+
setRealtimeDrainBurstMsForTesting(20);
|
|
11431
|
+
const scheduler = new CooperativePassScheduler(() => Date.now());
|
|
11432
|
+
let releaseChore = () => { };
|
|
11433
|
+
const choreHold = new Promise((resolve) => {
|
|
11434
|
+
releaseChore = resolve;
|
|
11435
|
+
});
|
|
11436
|
+
let firstStarted = () => { };
|
|
11437
|
+
const firstRunning = new Promise((resolve) => {
|
|
11438
|
+
firstStarted = resolve;
|
|
11439
|
+
});
|
|
11440
|
+
let openCheckpoint = () => { };
|
|
11441
|
+
const checkpointGate = new Promise((resolve) => {
|
|
11442
|
+
openCheckpoint = resolve;
|
|
11443
|
+
});
|
|
11444
|
+
const chore = scheduler.submit(async ({ checkpoint }) => {
|
|
11445
|
+
await checkpointGate;
|
|
11446
|
+
await checkpoint();
|
|
11447
|
+
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
11448
|
+
await checkpoint();
|
|
11449
|
+
await choreHold;
|
|
11450
|
+
return 0;
|
|
11451
|
+
}, "chore");
|
|
11452
|
+
const first = scheduler.submit(async () => {
|
|
11453
|
+
firstStarted();
|
|
11454
|
+
await new Promise((resolve) => setTimeout(resolve, 60)); // outlives the burst window
|
|
11455
|
+
return 0;
|
|
11456
|
+
}, "realtime");
|
|
11457
|
+
openCheckpoint();
|
|
11458
|
+
await firstRunning;
|
|
11459
|
+
const queued = scheduler.submit(async () => 7, "realtime");
|
|
11460
|
+
expect(await settledWithin(queued, 600)).toBe("resolved");
|
|
11461
|
+
await expect(queued).resolves.toBe(7);
|
|
11462
|
+
await first;
|
|
11463
|
+
releaseChore();
|
|
11464
|
+
await chore;
|
|
11465
|
+
});
|
|
11466
|
+
it("does not refuse queued realtime on a chore-era deadline once the chore finishes and only realtime is ahead", async () => {
|
|
11467
|
+
// Armed while the chore held the slot; after the chore finishes the op
|
|
11468
|
+
// waits only behind realtime, which the deadline does not count.
|
|
11469
|
+
setRealtimeAdmissionDeadlineMsForTesting(80);
|
|
11470
|
+
const scheduler = new CooperativePassScheduler(() => Date.now());
|
|
11471
|
+
let releaseChore = () => { };
|
|
11472
|
+
const choreHold = new Promise((resolve) => {
|
|
11473
|
+
releaseChore = resolve;
|
|
11474
|
+
});
|
|
11475
|
+
const chore = scheduler.submit(async () => {
|
|
11476
|
+
await choreHold;
|
|
11477
|
+
return 0;
|
|
11478
|
+
}, "chore");
|
|
11479
|
+
const ahead = scheduler.submit(async () => {
|
|
11480
|
+
await new Promise((resolve) => setTimeout(resolve, 150)); // runs past the deadline
|
|
11481
|
+
return 1;
|
|
11482
|
+
}, "realtime");
|
|
11483
|
+
const queued = scheduler.submit(async () => 2, "realtime");
|
|
11484
|
+
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
11485
|
+
releaseChore();
|
|
11486
|
+
await chore;
|
|
11487
|
+
expect(await settledWithin(queued, 600)).toBe("resolved");
|
|
11488
|
+
await expect(ahead).resolves.toBe(1);
|
|
11489
|
+
await expect(queued).resolves.toBe(2);
|
|
11490
|
+
});
|
|
11491
|
+
it("rejects a timed-out realtime op as retryable and never runs it around the chore's gate", async () => {
|
|
11492
|
+
// scoped-sync-passes-must-not-bypass-global-lock: the deadline only
|
|
11493
|
+
// refuses the op. Its body must not run while the chore holds the slot,
|
|
11494
|
+
// and must not run later either; the caller retries it as a new pass.
|
|
11495
|
+
setRealtimeAdmissionDeadlineMsForTesting(80);
|
|
11496
|
+
setRealtimeDrainBurstMsForTesting(20);
|
|
11497
|
+
const scheduler = new CooperativePassScheduler(() => Date.now());
|
|
11498
|
+
let choreHolding = false;
|
|
11499
|
+
let releaseChore = () => { };
|
|
11500
|
+
const choreHold = new Promise((resolve) => {
|
|
11501
|
+
releaseChore = resolve;
|
|
11502
|
+
});
|
|
11503
|
+
let firstStarted = () => { };
|
|
11504
|
+
const firstRunning = new Promise((resolve) => {
|
|
11505
|
+
firstStarted = resolve;
|
|
11506
|
+
});
|
|
11507
|
+
let openCheckpoint = () => { };
|
|
11508
|
+
const checkpointGate = new Promise((resolve) => {
|
|
11509
|
+
openCheckpoint = resolve;
|
|
11510
|
+
});
|
|
11511
|
+
const chore = scheduler.submit(async ({ checkpoint }) => {
|
|
11512
|
+
await checkpointGate;
|
|
11513
|
+
await checkpoint();
|
|
11514
|
+
choreHolding = true;
|
|
11515
|
+
await choreHold;
|
|
11516
|
+
choreHolding = false;
|
|
11517
|
+
return 0;
|
|
11518
|
+
}, "chore");
|
|
11519
|
+
const first = scheduler.submit(async () => {
|
|
11520
|
+
firstStarted();
|
|
11521
|
+
await new Promise((resolve) => setTimeout(resolve, 60));
|
|
11522
|
+
return 0;
|
|
11523
|
+
}, "realtime");
|
|
11524
|
+
openCheckpoint();
|
|
11525
|
+
await firstRunning;
|
|
11526
|
+
const bodyRuns = [];
|
|
11527
|
+
const queued = scheduler.submit(async () => {
|
|
11528
|
+
bodyRuns.push(choreHolding);
|
|
11529
|
+
return 0;
|
|
11530
|
+
}, "realtime");
|
|
11531
|
+
const error = await queued.then(() => null, (rejection) => rejection);
|
|
11532
|
+
expect(error).toBeInstanceOf(RealtimeAdmissionTimeoutError);
|
|
11533
|
+
expect(error.retryable).toBe(true);
|
|
11534
|
+
expect(bodyRuns).toEqual([]);
|
|
11535
|
+
await first;
|
|
11536
|
+
releaseChore();
|
|
11537
|
+
await chore;
|
|
11538
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
11539
|
+
expect(bodyRuns).toEqual([]);
|
|
11540
|
+
});
|
|
11237
11541
|
it("keeps retryable realtime ahead of a queued full chore", async () => {
|
|
11238
11542
|
const scheduler = new CooperativePassScheduler(() => 0);
|
|
11239
11543
|
const order = [];
|
|
@@ -11376,6 +11680,307 @@ describe("first-tick realtime yield", () => {
|
|
|
11376
11680
|
}
|
|
11377
11681
|
});
|
|
11378
11682
|
});
|
|
11683
|
+
describe("realtime admission timeout is a deferral, not a pass failure", () => {
|
|
11684
|
+
// indigo-hq-sync-deliberate-skip-not-fatal-error-exit2: a realtime op
|
|
11685
|
+
// refused at the admission deadline never ran. It must not advance the
|
|
11686
|
+
// route's push-failure backoff or be reported as a failed receiver pull.
|
|
11687
|
+
const choreRunPass = (choreHold, onScoped) => vi.fn(async (argv, checkpoint, onJournalsReady) => {
|
|
11688
|
+
const scoped = Array.isArray(argv) && argv.includes("--scope-path");
|
|
11689
|
+
if (!scoped && checkpoint) {
|
|
11690
|
+
onJournalsReady?.();
|
|
11691
|
+
await choreHold; // holds the slot and never checkpoints
|
|
11692
|
+
return completedPassOutcome();
|
|
11693
|
+
}
|
|
11694
|
+
if (scoped)
|
|
11695
|
+
onScoped(argv);
|
|
11696
|
+
return completedPassOutcome();
|
|
11697
|
+
});
|
|
11698
|
+
/** The loop may be mid-pass rather than asleep; only wake a pending sleep. */
|
|
11699
|
+
const tickIfAsleep = (sleep) => {
|
|
11700
|
+
try {
|
|
11701
|
+
sleep.tick();
|
|
11702
|
+
}
|
|
11703
|
+
catch {
|
|
11704
|
+
// not sleeping yet
|
|
11705
|
+
}
|
|
11706
|
+
};
|
|
11707
|
+
let previousCoordinator;
|
|
11708
|
+
let previousInterval;
|
|
11709
|
+
beforeEach(() => {
|
|
11710
|
+
previousCoordinator = process.env.HQ_SYNC_PARALLEL_COORDINATOR;
|
|
11711
|
+
previousInterval = process.env.HQ_SYNC_FULL_RECONCILE_MS;
|
|
11712
|
+
process.env.HQ_SYNC_PARALLEL_COORDINATOR = "1";
|
|
11713
|
+
process.env.HQ_SYNC_FULL_RECONCILE_MS = "10800000";
|
|
11714
|
+
setRealtimeAdmissionDeadlineMsForTesting(50);
|
|
11715
|
+
});
|
|
11716
|
+
afterEach(() => {
|
|
11717
|
+
setRealtimeAdmissionDeadlineMsForTesting(undefined);
|
|
11718
|
+
if (previousCoordinator === undefined)
|
|
11719
|
+
delete process.env.HQ_SYNC_PARALLEL_COORDINATOR;
|
|
11720
|
+
else
|
|
11721
|
+
process.env.HQ_SYNC_PARALLEL_COORDINATOR = previousCoordinator;
|
|
11722
|
+
if (previousInterval === undefined)
|
|
11723
|
+
delete process.env.HQ_SYNC_FULL_RECONCILE_MS;
|
|
11724
|
+
else
|
|
11725
|
+
process.env.HQ_SYNC_FULL_RECONCILE_MS = previousInterval;
|
|
11726
|
+
});
|
|
11727
|
+
it("retries a refused watcher push after a fixed delay without counting a route failure", async () => {
|
|
11728
|
+
const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-admission-push-"));
|
|
11729
|
+
const retryDelays = [];
|
|
11730
|
+
const retryCallbacks = [];
|
|
11731
|
+
const pushRetryScheduler = {
|
|
11732
|
+
setTimeout(callback, delayMs) {
|
|
11733
|
+
retryDelays.push(delayMs);
|
|
11734
|
+
retryCallbacks.push(callback);
|
|
11735
|
+
return retryCallbacks.length;
|
|
11736
|
+
},
|
|
11737
|
+
clearTimeout() { },
|
|
11738
|
+
};
|
|
11739
|
+
const diagnostics = [];
|
|
11740
|
+
const failureTelemetry = [];
|
|
11741
|
+
const scopedPushes = [];
|
|
11742
|
+
let releaseChore = () => { };
|
|
11743
|
+
const choreHold = new Promise((resolve) => {
|
|
11744
|
+
releaseChore = resolve;
|
|
11745
|
+
});
|
|
11746
|
+
const runPass = choreRunPass(choreHold, (argv) => {
|
|
11747
|
+
if (argv.includes("push"))
|
|
11748
|
+
scopedPushes.push(argv);
|
|
11749
|
+
});
|
|
11750
|
+
const watcher = makeBatchWatcherStub();
|
|
11751
|
+
const sleep = makeSteppableSleep();
|
|
11752
|
+
let triggerShutdown = () => { };
|
|
11753
|
+
const loop = runRunnerWithLoop([
|
|
11754
|
+
"--companies", "--watch", "--event-push", "--direction", "both",
|
|
11755
|
+
"--hq-root", hqRoot, "--poll-remote-ms", "60000",
|
|
11756
|
+
], {
|
|
11757
|
+
runPass: runPass,
|
|
11758
|
+
runPassSupportsCooperativeCheckpoint: true,
|
|
11759
|
+
clock: new FakeClock(),
|
|
11760
|
+
pushRetryScheduler,
|
|
11761
|
+
stderr: { write: (line) => { diagnostics.push(String(line)); return true; } },
|
|
11762
|
+
emitScopedPushFailureTelemetry: (event) => { failureTelemetry.push(event); },
|
|
11763
|
+
createWatcher: () => watcher,
|
|
11764
|
+
sleep: sleep.sleep,
|
|
11765
|
+
onShutdownSignal: (handler) => {
|
|
11766
|
+
triggerShutdown = handler;
|
|
11767
|
+
return () => { };
|
|
11768
|
+
},
|
|
11769
|
+
});
|
|
11770
|
+
try {
|
|
11771
|
+
await flushLoopMicrotasks(40);
|
|
11772
|
+
const notePath = path.join(hqRoot, "companies/indigo/note.md");
|
|
11773
|
+
fs.mkdirSync(path.dirname(notePath), { recursive: true });
|
|
11774
|
+
fs.writeFileSync(notePath, "edit while a reconcile holds the slot");
|
|
11775
|
+
watcher.emit("companies/indigo/note.md", {
|
|
11776
|
+
paths: new Map([[notePath, "companies/indigo/note.md"]]),
|
|
11777
|
+
});
|
|
11778
|
+
const deadline = Date.now() + 2_000;
|
|
11779
|
+
while (!retryDelays.includes(REALTIME_ADMISSION_RETRY_MS) && Date.now() < deadline) {
|
|
11780
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
11781
|
+
}
|
|
11782
|
+
expect(retryDelays).toContain(REALTIME_ADMISSION_RETRY_MS);
|
|
11783
|
+
expect(scopedPushes).toEqual([]);
|
|
11784
|
+
expect(failureTelemetry).toEqual([]);
|
|
11785
|
+
expect(diagnostics.join("")).not.toContain("scoped-push-failure");
|
|
11786
|
+
// Once the chore lets go, the retry pushes the restored path.
|
|
11787
|
+
releaseChore();
|
|
11788
|
+
await flushLoopMicrotasks(40);
|
|
11789
|
+
for (const callback of retryCallbacks.splice(0))
|
|
11790
|
+
callback();
|
|
11791
|
+
const pushDeadline = Date.now() + 2_000;
|
|
11792
|
+
while (scopedPushes.length === 0 && Date.now() < pushDeadline) {
|
|
11793
|
+
tickIfAsleep(sleep);
|
|
11794
|
+
await flushLoopMicrotasks(20);
|
|
11795
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
11796
|
+
}
|
|
11797
|
+
expect(scopedPushes.some((argv) => argv.join(" ").includes("note.md")))
|
|
11798
|
+
.toBe(true);
|
|
11799
|
+
expect(failureTelemetry).toEqual([]);
|
|
11800
|
+
}
|
|
11801
|
+
finally {
|
|
11802
|
+
releaseChore();
|
|
11803
|
+
triggerShutdown();
|
|
11804
|
+
await loop;
|
|
11805
|
+
}
|
|
11806
|
+
});
|
|
11807
|
+
/**
|
|
11808
|
+
* Runs the loop with a scoped watcher change taken by the main loop's
|
|
11809
|
+
* drain (no batch, so not the coordinator fast dispatch). `holdChore`
|
|
11810
|
+
* keeps the startup reconcile on the slot so realtime is refused.
|
|
11811
|
+
*/
|
|
11812
|
+
async function runMainLoopPull(options) {
|
|
11813
|
+
const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-admission-pull-"));
|
|
11814
|
+
fs.mkdirSync(path.join(hqRoot, "companies/indigo"), { recursive: true });
|
|
11815
|
+
fs.writeFileSync(path.join(hqRoot, "companies/indigo/note.md"), "x");
|
|
11816
|
+
const retryDelays = [];
|
|
11817
|
+
const retryCallbacks = [];
|
|
11818
|
+
const pushRetryScheduler = {
|
|
11819
|
+
setTimeout(callback, delayMs) {
|
|
11820
|
+
retryDelays.push(delayMs);
|
|
11821
|
+
retryCallbacks.push(callback);
|
|
11822
|
+
return retryCallbacks.length;
|
|
11823
|
+
},
|
|
11824
|
+
clearTimeout() { },
|
|
11825
|
+
};
|
|
11826
|
+
const stderr = [];
|
|
11827
|
+
const spy = vi.spyOn(process.stderr, "write").mockImplementation((chunk) => {
|
|
11828
|
+
stderr.push(String(chunk));
|
|
11829
|
+
return true;
|
|
11830
|
+
});
|
|
11831
|
+
let releaseChore = () => { };
|
|
11832
|
+
const choreHold = options.holdChore
|
|
11833
|
+
? new Promise((resolve) => {
|
|
11834
|
+
releaseChore = resolve;
|
|
11835
|
+
})
|
|
11836
|
+
: Promise.resolve();
|
|
11837
|
+
const scopedPulls = [];
|
|
11838
|
+
const runPass = vi.fn(async (argv, checkpoint, onJournalsReady) => {
|
|
11839
|
+
const scoped = Array.isArray(argv) && argv.includes("--scope-path");
|
|
11840
|
+
if (!scoped && checkpoint) {
|
|
11841
|
+
onJournalsReady?.();
|
|
11842
|
+
await choreHold;
|
|
11843
|
+
return completedPassOutcome();
|
|
11844
|
+
}
|
|
11845
|
+
if (scoped && argv.includes("pull")) {
|
|
11846
|
+
scopedPulls.push(argv);
|
|
11847
|
+
if (options.pullExit !== 0)
|
|
11848
|
+
return options.pullExit;
|
|
11849
|
+
}
|
|
11850
|
+
return completedPassOutcome();
|
|
11851
|
+
});
|
|
11852
|
+
const watcher = makeBatchWatcherStub();
|
|
11853
|
+
const sleep = makeSteppableSleep();
|
|
11854
|
+
let triggerShutdown = () => { };
|
|
11855
|
+
const loop = runRunnerWithLoop([
|
|
11856
|
+
"--companies", "--watch", "--event-push", "--direction", "both",
|
|
11857
|
+
"--hq-root", hqRoot, "--poll-remote-ms", "60000",
|
|
11858
|
+
], {
|
|
11859
|
+
runPass: runPass,
|
|
11860
|
+
runPassSupportsCooperativeCheckpoint: true,
|
|
11861
|
+
clock: new FakeClock(),
|
|
11862
|
+
pushRetryScheduler,
|
|
11863
|
+
createWatcher: () => watcher,
|
|
11864
|
+
sleep: sleep.sleep,
|
|
11865
|
+
onShutdownSignal: (handler) => {
|
|
11866
|
+
triggerShutdown = handler;
|
|
11867
|
+
return () => { };
|
|
11868
|
+
},
|
|
11869
|
+
});
|
|
11870
|
+
try {
|
|
11871
|
+
await flushLoopMicrotasks(40);
|
|
11872
|
+
watcher.emit("companies/indigo/note.md");
|
|
11873
|
+
const settled = () => options.holdChore
|
|
11874
|
+
? retryDelays.filter((delay) => delay === REALTIME_ADMISSION_RETRY_MS).length >= 2
|
|
11875
|
+
: stderr.join("").includes("watch pass failed");
|
|
11876
|
+
const deadline = Date.now() + 3_000;
|
|
11877
|
+
while (!settled() && Date.now() < deadline) {
|
|
11878
|
+
tickIfAsleep(sleep);
|
|
11879
|
+
await flushLoopMicrotasks(20);
|
|
11880
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
11881
|
+
}
|
|
11882
|
+
if (options.holdChore) {
|
|
11883
|
+
// The refused pull's paths were restored: once the chore lets go,
|
|
11884
|
+
// the scheduled retry pulls the same path.
|
|
11885
|
+
expect(scopedPulls).toEqual([]);
|
|
11886
|
+
releaseChore();
|
|
11887
|
+
await flushLoopMicrotasks(40);
|
|
11888
|
+
for (const callback of retryCallbacks.splice(0))
|
|
11889
|
+
callback();
|
|
11890
|
+
const pullDeadline = Date.now() + 3_000;
|
|
11891
|
+
while (scopedPulls.length === 0 && Date.now() < pullDeadline) {
|
|
11892
|
+
tickIfAsleep(sleep);
|
|
11893
|
+
await flushLoopMicrotasks(20);
|
|
11894
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
11895
|
+
}
|
|
11896
|
+
}
|
|
11897
|
+
return { stderr: stderr.join(""), retryDelays, scopedPulls };
|
|
11898
|
+
}
|
|
11899
|
+
finally {
|
|
11900
|
+
releaseChore();
|
|
11901
|
+
triggerShutdown();
|
|
11902
|
+
await loop;
|
|
11903
|
+
spy.mockRestore();
|
|
11904
|
+
}
|
|
11905
|
+
}
|
|
11906
|
+
it("defers a refused watcher pull: no pass-failed breadcrumb, paths restored, retry scheduled", async () => {
|
|
11907
|
+
const { stderr, retryDelays, scopedPulls } = await runMainLoopPull({
|
|
11908
|
+
holdChore: true,
|
|
11909
|
+
pullExit: 0,
|
|
11910
|
+
});
|
|
11911
|
+
// One retry for the deferred push and one for the deferred pull.
|
|
11912
|
+
expect(retryDelays.filter((delay) => delay === REALTIME_ADMISSION_RETRY_MS).length)
|
|
11913
|
+
.toBeGreaterThanOrEqual(2);
|
|
11914
|
+
expect(stderr).not.toContain("watch pass failed");
|
|
11915
|
+
expect(stderr).not.toContain("targeted event pass failed");
|
|
11916
|
+
expect(scopedPulls.some((argv) => argv.join(" ").includes("note.md"))).toBe(true);
|
|
11917
|
+
});
|
|
11918
|
+
it("still reports a real watcher pull failure as exit=1", async () => {
|
|
11919
|
+
const { stderr, retryDelays } = await runMainLoopPull({
|
|
11920
|
+
holdChore: false,
|
|
11921
|
+
pullExit: 1,
|
|
11922
|
+
});
|
|
11923
|
+
expect(stderr).toContain("watch pass failed (exit=1)");
|
|
11924
|
+
expect(retryDelays).not.toContain(REALTIME_ADMISSION_RETRY_MS);
|
|
11925
|
+
});
|
|
11926
|
+
it("hands the receiver the typed admission timeout so it keeps the lease instead of bisecting", async () => {
|
|
11927
|
+
const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-admission-receiver-"));
|
|
11928
|
+
fs.mkdirSync(path.join(hqRoot, "companies", "indigo"), { recursive: true });
|
|
11929
|
+
let syncBatch;
|
|
11930
|
+
let releaseChore = () => { };
|
|
11931
|
+
const choreHold = new Promise((resolve) => {
|
|
11932
|
+
releaseChore = resolve;
|
|
11933
|
+
});
|
|
11934
|
+
const scopedPulls = [];
|
|
11935
|
+
const runPass = choreRunPass(choreHold, (argv) => scopedPulls.push(argv));
|
|
11936
|
+
const watcher = makeBatchWatcherStub();
|
|
11937
|
+
const sleep = makeSteppableSleep();
|
|
11938
|
+
let triggerShutdown = () => { };
|
|
11939
|
+
const loop = runRunnerWithLoop([
|
|
11940
|
+
"--companies", "--watch", "--event-push", "--direction", "both",
|
|
11941
|
+
"--hq-root", hqRoot, "--poll-remote-ms", "60000",
|
|
11942
|
+
], {
|
|
11943
|
+
runPass: runPass,
|
|
11944
|
+
runPassSupportsCooperativeCheckpoint: true,
|
|
11945
|
+
clock: new FakeClock(),
|
|
11946
|
+
createWatcher: () => watcher,
|
|
11947
|
+
createReceiver: ({ syncBatchFn }) => {
|
|
11948
|
+
syncBatch = syncBatchFn;
|
|
11949
|
+
return { connected: true, start: async () => { }, dispose: async () => { } };
|
|
11950
|
+
},
|
|
11951
|
+
sleep: sleep.sleep,
|
|
11952
|
+
onShutdownSignal: (handler) => {
|
|
11953
|
+
triggerShutdown = handler;
|
|
11954
|
+
return () => { };
|
|
11955
|
+
},
|
|
11956
|
+
});
|
|
11957
|
+
try {
|
|
11958
|
+
await flushLoopMicrotasks(40);
|
|
11959
|
+
expect(syncBatch).toBeDefined();
|
|
11960
|
+
const event = {
|
|
11961
|
+
relativePath: "companies/indigo/docs/peer.md",
|
|
11962
|
+
kind: "upsert",
|
|
11963
|
+
contentHash: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
11964
|
+
mtime: "2026-10-03T00:00:00.000Z",
|
|
11965
|
+
originDeviceId: "peer-device",
|
|
11966
|
+
sequenceNumber: 1,
|
|
11967
|
+
eventTimestamp: "2026-10-03T00:00:00.000Z",
|
|
11968
|
+
};
|
|
11969
|
+
const error = await syncBatch({
|
|
11970
|
+
events: [event],
|
|
11971
|
+
signal: new AbortController().signal,
|
|
11972
|
+
}).then(() => null, (rejection) => rejection);
|
|
11973
|
+
expect(error).toBeInstanceOf(RealtimeAdmissionTimeoutError);
|
|
11974
|
+
expect(error.retryable).toBe(true);
|
|
11975
|
+
expect(scopedPulls).toEqual([]);
|
|
11976
|
+
}
|
|
11977
|
+
finally {
|
|
11978
|
+
releaseChore();
|
|
11979
|
+
triggerShutdown();
|
|
11980
|
+
await loop;
|
|
11981
|
+
}
|
|
11982
|
+
});
|
|
11983
|
+
});
|
|
11379
11984
|
describe("CooperativePassScheduler cpu cap", () => {
|
|
11380
11985
|
afterEach(() => {
|
|
11381
11986
|
setRealtimeAdmissionDeadlineMsForTesting(undefined);
|