p2p-fret 1.2.0 → 1.2.1
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/src/service/fret-service.d.ts +42 -4
- package/dist/src/service/fret-service.js +81 -25
- package/dist/src/service/idle-backoff.d.ts +5 -4
- package/dist/src/service/idle-backoff.js +5 -4
- package/dist/src/store/digitree-store.d.ts +12 -0
- package/dist/src/store/digitree-store.js +5 -0
- package/package.json +1 -1
- package/src/service/fret-service.ts +94 -26
- package/src/service/idle-backoff.ts +5 -4
- package/src/store/digitree-store.ts +17 -0
|
@@ -638,7 +638,9 @@ export declare class FretService implements IFretService, Startable {
|
|
|
638
638
|
* Classify `id` from a libp2p-reported protocol list (available once identify has
|
|
639
639
|
* run). Member if any of our namespaced protocols appears; foreign if the list is
|
|
640
640
|
* non-empty but contains none of them; left unknown if empty (identify pending).
|
|
641
|
-
* The demotion arm is deliberately weak — see `applyMembershipSignal`.
|
|
641
|
+
* The demotion arm is deliberately weak — see `applyMembershipSignal`. A non-empty list also
|
|
642
|
+
* records whether it named ours (`PeerEntry.advertisedForeign`), which the label alone cannot
|
|
643
|
+
* say for a member — see {@link noteAdvertisedProtocols}.
|
|
642
644
|
*
|
|
643
645
|
* NOTE: the positive arm resets the negotiate-failure run (identify listing one of our protocols
|
|
644
646
|
* is strong evidence), so a stale-positive list re-applied often enough would keep a peer that
|
|
@@ -656,6 +658,16 @@ export declare class FretService implements IFretService, Startable {
|
|
|
656
658
|
* service-only-outage block still runs on identify nodes, the realistic rig.
|
|
657
659
|
*/
|
|
658
660
|
private classifyByProtocols;
|
|
661
|
+
/**
|
|
662
|
+
* Record on `id`'s entry whether its latest non-empty advertised protocol list named ours
|
|
663
|
+
* (`PeerEntry.advertisedForeign`) — whatever its label, which the evidence-strength ordering
|
|
664
|
+
* settles separately: a confirmed member stays `member` on a list lacking ours, but the flag
|
|
665
|
+
* still says what it now advertises. Never a change on its own: clearing it coincides with the
|
|
666
|
+
* `identify-member` promotion, which counts for itself, and setting it on a member moves nothing
|
|
667
|
+
* a pass acts on — the flag only helps keep an *off-ring* peer out of the re-probe arms (see
|
|
668
|
+
* {@link isSettledForeign}).
|
|
669
|
+
*/
|
|
670
|
+
private noteAdvertisedProtocols;
|
|
659
671
|
/**
|
|
660
672
|
* Admit a peer libp2p's peerStore told us about, then classify it off `protocols` — the shared
|
|
661
673
|
* step of the `peer:update` / `peer:identify` listeners and the start walk.
|
|
@@ -1400,6 +1412,9 @@ export declare class FretService implements IFretService, Startable {
|
|
|
1400
1412
|
*
|
|
1401
1413
|
* The gate lives here rather than inside the selectors so a bare selector call (from a test)
|
|
1402
1414
|
* still exercises the real selection logic against the live table.
|
|
1415
|
+
*
|
|
1416
|
+
* The two selections come back apart ({@link PhaseTwoTargets}) because only the classify arm
|
|
1417
|
+
* holds the idle backoff at its base; the tick pools them together.
|
|
1403
1418
|
*/
|
|
1404
1419
|
private phaseTwoTargets;
|
|
1405
1420
|
/**
|
|
@@ -1438,9 +1453,12 @@ export declare class FretService implements IFretService, Startable {
|
|
|
1438
1453
|
* - **`foreign`** — a same-network peer *mislabeled* foreign (e.g. identify completed before
|
|
1439
1454
|
* it registered our protocol handlers) is re-admitted by a successful namespaced ping.
|
|
1440
1455
|
* Before ring-membership gating this self-healed for free (`probeNeighborLatency` pinged
|
|
1441
|
-
* near ring peers regardless of label); gating closed that path.
|
|
1442
|
-
*
|
|
1443
|
-
* backstop.
|
|
1456
|
+
* near ring peers regardless of label); gating closed that path. For a peer demoted by
|
|
1457
|
+
* negotiate strikes whose latest advertised list still names ours (a same-network peer
|
|
1458
|
+
* mid-restart on a host without `identifyPush`) this is the dependable backstop. A peer whose
|
|
1459
|
+
* latest list names none of ours is re-probed only until it has refused a full negotiate run
|
|
1460
|
+
* (see {@link isSettledForeign}); after that it comes back through identify delivering a list
|
|
1461
|
+
* that does, or its own inbound RPC.
|
|
1444
1462
|
* - **`dead`** — a peer marked dead by a run of failed contacts is out of every ring view, so
|
|
1445
1463
|
* `stabilizeOnce` (which draws its probe targets from `getNeighbors`) never touches it
|
|
1446
1464
|
* again. Without this arm a peer that recovers but never dials us and never forms a
|
|
@@ -1458,6 +1476,18 @@ export declare class FretService implements IFretService, Startable {
|
|
|
1458
1476
|
* `entries` defaults to a fresh `store.list()`, exactly as {@link classifyTargets}'s does.
|
|
1459
1477
|
*/
|
|
1460
1478
|
private reprobeExcludedTargets;
|
|
1479
|
+
/**
|
|
1480
|
+
* Whether `e` is foreign on both kinds of evidence at once: its latest advertised protocol list
|
|
1481
|
+
* names none of ours (`advertisedForeign`), *and* a full run of spaced negotiate refusals since
|
|
1482
|
+
* the last positive proof says it really answers none of them. Neither alone stops re-probing.
|
|
1483
|
+
* The list can be stale — captured before the remote's FRET registered its handlers, the
|
|
1484
|
+
* ordinary shape for a host that starts the core service by hand after its node — and without
|
|
1485
|
+
* `identifyPush` nothing refreshes it: two such peers trusting the list alone would never contact
|
|
1486
|
+
* each other at all (`test/address-hints.relay.spec.ts` is that rig). A refusal run alone is what
|
|
1487
|
+
* a same-network peer mid-restart produces, which is what the foreign arm exists to recover.
|
|
1488
|
+
* Costs a relay {@link NEGOTIATE_FAILURE_THRESHOLD} failed pings per run, then silence.
|
|
1489
|
+
*/
|
|
1490
|
+
private isSettledForeign;
|
|
1461
1491
|
/**
|
|
1462
1492
|
* Select one bounded re-probe arm's targets — peers matching `isCandidate` — the shared
|
|
1463
1493
|
* mechanics of both arms of {@link reprobeExcludedTargets}: off-backoff and reachable candidates
|
|
@@ -1469,6 +1499,14 @@ export declare class FretService implements IFretService, Startable {
|
|
|
1469
1499
|
* re-probed at most ~once per backoff window (doubling to a 32× cap) rather than every tick.
|
|
1470
1500
|
* That growing factor is also what rotates the list under tick truncation: a probed peer's
|
|
1471
1501
|
* factor grows, so it sorts behind the ones a truncated tick never reached.
|
|
1502
|
+
*
|
|
1503
|
+
* A peer that is foreign on both kinds of evidence ({@link isSettledForeign} — a relay, another
|
|
1504
|
+
* network's node, once it has refused a run of our probes) is skipped by **both** arms: answering
|
|
1505
|
+
* our ping cannot make it a ring member, so probing it — and redialing it when disconnected —
|
|
1506
|
+
* was traffic forever with nothing to recover. If it does start serving this network, identify
|
|
1507
|
+
* delivers the new list (`identifyPush`, or a reconnect re-running identify), or it contacts us
|
|
1508
|
+
* itself; a dead one that comes back is revived by `peer:connect`. The filter only removes
|
|
1509
|
+
* candidates, so the separate per-arm budgets stand.
|
|
1472
1510
|
*/
|
|
1473
1511
|
private reprobeOffRingTargets;
|
|
1474
1512
|
/**
|
|
@@ -1283,7 +1283,9 @@ export class FretService {
|
|
|
1283
1283
|
* Classify `id` from a libp2p-reported protocol list (available once identify has
|
|
1284
1284
|
* run). Member if any of our namespaced protocols appears; foreign if the list is
|
|
1285
1285
|
* non-empty but contains none of them; left unknown if empty (identify pending).
|
|
1286
|
-
* The demotion arm is deliberately weak — see `applyMembershipSignal`.
|
|
1286
|
+
* The demotion arm is deliberately weak — see `applyMembershipSignal`. A non-empty list also
|
|
1287
|
+
* records whether it named ours (`PeerEntry.advertisedForeign`), which the label alone cannot
|
|
1288
|
+
* say for a member — see {@link noteAdvertisedProtocols}.
|
|
1287
1289
|
*
|
|
1288
1290
|
* NOTE: the positive arm resets the negotiate-failure run (identify listing one of our protocols
|
|
1289
1291
|
* is strong evidence), so a stale-positive list re-applied often enough would keep a peer that
|
|
@@ -1304,8 +1306,24 @@ export class FretService {
|
|
|
1304
1306
|
if (!protocols || protocols.length === 0)
|
|
1305
1307
|
return; // identify not complete → stay unknown
|
|
1306
1308
|
const mine = Object.values(this.protocols);
|
|
1307
|
-
const
|
|
1308
|
-
this.
|
|
1309
|
+
const advertisesOurs = protocols.some((p) => mine.includes(p));
|
|
1310
|
+
this.noteAdvertisedProtocols(id, advertisesOurs);
|
|
1311
|
+
this.applyMembershipSignal(id, advertisesOurs ? 'identify-member' : 'identify-foreign');
|
|
1312
|
+
}
|
|
1313
|
+
/**
|
|
1314
|
+
* Record on `id`'s entry whether its latest non-empty advertised protocol list named ours
|
|
1315
|
+
* (`PeerEntry.advertisedForeign`) — whatever its label, which the evidence-strength ordering
|
|
1316
|
+
* settles separately: a confirmed member stays `member` on a list lacking ours, but the flag
|
|
1317
|
+
* still says what it now advertises. Never a change on its own: clearing it coincides with the
|
|
1318
|
+
* `identify-member` promotion, which counts for itself, and setting it on a member moves nothing
|
|
1319
|
+
* a pass acts on — the flag only helps keep an *off-ring* peer out of the re-probe arms (see
|
|
1320
|
+
* {@link isSettledForeign}).
|
|
1321
|
+
*/
|
|
1322
|
+
noteAdvertisedProtocols(id, advertisesOurs) {
|
|
1323
|
+
const advertisedForeign = !advertisesOurs;
|
|
1324
|
+
const e = this.store.getById(id);
|
|
1325
|
+
if (e && e.advertisedForeign !== advertisedForeign)
|
|
1326
|
+
this.store.update(id, { advertisedForeign });
|
|
1309
1327
|
}
|
|
1310
1328
|
/**
|
|
1311
1329
|
* Admit a peer libp2p's peerStore told us about, then classify it off `protocols` — the shared
|
|
@@ -2997,26 +3015,36 @@ export class FretService {
|
|
|
2997
3015
|
// buys phase 2 its turn. This return still fires for a `stop()` or a tick that really
|
|
2998
3016
|
// is out of time.
|
|
2999
3017
|
if (budget.signal.aborted) {
|
|
3000
|
-
// Work left undone that no event will announce —
|
|
3001
|
-
//
|
|
3018
|
+
// Work left undone that no event will announce — phase 2 never selected, so nothing
|
|
3019
|
+
// can tell whether unknowns were waiting. Harmless on a `stop()`, which resets the
|
|
3020
|
+
// backoff anyway.
|
|
3002
3021
|
this.idle.notePending();
|
|
3003
3022
|
return;
|
|
3004
3023
|
}
|
|
3005
|
-
const
|
|
3006
|
-
// Pending on the
|
|
3007
|
-
//
|
|
3008
|
-
//
|
|
3009
|
-
//
|
|
3010
|
-
//
|
|
3011
|
-
//
|
|
3012
|
-
//
|
|
3013
|
-
//
|
|
3014
|
-
//
|
|
3015
|
-
//
|
|
3016
|
-
//
|
|
3017
|
-
//
|
|
3018
|
-
|
|
3024
|
+
const { classify, reprobe } = this.phaseTwoTargets();
|
|
3025
|
+
// Pending on the classify arm's selection alone. An `unknown` peer is a transient
|
|
3026
|
+
// population the ring is waiting on, so the next pass should carry on classifying at the
|
|
3027
|
+
// base. A re-probe's outcome lands inside this pass: a success is a membership or revival
|
|
3028
|
+
// change, a failure that progresses a run is a change, and one that progresses nothing
|
|
3029
|
+
// needs no follow-up pass — so holding for it only kept a ring with a permanently
|
|
3030
|
+
// off-ring peer (a dead phone, a relay) restarting the ladder every backoff window. A
|
|
3031
|
+
// redial this pass outlives is held by `runStabilizationPass` on its own.
|
|
3032
|
+
// NOTE: accepted tradeoff — once the loop has backed off, the re-probe arms service their
|
|
3033
|
+
// budget (Core 2 / Edge 1 per arm) per *interval*, not per 1.5 s: a dead or
|
|
3034
|
+
// negotiate-foreign peer whose window expires during a backed-off interval is re-probed up
|
|
3035
|
+
// to one interval late, and with more such peers than the budget each waits about
|
|
3036
|
+
// ceil(count / budget) intervals — at the 60 s cap, a Core ring holding 10 dead entries
|
|
3037
|
+
// re-probes each about every 5 minutes while idle. Accepted because a recovered neighbour
|
|
3038
|
+
// contacts us itself (an inbound RPC revives it), a reconnect revives it through
|
|
3039
|
+
// `peer:connect`, and any change returns the loop to the base. Holding pending whenever an
|
|
3040
|
+
// arm was truncated by its budget was rejected: at Edge's budget of 1, two dead entries
|
|
3041
|
+
// whose windows align truncate every window and restart the ladder about every 32 s.
|
|
3042
|
+
// Revisit if dead-recovery latency on large idle rings shows up as a problem — then hold
|
|
3043
|
+
// pending when an off-ring arm was budget-truncated, accepting that saturated arms keep the
|
|
3044
|
+
// loop awake.
|
|
3045
|
+
if (classify.length > 0)
|
|
3019
3046
|
this.idle.notePending();
|
|
3047
|
+
const targets = [...classify, ...reprobe];
|
|
3020
3048
|
const phaseTwoPool = { concurrency: this.maintenanceConcurrency, signal: budget.signal };
|
|
3021
3049
|
const probed = await runPooled(targets.map((id) => () => this.probeMembership(id, budget.signal)), phaseTwoPool);
|
|
3022
3050
|
logRejected(probed, targets, 'probeMembership');
|
|
@@ -3363,12 +3391,15 @@ export class FretService {
|
|
|
3363
3391
|
*
|
|
3364
3392
|
* The gate lives here rather than inside the selectors so a bare selector call (from a test)
|
|
3365
3393
|
* still exercises the real selection logic against the live table.
|
|
3394
|
+
*
|
|
3395
|
+
* The two selections come back apart ({@link PhaseTwoTargets}) because only the classify arm
|
|
3396
|
+
* holds the idle backoff at its base; the tick pools them together.
|
|
3366
3397
|
*/
|
|
3367
3398
|
phaseTwoTargets() {
|
|
3368
3399
|
if (this.store.countByMembership('unknown') === 0
|
|
3369
3400
|
&& this.store.countByMembership('foreign') === 0
|
|
3370
3401
|
&& this.store.countByState('dead') === 0)
|
|
3371
|
-
return [];
|
|
3402
|
+
return { classify: [], reprobe: [] };
|
|
3372
3403
|
// NOTE: the walk is captured once and `put` writes a *new* entry object rather than editing
|
|
3373
3404
|
// in place, so a store mutation made after this line is invisible to the arms that run
|
|
3374
3405
|
// after it. Sound today: all three arms are pure selection — they read the backoff state,
|
|
@@ -3376,7 +3407,7 @@ export class FretService {
|
|
|
3376
3407
|
// happens later, in the pooled phase. If an arm ever writes to the store while selecting,
|
|
3377
3408
|
// re-walk per arm rather than leaving the later arms on a stale array.
|
|
3378
3409
|
const entries = this.store.list();
|
|
3379
|
-
return
|
|
3410
|
+
return { classify: this.classifyTargets(entries), reprobe: this.reprobeExcludedTargets(entries) };
|
|
3380
3411
|
}
|
|
3381
3412
|
/**
|
|
3382
3413
|
* Bounded classification probe pass over `unknown` peers.
|
|
@@ -3427,9 +3458,12 @@ export class FretService {
|
|
|
3427
3458
|
* - **`foreign`** — a same-network peer *mislabeled* foreign (e.g. identify completed before
|
|
3428
3459
|
* it registered our protocol handlers) is re-admitted by a successful namespaced ping.
|
|
3429
3460
|
* Before ring-membership gating this self-healed for free (`probeNeighborLatency` pinged
|
|
3430
|
-
* near ring peers regardless of label); gating closed that path.
|
|
3431
|
-
*
|
|
3432
|
-
* backstop.
|
|
3461
|
+
* near ring peers regardless of label); gating closed that path. For a peer demoted by
|
|
3462
|
+
* negotiate strikes whose latest advertised list still names ours (a same-network peer
|
|
3463
|
+
* mid-restart on a host without `identifyPush`) this is the dependable backstop. A peer whose
|
|
3464
|
+
* latest list names none of ours is re-probed only until it has refused a full negotiate run
|
|
3465
|
+
* (see {@link isSettledForeign}); after that it comes back through identify delivering a list
|
|
3466
|
+
* that does, or its own inbound RPC.
|
|
3433
3467
|
* - **`dead`** — a peer marked dead by a run of failed contacts is out of every ring view, so
|
|
3434
3468
|
* `stabilizeOnce` (which draws its probe targets from `getNeighbors`) never touches it
|
|
3435
3469
|
* again. Without this arm a peer that recovers but never dials us and never forms a
|
|
@@ -3465,6 +3499,20 @@ export class FretService {
|
|
|
3465
3499
|
...this.reprobeOffRingTargets(entries, (e) => e.state === 'dead', budget),
|
|
3466
3500
|
];
|
|
3467
3501
|
}
|
|
3502
|
+
/**
|
|
3503
|
+
* Whether `e` is foreign on both kinds of evidence at once: its latest advertised protocol list
|
|
3504
|
+
* names none of ours (`advertisedForeign`), *and* a full run of spaced negotiate refusals since
|
|
3505
|
+
* the last positive proof says it really answers none of them. Neither alone stops re-probing.
|
|
3506
|
+
* The list can be stale — captured before the remote's FRET registered its handlers, the
|
|
3507
|
+
* ordinary shape for a host that starts the core service by hand after its node — and without
|
|
3508
|
+
* `identifyPush` nothing refreshes it: two such peers trusting the list alone would never contact
|
|
3509
|
+
* each other at all (`test/address-hints.relay.spec.ts` is that rig). A refusal run alone is what
|
|
3510
|
+
* a same-network peer mid-restart produces, which is what the foreign arm exists to recover.
|
|
3511
|
+
* Costs a relay {@link NEGOTIATE_FAILURE_THRESHOLD} failed pings per run, then silence.
|
|
3512
|
+
*/
|
|
3513
|
+
isSettledForeign(e) {
|
|
3514
|
+
return e.advertisedForeign && e.negotiateFailures >= FretService.NEGOTIATE_FAILURE_THRESHOLD;
|
|
3515
|
+
}
|
|
3468
3516
|
/**
|
|
3469
3517
|
* Select one bounded re-probe arm's targets — peers matching `isCandidate` — the shared
|
|
3470
3518
|
* mechanics of both arms of {@link reprobeExcludedTargets}: off-backoff and reachable candidates
|
|
@@ -3476,10 +3524,18 @@ export class FretService {
|
|
|
3476
3524
|
* re-probed at most ~once per backoff window (doubling to a 32× cap) rather than every tick.
|
|
3477
3525
|
* That growing factor is also what rotates the list under tick truncation: a probed peer's
|
|
3478
3526
|
* factor grows, so it sorts behind the ones a truncated tick never reached.
|
|
3527
|
+
*
|
|
3528
|
+
* A peer that is foreign on both kinds of evidence ({@link isSettledForeign} — a relay, another
|
|
3529
|
+
* network's node, once it has refused a run of our probes) is skipped by **both** arms: answering
|
|
3530
|
+
* our ping cannot make it a ring member, so probing it — and redialing it when disconnected —
|
|
3531
|
+
* was traffic forever with nothing to recover. If it does start serving this network, identify
|
|
3532
|
+
* delivers the new list (`identifyPush`, or a reconnect re-running identify), or it contacts us
|
|
3533
|
+
* itself; a dead one that comes back is revived by `peer:connect`. The filter only removes
|
|
3534
|
+
* candidates, so the separate per-arm budgets stand.
|
|
3479
3535
|
*/
|
|
3480
3536
|
reprobeOffRingTargets(entries, isCandidate, budget) {
|
|
3481
3537
|
const selfStr = this.selfIdStr;
|
|
3482
|
-
const candidates = entries.filter((e) => e.id !== selfStr && !this.backoff.isBackedOff(e.id) && isCandidate(e));
|
|
3538
|
+
const candidates = entries.filter((e) => e.id !== selfStr && !this.isSettledForeign(e) && !this.backoff.isBackedOff(e.id) && isCandidate(e));
|
|
3483
3539
|
if (candidates.length === 0)
|
|
3484
3540
|
return [];
|
|
3485
3541
|
// Only reachable peers are probeable; prefer connected over has-addresses.
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* base the moment a **change** is noted — anything the next pass could act on (a connection
|
|
10
10
|
* forming or dropping, a new entry, a label or liveness transition, an address becoming known) —
|
|
11
11
|
* and holds at the base, without counting as a change, while a pass left work **pending**
|
|
12
|
-
* (
|
|
12
|
+
* (a near probe it skipped on recent evidence, unknown peers it selected for classification, a
|
|
13
|
+
* redial it outlived, a phase its tick budget cut off).
|
|
13
14
|
*
|
|
14
15
|
* A change is recorded as a bump of a monotonic counter, and a pass snapshots that counter when it
|
|
15
16
|
* opens: {@link endPass} compares the two, so a change that lands *during* a pass is seen at that
|
|
@@ -52,10 +53,10 @@ export declare class IdleBackoff {
|
|
|
52
53
|
/** Open a pass: snapshot the change counter and clear the pending flag. */
|
|
53
54
|
beginPass(): void;
|
|
54
55
|
/**
|
|
55
|
-
* The open pass left work no event will announce the outcome of — it selected
|
|
56
|
-
*
|
|
56
|
+
* The open pass left work no event will announce the outcome of — it selected unknown peers to
|
|
57
|
+
* classify, or a redial it started is still running. Holds the next delay at the base. Not a
|
|
57
58
|
* change: nothing happened yet, and counting it would reset the quiet count on every pass that
|
|
58
|
-
* merely *
|
|
59
|
+
* merely *selected* a peer to probe.
|
|
59
60
|
*/
|
|
60
61
|
notePending(): void;
|
|
61
62
|
/**
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* base the moment a **change** is noted — anything the next pass could act on (a connection
|
|
10
10
|
* forming or dropping, a new entry, a label or liveness transition, an address becoming known) —
|
|
11
11
|
* and holds at the base, without counting as a change, while a pass left work **pending**
|
|
12
|
-
* (
|
|
12
|
+
* (a near probe it skipped on recent evidence, unknown peers it selected for classification, a
|
|
13
|
+
* redial it outlived, a phase its tick budget cut off).
|
|
13
14
|
*
|
|
14
15
|
* A change is recorded as a bump of a monotonic counter, and a pass snapshots that counter when it
|
|
15
16
|
* opens: {@link endPass} compares the two, so a change that lands *during* a pass is seen at that
|
|
@@ -66,10 +67,10 @@ export class IdleBackoff {
|
|
|
66
67
|
this.pending = false;
|
|
67
68
|
}
|
|
68
69
|
/**
|
|
69
|
-
* The open pass left work no event will announce the outcome of — it selected
|
|
70
|
-
*
|
|
70
|
+
* The open pass left work no event will announce the outcome of — it selected unknown peers to
|
|
71
|
+
* classify, or a redial it started is still running. Holds the next delay at the base. Not a
|
|
71
72
|
* change: nothing happened yet, and counting it would reset the quiet count on every pass that
|
|
72
|
-
* merely *
|
|
73
|
+
* merely *selected* a peer to probe.
|
|
73
74
|
*/
|
|
74
75
|
notePending() {
|
|
75
76
|
this.pending = true;
|
|
@@ -31,6 +31,18 @@ export interface PeerEntry {
|
|
|
31
31
|
* the store only stores and exposes the count; it never branches on it.
|
|
32
32
|
*/
|
|
33
33
|
negotiateFailures: number;
|
|
34
|
+
/**
|
|
35
|
+
* Whether the most recent non-empty protocol list libp2p delivered for this peer (identify,
|
|
36
|
+
* `peer:update`, or the start walk) named none of this network's protocols.
|
|
37
|
+
*
|
|
38
|
+
* Independent of `membership`: a confirmed `member` keeps its label when a later list drops our
|
|
39
|
+
* protocols (that list may be stale), but the flag still records what the peer now advertises.
|
|
40
|
+
* Callers use it, beside a completed `negotiateFailures` run, to stop re-probing an off-ring
|
|
41
|
+
* peer that cannot be serving this network. Not serialized — it is re-derived from the
|
|
42
|
+
* peerStore — so `false` on insert and on import. Like `membership`, the store only stores and
|
|
43
|
+
* exposes it; it never branches on it.
|
|
44
|
+
*/
|
|
45
|
+
advertisedForeign: boolean;
|
|
34
46
|
/**
|
|
35
47
|
* Unix ms of the last handshake failure that was *counted* toward `negotiateFailures`.
|
|
36
48
|
*
|
|
@@ -206,6 +206,7 @@ export class DigitreeStore {
|
|
|
206
206
|
state: 'disconnected',
|
|
207
207
|
membership: 'unknown',
|
|
208
208
|
negotiateFailures: 0,
|
|
209
|
+
advertisedForeign: false,
|
|
209
210
|
lastNegotiateFailureAt: 0,
|
|
210
211
|
contactFailures: 0,
|
|
211
212
|
lastContactFailureAt: 0,
|
|
@@ -485,6 +486,10 @@ export class DigitreeStore {
|
|
|
485
486
|
// too. Reset for the same reason `state` is forced to 'disconnected'.
|
|
486
487
|
negotiateFailures: 0,
|
|
487
488
|
lastNegotiateFailureAt: 0,
|
|
489
|
+
// Re-derived from the peerStore, not restored: the start walk re-applies each peer's
|
|
490
|
+
// protocol list. A table imported *after* start() therefore re-probes a replaced
|
|
491
|
+
// foreign entry until its next `peer:update` — a few cheap pings, accepted.
|
|
492
|
+
advertisedForeign: false,
|
|
488
493
|
// Likewise unreachability: a peer we could not contact before the restart may be
|
|
489
494
|
// reachable now. `state` is already forced to 'disconnected', so resetting the
|
|
490
495
|
// counter alongside it is what stops an imported table from re-killing that peer
|
package/package.json
CHANGED
|
@@ -152,6 +152,17 @@ interface NearWindow {
|
|
|
152
152
|
uncontactable: string[];
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
/**
|
|
156
|
+
* A tick's phase-2 selection, kept apart by arm because only one of them holds the idle backoff at
|
|
157
|
+
* its base (see `stabilizeOnce`). Disjoint, and pooled together for execution.
|
|
158
|
+
*/
|
|
159
|
+
interface PhaseTwoTargets {
|
|
160
|
+
/** `unknown` peers to classify — a transient population the ring is waiting on. */
|
|
161
|
+
classify: string[];
|
|
162
|
+
/** `foreign` and `dead` peers to re-probe — each probe's outcome lands inside the pass. */
|
|
163
|
+
reprobe: string[];
|
|
164
|
+
}
|
|
165
|
+
|
|
155
166
|
/**
|
|
156
167
|
* The one verdict a maintenance redial ({@link FretService.redial}) reaches, shared by everyone
|
|
157
168
|
* who awaited it.
|
|
@@ -1409,7 +1420,9 @@ export class FretService implements IFretService, Startable {
|
|
|
1409
1420
|
* Classify `id` from a libp2p-reported protocol list (available once identify has
|
|
1410
1421
|
* run). Member if any of our namespaced protocols appears; foreign if the list is
|
|
1411
1422
|
* non-empty but contains none of them; left unknown if empty (identify pending).
|
|
1412
|
-
* The demotion arm is deliberately weak — see `applyMembershipSignal`.
|
|
1423
|
+
* The demotion arm is deliberately weak — see `applyMembershipSignal`. A non-empty list also
|
|
1424
|
+
* records whether it named ours (`PeerEntry.advertisedForeign`), which the label alone cannot
|
|
1425
|
+
* say for a member — see {@link noteAdvertisedProtocols}.
|
|
1413
1426
|
*
|
|
1414
1427
|
* NOTE: the positive arm resets the negotiate-failure run (identify listing one of our protocols
|
|
1415
1428
|
* is strong evidence), so a stale-positive list re-applied often enough would keep a peer that
|
|
@@ -1429,8 +1442,24 @@ export class FretService implements IFretService, Startable {
|
|
|
1429
1442
|
private classifyByProtocols(id: string, protocols: string[] | undefined): void {
|
|
1430
1443
|
if (!protocols || protocols.length === 0) return; // identify not complete → stay unknown
|
|
1431
1444
|
const mine = Object.values(this.protocols);
|
|
1432
|
-
const
|
|
1433
|
-
this.
|
|
1445
|
+
const advertisesOurs = protocols.some((p) => mine.includes(p));
|
|
1446
|
+
this.noteAdvertisedProtocols(id, advertisesOurs);
|
|
1447
|
+
this.applyMembershipSignal(id, advertisesOurs ? 'identify-member' : 'identify-foreign');
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
/**
|
|
1451
|
+
* Record on `id`'s entry whether its latest non-empty advertised protocol list named ours
|
|
1452
|
+
* (`PeerEntry.advertisedForeign`) — whatever its label, which the evidence-strength ordering
|
|
1453
|
+
* settles separately: a confirmed member stays `member` on a list lacking ours, but the flag
|
|
1454
|
+
* still says what it now advertises. Never a change on its own: clearing it coincides with the
|
|
1455
|
+
* `identify-member` promotion, which counts for itself, and setting it on a member moves nothing
|
|
1456
|
+
* a pass acts on — the flag only helps keep an *off-ring* peer out of the re-probe arms (see
|
|
1457
|
+
* {@link isSettledForeign}).
|
|
1458
|
+
*/
|
|
1459
|
+
private noteAdvertisedProtocols(id: string, advertisesOurs: boolean): void {
|
|
1460
|
+
const advertisedForeign = !advertisesOurs;
|
|
1461
|
+
const e = this.store.getById(id);
|
|
1462
|
+
if (e && e.advertisedForeign !== advertisedForeign) this.store.update(id, { advertisedForeign });
|
|
1434
1463
|
}
|
|
1435
1464
|
|
|
1436
1465
|
/**
|
|
@@ -3064,25 +3093,35 @@ export class FretService implements IFretService, Startable {
|
|
|
3064
3093
|
// buys phase 2 its turn. This return still fires for a `stop()` or a tick that really
|
|
3065
3094
|
// is out of time.
|
|
3066
3095
|
if (budget.signal.aborted) {
|
|
3067
|
-
// Work left undone that no event will announce —
|
|
3068
|
-
//
|
|
3096
|
+
// Work left undone that no event will announce — phase 2 never selected, so nothing
|
|
3097
|
+
// can tell whether unknowns were waiting. Harmless on a `stop()`, which resets the
|
|
3098
|
+
// backoff anyway.
|
|
3069
3099
|
this.idle.notePending();
|
|
3070
3100
|
return;
|
|
3071
3101
|
}
|
|
3072
|
-
const
|
|
3073
|
-
// Pending on the
|
|
3074
|
-
//
|
|
3075
|
-
//
|
|
3076
|
-
//
|
|
3077
|
-
//
|
|
3078
|
-
//
|
|
3079
|
-
//
|
|
3080
|
-
//
|
|
3081
|
-
//
|
|
3082
|
-
//
|
|
3083
|
-
//
|
|
3084
|
-
//
|
|
3085
|
-
|
|
3102
|
+
const { classify, reprobe } = this.phaseTwoTargets();
|
|
3103
|
+
// Pending on the classify arm's selection alone. An `unknown` peer is a transient
|
|
3104
|
+
// population the ring is waiting on, so the next pass should carry on classifying at the
|
|
3105
|
+
// base. A re-probe's outcome lands inside this pass: a success is a membership or revival
|
|
3106
|
+
// change, a failure that progresses a run is a change, and one that progresses nothing
|
|
3107
|
+
// needs no follow-up pass — so holding for it only kept a ring with a permanently
|
|
3108
|
+
// off-ring peer (a dead phone, a relay) restarting the ladder every backoff window. A
|
|
3109
|
+
// redial this pass outlives is held by `runStabilizationPass` on its own.
|
|
3110
|
+
// NOTE: accepted tradeoff — once the loop has backed off, the re-probe arms service their
|
|
3111
|
+
// budget (Core 2 / Edge 1 per arm) per *interval*, not per 1.5 s: a dead or
|
|
3112
|
+
// negotiate-foreign peer whose window expires during a backed-off interval is re-probed up
|
|
3113
|
+
// to one interval late, and with more such peers than the budget each waits about
|
|
3114
|
+
// ceil(count / budget) intervals — at the 60 s cap, a Core ring holding 10 dead entries
|
|
3115
|
+
// re-probes each about every 5 minutes while idle. Accepted because a recovered neighbour
|
|
3116
|
+
// contacts us itself (an inbound RPC revives it), a reconnect revives it through
|
|
3117
|
+
// `peer:connect`, and any change returns the loop to the base. Holding pending whenever an
|
|
3118
|
+
// arm was truncated by its budget was rejected: at Edge's budget of 1, two dead entries
|
|
3119
|
+
// whose windows align truncate every window and restart the ladder about every 32 s.
|
|
3120
|
+
// Revisit if dead-recovery latency on large idle rings shows up as a problem — then hold
|
|
3121
|
+
// pending when an off-ring arm was budget-truncated, accepting that saturated arms keep the
|
|
3122
|
+
// loop awake.
|
|
3123
|
+
if (classify.length > 0) this.idle.notePending();
|
|
3124
|
+
const targets = [...classify, ...reprobe];
|
|
3086
3125
|
const phaseTwoPool = { concurrency: this.maintenanceConcurrency, signal: budget.signal };
|
|
3087
3126
|
const probed = await runPooled(targets.map((id) => () => this.probeMembership(id, budget.signal)), phaseTwoPool);
|
|
3088
3127
|
logRejected(probed, targets, 'probeMembership');
|
|
@@ -3424,11 +3463,14 @@ export class FretService implements IFretService, Startable {
|
|
|
3424
3463
|
*
|
|
3425
3464
|
* The gate lives here rather than inside the selectors so a bare selector call (from a test)
|
|
3426
3465
|
* still exercises the real selection logic against the live table.
|
|
3466
|
+
*
|
|
3467
|
+
* The two selections come back apart ({@link PhaseTwoTargets}) because only the classify arm
|
|
3468
|
+
* holds the idle backoff at its base; the tick pools them together.
|
|
3427
3469
|
*/
|
|
3428
|
-
private phaseTwoTargets():
|
|
3470
|
+
private phaseTwoTargets(): PhaseTwoTargets {
|
|
3429
3471
|
if (this.store.countByMembership('unknown') === 0
|
|
3430
3472
|
&& this.store.countByMembership('foreign') === 0
|
|
3431
|
-
&& this.store.countByState('dead') === 0) return [];
|
|
3473
|
+
&& this.store.countByState('dead') === 0) return { classify: [], reprobe: [] };
|
|
3432
3474
|
// NOTE: the walk is captured once and `put` writes a *new* entry object rather than editing
|
|
3433
3475
|
// in place, so a store mutation made after this line is invisible to the arms that run
|
|
3434
3476
|
// after it. Sound today: all three arms are pure selection — they read the backoff state,
|
|
@@ -3436,7 +3478,7 @@ export class FretService implements IFretService, Startable {
|
|
|
3436
3478
|
// happens later, in the pooled phase. If an arm ever writes to the store while selecting,
|
|
3437
3479
|
// re-walk per arm rather than leaving the later arms on a stale array.
|
|
3438
3480
|
const entries = this.store.list();
|
|
3439
|
-
return
|
|
3481
|
+
return { classify: this.classifyTargets(entries), reprobe: this.reprobeExcludedTargets(entries) };
|
|
3440
3482
|
}
|
|
3441
3483
|
|
|
3442
3484
|
/**
|
|
@@ -3490,9 +3532,12 @@ export class FretService implements IFretService, Startable {
|
|
|
3490
3532
|
* - **`foreign`** — a same-network peer *mislabeled* foreign (e.g. identify completed before
|
|
3491
3533
|
* it registered our protocol handlers) is re-admitted by a successful namespaced ping.
|
|
3492
3534
|
* Before ring-membership gating this self-healed for free (`probeNeighborLatency` pinged
|
|
3493
|
-
* near ring peers regardless of label); gating closed that path.
|
|
3494
|
-
*
|
|
3495
|
-
* backstop.
|
|
3535
|
+
* near ring peers regardless of label); gating closed that path. For a peer demoted by
|
|
3536
|
+
* negotiate strikes whose latest advertised list still names ours (a same-network peer
|
|
3537
|
+
* mid-restart on a host without `identifyPush`) this is the dependable backstop. A peer whose
|
|
3538
|
+
* latest list names none of ours is re-probed only until it has refused a full negotiate run
|
|
3539
|
+
* (see {@link isSettledForeign}); after that it comes back through identify delivering a list
|
|
3540
|
+
* that does, or its own inbound RPC.
|
|
3496
3541
|
* - **`dead`** — a peer marked dead by a run of failed contacts is out of every ring view, so
|
|
3497
3542
|
* `stabilizeOnce` (which draws its probe targets from `getNeighbors`) never touches it
|
|
3498
3543
|
* again. Without this arm a peer that recovers but never dials us and never forms a
|
|
@@ -3529,6 +3574,21 @@ export class FretService implements IFretService, Startable {
|
|
|
3529
3574
|
];
|
|
3530
3575
|
}
|
|
3531
3576
|
|
|
3577
|
+
/**
|
|
3578
|
+
* Whether `e` is foreign on both kinds of evidence at once: its latest advertised protocol list
|
|
3579
|
+
* names none of ours (`advertisedForeign`), *and* a full run of spaced negotiate refusals since
|
|
3580
|
+
* the last positive proof says it really answers none of them. Neither alone stops re-probing.
|
|
3581
|
+
* The list can be stale — captured before the remote's FRET registered its handlers, the
|
|
3582
|
+
* ordinary shape for a host that starts the core service by hand after its node — and without
|
|
3583
|
+
* `identifyPush` nothing refreshes it: two such peers trusting the list alone would never contact
|
|
3584
|
+
* each other at all (`test/address-hints.relay.spec.ts` is that rig). A refusal run alone is what
|
|
3585
|
+
* a same-network peer mid-restart produces, which is what the foreign arm exists to recover.
|
|
3586
|
+
* Costs a relay {@link NEGOTIATE_FAILURE_THRESHOLD} failed pings per run, then silence.
|
|
3587
|
+
*/
|
|
3588
|
+
private isSettledForeign(e: PeerEntry): boolean {
|
|
3589
|
+
return e.advertisedForeign && e.negotiateFailures >= FretService.NEGOTIATE_FAILURE_THRESHOLD;
|
|
3590
|
+
}
|
|
3591
|
+
|
|
3532
3592
|
/**
|
|
3533
3593
|
* Select one bounded re-probe arm's targets — peers matching `isCandidate` — the shared
|
|
3534
3594
|
* mechanics of both arms of {@link reprobeExcludedTargets}: off-backoff and reachable candidates
|
|
@@ -3540,11 +3600,19 @@ export class FretService implements IFretService, Startable {
|
|
|
3540
3600
|
* re-probed at most ~once per backoff window (doubling to a 32× cap) rather than every tick.
|
|
3541
3601
|
* That growing factor is also what rotates the list under tick truncation: a probed peer's
|
|
3542
3602
|
* factor grows, so it sorts behind the ones a truncated tick never reached.
|
|
3603
|
+
*
|
|
3604
|
+
* A peer that is foreign on both kinds of evidence ({@link isSettledForeign} — a relay, another
|
|
3605
|
+
* network's node, once it has refused a run of our probes) is skipped by **both** arms: answering
|
|
3606
|
+
* our ping cannot make it a ring member, so probing it — and redialing it when disconnected —
|
|
3607
|
+
* was traffic forever with nothing to recover. If it does start serving this network, identify
|
|
3608
|
+
* delivers the new list (`identifyPush`, or a reconnect re-running identify), or it contacts us
|
|
3609
|
+
* itself; a dead one that comes back is revived by `peer:connect`. The filter only removes
|
|
3610
|
+
* candidates, so the separate per-arm budgets stand.
|
|
3543
3611
|
*/
|
|
3544
3612
|
private reprobeOffRingTargets(entries: PeerEntry[], isCandidate: (e: PeerEntry) => boolean, budget: number): string[] {
|
|
3545
3613
|
const selfStr = this.selfIdStr;
|
|
3546
3614
|
const candidates = entries.filter(
|
|
3547
|
-
(e) => e.id !== selfStr && !this.backoff.isBackedOff(e.id) && isCandidate(e)
|
|
3615
|
+
(e) => e.id !== selfStr && !this.isSettledForeign(e) && !this.backoff.isBackedOff(e.id) && isCandidate(e)
|
|
3548
3616
|
);
|
|
3549
3617
|
if (candidates.length === 0) return [];
|
|
3550
3618
|
// Only reachable peers are probeable; prefer connected over has-addresses.
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* base the moment a **change** is noted — anything the next pass could act on (a connection
|
|
10
10
|
* forming or dropping, a new entry, a label or liveness transition, an address becoming known) —
|
|
11
11
|
* and holds at the base, without counting as a change, while a pass left work **pending**
|
|
12
|
-
* (
|
|
12
|
+
* (a near probe it skipped on recent evidence, unknown peers it selected for classification, a
|
|
13
|
+
* redial it outlived, a phase its tick budget cut off).
|
|
13
14
|
*
|
|
14
15
|
* A change is recorded as a bump of a monotonic counter, and a pass snapshots that counter when it
|
|
15
16
|
* opens: {@link endPass} compares the two, so a change that lands *during* a pass is seen at that
|
|
@@ -73,10 +74,10 @@ export class IdleBackoff {
|
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
/**
|
|
76
|
-
* The open pass left work no event will announce the outcome of — it selected
|
|
77
|
-
*
|
|
77
|
+
* The open pass left work no event will announce the outcome of — it selected unknown peers to
|
|
78
|
+
* classify, or a redial it started is still running. Holds the next delay at the base. Not a
|
|
78
79
|
* change: nothing happened yet, and counting it would reset the quiet count on every pass that
|
|
79
|
-
* merely *
|
|
80
|
+
* merely *selected* a peer to probe.
|
|
80
81
|
*/
|
|
81
82
|
notePending(): void {
|
|
82
83
|
this.pending = true;
|
|
@@ -38,6 +38,18 @@ export interface PeerEntry {
|
|
|
38
38
|
* the store only stores and exposes the count; it never branches on it.
|
|
39
39
|
*/
|
|
40
40
|
negotiateFailures: number;
|
|
41
|
+
/**
|
|
42
|
+
* Whether the most recent non-empty protocol list libp2p delivered for this peer (identify,
|
|
43
|
+
* `peer:update`, or the start walk) named none of this network's protocols.
|
|
44
|
+
*
|
|
45
|
+
* Independent of `membership`: a confirmed `member` keeps its label when a later list drops our
|
|
46
|
+
* protocols (that list may be stale), but the flag still records what the peer now advertises.
|
|
47
|
+
* Callers use it, beside a completed `negotiateFailures` run, to stop re-probing an off-ring
|
|
48
|
+
* peer that cannot be serving this network. Not serialized — it is re-derived from the
|
|
49
|
+
* peerStore — so `false` on insert and on import. Like `membership`, the store only stores and
|
|
50
|
+
* exposes it; it never branches on it.
|
|
51
|
+
*/
|
|
52
|
+
advertisedForeign: boolean;
|
|
41
53
|
/**
|
|
42
54
|
* Unix ms of the last handshake failure that was *counted* toward `negotiateFailures`.
|
|
43
55
|
*
|
|
@@ -408,6 +420,7 @@ export class DigitreeStore {
|
|
|
408
420
|
state: 'disconnected',
|
|
409
421
|
membership: 'unknown',
|
|
410
422
|
negotiateFailures: 0,
|
|
423
|
+
advertisedForeign: false,
|
|
411
424
|
lastNegotiateFailureAt: 0,
|
|
412
425
|
contactFailures: 0,
|
|
413
426
|
lastContactFailureAt: 0,
|
|
@@ -702,6 +715,10 @@ export class DigitreeStore {
|
|
|
702
715
|
// too. Reset for the same reason `state` is forced to 'disconnected'.
|
|
703
716
|
negotiateFailures: 0,
|
|
704
717
|
lastNegotiateFailureAt: 0,
|
|
718
|
+
// Re-derived from the peerStore, not restored: the start walk re-applies each peer's
|
|
719
|
+
// protocol list. A table imported *after* start() therefore re-probes a replaced
|
|
720
|
+
// foreign entry until its next `peer:update` — a few cheap pings, accepted.
|
|
721
|
+
advertisedForeign: false,
|
|
705
722
|
// Likewise unreachability: a peer we could not contact before the restart may be
|
|
706
723
|
// reachable now. `state` is already forced to 'disconnected', so resetting the
|
|
707
724
|
// counter alongside it is what stops an imported table from re-killing that peer
|