@kici-dev/orchestrator 0.1.10 → 0.1.12

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.
@@ -10,7 +10,7 @@
10
10
  * - "Peers report step-by-step progress back to coordinator"
11
11
  * - "Cancel mode: graceful -- finish current step, cancel remaining"
12
12
  */
13
- import type { JobReroute, JobProgress, PeerToPeerMessage, ResourceRequest } from '@kici-dev/engine';
13
+ import type { JobReroute, JobProgress, PeerScalerEvent, PeerToPeerMessage, ResourceRequest } from '@kici-dev/engine';
14
14
  import type { PeerRegistry } from './peer-registry.js';
15
15
  import type { PeerClient } from './peer-client.js';
16
16
  import type { Dispatcher } from '../agent/dispatcher.js';
@@ -139,6 +139,16 @@ export declare class RunCoordinator {
139
139
  * `onStepStatus` and the run stayed in `running` forever.
140
140
  */
141
141
  onPeerJobProgress(msg: JobProgress): void;
142
+ /**
143
+ * Handle a scaler provisioning event forwarded by a worker peer.
144
+ *
145
+ * Workers have no database, so they relay scaler events for jobs the
146
+ * worker is provisioning to the coordinator that owns the run. The
147
+ * coordinator's ExecutionTracker persists the event (provisioning log
148
+ * line + dispatch-queue last-error column) just as it would for a
149
+ * locally-emitted scaler event.
150
+ */
151
+ onPeerScalerEvent(msg: PeerScalerEvent): void;
142
152
  /**
143
153
  * Handle peer job completion. Updates ExecutionTracker.
144
154
  * The execution tracker's onJobStatus fires the needs-aware scheduler hook,
@@ -6,7 +6,7 @@
6
6
  * proof (reconnection), registers the peer in PeerRegistry, and routes
7
7
  * messages bidirectionally. Sends periodic heartbeats to the connecting peer.
8
8
  */
9
- import { type PeerHeartbeat, type PeerToPeerMessage, type JobReroute, type JobProgress, type PeerJobCancel, type PeerLogChunk, type PeerCacheUploadRequest, type PeerCacheUploadResponse, type PeerConfigReload, type PeerConfigReloadResponse, type PeerLeaving, type PeerAgentTokenRevoke, type RaftVoteRequest, type RaftVoteResponse, type RaftAppendEntries } from '@kici-dev/engine';
9
+ import { type PeerHeartbeat, type PeerToPeerMessage, type JobReroute, type JobProgress, type PeerScalerEvent, type PeerJobCancel, type PeerLogChunk, type PeerCacheUploadRequest, type PeerCacheUploadResponse, type PeerConfigReload, type PeerConfigReloadResponse, type PeerLeaving, type PeerAgentTokenRevoke, type RaftVoteRequest, type RaftVoteResponse, type RaftAppendEntries } from '@kici-dev/engine';
10
10
  import type { PeerRegistry } from './peer-registry.js';
11
11
  import type { PeerCredentialStore } from './peer-credentials.js';
12
12
  import { type JoinTokenManager } from './join-token.js';
@@ -41,6 +41,8 @@ export interface PeerHandlerDeps {
41
41
  onJobReroute: (msg: JobReroute) => Promise<void>;
42
42
  /** Callback when a job progress update is received from peer. */
43
43
  onJobProgress: (msg: JobProgress) => void;
44
+ /** Callback when a scaler provisioning event is forwarded by a worker peer. */
45
+ onPeerScalerEvent?: (msg: PeerScalerEvent) => void;
44
46
  /** Callback when a job cancel request is received from peer. */
45
47
  onJobCancel: (msg: PeerJobCancel) => void;
46
48
  /** Callback when a log chunk is received from a worker peer. */
@@ -0,0 +1,18 @@
1
+ import { type Kysely } from 'kysely';
2
+ /**
3
+ * Add `dispatch_queue.last_provisioning_error TEXT` recording the most
4
+ * recent scaler spawn-failure detail for a queued job.
5
+ *
6
+ * When the scaler fails to provision an agent for a job (e.g. the agent
7
+ * process exits with "spawn node ENOENT", or a container backend rejects
8
+ * the run), a `scaler.failed` event bound to the job writes the failure
9
+ * detail into this column. The queue-timeout reaper reads it so the job's
10
+ * eventual failure surfaces the real provisioning cause instead of a bare
11
+ * "no agent available" timeout. The column is cleared on dispatch and is
12
+ * NULL whenever no provisioning failure has been recorded.
13
+ *
14
+ * Idempotent: re-running on a DB that already has the column is a no-op.
15
+ */
16
+ export declare function up(db: Kysely<unknown>): Promise<void>;
17
+ export declare function down(db: Kysely<unknown>): Promise<void>;
18
+ //# sourceMappingURL=024_dispatch_queue_provisioning_error.d.ts.map
@@ -150,6 +150,13 @@ export interface DispatchQueueTable {
150
150
  * the rightful claimant. NULL for non-recovering rows.
151
151
  */
152
152
  recovery_agent_id: ColumnType<string | null, string | null | undefined, string | null>;
153
+ /**
154
+ * Most recent scaler spawn-failure detail for this queued job (e.g.
155
+ * "agent process error: spawn node ENOENT"). Written on a `scaler.failed`
156
+ * event bound to the job; read by the queue-timeout reaper to surface the
157
+ * real provisioning cause; cleared on dispatch. NULL when none recorded.
158
+ */
159
+ last_provisioning_error: ColumnType<string | null, string | null | undefined, string | null>;
153
160
  }
154
161
  /**
155
162
  * Deduplication cache table
@@ -21,12 +21,22 @@ interface ScalerUsageRow {
21
21
  export declare function setScalerUsageBreakdown(rows: ScalerUsageRow[]): void;
22
22
  /** Increment the spawn-refusal counter (called by ScalerManager when a request fails the cap check). */
23
23
  export declare function incScalerSpawnRefusals(): void;
24
+ /**
25
+ * Merge current per-label counts with explicit `0`s for previously-seen labels
26
+ * whose queue has drained. Mutates `everSeen` to include the current labels.
27
+ *
28
+ * Exported for direct unit testing of the sticky-zero invariant: a label that
29
+ * appears in one snapshot and is absent from the next must report `0` rather
30
+ * than vanish, otherwise the by-label gauge series freezes at its last value.
31
+ */
32
+ export declare function applyStickyQueueLabels(everSeen: Set<string>, byLabel: Record<string, number>): Record<string, number>;
24
33
  /**
25
34
  * Update the dispatch-queue depth breakdown used by the Prometheus gauges.
26
35
  *
27
36
  * Called by the orchestrator's periodic depth refresher after polling the
28
37
  * JobQueue. Normalizes the input so the gauge callback can emit explicit
29
- * zero series for known statuses even when the queue is empty.
38
+ * zero series for known statuses even when the queue is empty, and keeps
39
+ * emitting `0` for any per-label series whose queue has drained.
30
40
  */
31
41
  export declare function setDispatchQueueDepthBreakdown(snapshot: {
32
42
  byStatus: Partial<Record<'pending' | 'dispatched', number>>;
@@ -113,6 +123,23 @@ export declare const logBytesStoredTotal: import("@opentelemetry/api").Counter<i
113
123
  * - result: attempted | success | failed
114
124
  */
115
125
  export declare const scalerConfigReloadsTotal: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
126
+ /**
127
+ * `bound` label for `kici_orch_scaler_spawn_failures_total`.
128
+ * - `true` — the failed spawn was bound to a queued job (a run is affected).
129
+ * - `false` — a warm-pool / unbound pre-spawn (no run impact yet; fleet-health signal).
130
+ */
131
+ export declare const ScalerSpawnFailureBound: {
132
+ readonly Bound: "true";
133
+ readonly Unbound: "false";
134
+ };
135
+ export type ScalerSpawnFailureBound = (typeof ScalerSpawnFailureBound)[keyof typeof ScalerSpawnFailureBound];
136
+ /**
137
+ * Total scaler agent spawn failures.
138
+ * Labels:
139
+ * - backend: scaler backend type (bare-metal | container | firecracker | unknown)
140
+ * - bound: true (job-bound spawn) | false (warm-pool/unbound)
141
+ */
142
+ export declare const scalerSpawnFailuresTotal: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
116
143
  /**
117
144
  * Total number of config reload operations.
118
145
  * Labels:
@@ -7,6 +7,8 @@ export interface ExpiredJobInfo {
7
7
  id: string;
8
8
  runId: string;
9
9
  jobName: string;
10
+ /** Most recent scaler spawn-failure detail, if any was recorded. */
11
+ lastProvisioningError: string | null;
10
12
  }
11
13
  /**
12
14
  * Point-in-time breakdown of dispatch_queue depth used for Prometheus gauges
@@ -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 } from '@kici-dev/engine';
16
+ import { ExecutionRunStatus, 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';
@@ -509,7 +509,7 @@ export declare class ExecutionTracker {
509
509
  */
510
510
  emitScalerEvent(runId: string, jobId: string, event: {
511
511
  agentId: string;
512
- eventType: string;
512
+ eventType: ScalerEventType;
513
513
  detail: string;
514
514
  timestampMs: number;
515
515
  }): void;
@@ -5,7 +5,7 @@
5
5
  * Supports Docker, bare-metal, and Firecracker backends with YAML configuration,
6
6
  * label-set matching, and warm pool management.
7
7
  */
8
- export type { ResourceLimits, LabelSetConfig, ManagedAgent, ScaleResult, ValidationResult, ScalerBackend, ScalerConfig, ScalerEntry, WarmPoolConfig, FirecrackerNetworkConfig, } from './types.js';
8
+ export type { ResourceLimits, LabelSetConfig, ManagedAgent, ScaleResult, ValidationResult, ScalerBackend, ScalerConfig, ScalerEntry, ScalerEvent, WarmPoolConfig, FirecrackerNetworkConfig, } from './types.js';
9
9
  export { loadScalerConfig, scalerFileSchema, firecrackerNetworkSchema, parseMemoryString, } from './config.js';
10
10
  export { normalizeLabelSet, labelSetsMatch, detectLabelSetOverlaps, findBackendForLabels, } from './label-matcher.js';
11
11
  export { ContainerScalerBackend, detectRuntime } from './container-backend.js';
@@ -192,7 +192,7 @@ export declare class ScalerManager {
192
192
  * back to scaler defaults). Used by per-scaler / per-orchestrator / per-machine
193
193
  * resource caps; the cap math is wired up by ScalerManager itself.
194
194
  */
195
- requestScale(labels: string[], jobId: string, excludeLabels?: string[], resources?: ResourceRequest): Promise<ScaleResult>;
195
+ requestScale(labels: string[], jobId: string, runId: string, excludeLabels?: string[], resources?: ResourceRequest): Promise<ScaleResult>;
196
196
  /**
197
197
  * Run a critical section under the reservation lock. Serializes concurrent
198
198
  * `requestScale()` calls so the check+reserve sequence is atomic.
@@ -381,8 +381,21 @@ export declare class ScalerManager {
381
381
  private createEventEmitter;
382
382
  /**
383
383
  * Handle a scaler event from a backend.
384
- * If the agent is correlated to a job, relay immediately.
385
- * Otherwise, buffer until correlation is established.
384
+ *
385
+ * For spawn FAILURES the bound spawning entry is the usual resolver, since
386
+ * correlation isn't set until a job is dispatched post-registration. The
387
+ * correlation map wins only in the rarer post-registration failure window
388
+ * (e.g. a bare-metal child 'error' firing after the agent already
389
+ * registered, by which point the spawning entry is gone). Warm-pool /
390
+ * unbound spawns resolve to neither. When attributed, the event is relayed
391
+ * immediately; otherwise it is buffered until correlateAgentToJob() flushes
392
+ * it.
393
+ *
394
+ * Every `scaler.failed` also increments the fleet-wide spawn-failure counter
395
+ * and emits a structured warn, regardless of whether it could be attributed.
396
+ * The backend label is resolved from the spawning entry first, then the
397
+ * managed-agent index — so a late failure after the spawning entry is gone
398
+ * still buckets under the real backend instead of "unknown".
386
399
  */
387
400
  private handleScalerEvent;
388
401
  }
@@ -4,8 +4,11 @@
4
4
  * Provides the foundational types that all scaler backends, the ScalerManager,
5
5
  * and the configuration layer depend on.
6
6
  */
7
+ import { z } from 'zod';
8
+ import { ScalerEventType } from '@kici-dev/engine';
7
9
  import type { ResourceRequest, ResourceSpec, ScalerBackendType } from '@kici-dev/engine';
8
10
  export type { ResourceRequest, ResourceSpec } from '@kici-dev/engine';
11
+ export { ScalerEventType } from '@kici-dev/engine';
9
12
  /**
10
13
  * Resource limits for spawned agents.
11
14
  *
@@ -353,18 +356,13 @@ export interface WarmPoolConfig {
353
356
  */
354
357
  idleTimeoutSeconds: number;
355
358
  }
356
- /**
357
- * Standardized scaler lifecycle event types emitted by all backends.
358
- * These are stored in the event_log table for timeline rendering.
359
- */
360
- type ScalerEventType = 'scaler.provisioning' | 'scaler.network' | 'scaler.ready' | 'scaler.failed' | 'agent.connecting';
361
359
  /**
362
360
  * A single scaler lifecycle event.
363
361
  * Each backend emits these at key provisioning milestones.
364
362
  */
365
363
  export interface ScalerEvent {
366
364
  agentId: string;
367
- eventType: ScalerEventType;
365
+ eventType: z.infer<typeof ScalerEventType>;
368
366
  /** Backend-specific detail text (e.g. "pulling image nginx:latest", "booting microVM") */
369
367
  detail: string;
370
368
  timestampMs: number;
@@ -382,6 +380,12 @@ export type ScalerEventCallback = (event: ScalerEvent) => void;
382
380
  export interface LogCapture {
383
381
  /** Async iterable yielding individual log lines from the agent process. */
384
382
  lines(): AsyncIterable<string>;
383
+ /**
384
+ * Return the most recent buffered output lines (bounded ring buffer),
385
+ * joined oldest→newest. Used to enrich a `scaler.failed` event when a
386
+ * spawn dies before WS registration. Empty string when nothing captured.
387
+ */
388
+ tail(): string;
385
389
  /** Stop capturing and destroy underlying streams. Safe to call multiple times. */
386
390
  close(): void;
387
391
  }