@kici-dev/orchestrator 0.1.13 → 0.1.14
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/dist/cli/service/index.d.ts +1 -1
- package/dist/cli/service/instance/manifest.d.ts +9 -0
- package/dist/cli.js +179 -103
- package/dist/db/migrations/025_init_failure.d.ts +16 -0
- package/dist/db/types.d.ts +15 -0
- package/dist/diagnostics/checks/index.d.ts +2 -1
- package/dist/diagnostics/checks/scaler.d.ts +13 -0
- package/dist/diagnostics/types.d.ts +5 -2
- package/dist/reporting/execution-tracker.d.ts +77 -6
- package/dist/scaler/failure-tracker.d.ts +46 -0
- package/dist/scaler/manager.d.ts +8 -0
- package/dist/server.js +570 -168
- package/dist/standalone.js +547 -156
- package/package.json +4 -4
- package/sbom.spdx.json +91 -36
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type Kysely } from 'kysely';
|
|
2
|
+
/**
|
|
3
|
+
* Add `init_failure jsonb` columns to `execution_runs` and `execution_jobs`.
|
|
4
|
+
*
|
|
5
|
+
* Presence of this column on a row means the run/job never executed a step
|
|
6
|
+
* because of an init-phase failure; absence (NULL) means a normal run.
|
|
7
|
+
* Shape on the wire is `InitFailure` from `@kici-dev/engine`. The dashboard
|
|
8
|
+
* reads this column directly so it can render the right banner without
|
|
9
|
+
* round-tripping to the orchestrator (which may be offline).
|
|
10
|
+
*
|
|
11
|
+
* Idempotent: re-running on a DB that already has either column is a no-op
|
|
12
|
+
* for that column.
|
|
13
|
+
*/
|
|
14
|
+
export declare function up(db: Kysely<unknown>): Promise<void>;
|
|
15
|
+
export declare function down(db: Kysely<unknown>): Promise<void>;
|
|
16
|
+
//# sourceMappingURL=025_init_failure.d.ts.map
|
package/dist/db/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ColumnType, Generated, Insertable, Selectable, Updateable } from 'kysely';
|
|
2
|
+
import type { InitFailure } from '@kici-dev/engine';
|
|
2
3
|
/**
|
|
3
4
|
* PostgreSQL-only database types.
|
|
4
5
|
* Column names use snake_case matching the actual database column names.
|
|
@@ -258,6 +259,13 @@ export interface ExecutionRunTable {
|
|
|
258
259
|
contributor_username: string | null;
|
|
259
260
|
/** Human-readable reason why the run failed (null for non-failed runs). */
|
|
260
261
|
failure_reason: string | null;
|
|
262
|
+
/**
|
|
263
|
+
* Structured init-phase failure detail (shape: `InitFailure` from
|
|
264
|
+
* `@kici-dev/engine`). Non-null means the run never executed a step
|
|
265
|
+
* because something failed during the init phase (lock-file fetch,
|
|
266
|
+
* provider context, agent spawn). NULL for normal runs.
|
|
267
|
+
*/
|
|
268
|
+
init_failure: ColumnType<InitFailure | null, unknown, unknown>;
|
|
261
269
|
/** When this record was created */
|
|
262
270
|
created_at: Generated<Date>;
|
|
263
271
|
/**
|
|
@@ -304,6 +312,13 @@ export interface ExecutionJobTable {
|
|
|
304
312
|
log_bytes: Generated<number>;
|
|
305
313
|
/** Error info if failed */
|
|
306
314
|
error_message: string | null;
|
|
315
|
+
/**
|
|
316
|
+
* Structured init-phase failure detail (shape: `InitFailure` from
|
|
317
|
+
* `@kici-dev/engine`). Non-null means the job never executed a step
|
|
318
|
+
* because something failed during init (lock-file fetch, provider
|
|
319
|
+
* context, agent spawn). NULL for normal runs.
|
|
320
|
+
*/
|
|
321
|
+
init_failure: ColumnType<InitFailure | null, unknown, unknown>;
|
|
307
322
|
/** Labels used for agent routing (e.g. ["kici:os:linux", "kici:arch:x64"]). JSONB. */
|
|
308
323
|
runs_on_labels: string | null;
|
|
309
324
|
/** Last heartbeat received from agent (for stale run detection) */
|
|
@@ -11,7 +11,8 @@ import { checkAgentConnectivity } from './agents.js';
|
|
|
11
11
|
import { checkDiskSpace } from './disk.js';
|
|
12
12
|
import { checkConfigValidity } from './config.js';
|
|
13
13
|
import { checkCertificateExpiry } from './certs.js';
|
|
14
|
+
import { checkScalerProvisioning } from './scaler.js';
|
|
14
15
|
/** All diagnostic checks in display order. */
|
|
15
16
|
export declare const defaultChecks: DiagnosticCheck[];
|
|
16
|
-
export { checkDbConnectivity, checkWsToPlatform, checkAgentConnectivity, checkDiskSpace, checkConfigValidity, checkCertificateExpiry, };
|
|
17
|
+
export { checkDbConnectivity, checkWsToPlatform, checkAgentConnectivity, checkDiskSpace, checkConfigValidity, checkCertificateExpiry, checkScalerProvisioning, };
|
|
17
18
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scaler provisioning diagnostic check.
|
|
3
|
+
*
|
|
4
|
+
* Reads recent scaler spawn failures (in-process, over a short rolling window)
|
|
5
|
+
* and emits one row per configured scaler backend instance. Severity is
|
|
6
|
+
* bound-aware: a warm-pool/unbound failure is a warning (no run impact yet); a
|
|
7
|
+
* job-bound failure is a failure (a queued run could not get an agent).
|
|
8
|
+
*/
|
|
9
|
+
import type { DiagnosticDeps, DiagnosticResult } from '../types.js';
|
|
10
|
+
/** Rolling window for "recent" spawn failures. */
|
|
11
|
+
export declare const SCALER_FAILURE_WINDOW_MS: number;
|
|
12
|
+
export declare function checkScalerProvisioning(deps: DiagnosticDeps): Promise<DiagnosticResult[]>;
|
|
13
|
+
//# sourceMappingURL=scaler.d.ts.map
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import type { Kysely } from 'kysely';
|
|
8
8
|
import type { Database } from '../db/types.js';
|
|
9
9
|
import type { AgentRegistry } from '../agent/registry.js';
|
|
10
|
+
import type { ScalerManager } from '../scaler/manager.js';
|
|
10
11
|
/** Result of a single diagnostic check. */
|
|
11
12
|
export interface DiagnosticResult {
|
|
12
13
|
/** Human-readable check name (e.g., "Database connectivity"). */
|
|
@@ -32,7 +33,9 @@ export interface DiagnosticDeps {
|
|
|
32
33
|
config: Record<string, unknown>;
|
|
33
34
|
/** TLS cert path (for expiry check). */
|
|
34
35
|
tlsCertPath?: string;
|
|
36
|
+
/** Scaler manager for recent spawn-failure health (optional -- no scaler configured). */
|
|
37
|
+
scalerManager?: ScalerManager;
|
|
35
38
|
}
|
|
36
|
-
/** A diagnostic check function. */
|
|
37
|
-
export type DiagnosticCheck = (deps: DiagnosticDeps) => Promise<DiagnosticResult>;
|
|
39
|
+
/** A diagnostic check function. May return one result or several (e.g. one per scaler backend). */
|
|
40
|
+
export type DiagnosticCheck = (deps: DiagnosticDeps) => Promise<DiagnosticResult | DiagnosticResult[]>;
|
|
38
41
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { type Kysely } from 'kysely';
|
|
15
15
|
import type { Database } from '../db/types.js';
|
|
16
|
-
import { ExecutionRunStatus, ScalerEventType } from '@kici-dev/engine';
|
|
16
|
+
import { ExecutionRunStatus, type InitFailure, ScalerEventType } from '@kici-dev/engine';
|
|
17
17
|
import type { ObserverRegistry } from '../ws/observer-registry.js';
|
|
18
18
|
import type { LogStorage } from './log-storage.js';
|
|
19
19
|
import type { JobQueue } from '../queue/job-queue.js';
|
|
@@ -92,7 +92,13 @@ export interface ExecutionTrackerDeps {
|
|
|
92
92
|
* Set on terminal run states only. Powers the operator-side
|
|
93
93
|
* `kici_org_log_bytes` capacity-planning gauge on the Platform.
|
|
94
94
|
*/
|
|
95
|
-
logBytes?: number
|
|
95
|
+
logBytes?: number,
|
|
96
|
+
/**
|
|
97
|
+
* Structured init-failure signal. Set when the run never executed a step
|
|
98
|
+
* because of an init-phase failure. Forwarded to Platform's execution.status
|
|
99
|
+
* forward and persisted in execution_runs.init_failure on both sides.
|
|
100
|
+
*/
|
|
101
|
+
initFailure?: InitFailure) => void;
|
|
96
102
|
/** Optional callback to forward job status changes to Platform.
|
|
97
103
|
* Fires on every job state transition (pending->running, running->success, etc.).
|
|
98
104
|
* Used to populate Platform's execution_jobs projection table. */
|
|
@@ -101,7 +107,12 @@ export interface ExecutionTrackerDeps {
|
|
|
101
107
|
* Total raw log bytes accumulated across the job (sum of per-step totals).
|
|
102
108
|
* Set on terminal job states only.
|
|
103
109
|
*/
|
|
104
|
-
logBytes?: number
|
|
110
|
+
logBytes?: number,
|
|
111
|
+
/**
|
|
112
|
+
* Structured init-failure signal. Set for synthetic rejected-* / init-failed-*
|
|
113
|
+
* jobs that never started. Persisted in execution_jobs.init_failure.
|
|
114
|
+
*/
|
|
115
|
+
initFailure?: InitFailure) => void;
|
|
105
116
|
/**
|
|
106
117
|
* Optional callback to emit run.event messages to Platform.
|
|
107
118
|
* Fires at orchestrator lifecycle points (dispatch, agent assignment, job start/complete).
|
|
@@ -142,6 +153,22 @@ export declare class ExecutionTracker {
|
|
|
142
153
|
private readonly orgId?;
|
|
143
154
|
private readonly jobQueue?;
|
|
144
155
|
private readonly runs;
|
|
156
|
+
/**
|
|
157
|
+
* Per-run async-mutex chain. `onJobStatus` and `addJobsToRun` mutate the same
|
|
158
|
+
* `run.jobs` Map / `execution_jobs` row; without serialization a job-status
|
|
159
|
+
* reply that lands mid synthetic→real swap (`addJobsToRun`) clobbers the swap
|
|
160
|
+
* and the run hangs in `running` forever (see `withRunLock`). The map value is
|
|
161
|
+
* the tail of the chain for that runId; entries are GC'd when the last holder
|
|
162
|
+
* releases.
|
|
163
|
+
*/
|
|
164
|
+
private readonly runLockTails;
|
|
165
|
+
/**
|
|
166
|
+
* Tracks which runIds the current async context already holds in
|
|
167
|
+
* `withRunLock`, so reentrant calls (e.g. onJobStatus → scheduler hook →
|
|
168
|
+
* dispatchReadyJob → addJobsToRun, all the same runId) bypass re-acquisition
|
|
169
|
+
* instead of deadlocking. Propagates across awaits via AsyncLocalStorage.
|
|
170
|
+
*/
|
|
171
|
+
private readonly heldRunLocks;
|
|
145
172
|
/** Tracks which runs are test runs for observer broadcasting. */
|
|
146
173
|
private readonly testRunIds;
|
|
147
174
|
/**
|
|
@@ -208,6 +235,22 @@ export declare class ExecutionTracker {
|
|
|
208
235
|
* which peer owns the downstream dispatch.
|
|
209
236
|
*/
|
|
210
237
|
findSyntheticJobId(runId: string, jobName: string): Promise<string | undefined>;
|
|
238
|
+
/**
|
|
239
|
+
* Run `fn` while holding a per-run lock, serializing the run-mutating methods
|
|
240
|
+
* (`onJobStatus`, `addJobsToRun`) so a status reply cannot interleave with the
|
|
241
|
+
* synthetic→real job swap and wedge the run in `running`.
|
|
242
|
+
*
|
|
243
|
+
* The lock is **reentrant**: three paths re-enter a locked method within the
|
|
244
|
+
* same async context for the same runId — onJobStatus → scheduler hook →
|
|
245
|
+
* dispatchReadyJob → addJobsToRun; onJobStatus → enforceSchedulerInvariant →
|
|
246
|
+
* onJobStatus; runSchedulerHook → onJobStatus (skip). A non-reentrant mutex
|
|
247
|
+
* would deadlock on these, so a context that already holds the runId's lock
|
|
248
|
+
* (tracked via `heldRunLocks`) runs `fn` inline. A genuinely concurrent caller
|
|
249
|
+
* for the same runId (a separate WS message) is a different async context and
|
|
250
|
+
* correctly waits. All reentrant paths are same-runId, so there is no
|
|
251
|
+
* cross-run lock-ordering deadlock.
|
|
252
|
+
*/
|
|
253
|
+
private withRunLock;
|
|
211
254
|
addJobsToRun(runId: string, jobs: Array<{
|
|
212
255
|
jobId: string;
|
|
213
256
|
jobName: string;
|
|
@@ -216,6 +259,7 @@ export declare class ExecutionTracker {
|
|
|
216
259
|
}>, dispatchedContexts?: string[],
|
|
217
260
|
/** Synthetic job ID to replace (e.g. needs-pending-deploy-{uuid}). */
|
|
218
261
|
replaceSyntheticId?: string): Promise<void>;
|
|
262
|
+
private addJobsToRunImpl;
|
|
219
263
|
/**
|
|
220
264
|
* Mark a run as a test run for observer broadcasting.
|
|
221
265
|
* Called by the test pipeline after creating the execution run.
|
|
@@ -233,6 +277,7 @@ export declare class ExecutionTracker {
|
|
|
233
277
|
* fires the onExecutionComplete callback.
|
|
234
278
|
*/
|
|
235
279
|
onJobStatus(runId: string, jobId: string, state: string, timestamp: number, agentId?: string, data?: Record<string, unknown>): Promise<void>;
|
|
280
|
+
private onJobStatusImpl;
|
|
236
281
|
/**
|
|
237
282
|
* Phase 1a: recover run state from the DB when in-memory tracking is empty.
|
|
238
283
|
* Returns the rehydrated RunState or null if the run is unknown to the DB
|
|
@@ -313,7 +358,7 @@ export declare class ExecutionTracker {
|
|
|
313
358
|
* onExecutionStarted but the build subsequently fails. Without this,
|
|
314
359
|
* the execution_runs row would stay in a non-terminal state.
|
|
315
360
|
*/
|
|
316
|
-
onBuildFailed(runId: string): Promise<void>;
|
|
361
|
+
onBuildFailed(runId: string, initFailure?: InitFailure): Promise<void>;
|
|
317
362
|
/**
|
|
318
363
|
* Create a failed execution run when the build timed out before onExecutionStarted
|
|
319
364
|
* had a chance to insert the row (buildJobTrackedEarly was false).
|
|
@@ -321,7 +366,33 @@ export declare class ExecutionTracker {
|
|
|
321
366
|
* Inserts a minimal execution_runs row with status='failed' directly so the E2E
|
|
322
367
|
* test (and dashboard) can observe the failure instead of a missing run.
|
|
323
368
|
*/
|
|
324
|
-
onBuildFailedBeforeTracking(runId: string, workflowName: string, provider: string, repoIdentifier: string, ref: string, sha: string, deliveryId: string | null, providerContext: Record<string, unknown>, routingKey: string, triggerEvent?: string, commitMessage?: string, failureReason?: string): Promise<void>;
|
|
369
|
+
onBuildFailedBeforeTracking(runId: string, workflowName: string, provider: string, repoIdentifier: string, ref: string, sha: string, deliveryId: string | null, providerContext: Record<string, unknown>, routingKey: string, triggerEvent?: string, commitMessage?: string, failureReason?: string, initFailure?: InitFailure): Promise<void>;
|
|
370
|
+
/**
|
|
371
|
+
* Insert a `failed` execution_runs row directly for an init failure that
|
|
372
|
+
* occurred BEFORE onExecutionStarted ran (so no in-memory state exists
|
|
373
|
+
* and no jobs were dispatched). Also writes the structured init_failure
|
|
374
|
+
* signal and fires onExecutionStatusChange so Platform's projection picks
|
|
375
|
+
* it up via the normal forward path. Idempotent: if a row already exists
|
|
376
|
+
* for this runId, the insert is a no-op (ON CONFLICT DO NOTHING).
|
|
377
|
+
*
|
|
378
|
+
* Closes the silent pre-run-failure gap — without this helper, secret /
|
|
379
|
+
* install-secret / all-jobs-rejected early-exits in dispatch-matched-workflow
|
|
380
|
+
* leave no trace on the dashboard.
|
|
381
|
+
*/
|
|
382
|
+
recordInitFailureRun(args: {
|
|
383
|
+
runId: string;
|
|
384
|
+
workflowName: string;
|
|
385
|
+
provider: string;
|
|
386
|
+
repoIdentifier: string;
|
|
387
|
+
ref: string;
|
|
388
|
+
sha: string;
|
|
389
|
+
deliveryId: string | null;
|
|
390
|
+
providerContext: Record<string, unknown>;
|
|
391
|
+
routingKey: string;
|
|
392
|
+
initFailure: InitFailure;
|
|
393
|
+
triggerEvent?: string;
|
|
394
|
+
commitMessage?: string;
|
|
395
|
+
}): Promise<void>;
|
|
325
396
|
/**
|
|
326
397
|
* Mark a run as failed immediately with a reason message.
|
|
327
398
|
*
|
|
@@ -330,7 +401,7 @@ export declare class ExecutionTracker {
|
|
|
330
401
|
* Instead of leaving the run in 'running' for OrphanRecovery to catch after 5 min,
|
|
331
402
|
* this fails it right away.
|
|
332
403
|
*/
|
|
333
|
-
failRun(runId: string, reason: string): Promise<void>;
|
|
404
|
+
failRun(runId: string, reason: string, initFailure?: InitFailure): Promise<void>;
|
|
334
405
|
/**
|
|
335
406
|
* Update step status within a job.
|
|
336
407
|
*
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-process bounded record of recent scaler spawn failures.
|
|
3
|
+
*
|
|
4
|
+
* The scaler manager records every `scaler.failed` event here at the same site
|
|
5
|
+
* it increments the fleet-wide Prometheus counter. The diagnose scaler check
|
|
6
|
+
* reads recent failures grouped per backend instance to produce its rows. This
|
|
7
|
+
* is memory-only and bounded — the recent-failures window resets on restart,
|
|
8
|
+
* which is acceptable for an on-demand operator view over a short window.
|
|
9
|
+
*/
|
|
10
|
+
/** A single recorded scaler spawn failure. */
|
|
11
|
+
export interface ScalerFailureRecord {
|
|
12
|
+
/** Scaler instance name (the configured scaler `name`); the diagnose row key. */
|
|
13
|
+
backendName: string;
|
|
14
|
+
/** Backend type: 'container' | 'bare-metal' | 'firecracker' | 'unknown'. */
|
|
15
|
+
backendType: string;
|
|
16
|
+
/** True when the failed spawn was bound to a queued job (a run was affected). */
|
|
17
|
+
bound: boolean;
|
|
18
|
+
/** Captured error string from the scaler event detail. */
|
|
19
|
+
detail: string;
|
|
20
|
+
/** Event timestamp in epoch milliseconds. */
|
|
21
|
+
timestampMs: number;
|
|
22
|
+
}
|
|
23
|
+
/** Per-backend summary of recent failures within a window. */
|
|
24
|
+
export interface BackendFailureSummary {
|
|
25
|
+
backendType: string;
|
|
26
|
+
boundCount: number;
|
|
27
|
+
unboundCount: number;
|
|
28
|
+
/** Detail of the most recent failure in the window. */
|
|
29
|
+
lastError: string;
|
|
30
|
+
/** Timestamp of the most recent failure in the window. */
|
|
31
|
+
lastAtMs: number;
|
|
32
|
+
}
|
|
33
|
+
export declare class ScalerFailureTracker {
|
|
34
|
+
private readonly records;
|
|
35
|
+
private readonly maxEntries;
|
|
36
|
+
constructor(maxEntries?: number);
|
|
37
|
+
/** Record a failure, evicting the oldest entry when over capacity. */
|
|
38
|
+
record(rec: ScalerFailureRecord): void;
|
|
39
|
+
/**
|
|
40
|
+
* Group failures newer than `nowMs - windowMs` by backend instance name.
|
|
41
|
+
* `nowMs` is injected so callers control the clock (and tests are
|
|
42
|
+
* deterministic).
|
|
43
|
+
*/
|
|
44
|
+
recentByBackend(windowMs: number, nowMs: number): Map<string, BackendFailureSummary>;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=failure-tracker.d.ts.map
|
package/dist/scaler/manager.d.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* and manages the agent lifecycle from spawn to destroy.
|
|
8
8
|
*/
|
|
9
9
|
import type { ResourceRequest } from '@kici-dev/engine';
|
|
10
|
+
import type { BackendFailureSummary } from './failure-tracker.js';
|
|
10
11
|
import type { ScalerBackend, ScalerConfig, ScaleResult, ScalerEvent, ResourceCap, ValidationResult } from './types.js';
|
|
11
12
|
import type { ScalerStateStore, ScalerStateRecovery } from './scaler-state-store.js';
|
|
12
13
|
/**
|
|
@@ -80,6 +81,8 @@ export interface ScalerStatus {
|
|
|
80
81
|
export declare class ScalerManager {
|
|
81
82
|
private readonly backends;
|
|
82
83
|
private readonly backendRoles;
|
|
84
|
+
/** Recent scaler spawn failures, surfaced by `kici-admin diagnose`. */
|
|
85
|
+
private readonly failureTracker;
|
|
83
86
|
private globalMaxAgents;
|
|
84
87
|
/** Per-scaler resource caps (`{ maxCpu, maxMemoryBytes }`), keyed by scaler name. */
|
|
85
88
|
private readonly resourceCaps;
|
|
@@ -320,6 +323,11 @@ export declare class ScalerManager {
|
|
|
320
323
|
/**
|
|
321
324
|
* Return status summary for metrics and health endpoints.
|
|
322
325
|
*/
|
|
326
|
+
/**
|
|
327
|
+
* Recent scaler spawn failures grouped per backend instance, for the
|
|
328
|
+
* diagnose scaler check. `nowMs` is injected by the caller.
|
|
329
|
+
*/
|
|
330
|
+
recentSpawnFailures(windowMs: number, nowMs: number): Map<string, BackendFailureSummary>;
|
|
323
331
|
getStatus(): ScalerStatus;
|
|
324
332
|
/**
|
|
325
333
|
* Get the backend name managing a specific agent.
|