Files
2026-05-27 23:29:53 +01:00

7.2 KiB
Raw Permalink Blame History

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 — MeshCapacityTracker tracks currentPeers / maxPeers per mesh from registry events.
  • NET-72 — AssignmentEngine scores candidate meshes using latency, capacity, load, region, and tier fit.
  • NET-73 — AssignmentEngine returns null when no suitable mesh is found.
  • NET-74 — MeshBalancer detects imbalance and generates RebalancePlan objects.
  • NET-75 — AdmissionController rejects peers when global capacity is exceeded.
  • NET-76 — AdmissionController returns ranked alternative meshes when the primary mesh is full.
  • NET-77 — Rebalancing triggers network-plugin-migration for peer state transfer.

Out of scope

  • Global DHT for mesh discovery → handled by network-plugin-relay MeshDirectory.
  • Forced peer eviction → v2; v1 only does cooperative rebalancing.
  • Real-time load metrics (CPU/bandwidth) → consumes from network-plugin-monitor when available; v1 uses peer count as load proxy.