fix(core): a learned RIP route expires at its time and is announced at once
Java's RIPTable.check marks an expired route only at the router's next beacon, up to some 35 s late. The engine schedules each router's first expiry, marks the route with 16 hops at that time and sends its table at once, as after any change (better than Java where Java only samples, decided 01.10.2026). ripJava's expire story is compared up to the expiry and then checks that R2 marks the route at its time and R1 a cable later. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,8 +9,9 @@
|
||||
// lays the cable by it; Swift's wirelessAssociations, which join by
|
||||
// SSID at run time, are not taken. Measured with Java FILIUS)
|
||||
// "UDP sockets and Router RIP" (lines ~4472–5187; RIP
|
||||
// after Java's filius.software.rip: expiry checked at the beacon, a
|
||||
// random of its own per router, any source port)
|
||||
// after Java's filius.software.rip: a random of its own per router,
|
||||
// any source port; a route expires at its time, not at the next
|
||||
// beacon as in Java)
|
||||
// "Java-compatible TCP sockets" (lines ~5187–5913; the
|
||||
// timing and closing after Java's TCPSocket, measured with Java
|
||||
// FILIUS: 3 round trips per attempt and for TIME_WAIT, the other end
|
||||
@@ -142,6 +143,8 @@ export class NotPortedError extends Error {
|
||||
export type ScheduledEventKind =
|
||||
| { readonly kind: 'parityMarker'; readonly label: string }
|
||||
| { readonly kind: 'ripBeacon'; readonly nodeId: Id }
|
||||
/** A learned route of the router reaches its time. */
|
||||
| { readonly kind: 'ripRouteExpiry'; readonly nodeId: Id }
|
||||
| { readonly kind: 'dhcpClientStart'; readonly nodeId: Id }
|
||||
| { readonly kind: 'dhcpTimeout'; readonly nodeId: Id }
|
||||
/** An ARP lookup waited a round trip for its answer. */
|
||||
@@ -1136,6 +1139,9 @@ export class NetworkRuntimeEngine {
|
||||
case 'ripBeacon':
|
||||
this.sendRipBeacon(kind.nodeId);
|
||||
return;
|
||||
case 'ripRouteExpiry':
|
||||
this.expireRipRoutes(kind.nodeId);
|
||||
return;
|
||||
case 'dhcpClientStart':
|
||||
this.runDhcpClient(kind.nodeId);
|
||||
return;
|
||||
@@ -2481,23 +2487,43 @@ export class NetworkRuntimeEngine {
|
||||
}
|
||||
|
||||
/**
|
||||
* RIPBeacon.announce, one round: learned routes past their time get hop
|
||||
* count 16 (RIPTable.check — only here, not when they expire), the table
|
||||
* goes out on every interface, the next round comes 25.2 to 35.2 s later.
|
||||
* RIPBeacon.announce, one round: the table goes out on every interface,
|
||||
* the next round comes 25.2 to 35.2 s later.
|
||||
*/
|
||||
private sendRipBeacon(nodeId: Id): void {
|
||||
const router = this.ripRoutersByNodeId.get(nodeId);
|
||||
const table = this.stateValue.ripTablesByNodeId.get(nodeId);
|
||||
if (router === undefined || table === undefined) return;
|
||||
const now = this.stateValue.currentTimeMilliseconds;
|
||||
this.stateValue.ripTablesByNodeId.set(
|
||||
nodeId,
|
||||
table.map((route) =>
|
||||
route.expiresAtMilliseconds !== undefined && route.expiresAtMilliseconds < now ? { ...route, metric: RIP_INFINITY } : route,
|
||||
),
|
||||
);
|
||||
if (router === undefined || !this.stateValue.ripTablesByNodeId.has(nodeId)) return;
|
||||
this.advertiseRipRoutes(nodeId, router.beaconSocketId);
|
||||
this.scheduleRipBeacon(nodeId, now + router.random.nextRipJitterMilliseconds());
|
||||
this.scheduleRipBeacon(nodeId, this.stateValue.currentTimeMilliseconds + router.random.nextRipJitterMilliseconds());
|
||||
}
|
||||
|
||||
/**
|
||||
* A learned route that has heard nothing for its time gets hop count 16,
|
||||
* and the router tells its neighbours at once, as after any change. Java
|
||||
* (RIPTable.check) looks only at its next beacon, up to some 35 s later;
|
||||
* the engine at the time itself (better than Java where it only samples:
|
||||
* the user, 01.10.2026).
|
||||
*/
|
||||
private expireRipRoutes(nodeId: Id): void {
|
||||
const table = this.stateValue.ripTablesByNodeId.get(nodeId);
|
||||
if (table === undefined) return;
|
||||
const now = this.stateValue.currentTimeMilliseconds;
|
||||
const expired = (route: RouteRow) => route.expiresAtMilliseconds !== undefined && route.expiresAtMilliseconds <= now && (route.metric ?? RIP_INFINITY) < RIP_INFINITY;
|
||||
if (table.some(expired)) {
|
||||
this.stateValue.ripTablesByNodeId.set(nodeId, table.map((route) => (expired(route) ? { ...route, metric: RIP_INFINITY } : route)));
|
||||
this.scheduleRipBeacon(nodeId, now);
|
||||
}
|
||||
this.scheduleRipRouteExpiry(nodeId);
|
||||
}
|
||||
|
||||
/** One pending expiry per router: when its first reachable learned route runs out. */
|
||||
private scheduleRipRouteExpiry(nodeId: Id): void {
|
||||
const state = this.stateValue;
|
||||
state.pendingEvents = state.pendingEvents.filter((event) => !(event.kind.kind === 'ripRouteExpiry' && event.kind.nodeId === nodeId));
|
||||
const deadlines = (state.ripTablesByNodeId.get(nodeId) ?? [])
|
||||
.filter((route) => route.expiresAtMilliseconds !== undefined && (route.metric ?? RIP_INFINITY) < RIP_INFINITY)
|
||||
.map((route) => route.expiresAtMilliseconds!);
|
||||
if (deadlines.length > 0) this.schedule(Math.min(...deadlines), { kind: 'ripRouteExpiry', nodeId });
|
||||
}
|
||||
|
||||
/** One pending beacon per router, the earlier one wins (Java: setNextBeacon, then notify). */
|
||||
@@ -2600,6 +2626,7 @@ export class NetworkRuntimeEngine {
|
||||
}
|
||||
}
|
||||
state.ripTablesByNodeId.set(nodeId, table);
|
||||
this.scheduleRipRouteExpiry(nodeId);
|
||||
if (announce) this.scheduleRipBeacon(nodeId, state.currentTimeMilliseconds);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user