add grouped retry orchestration, duplicate arbitration hardening, and transport-aware peer scoring expand IPC with readiness gates, soft backpressure, request deadlines, batching, and method circuit-breakers strengthen worker/control-plane lifecycle via idempotent termination, orphan detection, and liveness checkpoint helpers improve lifecycle correctness with state-machine + transactional hook infrastructure and adaptive quiesce utilities enrich operator surfaces/tests for degraded readiness, crash-safe telemetry patterns, and contract-level regressions
211 lines
5.8 KiB
JavaScript
211 lines
5.8 KiB
JavaScript
/**
|
||
* Swarm connection manager: delegates to {@link BareOsSwarmPeerPolicyEngine} for
|
||
* peer health, scoring, backoff/ban windows, and replication scheduling hints.
|
||
*/
|
||
|
||
import { BareOsSwarmPeerPolicyEngine } from './bare-os-swarm-peer-policy.js'
|
||
|
||
export class BareOsSwarmConnectionManager {
|
||
/**
|
||
* @param {import('hyperswarm').default | null} swarm
|
||
* @param {{ env?: Record<string, string | undefined> | null }} [opts]
|
||
*/
|
||
constructor(swarm, opts = {}) {
|
||
this._engine = new BareOsSwarmPeerPolicyEngine(swarm, opts)
|
||
/** Protomux RPC pool–style reuse counter (call {@link recordRpcPoolReuse} from hot paths). */
|
||
this._rpcPoolReuse = 0
|
||
/** @type {Map<string, { openedAtMs: number, lastActivityAtMs: number, initiator: boolean }>} */
|
||
this._activeConnections = new Map()
|
||
this._duplicateDrops = 0
|
||
/** @type {Map<'short'|'medium'|'long'|'xlong', Set<string>>} */
|
||
this._retryBuckets = new Map([
|
||
['short', new Set()],
|
||
['medium', new Set()],
|
||
['long', new Set()],
|
||
['xlong', new Set()]
|
||
])
|
||
this._bucketTimer = null
|
||
this._retryDequeued = 0
|
||
}
|
||
|
||
/** Increment when a logical RPC channel is reused instead of opened fresh. */
|
||
recordRpcPoolReuse() {
|
||
this._rpcPoolReuse++
|
||
}
|
||
|
||
get swarm() {
|
||
return this._engine.swarm
|
||
}
|
||
|
||
/**
|
||
* @param {string} peerKey
|
||
* @param {boolean} ok
|
||
* @param {{ latencyMs?: number }} [probeMeta]
|
||
*/
|
||
noteProbe(peerKey, ok, probeMeta) {
|
||
this._engine.noteProbe(peerKey, ok, probeMeta)
|
||
}
|
||
|
||
/**
|
||
* @param {string} peerKey
|
||
* @param {{ dhtAddressClass?: string } | null | undefined} [meta]
|
||
*/
|
||
shouldAttemptPeer(peerKey, meta) {
|
||
return this._engine.shouldAttemptPeer(peerKey, meta)
|
||
}
|
||
|
||
/**
|
||
* @param {string} peerKey
|
||
*/
|
||
consumeReconnectBudget(peerKey) {
|
||
this._engine.consumeReconnectBudget(peerKey)
|
||
}
|
||
|
||
/**
|
||
* @param {string} peerKey
|
||
* @param {number} [amount]
|
||
*/
|
||
replenishReconnectBudget(peerKey, amount) {
|
||
this._engine.replenishReconnectBudget(peerKey, amount)
|
||
}
|
||
|
||
/**
|
||
* @param {string} peerKey
|
||
*/
|
||
score(peerKey) {
|
||
return this._engine.score(peerKey)
|
||
}
|
||
|
||
snapshot() {
|
||
const base = this._engine.snapshot()
|
||
return {
|
||
...base,
|
||
protomuxPoolMetrics: {
|
||
schema: 1,
|
||
rpcPoolReuseCount: this._rpcPoolReuse,
|
||
note: 'Aligns with protomux-rpc-client-pool-style accounting; stock booter increments only when callers invoke recordRpcPoolReuse.'
|
||
},
|
||
duplicateArbitration: {
|
||
schema: 1,
|
||
activeConnectionCount: this._activeConnections.size,
|
||
duplicateDrops: this._duplicateDrops,
|
||
note: 'Deterministic tie-break: keep older open; when equal age keep initiator.'
|
||
},
|
||
groupedRetry: {
|
||
schema: 1,
|
||
queued: {
|
||
short: this._retryBuckets.get('short')?.size || 0,
|
||
medium: this._retryBuckets.get('medium')?.size || 0,
|
||
long: this._retryBuckets.get('long')?.size || 0,
|
||
xlong: this._retryBuckets.get('xlong')?.size || 0
|
||
},
|
||
dequeuedTotal: this._retryDequeued
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {string[]} peerKeys
|
||
*/
|
||
rankPeers(peerKeys) {
|
||
return this._engine.rankPeers(peerKeys)
|
||
}
|
||
|
||
/**
|
||
* Deterministic duplicate arbitration. Returns true when caller should keep the incoming edge.
|
||
* @param {string} peerKey
|
||
* @param {{ initiator?: boolean }} [incoming]
|
||
*/
|
||
shouldAcceptConnection(peerKey, incoming = {}) {
|
||
const key = String(peerKey || '').slice(0, 128) || 'unknown'
|
||
const now = Date.now()
|
||
const cur = this._activeConnections.get(key)
|
||
if (!cur) return true
|
||
const incomingInitiator = incoming.initiator === true
|
||
if (now - cur.lastActivityAtMs <= 15000) {
|
||
this._duplicateDrops++
|
||
return false
|
||
}
|
||
if (cur.initiator && !incomingInitiator) {
|
||
this._duplicateDrops++
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
/**
|
||
* @param {string} peerKey
|
||
* @param {{ initiator?: boolean }} [meta]
|
||
*/
|
||
markConnectionOpen(peerKey, meta = {}) {
|
||
const key = String(peerKey || '').slice(0, 128) || 'unknown'
|
||
const now = Date.now()
|
||
this._activeConnections.set(key, {
|
||
openedAtMs: now,
|
||
lastActivityAtMs: now,
|
||
initiator: meta.initiator === true
|
||
})
|
||
}
|
||
|
||
/**
|
||
* @param {string} peerKey
|
||
*/
|
||
markConnectionClosed(peerKey) {
|
||
const key = String(peerKey || '').slice(0, 128) || 'unknown'
|
||
this._activeConnections.delete(key)
|
||
}
|
||
|
||
/**
|
||
* @param {string} peerKey
|
||
*/
|
||
markConnectionActivity(peerKey) {
|
||
const key = String(peerKey || '').slice(0, 128) || 'unknown'
|
||
const row = this._activeConnections.get(key)
|
||
if (!row) return
|
||
row.lastActivityAtMs = Date.now()
|
||
}
|
||
|
||
/**
|
||
* Queue peer for grouped retry scheduling.
|
||
* @param {string} peerKey
|
||
* @param {'short'|'medium'|'long'|'xlong'} [tier]
|
||
*/
|
||
queueRetry(peerKey, tier = 'short') {
|
||
const k = String(peerKey || '').slice(0, 128) || 'unknown'
|
||
const t = this._retryBuckets.get(tier)
|
||
if (!t) return
|
||
t.add(k)
|
||
if (this._bucketTimer) return
|
||
this._bucketTimer = setInterval(() => {
|
||
for (const bucket of this._retryBuckets.values()) {
|
||
const first = bucket.values().next()
|
||
if (!first.done) {
|
||
bucket.delete(first.value)
|
||
this._retryDequeued++
|
||
break
|
||
}
|
||
}
|
||
const allEmpty = [...this._retryBuckets.values()].every((b) => b.size === 0)
|
||
if (allEmpty && this._bucketTimer) {
|
||
clearInterval(this._bucketTimer)
|
||
this._bucketTimer = null
|
||
}
|
||
}, 50)
|
||
}
|
||
|
||
/**
|
||
* Trigger synthetic keepalive probe accounting for all active peers.
|
||
* @param {boolean} ok
|
||
* @param {{ latencyMs?: number }} [meta]
|
||
*/
|
||
emitKeepaliveProbeSweep(ok, meta = {}) {
|
||
for (const key of this._activeConnections.keys()) {
|
||
this.noteProbe(key, ok, {
|
||
latencyMs: meta.latencyMs,
|
||
failClass: ok ? undefined : 'timeout',
|
||
transportClass: 'tcp'
|
||
})
|
||
}
|
||
}
|
||
}
|