@indigoai-us/hq-cloud 6.15.80 → 6.15.81
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-company.d.ts.map +1 -1
- package/dist/bin/sync-runner-company.js +15 -0
- package/dist/bin/sync-runner-company.js.map +1 -1
- package/dist/bin/sync-runner-company.test.js +65 -0
- package/dist/bin/sync-runner-company.test.js.map +1 -1
- package/dist/bin/sync-runner-events.test.js +20 -0
- package/dist/bin/sync-runner-events.test.js.map +1 -1
- package/dist/bin/sync-runner-watch-loop.d.ts +7 -0
- package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-loop.js +308 -20
- package/dist/bin/sync-runner-watch-loop.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +5 -0
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +144 -1
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +29 -5
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +131 -24
- package/dist/cli/share.test.js.map +1 -1
- package/dist/cli/sync.d.ts +23 -0
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +72 -7
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +199 -0
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/cli/watch-event-push-conflict.test.js +22 -11
- package/dist/cli/watch-event-push-conflict.test.js.map +1 -1
- package/dist/s3.d.ts +62 -0
- package/dist/s3.d.ts.map +1 -1
- package/dist/s3.js +91 -1
- package/dist/s3.js.map +1 -1
- package/dist/s3.rename-retry.test.d.ts +2 -0
- package/dist/s3.rename-retry.test.d.ts.map +1 -0
- package/dist/s3.rename-retry.test.js +153 -0
- package/dist/s3.rename-retry.test.js.map +1 -0
- package/dist/sync/feature-flags.d.ts +9 -0
- package/dist/sync/feature-flags.d.ts.map +1 -1
- package/dist/sync/feature-flags.js +19 -0
- package/dist/sync/feature-flags.js.map +1 -1
- package/dist/sync/feature-flags.test.js +11 -1
- package/dist/sync/feature-flags.test.js.map +1 -1
- package/dist/sync/parallel-sync-coordinator.d.ts.map +1 -1
- package/dist/sync/parallel-sync-coordinator.js +0 -3
- package/dist/sync/parallel-sync-coordinator.js.map +1 -1
- package/dist/sync/parallel-sync-coordinator.test.js +14 -0
- package/dist/sync/parallel-sync-coordinator.test.js.map +1 -1
- package/package.json +1 -1
|
@@ -15,6 +15,9 @@ import { TreeWatcher, resolveEventDebounceConfig, systemClock, } from "../watche
|
|
|
15
15
|
import { clearLocalDeleteIntent, getStateDir, markLocalDeleteIntent, PERSONAL_VAULT_JOURNAL_SLUG, openJournalStoreSession, openJournalStoreSessionAsync, listJournals, readJournal, writeJournal, } from "../journal.js";
|
|
16
16
|
import { NoopPushReceiver, } from "../sync/push-receiver.js";
|
|
17
17
|
import { resolveEventSync, startEventSync as defaultStartEventSync, } from "../sync/event-sync.js";
|
|
18
|
+
import { isParallelCoordinatorEnabled } from "../sync/feature-flags.js";
|
|
19
|
+
import { DefaultParallelSyncCoordinator } from "../sync/parallel-sync-coordinator.js";
|
|
20
|
+
import { InlineSyncWorkerAdapter, } from "../sync/sync-work.js";
|
|
18
21
|
import { buildRoutePullArgv, buildScopedPushArgv, buildScopedDrainPullArgv, buildTargetedPullArgv, isWatchRouteSelected, routeChangeToTarget, routeKey, } from "./sync-runner-watch-routes.js";
|
|
19
22
|
import { resolveSkipCompanies } from "./sync-runner-planning.js";
|
|
20
23
|
const FULL_RECONCILE_EVERY_TICKS = 10;
|
|
@@ -62,6 +65,16 @@ export const DEFAULT_FAST_LANE_MAX_BYTES = 4 * 1024 * 1024;
|
|
|
62
65
|
* leaving session logs and other transfer-heavy files in the slow lane.
|
|
63
66
|
*/
|
|
64
67
|
export const DEFAULT_FAST_LANE_MAX_FILE_BYTES = 256 * 1024;
|
|
68
|
+
/**
|
|
69
|
+
* A coordinator submission must not contain both a directory and one of its
|
|
70
|
+
* descendants: they are one mutation scope, and the lease manager correctly
|
|
71
|
+
* rejects internally-conflicting submissions. Prefer the widest scope so an
|
|
72
|
+
* unlinkDir following child unlink events remains admissible.
|
|
73
|
+
*/
|
|
74
|
+
export function normalizeCoordinatorPaths(paths) {
|
|
75
|
+
const ordered = [...new Set(paths)].sort((left, right) => left.length - right.length || left.localeCompare(right));
|
|
76
|
+
return ordered.filter((candidate, index) => !ordered.slice(0, index).some((ancestor) => candidate.startsWith(`${ancestor}/`)));
|
|
77
|
+
}
|
|
65
78
|
function positiveEnvInt(name, fallback) {
|
|
66
79
|
const value = Number.parseInt(process.env[name] ?? "", 10);
|
|
67
80
|
return Number.isFinite(value) && value > 0 ? value : fallback;
|
|
@@ -443,6 +456,10 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
443
456
|
// Fast work may bypass the lock only while this process *actually owns* it;
|
|
444
457
|
// otherwise it must participate in normal lock acquisition.
|
|
445
458
|
let processOwnsOperationLock = false;
|
|
459
|
+
// Compatibility chores still mutate whole routes/vaults. While one is
|
|
460
|
+
// executing, a coordinator realtime task must not take the normal fast-path
|
|
461
|
+
// lock carve-out: it has no bounded chore lease to prove independence.
|
|
462
|
+
let coordinatorWholeMutationChores = 0;
|
|
446
463
|
let operationLockUsers = 0;
|
|
447
464
|
let resolveNoOperationLockUsers = null;
|
|
448
465
|
const acquireOperationLockUser = () => {
|
|
@@ -469,7 +486,9 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
469
486
|
// its keyed journal delta and per-object mutation are independent from the
|
|
470
487
|
// slow pass's bulk paths, and the slow pass already owns the root lock.
|
|
471
488
|
// Never bypass the lock when no slow pass is active.
|
|
472
|
-
if (lane === "fast" &&
|
|
489
|
+
if (lane === "fast" &&
|
|
490
|
+
processOwnsOperationLock &&
|
|
491
|
+
coordinatorWholeMutationChores === 0) {
|
|
473
492
|
acquireOperationLockUser();
|
|
474
493
|
try {
|
|
475
494
|
prepare?.();
|
|
@@ -731,6 +750,109 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
731
750
|
});
|
|
732
751
|
};
|
|
733
752
|
const runGuarded = (passArgvForRun, prepare, lane = "slow") => runGuardedTask(() => runPassWithLock(passArgvForRun, prepare, lane), lane);
|
|
753
|
+
/*
|
|
754
|
+
* The bridge is intentionally opt-in. The legacy guarded pass arrays stay
|
|
755
|
+
* exactly as they are until an outpost enables HQ_SYNC_PARALLEL_COORDINATOR.
|
|
756
|
+
* Once enabled, the coordinator owns admission and path fencing; the small
|
|
757
|
+
* inline adapters are only the compatibility execution seam while the
|
|
758
|
+
* runner's real slice executors are being moved into child workers.
|
|
759
|
+
*/
|
|
760
|
+
const parallelCoordinatorEnabled = isParallelCoordinatorEnabled(process.env.HQ_SYNC_PARALLEL_COORDINATOR);
|
|
761
|
+
const coordinatorExecutions = new WeakMap();
|
|
762
|
+
const coordinatorWorker = async (slice) => {
|
|
763
|
+
const execution = coordinatorExecutions.get(slice.work);
|
|
764
|
+
if (!execution) {
|
|
765
|
+
return {
|
|
766
|
+
paths: slice.paths.map((item) => ({
|
|
767
|
+
path: item.canonicalPath,
|
|
768
|
+
epoch: item.epoch,
|
|
769
|
+
status: "retryable",
|
|
770
|
+
reason: "coordinator execution was not registered",
|
|
771
|
+
})),
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
if (slice.work.class === "chore")
|
|
775
|
+
coordinatorWholeMutationChores += 1;
|
|
776
|
+
try {
|
|
777
|
+
execution.result = await execution.run(slice);
|
|
778
|
+
}
|
|
779
|
+
finally {
|
|
780
|
+
if (slice.work.class === "chore")
|
|
781
|
+
coordinatorWholeMutationChores -= 1;
|
|
782
|
+
}
|
|
783
|
+
const exitCode = passExitCode(execution.result);
|
|
784
|
+
return {
|
|
785
|
+
paths: slice.paths.map((item) => ({
|
|
786
|
+
path: item.canonicalPath,
|
|
787
|
+
epoch: item.epoch,
|
|
788
|
+
status: exitCode === 0 ? "durable" : "retryable",
|
|
789
|
+
...(exitCode === 0
|
|
790
|
+
? {}
|
|
791
|
+
: { reason: `sync pass exited ${exitCode}` }),
|
|
792
|
+
})),
|
|
793
|
+
};
|
|
794
|
+
};
|
|
795
|
+
const coordinator = parallelCoordinatorEnabled
|
|
796
|
+
? new DefaultParallelSyncCoordinator({
|
|
797
|
+
realtimeWorker: new InlineSyncWorkerAdapter(coordinatorWorker),
|
|
798
|
+
choreWorker: new InlineSyncWorkerAdapter(coordinatorWorker),
|
|
799
|
+
identifyPath: (canonicalPath) => {
|
|
800
|
+
const route = routeChangeToTarget(canonicalPath);
|
|
801
|
+
if (!route)
|
|
802
|
+
throw new Error(`coordinator cannot route ${canonicalPath}`);
|
|
803
|
+
return route.kind === "company"
|
|
804
|
+
? { targetUid: `company:${route.slug}`, areaId: `company:${route.slug}` }
|
|
805
|
+
: { targetUid: "personal", areaId: "personal" };
|
|
806
|
+
},
|
|
807
|
+
// Compatibility chores still use whole-vault/whole-route executors,
|
|
808
|
+
// which cannot yet name sound bounded mutation paths. Their execution
|
|
809
|
+
// therefore remains behind the runner's guarded operation queue until
|
|
810
|
+
// those executors can provide concrete scopes.
|
|
811
|
+
planChorePaths: () => [],
|
|
812
|
+
})
|
|
813
|
+
: null;
|
|
814
|
+
const runCoordinatorWork = async (work, task) => {
|
|
815
|
+
if (!coordinator) {
|
|
816
|
+
return {
|
|
817
|
+
result: await task({
|
|
818
|
+
id: "legacy-direct",
|
|
819
|
+
work,
|
|
820
|
+
paths: [],
|
|
821
|
+
signal: new AbortController().signal,
|
|
822
|
+
}),
|
|
823
|
+
accepted: true,
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
const execution = { run: task };
|
|
827
|
+
coordinatorExecutions.set(work, execution);
|
|
828
|
+
try {
|
|
829
|
+
const outcome = await coordinator.submit(work);
|
|
830
|
+
const accepted = outcome.paths.every((path) => path.status === "durable");
|
|
831
|
+
return {
|
|
832
|
+
result: execution.result ?? 1,
|
|
833
|
+
accepted,
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
finally {
|
|
837
|
+
coordinatorExecutions.delete(work);
|
|
838
|
+
}
|
|
839
|
+
};
|
|
840
|
+
let nextCoordinatorCausality = 0;
|
|
841
|
+
const realtimeWork = (source, direction, paths) => ({
|
|
842
|
+
class: "realtime",
|
|
843
|
+
source,
|
|
844
|
+
direction,
|
|
845
|
+
paths: normalizeCoordinatorPaths(paths),
|
|
846
|
+
causality: `${source}-${++nextCoordinatorCausality}`,
|
|
847
|
+
});
|
|
848
|
+
const choreWork = (kind) => ({ class: "chore", kind, areas: [] });
|
|
849
|
+
const runRealtimePass = (source, direction, paths, task) => runCoordinatorWork(realtimeWork(source, direction, paths), task);
|
|
850
|
+
const enqueueChore = (kind, task, onSettled) => {
|
|
851
|
+
void runCoordinatorWork(choreWork(kind), task).then(onSettled).catch((error) => {
|
|
852
|
+
process.stderr.write(`hq-sync-runner: coordinator ${kind} chore failed: ${describeError(error)}\n`);
|
|
853
|
+
onSettled({ result: 1, accepted: false });
|
|
854
|
+
});
|
|
855
|
+
};
|
|
734
856
|
let watcher = null;
|
|
735
857
|
let eventPushSurfacesActivated = false;
|
|
736
858
|
let watcherUnavailableWarned = false;
|
|
@@ -1417,6 +1539,10 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1417
1539
|
const skipCompanies = grouped ? resolveSkipCompanies() : null;
|
|
1418
1540
|
const isPausedRoute = (route) => route.kind === "company" &&
|
|
1419
1541
|
(skipCompanies?.has(route.slug.toLowerCase()) ?? false);
|
|
1542
|
+
const changesForAdmittedPaths = (group, admittedPaths) => new Map([...group.changes.entries()].filter(([absolutePath]) => {
|
|
1543
|
+
const relativePath = group.paths.get(absolutePath);
|
|
1544
|
+
return relativePath !== undefined && admittedPaths.some((scope) => relativePath === scope || relativePath.startsWith(`${scope}/`));
|
|
1545
|
+
}));
|
|
1420
1546
|
if (watcherDrainRunsPush && grouped) {
|
|
1421
1547
|
for (const group of grouped.values()) {
|
|
1422
1548
|
// Paused workspaces: drop the scoped push without uploading or
|
|
@@ -1424,15 +1550,24 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1424
1550
|
if (isPausedRoute(group.route)) {
|
|
1425
1551
|
continue;
|
|
1426
1552
|
}
|
|
1427
|
-
const scopedArgv = buildScopedPushArgv(group.route, group.relPaths, passArgv, group.deleteScopeRoots);
|
|
1428
1553
|
try {
|
|
1429
1554
|
const lane = isFastLocalBatch(group.paths.keys(), group.changes)
|
|
1430
1555
|
? "fast"
|
|
1431
1556
|
: "slow";
|
|
1432
|
-
const
|
|
1433
|
-
? (
|
|
1434
|
-
|
|
1435
|
-
|
|
1557
|
+
const execution = parallelCoordinatorEnabled
|
|
1558
|
+
? await runRealtimePass("watcher", "push", group.relPaths, async (slice) => {
|
|
1559
|
+
const admittedPaths = slice.paths.map((path) => path.canonicalPath);
|
|
1560
|
+
const admittedChanges = changesForAdmittedPaths(group, admittedPaths);
|
|
1561
|
+
const scopedArgv = buildScopedPushArgv(group.route, admittedPaths, passArgv, group.deleteScopeRoots);
|
|
1562
|
+
return runGuarded(scopedArgv, admittedChanges.size > 0
|
|
1563
|
+
? () => prepareWatcherChanges(admittedChanges)
|
|
1564
|
+
: undefined, lane);
|
|
1565
|
+
})
|
|
1566
|
+
: { result: await runGuarded(buildScopedPushArgv(group.route, group.relPaths, passArgv, group.deleteScopeRoots), group.changes.size > 0
|
|
1567
|
+
? () => prepareWatcherChanges(group.changes)
|
|
1568
|
+
: undefined, lane), accepted: true };
|
|
1569
|
+
const result = execution.result;
|
|
1570
|
+
if (execution.accepted && passExitCode(result) === 0) {
|
|
1436
1571
|
const refusedPaths = new Set(typeof result === "number"
|
|
1437
1572
|
? []
|
|
1438
1573
|
: (result.result?.pushPathResults ?? [])
|
|
@@ -1492,7 +1627,10 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1492
1627
|
continue;
|
|
1493
1628
|
ranPersonalDrain = true;
|
|
1494
1629
|
}
|
|
1495
|
-
const
|
|
1630
|
+
const execution = parallelCoordinatorEnabled
|
|
1631
|
+
? await runRealtimePass("watcher", "pull", group.relPaths, (slice) => runGuarded(buildTargetedPullArgv(group.route, passArgv, slice.paths.map((path) => path.canonicalPath)), undefined, isFastLocalBatch(group.paths.keys(), group.changes) ? "fast" : "slow"))
|
|
1632
|
+
: { result: await runGuarded(pullArgv, undefined, isFastLocalBatch(group.paths.keys(), group.changes) ? "fast" : "slow"), accepted: true };
|
|
1633
|
+
const exit = execution.accepted ? passExitCode(execution.result) : 1;
|
|
1496
1634
|
if (exit !== 0 && worstExit === 0)
|
|
1497
1635
|
worstExit = exit;
|
|
1498
1636
|
}
|
|
@@ -1527,6 +1665,30 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1527
1665
|
for (const hint of batch.droppedRouteHints ?? [])
|
|
1528
1666
|
addRoute(hint);
|
|
1529
1667
|
const skipCompanies = resolveSkipCompanies();
|
|
1668
|
+
if (parallelCoordinatorEnabled) {
|
|
1669
|
+
for (const route of routes.values()) {
|
|
1670
|
+
if (route.kind === "company" &&
|
|
1671
|
+
skipCompanies.has(route.slug.toLowerCase())) {
|
|
1672
|
+
continue;
|
|
1673
|
+
}
|
|
1674
|
+
enqueueChore("backlog", async () => {
|
|
1675
|
+
if (watcherDrainRunsPush) {
|
|
1676
|
+
const push = await runGuarded(buildScopedPushArgv(route, [], passArgv));
|
|
1677
|
+
if (passExitCode(push) !== 0)
|
|
1678
|
+
return push;
|
|
1679
|
+
}
|
|
1680
|
+
return drainRunsPull
|
|
1681
|
+
? runGuarded(buildRoutePullArgv(route, passArgv))
|
|
1682
|
+
: 0;
|
|
1683
|
+
}, (execution) => {
|
|
1684
|
+
if (!execution.accepted || passExitCode(execution.result) !== 0) {
|
|
1685
|
+
process.stderr.write(`hq-sync-runner: backlog chore for ${routeKey(route)} failed; will retry on cadence\n`);
|
|
1686
|
+
interruptWait();
|
|
1687
|
+
}
|
|
1688
|
+
});
|
|
1689
|
+
}
|
|
1690
|
+
return 0;
|
|
1691
|
+
}
|
|
1530
1692
|
let worstExit = 0;
|
|
1531
1693
|
for (const route of routes.values()) {
|
|
1532
1694
|
// Paused workspaces sync in NO direction while Off.
|
|
@@ -1647,6 +1809,29 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1647
1809
|
const lanes = selectedBatch && !selectedBatch.overflowed
|
|
1648
1810
|
? partitionLocalBatch(selectedBatch)
|
|
1649
1811
|
: null;
|
|
1812
|
+
if (parallelCoordinatorEnabled && lanes) {
|
|
1813
|
+
// Preserve the ingress partition/debounce, but both partitions now
|
|
1814
|
+
// submit semantic realtime work. In particular a fresh large file
|
|
1815
|
+
// never waits for a whole-backlog chore merely because it exceeded
|
|
1816
|
+
// the old fast byte cap.
|
|
1817
|
+
for (const lane of [lanes.fast, lanes.slow]) {
|
|
1818
|
+
if (!lane)
|
|
1819
|
+
continue;
|
|
1820
|
+
const generation = ++nextWatcherBatchGeneration;
|
|
1821
|
+
void runScopedDrain(lane, generation, true).then((exit) => {
|
|
1822
|
+
if (exit === 0)
|
|
1823
|
+
return;
|
|
1824
|
+
restorePendingWatcherPaths(lane.paths, generation);
|
|
1825
|
+
interruptWait();
|
|
1826
|
+
}).catch((err) => {
|
|
1827
|
+
restorePendingWatcherPaths(lane.paths, generation);
|
|
1828
|
+
process.stderr.write(`watch coordinator scoped push failed, will retry next tick: ${describeError(err)}\n`);
|
|
1829
|
+
interruptWait();
|
|
1830
|
+
});
|
|
1831
|
+
}
|
|
1832
|
+
realtimeState.scheduler?.signal("watcher");
|
|
1833
|
+
return;
|
|
1834
|
+
}
|
|
1650
1835
|
if (lanes?.fast &&
|
|
1651
1836
|
activeSlowPass !== null) {
|
|
1652
1837
|
const generation = ++nextWatcherBatchGeneration;
|
|
@@ -1768,6 +1953,28 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1768
1953
|
// Apply the caps once across the whole delivery, then retain the residue
|
|
1769
1954
|
// in the normal lane. The receiver already coalesces to one latest event
|
|
1770
1955
|
// per path, so the partition cannot split revisions of one path.
|
|
1956
|
+
if (parallelCoordinatorEnabled) {
|
|
1957
|
+
// #423's coalescing/caps remain ingress protection only. A newly
|
|
1958
|
+
// delivered single large path is realtime work, not a slow-pass item;
|
|
1959
|
+
// byte size is handled by the transfer budget below the coordinator.
|
|
1960
|
+
const pathsByRoute = new Map();
|
|
1961
|
+
for (const event of events) {
|
|
1962
|
+
const route = routeChangeToTarget(event.relativePath);
|
|
1963
|
+
if (!route || !isWatchRouteSelected(route, watchRouteSelection))
|
|
1964
|
+
continue;
|
|
1965
|
+
const key = routeKey(route);
|
|
1966
|
+
const group = pathsByRoute.get(key) ?? { route, paths: [] };
|
|
1967
|
+
group.paths.push(event.relativePath);
|
|
1968
|
+
pathsByRoute.set(key, group);
|
|
1969
|
+
}
|
|
1970
|
+
for (const { route, paths } of pathsByRoute.values()) {
|
|
1971
|
+
const execution = await runRealtimePass("receiver", "pull", paths, (slice) => runGuarded(buildTargetedPullArgv(route, passArgv, slice.paths.map((path) => path.canonicalPath)), undefined, "slow"));
|
|
1972
|
+
if (!execution.accepted || passExitCode(execution.result) !== 0) {
|
|
1973
|
+
throw new Error(`targeted pull failed with exit code ${passExitCode(execution.result)}`);
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
return;
|
|
1977
|
+
}
|
|
1771
1978
|
const lanes = partitionByKnownSizes(events, receiverEventSize);
|
|
1772
1979
|
const drainLane = async (lane, laneEvents) => {
|
|
1773
1980
|
const pathsByRoute = new Map();
|
|
@@ -1800,6 +2007,8 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1800
2007
|
await drainLane("slow", lanes.slow);
|
|
1801
2008
|
};
|
|
1802
2009
|
receiverSyncBatchFn.partition = (events) => {
|
|
2010
|
+
if (parallelCoordinatorEnabled)
|
|
2011
|
+
return events.length > 0 ? [events] : [];
|
|
1803
2012
|
const lanes = partitionByKnownSizes(events, receiverEventSize);
|
|
1804
2013
|
return [lanes.fast, lanes.slow].filter((lane) => lane.length > 0);
|
|
1805
2014
|
};
|
|
@@ -1821,10 +2030,19 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1821
2030
|
runGuarded: async (drain) => {
|
|
1822
2031
|
let outcome;
|
|
1823
2032
|
try {
|
|
1824
|
-
|
|
1825
|
-
await
|
|
1826
|
-
|
|
1827
|
-
|
|
2033
|
+
if (parallelCoordinatorEnabled) {
|
|
2034
|
+
const execution = await runRealtimePass("wake", "pull", [], async () => runGuardedTask(() => runTaskWithLock(async () => {
|
|
2035
|
+
await drain();
|
|
2036
|
+
return 0;
|
|
2037
|
+
})));
|
|
2038
|
+
outcome = execution.accepted ? execution.result : 1;
|
|
2039
|
+
}
|
|
2040
|
+
else {
|
|
2041
|
+
outcome = await runGuardedTask(() => runTaskWithLock(async () => {
|
|
2042
|
+
await drain();
|
|
2043
|
+
return 0;
|
|
2044
|
+
}));
|
|
2045
|
+
}
|
|
1828
2046
|
}
|
|
1829
2047
|
catch (err) {
|
|
1830
2048
|
// Scheduler clients may have already been disposed when a queued
|
|
@@ -1899,11 +2117,16 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1899
2117
|
});
|
|
1900
2118
|
}
|
|
1901
2119
|
}
|
|
2120
|
+
let coordinatorShutdown = null;
|
|
1902
2121
|
const shutdown = () => {
|
|
1903
2122
|
if (stopped)
|
|
1904
|
-
return;
|
|
2123
|
+
return coordinatorShutdown ?? Promise.resolve();
|
|
1905
2124
|
stopped = true;
|
|
1906
2125
|
resolveStoppedQueue();
|
|
2126
|
+
// Reject queued coordinator work before tearing down ingress. Active
|
|
2127
|
+
// work receives its abort signal; receiver receipts remain unacknowledged
|
|
2128
|
+
// unless their durable pull has already completed.
|
|
2129
|
+
coordinatorShutdown = coordinator?.shutdown() ?? Promise.resolve();
|
|
1907
2130
|
try {
|
|
1908
2131
|
void receiver?.dispose();
|
|
1909
2132
|
}
|
|
@@ -1931,10 +2154,11 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1931
2154
|
/* ignore */
|
|
1932
2155
|
}
|
|
1933
2156
|
resolveStopped?.();
|
|
2157
|
+
return coordinatorShutdown;
|
|
1934
2158
|
};
|
|
1935
2159
|
const onShutdownSignal = deps.onShutdownSignal ??
|
|
1936
2160
|
((handler) => {
|
|
1937
|
-
const wrapped = () => handler();
|
|
2161
|
+
const wrapped = () => { void handler(); };
|
|
1938
2162
|
process.on("SIGTERM", wrapped);
|
|
1939
2163
|
process.on("SIGINT", wrapped);
|
|
1940
2164
|
return () => {
|
|
@@ -1948,8 +2172,13 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1948
2172
|
let nextDueAt = monotonicNow();
|
|
1949
2173
|
let pollTick = 0;
|
|
1950
2174
|
let lastCompletedFullReconcileAt = null;
|
|
2175
|
+
let fullChorePending = false;
|
|
2176
|
+
let fullChoreFollowupPending = false;
|
|
2177
|
+
let coordinatorAuthRequired = false;
|
|
1951
2178
|
try {
|
|
1952
2179
|
while (!stopped) {
|
|
2180
|
+
if (coordinatorAuthRequired)
|
|
2181
|
+
return 0;
|
|
1953
2182
|
pollTick++;
|
|
1954
2183
|
const pending = takePendingWatcherChange();
|
|
1955
2184
|
// The quarantine is scoped to the delete episode it refused, and only
|
|
@@ -2020,10 +2249,58 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
2020
2249
|
};
|
|
2021
2250
|
let result;
|
|
2022
2251
|
let ranFullReconcile = false;
|
|
2252
|
+
let enqueuedFullChore = false;
|
|
2023
2253
|
if (shouldRunFull) {
|
|
2024
|
-
ranFullReconcile = true;
|
|
2025
2254
|
emitDecision("full", reasons);
|
|
2026
|
-
|
|
2255
|
+
if (parallelCoordinatorEnabled) {
|
|
2256
|
+
// Cadence/full/backlog work is admitted to the reserved chore worker
|
|
2257
|
+
// and deliberately never awaited by the watch loop. The completion
|
|
2258
|
+
// callback, rather than submission, advances the full-reconcile
|
|
2259
|
+
// clock and turns on event surfaces.
|
|
2260
|
+
if (!fullChorePending) {
|
|
2261
|
+
fullChorePending = true;
|
|
2262
|
+
enqueueChore("full-reconcile", () => runGuarded(passArgv), (execution) => {
|
|
2263
|
+
fullChorePending = false;
|
|
2264
|
+
const exit = passExitCode(execution.result);
|
|
2265
|
+
if (exit === runtime.authRequiredPassExit) {
|
|
2266
|
+
coordinatorAuthRequired = true;
|
|
2267
|
+
interruptWait();
|
|
2268
|
+
return;
|
|
2269
|
+
}
|
|
2270
|
+
if (execution.accepted && exit === 0) {
|
|
2271
|
+
lastCompletedFullReconcileAt = monotonicNow();
|
|
2272
|
+
activateEventPushSurfaces();
|
|
2273
|
+
if (fullChoreFollowupPending) {
|
|
2274
|
+
fullChoreFollowupPending = false;
|
|
2275
|
+
interruptWait();
|
|
2276
|
+
}
|
|
2277
|
+
return;
|
|
2278
|
+
}
|
|
2279
|
+
process.stderr.write(`hq-sync-runner: full reconcile chore failed (exit=${exit}); will retry on cadence\n`);
|
|
2280
|
+
if (fullChoreFollowupPending) {
|
|
2281
|
+
fullChoreFollowupPending = false;
|
|
2282
|
+
interruptWait();
|
|
2283
|
+
}
|
|
2284
|
+
});
|
|
2285
|
+
}
|
|
2286
|
+
else {
|
|
2287
|
+
// A full pass can have already scanned this route. Preserve any
|
|
2288
|
+
// incoming evidence and make the completion schedule one more
|
|
2289
|
+
// reconcile rather than dropping the taken batch.
|
|
2290
|
+
if (pending.batch) {
|
|
2291
|
+
restorePendingWatcherPaths(pending.batch.paths, pending.generation);
|
|
2292
|
+
}
|
|
2293
|
+
if (pending.bare)
|
|
2294
|
+
pendingWatcherBareChange = true;
|
|
2295
|
+
fullChoreFollowupPending ||= pending.batch !== null || pending.bare;
|
|
2296
|
+
}
|
|
2297
|
+
enqueuedFullChore = true;
|
|
2298
|
+
result = 0;
|
|
2299
|
+
}
|
|
2300
|
+
else {
|
|
2301
|
+
ranFullReconcile = true;
|
|
2302
|
+
result = await runGuarded(passArgv);
|
|
2303
|
+
}
|
|
2027
2304
|
}
|
|
2028
2305
|
else if (pending.batch === null) {
|
|
2029
2306
|
// There is no watcher batch and no full-reconcile trigger. Do not
|
|
@@ -2104,9 +2381,9 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
2104
2381
|
else if (exitCode !== 0) {
|
|
2105
2382
|
return exitCode;
|
|
2106
2383
|
}
|
|
2107
|
-
const completedPostRepairPass = typeof result === "number"
|
|
2384
|
+
const completedPostRepairPass = !enqueuedFullChore && (typeof result === "number"
|
|
2108
2385
|
? exitCode === 0
|
|
2109
|
-
: result.result !== undefined;
|
|
2386
|
+
: result.result !== undefined);
|
|
2110
2387
|
if (completedPostRepairPass) {
|
|
2111
2388
|
// The production adapter returns RunnerPassResult only after fanout,
|
|
2112
2389
|
// which follows journal repair. Its numeric seam has the same
|
|
@@ -2118,9 +2395,20 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
2118
2395
|
if (Date.now() >= nextBackupPruneAt) {
|
|
2119
2396
|
// Advance before invoking: a failed/unavailable backstop cannot become
|
|
2120
2397
|
// an every-tick retry loop. It is diagnostic only and never restarts
|
|
2121
|
-
// the sync daemon.
|
|
2122
|
-
//
|
|
2398
|
+
// the sync daemon. Under coordinator mode it is a reserved-worker
|
|
2399
|
+
// chore, never a dependency of realtime admission.
|
|
2123
2400
|
nextBackupPruneAt = Date.now() + 24 * 60 * 60 * 1000;
|
|
2401
|
+
if (parallelCoordinatorEnabled) {
|
|
2402
|
+
enqueueChore("rotation", async () => runGuardedTask(() => runTaskWithLock(async () => {
|
|
2403
|
+
await runBackupPrune(hqRoot);
|
|
2404
|
+
return 0;
|
|
2405
|
+
})), (execution) => {
|
|
2406
|
+
if (!execution.accepted || passExitCode(execution.result) !== 0) {
|
|
2407
|
+
process.stderr.write("hq-sync-runner: backup retention chore failed; will retry tomorrow\n");
|
|
2408
|
+
}
|
|
2409
|
+
});
|
|
2410
|
+
continue;
|
|
2411
|
+
}
|
|
2124
2412
|
try {
|
|
2125
2413
|
await runGuardedTask(() => runTaskWithLock(async () => {
|
|
2126
2414
|
await runBackupPrune(hqRoot);
|
|
@@ -2160,7 +2448,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
2160
2448
|
return 0;
|
|
2161
2449
|
}
|
|
2162
2450
|
finally {
|
|
2163
|
-
shutdown();
|
|
2451
|
+
await shutdown();
|
|
2164
2452
|
detachSignal?.();
|
|
2165
2453
|
}
|
|
2166
2454
|
}
|