@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
|
@@ -0,0 +1,1070 @@
|
|
|
1
|
+
import * as ManagerPB from "@norskvideo/norsk-api/lib/manager_pb";
|
|
2
|
+
import { Clock } from "./clock";
|
|
3
|
+
import { Bundle as TypedBundle, BundleId, NodeInventory as TypedNodeInventory, NodeService, PriorityBand, ServiceId } from "./types";
|
|
4
|
+
import { ValidationError, ValidationResult } from "./validation";
|
|
5
|
+
import { PlacementFailureReason, PlacementPool, PlacementResult, PlacementTrace, ResilienceFlag } from "./placement";
|
|
6
|
+
import { NorskManager, JobId, JobInfo, JobWithHistory, NodeId, NodeMetadata, Role, RunningNodeMetadata } from "@norskvideo/norsk-manager-sdk";
|
|
7
|
+
import type * as ManagerSdk from "@norskvideo/norsk-manager-sdk";
|
|
8
|
+
export type NodeState = "starting" | "running" | "stopping" | "stopped" | "terminating" | "terminated";
|
|
9
|
+
export type NodeSummary = {
|
|
10
|
+
nodeId: NodeId;
|
|
11
|
+
nodeMetadata?: NodeMetadata;
|
|
12
|
+
runningNodeMetadata?: RunningNodeMetadata;
|
|
13
|
+
nodeState: NodeState;
|
|
14
|
+
lastActivity: Date;
|
|
15
|
+
provisional?: {
|
|
16
|
+
jobId: JobId;
|
|
17
|
+
role: Role;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
export interface AutoSettings {
|
|
21
|
+
url?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Abort the connection / run. Aborting before `run()` connects rejects it
|
|
24
|
+
* (so a connect-with-timeout retry loop can give up cleanly without
|
|
25
|
+
* leaving an abandoned SDK connection alive); aborting a running
|
|
26
|
+
* AutoManager tears down its Manager connection, ending the run loop.
|
|
27
|
+
*/
|
|
28
|
+
signal?: AbortSignal;
|
|
29
|
+
/** Seconds-from-now window for jobs to be considered pending in the eventStream. */
|
|
30
|
+
pendingWindow: number;
|
|
31
|
+
/** Seconds stopped nodes persist before being terminated. */
|
|
32
|
+
removeStoppedNodesAfter: number;
|
|
33
|
+
/** Configured pools; a bundle names one, placement walks its tiers. */
|
|
34
|
+
placementPools: PlacementPool[];
|
|
35
|
+
/**
|
|
36
|
+
* Window (ms) for counting jobRejected events per-node. Default 5_000.
|
|
37
|
+
* Once `rejectionEscalationCount` rejections fall inside this window the
|
|
38
|
+
* node is marked "unhealthy" and excluded from placement until the next
|
|
39
|
+
* `nodeInventoryUpdated` event clears the mark.
|
|
40
|
+
*/
|
|
41
|
+
rejectionBackoffMs?: number;
|
|
42
|
+
/** Default 3. */
|
|
43
|
+
rejectionEscalationCount?: number;
|
|
44
|
+
/**
|
|
45
|
+
* After a node fails (`nodeStopping`), how long to keep it excluded from
|
|
46
|
+
* placement. Default 30_000 ms. Cleared by a subsequent
|
|
47
|
+
* `nodeInventoryUpdated` event reporting the node as reachable again.
|
|
48
|
+
*/
|
|
49
|
+
failureBackoffMs?: number;
|
|
50
|
+
/**
|
|
51
|
+
* Phase C2: per-replica restart-rate ceiling on the recovery path.
|
|
52
|
+
* When a replica's restart count in the trailing hour exceeds this,
|
|
53
|
+
* AutoManager stops re-placing it (and every other replica of the
|
|
54
|
+
* same bundle), marks the bundle broken, and fires `onBundleBroken`.
|
|
55
|
+
* Cleared when the bundle is updated or deleted, or when a fresh
|
|
56
|
+
* `jobUpdated` arrives for the broken job. Default 6 (per design).
|
|
57
|
+
*/
|
|
58
|
+
maxRestartsPerHour?: number;
|
|
59
|
+
/**
|
|
60
|
+
* Phase C3: node startup grace. If a node stays in `nodeStarting`
|
|
61
|
+
* for longer than this, AutoManager terminates it — Manager emits a
|
|
62
|
+
* `nodeStopping` and the existing band-aware recovery path replaces
|
|
63
|
+
* the job. Default 120_000 ms.
|
|
64
|
+
*
|
|
65
|
+
* (In v1, "job in starting" and "node in starting" collapse onto
|
|
66
|
+
* the same signal because `running → ready` is auto-marked. The
|
|
67
|
+
* design's distinction matters when explicit readiness lands.)
|
|
68
|
+
*/
|
|
69
|
+
jobStartupGraceMs?: number;
|
|
70
|
+
/**
|
|
71
|
+
* Phase C4 — per-band recovery policy. When set, takes precedence
|
|
72
|
+
* over the flat fields above. Bronze always drops (`onFailure:
|
|
73
|
+
* "drop"`) — no per-band restart options. Gold and silver get
|
|
74
|
+
* their own `maxRestartsPerHour`; gold additionally has
|
|
75
|
+
* `restartPlacement: "preferHotSpare"` baked in by the recovery
|
|
76
|
+
* path. Omitted bands fall back to the flat top-level fields, then
|
|
77
|
+
* to defaults.
|
|
78
|
+
*/
|
|
79
|
+
bands?: BandConfig;
|
|
80
|
+
/**
|
|
81
|
+
* Phase C4 — placement / recovery tunables grouped into one typed
|
|
82
|
+
* object. Same precedence as `bands`: object field beats flat field
|
|
83
|
+
* beats default. The flat fields above are preserved for one
|
|
84
|
+
* release; new code should prefer this shape.
|
|
85
|
+
*/
|
|
86
|
+
placement?: PlacementConfig;
|
|
87
|
+
/**
|
|
88
|
+
* Phase C6 — hot-spare reconciliation configuration. One entry per
|
|
89
|
+
* (pool, optional band). The reconciler runs on node lifecycle
|
|
90
|
+
* events and on a periodic tick: counts live spares per entry,
|
|
91
|
+
* provisions short, terminates excess. Empty/omitted = no spares.
|
|
92
|
+
*/
|
|
93
|
+
hotSpares?: HotSpareConfig[];
|
|
94
|
+
/**
|
|
95
|
+
* Phase F — NodeService configurations. Each entry describes a
|
|
96
|
+
* long-running service that consumes capabilities (typically
|
|
97
|
+
* exclusive hardware) and advertises new ones for sibling jobs
|
|
98
|
+
* to consume. AutoManager's reconciler places eager services on
|
|
99
|
+
* every eligible node and tears them down when a node loses
|
|
100
|
+
* eligibility.
|
|
101
|
+
*
|
|
102
|
+
* Worker-side capability advertisement is required for sibling
|
|
103
|
+
* jobs to actually see the provided capabilities — see
|
|
104
|
+
* `§Worker-side work` in the design doc.
|
|
105
|
+
*/
|
|
106
|
+
nodeServices?: NodeService[];
|
|
107
|
+
/**
|
|
108
|
+
* Phase F — how often the NodeService reconciler's periodic tick
|
|
109
|
+
* fires. Event-driven reconciles handle most cases; the tick is a
|
|
110
|
+
* safety net. Default 60_000 ms.
|
|
111
|
+
*/
|
|
112
|
+
nodeServiceReconcileIntervalMs?: number;
|
|
113
|
+
/**
|
|
114
|
+
* Phase C6 — how often the spare reconciler's periodic tick fires.
|
|
115
|
+
* Event-driven reconciles handle most cases; the tick is a safety
|
|
116
|
+
* net for missed signals. Default 60_000 ms.
|
|
117
|
+
*/
|
|
118
|
+
hotSpareReconcileIntervalMs?: number;
|
|
119
|
+
/**
|
|
120
|
+
* Phase E — per-phase deadline configuration for in-flight
|
|
121
|
+
* migrations. If a migration sits in a phase longer than the
|
|
122
|
+
* configured window, AutoManager auto-aborts with reason
|
|
123
|
+
* `phase timeout: <phase>`. Operators can still abort manually via
|
|
124
|
+
* `abortMigration` before the deadline fires.
|
|
125
|
+
*/
|
|
126
|
+
migration?: MigrationConfig;
|
|
127
|
+
onError?: (err: unknown) => void;
|
|
128
|
+
onNodeStarting?: (node: NodeId) => void;
|
|
129
|
+
onNodeStarted?: (node: NodeSummary) => void;
|
|
130
|
+
/** Fired when the daemon emits a NodeStopping event — the node is
|
|
131
|
+
* going away but may not have fully terminated yet. For cluster
|
|
132
|
+
* workers Ctrl-C'd at the keyboard this is the signal that fires
|
|
133
|
+
* (NodeStopped/NodeTerminated come later, after the daemon's
|
|
134
|
+
* keepalive timeout or cleanup loop). Consumers wanting prompt
|
|
135
|
+
* "worker dropped" UI updates listen here. */
|
|
136
|
+
onNodeStopping?: (nodeId: NodeId) => void;
|
|
137
|
+
onNodeStopped?: (nodeId: NodeId) => void;
|
|
138
|
+
onNodeTerminated?: (nodeId: NodeId) => void;
|
|
139
|
+
onBundleUpdated?: (bundle: TypedBundle) => void;
|
|
140
|
+
onBundleDeleted?: (bundleId: BundleId) => void;
|
|
141
|
+
/**
|
|
142
|
+
* Fired when the daemon appends a new audit-log entry (any source).
|
|
143
|
+
* Pure pass-through of the SDK's `AuditLogEntry` — AutoManager doesn't
|
|
144
|
+
* own audit semantics, it just forwards the live append so consumers
|
|
145
|
+
* can tail the log without re-polling GetAuditLog.
|
|
146
|
+
*/
|
|
147
|
+
onAuditAppended?: (entry: ManagerSdk.AuditLogEntry) => void;
|
|
148
|
+
/**
|
|
149
|
+
* Fires when a node accumulates enough rejections within
|
|
150
|
+
* `rejectionBackoffMs` to be marked unhealthy. Operators may want to
|
|
151
|
+
* surface this in monitoring.
|
|
152
|
+
*/
|
|
153
|
+
onNodeUnhealthy?: (nodeId: NodeId) => void;
|
|
154
|
+
/**
|
|
155
|
+
* Fired after `jobMap` is updated in response to a `jobUpdated` or
|
|
156
|
+
* `jobPending` event. Carries the full `JobWithHistory` so consumers
|
|
157
|
+
* can render state transitions without a separate SDK round-trip.
|
|
158
|
+
*/
|
|
159
|
+
onJobUpdated?: (jobWithHistory: JobWithHistory) => void;
|
|
160
|
+
/**
|
|
161
|
+
* Fired for an informational job message from the worker — launch
|
|
162
|
+
* progress (pulling-images / starting-containers / health), container
|
|
163
|
+
* events, service lifecycle. Lets consumers surface live launch feedback
|
|
164
|
+
* tied to a job without changing its placement state.
|
|
165
|
+
*/
|
|
166
|
+
onJobInfo?: (info: JobInfo) => void;
|
|
167
|
+
/**
|
|
168
|
+
* Fired after a job is removed from `jobMap` in response to a
|
|
169
|
+
* `jobDeleted` or `jobOutOfWindow` event.
|
|
170
|
+
*/
|
|
171
|
+
onJobDeleted?: (jobId: JobId) => void;
|
|
172
|
+
/**
|
|
173
|
+
* Fired on every `jobRejected` event after AutoManager has updated
|
|
174
|
+
* its inventory cache. Note the node is not necessarily marked
|
|
175
|
+
* unhealthy by this single occurrence — see `onNodeUnhealthy` for
|
|
176
|
+
* escalation.
|
|
177
|
+
*/
|
|
178
|
+
onJobRejected?: (info: JobRejectedInfo) => void;
|
|
179
|
+
/**
|
|
180
|
+
* Fired on `jobConfigUpdated` after AutoManager has updated its
|
|
181
|
+
* mirror of the job's `managerConfiguration`.
|
|
182
|
+
*/
|
|
183
|
+
onJobConfigUpdated?: (jobId: JobId, config: string) => void;
|
|
184
|
+
/**
|
|
185
|
+
* Fires every time the placement engine runs, regardless of outcome.
|
|
186
|
+
* Carries the full trace so consumers (e.g. an operator UI) can show
|
|
187
|
+
* "why did my job land here?" without re-running placement
|
|
188
|
+
* themselves.
|
|
189
|
+
*/
|
|
190
|
+
onPlacementDecision?: (info: PlacementDecisionInfo) => void;
|
|
191
|
+
/**
|
|
192
|
+
* Fires when the placement engine returns `kind: "failure"`. Today
|
|
193
|
+
* the failure also surfaces via `onError` as a stringified message;
|
|
194
|
+
* this typed callback is the recommended path. The two are not
|
|
195
|
+
* mutually exclusive — `onError` is preserved for back-compat.
|
|
196
|
+
*/
|
|
197
|
+
onPlacementFailed?: (info: PlacementFailedInfo) => void;
|
|
198
|
+
/**
|
|
199
|
+
* Fired whenever the underlying gRPC channel changes connectivity
|
|
200
|
+
* state. Maps the five gRPC connectivity states to the strings
|
|
201
|
+
* `idle | connecting | ready | transientFailure | shutdown`.
|
|
202
|
+
* Lets a consumer surface "connected / reconnecting / disconnected"
|
|
203
|
+
* without holding a separate NorskManager handle.
|
|
204
|
+
*/
|
|
205
|
+
onConnectionStateChange?: (state: ConnectionState) => void;
|
|
206
|
+
/**
|
|
207
|
+
* Phase-C: fired when a bundle's restart rate exceeds the band's
|
|
208
|
+
* `maxRestartsPerHour` and AutoManager marks it broken. Declared
|
|
209
|
+
* now so consumers can wire the callback; not fired in pre-Phase-C
|
|
210
|
+
* code.
|
|
211
|
+
*/
|
|
212
|
+
onBundleBroken?: (bundleId: BundleId, reason: string) => void;
|
|
213
|
+
/**
|
|
214
|
+
* Phase-C: fired when a bronze-band job fails and is dropped
|
|
215
|
+
* (rather than restarted). Declared now; not fired in pre-Phase-C
|
|
216
|
+
* code.
|
|
217
|
+
*/
|
|
218
|
+
onJobDropped?: (jobId: JobId) => void;
|
|
219
|
+
/**
|
|
220
|
+
* Phase-C: fired when a hot-spare pool's live spare count is below
|
|
221
|
+
* its configured target. Declared now; not fired in pre-Phase-C
|
|
222
|
+
* code.
|
|
223
|
+
*/
|
|
224
|
+
onSpareUnderflow?: (pool: string, current: number, target: number) => void;
|
|
225
|
+
/**
|
|
226
|
+
* Fired when a bundle's backup is best-effort placed inside a failure
|
|
227
|
+
* domain its resilience policy asked to avoid (no compliant node had
|
|
228
|
+
* capacity). The backup is running, but DR posture is reduced — surface
|
|
229
|
+
* it so an operator can act / re-place when capacity frees.
|
|
230
|
+
*/
|
|
231
|
+
onResilienceDegraded?: (info: ResilienceDegradedInfo) => void;
|
|
232
|
+
/**
|
|
233
|
+
* Phase E — fired on every migration phase transition. Use for
|
|
234
|
+
* telemetry / ops UIs. The payload is the post-transition state;
|
|
235
|
+
* `phaseHistory[0]` is launch, `phaseHistory.at(-1)` is the current
|
|
236
|
+
* phase at the time of the callback.
|
|
237
|
+
*/
|
|
238
|
+
onMigrationPhase?: (info: MigrationState) => void;
|
|
239
|
+
/** Phase E — fired once when a migration reaches `done`. */
|
|
240
|
+
onMigrationCompleted?: (migrationId: MigrationId) => void;
|
|
241
|
+
/** Phase E — fired once when a migration enters `aborted`. */
|
|
242
|
+
onMigrationAborted?: (migrationId: MigrationId, reason: string) => void;
|
|
243
|
+
/** Optional clock injection. Production unset (RealClock); tests pass MockClock. */
|
|
244
|
+
clock?: Clock;
|
|
245
|
+
}
|
|
246
|
+
/** Fixed role for the migration-target instance. No concurrent migrations per Job. @public */
|
|
247
|
+
export declare const MIGRATION_TARGET_ROLE = "migrationTarget";
|
|
248
|
+
/**
|
|
249
|
+
* Fixed role assigned to runtime jobs that exist to back a
|
|
250
|
+
* NodeService. Worker reads `WorkerJob.role === NODE_SERVICE_ROLE`
|
|
251
|
+
* to recognise the instance as a service rather than a regular
|
|
252
|
+
* workload, and adds the service's `provides[]` capabilities to its
|
|
253
|
+
* inventory once the job reaches `ready`.
|
|
254
|
+
*
|
|
255
|
+
* @public
|
|
256
|
+
*/
|
|
257
|
+
export declare const NODE_SERVICE_ROLE = "nodeService";
|
|
258
|
+
/** @public */
|
|
259
|
+
export type MigrationId = string;
|
|
260
|
+
/** @public */
|
|
261
|
+
export type MigrationPhase = "launching" | "awaitingTargetReady" | "awaitingSourceStop" | "applying" | "done" | "aborted";
|
|
262
|
+
/**
|
|
263
|
+
* Phase E migration state, kept entirely in AutoManager. Per the
|
|
264
|
+
* design, "no concurrent migrations per Job" — `sourceJobId` is the
|
|
265
|
+
* primary index; `migrationId` is a stable opaque handle for ops
|
|
266
|
+
* tooling.
|
|
267
|
+
*
|
|
268
|
+
* @public
|
|
269
|
+
*/
|
|
270
|
+
export interface MigrationState {
|
|
271
|
+
migrationId: MigrationId;
|
|
272
|
+
sourceJobId: JobId;
|
|
273
|
+
sourceRole: Role;
|
|
274
|
+
targetNodeId: NodeId;
|
|
275
|
+
startedAt: Date;
|
|
276
|
+
phase: MigrationPhase;
|
|
277
|
+
phaseHistory: {
|
|
278
|
+
phase: MigrationPhase;
|
|
279
|
+
at: Date;
|
|
280
|
+
}[];
|
|
281
|
+
/** Set when phase becomes "aborted"; undefined while in flight or on done. */
|
|
282
|
+
abortReason?: string;
|
|
283
|
+
/** Timestamp the source reported as its last-output, captured during cutover. */
|
|
284
|
+
cutoverTimestampNs?: number;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Coarse connection state surfaced by `onConnectionStateChange`. Maps
|
|
288
|
+
* to gRPC's `ChannelConnectivityState` values via NorskManager's
|
|
289
|
+
* existing watcher.
|
|
290
|
+
*
|
|
291
|
+
* @public
|
|
292
|
+
*/
|
|
293
|
+
export type ConnectionState = "idle" | "connecting" | "ready" | "transientFailure" | "shutdown";
|
|
294
|
+
/**
|
|
295
|
+
* Phase C4 — per-band recovery policy. Bronze always drops on
|
|
296
|
+
* failure; gold and silver restart with their own restart-rate
|
|
297
|
+
* ceilings. Gold's `restartPlacement: "preferHotSpare"` is the
|
|
298
|
+
* default behaviour of the recovery path (placement engine biases
|
|
299
|
+
* scoring toward spare-tagged nodes when the recovering job is gold).
|
|
300
|
+
*
|
|
301
|
+
* Omitted bands fall through to the flat `AutoSettings` fields, then
|
|
302
|
+
* to compiled-in defaults.
|
|
303
|
+
*
|
|
304
|
+
* @public
|
|
305
|
+
*/
|
|
306
|
+
export interface BandConfig {
|
|
307
|
+
gold?: {
|
|
308
|
+
onFailure: "restart";
|
|
309
|
+
restartPlacement: "preferHotSpare" | "any";
|
|
310
|
+
/** Default: 6 */
|
|
311
|
+
maxRestartsPerHour?: number;
|
|
312
|
+
};
|
|
313
|
+
silver?: {
|
|
314
|
+
onFailure: "restart";
|
|
315
|
+
restartPlacement: "any";
|
|
316
|
+
/** Default: 6 */
|
|
317
|
+
maxRestartsPerHour?: number;
|
|
318
|
+
};
|
|
319
|
+
bronze?: {
|
|
320
|
+
onFailure: "drop";
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Phase C4 — placement / recovery tunables. Same fields as the flat
|
|
325
|
+
* top-level settings; this typed grouping is the recommended path.
|
|
326
|
+
* Omitted fields fall through to the flat settings, then to
|
|
327
|
+
* compiled-in defaults.
|
|
328
|
+
*
|
|
329
|
+
* @public
|
|
330
|
+
*/
|
|
331
|
+
export interface PlacementConfig {
|
|
332
|
+
/** Default: 5_000 ms. */
|
|
333
|
+
rejectionBackoffMs?: number;
|
|
334
|
+
/** Default: 3. */
|
|
335
|
+
rejectionEscalationCount?: number;
|
|
336
|
+
/** Default: 30_000 ms. */
|
|
337
|
+
failureBackoffMs?: number;
|
|
338
|
+
/** Default: 120_000 ms. */
|
|
339
|
+
jobStartupGraceMs?: number;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Phase C6 — keep `targetCount` warm-running nodes ready per pool
|
|
343
|
+
* (and optional band). The reconciler provisions short, terminates
|
|
344
|
+
* excess. Cluster pools cannot auto-scale — `targetCount` there
|
|
345
|
+
* means "refuse to place onto a spare-tagged node unless this is a
|
|
346
|
+
* recovery placement"; reconciler emits `onSpareUnderflow` and leaves
|
|
347
|
+
* the rest to operator IT.
|
|
348
|
+
*
|
|
349
|
+
* @public
|
|
350
|
+
*/
|
|
351
|
+
export interface HotSpareConfig {
|
|
352
|
+
pool: string;
|
|
353
|
+
targetCount: number;
|
|
354
|
+
/** Optional: dedicate spares to a specific band. */
|
|
355
|
+
forBand?: PriorityBand;
|
|
356
|
+
spec: NodeSpec;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Phase C6 — what kind of node the spare reconciler should
|
|
360
|
+
* provision. For elastic (aws/oci) pools, `instanceType` is
|
|
361
|
+
* required. For cluster pools, the spec is only used to constrain
|
|
362
|
+
* which existing cluster nodes are eligible to be tagged as spare
|
|
363
|
+
* (future).
|
|
364
|
+
*
|
|
365
|
+
* @public
|
|
366
|
+
*/
|
|
367
|
+
export interface NodeSpec {
|
|
368
|
+
/** AwsInstanceType | OciShape; required for elastic pools. */
|
|
369
|
+
instanceType?: string;
|
|
370
|
+
/** Cloud-specific region or AZ; pass through to provider. */
|
|
371
|
+
region?: string;
|
|
372
|
+
az?: string;
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Phase E — per-phase deadlines for in-flight migrations. Set any
|
|
376
|
+
* field to override its default; omitted fields use the compiled-in
|
|
377
|
+
* default below.
|
|
378
|
+
*
|
|
379
|
+
* @public
|
|
380
|
+
*/
|
|
381
|
+
export interface MigrationConfig {
|
|
382
|
+
/** Default 120_000 ms — combined "launch + ready" window for the target. */
|
|
383
|
+
launchingDeadlineMs?: number;
|
|
384
|
+
/** Default 120_000 ms — workflow has this long after target running to signal ready. */
|
|
385
|
+
awaitingTargetReadyDeadlineMs?: number;
|
|
386
|
+
/** Default 30_000 ms — source workflow has this long to stop output after the cutover signal. */
|
|
387
|
+
awaitingSourceStopDeadlineMs?: number;
|
|
388
|
+
/** Default 10_000 ms — target workflow has this long to apply the handover. */
|
|
389
|
+
applyingDeadlineMs?: number;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Phase C4 — result of `validateBundleAdmission`. `ok` means the
|
|
393
|
+
* bundle passes static validation AND every (replica, jobName)
|
|
394
|
+
* placeDryRun would succeed at the current moment. The two failure
|
|
395
|
+
* modes are distinct: `validationErrors` is "the bundle spec itself
|
|
396
|
+
* is broken" (operator must fix); `noCapacity` is "current cluster
|
|
397
|
+
* state can't host this" (operator must add capacity or wait).
|
|
398
|
+
*
|
|
399
|
+
* @public
|
|
400
|
+
*/
|
|
401
|
+
export type AdmissionResult = {
|
|
402
|
+
kind: "ok";
|
|
403
|
+
} | {
|
|
404
|
+
kind: "validationErrors";
|
|
405
|
+
errors: ValidationError[];
|
|
406
|
+
} | {
|
|
407
|
+
kind: "noCapacity";
|
|
408
|
+
unplaceable: AdmissionUnplaceable[];
|
|
409
|
+
};
|
|
410
|
+
/** @public */
|
|
411
|
+
export interface AdmissionUnplaceable {
|
|
412
|
+
replicaIndex: number;
|
|
413
|
+
jobName: string;
|
|
414
|
+
reason: PlacementFailureReason;
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Phase D4 — operational metrics snapshot. Cumulative counters since
|
|
418
|
+
* `startedAt`; per-pool gauges are point-in-time at call. Consumers
|
|
419
|
+
* (telemetry pipelines, ops UIs) poll `AutoManager.metrics()` on
|
|
420
|
+
* their own cadence.
|
|
421
|
+
*
|
|
422
|
+
* @public
|
|
423
|
+
*/
|
|
424
|
+
export interface AutoManagerMetrics {
|
|
425
|
+
/** AutoManager.run() entry time; undefined before runInternal starts. */
|
|
426
|
+
startedAt?: Date;
|
|
427
|
+
/** Milliseconds since startedAt, 0 if not yet started. */
|
|
428
|
+
uptimeMs: number;
|
|
429
|
+
placementsAttempted: number;
|
|
430
|
+
/**
|
|
431
|
+
* Placement that successfully dispatched its wire call (startJob /
|
|
432
|
+
* createAwsNode / createOciNode). Counted post-RPC, so a successful
|
|
433
|
+
* decision whose dispatch then fails does not bump this counter —
|
|
434
|
+
* see `placementsDispatchFailed`.
|
|
435
|
+
*/
|
|
436
|
+
placementsSucceeded: number;
|
|
437
|
+
/** Placement engine returned `failure` (no suitable target). */
|
|
438
|
+
placementsFailed: number;
|
|
439
|
+
/**
|
|
440
|
+
* Decision succeeded but the downstream RPC (startJob /
|
|
441
|
+
* createAwsNode / createOciNode) rejected. The job is left
|
|
442
|
+
* unplaced; AutoManager surfaces the error via onError.
|
|
443
|
+
*/
|
|
444
|
+
placementsDispatchFailed: number;
|
|
445
|
+
jobsRejected: number;
|
|
446
|
+
bundlesBroken: number;
|
|
447
|
+
/** Bronze-band drops. */
|
|
448
|
+
jobsDropped: number;
|
|
449
|
+
pools: PoolMetrics[];
|
|
450
|
+
/** Bundles currently marked broken by the restart-rate ceiling. */
|
|
451
|
+
brokenBundleIds: BundleId[];
|
|
452
|
+
}
|
|
453
|
+
/** @public */
|
|
454
|
+
export interface PoolMetrics {
|
|
455
|
+
poolName: string;
|
|
456
|
+
nodeCount: number;
|
|
457
|
+
/** Spare-tagged, running, no jobs assigned. */
|
|
458
|
+
liveSpares: number;
|
|
459
|
+
/** Sum of `targetCount` across hotSpares entries for this pool. */
|
|
460
|
+
spareTarget: number;
|
|
461
|
+
/**
|
|
462
|
+
* Mean reservedCapacity / totalCapacity across nodes with inventory.
|
|
463
|
+
* 0 if there are no nodes with inventory.
|
|
464
|
+
*/
|
|
465
|
+
usedCapacityFraction: number;
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Payload for `onPlacementDecision`. The `bundleId`/`replicaIndex`/
|
|
469
|
+
* `jobName` triple identifies which bundle member the decision was made
|
|
470
|
+
* for; `result` is the placement engine's verdict; `trace` is the
|
|
471
|
+
* per-pool filter/score breakdown described in §6 of the design.
|
|
472
|
+
*
|
|
473
|
+
* @public
|
|
474
|
+
*/
|
|
475
|
+
export interface PlacementDecisionInfo {
|
|
476
|
+
jobId: JobId;
|
|
477
|
+
bundleId: BundleId;
|
|
478
|
+
replicaIndex: number;
|
|
479
|
+
jobName: string;
|
|
480
|
+
result: PlacementResult;
|
|
481
|
+
trace: PlacementTrace;
|
|
482
|
+
at: Date;
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Payload for `onPlacementFailed`. `triedPools` is the ordered list of
|
|
486
|
+
* tier names AutoManager attempted within the bundle's `pool` before
|
|
487
|
+
* giving up.
|
|
488
|
+
*
|
|
489
|
+
* @public
|
|
490
|
+
*/
|
|
491
|
+
export interface PlacementFailedInfo {
|
|
492
|
+
jobId: JobId;
|
|
493
|
+
bundleId: BundleId;
|
|
494
|
+
replicaIndex: number;
|
|
495
|
+
jobName: string;
|
|
496
|
+
reason: PlacementFailureReason;
|
|
497
|
+
triedPools: string[];
|
|
498
|
+
at: Date;
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Payload for `onResilienceDegraded`. Fired when a bundle's backup
|
|
502
|
+
* (replicaIndex 1) is placed inside a failure domain its resilience policy
|
|
503
|
+
* asked to avoid, because no compliant node had capacity. `violated` lists
|
|
504
|
+
* the anti-affinity flags that couldn't be honoured. The placement still
|
|
505
|
+
* happened — a degraded backup beats no backup — but DR posture is reduced.
|
|
506
|
+
*
|
|
507
|
+
* @public
|
|
508
|
+
*/
|
|
509
|
+
export interface ResilienceDegradedInfo {
|
|
510
|
+
jobId: JobId;
|
|
511
|
+
bundleId: BundleId;
|
|
512
|
+
replicaIndex: number;
|
|
513
|
+
jobName: string;
|
|
514
|
+
violated: ResilienceFlag[];
|
|
515
|
+
/** Set when the primary is on interruptible (spot) capacity with no
|
|
516
|
+
* durable backup — see ResilienceDegradation.interruptiblePrimary. */
|
|
517
|
+
interruptiblePrimary?: boolean;
|
|
518
|
+
at: Date;
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Payload for `onJobRejected`. `inventory` is the typed snapshot the
|
|
522
|
+
* worker sent alongside the rejection so consumers can render "why" in
|
|
523
|
+
* the same UI tick as the rejection itself.
|
|
524
|
+
*
|
|
525
|
+
* @public
|
|
526
|
+
*/
|
|
527
|
+
export interface JobRejectedInfo {
|
|
528
|
+
jobId: JobId;
|
|
529
|
+
nodeId: NodeId;
|
|
530
|
+
reason: ManagerPB.JobRejected["reason"];
|
|
531
|
+
inventory?: TypedNodeInventory;
|
|
532
|
+
at: Date;
|
|
533
|
+
}
|
|
534
|
+
export declare class AutoManager {
|
|
535
|
+
norsk: NorskManager;
|
|
536
|
+
settings: AutoSettings;
|
|
537
|
+
clock: Clock;
|
|
538
|
+
jobMap: Map<JobId, JobWithHistory>;
|
|
539
|
+
nodeMap: Map<NodeId, NodeSummary>;
|
|
540
|
+
bundleMap: Map<BundleId, TypedBundle>;
|
|
541
|
+
inventoryMap: Map<NodeId, TypedNodeInventory>;
|
|
542
|
+
private rejectionsPerNode;
|
|
543
|
+
private unhealthyNodes;
|
|
544
|
+
private recentFailures;
|
|
545
|
+
private restartHistory;
|
|
546
|
+
private jobToNode;
|
|
547
|
+
private placementsByJob;
|
|
548
|
+
private pendingPlacementTimers;
|
|
549
|
+
private pendingStopTimers;
|
|
550
|
+
private pendingPlacementRetries;
|
|
551
|
+
private lastPlacementAttemptAt;
|
|
552
|
+
private brokenBundles;
|
|
553
|
+
private startupGraceTimers;
|
|
554
|
+
private pendingSpareProvisions;
|
|
555
|
+
private hotSpareReconcileInterval?;
|
|
556
|
+
private cordonedNodes;
|
|
557
|
+
private metricsCounters;
|
|
558
|
+
private migrationStates;
|
|
559
|
+
private migrationByJobId;
|
|
560
|
+
private migrationPhaseTimers;
|
|
561
|
+
private nodeServicePlacements;
|
|
562
|
+
private nodeServiceReconcileInterval?;
|
|
563
|
+
/**
|
|
564
|
+
* The moment `runInternal()` first entered (i.e. the eventStream was
|
|
565
|
+
* established). Undefined until then. Powers "AutoManager up for N
|
|
566
|
+
* minutes" affordances.
|
|
567
|
+
* @public
|
|
568
|
+
*/
|
|
569
|
+
startedAt?: Date;
|
|
570
|
+
private cleanupInterval?;
|
|
571
|
+
private closed;
|
|
572
|
+
/** @public */
|
|
573
|
+
static run(settings: AutoSettings): Promise<[AutoManager, Promise<void>]>;
|
|
574
|
+
/** @public */
|
|
575
|
+
terminateNode(nodeId: NodeId): Promise<void>;
|
|
576
|
+
/**
|
|
577
|
+
* Phase C8 — cordon a node. AutoManager projects this onto every
|
|
578
|
+
* placement decision: cordoned nodes are filtered out, so no new
|
|
579
|
+
* placements land on them. Existing jobs continue running.
|
|
580
|
+
* Idempotent.
|
|
581
|
+
* @public
|
|
582
|
+
*/
|
|
583
|
+
cordonNode(nodeId: NodeId): void;
|
|
584
|
+
/**
|
|
585
|
+
* Phase C8 — un-cordon a node, restoring it to the placement pool.
|
|
586
|
+
* Idempotent.
|
|
587
|
+
* @public
|
|
588
|
+
*/
|
|
589
|
+
uncordonNode(nodeId: NodeId): void;
|
|
590
|
+
/**
|
|
591
|
+
* Phase C8 — whether a node is currently cordoned by AutoManager.
|
|
592
|
+
* @public
|
|
593
|
+
*/
|
|
594
|
+
isCordoned(nodeId: NodeId): boolean;
|
|
595
|
+
/**
|
|
596
|
+
* Phase C8 — drain a node. Cordons it (so no new placements land
|
|
597
|
+
* here) and terminates the node — Manager emits `nodeStopping`
|
|
598
|
+
* for every job hosted there, and the existing band-aware recovery
|
|
599
|
+
* path re-places them onto other nodes (excluding this cordoned
|
|
600
|
+
* one). Bronze jobs are dropped; gold prefers hot spares.
|
|
601
|
+
*
|
|
602
|
+
* v1 takes the immediate-terminate path; the design's "graceful
|
|
603
|
+
* shuffle without recovery gap" needs Phase E job migration to
|
|
604
|
+
* land first.
|
|
605
|
+
* @public
|
|
606
|
+
*/
|
|
607
|
+
drainNode(nodeId: NodeId): Promise<void>;
|
|
608
|
+
/**
|
|
609
|
+
* Shut down this AutoManager. Cancels periodic intervals and closes
|
|
610
|
+
* the underlying NorskManager — which causes the event-loop iterator
|
|
611
|
+
* to terminate, resolving the `Promise<void>` returned from `run()`.
|
|
612
|
+
* Idempotent.
|
|
613
|
+
* @public
|
|
614
|
+
*/
|
|
615
|
+
close(): void;
|
|
616
|
+
/**
|
|
617
|
+
* Alias for `close()`. Provided for symmetry with the
|
|
618
|
+
* `using`-style disposal conventions emerging in TypeScript.
|
|
619
|
+
* @public
|
|
620
|
+
*/
|
|
621
|
+
dispose(): void;
|
|
622
|
+
/**
|
|
623
|
+
* AutoManager v1 — submit a new bundle. The bundle is validated by
|
|
624
|
+
* Manager (per §4.3) and expanded into N replica jobs server-side;
|
|
625
|
+
* AutoManager mirrors the resulting state via the bundleUpdated +
|
|
626
|
+
* jobUpdated events. The returned bundleId matches the one Manager
|
|
627
|
+
* assigns (typically the same as bundle.bundleId, but Manager has
|
|
628
|
+
* the final word).
|
|
629
|
+
*
|
|
630
|
+
* @public
|
|
631
|
+
*/
|
|
632
|
+
createBundle(bundle: TypedBundle): Promise<BundleId>;
|
|
633
|
+
/**
|
|
634
|
+
* AutoManager v1 — update an existing bundle.
|
|
635
|
+
* @public
|
|
636
|
+
*/
|
|
637
|
+
updateBundle(bundle: TypedBundle): Promise<void>;
|
|
638
|
+
/**
|
|
639
|
+
* AutoManager v1 — delete a bundle and all its replica jobs.
|
|
640
|
+
* @public
|
|
641
|
+
*/
|
|
642
|
+
deleteBundle(bundleId: BundleId): Promise<void>;
|
|
643
|
+
/**
|
|
644
|
+
* AutoManager v1 — activate a dormant (start_mode = "manual") bundle
|
|
645
|
+
* job. Manager removes the dormant tag and re-emits jobUpdated;
|
|
646
|
+
* AutoManager's placement path picks the job up via the standard
|
|
647
|
+
* flow. See §10.2 of the design.
|
|
648
|
+
* @public
|
|
649
|
+
*/
|
|
650
|
+
startBundleJob(bundleId: BundleId, replicaIndex: number, jobName: string): Promise<void>;
|
|
651
|
+
/**
|
|
652
|
+
* AutoManager v1 — deactivate a bundle job (tag-flip).
|
|
653
|
+
*
|
|
654
|
+
* Runtime termination of an in-flight workload and empty-node
|
|
655
|
+
* release with linger are pre-go-live work tracked as Phase E.5
|
|
656
|
+
* (§10.2.1). Until they land, calling stopBundleJob on an active
|
|
657
|
+
* job marks it dormant for future placement but does not stop the
|
|
658
|
+
* currently-running worker.
|
|
659
|
+
* @public
|
|
660
|
+
*/
|
|
661
|
+
stopBundleJob(bundleId: BundleId, replicaIndex: number, jobName: string): Promise<void>;
|
|
662
|
+
/**
|
|
663
|
+
* AutoManager v1 — typed view of the jobs Manager has told us about.
|
|
664
|
+
* `JobWithHistory` carries the state-transition log for each job so
|
|
665
|
+
* consumers can render lifecycle timelines without a separate
|
|
666
|
+
* round-trip.
|
|
667
|
+
* @public
|
|
668
|
+
*/
|
|
669
|
+
jobs(): JobWithHistory[];
|
|
670
|
+
nodes(): NodeSummary[];
|
|
671
|
+
/**
|
|
672
|
+
* AutoManager v1 — typed view of the bundle definitions Manager has
|
|
673
|
+
* told us about. Mirror only; AutoManager owns no durable state.
|
|
674
|
+
* @public
|
|
675
|
+
*/
|
|
676
|
+
bundles(): TypedBundle[];
|
|
677
|
+
/**
|
|
678
|
+
* Phase D4 — snapshot of AutoManager's operational metrics.
|
|
679
|
+
* Cumulative counters since `startedAt`; per-pool gauges computed
|
|
680
|
+
* at call time. Cheap to call repeatedly — consumers poll on their
|
|
681
|
+
* own cadence and compute deltas if they want rates.
|
|
682
|
+
* @public
|
|
683
|
+
*/
|
|
684
|
+
metrics(): AutoManagerMetrics;
|
|
685
|
+
private computePoolMetrics;
|
|
686
|
+
/**
|
|
687
|
+
* Look up a single bundle by id. Convenience over `bundles().find(...)`.
|
|
688
|
+
* @public
|
|
689
|
+
*/
|
|
690
|
+
bundleById(bundleId: BundleId): TypedBundle | undefined;
|
|
691
|
+
/**
|
|
692
|
+
* Every job (with history) that belongs to a given bundle. Derived
|
|
693
|
+
* from `tags.bundleId` on the job entries in jobMap. Returns an empty
|
|
694
|
+
* array if the bundle is unknown or has no jobs.
|
|
695
|
+
* @public
|
|
696
|
+
*/
|
|
697
|
+
jobsForBundle(bundleId: BundleId): JobWithHistory[];
|
|
698
|
+
/**
|
|
699
|
+
* The node the given job is currently placed on, or undefined if
|
|
700
|
+
* the job hasn't been placed (or has been deleted). Useful for
|
|
701
|
+
* operator UIs that render "job X is on node Y" and for proxy
|
|
702
|
+
* routers that need to know which worker hosts each job.
|
|
703
|
+
*
|
|
704
|
+
* O(1) lookup against the `jobToNode` index — set at placement
|
|
705
|
+
* time, deleted on jobDeleted, rebuilt from job history on R7
|
|
706
|
+
* reconnect. Works regardless of whether the node hosts one job
|
|
707
|
+
* (cloud, today) or many (cluster, or any future cloud N:1)
|
|
708
|
+
* because the assignment is recorded against the JOB rather than
|
|
709
|
+
* scanned out of NODE tags.
|
|
710
|
+
* @public
|
|
711
|
+
*/
|
|
712
|
+
nodeForJob(jobId: JobId): NodeSummary | undefined;
|
|
713
|
+
/**
|
|
714
|
+
* Current placement of every job AutoManager has bound to a node,
|
|
715
|
+
* newest-first by decision timestamp. Mirrors `nodeForJob` but
|
|
716
|
+
* returns the full `PlacementDecisionInfo` (so consumers get the
|
|
717
|
+
* pool, trace, replicaIndex etc., not just the node) — and exposes
|
|
718
|
+
* it as queryable live state rather than only via the
|
|
719
|
+
* `onPlacementDecision` event stream.
|
|
720
|
+
*
|
|
721
|
+
* Hydrated jobs (placed in a previous AutoManager generation, picked
|
|
722
|
+
* up on R7 reconnect) get an entry with an empty `trace.attempts`
|
|
723
|
+
* and a decision inferred from job history. Consumers that *need*
|
|
724
|
+
* a real trace should subscribe to `onPlacementDecision` for live
|
|
725
|
+
* decisions; this getter is for "where is everything right now?"
|
|
726
|
+
* operator views.
|
|
727
|
+
*
|
|
728
|
+
* @public
|
|
729
|
+
*/
|
|
730
|
+
placements(): PlacementDecisionInfo[];
|
|
731
|
+
/**
|
|
732
|
+
* O(1) lookup for the most recent placement decision binding `jobId`
|
|
733
|
+
* to a node. Returns undefined for jobs that have never been placed
|
|
734
|
+
* (or whose placement attempts have only failed). @public
|
|
735
|
+
*/
|
|
736
|
+
placementForJob(jobId: JobId): PlacementDecisionInfo | undefined;
|
|
737
|
+
/**
|
|
738
|
+
* Run the placement engine against AutoManager's current inventory
|
|
739
|
+
* snapshot and configured pools, without actually placing or
|
|
740
|
+
* provisioning anything. Powers preview affordances in operator
|
|
741
|
+
* UIs ("if I submitted this bundle now, where would each replica
|
|
742
|
+
* land?").
|
|
743
|
+
*
|
|
744
|
+
* `jobName` is optional; if omitted the *first* job in the bundle
|
|
745
|
+
* is used. For multi-job bundles, supply the name to get a decision
|
|
746
|
+
* specific to that job.
|
|
747
|
+
*
|
|
748
|
+
* Returns undefined if the bundle's named job is missing or the
|
|
749
|
+
* replica index is out of range.
|
|
750
|
+
* @public
|
|
751
|
+
*/
|
|
752
|
+
placeDryRun(bundle: TypedBundle, replicaIndex: number, jobName?: string): {
|
|
753
|
+
result: PlacementResult;
|
|
754
|
+
trace: PlacementTrace;
|
|
755
|
+
} | undefined;
|
|
756
|
+
/**
|
|
757
|
+
* Static validation of a bundle against AutoManager's currently
|
|
758
|
+
* configured pools. Equivalent to calling the module-level
|
|
759
|
+
* `validateBundle` with `PoolDescriptor[]` derived from
|
|
760
|
+
* `AutoSettings.placementPools` — elastic pools project their
|
|
761
|
+
* `candidateInstanceTypes` into node profiles; cluster pools project
|
|
762
|
+
* their currently-known live inventory.
|
|
763
|
+
* @public
|
|
764
|
+
*/
|
|
765
|
+
validateBundle(bundle: TypedBundle): ValidationResult;
|
|
766
|
+
/**
|
|
767
|
+
* Phase C4 — packaged admission check. Combines `validateBundle`
|
|
768
|
+
* (static spec sanity) with `placeDryRun` for every (replica,
|
|
769
|
+
* jobName) pair. Returns `ok` only when both pass for every
|
|
770
|
+
* member; otherwise reports the structured failure. Use as a
|
|
771
|
+
* pre-submit guard in operator-facing tooling.
|
|
772
|
+
*
|
|
773
|
+
* Per the design's §3.4 band table, all bands reject at admission
|
|
774
|
+
* on "no capacity right now" — there's no band differentiation
|
|
775
|
+
* here. (Gold/silver get *queued* internally on capacity-after-
|
|
776
|
+
* submit failures via the rejection re-place path, but that's a
|
|
777
|
+
* runtime concern, not admission.)
|
|
778
|
+
*
|
|
779
|
+
* @public
|
|
780
|
+
*/
|
|
781
|
+
validateBundleAdmission(bundle: TypedBundle): AdmissionResult;
|
|
782
|
+
private poolDescriptorsForValidation;
|
|
783
|
+
/**
|
|
784
|
+
* AutoManager v1 — typed inventory snapshot for a single node, or
|
|
785
|
+
* undefined if the worker hasn't (yet) advertised one.
|
|
786
|
+
* @public
|
|
787
|
+
*/
|
|
788
|
+
inventory(nodeId: NodeId): TypedNodeInventory | undefined;
|
|
789
|
+
/**
|
|
790
|
+
* AutoManager v1 — typed inventory snapshots for every node we know
|
|
791
|
+
* about. Iteration order is insertion order.
|
|
792
|
+
* @public
|
|
793
|
+
*/
|
|
794
|
+
inventories(): {
|
|
795
|
+
nodeId: NodeId;
|
|
796
|
+
inventory: TypedNodeInventory;
|
|
797
|
+
}[];
|
|
798
|
+
private handleNodeInventoryUpdated;
|
|
799
|
+
/**
|
|
800
|
+
* A running job has had its config updated at runtime (operator action,
|
|
801
|
+
* job-driven, or other side channel). Refresh our in-memory copy so a
|
|
802
|
+
* subsequent `redeploy` (placeAndProvision triggered by node failure
|
|
803
|
+
* etc.) hands the new config rather than the launch one.
|
|
804
|
+
*
|
|
805
|
+
* Manager is responsible for persisting the new value; AutoManager
|
|
806
|
+
* just keeps its mirror up to date.
|
|
807
|
+
*/
|
|
808
|
+
private handleJobConfigUpdated;
|
|
809
|
+
private handleJobRejected;
|
|
810
|
+
private recordRejection;
|
|
811
|
+
private currentExcludeSet;
|
|
812
|
+
/**
|
|
813
|
+
* Phase C2 — restart-rate check. Records the current attempt; if the
|
|
814
|
+
* trailing-hour count crosses the band's `maxRestartsPerHour`, marks
|
|
815
|
+
* the bundle broken, fires `onBundleBroken`, and returns true
|
|
816
|
+
* (caller must short-circuit). Returns false when within budget.
|
|
817
|
+
*/
|
|
818
|
+
private shouldBreakOnRestart;
|
|
819
|
+
/**
|
|
820
|
+
* Whether the named bundle has tripped the restart ceiling and is
|
|
821
|
+
* being held by AutoManager. Cleared by `bundleUpdated` (operator
|
|
822
|
+
* pushed a fresh spec) or `bundleDeleted`.
|
|
823
|
+
* @public
|
|
824
|
+
*/
|
|
825
|
+
isBundleBroken(bundleId: BundleId): boolean;
|
|
826
|
+
/**
|
|
827
|
+
* Initiate a migration of a running job to a target node. AutoManager
|
|
828
|
+
* provisions a second instance of the same Job on `targetNodeId` with
|
|
829
|
+
* role="migrationTarget" and drives the cutover handshake. Returns
|
|
830
|
+
* a stable `migrationId` for tracking.
|
|
831
|
+
*
|
|
832
|
+
* Throws if a migration is already in flight for `sourceJobId` (no
|
|
833
|
+
* concurrent migrations per Job).
|
|
834
|
+
*
|
|
835
|
+
* `sourceRole` defaults to whichever role the first node hosting
|
|
836
|
+
* `sourceJobId` has — adequate for single-instance Jobs. Specify
|
|
837
|
+
* explicitly for multi-replica bundles.
|
|
838
|
+
*
|
|
839
|
+
* @public
|
|
840
|
+
*/
|
|
841
|
+
migrateJob(sourceJobId: JobId, targetNodeId: NodeId, sourceRole?: Role): Promise<MigrationId>;
|
|
842
|
+
/**
|
|
843
|
+
* Abort an in-flight migration. AutoManager sends MigrationAbort to
|
|
844
|
+
* any live source/target instances and marks the migration aborted.
|
|
845
|
+
* No-op (with warning log) for an unknown migration id.
|
|
846
|
+
*
|
|
847
|
+
* @public
|
|
848
|
+
*/
|
|
849
|
+
abortMigration(migrationId: MigrationId, reason: string): Promise<void>;
|
|
850
|
+
/** All known migrations (active + done + aborted). @public */
|
|
851
|
+
migrations(): MigrationState[];
|
|
852
|
+
/** Look up a single migration by id. @public */
|
|
853
|
+
migrationById(migrationId: MigrationId): MigrationState | undefined;
|
|
854
|
+
/** Migration active for the given source jobId, if any. @public */
|
|
855
|
+
migrationForJob(jobId: JobId): MigrationState | undefined;
|
|
856
|
+
/**
|
|
857
|
+
* Find the role of the (first) node currently running this Job.
|
|
858
|
+
* Returns undefined if no node hosts the job.
|
|
859
|
+
*/
|
|
860
|
+
private inferSourceRole;
|
|
861
|
+
private advanceMigration;
|
|
862
|
+
/** Phase E deadlines — re-arm the per-phase timer for the migration. */
|
|
863
|
+
private armMigrationPhaseDeadline;
|
|
864
|
+
private cfgMigrationPhaseDeadline;
|
|
865
|
+
private markMigrationDone;
|
|
866
|
+
private markMigrationAborted;
|
|
867
|
+
private failMigration;
|
|
868
|
+
private dispatchMigrationAborts;
|
|
869
|
+
private handleMigrationTargetReady;
|
|
870
|
+
private handleMigrationSourceStoppedOutput;
|
|
871
|
+
private handleMigrationAborted;
|
|
872
|
+
private findNodeForJobKey;
|
|
873
|
+
private cfgRejectionBackoffMs;
|
|
874
|
+
private cfgRejectionEscalationCount;
|
|
875
|
+
private cfgFailureBackoffMs;
|
|
876
|
+
private cfgJobStartupGraceMs;
|
|
877
|
+
private cfgMaxRestartsPerHour;
|
|
878
|
+
private placeAndProvision;
|
|
879
|
+
private inventoriesAsNodeViews;
|
|
880
|
+
/** Map a placed jobId to the RunningJob view the placement engine uses
|
|
881
|
+
* for affinity / spread, via the job's bundle-context tags. Empty when
|
|
882
|
+
* the job lacks bundle tags. */
|
|
883
|
+
private runningJobFor;
|
|
884
|
+
/** Sum the resource requirements of the given placed jobs into a node
|
|
885
|
+
* reservation. Capacity / cores / capabilities come from each job's
|
|
886
|
+
* bundle spec — the canonical typed requirements the placement engine
|
|
887
|
+
* scores against, looked up via the job's bundleId / jobName tags. Jobs
|
|
888
|
+
* whose bundle or spec we don't have yet contribute nothing. */
|
|
889
|
+
private derivedReservation;
|
|
890
|
+
/** The canonical typed requirements for a placed job, from its bundle
|
|
891
|
+
* spec (the same source `placeAndProvision` scores against). */
|
|
892
|
+
private requirementsForPlacedJob;
|
|
893
|
+
/**
|
|
894
|
+
* Phase C5 — is the named node currently tagged as a hot spare?
|
|
895
|
+
* Spares are warm-running nodes provisioned (by the C6 reconciler)
|
|
896
|
+
* with `NodeMetadata.tags["spare"] = "true"`. The optional
|
|
897
|
+
* `tags["spareBand"]` ("gold" | "silver") dedicates a spare to a
|
|
898
|
+
* band; if absent, the spare is band-agnostic.
|
|
899
|
+
* @public
|
|
900
|
+
*/
|
|
901
|
+
isHotSpareNode(nodeId: NodeId): boolean;
|
|
902
|
+
/**
|
|
903
|
+
* Phase C5 — band a spare is dedicated to, if any. `undefined` for a
|
|
904
|
+
* non-spare or for a band-agnostic spare.
|
|
905
|
+
* @public
|
|
906
|
+
*/
|
|
907
|
+
spareBandForNode(nodeId: NodeId): PriorityBand | undefined;
|
|
908
|
+
/**
|
|
909
|
+
* Drive every configured `hotSpares` entry toward its `targetCount`.
|
|
910
|
+
* Counts live + pending spares matching (pool, band); provisions
|
|
911
|
+
* short, terminates excess (oldest live first). Cluster pools are
|
|
912
|
+
* non-elastic — under-target just fires `onSpareUnderflow` and
|
|
913
|
+
* leaves provisioning to the operator. Idempotent and cheap; safe
|
|
914
|
+
* to call on every lifecycle event plus the periodic tick.
|
|
915
|
+
*/
|
|
916
|
+
private reconcileHotSpares;
|
|
917
|
+
private spareNodesMatching;
|
|
918
|
+
private pendingSparesMatching;
|
|
919
|
+
private provisionHotSpare;
|
|
920
|
+
/**
|
|
921
|
+
* @public
|
|
922
|
+
* Current placements for each configured NodeService. Returns a
|
|
923
|
+
* snapshot map of `serviceId → (nodeId → runtimeJobId)`.
|
|
924
|
+
*/
|
|
925
|
+
nodeServices(): NodeService[];
|
|
926
|
+
/**
|
|
927
|
+
* @public
|
|
928
|
+
* Nodes on which the named NodeService is currently placed,
|
|
929
|
+
* keyed by the runtime job's jobId.
|
|
930
|
+
*/
|
|
931
|
+
nodeServicePlacementsFor(serviceId: ServiceId): Map<NodeId, JobId>;
|
|
932
|
+
/**
|
|
933
|
+
* Drive every configured NodeService toward its lifecycle target.
|
|
934
|
+
* v1 only handles `eager` — places on every eligible node.
|
|
935
|
+
* `lazy` placements + capability-loss-driven consumer recovery (F3)
|
|
936
|
+
* are deferred.
|
|
937
|
+
*/
|
|
938
|
+
private reconcileNodeServices;
|
|
939
|
+
private reconcileEagerNodeService;
|
|
940
|
+
/**
|
|
941
|
+
* Identify nodes in the service's named pools whose current inventory
|
|
942
|
+
* satisfies its `consumes` requirements. Used both for placement and
|
|
943
|
+
* for tear-down decisions.
|
|
944
|
+
*/
|
|
945
|
+
private eligibleNodesForService;
|
|
946
|
+
private nodeSatisfiesConsumes;
|
|
947
|
+
private placeNodeService;
|
|
948
|
+
private teardownNodeService;
|
|
949
|
+
private tagsForJob;
|
|
950
|
+
private poolNameForNode;
|
|
951
|
+
private tierNameForNode;
|
|
952
|
+
private azForNode;
|
|
953
|
+
private cloudForNode;
|
|
954
|
+
private handleBundleUpdated;
|
|
955
|
+
private handleBundleDeleted;
|
|
956
|
+
private clearRestartHistoryForBundle;
|
|
957
|
+
private nodesByJob;
|
|
958
|
+
private handleJobChanged;
|
|
959
|
+
/** Read the `__stopDateTime` tag off the job, and (re)schedule the
|
|
960
|
+
* bundle's stop timer accordingly. Tag is set per-job but applies
|
|
961
|
+
* to the whole bundle — first-job-wins for read; all jobs in a
|
|
962
|
+
* bundle share the same value at submit. */
|
|
963
|
+
private reconcileStopTimer;
|
|
964
|
+
private scheduleStopTimer;
|
|
965
|
+
/**
|
|
966
|
+
* Update (or clear) a bundle's auto-stop time. Calls `updateBundle`
|
|
967
|
+
* so the new value persists via the daemon's tag store and any other
|
|
968
|
+
* clients see it through their event streams. Pass `undefined` to
|
|
969
|
+
* cancel a previously-set stop time.
|
|
970
|
+
*
|
|
971
|
+
* Throws if the bundle isn't known to this AutoManager (no live
|
|
972
|
+
* bundleMap entry); operators wanting to set stop on a not-yet-
|
|
973
|
+
* streamed bundle should retry after JobPending fires.
|
|
974
|
+
* @public
|
|
975
|
+
*/
|
|
976
|
+
setBundleStopTime(bundleId: BundleId, stopDateTime?: Date): Promise<void>;
|
|
977
|
+
/**
|
|
978
|
+
* Snapshot of all currently-scheduled bundle stop timers. UIs render
|
|
979
|
+
* this alongside `pendingPlacements()` for an "upcoming events"
|
|
980
|
+
* view.
|
|
981
|
+
* @public
|
|
982
|
+
*/
|
|
983
|
+
bundleStopTimes(): Array<{
|
|
984
|
+
bundleId: BundleId;
|
|
985
|
+
stopAt: Date;
|
|
986
|
+
}>;
|
|
987
|
+
/** A job that should be running but currently has no node bound to
|
|
988
|
+
* it: it carries placement `requirements`, isn't manual-start, is in
|
|
989
|
+
* a placeable state (`pre` / `active`), and `nodesByJob` is empty.
|
|
990
|
+
* This is the single source of truth for "this job wants a placement
|
|
991
|
+
* and doesn't have one" — shared by initial hydration, the per-job
|
|
992
|
+
* `handleJobChanged` path, and the capacity-driven reconcile sweep,
|
|
993
|
+
* so the decision lives in exactly one place. */
|
|
994
|
+
private wantsPlacementNow;
|
|
995
|
+
/**
|
|
996
|
+
* Re-run placement for every job that wants a node but hasn't got
|
|
997
|
+
* one. Called whenever a *capacity-increasing* event arrives — a node
|
|
998
|
+
* comes online (`nodeStarted`), a worker reports fresh inventory
|
|
999
|
+
* (`nodeInventoryUpdated`, which also covers an un-cordon or a node
|
|
1000
|
+
* that has just freed reserved capacity), or a job finishes and
|
|
1001
|
+
* releases its node (`jobDeleted`). Those are the only transitions
|
|
1002
|
+
* that can turn a previously-unplaceable job placeable, so they are
|
|
1003
|
+
* exactly when re-running the engine is worthwhile — there is no
|
|
1004
|
+
* polling retry, the trigger is always a concrete event.
|
|
1005
|
+
*
|
|
1006
|
+
* Placement stays implemented once: each candidate routes through
|
|
1007
|
+
* `placeOrDefer` (the same entry point hydration and `handleJobChanged`
|
|
1008
|
+
* use), inheriting its scheduling, the in-flight gate, and the
|
|
1009
|
+
* failure path. Jobs already waiting on a schedule timer or a recovery
|
|
1010
|
+
* backoff are skipped — their own timers own them — so a capacity
|
|
1011
|
+
* event never disturbs a future-scheduled or backing-off job.
|
|
1012
|
+
*
|
|
1013
|
+
* Cheap and idempotent (mirrors `reconcileHotSpares` /
|
|
1014
|
+
* `reconcileNodeServices`); safe to call on every relevant event.
|
|
1015
|
+
*/
|
|
1016
|
+
private reconcilePendingPlacements;
|
|
1017
|
+
/** Decide whether to place immediately or defer until the job's
|
|
1018
|
+
* scheduled `startDateTime` (minus pool lead time) arrives.
|
|
1019
|
+
* Re-entry from job-update events: cancels and re-evaluates so a
|
|
1020
|
+
* schedule change moves the timer. Hydrated jobs with `startTime -
|
|
1021
|
+
* lead` in the past place immediately (operator intent was to run;
|
|
1022
|
+
* schedule was a "when", not a "drop if missed"). */
|
|
1023
|
+
private placeOrDefer;
|
|
1024
|
+
/** Maximum `placementLeadMs` across the bundle pool's tiers (worst case
|
|
1025
|
+
* — guarantees the job is ready by `startDateTime` regardless of which
|
|
1026
|
+
* tier the placement engine ends up picking). Falls back to the per-kind
|
|
1027
|
+
* default when a tier doesn't specify one. Returns 0 if the bundle or
|
|
1028
|
+
* pool isn't found — we'd rather race the schedule than indefinitely
|
|
1029
|
+
* delay placement. */
|
|
1030
|
+
private placementLeadMsForJob;
|
|
1031
|
+
/**
|
|
1032
|
+
* Currently-scheduled placements (jobs AutoManager has deferred
|
|
1033
|
+
* because their `startDateTime - leadTime` is still in the future).
|
|
1034
|
+
* Returns one entry per pending timer; the UI can render an
|
|
1035
|
+
* "upcoming jobs" list and show when each will go live.
|
|
1036
|
+
* @public
|
|
1037
|
+
*/
|
|
1038
|
+
pendingPlacements(): Array<{
|
|
1039
|
+
jobId: JobId;
|
|
1040
|
+
scheduledFor: Date;
|
|
1041
|
+
}>;
|
|
1042
|
+
private handleJobDeleted;
|
|
1043
|
+
private handleNodeStarting;
|
|
1044
|
+
/** Phase C3. Schedule the grace timer; replace any prior timer. */
|
|
1045
|
+
private armStartupGrace;
|
|
1046
|
+
private cancelStartupGrace;
|
|
1047
|
+
private handleNodeStarted;
|
|
1048
|
+
private advanceMigrationsOnNodeStarted;
|
|
1049
|
+
private handleNodeStopping;
|
|
1050
|
+
private handleNodeStopped;
|
|
1051
|
+
private dropNodeServicePlacementsOnNode;
|
|
1052
|
+
private updateNodeMap;
|
|
1053
|
+
}
|
|
1054
|
+
export declare function getJobIdForNode(nodeMetadata: NodeMetadata | undefined): JobId | undefined;
|
|
1055
|
+
/**
|
|
1056
|
+
* Walk a job's history backward and return the nodeId where it is
|
|
1057
|
+
* currently placed (or was most recently placed). Prefers `running`
|
|
1058
|
+
* entries; falls back to `provisioned` for jobs still booting their
|
|
1059
|
+
* node. Returns undefined for jobs that have never been placed or
|
|
1060
|
+
* whose history is empty (defensive — runtime data always has at
|
|
1061
|
+
* least a `created` entry, which carries no nodeMetadata).
|
|
1062
|
+
*
|
|
1063
|
+
* This is the canonical "where does this job live" lookup and
|
|
1064
|
+
* underpins `AutoManager.nodeForJob`. It works regardless of whether
|
|
1065
|
+
* a node hosts one job (cloud, today) or many (cluster, or future
|
|
1066
|
+
* cloud N:1) because the nodeId is recorded against the JOB rather
|
|
1067
|
+
* than scanned out of NODE tags.
|
|
1068
|
+
*/
|
|
1069
|
+
export declare function currentNodeIdForJob(jh: JobWithHistory | undefined): NodeId | undefined;
|
|
1070
|
+
//# sourceMappingURL=automanager.d.ts.map
|