179 lines
6.9 KiB
Markdown
179 lines
6.9 KiB
Markdown
# OpenSpec Change: network-plugin-migration
|
|
|
|
- **Change ID**: network-plugin-migration
|
|
- **Package**: `@suaveplan/network-plugin-migration` (new)
|
|
- **Status**: PROPOSED
|
|
- **Date**: 2026-05-01
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
Implement peer state migration between meshes. When `network-plugin-assignment` rebalances a peer from an overloaded mesh to an underloaded one, or when a peer manually requests to join a different mesh, there is currently no mechanism to transfer state or maintain message continuity during the handoff. This plugin provides the migration protocol: state snapshot transfer, graceful handoff with a forwarding tunnel, and session persistence across mesh boundaries.
|
|
|
|
---
|
|
|
|
## Why
|
|
|
|
**User requirement**: *"automatically place a new connection in a tiered mesh with available spaces"* implies rebalancing, which implies migration.
|
|
|
|
**Current state**: No migration logic exists anywhere in the network suite.
|
|
|
|
**What's missing**:
|
|
|
|
- State transfer when a peer moves from Mesh A to Mesh B
|
|
- Graceful handoff: old host forwards messages to new host during transition
|
|
- Session persistence across mesh boundaries
|
|
|
|
---
|
|
|
|
## Core components
|
|
|
|
### 1. `MigrationCoordinator`
|
|
|
|
Orchestrates the end-to-end migration flow.
|
|
|
|
```ts
|
|
class MigrationCoordinator {
|
|
constructor(options: {
|
|
localPeerId: string;
|
|
localMeshId: string;
|
|
bus: NetworkBus;
|
|
});
|
|
initiateMigration(request: MigrationRequest): Promise<MigrationResult>;
|
|
acceptMigration(request: MigrationRequest): Promise<MigrationResult>;
|
|
}
|
|
|
|
interface MigrationRequest {
|
|
peerId: string;
|
|
fromMeshId: string;
|
|
toMeshId: string;
|
|
reason: string; // e.g. "rebalance", "user-request", "region-optimization"
|
|
priority: "graceful" | "urgent";
|
|
}
|
|
|
|
interface MigrationResult {
|
|
success: boolean;
|
|
peerId: string;
|
|
newMeshId: string;
|
|
transferredBytes: number;
|
|
durationMs: number;
|
|
forwardedMessages: number;
|
|
}
|
|
```
|
|
|
|
**Migration flow**:
|
|
|
|
1. **Request** — `MigrationCoordinator` sends `migration:request` to the target mesh's assignment controller.
|
|
2. **Accept** — Target mesh admits the peer via `AdmissionController`.
|
|
3. **Snapshot** — Old host serializes peer state (registry entry, topology position, session memberships) and sends it to the new host.
|
|
4. **Transfer** — New host installs the peer into its local registry and topology.
|
|
5. **Tunnel** — Old host starts a forwarding tunnel for messages addressed to the migrating peer.
|
|
6. **Cutover** — Peer disconnects from old mesh and connects to new mesh.
|
|
7. **Drain** — Old host drains the forwarding tunnel (default 30s or until empty).
|
|
8. **Complete** — Old host removes peer from its registry. `migration:completed` is emitted on both meshes' buses.
|
|
|
|
### 2. `StateTransferProtocol`
|
|
|
|
Serializes and deserializes peer state for migration.
|
|
|
|
```ts
|
|
class StateTransferProtocol {
|
|
serialize(peerId: string, registry: RegistrySnapshot, topology: TopologyPosition, sessions: SessionMembership[]): Uint8Array;
|
|
deserialize(bytes: Uint8Array): PeerStateSnapshot;
|
|
}
|
|
|
|
interface PeerStateSnapshot {
|
|
peerId: string;
|
|
registryEntry: PeerRegistryEntry;
|
|
topologyPosition: TopologyPosition;
|
|
sessionMemberships: SessionMembership[];
|
|
version: number; // monotonic, for conflict detection
|
|
}
|
|
```
|
|
|
|
Uses `@suaveplan/bitpacker` for compact binary encoding. Max snapshot size: 64 KiB (peers with larger state must use lazy rehydration).
|
|
|
|
### 3. `ForwardingTunnel`
|
|
|
|
Temporary message forwarding from old host to new host during cutover.
|
|
|
|
```ts
|
|
class ForwardingTunnel {
|
|
constructor(options: {
|
|
fromHostId: string;
|
|
toHostId: string;
|
|
peerId: string;
|
|
relay: RelayManager; // from network-plugin-relay
|
|
drainTimeoutMs: number; // default 30000
|
|
maxBufferedMessages: number; // default 1000
|
|
});
|
|
open(): void;
|
|
close(): Promise<void>;
|
|
forward(envelope: MessageEnvelope): void;
|
|
getBufferedCount(): number;
|
|
}
|
|
```
|
|
|
|
**Behaviour**:
|
|
|
|
- All messages addressed to `peerId` that arrive at the old host are forwarded to the new host via `RelayManager`.
|
|
- If the buffer exceeds `maxBufferedMessages`, oldest messages are dropped (application is notified).
|
|
- After `drainTimeoutMs`, the tunnel is closed regardless of buffer state.
|
|
- `ForwardingTunnel` is only used for `graceful` migrations. `urgent` migrations skip the tunnel and accept message loss.
|
|
|
|
---
|
|
|
|
## Error codes
|
|
|
|
```ts
|
|
const MIGRATE_CODES = defineCodes("MIGRATE", [
|
|
"TARGET_MESH_REJECTED",
|
|
"STATE_TRANSFER_FAILED",
|
|
"TUNNEL_OVERFLOW",
|
|
"TUNNEL_TIMEOUT",
|
|
"CUTOVER_FAILED",
|
|
"MIGRATION_ABORTED",
|
|
"INVALID_SNAPSHOT",
|
|
] as const);
|
|
```
|
|
|
|
`MigrationError extends SuaveplanError<MigrationErrorCode>`.
|
|
|
|
---
|
|
|
|
## Dependencies
|
|
|
|
- **Consumes**: `@suaveplan/network` (`NetworkNode`, `NetworkBus`), `@suaveplan/network-plugin-relay` (`RelayManager`, cross-mesh forwarding), `@suaveplan/network-plugin-assignment` (`AdmissionController`, `MeshBalancer`), `@suaveplan/network-plugin-registry` (`RegistrySnapshot`), `@suaveplan/network-plugin-topology` (`TopologyPosition`), `@suaveplan/network-plugin-session` (`SessionMembership`), `@suaveplan/bitpacker` (snapshot encoding), `@suaveplan/error`, `@suaveplan/types`, `@opentelemetry/api`.
|
|
- **Blocked on**: `network-core` (shipped), `network-plugin-relay`, `network-plugin-assignment`, `network-plugin-registry`, `network-plugin-topology`, `network-plugin-session`.
|
|
|
|
---
|
|
|
|
## Verification approach
|
|
|
|
- `src/coordinator/coordinator.spec-conformance.test.ts` — pins full migration flow: request → accept → snapshot → transfer → tunnel → cutover → drain.
|
|
- `src/state-transfer/state-transfer.spec-conformance.test.ts` — pins serialize/deserialize roundtrip, version tracking, size limits.
|
|
- `src/tunnel/forwarding-tunnel.spec-conformance.test.ts` — pins buffer limits, overflow drop, drain timeout.
|
|
- `tests/integration/migration.spec-conformance.test.ts` — end-to-end: peer moves from Mesh A to Mesh B, messages continue to arrive.
|
|
- 100/100/100/100 Istanbul coverage.
|
|
|
|
---
|
|
|
|
## Conformance requirements (proposed)
|
|
|
|
- `NET-99` — `MigrationCoordinator` executes a 8-step migration flow (request, accept, snapshot, transfer, tunnel, cutover, drain, complete).
|
|
- `NET-100` — `StateTransferProtocol` serializes peer registry, topology, and session state using `@suaveplan/bitpacker`.
|
|
- `NET-101` — `ForwardingTunnel` buffers messages for the migrating peer and forwards them to the new host.
|
|
- `NET-102` — `ForwardingTunnel` drops oldest messages when buffer exceeds `maxBufferedMessages`.
|
|
- `NET-103` — `ForwardingTunnel` closes after `drainTimeoutMs` regardless of buffer state.
|
|
- `NET-104` — `urgent` migrations skip the forwarding tunnel and accept message loss.
|
|
- `NET-105` — `migration:completed` is emitted on both the source and target mesh buses.
|
|
|
|
---
|
|
|
|
## Out of scope
|
|
|
|
- **Live migration without disconnect** → v2; v1 requires a brief disconnect/cutover.
|
|
- **Migration of in-flight game state** → application-level; v1 transfers session memberships only.
|
|
- **Rollback on failure** → v2; v1 reports failure and leaves application to retry.
|