7.2 KiB
OpenSpec Change: network-plugin-assignment
- Change ID: network-plugin-assignment
- Package:
@suaveplan/network-plugin-assignment(new) - Status: PROPOSED
- Date: 2026-05-01
Summary
Implement mesh auto-assignment and load balancing. When a new peer connects to the system, there is currently no algorithm to decide which mesh it should join. This plugin provides the "placement layer": tracking mesh capacity across the fleet, scoring candidate meshes for a new peer, rebalancing peers when meshes become overloaded, and admitting or rejecting connections based on global capacity.
This is a P0 gap blocking the user's stated requirement: "automatically place a new connection in a tiered mesh with available spaces."
Why
User requirement: "automatically place a new connection in a tiered mesh with available spaces"
Current state: enterprise-network-assignment (dated 2026-04-24) provides consistent hashing and rendezvous hashing for peer selection within a mesh. It does NOT assign a new peer to a mesh.
What's missing:
- Mesh capacity tracking across the fleet
- Algorithm for placing a new peer into the "best" mesh (latency, geography, capacity, load)
- Rebalancing when meshes become imbalanced
- Handling of "mesh full" scenarios
Core components
1. MeshCapacityTracker
Consumes peer:joined / peer:left / peer:updated events from network-plugin-registry to maintain a live view of currentPeers / maxPeers per mesh.
class MeshCapacityTracker {
recordMesh(meshId: string, maxPeers: number): void;
recordJoin(meshId: string, peerId: string): void;
recordLeave(meshId: string, peerId: string): void;
getUtilization(meshId: string): number; // 0.0 – 1.0
getAvailableSlots(meshId: string): number;
listMeshes(): string[];
on(event: "mesh:full" | "mesh:available", handler: (meshId: string) => void): void;
}
2. AssignmentEngine
Scores candidate meshes for a new peer and returns the best assignment.
interface PeerInfo {
peerId: string;
latencyMap: Map<string, number>; // meshId → RTT ms
region?: string;
tierEligibility: number; // max tier this peer can reach (0, 1, 2)
capabilities: DeviceProfile; // CONSTRAINED, STANDARD, POWERFUL, SERVER
}
interface AssignmentScore {
meshId: string;
score: number; // higher is better
factors: {
latencyScore: number;
capacityScore: number;
loadScore: number;
regionScore: number;
tierFitScore: number;
};
}
class AssignmentEngine {
constructor(options: {
tracker: MeshCapacityTracker;
weights: AssignmentWeights; // configurable
minScoreThreshold: number;
});
assign(peer: PeerInfo): AssignmentScore | null; // null = no suitable mesh
rankCandidates(peer: PeerInfo): AssignmentScore[];
}
Scoring factors (weights sum to 1.0, configurable):
- Latency (default 0.30): lower RTT to mesh host = higher score
- Capacity (default 0.25): more available slots = higher score
- Load (default 0.20): lower current CPU/bandwidth load = higher score
- Region (default 0.15): same region as peer = higher score
- Tier fit (default 0.10): mesh needs peers of this peer's capability level
Fallback: If all meshes are full, the engine returns null. The caller (AdmissionController) may then trigger mesh creation or reject the peer.
3. MeshBalancer
Periodic rebalancer that moves peers from overloaded meshes to underloaded ones.
class MeshBalancer {
constructor(options: {
tracker: MeshCapacityTracker;
imbalanceThreshold: number; // e.g. 0.20 = 20% variance triggers rebalance
rebalanceIntervalMs: number;
maxMovesPerCycle: number;
});
start(): void;
stop(): void;
evaluate(): RebalancePlan[];
executePlan(plan: RebalancePlan): Promise<void>;
}
interface RebalancePlan {
peerId: string;
fromMeshId: string;
toMeshId: string;
reason: string;
}
Rebalancing is cooperative: the target mesh must accept the peer, and the peer must confirm the move. Uses network-plugin-migration for the actual state transfer.
4. AdmissionController
Gatekeeper that accepts or rejects incoming peer connections.
class AdmissionController {
constructor(options: {
engine: AssignmentEngine;
balancer: MeshBalancer;
allowMeshCreation: boolean; // if true, creates new mesh when all full
globalMaxPeers: number;
});
admit(peer: PeerInfo): AdmissionResult;
}
interface AdmissionResult {
accepted: boolean;
meshId?: string;
reason?: string;
alternativeMeshIds?: string[]; // ranked alternatives if primary is full
}
Error codes
const ASSIGN_CODES = defineCodes("ASSIGN", [
"MESH_FULL",
"NO_MESH_AVAILABLE",
"GLOBAL_CAPACITY_EXCEEDED",
"PEER_REJECTED",
"REBALANCE_FAILED",
"TRACKER_SYNC_ERROR",
"INVALID_PEER_INFO",
] as const);
AssignmentError extends SuaveplanError<AssignmentErrorCode>.
Dependencies
- Consumes:
@suaveplan/network(NetworkNode,NetworkBus),@suaveplan/network-plugin-registry(peer events for capacity tracking),@suaveplan/network-plugin-topology(tier eligibility, region data),@suaveplan/network-plugin-relay(cross-mesh routing for rebalanced peers),@suaveplan/network-plugin-migration(state transfer during rebalance),@suaveplan/error,@suaveplan/types,@opentelemetry/api. - Blocked on:
network-core(shipped),network-plugin-registry,network-plugin-topology.
Verification approach
src/tracker/tracker.spec-conformance.test.ts— pins capacity tracking, utilization math, full/available events.src/engine/engine.spec-conformance.test.ts— pins scoring algorithm, weights, fallback behaviour, tier fit.src/balancer/balancer.spec-conformance.test.ts— pins imbalance detection, plan generation, maxMovesPerCycle.src/controller/controller.spec-conformance.test.ts— pins admit/reject paths, global capacity enforcement.tests/integration/assignment.spec-conformance.test.ts— end-to-end: 3 meshes, 10 peers, verify balanced distribution.- Property-based test: random peer latencies/regions/capabilities → assignment is deterministic for same inputs.
- 100/100/100/100 Istanbul coverage.
Conformance requirements (proposed)
NET-71—MeshCapacityTrackertrackscurrentPeers / maxPeersper mesh from registry events.NET-72—AssignmentEnginescores candidate meshes using latency, capacity, load, region, and tier fit.NET-73—AssignmentEnginereturnsnullwhen no suitable mesh is found.NET-74—MeshBalancerdetects imbalance and generatesRebalancePlanobjects.NET-75—AdmissionControllerrejects peers when global capacity is exceeded.NET-76—AdmissionControllerreturns ranked alternative meshes when the primary mesh is full.NET-77— Rebalancing triggersnetwork-plugin-migrationfor peer state transfer.
Out of scope
- Global DHT for mesh discovery → handled by
network-plugin-relayMeshDirectory. - Forced peer eviction → v2; v1 only does cooperative rebalancing.
- Real-time load metrics (CPU/bandwidth) → consumes from
network-plugin-monitorwhen available; v1 uses peer count as load proxy.