Files
suaveplan/openspec/changes/archive/2026-05-04-network-plugin-assignment/proposal.md
2026-05-27 23:29:53 +01:00

204 lines
7.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.
```ts
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.
```ts
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.
```ts
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.
```ts
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
```ts
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.