Skip to content

Commit 2e0a3d0

Browse files
committed
Fix ordered catch-up completion handoff
1 parent 464734c commit 2e0a3d0

3 files changed

Lines changed: 132 additions & 6 deletions

File tree

services/mediators/hyperswarm/src/hyperswarm-mediator.ts

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import {
4949
shouldDeferLegacySync,
5050
shouldSchedulePeriodicRepair,
5151
shouldStartConnectTimeNegentropy,
52+
shouldStartPostOrderedCatchupNegentropy,
5253
} from './negentropy/policy.js';
5354
import {
5455
addAggregateSample,
@@ -415,6 +416,7 @@ const addedPeers: Record<string, number> = {};
415416
const badPeers: Record<string, number> = {};
416417
const malformedPeers: Record<string, MalformedPeerState> = {};
417418
const peerSessions = new Map<string, PeerSyncSession>();
419+
const orderedCatchupPostImportPeers = new Set<string>();
418420
const syncStats: MediatorSyncStats = {
419421
modeSelectionsTotal: 0,
420422
modeSelectionsLegacy: 0,
@@ -2252,18 +2254,16 @@ async function sendOrderedCatchupPage(peerKey: string, msg: OrderedCatchupReqMes
22522254
syncStats.orderedCatchupOpsSent += push.data.length;
22532255
}
22542256

2255-
async function completeOrderedCatchup(peerKey: string, session: PeerSyncSession, reason: string): Promise<void> {
2257+
function completeOrderedCatchup(peerKey: string, session: PeerSyncSession, reason: string): void {
22562258
const conn = connectionInfo[peerKey];
22572259
if (!conn) {
22582260
return;
22592261
}
22602262

22612263
conn.orderedCatchupAttempted = true;
22622264
session.reconciliationComplete = true;
2263-
await waitForImportQueueIdle('ordered_catchup_complete');
22642265
closePeerSession(peerKey, reason);
2265-
markNegentropyAdapterDirty();
2266-
await startNegentropySessionForPeer(peerKey, 'ordered_catchup_complete', 'ordered_catchup_complete');
2266+
queueOrderedCatchupPostImport(peerKey, reason);
22672267
}
22682268

22692269
async function waitForImportQueueIdle(reason: string): Promise<void> {
@@ -2280,6 +2280,65 @@ async function waitForImportQueueIdle(reason: string): Promise<void> {
22802280
}
22812281
}
22822282

2283+
function queueOrderedCatchupPostImport(peerKey: string, reason: string): void {
2284+
if (orderedCatchupPostImportPeers.has(peerKey)) {
2285+
log.debug({ peer: shortName(peerKey), reason }, 'ordered catch-up post-import continuation already queued');
2286+
return;
2287+
}
2288+
2289+
orderedCatchupPostImportPeers.add(peerKey);
2290+
(async () => {
2291+
await waitForImportQueueIdle('ordered_catchup_complete');
2292+
await syncGatekeeperIndexToStore('ordered_catchup_complete');
2293+
markNegentropyAdapterDirty();
2294+
await maybeStartPostOrderedCatchupNegentropy(peerKey, reason);
2295+
})()
2296+
.catch(error => {
2297+
log.error({ error, peer: shortName(peerKey), reason }, 'ordered catch-up post-import continuation failed');
2298+
})
2299+
.finally(() => {
2300+
orderedCatchupPostImportPeers.delete(peerKey);
2301+
});
2302+
}
2303+
2304+
async function maybeStartPostOrderedCatchupNegentropy(peerKey: string, reason: string): Promise<void> {
2305+
const conn = connectionInfo[peerKey];
2306+
const shouldStart = shouldStartPostOrderedCatchupNegentropy({
2307+
syncMode: conn?.syncMode ?? 'unknown',
2308+
peerConnected: !!conn,
2309+
peerSupportsNegentropyTransport: conn ? supportsPeerNegentropyTransport(conn) : false,
2310+
hasActiveSession: peerSessions.has(peerKey),
2311+
importQueueLength: importQueue.length(),
2312+
importQueueRunning: importQueue.running(),
2313+
activeNegentropySessions: getActiveNegentropySessions(),
2314+
syncCompleted: conn?.negentropySynced ?? false,
2315+
});
2316+
2317+
if (shouldStart) {
2318+
await maybeStartPeerSync(peerKey, 'periodic');
2319+
} else {
2320+
log.debug(
2321+
{
2322+
peer: shortName(peerKey),
2323+
reason,
2324+
syncMode: conn?.syncMode ?? 'unknown',
2325+
peerConnected: !!conn,
2326+
peerSupportsNegentropyTransport: conn ? supportsPeerNegentropyTransport(conn) : false,
2327+
hasActiveSession: peerSessions.has(peerKey),
2328+
importQueueLength: importQueue.length(),
2329+
importQueueRunning: importQueue.running(),
2330+
activeNegentropySessions: getActiveNegentropySessions(),
2331+
syncCompleted: conn?.negentropySynced ?? false,
2332+
},
2333+
'post ordered catch-up negentropy handoff deferred'
2334+
);
2335+
}
2336+
2337+
if (getActiveNegentropySessions() === 0) {
2338+
await maybeSchedulePreferredSyncs(`ordered_catchup_post_import:${reason}`);
2339+
}
2340+
}
2341+
22832342
async function handleOrderedCatchupPush(peerKey: string, msg: OrderedCatchupPushMessage): Promise<void> {
22842343
const session = peerSessions.get(peerKey);
22852344
if (!session || session.mode !== 'ordered_catchup' || session.sessionId !== msg.sessionId) {
@@ -2320,7 +2379,7 @@ async function handleOrderedCatchupPush(peerKey: string, msg: OrderedCatchupPush
23202379
return;
23212380
}
23222381

2323-
await completeOrderedCatchup(peerKey, session, 'ordered_catchup_complete');
2382+
completeOrderedCatchup(peerKey, session, 'ordered_catchup_complete');
23242383
}
23252384

23262385
async function handleOrderedCatchupDone(peerKey: string, msg: OrderedCatchupDoneMessage): Promise<void> {
@@ -2330,7 +2389,7 @@ async function handleOrderedCatchupDone(peerKey: string, msg: OrderedCatchupDone
23302389
return;
23312390
}
23322391

2333-
await completeOrderedCatchup(peerKey, session, 'ordered_catchup_done');
2392+
completeOrderedCatchup(peerKey, session, 'ordered_catchup_done');
23342393
}
23352394

23362395
function sendNegMsg(peerKey: string, session: PeerSyncSession, frame: string | Uint8Array): boolean {

services/mediators/hyperswarm/src/negentropy/policy.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,17 @@ export interface RepairSchedulingInput {
1212
syncCompleted: boolean;
1313
}
1414

15+
export interface PostOrderedCatchupNegentropyInput {
16+
syncMode: SyncMode | 'unknown';
17+
peerConnected: boolean;
18+
peerSupportsNegentropyTransport: boolean;
19+
hasActiveSession: boolean;
20+
importQueueLength: number;
21+
importQueueRunning: number;
22+
activeNegentropySessions: number;
23+
syncCompleted: boolean;
24+
}
25+
1526
export interface LegacySyncPriorityInput {
1627
syncMode: SyncMode | 'unknown';
1728
legacySyncEnabled: boolean;
@@ -93,6 +104,38 @@ export function shouldSchedulePeriodicRepair(input: RepairSchedulingInput): bool
93104
return (input.nowMs - input.lastAttemptAtMs) >= input.repairIntervalMs;
94105
}
95106

107+
export function shouldStartPostOrderedCatchupNegentropy(input: PostOrderedCatchupNegentropyInput): boolean {
108+
if (!input.peerConnected) {
109+
return false;
110+
}
111+
112+
if (input.syncMode !== 'negentropy') {
113+
return false;
114+
}
115+
116+
if (!input.peerSupportsNegentropyTransport) {
117+
return false;
118+
}
119+
120+
if (input.hasActiveSession) {
121+
return false;
122+
}
123+
124+
if (input.importQueueLength > 0 || input.importQueueRunning > 0) {
125+
return false;
126+
}
127+
128+
if (input.activeNegentropySessions > 0) {
129+
return false;
130+
}
131+
132+
if (input.syncCompleted) {
133+
return false;
134+
}
135+
136+
return true;
137+
}
138+
96139
export function shouldDeferLegacySync(input: LegacySyncPriorityInput): boolean {
97140
if (!input.legacySyncEnabled) {
98141
return false;

tests/hyperswarm/negentropy-policy.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
shouldDeferLegacySync,
55
shouldSchedulePeriodicRepair,
66
shouldStartConnectTimeNegentropy,
7+
shouldStartPostOrderedCatchupNegentropy,
78
} from '../../services/mediators/hyperswarm/src/negentropy/policy.ts';
89

910
describe('negentropy sync policy', () => {
@@ -57,6 +58,29 @@ describe('negentropy sync policy', () => {
5758
})).toBe(false);
5859
});
5960

61+
it('starts post ordered catch-up negentropy only after import work drains', () => {
62+
const base = {
63+
syncMode: 'negentropy' as const,
64+
peerConnected: true,
65+
peerSupportsNegentropyTransport: true,
66+
hasActiveSession: false,
67+
importQueueLength: 0,
68+
importQueueRunning: 0,
69+
activeNegentropySessions: 0,
70+
syncCompleted: false,
71+
};
72+
73+
expect(shouldStartPostOrderedCatchupNegentropy(base)).toBe(true);
74+
expect(shouldStartPostOrderedCatchupNegentropy({ ...base, peerConnected: false })).toBe(false);
75+
expect(shouldStartPostOrderedCatchupNegentropy({ ...base, syncMode: 'legacy' })).toBe(false);
76+
expect(shouldStartPostOrderedCatchupNegentropy({ ...base, peerSupportsNegentropyTransport: false })).toBe(false);
77+
expect(shouldStartPostOrderedCatchupNegentropy({ ...base, hasActiveSession: true })).toBe(false);
78+
expect(shouldStartPostOrderedCatchupNegentropy({ ...base, importQueueLength: 1 })).toBe(false);
79+
expect(shouldStartPostOrderedCatchupNegentropy({ ...base, importQueueRunning: 1 })).toBe(false);
80+
expect(shouldStartPostOrderedCatchupNegentropy({ ...base, activeNegentropySessions: 1 })).toBe(false);
81+
expect(shouldStartPostOrderedCatchupNegentropy({ ...base, syncCompleted: true })).toBe(false);
82+
});
83+
6084
it('defers legacy while negentropy peers are pending, but allows fallback after timeout', () => {
6185
const base = {
6286
syncMode: 'legacy' as const,

0 commit comments

Comments
 (0)