@norskvideo/norsk-auto-manager 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +344 -0
- package/lib/src/automanager.d.ts +1070 -0
- package/lib/src/automanager.js +2746 -0
- package/lib/src/clock.d.ts +14 -0
- package/lib/src/clock.js +21 -0
- package/lib/src/conversions.d.ts +59 -0
- package/lib/src/conversions.js +416 -0
- package/lib/src/index.d.ts +9 -0
- package/lib/src/index.js +37 -0
- package/lib/src/inventory.d.ts +34 -0
- package/lib/src/inventory.js +64 -0
- package/lib/src/knownCapabilities.d.ts +18 -0
- package/lib/src/knownCapabilities.js +22 -0
- package/lib/src/placement.d.ts +268 -0
- package/lib/src/placement.js +468 -0
- package/lib/src/settingsValidation.d.ts +34 -0
- package/lib/src/settingsValidation.js +232 -0
- package/lib/src/shared/utils.d.ts +17 -0
- package/lib/src/shared/utils.js +220 -0
- package/lib/src/types.d.ts +295 -0
- package/lib/src/types.js +20 -0
- package/lib/src/validation.d.ts +68 -0
- package/lib/src/validation.js +198 -0
- package/package.json +66 -0
- package/src/automanager.ts +3570 -0
- package/src/clock.ts +29 -0
- package/src/conversions.ts +458 -0
- package/src/index.ts +27 -0
- package/src/inventory.ts +64 -0
- package/src/knownCapabilities.ts +23 -0
- package/src/placement.ts +804 -0
- package/src/settingsValidation.ts +386 -0
- package/src/types.ts +346 -0
- package/src/validation.ts +297 -0
- package/tsconfig.json +25 -0
package/README.md
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
# `@norskvideo/norsk-auto-manager`
|
|
2
|
+
|
|
3
|
+
The opinionated orchestration layer above Norsk Manager. Manager exposes a
|
|
4
|
+
mechanical API — start node, terminate node, register job, report events —
|
|
5
|
+
without policy. AutoManager owns policy: where jobs run, how nodes are sized
|
|
6
|
+
and packed, how failures recover, when hot spares are provisioned.
|
|
7
|
+
|
|
8
|
+
This README is the conceptual guide. The TSDoc comments on each public
|
|
9
|
+
symbol are the authoritative API reference; run `npx tsc --declaration` or
|
|
10
|
+
read `lib/src/automanager.d.ts` for the full signatures.
|
|
11
|
+
|
|
12
|
+
For the design rationale, see `.ai/steve/tasks/37-auto-manager-design.md`
|
|
13
|
+
in the repo root.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { AutoManager, PlacementPool } from "@norskvideo/norsk-auto-manager";
|
|
21
|
+
|
|
22
|
+
const awsEast: PlacementPool = {
|
|
23
|
+
name: "aws-east",
|
|
24
|
+
kind: "aws",
|
|
25
|
+
region: "eu-west-1",
|
|
26
|
+
packingStrategy: "binpack",
|
|
27
|
+
scaleOut: "elastic",
|
|
28
|
+
candidateInstanceTypes: [
|
|
29
|
+
{ instanceType: "m5.xlarge", totalCapacity: 100, totalCores: 16, capabilities: [] },
|
|
30
|
+
],
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const [autoManager, runPromise] = await AutoManager.run({
|
|
34
|
+
url: "manager.example.com:6789",
|
|
35
|
+
pendingWindow: 60,
|
|
36
|
+
removeStoppedNodesAfter: 3600,
|
|
37
|
+
placementPools: [awsEast],
|
|
38
|
+
|
|
39
|
+
// Recommended callbacks for ops tooling:
|
|
40
|
+
onPlacementDecision: (info) => console.log("placement", info),
|
|
41
|
+
onBundleBroken: (id, reason) => console.warn("bundle broken", id, reason),
|
|
42
|
+
onConnectionStateChange: (state) => console.log("manager", state),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// AutoManager runs in the background. `runPromise` resolves when the
|
|
46
|
+
// event-stream loop exits (e.g. after `autoManager.close()`).
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The `AutoManager.run` call validates the settings up front (throws
|
|
50
|
+
`AutoSettingsValidationError` on shape/typo problems) and connects to
|
|
51
|
+
Manager. Once the event stream is established, AutoManager mirrors
|
|
52
|
+
Manager's state (jobs, nodes, bundles, inventories) and starts running
|
|
53
|
+
placement decisions.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Bundles
|
|
58
|
+
|
|
59
|
+
A **bundle** is the unit of work you submit. It has one or more **replicas**
|
|
60
|
+
(1 = no backup, 2 = primary + backup, N>2 = N-way redundancy). Each replica
|
|
61
|
+
contains one or more **jobs** — the atomic placeable units.
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { Bundle } from "@norskvideo/norsk-auto-manager";
|
|
65
|
+
|
|
66
|
+
const bundle: Bundle = {
|
|
67
|
+
bundleId: "my-channel-1",
|
|
68
|
+
replicas: 2, // primary + backup
|
|
69
|
+
pools: ["aws-east", "aws-west"], // preference order; spills if first is full
|
|
70
|
+
jobs: [
|
|
71
|
+
{
|
|
72
|
+
jobName: "encode",
|
|
73
|
+
requirements: {
|
|
74
|
+
requiredCapacity: 8, // benchmarked reference-workload units
|
|
75
|
+
requiredCores: 4, // explicit cpuset pinning
|
|
76
|
+
requiredCapabilities: [], // hardware/software requirements
|
|
77
|
+
priorityBand: "gold", // see "Priority bands" below
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
await autoManager.createBundle(bundle);
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Submit a bundle by calling `autoManager.createBundle(bundle)`. Manager
|
|
87
|
+
expands it into one runtime Job per replica, AutoManager runs placement
|
|
88
|
+
for each, and the workflow begins.
|
|
89
|
+
|
|
90
|
+
### Placement preferences
|
|
91
|
+
|
|
92
|
+
`pools` is an ordered preference list. Placement tries the first pool; if
|
|
93
|
+
it can't satisfy the job there, it falls through to the next. Each replica
|
|
94
|
+
can override the bundle default via `replicaOverrides`:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
replicaOverrides: { 1: { pools: ["oci-fra", "aws-east"] } }
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
This is the canonical "primary in AWS, backup in OCI, fall back to the
|
|
101
|
+
other on either side" cross-cloud DR pattern.
|
|
102
|
+
|
|
103
|
+
### Intra/inter-replica rules
|
|
104
|
+
|
|
105
|
+
Within a replica, jobs can be co-located or kept apart on different
|
|
106
|
+
nodes (`coLocateWith`, `separateFrom` on `BundleJobSpec`). Between
|
|
107
|
+
replicas, the defaults are "different node" (hard) and "same AZ" (soft),
|
|
108
|
+
configurable via `intraReplicaPlacement` and `interReplicaPlacement`.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Priority bands
|
|
113
|
+
|
|
114
|
+
Every job declares a `priorityBand: "gold" | "silver" | "bronze"`. The band
|
|
115
|
+
governs failure behaviour:
|
|
116
|
+
|
|
117
|
+
| Band | On failure | Restart placement |
|
|
118
|
+
|--------|------------------|------------------------|
|
|
119
|
+
| gold | restart | **prefers hot spare** |
|
|
120
|
+
| silver | restart | any available |
|
|
121
|
+
| bronze | drop (no restart)| n/a |
|
|
122
|
+
|
|
123
|
+
Restart bounds (`maxRestartsPerHour`, default 6) apply to gold and silver:
|
|
124
|
+
if a replica's restart rate exceeds the ceiling, AutoManager marks the
|
|
125
|
+
bundle `broken`, fires `onBundleBroken`, and stops trying. A successful
|
|
126
|
+
restart clears the count for that replica.
|
|
127
|
+
|
|
128
|
+
Bronze jobs that hit a recovery path fire `onJobDropped` and exit
|
|
129
|
+
silently.
|
|
130
|
+
|
|
131
|
+
You can configure each band's behaviour explicitly:
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
bands: {
|
|
135
|
+
gold: { onFailure: "restart", restartPlacement: "preferHotSpare", maxRestartsPerHour: 6 },
|
|
136
|
+
silver: { onFailure: "restart", restartPlacement: "any", maxRestartsPerHour: 6 },
|
|
137
|
+
bronze: { onFailure: "drop" },
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Hot spares
|
|
144
|
+
|
|
145
|
+
A **hot spare** is a warm-running node that holds no jobs but is ready to
|
|
146
|
+
take one immediately when a recovery placement picks it. Configure per pool
|
|
147
|
+
(and optionally per band):
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
hotSpares: [
|
|
151
|
+
{
|
|
152
|
+
pool: "aws-east",
|
|
153
|
+
targetCount: 2,
|
|
154
|
+
forBand: "gold", // optional: dedicate spares to a band
|
|
155
|
+
spec: { instanceType: "m5.xlarge", region: "eu-west-1" },
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
The **reconciler** runs continually:
|
|
161
|
+
|
|
162
|
+
- Under target → provisions more spares (calls `createAwsNode`/`createOciNode` with the `spare=true` tag).
|
|
163
|
+
- Over target → terminates the oldest excess (supports rotation for security patching).
|
|
164
|
+
- A spare that picks up a job is automatically counted as "no longer a spare"; underflow fires immediately and a replacement is provisioned.
|
|
165
|
+
|
|
166
|
+
`onSpareUnderflow(pool, current, target)` fires whenever the live count
|
|
167
|
+
drops below the target.
|
|
168
|
+
|
|
169
|
+
For **cluster pools** (`scaleOut: "fixed"`), the reconciler can't
|
|
170
|
+
auto-scale — it fires `onSpareUnderflow` but leaves the actual hardware
|
|
171
|
+
to the operator's IT process.
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Drain / cordon
|
|
176
|
+
|
|
177
|
+
Take a node out of service gracefully:
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
autoManager.cordonNode(nodeId); // no new placements land here
|
|
181
|
+
autoManager.uncordonNode(nodeId); // reverse
|
|
182
|
+
|
|
183
|
+
autoManager.drainNode(nodeId); // cordon + terminate (existing jobs
|
|
184
|
+
// recover per band rules)
|
|
185
|
+
|
|
186
|
+
autoManager.isCordoned(nodeId); // → boolean
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
In v1, `drainNode` is the heavy-handed variant: cordon then terminate.
|
|
190
|
+
Manager emits `nodeStopping` for every job hosted on the node, and the
|
|
191
|
+
standard band-aware recovery path re-places them onto other nodes. Gold
|
|
192
|
+
jobs prefer spares; bronze jobs are dropped.
|
|
193
|
+
|
|
194
|
+
Phase E migration (next section) provides the drop-free variant.
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Migration
|
|
199
|
+
|
|
200
|
+
Move a running job from one node to another with cooperation from the job
|
|
201
|
+
code:
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
const migrationId = await autoManager.migrateJob(jobId, targetNodeId);
|
|
205
|
+
|
|
206
|
+
// Track progress via callbacks:
|
|
207
|
+
// onMigrationPhase(state) — fires on every phase transition
|
|
208
|
+
// onMigrationCompleted(id) — handshake completed successfully
|
|
209
|
+
// onMigrationAborted(id, reason) — workflow / operator aborted
|
|
210
|
+
|
|
211
|
+
// Or poll:
|
|
212
|
+
autoManager.migrationById(migrationId);
|
|
213
|
+
autoManager.migrationForJob(jobId);
|
|
214
|
+
autoManager.migrations();
|
|
215
|
+
|
|
216
|
+
// Operator escape hatch:
|
|
217
|
+
await autoManager.abortMigration(migrationId, "operator changed mind");
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
The protocol is **cooperative** — the workflow itself must implement the
|
|
221
|
+
cutover hooks (workflow ↔ worker channel; not modelled here). For
|
|
222
|
+
workflows that don't support migration, fall back to `drainNode` (which
|
|
223
|
+
is a frame drop).
|
|
224
|
+
|
|
225
|
+
No concurrent migrations per source jobId. The target instance wears
|
|
226
|
+
role `"migrationTarget"`; the source keeps its original role.
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Metrics
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
const m = autoManager.metrics();
|
|
234
|
+
// {
|
|
235
|
+
// startedAt: Date,
|
|
236
|
+
// uptimeMs: number,
|
|
237
|
+
// placementsAttempted / Succeeded / Failed,
|
|
238
|
+
// jobsRejected,
|
|
239
|
+
// bundlesBroken,
|
|
240
|
+
// jobsDropped,
|
|
241
|
+
// pools: [{ poolName, nodeCount, liveSpares, spareTarget, usedCapacityFraction }],
|
|
242
|
+
// brokenBundleIds: BundleId[],
|
|
243
|
+
// }
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Counters are cumulative since `startedAt`; per-pool gauges are computed
|
|
247
|
+
at call time. Poll on your own cadence; AutoManager doesn't push.
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Validation
|
|
252
|
+
|
|
253
|
+
Two checks worth running before submitting bundles:
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
// Static shape: replicas count, pool refs, capability satisfiability.
|
|
257
|
+
const errors = autoManager.validateBundle(bundle);
|
|
258
|
+
|
|
259
|
+
// Static + placement viability: "would each replica place right now?"
|
|
260
|
+
const result = autoManager.validateBundleAdmission(bundle);
|
|
261
|
+
// → { kind: "ok" }
|
|
262
|
+
// | { kind: "validationErrors"; errors: ValidationError[] }
|
|
263
|
+
// | { kind: "noCapacity"; unplaceable: { replicaIndex, jobName, reason }[] }
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
`AutoSettings` itself is validated by `AutoManager.run` — bad settings
|
|
267
|
+
throw `AutoSettingsValidationError` carrying the structured error list.
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## Callbacks reference
|
|
272
|
+
|
|
273
|
+
All optional. None block AutoManager's event loop on error — exceptions
|
|
274
|
+
are caught and forwarded to `onError`.
|
|
275
|
+
|
|
276
|
+
### Connection / lifecycle
|
|
277
|
+
|
|
278
|
+
- `onConnectionStateChange(state)` — `"idle" | "connecting" | "ready" | "transientFailure" | "shutdown"`.
|
|
279
|
+
- `onError(err)` — catch-all; back-compat surface.
|
|
280
|
+
|
|
281
|
+
### Job lifecycle
|
|
282
|
+
|
|
283
|
+
- `onJobUpdated(jobWithHistory)` — Manager pushed a job change.
|
|
284
|
+
- `onJobDeleted(jobId)` — job removed from the cache.
|
|
285
|
+
- `onJobRejected(info)` — worker rejected a placement (typed).
|
|
286
|
+
- `onJobConfigUpdated(jobId, config)` — runtime config refresh.
|
|
287
|
+
- `onJobDropped(jobId)` — bronze-band recovery dropped the job.
|
|
288
|
+
|
|
289
|
+
### Node lifecycle
|
|
290
|
+
|
|
291
|
+
- `onNodeStarting / onNodeStarted / onNodeStopped / onNodeTerminated`.
|
|
292
|
+
- `onNodeUnhealthy(nodeId)` — rejection rate trip.
|
|
293
|
+
|
|
294
|
+
### Bundle lifecycle
|
|
295
|
+
|
|
296
|
+
- `onBundleUpdated(bundle)` / `onBundleDeleted(bundleId)`.
|
|
297
|
+
- `onBundleBroken(bundleId, reason)` — restart-rate ceiling tripped.
|
|
298
|
+
|
|
299
|
+
### Placement
|
|
300
|
+
|
|
301
|
+
- `onPlacementDecision(info)` — fires every time placement runs, with full trace.
|
|
302
|
+
- `onPlacementFailed(info)` — typed failure callback (also surfaces via `onError`).
|
|
303
|
+
- `onSpareUnderflow(pool, current, target)` — hot-spare reconciler is short.
|
|
304
|
+
|
|
305
|
+
### Migration
|
|
306
|
+
|
|
307
|
+
- `onMigrationPhase / onMigrationCompleted / onMigrationAborted`.
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
## Operator-side config overrides (Manager)
|
|
312
|
+
|
|
313
|
+
Independent of this package but worth mentioning: Manager supports an
|
|
314
|
+
external JSON overrides file at `/var/data/manager-overrides.json` (or
|
|
315
|
+
`NORSK_MANAGER_OVERRIDES_FILE`) for the cloud / region / AZ / instance-
|
|
316
|
+
type fields of provider configs. Lets operators tune those without
|
|
317
|
+
rebuilding the Manager container. See
|
|
318
|
+
`.ai/steve/tasks/37-auto-manager-design.md` §D2 for the shape.
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
## Lifecycle
|
|
323
|
+
|
|
324
|
+
```ts
|
|
325
|
+
autoManager.close(); // cancels intervals, closes the gRPC channel,
|
|
326
|
+
// resolves runPromise
|
|
327
|
+
autoManager.dispose(); // alias for close()
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Idempotent. After `close()`, `runPromise` settles.
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## Worker-side dependencies
|
|
335
|
+
|
|
336
|
+
Several capabilities depend on worker-side work that lives outside this
|
|
337
|
+
package (in norsk-ctl, by deployment convention):
|
|
338
|
+
|
|
339
|
+
- **Hardware inventory & capacity scores** — workers advertise their cores, GPUs, NICs, and capabilities. Without this, placement runs against an empty `inventoryMap` and falls back to elastic-pool provisioning.
|
|
340
|
+
- **Worker-side enforcement** — workers refuse incoming starts that exceed their advertised capacity. Without this, AutoManager's optimistic placement isn't backstopped.
|
|
341
|
+
- **Migration handshake** — workers translate the four `NodeControllerMigration*` messages into workflow-level signals, and translate workflow responses back. Without this, `migrateJob` runs the protocol but the workflow never actually performs cutover.
|
|
342
|
+
|
|
343
|
+
See the `.ai/steve/tasks/37-auto-manager-design.md` §"Worker-side work"
|
|
344
|
+
section for the full norsk-ctl contract.
|