Skip to content

Commit 9ef4290

Browse files
committed
fix(peer): use canonical connectivity state
Generated-by: Codex (gpt-5.6-sol)
1 parent e91d05d commit 9ef4290

5 files changed

Lines changed: 48 additions & 26 deletions

File tree

docs/architecture/peer-reachability-recovery-plan.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
- Status: implementation plan
2323
- Tracking issue: [#4554](https://github.com/apache/maka/issues/4554)
24-
- Baseline: `main` at `6c8e749d3df8b5a41e570538523fa772cc4d9333`
24+
- Baseline: `main` at `c86da40d76af21600b780def8dbda4aa403f86d0`
2525
- Delivery: four stacked pull requests
2626

2727
## Review charter
@@ -281,7 +281,17 @@ charter. Their evidence is adjudicated into this ledger:
281281
| R1-C4 | correctness | confirmed | Windows recovery source closure follows the new reachability owner and publisher instead of the removed Mesh owner path. |
282282
| R1-S1 | simplification | confirmed | The endpoint owner is the sole peer-client lifetime authority; Desktop no longer closes the same client a second time. |
283283
| R1-S2 | simplification | confirmed | Route resolution has one lifecycle: clients start unattached and Mesh explicitly attaches and detaches the resolver. Constructor/factory injection was removed. |
284-
| R1-S3 | simplification | confirmed | Mesh authority targets derive identity from the verified signed lease instead of carrying a second peer-id field. |
284+
| R1-S3 | simplification | superseded | A later security review proved that deriving long-lived authority identity from a replaceable locator lets a member rebind the authority. R2-C2 replaces this decision. |
285+
| R2-C1 | correctness | confirmed | Startup verifies all persisted signatures, prunes leases beyond the bounded recovery horizon, and retains roster membership; ordinary offline time can no longer brick Mesh initialization. |
286+
| R2-C2 | correctness | confirmed | The authority PeerId is now part of the authority-signed roster and immutable across roster revisions. Invitations and authenticated streams must match it, while reachability remains only a replaceable locator. |
287+
| R2-S1 | simplification | confirmed | Remembered Relay anchors are persisted regardless of whether public discovery is enabled; discovery selects new anchors, while anchor recovery is a separate concern. |
288+
| R2-S2 | simplification | confirmed | Relay-anchor persistence uses one coalescing watch slot instead of an unbounded snapshot queue. |
289+
| R2-S3 | simplification | confirmed | Mesh presence reads the native Swarm connectivity snapshot instead of maintaining a partial, stale `recentlyReached` cache. |
290+
| R2-S4 | simplification | confirmed | Both reachability and advertisement anti-entropy use the same Mesh-scoped `{ peerId, revision }` vector. |
291+
| R2-S5 | simplification | rejected | The one-shot post-finalization refresh suppression remains: it prevents guest credential finalization from becoming a second network acquisition, as required by the frozen collaboration invariant. |
292+
| R2-S6 | simplification | confirmed | Replica state stores the authority's signed reachability lease directly; stable authority identity comes only from the signed roster, so the single-field target wrapper was removed. |
293+
| R2-CI1 | CI | confirmed | Lower-stack Desktop fixtures retain the flat transport shape until PR4 introduces signed profile reachability, preserving each PR's review boundary. |
294+
| R2-CI2 | CI | confirmed | The Peer Mesh protocol imports the reachability wire decoder directly from its model module, keeping filesystem-backed publisher code out of the Linux preload bundle. |
285295

286296
Only findings that affect the merge bar and have a proportionate root fix enter the
287297
stack. Narrow constructed paths and low-value polish do not. A local fix triggers a

packages/runtime-host/src/__tests__/peer-listener.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ function peerWith(streams: RuntimeHostPeerNativeStream[]): RuntimeHostPeerClient
197197
throw new Error('not used');
198198
},
199199
verifyIdentity: () => false,
200+
isConnected: () => false,
200201
transitSnapshot: () => ({
201202
allowedPeerCount: 0,
202203
activeReservationCount: 0,

packages/runtime-host/src/__tests__/peer-mesh.test.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,7 @@ test('repairs an existing membership with a fresh invitation after every locator
409409
await authorityPeer.setCoordinationRelays([]);
410410
await authorityPeer.setRouteHints([]);
411411
await authority.reconcile();
412+
authorityPeer.setReachable(false);
412413
await member.reconcile();
413414
assert.equal(
414415
member.status()[0]?.memberRoutes.find(({ peerId }) => peerId === 'peer-a')?.state,
@@ -427,6 +428,7 @@ test('repairs an existing membership with a fresh invitation after every locator
427428
);
428429

429430
const recoveredRoute = '/memory/peer-a-recovered/p2p/peer-a';
431+
authorityPeer.setReachable(true);
430432
await authorityPeer.setRouteHints([recoveredRoute]);
431433
const repaired = await member.join(await authority.invite(meshId));
432434
assert.equal(member.status().length, 1);
@@ -1164,6 +1166,7 @@ class MemoryPeerClient implements PeerMeshTransport, PeerReachabilityPublisher {
11641166
#stallNextControl = false;
11651167
#responseDelayMs = 0;
11661168
#reachable = true;
1169+
readonly #connectedPeerIds = new Set<string>();
11671170
#routeHints: readonly string[];
11681171
#coordinationRelays: readonly string[];
11691172
#reachability: SignedPeerReachabilityLeaseV1 | undefined;
@@ -1282,6 +1285,10 @@ class MemoryPeerClient implements PeerMeshTransport, PeerReachabilityPublisher {
12821285

12831286
setReachable(reachable: boolean): void {
12841287
this.#reachable = reachable;
1288+
if (!reachable) {
1289+
this.#connectedPeerIds.clear();
1290+
for (const peer of this.peers.values()) peer.#connectedPeerIds.delete(this.peerId);
1291+
}
12851292
}
12861293

12871294
setResponseDelay(delayMs: number): void {
@@ -1350,6 +1357,10 @@ class MemoryPeerClient implements PeerMeshTransport, PeerReachabilityPublisher {
13501357
);
13511358
}
13521359

1360+
isConnected(peerId: string): boolean {
1361+
return this.#connectedPeerIds.has(peerId);
1362+
}
1363+
13531364
transitSnapshot() {
13541365
return {
13551366
allowedPeerCount: this.transitPolicy.allowedPeerIds.length,
@@ -1404,9 +1415,11 @@ class MemoryPeerClient implements PeerMeshTransport, PeerReachabilityPublisher {
14041415
}
14051416
signal?.throwIfAborted();
14061417
const remote = this.peers.get(input.peerId);
1407-
if (!remote || !remote.#reachable) {
1418+
if (!this.#reachable || !remote || !remote.#reachable) {
14081419
throw new Error('Peer is unavailable');
14091420
}
1421+
this.#connectedPeerIds.add(input.peerId);
1422+
remote.#connectedPeerIds.add(this.peerId);
14101423
const [localStream, remoteStream] = memoryStreamPair(this.peerId, input.peerId);
14111424
if (remote.#failNextResponse) {
14121425
remote.#failNextResponse = false;
@@ -1445,6 +1458,8 @@ class MemoryPeerClient implements PeerMeshTransport, PeerReachabilityPublisher {
14451458
close(): Promise<void> {
14461459
if (this.#closed) return Promise.resolve();
14471460
this.#closed = true;
1461+
this.#connectedPeerIds.clear();
1462+
for (const peer of this.peers.values()) peer.#connectedPeerIds.delete(this.peerId);
14481463
this.#reachabilityListeners.clear();
14491464
this.#meshServer?.stop();
14501465
return Promise.resolve();

packages/runtime-host/src/client/peer-client.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ export interface RuntimeHostPeerClient {
9090
}>;
9191
signIdentity(payload: Buffer): Promise<RuntimeHostPeerIdentityProof>;
9292
verifyIdentity(peerId: string, payload: Buffer, proof: RuntimeHostPeerIdentityProof): boolean;
93+
isConnected(peerId: string): boolean;
9394
transitSnapshot(): RuntimeHostPeerTransitSnapshot;
9495
configureTransit(input: {
9596
readonly allowedPeerIds: readonly string[];
@@ -253,6 +254,10 @@ class RuntimeHostPeerClientImpl implements RuntimeHostPeerClient {
253254
});
254255
}
255256

257+
isConnected(peerId: string): boolean {
258+
return this.#endpoint?.connectivitySnapshot.connectedPeerIds.includes(peerId) ?? false;
259+
}
260+
256261
transitSnapshot(): RuntimeHostPeerTransitSnapshot {
257262
return Object.freeze({ ...this.#requireEndpoint().transitSnapshot });
258263
}

packages/runtime-host/src/peer-mesh/node.ts

Lines changed: 14 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ export interface PeerMeshTransport {
208208
}>;
209209
signIdentity(payload: Buffer): Promise<RuntimeHostPeerIdentityProof>;
210210
verifyIdentity(peerId: string, payload: Buffer, proof: RuntimeHostPeerIdentityProof): boolean;
211+
isConnected(peerId: string): boolean;
211212
transitSnapshot(): RuntimeHostPeerTransitSnapshot;
212213
configureTransit(input: {
213214
readonly allowedPeerIds: readonly string[];
@@ -269,7 +270,6 @@ class PeerMeshNodeImpl implements PeerMeshNode {
269270
#unsubscribeReachability: (() => void) | undefined;
270271
#reconcileGeneration = 0;
271272
readonly #reconcileWaiters = new Set<() => void>();
272-
readonly #recentlyReached = new Set<string>();
273273
readonly #reachabilityReceipts = new Map<string, PeerReachabilityLeaseReceipt>();
274274
readonly #completedRecoverySweeps = new Set<string>();
275275
readonly #routeResolutionListeners = new Map<string, Set<() => void>>();
@@ -408,7 +408,7 @@ class PeerMeshNodeImpl implements PeerMeshNode {
408408
stored.reachability,
409409
stored.advertisements,
410410
this.#now(),
411-
this.#recentlyReached,
411+
(peerId) => this.#peer.isConnected(peerId),
412412
(peerId) => this.resolveRoutes(peerId),
413413
(signed) => this.#isReachabilityCurrent(signed),
414414
);
@@ -430,7 +430,7 @@ class PeerMeshNodeImpl implements PeerMeshNode {
430430
stored.reachability,
431431
stored.advertisements,
432432
this.#now(),
433-
this.#recentlyReached,
433+
(peerId) => this.#peer.isConnected(peerId),
434434
(peerId) => this.resolveRoutes(peerId),
435435
(signed) => this.#isReachabilityCurrent(signed),
436436
),
@@ -481,7 +481,7 @@ class PeerMeshNodeImpl implements PeerMeshNode {
481481
stored.reachability,
482482
stored.advertisements,
483483
now,
484-
this.#recentlyReached,
484+
(peerId) => this.#peer.isConnected(peerId),
485485
(peerId) => this.resolveRoutes(peerId),
486486
(signed) => this.#isReachabilityCurrent(signed),
487487
);
@@ -784,7 +784,7 @@ class PeerMeshNodeImpl implements PeerMeshNode {
784784
stored.reachability,
785785
stored.advertisements,
786786
this.#now(),
787-
this.#recentlyReached,
787+
(peerId) => this.#peer.isConnected(peerId),
788788
(peerId) => this.resolveRoutes(peerId),
789789
(signed) => this.#isReachabilityCurrent(signed),
790790
);
@@ -1199,7 +1199,7 @@ class PeerMeshNodeImpl implements PeerMeshNode {
11991199
state.role === 'replica'
12001200
? currentAuthorityTarget(state, stored.reachability, now)
12011201
: undefined;
1202-
if (authority && authority.reachability.lease.peerId !== excludedPeerId) {
1202+
if (authority && authority.lease.peerId !== excludedPeerId) {
12031203
pending.push({
12041204
kind: 'membership',
12051205
meshId,
@@ -1256,9 +1256,6 @@ class PeerMeshNodeImpl implements PeerMeshNode {
12561256
}
12571257
} catch (error) {
12581258
if (lifetimeSignal.aborted) lifetimeSignal.throwIfAborted();
1259-
if (operation.kind === 'membership') {
1260-
this.#recentlyReached.delete(operation.target.lease.peerId);
1261-
}
12621259
if (operation.kind === 'join' || operation.desiredMembership === 'left') {
12631260
failures.push(error);
12641261
}
@@ -1304,7 +1301,6 @@ class PeerMeshNodeImpl implements PeerMeshNode {
13041301
throw new Error('Peer Mesh authority rejected the leave request');
13051302
}
13061303
await this.#applySync(meshId, response.roster, [], []);
1307-
this.#recentlyReached.add(target.lease.peerId);
13081304
} finally {
13091305
await stream.close().catch(() => undefined);
13101306
}
@@ -1370,7 +1366,6 @@ class PeerMeshNodeImpl implements PeerMeshNode {
13701366
response.reachability,
13711367
response.advertisements,
13721368
);
1373-
this.#recentlyReached.add(targetPeerId);
13741369
if (!response.more) return;
13751370
} finally {
13761371
await stream.close().catch(() => undefined);
@@ -1694,7 +1689,7 @@ class PeerMeshNodeImpl implements PeerMeshNode {
16941689
stored.reachability,
16951690
stored.advertisements,
16961691
this.#now(),
1697-
this.#recentlyReached,
1692+
(peerId) => this.#peer.isConnected(peerId),
16981693
(peerId) => this.resolveRoutes(peerId),
16991694
(signed) => this.#isReachabilityCurrent(signed),
17001695
);
@@ -1729,7 +1724,6 @@ class PeerMeshNodeImpl implements PeerMeshNode {
17291724
if (response.kind === 'roster-rejected') {
17301725
throw new Error('Peer Mesh roster announcement was rejected');
17311726
}
1732-
this.#recentlyReached.add(target.lease.peerId);
17331727
} finally {
17341728
await stream.close().catch(() => undefined);
17351729
}
@@ -1984,7 +1978,6 @@ class PeerMeshNodeImpl implements PeerMeshNode {
19841978
).filter(({ lease }) => lease.peerId !== remotePeerId),
19851979
);
19861980
}
1987-
this.#recentlyReached.add(remotePeerId);
19881981
}
19891982
return response;
19901983
}
@@ -2123,7 +2116,6 @@ class PeerMeshNodeImpl implements PeerMeshNode {
21232116
};
21242117
});
21252118
if (response.kind === 'sync-result') {
2126-
this.#recentlyReached.add(remotePeerId);
21272119
await this.#reconcileTransit();
21282120
}
21292121
return response;
@@ -2288,7 +2280,7 @@ function peerMeshStatus(
22882280
reachability: readonly SignedPeerReachabilityLeaseV1[],
22892281
advertisements: readonly SignedPeerMeshMemberAdvertisementV1[],
22902282
now: number,
2291-
recentlyReached: ReadonlySet<string>,
2283+
isConnected: (peerId: string) => boolean,
22922284
resolveRoutes: (peerId: string) => RuntimeHostPeerRouteResolution,
22932285
isCurrent: (signed: SignedPeerReachabilityLeaseV1) => boolean,
22942286
): PeerMeshStatus {
@@ -2321,18 +2313,17 @@ function peerMeshStatus(
23212313
const lease = signed?.lease;
23222314
const resolution = resolveRoutes(peerId);
23232315
const current = Boolean(signed && isCurrent(signed));
2324-
const memberState =
2325-
resolution.state === 'exhausted'
2316+
const memberState = isConnected(peerId)
2317+
? ('reachable' as const)
2318+
: resolution.state === 'exhausted'
23262319
? ('needs_repair' as const)
23272320
: resolution.state === 'recovering'
23282321
? signed
23292322
? ('reconnecting' as const)
23302323
: ('connecting' as const)
2331-
: current && recentlyReached.has(peerId)
2332-
? ('reachable' as const)
2333-
: current
2334-
? ('connecting' as const)
2335-
: ('reconnecting' as const);
2324+
: current
2325+
? ('connecting' as const)
2326+
: ('reconnecting' as const);
23362327
return Object.freeze({
23372328
peerId,
23382329
...(advertisement?.endpointKind ? { endpointKind: advertisement.endpointKind } : {}),

0 commit comments

Comments
 (0)