first commit
This commit is contained in:
22
docs/networking/network/network-plugin-qos/README.md
Normal file
22
docs/networking/network/network-plugin-qos/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# @suaveplan/network-plugin-qos - Bandwidth management and QoS for the mesh: bandwidth-quota enforcer, message-priority classifier, QoS router, congestion monitor
|
||||
|
||||
## Summary
|
||||
|
||||
Bandwidth management and quality-of-service for the mesh: bandwidth-quota enforcer, message-priority classifier, QoS router (bypass / drop / queue / defer), congestion monitor with sustained-period alerts.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [classifier](./src/classifier/message-priority-classifier.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [message-priority-classifier](./src/classifier/message-priority-classifier.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [enforcer](./src/enforcer/bandwidth-quota-enforcer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [bandwidth-quota-enforcer](./src/enforcer/bandwidth-quota-enforcer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [monitor](./src/monitor/congestion-monitor.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [congestion-monitor](./src/monitor/congestion-monitor.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [router](./src/router/qos-router.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [qos-router](./src/router/qos-router.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [error-codes](./src/error-codes.md) — Frozen registry of error codes for the network-plugin-qos plugin. One code covers the wire surface: `QUOTA_EXCEEDED` is…
|
||||
<!-- end:auto-docs --> - [message-priority-classifier](./src/classifier/message-priority-classifier.md) — <!-- begin:auto-docs --> _(no submodules)_- [enforcer](./src/enforcer/bandwidth-quota-enforcer.md) — <!-- begin:auto-docs --> _(no submodules)_- [bandwidth-quota-enforcer](./src/enforcer/bandwidth-quota-enforcer.md) — <!-- begin:auto-docs --> _(no submodules)_- [monitor](./src/monitor/congestion-monitor.md) — <!-- begin:auto-docs --> _(no submodules)_- [congestion-monitor](./src/monitor/congestion-monitor.md) — <!-- begin:auto-docs --> _(no submodules)_- [router](./src/router/qos-router.md) — <!-- begin:auto-docs --> _(no submodules)_- [qos-router](./src/router/qos-router.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Frozen registry of error codes for the network-plugin-qos plugin. One code covers the wire surface: `QUOTA_EXCEEDED` is…
|
||||
- [message-priority-classifier](./src/classifier/message-priority-classifier.md) — <!-- begin:auto-docs --> _(no submodules)_- [enforcer](./src/enforcer/bandwidth-quota-enforcer.md) — <!-- begin:auto-docs --> _(no submodules)_- [bandwidth-quota-enforcer](./src/enforcer/bandwidth-quota-enforcer.md) — <!-- begin:auto-docs --> _(no submodules)_- [monitor](./src/monitor/congestion-monitor.md) — <!-- begin:auto-docs --> _(no submodules)_- [congestion-monitor](./src/monitor/congestion-monitor.md) — <!-- begin:auto-docs --> _(no submodules)_- [router](./src/router/qos-router.md) — <!-- begin:auto-docs --> _(no submodules)_- [qos-router](./src/router/qos-router.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Frozen registry of error codes for the network-plugin-qos plugin. One code covers the wire surface: `QUOTA_EXCEEDED` is…
|
||||
- [message-priority-classifier](./src/classifier/message-priority-classifier.md) — <!-- begin:auto-docs --> _(no submodules)_- [enforcer](./src/enforcer/bandwidth-quota-enforcer.md) — <!-- begin:auto-docs --> _(no submodules)_- [bandwidth-quota-enforcer](./src/enforcer/bandwidth-quota-enforcer.md) — <!-- begin:auto-docs --> _(no submodules)_- [monitor](./src/monitor/congestion-monitor.md) — <!-- begin:auto-docs --> _(no submodules)_- [congestion-monitor](./src/monitor/congestion-monitor.md) — <!-- begin:auto-docs --> _(no submodules)_- [router](./src/router/qos-router.md) — <!-- begin:auto-docs --> _(no submodules)_- [qos-router](./src/router/qos-router.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Frozen registry of error codes for the network-plugin-qos plugin. One code covers the wire surface: `QUOTA_EXCEEDED` is…
|
||||
- [message-priority-classifier](./src/classifier/message-priority-classifier.md) — <!-- begin:auto-docs --> _(no submodules)_- [enforcer](./src/enforcer/bandwidth-quota-enforcer.md) — <!-- begin:auto-docs --> _(no submodules)_ - [bandwidth-quota-enforcer](./src/enforcer/bandwidth-quota-enforcer.md) — <!-- begin:auto-docs --> _(no submodules)_- [monitor](./src/monitor/congestion-monitor.md) — <!-- begin:auto-docs --> _(no submodules)_ - [congestion-monitor](./src/monitor/congestion-monitor.md) — <!-- begin:auto-docs --> _(no submodules)_- [router](./src/router/qos-router.md) — <!-- begin:auto-docs --> _(no submodules)_ - [qos-router](./src/router/qos-router.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Frozen registry of error codes for the network-plugin-qos plugin. One code covers the wire surface: `QUOTA_EXCEEDED` is…
|
||||
@@ -0,0 +1,60 @@
|
||||
# MessagePriorityClassifier
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Walks an envelope through an ordered rule set and returns the first matching `QoSClass`. The result is what `QoSRouter` uses to decide whether to bypass, drop, queue, or defer. Rules are configurable so meshes with different traffic mixes can tune classification without touching the router.
|
||||
|
||||
## Features
|
||||
|
||||
- Four QoS classes: `URGENT`, `CONTROL`, `DATA`, `BULK`.
|
||||
- Each rule can match on `typeMatches` regex, presence of `priorityHeader`, and/or `maxBytes` ceiling.
|
||||
- Default rule set classifies `election:` / `failover:` / `heartbeat:` as `URGENT`, `registry:` / `topology:` / `ack:` as `CONTROL`, `audit:` / `backup:` / `sync:` as `BULK`, everything else as `DATA`.
|
||||
- Callers can replace the rules entirely or just the default class.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { MessagePriorityClassifier } from "@suaveplan/network-plugin-qos";
|
||||
|
||||
const classifier = new MessagePriorityClassifier();
|
||||
const cls = classifier.classify(envelope, payloadBytes);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
MessagePriorityClassifier,
|
||||
type ClassificationRule,
|
||||
} from "@suaveplan/network-plugin-qos";
|
||||
|
||||
const rules: ClassificationRule[] = [
|
||||
{
|
||||
typeMatches: /^game:/,
|
||||
priorityHeader: "x-urgent",
|
||||
classify: "URGENT",
|
||||
},
|
||||
{ typeMatches: /^chat:/, maxBytes: 1024, classify: "CONTROL" },
|
||||
{ typeMatches: /^chat:/, classify: "DATA" },
|
||||
{ typeMatches: /^stream:/, classify: "BULK" },
|
||||
];
|
||||
|
||||
const classifier = new MessagePriorityClassifier({
|
||||
rules,
|
||||
defaultClass: "DATA",
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `type QoSClass = "URGENT" | "CONTROL" | "DATA" | "BULK"`.
|
||||
- `interface ClassificationRule { typeMatches?, priorityHeader?, maxBytes?, classify }`.
|
||||
- `interface MessagePriorityClassifierConfig { rules?, defaultClass? }`.
|
||||
- `classify(envelope, sizeBytes): QoSClass`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Rules are evaluated in registration order with a first-match-wins semantic, which means more specific rules MUST come before catch-alls. We resist the temptation to score-and-rank rules because deterministic ordering is easier to reason about for operators tuning policy. The `priorityHeader` check tests for _presence_ of the header rather than a specific value — applications that need value-conditional priority should encode that via custom `typeMatches` patterns. The default rule set covers the common operational mesh traffic shapes; consumers with non-standard topic prefixes should pass their own `rules` array rather than mutating the defaults at runtime.
|
||||
@@ -0,0 +1,78 @@
|
||||
# BandwidthQuotaEnforcer
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Sliding-window bandwidth-quota enforcer for the QoS plugin. Each peer or path id has its own rolling history; `record(id, bytes)` is the gated write that throws `QUOTA_EXCEEDED` if the new total would breach the configured per-id quota inside the current window. Centralising this in one class means transport-manager, relay, and backbone pipelines all consult the same source of truth for "is this peer over its allocation?".
|
||||
|
||||
## Features
|
||||
|
||||
- Configurable `windowMs` (default 1 s) and `defaultQuotaBytes` (default 1 MiB/s).
|
||||
- Per-id quota override via `setQuota(id, bytes)`.
|
||||
- `usedBytes(id)` exposes current usage for observability.
|
||||
- `assertWithin(id, bytes)` is the dry-run check; `record` does the gated write atomically.
|
||||
- Pluggable `clock` for deterministic tests.
|
||||
- `clear(id?)` for forgetting one or all id histories.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { BandwidthQuotaEnforcer } from "@suaveplan/network-plugin-qos";
|
||||
|
||||
const enforcer = new BandwidthQuotaEnforcer({
|
||||
windowMs: 1000,
|
||||
defaultQuotaBytes: 1_048_576,
|
||||
});
|
||||
|
||||
enforcer.record("alice", 500_000);
|
||||
enforcer.record("alice", 400_000);
|
||||
// next 500_000 would exceed the 1 MiB quota → throws
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
BandwidthQuotaEnforcer,
|
||||
QOS_CODES,
|
||||
QosError,
|
||||
} from "@suaveplan/network-plugin-qos";
|
||||
|
||||
let now = 0;
|
||||
const enforcer = new BandwidthQuotaEnforcer({
|
||||
windowMs: 5_000,
|
||||
clock: () => now,
|
||||
});
|
||||
|
||||
enforcer.setQuota("alice", 200_000);
|
||||
enforcer.record("alice", 100_000);
|
||||
|
||||
try {
|
||||
enforcer.record("alice", 200_000);
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof QosError &&
|
||||
err.code === QOS_CODES.QUOTA_EXCEEDED
|
||||
) {
|
||||
await wait(now + 5_000);
|
||||
// window has rolled, quota is fresh
|
||||
now += 5_001;
|
||||
enforcer.record("alice", 200_000);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface BandwidthQuotaEnforcerConfig { windowMs?, defaultQuotaBytes?, clock? }`.
|
||||
- `setQuota(id, bytes)` / `getQuota(id): number`.
|
||||
- `usedBytes(id): number`.
|
||||
- `assertWithin(id, bytes)` — throws `QUOTA_EXCEEDED` on breach.
|
||||
- `record(id, bytes)` — gated write.
|
||||
- `clear(id?)` — forget one or all id histories.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The history is lazily pruned: `collect(id)` filters out entries older than the window every time it's called, so memory only ever grows up to the active history of recent writes per id. We accept this O(n) per call because n is bounded by the per-window write rate, and the alternative (background cleanup) requires owning a timer the consumer must remember to dispose. `record` calls `assertWithin` first so the check and write are always atomic from the caller's perspective.
|
||||
@@ -0,0 +1,53 @@
|
||||
# QOS_CODES
|
||||
|
||||
## Purpose
|
||||
|
||||
Frozen registry of error codes for the network-plugin-qos plugin. One code covers the wire surface: `QUOTA_EXCEEDED` is thrown by the bandwidth enforcer when a peer or path exceeds its allocation. The router communicates other QoS conditions (drop, defer, queue, bypass) via the `QoSAction` enum on the `QoSDecision` shape rather than thrown errors, because those conditions are normal outcomes that the caller is expected to handle without exception flow. Companion `QosError` extends the cross-package `SuaveplanError` so consumers pattern-match on `code` rather than parse messages.
|
||||
|
||||
## Features
|
||||
|
||||
- Single frozen code registered under the `NET_QOS` namespace.
|
||||
- `QosError extends SuaveplanError<QosErrorCode>`.
|
||||
- `name: "QosError"` on every instance — useful for log filters and dashboard alerts.
|
||||
- `cause` and `context` propagation via `SuaveplanErrorOptions`.
|
||||
- Codes are stable across versions; the registry only ever grows, never shrinks.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { QOS_CODES, QosError } from "@suaveplan/network-plugin-qos";
|
||||
|
||||
throw new QosError(QOS_CODES.QUOTA_EXCEEDED, "qos.quota-exceeded", {
|
||||
context: { peerId: "alice", quotaBytes: 1_000_000 },
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { QOS_CODES, QosError } from "@suaveplan/network-plugin-qos";
|
||||
|
||||
try {
|
||||
enforcer.record("alice", 100_000);
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof QosError &&
|
||||
err.code === QOS_CODES.QUOTA_EXCEEDED
|
||||
) {
|
||||
metrics.counter("qos.quota_breached").increment();
|
||||
await waitForWindowRoll();
|
||||
return enforcer.record("alice", 100_000);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `QOS_CODES` — frozen `{ QUOTA_EXCEEDED }`.
|
||||
- `type QosErrorCode` — union of registered code values.
|
||||
- `class QosError extends SuaveplanError<QosErrorCode>` — typed throwable.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The single-code registry is intentional. Drop, defer, and queue decisions are returned via the `QoSDecision.action` enum because they are routine outcomes; baking them into thrown exceptions would force every send-site to wrap a try-catch. The package barrel side-effect-imports `./error-codes.js` so subpath consumers register the codes too. Future failure modes (e.g. policy violations from operators) should be added as new `NET_QOS.*` codes rather than overloading `QUOTA_EXCEEDED`.
|
||||
@@ -0,0 +1,65 @@
|
||||
# CongestionMonitor
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Tracks `(observed / capacity)` ratios over time and surfaces a stable congested/recovered signal — only flips state once the breach has been sustained for the configured `sustainedMs`. This avoids a single noisy spike triggering bulk-drops or aggressive defers; the `QoSRouter` consults `isCongested()` to make routing decisions.
|
||||
|
||||
## Features
|
||||
|
||||
- Configurable `thresholdRatio` (default 0.8) and `sustainedMs` (default 5 s).
|
||||
- `observe(observed, capacity)` is the only ingestion point — no internal sampling, no leaked timers.
|
||||
- State flips only after the breach has been sustained.
|
||||
- Listeners fire on every `congested` / `recovered` transition; unsubscribe-returning `on()`.
|
||||
- Pluggable `clock` for deterministic tests.
|
||||
- `reset()` snaps back to "not congested" and fires a `recovered` event if applicable.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { CongestionMonitor } from "@suaveplan/network-plugin-qos";
|
||||
|
||||
const monitor = new CongestionMonitor();
|
||||
monitor.observe(1_500_000, 1_000_000); // 150% — breach
|
||||
|
||||
if (monitor.isCongested()) {
|
||||
pauseBulkTraffic();
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { CongestionMonitor } from "@suaveplan/network-plugin-qos";
|
||||
|
||||
let now = 0;
|
||||
const monitor = new CongestionMonitor({
|
||||
thresholdRatio: 0.7,
|
||||
sustainedMs: 1_000,
|
||||
clock: () => now,
|
||||
});
|
||||
|
||||
monitor.on((state) => {
|
||||
metrics.gauge("qos.congested", state === "congested" ? 1 : 0);
|
||||
});
|
||||
|
||||
for (const window of windows) {
|
||||
now = window.timestamp;
|
||||
monitor.observe(window.bytes, window.capacity);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface CongestionMonitorConfig { thresholdRatio?, sustainedMs?, clock? }`.
|
||||
- `type CongestionListener = (state: "congested" | "recovered") => void`.
|
||||
- `observe(observed, capacity)` — ingestion.
|
||||
- `isCongested(): boolean`.
|
||||
- `on(listener) → () => void`.
|
||||
- `reset()` — clears state and fires `recovered` if currently congested.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The `breachStart` watermark is captured on the first breach observation and cleared the first time the ratio drops back under threshold. Sustained-state requires the breach to persist _continuously_ — any single recovery resets the timer. `capacity === 0` is treated as 100% utilisation rather than producing `NaN`, because the sane interpretation of "no capacity" is "definitely congested". The monitor does not own a clock-driven loop — consumers MUST call `observe` whenever they have a fresh reading.
|
||||
@@ -0,0 +1,82 @@
|
||||
# QoSRouter
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Decides what the surrounding transport pipeline should do with an envelope: send it (with quota record), bypass quota for urgent traffic, queue control traffic when quota is breached, defer data traffic for retry, or drop bulk under congestion. Combines the `MessagePriorityClassifier` (what kind of traffic is this?) with the `BandwidthQuotaEnforcer` (do we have headroom?) into one policy decision.
|
||||
|
||||
## Features
|
||||
|
||||
- Five actions: `bypass`, `send`, `queue`, `drop`, `defer`.
|
||||
- `URGENT` traffic always bypasses the quota.
|
||||
- `BULK` traffic drops under congestion.
|
||||
- `CONTROL` quota breaches → `queue`; everything else → `defer`.
|
||||
- Pluggable `congested` predicate so `CongestionMonitor` (or any external observer) can drive the bypass/drop logic.
|
||||
- `commitSend(pathId, sizeBytes)` records the send against the quota only after the consumer has actually transmitted.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
BandwidthQuotaEnforcer,
|
||||
MessagePriorityClassifier,
|
||||
QoSRouter,
|
||||
} from "@suaveplan/network-plugin-qos";
|
||||
|
||||
const router = new QoSRouter({
|
||||
classifier: new MessagePriorityClassifier(),
|
||||
enforcer: new BandwidthQuotaEnforcer(),
|
||||
});
|
||||
|
||||
const decision = router.decide(envelope, payloadSize, "alice→bob");
|
||||
if (decision.action === "send") {
|
||||
await transport.send(envelope);
|
||||
router.commitSend("alice→bob", payloadSize);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { QoSRouter } from "@suaveplan/network-plugin-qos";
|
||||
|
||||
const monitor = new CongestionMonitor({ threshold: 0.8 });
|
||||
const router = new QoSRouter({
|
||||
classifier,
|
||||
enforcer,
|
||||
congested: () => monitor.isCongested(),
|
||||
});
|
||||
|
||||
const decision = router.decide(envelope, sizeBytes, pathId);
|
||||
switch (decision.action) {
|
||||
case "bypass":
|
||||
return forceSend(envelope);
|
||||
case "send":
|
||||
await transport.send(envelope);
|
||||
router.commitSend(pathId, sizeBytes);
|
||||
return;
|
||||
case "queue":
|
||||
controlQueue.push(envelope);
|
||||
return;
|
||||
case "defer":
|
||||
deferQueue.push({ envelope, retryAfterMs: 100 });
|
||||
return;
|
||||
case "drop":
|
||||
metrics.counter("qos.dropped").increment();
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `type QoSAction = "bypass" | "send" | "queue" | "drop" | "defer"`.
|
||||
- `interface QoSDecision { action, class, reason? }`.
|
||||
- `interface QoSRouterConfig { classifier, enforcer, congested? }`.
|
||||
- `decide(envelope, sizeBytes, pathId): QoSDecision`.
|
||||
- `commitSend(pathId, sizeBytes)` — records bytes against the quota.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`decide()` does not record the send — `commitSend` is split out so the consumer only counts bytes that actually went on the wire. This avoids double-counting when the consumer's transport has its own retry logic. The router throws non-`QUOTA_EXCEEDED` errors from the enforcer through unchanged, on the assumption that a different `QosErrorCode` indicates a programmer error rather than a recoverable QoS condition.
|
||||
Reference in New Issue
Block a user