@kici-dev/orchestrator 0.4.0 → 0.6.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/dist/__test-helpers__/fake-scaler-state-store.d.ts +24 -0
- package/dist/__test-helpers__/mock-db-query.d.ts +130 -0
- package/dist/__test-helpers__/mock-db.d.ts +27 -0
- package/dist/agent/agent-version.d.ts +34 -0
- package/dist/agent/dispatcher.d.ts +151 -13
- package/dist/agent/host-roster.d.ts +37 -5
- package/dist/agent/registry.d.ts +2 -0
- package/dist/app.d.ts +56 -7
- package/dist/approvals/apply-decision.d.ts +67 -19
- package/dist/approvals/held-run-release-wiring.d.ts +45 -0
- package/dist/approvals/independent-wiring.d.ts +55 -0
- package/dist/approvals/resume-router.d.ts +9 -0
- package/dist/approvals/triggerer-subject.d.ts +37 -0
- package/dist/cache/dep-cache.d.ts +54 -10
- package/dist/cache/global-eval-round-cache.d.ts +95 -0
- package/dist/cache/index.d.ts +3 -0
- package/dist/cache/pending-global-evals.d.ts +42 -0
- package/dist/cache/pending-inits.d.ts +10 -0
- package/dist/cli/api-client.d.ts +18 -0
- package/dist/cli/commands/cluster-settings.d.ts +52 -3
- package/dist/cli/commands/diagnose.d.ts +6 -0
- package/dist/cli/commands/held-run.d.ts +58 -0
- package/dist/cli/commands/runs.d.ts +1 -0
- package/dist/cli/commands/trust-policy.d.ts +117 -12
- package/dist/cli/kici-admin.d.ts +10 -0
- package/dist/cli.js +2770 -938
- package/dist/cluster/cluster-identity.d.ts +3 -3
- package/dist/cluster/cluster-settings-reader.d.ts +59 -1
- package/dist/cluster/coordinator.d.ts +44 -0
- package/dist/cluster/index.d.ts +4 -0
- package/dist/cluster/peer-handler.d.ts +19 -0
- package/dist/cluster/peer-registry.d.ts +17 -0
- package/dist/cluster/plan-headroom-store.d.ts +26 -0
- package/dist/cluster/worker-eviction.d.ts +69 -0
- package/dist/cold-store/load-event-log-range.d.ts +10 -3
- package/dist/cold-store/load-secret-audit-log-range.d.ts +4 -3
- package/dist/cold-store/tables/secret-audit-log.d.ts +4 -3
- package/dist/config.d.ts +48 -21
- package/dist/content-requirements-cache.d.ts +55 -0
- package/dist/contexts/held-runs.d.ts +123 -16
- package/dist/contexts/protection/aggregate.d.ts +3 -2
- package/dist/contexts/protection/branch-gate.d.ts +15 -0
- package/dist/contexts/protection/concurrency-gate.d.ts +8 -2
- package/dist/contexts/protection/pipeline.d.ts +18 -0
- package/dist/contexts/release-queued-holds.d.ts +35 -0
- package/dist/dashboard/attestation-filters.d.ts +8 -0
- package/dist/dashboard/handler.d.ts +6 -24
- package/dist/db/migrations/109_cluster_settings_cache_knobs.d.ts +4 -0
- package/dist/db/migrations/110_cluster_settings_global_eval_knobs.d.ts +4 -0
- package/dist/db/migrations/111_cluster_settings_global_eval_wait.d.ts +4 -0
- package/dist/db/migrations/112_execution_runs_workflow_repo.d.ts +4 -0
- package/dist/db/migrations/113_execution_runs_workflow_repo_index.d.ts +30 -0
- package/dist/db/migrations/114_ingest_queue_claim.d.ts +4 -0
- package/dist/db/migrations/115_global_workflows_cluster_switch.d.ts +9 -0
- package/dist/db/migrations/116_org_plan_headroom.d.ts +4 -0
- package/dist/db/migrations/117_invoke_gate_proxy_jobs.d.ts +4 -0
- package/dist/db/migrations/118_invoke_gate_scheduling.d.ts +4 -0
- package/dist/db/migrations/119_scaler_ha_ownership.d.ts +4 -0
- package/dist/db/migrations/120_cluster_settings_scaler_reaper.d.ts +4 -0
- package/dist/db/migrations/121_org_settings_allow_untrusted_dockerfile_builds.d.ts +19 -0
- package/dist/db/migrations/122_host_roster_scaler_managed.d.ts +25 -0
- package/dist/db/migrations/123_workflow_registrations_default_branch.d.ts +24 -0
- package/dist/db/migrations/124_cluster_settings_provision_backoff.d.ts +4 -0
- package/dist/db/migrations/125_org_trust_directory.d.ts +30 -0
- package/dist/db/migrations/126_held_runs_posted_pending_check.d.ts +28 -0
- package/dist/db/migrations/127_org_trust_policy_approval_expiry_seconds.d.ts +26 -0
- package/dist/db/migrations/128_execution_runs_global_eval_round.d.ts +23 -0
- package/dist/db/migrations/129_execution_runs_dispatch_routing_key.d.ts +24 -0
- package/dist/db/migrations/130_scaler_provision_outcomes.d.ts +32 -0
- package/dist/db/types.d.ts +428 -12
- package/dist/entry-helpers.d.ts +28 -0
- package/dist/events/event-emitter.d.ts +22 -0
- package/dist/events/event-router.d.ts +87 -1
- package/dist/events/types.d.ts +2 -23
- package/dist/git/credential-broker.d.ts +94 -0
- package/dist/git/inherited-secret.d.ts +24 -0
- package/dist/git/installation-token.d.ts +38 -0
- package/dist/git/job-context.d.ts +13 -0
- package/dist/index.js +324 -84
- package/dist/metrics/agent-metrics-aggregator.d.ts +4 -4
- package/dist/metrics/prometheus.d.ts +170 -3
- package/dist/oidc/oidc-mint-registration.d.ts +7 -3
- package/dist/orchestrator-core.d.ts +91 -24
- package/dist/pipeline/content-filter.d.ts +71 -0
- package/dist/pipeline/direct-ingress-deps.d.ts +100 -0
- package/dist/pipeline/dispatch-matched-workflow.d.ts +521 -11
- package/dist/pipeline/global-eval-round.d.ts +307 -0
- package/dist/pipeline/internal-event-pipeline.d.ts +140 -0
- package/dist/pipeline/invoke-gate.d.ts +164 -0
- package/dist/pipeline/job-contexts.d.ts +16 -17
- package/dist/pipeline/needs-scheduler.d.ts +13 -0
- package/dist/pipeline/process-webhook.d.ts +126 -31
- package/dist/pipeline/processor.d.ts +282 -23
- package/dist/pipeline/rerun.d.ts +21 -0
- package/dist/pipeline/resume-workflow.d.ts +40 -7
- package/dist/pipeline/route-or-dispatch-jobs.d.ts +15 -0
- package/dist/pipeline/security-hold-check.d.ts +217 -0
- package/dist/pipeline/test-pipeline.d.ts +12 -0
- package/dist/pipeline/undispatched-hold-checks.d.ts +23 -0
- package/dist/pipeline/webhook-payload-store.d.ts +20 -0
- package/dist/policy/dashboard-write-policy-listener.d.ts +62 -0
- package/dist/policy/dashboard-write-policy.d.ts +14 -0
- package/dist/provenance/backfill-run.d.ts +10 -1
- package/dist/provider-registry.d.ts +47 -4
- package/dist/providers/github/check-status-poster.d.ts +33 -3
- package/dist/providers/github/commit-message.d.ts +20 -0
- package/dist/providers/github/file-contents.d.ts +40 -0
- package/dist/providers/github/index.d.ts +2 -2
- package/dist/providers/github/normalizer.d.ts +3 -2
- package/dist/providers/local/normalizer.d.ts +3 -5
- package/dist/providers/universal-git/config.d.ts +2 -0
- package/dist/providers/universal-git/index.d.ts +11 -2
- package/dist/providers/universal-git/normalizer.d.ts +10 -0
- package/dist/queue/cleanup.d.ts +7 -1
- package/dist/queue/job-queue.d.ts +112 -7
- package/dist/queue/terminalize-unroutable.d.ts +13 -0
- package/dist/registration/extractor.d.ts +3 -3
- package/dist/registration/registration-index.d.ts +7 -0
- package/dist/registration/registration-run-match.d.ts +47 -0
- package/dist/registration/registration-store.d.ts +22 -0
- package/dist/reporting/check-run-reporter.d.ts +146 -2
- package/dist/reporting/check-run-summary.d.ts +25 -1
- package/dist/reporting/execution-tracker.d.ts +324 -9
- package/dist/reporting/log-chunk-sink.d.ts +8 -5
- package/dist/reporting/run-aggregator.d.ts +4 -14
- package/dist/reporting/step-display-order.d.ts +43 -0
- package/dist/routes/admin-held-runs.d.ts +142 -0
- package/dist/routes/admin-org-settings.d.ts +5 -0
- package/dist/routes/admin-trust-policy.d.ts +34 -6
- package/dist/routes/admin.d.ts +15 -0
- package/dist/scaler/backend-factory.d.ts +55 -0
- package/dist/scaler/bare-metal-backend.d.ts +43 -5
- package/dist/scaler/claim-store.d.ts +119 -0
- package/dist/scaler/config.d.ts +4 -0
- package/dist/scaler/container-backend.d.ts +18 -4
- package/dist/scaler/container-routing.d.ts +23 -0
- package/dist/scaler/container-spawn.d.ts +28 -0
- package/dist/scaler/event-backend.d.ts +124 -0
- package/dist/scaler/event-provision-reaper.d.ts +350 -0
- package/dist/scaler/failure-tracker.d.ts +1 -1
- package/dist/scaler/firecracker-backend.d.ts +33 -6
- package/dist/scaler/index.d.ts +11 -1
- package/dist/scaler/label-matcher.d.ts +4 -3
- package/dist/scaler/manager.d.ts +905 -76
- package/dist/scaler/resolve-container-auth.d.ts +45 -0
- package/dist/scaler/scaler-events.d.ts +11 -0
- package/dist/scaler/scaler-state-store.d.ts +294 -10
- package/dist/scaler/types.d.ts +131 -13
- package/dist/scaler/warm-pool.d.ts +132 -29
- package/dist/security/comment-handler.d.ts +50 -11
- package/dist/security/global-workflow-policy.d.ts +52 -12
- package/dist/security/identity-link.d.ts +60 -0
- package/dist/security/lock-source.d.ts +11 -13
- package/dist/security/reduced-privilege-note.d.ts +59 -0
- package/dist/security/trust-directory-store.d.ts +233 -0
- package/dist/security/trust-policy-gate.d.ts +94 -59
- package/dist/security/trust-policy-store.d.ts +3 -0
- package/dist/security/trust-resolver.d.ts +14 -98
- package/dist/server.d.ts +11 -1
- package/dist/server.js +56329 -42234
- package/dist/stale-detector/gate-deadline-detector.d.ts +52 -0
- package/dist/stale-detector/stale-run-detector.d.ts +61 -3
- package/dist/standalone.js +37619 -23570
- package/dist/webhook/ingest-accept.d.ts +70 -0
- package/dist/webhook/ingest-overflow-buffer.d.ts +35 -4
- package/dist/webhook/ingest-overflow-replayer.d.ts +50 -6
- package/dist/worker/in-memory-job-queue.d.ts +1 -1
- package/dist/ws/agent-handler.d.ts +31 -4
- package/dist/ws/dashboard-context-handler.d.ts +28 -4
- package/dist/ws/dashboard-global-workflows-handler.d.ts +36 -9
- package/dist/ws/execution-status-frame.d.ts +32 -0
- package/dist/ws/failure-messages.d.ts +2 -0
- package/dist/ws/git-credential-relay.d.ts +39 -0
- package/dist/ws/oidc-token-relay.d.ts +6 -11
- package/dist/ws/platform-client.d.ts +68 -9
- package/dist/ws/test-relay-handlers.d.ts +35 -10
- package/installer-image-digests.json +3 -3
- package/package.json +24 -19
- package/sbom.spdx.json +868 -821
- package/dist/approvals/team-membership-lookup.d.ts +0 -13
- package/dist/pipeline/inline-eval.d.ts +0 -44
- package/dist/providers/github/contributor-resolver.d.ts +0 -30
- package/dist/security/contributor-cache.d.ts +0 -83
package/dist/scaler/manager.d.ts
CHANGED
|
@@ -6,10 +6,25 @@
|
|
|
6
6
|
* enforces global limits, tracks spawning agents to prevent over-provisioning,
|
|
7
7
|
* and manages the agent lifecycle from spawn to destroy.
|
|
8
8
|
*/
|
|
9
|
-
import type { ResourceRequest } from '@kici-dev/engine';
|
|
9
|
+
import type { ResourceRequest, ScalerCapacitySummary } from '@kici-dev/engine';
|
|
10
|
+
import { ScaleDownReason } from './scaler-events.js';
|
|
11
|
+
import type { ScalerEventEmitterLike } from './event-backend.js';
|
|
12
|
+
import type { ClaimStore, ClaimedCredentials } from './claim-store.js';
|
|
10
13
|
import type { BackendFailureSummary } from './failure-tracker.js';
|
|
11
|
-
import type { ScalerBackend, ScalerConfig, ScaleResult, ScalerEvent, ResourceCap, ValidationResult } from './types.js';
|
|
12
|
-
import type { ScalerStateStore, ScalerStateRecovery } from './scaler-state-store.js';
|
|
14
|
+
import type { ScalerBackend, ScalerConfig, ScalerEntry, ScaleResult, ScalerEvent, ResourceCap, ValidationResult, ResolvedContainerSpawn } from './types.js';
|
|
15
|
+
import type { ScalerStateStore, ScalerStateRecovery, ReapCandidate } from './scaler-state-store.js';
|
|
16
|
+
/**
|
|
17
|
+
* Raised when a registering agent presents a scaler-minted id that the
|
|
18
|
+
* orchestrator cannot back with a spawn record — either because the lookup
|
|
19
|
+
* could not be completed (`cause` set) or because no record exists at all
|
|
20
|
+
* (`cause` omitted). The WS handler answers it by refusing the registration, so
|
|
21
|
+
* the agent reconnects rather than registering with no `mandatoryLabels` gate.
|
|
22
|
+
*/
|
|
23
|
+
export declare class ScalerAdoptionLookupError extends Error {
|
|
24
|
+
readonly agentId: string;
|
|
25
|
+
readonly cause?: unknown;
|
|
26
|
+
constructor(agentId: string, cause?: unknown);
|
|
27
|
+
}
|
|
13
28
|
/**
|
|
14
29
|
* Counting semaphore that throttles the number of concurrent async operations.
|
|
15
30
|
*
|
|
@@ -111,18 +126,184 @@ export interface ScalerStatus {
|
|
|
111
126
|
/** Machine-pool reference, if any. */
|
|
112
127
|
machinePool?: string;
|
|
113
128
|
/**
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
129
|
+
* The union of every entry in {@link labelSetMandatoryLabels}. Surfaced in
|
|
130
|
+
* heartbeat-side scaler capacity summaries for a peer that predates the
|
|
131
|
+
* per-label-set gate.
|
|
132
|
+
*
|
|
133
|
+
* @deprecated Use {@link labelSetMandatoryLabels}. On a scaler whose label
|
|
134
|
+
* sets declare different platforms this union names a taint no single set
|
|
135
|
+
* can satisfy, which is what made a mixed-platform scaler unroutable.
|
|
117
136
|
*/
|
|
118
137
|
mandatoryLabels: string[];
|
|
138
|
+
/**
|
|
139
|
+
* Labels a job MUST declare in `runsOn` to be allowed on each label set,
|
|
140
|
+
* index-aligned with `labelSets`. An empty entry means that set has no
|
|
141
|
+
* gate.
|
|
142
|
+
*/
|
|
143
|
+
labelSetMandatoryLabels: string[][];
|
|
144
|
+
/**
|
|
145
|
+
* True when this scaler was removed from the config and is draining: it
|
|
146
|
+
* accepts no new work, and disappears once its last agent goes away.
|
|
147
|
+
*/
|
|
148
|
+
retiring: boolean;
|
|
149
|
+
}>;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* What the coordinator holding an event agent needs to tear its provision
|
|
153
|
+
* down: enough to emit `kici.scaler.scale-down` with no local backend and no
|
|
154
|
+
* config entry.
|
|
155
|
+
*
|
|
156
|
+
* Recorded on both registration paths — the agent adopted from a peer and the
|
|
157
|
+
* one this coordinator spawned itself — so the teardown addresses the targets
|
|
158
|
+
* recorded at spawn either way.
|
|
159
|
+
*/
|
|
160
|
+
export interface AdoptedSpec {
|
|
161
|
+
scalerName: string;
|
|
162
|
+
provisioningTargets: string[];
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* The three knobs bounding how often a repeatedly failing external scaler is
|
|
166
|
+
* asked to provision again.
|
|
167
|
+
*/
|
|
168
|
+
export interface ProvisionBackoffSettings {
|
|
169
|
+
/** First deferral after one consecutive failure; doubles per further failure. */
|
|
170
|
+
baseMs: number;
|
|
171
|
+
/** Ceiling on the doubling. */
|
|
172
|
+
maxMs: number;
|
|
173
|
+
/** Consecutive failures past which a refusal names repeated failure. */
|
|
174
|
+
maxConsecutiveFailures: number;
|
|
175
|
+
}
|
|
176
|
+
/** Everything the {@link ScalerManager} is constructed from. */
|
|
177
|
+
export interface ScalerManagerDeps {
|
|
178
|
+
config: ScalerConfig;
|
|
179
|
+
backends: Array<{
|
|
180
|
+
name: string;
|
|
181
|
+
backend: ScalerBackend;
|
|
119
182
|
}>;
|
|
183
|
+
/**
|
|
184
|
+
* Read-only view of the agent registry, for the warm pool's deficit
|
|
185
|
+
* calculation. Structural on purpose — the scaler must not depend on the
|
|
186
|
+
* agent layer's concrete class.
|
|
187
|
+
*
|
|
188
|
+
* Both hosts pass one, and a host that skips it does not get an inert warm
|
|
189
|
+
* pool: the readiness count falls back to 0, so a configured pool never sees
|
|
190
|
+
* the agents it started and keeps filling until `warmCapacityRemaining`
|
|
191
|
+
* clamps it at `maxAgents` — starving the job-bound spawns that share the
|
|
192
|
+
* cap. Optional only because the unit tests drive the spawn path without a
|
|
193
|
+
* registry.
|
|
194
|
+
*/
|
|
195
|
+
agentRegistry?: {
|
|
196
|
+
findAvailable(requiredLabels: string[]): Array<{
|
|
197
|
+
agentId: string;
|
|
198
|
+
activeJobs: number;
|
|
199
|
+
registeredAt: number;
|
|
200
|
+
}>;
|
|
201
|
+
};
|
|
202
|
+
/**
|
|
203
|
+
* Id of the orchestrator instance this manager runs on. Rows this
|
|
204
|
+
* coordinator writes to the shared scaler tables are attributed to it, so
|
|
205
|
+
* every peer can tell which instance owns a given spawn.
|
|
206
|
+
*/
|
|
207
|
+
instanceId: string;
|
|
208
|
+
/** Callback for relaying scaler events with runId/jobId context. */
|
|
209
|
+
onScalerEvent?: (runId: string, jobId: string, event: ScalerEvent) => void;
|
|
210
|
+
/**
|
|
211
|
+
* Optional DB-backed state store. Tests omit it; production wires it
|
|
212
|
+
* up via the orchestrator-core bootstrap.
|
|
213
|
+
*/
|
|
214
|
+
stateStore?: ScalerStateStore;
|
|
215
|
+
/**
|
|
216
|
+
* Late-bound reserved-event emitter, resolved per call because the event
|
|
217
|
+
* router is built after the manager. Omitted where there is no event plane
|
|
218
|
+
* (the worker, and unit tests that never emit).
|
|
219
|
+
*/
|
|
220
|
+
eventEmitter?: () => ScalerEventEmitterLike;
|
|
221
|
+
/**
|
|
222
|
+
* Redemption-only claim store. Wired wherever the shared claim table is
|
|
223
|
+
* reachable, so a provisioning code minted by any coordinator can be
|
|
224
|
+
* redeemed here. Omitted where there is no database.
|
|
225
|
+
*/
|
|
226
|
+
claimStore?: ClaimStore;
|
|
227
|
+
/**
|
|
228
|
+
* Optional machine-ledger options. When `machinePools` are configured,
|
|
229
|
+
* the manager initializes a `MachineLedger` keyed off this directory and
|
|
230
|
+
* the orchestrator's instance id; reservations sum across orchestrators
|
|
231
|
+
* on the same host.
|
|
232
|
+
*/
|
|
233
|
+
machineLedger?: {
|
|
234
|
+
/** Override the on-disk ledger directory. Falls back to `KICI_MACHINE_LEDGER_DIR`. */
|
|
235
|
+
dir?: string;
|
|
236
|
+
/** Orchestrator instance id (used in ledger rows for ownership). */
|
|
237
|
+
instanceId: string;
|
|
238
|
+
};
|
|
239
|
+
/** Coordinator drain predicate. When true, requestScale() declines to spawn
|
|
240
|
+
* fresh capacity for the held Pending backlog. Defaults to never-draining. */
|
|
241
|
+
isDraining?: () => boolean;
|
|
242
|
+
/**
|
|
243
|
+
* Cluster-wide default deadline (ms) for a single `backend.spawn`. Always
|
|
244
|
+
* supplied (from config.scalerSpawnTimeoutMs); the per-org resolver, when
|
|
245
|
+
* present, overrides it per tenant.
|
|
246
|
+
*/
|
|
247
|
+
spawnTimeoutMs: number;
|
|
248
|
+
/**
|
|
249
|
+
* Optional per-org resolver for the spawn deadline: given the job's org
|
|
250
|
+
* (jobConfig.cacheOrgId, undefined for warm-pool spawns), returns the
|
|
251
|
+
* effective timeout, falling back to the cluster default internally. Wired
|
|
252
|
+
* in orchestrator-core (has DB); omitted by the worker + unit tests → the
|
|
253
|
+
* cluster default spawnTimeoutMs is used.
|
|
254
|
+
*/
|
|
255
|
+
resolveSpawnTimeoutMs?: (orgId: string | undefined) => Promise<number>;
|
|
256
|
+
/**
|
|
257
|
+
* Live resolver for the external-provision backoff knobs, read once per spawn
|
|
258
|
+
* request so an operator retunes a running cluster. Wired in
|
|
259
|
+
* orchestrator-core from `ClusterSettingsReader` + the config defaults;
|
|
260
|
+
* omitted by the worker and unit tests, which then get
|
|
261
|
+
* {@link DEFAULT_PROVISION_BACKOFF}.
|
|
262
|
+
*/
|
|
263
|
+
resolveProvisionBackoff?: () => Promise<ProvisionBackoffSettings>;
|
|
264
|
+
/**
|
|
265
|
+
* Constructs a backend for a scaler added by a config reload. Supplied by
|
|
266
|
+
* both hosts from the shared backend factory, so a reload can only build
|
|
267
|
+
* what startup already exercises. When omitted (unit tests), a reload that
|
|
268
|
+
* adds a scaler is rejected rather than silently ignored.
|
|
269
|
+
*
|
|
270
|
+
* `config` is the config being reloaded, not the boot-time one. The factory
|
|
271
|
+
* reads `defaults.resources` and the whole `firecracker` network block off
|
|
272
|
+
* it, so an added scaler built from a stale config would allocate IPs from
|
|
273
|
+
* the old CIDR and attach TAPs to the old bridge — silently unreachable.
|
|
274
|
+
*/
|
|
275
|
+
createBackend?: (entry: ScalerEntry, config: ScalerConfig) => Promise<ScalerBackend | null>;
|
|
120
276
|
}
|
|
121
277
|
export declare class ScalerManager {
|
|
122
278
|
private readonly backends;
|
|
123
279
|
private readonly backendRoles;
|
|
280
|
+
/**
|
|
281
|
+
* Scalers deleted from the config that still have agents running. They stay
|
|
282
|
+
* in `backends` — and so stay counted by `getGlobalActiveCount` and reachable
|
|
283
|
+
* for destroy / log forwarding — but are filtered out of routing, and are
|
|
284
|
+
* torn down once their last agent goes away.
|
|
285
|
+
*/
|
|
286
|
+
private readonly retiring;
|
|
287
|
+
/** When each retiring scaler was marked, for the drain-duration log. */
|
|
288
|
+
private readonly retiredAt;
|
|
289
|
+
/** Periodic sweep that tears down drained retiring backends. */
|
|
290
|
+
private retirementSweep;
|
|
124
291
|
/** Recent scaler spawn failures, surfaced by `kici-admin diagnose`. */
|
|
125
292
|
private readonly failureTracker;
|
|
293
|
+
/**
|
|
294
|
+
* Consecutive external-provision failures per scaler NAME, and the deferral
|
|
295
|
+
* they earned.
|
|
296
|
+
*
|
|
297
|
+
* Keyed by name rather than by backend type so one failing scaler never
|
|
298
|
+
* defers spawns for an unrelated one — two event scalers routinely drive two
|
|
299
|
+
* different providers, and an outage at one says nothing about the other.
|
|
300
|
+
*
|
|
301
|
+
* In-memory and leader-local, like `ScalerFailureTracker`: the state resets on
|
|
302
|
+
* restart, which is the conservative direction (a fresh coordinator retries
|
|
303
|
+
* once immediately rather than inheriting a deferral it never observed).
|
|
304
|
+
*/
|
|
305
|
+
private readonly provisionFailures;
|
|
306
|
+
private readonly resolveProvisionBackoff;
|
|
126
307
|
private globalMaxAgents;
|
|
127
308
|
/** Per-scaler resource caps (`{ maxCpu, maxMemoryBytes }`), keyed by scaler name. */
|
|
128
309
|
private readonly resourceCaps;
|
|
@@ -151,12 +332,31 @@ export declare class ScalerManager {
|
|
|
151
332
|
private reservationLock;
|
|
152
333
|
/**
|
|
153
334
|
* File-backed cross-process ledger for named machine pools (optional).
|
|
154
|
-
*
|
|
155
|
-
*
|
|
335
|
+
* Initialized when at least one scaler entry references a pool — at
|
|
336
|
+
* construction, or on the reload that first declares one.
|
|
337
|
+
*/
|
|
338
|
+
private machineLedger;
|
|
339
|
+
/** Ledger options, retained so a reload that first declares a pool can build one. */
|
|
340
|
+
private readonly machineLedgerOptions;
|
|
341
|
+
/** True between `start()` and `shutdownAll()`; a ledger built later must be started too. */
|
|
342
|
+
private started;
|
|
343
|
+
/** Builds a backend for a scaler a reload added. Absent in unit tests. */
|
|
344
|
+
private readonly createBackend?;
|
|
345
|
+
/**
|
|
346
|
+
* Read-only agent-registry view backing the warm pool's deficit pass.
|
|
347
|
+
* Absent in unit tests and in any host that has no registry.
|
|
156
348
|
*/
|
|
157
|
-
private readonly
|
|
349
|
+
private readonly agentRegistry?;
|
|
158
350
|
/** Per-scaler URL overrides from config, keyed by scaler name */
|
|
159
351
|
private readonly scalerUrls;
|
|
352
|
+
/**
|
|
353
|
+
* Per-scaler `provisioningTargets` from config, keyed by scaler name.
|
|
354
|
+
*
|
|
355
|
+
* Copied onto every spawn row, because it is the only thing that lets a
|
|
356
|
+
* coordinator with no config entry for the scaler address the customer's
|
|
357
|
+
* teardown workflow when the agent it adopted disconnects.
|
|
358
|
+
*/
|
|
359
|
+
private readonly scalerProvisioningTargets;
|
|
160
360
|
/**
|
|
161
361
|
* Tracks agents being spawned but not yet registered via WS.
|
|
162
362
|
* Keyed by pre-generated agentId.
|
|
@@ -166,6 +366,32 @@ export declare class ScalerManager {
|
|
|
166
366
|
* Maps registered agentId to backendName for lifecycle events.
|
|
167
367
|
*/
|
|
168
368
|
private readonly managedAgentIndex;
|
|
369
|
+
/**
|
|
370
|
+
* Agents this instance pre-spawned to wait for work, rather than for a
|
|
371
|
+
* specific queued job. The agent registry tracks readiness (`activeJobs`,
|
|
372
|
+
* `registeredAt`) and has never tracked provenance — it cannot derive this,
|
|
373
|
+
* and the spawning entry that carries it is deleted at registration.
|
|
374
|
+
*
|
|
375
|
+
* This is not a second copy of the pool's readiness. It is provenance, which
|
|
376
|
+
* nothing else knows, and only the reaper reads it: it may destroy an agent
|
|
377
|
+
* the pool itself created, never one that registered for a bound job whose
|
|
378
|
+
* dispatch has not yet arrived.
|
|
379
|
+
*/
|
|
380
|
+
private readonly warmAgents;
|
|
381
|
+
/**
|
|
382
|
+
* Warm agents whose destroy is issued but has not settled yet.
|
|
383
|
+
*
|
|
384
|
+
* An agent leaves `warmAgents` only once its destroy resolves, so until then
|
|
385
|
+
* `listIdle` keeps returning it and the next reap pass — 30 seconds later,
|
|
386
|
+
* or a reload that lowers the target — selects the very same agent again. A
|
|
387
|
+
* backend whose teardown outlives a tick would be handed a second destroy for
|
|
388
|
+
* an agent it is already destroying, and every extra pass would count another
|
|
389
|
+
* `kici_orch_scaler_warm_pool_reaped_total`. One entry per agent, cleared
|
|
390
|
+
* when the destroy settles either way.
|
|
391
|
+
*/
|
|
392
|
+
private readonly warmDestroying;
|
|
393
|
+
/** Agents this instance adopted from another instance's spawn record. */
|
|
394
|
+
private readonly adoptedAgents;
|
|
169
395
|
/**
|
|
170
396
|
* Active log forwarders for scaler-managed agents (container/bare-metal).
|
|
171
397
|
* Keyed by agentId. Each forwarder consumes a LogCapture stream.
|
|
@@ -203,6 +429,11 @@ export declare class ScalerManager {
|
|
|
203
429
|
/** Trailing debounce window for {@link notifyCapacityFreed}. */
|
|
204
430
|
private static readonly CAPACITY_FREED_DEBOUNCE_MS;
|
|
205
431
|
private readonly warmPool;
|
|
432
|
+
/**
|
|
433
|
+
* The callback set handed to `warmPool`, kept so the manager's own view of
|
|
434
|
+
* pool capacity and reapability is reachable without a live agent registry.
|
|
435
|
+
*/
|
|
436
|
+
private readonly warmPoolCallbacks;
|
|
206
437
|
/**
|
|
207
438
|
* Optional DB-backed state store. When wired (production path), every
|
|
208
439
|
* mutation to `spawningAgents` / `agentJobCorrelation` / `reservations`
|
|
@@ -211,53 +442,174 @@ export declare class ScalerManager {
|
|
|
211
442
|
* Unit tests can omit the store and operate from in-memory Maps only.
|
|
212
443
|
*/
|
|
213
444
|
private readonly stateStore?;
|
|
445
|
+
/** Id of the orchestrator instance this manager runs on. */
|
|
446
|
+
private readonly instanceId;
|
|
447
|
+
/**
|
|
448
|
+
* Late-bound reserved-event emitter. Undefined where there is no event
|
|
449
|
+
* plane (the worker, and unit tests that never emit).
|
|
450
|
+
*/
|
|
451
|
+
private readonly eventEmitter?;
|
|
452
|
+
/**
|
|
453
|
+
* Redemption-only claim store, reading the shared `scaler_pending_claims`
|
|
454
|
+
* table. Undefined where there is no database.
|
|
455
|
+
*/
|
|
456
|
+
private readonly claimStore?;
|
|
214
457
|
/** Cluster-wide default deadline (ms) for a single `backend.spawn`. */
|
|
215
458
|
private readonly spawnTimeoutMs;
|
|
216
459
|
/** Optional per-org spawn-deadline resolver (production path; DB-backed). */
|
|
217
460
|
private readonly resolveSpawnTimeoutMs?;
|
|
218
|
-
constructor(deps:
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
461
|
+
constructor(deps: ScalerManagerDeps);
|
|
462
|
+
/**
|
|
463
|
+
* Pre-spawn one warm agent for a pool's label set.
|
|
464
|
+
*
|
|
465
|
+
* The pool's counterpart of `requestScale`, with the job term of every
|
|
466
|
+
* resolution absent: no bound job, no run, and resources resolved
|
|
467
|
+
* from the label set (then the scaler defaults) alone. It takes the same
|
|
468
|
+
* reservations through the same {@link reserveForSpawn} helper — a warm
|
|
469
|
+
* agent occupies the same cpu and memory as a job-bound one, and on an event
|
|
470
|
+
* backend it claims a cluster slot too, so a pool can no longer carry a
|
|
471
|
+
* scaler past `maxAgents` fleet-wide.
|
|
472
|
+
*
|
|
473
|
+
* A refused reservation is reported back to the pool as a failed spawn, so
|
|
474
|
+
* the in-flight slot it holds is released and the deficit is retried on the
|
|
475
|
+
* next tick rather than the pool stalling below target.
|
|
476
|
+
*/
|
|
477
|
+
private spawnWarmAgent;
|
|
478
|
+
/**
|
|
479
|
+
* Whether a warm pool for this label set can count its own agents.
|
|
480
|
+
*
|
|
481
|
+
* The pool measures readiness with `AgentRegistry.findAvailable(labels)`, and
|
|
482
|
+
* that query applies the agent's `mandatoryLabels` gate: an agent is dropped
|
|
483
|
+
* unless every label in its gate also appears in the query. A scaler-spawned
|
|
484
|
+
* agent's gate is `labelSetMandatoryLabels()` for the set it was spawned for,
|
|
485
|
+
* which unions the configured gate with the pool's platform taints.
|
|
486
|
+
* `warmPoolQueryLabels` puts the structured taints into the query, so that
|
|
487
|
+
* part of the gate is satisfied by construction, and the legacy shim derives
|
|
488
|
+
* from this label set's own labels, so it is satisfied by construction too.
|
|
489
|
+
*
|
|
490
|
+
* That leaves one residual case: a configured `mandatoryLabels` entry absent
|
|
491
|
+
* from the label set, which `scaler/config.ts` validation already rejects.
|
|
492
|
+
* The check stays as the fail-safe — without it a pool in that state would
|
|
493
|
+
* spawn to `maxAgents` with `ready` stuck at 0, starving the on-demand spawns
|
|
494
|
+
* that share the cap. A refused pool is inert, exactly as it is before it
|
|
495
|
+
* fills at all — never partially filled.
|
|
496
|
+
*
|
|
497
|
+
* The gate derives from `declaredLabels` and is compared against
|
|
498
|
+
* `queryLabels`. Deriving it from `queryLabels` would fold a structured taint
|
|
499
|
+
* back in as a legacy one; comparing it against `declaredLabels` alone would
|
|
500
|
+
* refuse every structurally-tainted pool.
|
|
501
|
+
*/
|
|
502
|
+
private warmPoolLabelSetFillable;
|
|
503
|
+
/**
|
|
504
|
+
* Whether this scaler pre-spawned (warm-filled) the agent, i.e. whether
|
|
505
|
+
* {@link canPrespawnedAgentServe} can ever answer false for it. A caller that
|
|
506
|
+
* picks one agent at a time uses this to decide whether the suitability check
|
|
507
|
+
* is worth carrying at all.
|
|
508
|
+
*/
|
|
509
|
+
isPrespawnedAgent(agentId: string): boolean;
|
|
510
|
+
/**
|
|
511
|
+
* Whether a pre-spawned (warm) agent can serve this job.
|
|
512
|
+
*
|
|
513
|
+
* A warm agent is generic by construction: it was started before the job
|
|
514
|
+
* existed, at the pool's declared shape and running the pool's agent image.
|
|
515
|
+
* Both are applied when the agent starts and cannot be changed afterwards —
|
|
516
|
+
* nothing in the agent reads a job's `resources`, and an already-running
|
|
517
|
+
* container cannot become a different image. So a job that needs something
|
|
518
|
+
* else must get its own agent instead of silently running with the pool's.
|
|
519
|
+
*
|
|
520
|
+
* Returns true for any agent this scaler did not pre-spawn: a job-bound or
|
|
521
|
+
* static agent is not this predicate's business. {@link isPrespawnedAgent}
|
|
522
|
+
* answers that half on its own.
|
|
523
|
+
*
|
|
524
|
+
* Only the fields the job actually declares are compared. `resolveEffective`
|
|
525
|
+
* layers a job's declaration over the label set and the scaler defaults field
|
|
526
|
+
* by field, and the warm agent's own reservation is what those two layers
|
|
527
|
+
* resolve to — so a field the job leaves out already matches whatever the
|
|
528
|
+
* agent was started at, and comparing it would refuse a job that fits.
|
|
529
|
+
*
|
|
530
|
+
* A field the job DOES declare must match exactly, in both directions. A
|
|
531
|
+
* bigger ready agent is not a superset: limits are what the kernel enforces
|
|
532
|
+
* on the running agent, so a job asking to be capped at 2 cpus, served by an
|
|
533
|
+
* agent started at 4, runs uncapped. That is the same wrong execution
|
|
534
|
+
* environment as the under-provisioned case, in the other direction, so `>=`
|
|
535
|
+
* is not a safe relaxation.
|
|
536
|
+
*
|
|
537
|
+
* Both sides of the shape are compared, not just `requests`. The two answer
|
|
538
|
+
* different questions — `requests` is what the agent bills against the caps,
|
|
539
|
+
* `limits` is what the kernel enforces — and a declaration may legally set
|
|
540
|
+
* them apart, so a job matched on `requests` alone can still land on an agent
|
|
541
|
+
* capped somewhere else entirely.
|
|
542
|
+
*/
|
|
543
|
+
canPrespawnedAgentServe(agentId: string, job: {
|
|
544
|
+
resources?: ResourceRequest;
|
|
545
|
+
hasOwnContainerImage: boolean;
|
|
546
|
+
}): boolean;
|
|
547
|
+
/**
|
|
548
|
+
* Whether one side (`requests` or `limits`) of a job's declared shape leaves
|
|
549
|
+
* the agent's resolved side unchanged.
|
|
550
|
+
*
|
|
551
|
+
* True when the job declares nothing on this side, and true when `resolved`
|
|
552
|
+
* is absent — both mean "this cannot be told", and the predicate's rule is to
|
|
553
|
+
* fail open rather than strand a job.
|
|
554
|
+
*/
|
|
555
|
+
private shapeSideMatches;
|
|
556
|
+
/**
|
|
557
|
+
* Whether a warm pool for this label set has a declared agent shape.
|
|
558
|
+
*
|
|
559
|
+
* A warm agent starts before any job exists, so its cpu and memory come from
|
|
560
|
+
* the label set (or the scaler defaults) and from nowhere else. Both are
|
|
561
|
+
* applied when the agent starts and cannot be changed afterwards, so an
|
|
562
|
+
* undeclared shape means the pool reserves a guess and no job can be tested
|
|
563
|
+
* for fit against it. Refusing is the fail-safe reading, and it is what makes
|
|
564
|
+
* a pool predictable — on a cloud backend it is what lets an operator reason
|
|
565
|
+
* about instance size and cost at all.
|
|
566
|
+
*
|
|
567
|
+
* Limits alone count as a declaration: `resolveEffective` mirrors limits into
|
|
568
|
+
* requests, so a label set naming only `limits` still resolves to a concrete
|
|
569
|
+
* shape.
|
|
570
|
+
*
|
|
571
|
+
* A refused pool is inert, exactly as it was before it filled at all — never
|
|
572
|
+
* partially filled.
|
|
573
|
+
*
|
|
574
|
+
* A declaration this cannot resolve is refused the same way, and must never
|
|
575
|
+
* throw: the config schema types `resources.*.memory` as a plain string, so
|
|
576
|
+
* an unparseable value such as `4Gi` reaches `resolveEffective` and
|
|
577
|
+
* `parseMemoryString` rejects it. This runs inside the constructor and inside
|
|
578
|
+
* the commit block of {@link reload}, where an exception would abort
|
|
579
|
+
* orchestrator startup or leave a half-applied config behind.
|
|
580
|
+
*/
|
|
581
|
+
private warmPoolShapeDeclared;
|
|
582
|
+
/**
|
|
583
|
+
* Agents this backend may still start before it reaches the scaler's
|
|
584
|
+
* `maxAgents` or the orchestrator's `globalMaxAgents`, whichever binds first.
|
|
585
|
+
*
|
|
586
|
+
* The warm pool clamps its deficit to this, so a pool that has reached a
|
|
587
|
+
* count cap asks for nothing at all rather than `size` spawns on every tick.
|
|
588
|
+
* Only the count caps are visible here: a pool with count headroom but no
|
|
589
|
+
* cpu/memory headroom still asks, and {@link reserveForSpawn} refuses each
|
|
590
|
+
* request under the reservation lock. Those refusals cost a log line and a
|
|
591
|
+
* `spawn_refusals` increment per tick, never a spawned agent.
|
|
592
|
+
*
|
|
593
|
+
* It is a best-effort read, not a reservation: the authority is
|
|
594
|
+
* {@link reserveForSpawn}, which every warm pre-spawn goes through and which
|
|
595
|
+
* re-checks both count caps under the reservation lock. The two backend
|
|
596
|
+
* families read differently here. A local backend (container, bare-metal,
|
|
597
|
+
* Firecracker) pins its compute to this host, so `getActiveCount()` is the
|
|
598
|
+
* whole population and the room it reports is exact. An **event** backend is
|
|
599
|
+
* capped cluster-wide — the reserve sequence claims a spawn row under
|
|
600
|
+
* `withScalerCapLock` — while `getActiveCount()` sees only the provisions
|
|
601
|
+
* this coordinator tracks, so the room reported here counts slots a peer may
|
|
602
|
+
* already hold. The cluster-wide cap is still enforced, by that claim rather
|
|
603
|
+
* than by this clamp.
|
|
604
|
+
*/
|
|
605
|
+
private warmCapacityRemaining;
|
|
606
|
+
/**
|
|
607
|
+
* Publish the warm-pool gauges from the pool's current fill state.
|
|
608
|
+
*
|
|
609
|
+
* Called after each pool tick and after a config reload, so an operator sees
|
|
610
|
+
* a newly configured target immediately rather than at the next tick.
|
|
611
|
+
*/
|
|
612
|
+
private publishWarmPoolGauges;
|
|
261
613
|
/** Per-scaler mandatoryLabels (taint-style opt-in gate). */
|
|
262
614
|
private readonly scalerMandatoryLabels;
|
|
263
615
|
/** Declared structured platform per scaler (undefined = host-derive / linux default). */
|
|
@@ -280,12 +632,63 @@ export declare class ScalerManager {
|
|
|
280
632
|
*/
|
|
281
633
|
private resolveScalerPlatform;
|
|
282
634
|
/**
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
635
|
+
* The plain platform-taint tokens (`windows`, `macos`, `arm64`) for a scaler,
|
|
636
|
+
* derived from the same resolved platform `labelSetMandatoryLabels` gates on.
|
|
637
|
+
*
|
|
638
|
+
* This is the single source for the taint: it feeds the matcher's view of the
|
|
639
|
+
* label sets, the taint gate, the labels the spawned agent registers with, and
|
|
640
|
+
* the warm-pool readiness query. Deriving it anywhere else — in a backend, say
|
|
641
|
+
* — is what let the gate and the agent's labels disagree, so a tainted pool
|
|
642
|
+
* spawned agents `AgentRegistry.findAvailable` could never return.
|
|
643
|
+
*
|
|
644
|
+
* `backendType` is passed rather than looked up because the constructor
|
|
645
|
+
* configures warm pools from config entries whose backend may not be indexed.
|
|
646
|
+
*/
|
|
647
|
+
private platformTaintsFor;
|
|
648
|
+
/**
|
|
649
|
+
* The labels a warm pool measures its readiness with: the declared label set
|
|
650
|
+
* plus the pool's platform taints.
|
|
651
|
+
*
|
|
652
|
+
* `AgentRegistry.findAvailable` drops an agent unless its whole
|
|
653
|
+
* `mandatoryLabels` gate appears in the query, and a tainted pool's gate
|
|
654
|
+
* carries the taints — so a query built from the declared labels alone counts
|
|
655
|
+
* none of the pool's own agents and the pool fills to `maxAgents` with `ready`
|
|
656
|
+
* stuck at 0. Both construction sites (the constructor and the reload path)
|
|
657
|
+
* call this so the two can never diverge.
|
|
658
|
+
*
|
|
659
|
+
* These labels are for the QUERY only. The pool is keyed by, and spawns with,
|
|
660
|
+
* the DECLARED label set: a backend matches the requested set against its own
|
|
661
|
+
* `labelSets` to resolve the image / binary / VM config and throws on a set it
|
|
662
|
+
* does not have, and every in-flight release path normalizes the spawn labels
|
|
663
|
+
* back to the map key.
|
|
664
|
+
*/
|
|
665
|
+
private warmPoolQueryLabels;
|
|
666
|
+
/**
|
|
667
|
+
* Taint gate for ONE of a scaler's label sets: the configured
|
|
668
|
+
* `mandatoryLabels` plus the platform taints derived from the pool's resolved
|
|
669
|
+
* structured platform, unioned with the legacy plain-label denylist shim read
|
|
670
|
+
* from THIS label set alone.
|
|
671
|
+
*
|
|
672
|
+
* This is the gate every routing and stamping decision uses. Deriving the
|
|
673
|
+
* legacy shim per label set is what keeps a mixed-platform scaler routable: a
|
|
674
|
+
* pool declaring `[linux, gpu]` alongside `[macos, xcode]` gates only the
|
|
675
|
+
* second set on `macos`, where the union would gate both and leave the linux
|
|
676
|
+
* set unreachable by any sensible job.
|
|
677
|
+
*
|
|
678
|
+
* `backend` carries only `type` because the label set is passed in — a caller
|
|
679
|
+
* that holds one label set never has to reach for the whole backend.
|
|
680
|
+
*/
|
|
681
|
+
private labelSetMandatoryLabels;
|
|
682
|
+
/**
|
|
683
|
+
* The scaler-wide taint gate: {@link labelSetMandatoryLabels} unioned across
|
|
684
|
+
* every label set the backend declares.
|
|
685
|
+
*
|
|
686
|
+
* Its only remaining consumers are the deprecated scaler-wide fields — the
|
|
687
|
+
* `mandatoryLabels` entry on `ScalerStatus['backends']` and the peer
|
|
688
|
+
* scaler-capacity summary — which stay populated for a peer that predates the
|
|
689
|
+
* per-label-set gate. Do NOT gate routing or stamp an agent with this: on a
|
|
690
|
+
* scaler whose label sets declare different platforms the union names a taint
|
|
691
|
+
* no single set can satisfy.
|
|
289
692
|
*/
|
|
290
693
|
private effectiveMandatoryLabels;
|
|
291
694
|
/**
|
|
@@ -298,8 +701,32 @@ export declare class ScalerManager {
|
|
|
298
701
|
* (declared field wins, else host-derive for bare-metal, else linux), and the
|
|
299
702
|
* mandatory labels combine the per-scaler config gate with the platform taint,
|
|
300
703
|
* so the label matcher applies the gate alongside subset matching.
|
|
704
|
+
*
|
|
705
|
+
* Retiring scalers are skipped: they were removed from the config and must
|
|
706
|
+
* take no new work while their remaining agents finish.
|
|
301
707
|
*/
|
|
302
708
|
private getEnrichedScalerEntries;
|
|
709
|
+
/**
|
|
710
|
+
* Mark a scaler retiring: routing stops immediately, but job-bound agents are
|
|
711
|
+
* left to finish their work. The backend is torn down by
|
|
712
|
+
* `sweepRetiredBackends` once its active count reaches zero.
|
|
713
|
+
*/
|
|
714
|
+
private retireBackend;
|
|
715
|
+
/**
|
|
716
|
+
* Tear down every retiring backend that has drained. Runs on an interval and
|
|
717
|
+
* after each agent teardown, so a wind-down finishes promptly without a
|
|
718
|
+
* dedicated per-backend watcher.
|
|
719
|
+
*/
|
|
720
|
+
private sweepRetiredBackends;
|
|
721
|
+
/**
|
|
722
|
+
* True while any spawn this manager started for `name` has neither registered
|
|
723
|
+
* nor failed — including one still queued behind the spawn semaphore, which
|
|
724
|
+
* has not reached `backend.spawn` at all. Such a spawn counts toward the
|
|
725
|
+
* scaler's drain even though the backend reports no active agent.
|
|
726
|
+
*/
|
|
727
|
+
private hasSpawnInFlight;
|
|
728
|
+
/** Drop every trace of a scaler: the backend and all its per-scaler metadata. */
|
|
729
|
+
private forgetScaler;
|
|
303
730
|
/**
|
|
304
731
|
* Main entry point: called by the Dispatcher when no agent is available.
|
|
305
732
|
*
|
|
@@ -317,7 +744,40 @@ export declare class ScalerManager {
|
|
|
317
744
|
* surface as an unhandled rejection.
|
|
318
745
|
*/
|
|
319
746
|
private runSpawnWithTimeout;
|
|
320
|
-
requestScale(labels: string[], jobId: string, runId: string, excludeLabels?: string[], resources?: ResourceRequest, orgId?: string
|
|
747
|
+
requestScale(labels: string[], jobId: string, runId: string, excludeLabels?: string[], resources?: ResourceRequest, orgId?: string,
|
|
748
|
+
/**
|
|
749
|
+
* The job's resolved container image + registry credentials, when it
|
|
750
|
+
* declared one. Threaded to the backend so it can spawn the job's own
|
|
751
|
+
* image instead of the pool's fixed agent image.
|
|
752
|
+
*/
|
|
753
|
+
containerSpawn?: ResolvedContainerSpawn): Promise<ScaleResult>;
|
|
754
|
+
/**
|
|
755
|
+
* Take every reservation one spawn needs, or take none at all.
|
|
756
|
+
*
|
|
757
|
+
* The single reserve sequence both spawn paths share — the job-bound
|
|
758
|
+
* `requestScale` and the warm pool's pre-spawn. The only difference between
|
|
759
|
+
* them is `boundJobId`, which is `undefined` for a pre-spawn; a warm agent
|
|
760
|
+
* costs the same host resources as a job-bound one, so it takes the same
|
|
761
|
+
* reservations.
|
|
762
|
+
*
|
|
763
|
+
* Ordering, and why:
|
|
764
|
+
* - Count caps and the in-memory resource reservation run under the
|
|
765
|
+
* in-process reservation lock, so the check and the reserve are atomic
|
|
766
|
+
* against a concurrent caller. A local backend counts its own host's
|
|
767
|
+
* agents in process, which is what its cap means: the compute is pinned to
|
|
768
|
+
* this machine. An event backend provisions cloud instances any
|
|
769
|
+
* coordinator can start, so its cap is counted — and its slot claimed —
|
|
770
|
+
* cluster-wide, inside one advisory-locked transaction. The in-process
|
|
771
|
+
* lock stays the outer guard so the two never contend.
|
|
772
|
+
* - The cross-process machine-pool ledger is taken outside that lock,
|
|
773
|
+
* because the ledger has its own cross-process mutex; holding both at once
|
|
774
|
+
* buys nothing.
|
|
775
|
+
*
|
|
776
|
+
* Every refusal rolls back exactly what it already took, so a `false` return
|
|
777
|
+
* leaves no reservation behind. On success the caller owns the release,
|
|
778
|
+
* which is always `releaseAll(agentId)`.
|
|
779
|
+
*/
|
|
780
|
+
private reserveForSpawn;
|
|
321
781
|
/**
|
|
322
782
|
* Run a critical section under the reservation lock. Serializes concurrent
|
|
323
783
|
* `requestScale()` calls so the check+reserve sequence is atomic.
|
|
@@ -410,28 +870,245 @@ export declare class ScalerManager {
|
|
|
410
870
|
* (`dispatchBoundJob` → `dequeueById`) both reject queued jobs whose
|
|
411
871
|
* `runsOn` does not include every gate label.
|
|
412
872
|
*
|
|
413
|
-
* Returns `null` for static
|
|
414
|
-
*
|
|
873
|
+
* Returns `null` for a static agent — no spawning entry exists, and no other
|
|
874
|
+
* instance's spawn record claims the agent either. A warm-pool spawn is not
|
|
875
|
+
* that case: its spawning entry (and durable row) survives until it
|
|
876
|
+
* registers, so it resolves with `mandatoryLabels` and no `boundJobId`, which
|
|
877
|
+
* is exactly what marks the agent warm on `register.ack`.
|
|
878
|
+
*
|
|
879
|
+
* A local hit (this instance spawned the agent) is answered from memory. A
|
|
880
|
+
* miss falls back to the shared spawn record: on an HA cluster behind one
|
|
881
|
+
* endpoint the agent routinely reaches an instance that did not spawn it,
|
|
882
|
+
* and adopting is the normal path, not an exception. Adoption is a
|
|
883
|
+
* conditional UPDATE, so exactly one instance can win.
|
|
415
884
|
*/
|
|
416
|
-
onAgentRegistered(agentId: string, labels: string[]): {
|
|
885
|
+
onAgentRegistered(agentId: string, labels: string[]): Promise<{
|
|
417
886
|
boundJobId?: string;
|
|
418
887
|
mandatoryLabels: string[];
|
|
419
|
-
} | null
|
|
888
|
+
} | null>;
|
|
889
|
+
/**
|
|
890
|
+
* Reclaim the host-local compute of a scaler-spawned agent this coordinator
|
|
891
|
+
* refused for want of a spawn record.
|
|
892
|
+
*
|
|
893
|
+
* Refusing closes the WS before registration completes, so `onAgentDisconnected`
|
|
894
|
+
* never runs for that agent, and `managedAgentIndex` never held it in the first
|
|
895
|
+
* place — the two writes that populate it (`completeLocalRegistration` and the
|
|
896
|
+
* adoption branch) both come after this point. So even the disconnect hook is a
|
|
897
|
+
* no-op here: it reads that index, finds nothing, and takes the static-agent
|
|
898
|
+
* exit. The agent keeps its VM or host process, reconnects, is refused again,
|
|
899
|
+
* and loops until an operator stops it by hand.
|
|
900
|
+
*
|
|
901
|
+
* **This force-destroys a live instance, so read what makes it safe.**
|
|
902
|
+
*
|
|
903
|
+
* - **Local backends only.** An event agent's compute is a customer cloud
|
|
904
|
+
* instance, reachable from any coordinator, and its missing adoption is
|
|
905
|
+
* ambiguous: `adoptSpawningAgent` resolves `null` both when no row exists AND
|
|
906
|
+
* when a peer coordinator already holds the row, so an event agent that is
|
|
907
|
+
* alive and legitimately owned elsewhere reaches this exact branch. Tearing
|
|
908
|
+
* it down there would destroy a peer's healthy provision. A local backend's
|
|
909
|
+
* compute cannot be reached that way: it lives on the host that spawned it.
|
|
910
|
+
* - **Host-local evidence decides, not this verdict.** The verdict alone is NOT
|
|
911
|
+
* evidence the agent is this host's: `adoptSpawningAgent` adopts `event` rows
|
|
912
|
+
* only, so a `container` / `bare-metal` / `firecracker` agent that reconnects
|
|
913
|
+
* to a peer behind a shared endpoint is refused here even though its spawn
|
|
914
|
+
* row is intact and its compute is alive on the peer's host. What keeps that
|
|
915
|
+
* harmless is that each backend reclaims only what its own tracking or its
|
|
916
|
+
* own on-host artifacts name — never a shared database row, which on an HA
|
|
917
|
+
* pair names an identically-configured scaler on both hosts. A wrongly-routed
|
|
918
|
+
* agent id finds nothing here and nothing is touched.
|
|
919
|
+
* - **Never on a transient failure.** The caller must have positive evidence
|
|
920
|
+
* the agent is unowned. A store read that merely *failed* is not evidence —
|
|
921
|
+
* the record may exist and be perfectly healthy.
|
|
922
|
+
*
|
|
923
|
+
* Fire-and-forget, matching `onAgentDisconnected`'s own teardown: the caller
|
|
924
|
+
* refuses the registration immediately rather than holding the agent's socket
|
|
925
|
+
* open for a VM shutdown.
|
|
926
|
+
*/
|
|
927
|
+
private reclaimUnownedScalerAgent;
|
|
928
|
+
/**
|
|
929
|
+
* Finish registration for an agent this instance spawned itself, from the
|
|
930
|
+
* in-memory spawning entry.
|
|
931
|
+
*/
|
|
932
|
+
private completeLocalRegistration;
|
|
420
933
|
/**
|
|
421
934
|
* Called from agent-handler.ts when an agent disconnects.
|
|
422
935
|
*/
|
|
423
936
|
onAgentDisconnected(agentId: string): void;
|
|
424
937
|
/**
|
|
425
|
-
*
|
|
426
|
-
*
|
|
427
|
-
*
|
|
938
|
+
* Tear down a stranded provision the `EventProvisionReaper` found.
|
|
939
|
+
*
|
|
940
|
+
* Public because the reaper is leader-gated and lives outside the manager,
|
|
941
|
+
* while the emit and the row delete must stay in one place. The reason comes
|
|
942
|
+
* from the reaper's verdict — a provision whose agent never registered is a
|
|
943
|
+
* `spawn-timeout`, one whose agent went away is a `heartbeat-timeout` — so
|
|
944
|
+
* unlike the disconnect path this does not default to `shutdown`.
|
|
945
|
+
*
|
|
946
|
+
* `kici_orch_scaler_external_provision_timeout_total` counts the spawn-timeout
|
|
947
|
+
* arm. Nothing else increments it in production: `runSpawnWithTimeout` bounds
|
|
948
|
+
* only the `spawn()` call, and an event backend's `spawn()` returns the moment
|
|
949
|
+
* the scale-up event is emitted.
|
|
950
|
+
*
|
|
951
|
+
* The spawn-timeout arm is also where an external provisioning failure enters
|
|
952
|
+
* the ordinary failure machinery. The local backends report a dead spawn
|
|
953
|
+
* through the `scaler.failed` event their `spawn()` emits; an event scaler's
|
|
954
|
+
* `spawn()` succeeds by definition (it only emits a scale-up), so the failure
|
|
955
|
+
* is not observable until the provision it asked for never registers — which
|
|
956
|
+
* is exactly this verdict. Synthesizing the event here gives the fleet
|
|
957
|
+
* counter, the operator's `diagnose scaler` view, and `last_provisioning_error`
|
|
958
|
+
* the same input every other backend already supplies, so a job whose external
|
|
959
|
+
* provisioning failed settles with the provisioning cause instead of a
|
|
960
|
+
* label-mismatch complaint that sends its operator to fix a correct `runsOn`.
|
|
961
|
+
*/
|
|
962
|
+
emitOrphanScaleDown(candidate: ReapCandidate, reason: ScaleDownReason): Promise<void>;
|
|
963
|
+
/**
|
|
964
|
+
* Refuse a spawn request while the scaler is inside its backoff window.
|
|
965
|
+
*
|
|
966
|
+
* Returns the refusal to hand back, or null to proceed. The message names how
|
|
967
|
+
* many consecutive failures produced the deferral and when it lifts. It
|
|
968
|
+
* reaches the operator through this method's own log line: the dispatcher
|
|
969
|
+
* branches on `action` alone and discards `reason`, so a job deferred here
|
|
970
|
+
* carries no record of the deferral on its queue row. The job whose provision
|
|
971
|
+
* actually failed is the one that gets `last_provisioning_error`, written by
|
|
972
|
+
* the `scaler.failed` this backoff's own trigger emits.
|
|
973
|
+
*
|
|
974
|
+
* The knobs are read here per request rather than per process, so an operator
|
|
975
|
+
* lowering the failure limit mid-outage sees the repeated-failure wording on
|
|
976
|
+
* the next refusal. The window itself was armed by `recordProvisionFailure`
|
|
977
|
+
* from the base and ceiling live at that moment, so a retuned window applies
|
|
978
|
+
* from the next recorded failure.
|
|
979
|
+
*/
|
|
980
|
+
private provisionBackoffRefusal;
|
|
981
|
+
/**
|
|
982
|
+
* Record a consecutive provisioning failure for a scaler and arm its next
|
|
983
|
+
* deferral.
|
|
984
|
+
*
|
|
985
|
+
* The delay doubles per consecutive failure and is capped, so a provider
|
|
986
|
+
* outage settles into a steady retry cadence rather than either hammering the
|
|
987
|
+
* provider or growing without bound. `2 ** (n - 1)` is bounded by the cap
|
|
988
|
+
* before it is used, so a long outage cannot overflow the shift.
|
|
989
|
+
*/
|
|
990
|
+
private recordProvisionFailure;
|
|
991
|
+
/**
|
|
992
|
+
* Claim a dead provision as counted, or report that someone already did.
|
|
993
|
+
*
|
|
994
|
+
* The same provision is seen twice on the leader — once by its own
|
|
995
|
+
* stale-spawn prune, once by the leader-gated reaper — and every consequence
|
|
996
|
+
* of a failure is counted per provision, not per observer. The claim is
|
|
997
|
+
* synchronous and happens before any await, so two observers arriving in the
|
|
998
|
+
* same tick cannot both pass it.
|
|
999
|
+
*/
|
|
1000
|
+
private claimFailedProvision;
|
|
1001
|
+
/**
|
|
1002
|
+
* Report one dead external provision, wherever this coordinator noticed it.
|
|
1003
|
+
*
|
|
1004
|
+
* Everything a failed provision owes an operator happens here: the fleet
|
|
1005
|
+
* spawn-failure counter, the structured warn, the `ScalerFailureTracker`
|
|
1006
|
+
* record `kici-admin diagnose scaler` reads, the `last_provisioning_error`
|
|
1007
|
+
* that tells the waiting job why it never ran, and the backoff.
|
|
1008
|
+
*
|
|
1009
|
+
* It is deliberately reachable from BOTH observers rather than from the
|
|
1010
|
+
* reaper alone. `ScalerFailureTracker` and `provisionFailures` are
|
|
1011
|
+
* per-process, and the reaper is leader-gated — so a reaper-only report left
|
|
1012
|
+
* every non-leader with an empty `diagnose scaler` for a scaler failing
|
|
1013
|
+
* fleet-wide, and dispatching at the un-deferred cadence while only the
|
|
1014
|
+
* leader backed off. The local prune is the observation every coordinator has
|
|
1015
|
+
* of its own spawns.
|
|
1016
|
+
*
|
|
1017
|
+
* Synchronous up to the backoff arm, so a caller that must clean up after the
|
|
1018
|
+
* event (the prune drops the agent's event buffer) sees the effect
|
|
1019
|
+
* immediately.
|
|
1020
|
+
*/
|
|
1021
|
+
private reportProvisionFailure;
|
|
1022
|
+
/**
|
|
1023
|
+
* Report a pruned event spawn as a dead provision — unless a peer adopted it.
|
|
1024
|
+
*
|
|
1025
|
+
* The prune is NOT proof of failure on an HA cluster. Nothing tells the
|
|
1026
|
+
* spawning coordinator that its agent registered on a peer, so the entry
|
|
1027
|
+
* survives adoption and lands in the prune the stale window later next to a
|
|
1028
|
+
* perfectly live agent — the same asymmetry `deleteUnadoptedSpawningAgent`
|
|
1029
|
+
* exists for. Reporting that as a dead provision writes a `scaler.failed` and
|
|
1030
|
+
* a `last_provisioning_error` onto a job that is running, and backs a healthy
|
|
1031
|
+
* scaler off, which is the misattribution this whole report exists to remove.
|
|
1032
|
+
* The reaper already applies the same test: it returns `spawn-timeout` only
|
|
1033
|
+
* for a candidate with no `adoptedBy`, so without this the two observers of
|
|
1034
|
+
* one provision disagree.
|
|
1035
|
+
*
|
|
1036
|
+
* The question is "was this provision ever adopted?", and the spawn row alone
|
|
1037
|
+
* cannot answer it: the row is deleted on teardown, so its absence is BOTH
|
|
1038
|
+
* "never adopted" and "adopted, then torn down". `provisionAdopter` reads the
|
|
1039
|
+
* live row first and the durable provision-outcome record second, which
|
|
1040
|
+
* survives the delete — so an adopted provision the reaper has since
|
|
1041
|
+
* condemned no longer reads as a dead one.
|
|
1042
|
+
*
|
|
1043
|
+
* The lookup is fire-and-forget so the prune stays off the request path, and
|
|
1044
|
+
* it fails open. Both "the store errored" and "there is no verdict" leave the
|
|
1045
|
+
* failure reported, deliberately: a provision with no positive evidence of
|
|
1046
|
+
* adoption is what a single coordinator sees on a real provisioning outage,
|
|
1047
|
+
* and suppressing there would disarm the per-coordinator backoff on every
|
|
1048
|
+
* follower — an invisible missing report in place of a visible false one. The
|
|
1049
|
+
* only rows with no verdict are those adopted before the outcome table
|
|
1050
|
+
* existed, and they age out within one stale-prune window of the deploy.
|
|
1051
|
+
*/
|
|
1052
|
+
private reportPrunedProvisionFailure;
|
|
1053
|
+
/**
|
|
1054
|
+
* Clear a scaler's consecutive-failure state after a successful provision.
|
|
1055
|
+
*
|
|
1056
|
+
* Called from `onAgentRegistered`, which is the single entry point for BOTH a
|
|
1057
|
+
* locally-spawned agent and one this coordinator adopted from a peer. An
|
|
1058
|
+
* adopted agent is a successful provision — the instance that spawned it
|
|
1059
|
+
* is not the one it reached — so counting only local registrations
|
|
1060
|
+
* would leave a healthy scaler deferred on an HA cluster.
|
|
1061
|
+
*/
|
|
1062
|
+
private clearProvisionFailures;
|
|
1063
|
+
/**
|
|
1064
|
+
* Emit `kici.scaler.scale-down` straight from a persisted spawn spec, then
|
|
1065
|
+
* drop the durable row.
|
|
1066
|
+
*
|
|
1067
|
+
* Used where the instance holding the agent has no local backend for its
|
|
1068
|
+
* scaler — the ordinary shape on an HA cluster behind one shared endpoint,
|
|
1069
|
+
* where the coordinator the agent reached may not configure the scaler that
|
|
1070
|
+
* spawned it. The row's own targets are therefore primary, with local config
|
|
1071
|
+
* as the fallback for a row that recorded none.
|
|
1072
|
+
*
|
|
1073
|
+
* A failed emit leaves the row in place: it is the only durable pointer at
|
|
1074
|
+
* the customer's running instance, so the reaper needs it to retry from.
|
|
1075
|
+
*
|
|
1076
|
+
* Returns whether the teardown was actually emitted, so a caller that counts
|
|
1077
|
+
* teardowns counts deliveries rather than attempts.
|
|
1078
|
+
*/
|
|
1079
|
+
private emitScaleDownForSpec;
|
|
1080
|
+
/**
|
|
1081
|
+
* Look up the scaler backend TYPE (a `ScalerBackendType` — `container`,
|
|
1082
|
+
* `firecracker`, `bare-metal`, `event`, …) for a registered agent.
|
|
1083
|
+
* Returns null if the agent is not scaler-managed (a static / stateful
|
|
1084
|
+
* agent).
|
|
428
1085
|
*
|
|
429
|
-
* Used by AgentMetricsAggregator to
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
1086
|
+
* Used by AgentMetricsAggregator to stamp the `scaler` label on each
|
|
1087
|
+
* kici_agent_* series. The label MUST be the backend type, not the
|
|
1088
|
+
* operator-chosen scaler name: the Platform catalog filter constrains
|
|
1089
|
+
* it to the ScalerBackendType set (plus `stateful`), so a free-form name is
|
|
1090
|
+
* dropped as bad_label_value. `managedAgentIndex` stores the scaler
|
|
1091
|
+
* name, so resolve it through `backends` to the type.
|
|
433
1092
|
*/
|
|
434
|
-
|
|
1093
|
+
getBackendType(agentId: string): string | null;
|
|
1094
|
+
/**
|
|
1095
|
+
* Exchange a provisioning claim code for freshly minted ephemeral credentials.
|
|
1096
|
+
*
|
|
1097
|
+
* Reads the shared claim table directly rather than routing through the
|
|
1098
|
+
* backend that minted the code: the mint needs only the agent-token store,
|
|
1099
|
+
* which is already DB-backed, so redemption succeeds on an instance that has
|
|
1100
|
+
* never heard of the emitting scaler. That is what lets an HA cluster front
|
|
1101
|
+
* its coordinators with one shared endpoint — the provisioned agent reaches
|
|
1102
|
+
* whichever coordinator the load balancer picks.
|
|
1103
|
+
*
|
|
1104
|
+
* Returns `{ error }` when this instance has no claim store (no database), or
|
|
1105
|
+
* when the redeem is rejected (unknown / consumed / expired / minting error).
|
|
1106
|
+
* Called from the `scaler.claim-credentials` WS handler.
|
|
1107
|
+
*/
|
|
1108
|
+
claimScalerCredentials(code: string): Promise<{
|
|
1109
|
+
credentials?: ClaimedCredentials;
|
|
1110
|
+
error?: string;
|
|
1111
|
+
}>;
|
|
435
1112
|
/**
|
|
436
1113
|
* Called from agent-handler.ts when an agent sends config.ack.
|
|
437
1114
|
* For Firecracker agents, clears MMDS data (belt-and-suspenders with in-VM iptables).
|
|
@@ -456,7 +1133,60 @@ export declare class ScalerManager {
|
|
|
456
1133
|
*/
|
|
457
1134
|
shutdownAll(): Promise<void>;
|
|
458
1135
|
/**
|
|
459
|
-
*
|
|
1136
|
+
* Classify every configured scaler against the currently loaded backends.
|
|
1137
|
+
* Pure — it reads state and writes none.
|
|
1138
|
+
*/
|
|
1139
|
+
private planReload;
|
|
1140
|
+
/**
|
|
1141
|
+
* Validate the new config: label-set overlaps, backend type changes, and
|
|
1142
|
+
* each kept backend's own view of its label sets.
|
|
1143
|
+
*
|
|
1144
|
+
* `backend.reload` validates and applies in one call, so a backend rejected
|
|
1145
|
+
* late in the loop would otherwise leave the ones before it holding the new
|
|
1146
|
+
* label sets and `maxAgents` while the commit never runs. Each successful
|
|
1147
|
+
* call's previous state is therefore snapshotted and restored when any
|
|
1148
|
+
* backend rejects — a rejected reload leaves every backend as it was.
|
|
1149
|
+
*
|
|
1150
|
+
* The snapshots are returned as well as used here: a later stage can still
|
|
1151
|
+
* fail (an added scaler whose backend does not construct), and the same
|
|
1152
|
+
* rollback is what keeps that rejection from half-applying too.
|
|
1153
|
+
*/
|
|
1154
|
+
private validateReload;
|
|
1155
|
+
/** Put every backend a rejected reload had already mutated back as it was. */
|
|
1156
|
+
private restoreReloadedBackends;
|
|
1157
|
+
/**
|
|
1158
|
+
* Construct every added backend into a staging list. On any failure the
|
|
1159
|
+
* partially built set is torn down and the caller keeps the old config.
|
|
1160
|
+
*/
|
|
1161
|
+
private buildAddedBackends;
|
|
1162
|
+
/**
|
|
1163
|
+
* Host prep for backends added by a reload: bridge provisioning, a reap of
|
|
1164
|
+
* leftovers from a previous incarnation of the same scaler, and the periodic
|
|
1165
|
+
* TAP sweep a long-running Firecracker backend needs. Every failure degrades
|
|
1166
|
+
* that one scaler; none of them fails the reload.
|
|
1167
|
+
*/
|
|
1168
|
+
private prepareAddedBackends;
|
|
1169
|
+
/**
|
|
1170
|
+
* Refresh per-scaler metadata from the new config. Keys are deleted only for
|
|
1171
|
+
* scalers that are neither configured nor retiring — a retiring scaler keeps
|
|
1172
|
+
* its cap, URL, roles and defaults until its last agent is gone.
|
|
1173
|
+
*/
|
|
1174
|
+
private applyScalerMetadata;
|
|
1175
|
+
/**
|
|
1176
|
+
* Register every configured machine pool with the ledger, building the
|
|
1177
|
+
* ledger when this reload is the first config to declare a pool. `cap` is
|
|
1178
|
+
* re-registered on every reload, so a changed cap applies without a restart.
|
|
1179
|
+
*/
|
|
1180
|
+
private applyMachinePools;
|
|
1181
|
+
/** Rebuild the warm-pool config map from the new config (see WarmPoolManager.reload). */
|
|
1182
|
+
private applyWarmPoolConfig;
|
|
1183
|
+
/**
|
|
1184
|
+
* Validate and reload configuration: plan → validate → build → commit.
|
|
1185
|
+
*
|
|
1186
|
+
* Either the whole new config applies, or nothing does and the previous one
|
|
1187
|
+
* keeps running. A scaler the new config no longer names is retired, not
|
|
1188
|
+
* dropped: it stops taking work at once, its idle warm agents are destroyed,
|
|
1189
|
+
* its job-bound agents finish, and its backend is torn down when it drains.
|
|
460
1190
|
*/
|
|
461
1191
|
reload(newConfig: ScalerConfig): Promise<ValidationResult>;
|
|
462
1192
|
/**
|
|
@@ -468,6 +1198,15 @@ export declare class ScalerManager {
|
|
|
468
1198
|
*/
|
|
469
1199
|
recentSpawnFailures(windowMs: number, nowMs: number): Map<string, BackendFailureSummary>;
|
|
470
1200
|
getStatus(): ScalerStatus;
|
|
1201
|
+
/**
|
|
1202
|
+
* Scaler capacity advertised to peers in the cluster heartbeat.
|
|
1203
|
+
*
|
|
1204
|
+
* Retiring backends are excluded: they accept no new work locally, so a peer
|
|
1205
|
+
* that saw them would keep selecting this host for jobs it can no longer
|
|
1206
|
+
* serve. Agents still draining on a retiring backend stay visible through the
|
|
1207
|
+
* separate per-agent inventory, so in-flight work is unaffected.
|
|
1208
|
+
*/
|
|
1209
|
+
getRoutableCapacity(): ScalerCapacitySummary[];
|
|
471
1210
|
/**
|
|
472
1211
|
* Get the backend name managing a specific agent.
|
|
473
1212
|
* Returns null if the agent is not scaler-managed (standalone).
|
|
@@ -509,7 +1248,8 @@ export declare class ScalerManager {
|
|
|
509
1248
|
*/
|
|
510
1249
|
getScalerContextForAgent(agentId: string): Record<string, unknown> | undefined;
|
|
511
1250
|
/**
|
|
512
|
-
* Start the warm pool idle check interval
|
|
1251
|
+
* Start the warm pool idle check interval, the machine-pool ledger reaper,
|
|
1252
|
+
* and the retirement sweep that tears down drained retiring backends.
|
|
513
1253
|
*/
|
|
514
1254
|
start(): void;
|
|
515
1255
|
/**
|
|
@@ -521,9 +1261,21 @@ export declare class ScalerManager {
|
|
|
521
1261
|
*/
|
|
522
1262
|
ensureHostsReady(): Promise<void>;
|
|
523
1263
|
/**
|
|
524
|
-
*
|
|
525
|
-
*
|
|
526
|
-
*
|
|
1264
|
+
* The window a spawning entry may sit unregistered before it is pruned.
|
|
1265
|
+
*
|
|
1266
|
+
* An entry cannot be stale before its own spawn deadline has passed, so the
|
|
1267
|
+
* window is floored at the configured deadline: an operator raising
|
|
1268
|
+
* `KICI_SCALER_SPAWN_TIMEOUT_MS` for a slow cloud must not have this prune
|
|
1269
|
+
* fire first and drop the correlation a provision booting inside the raised
|
|
1270
|
+
* deadline still needs. The floor only ever extends the window — a lowered
|
|
1271
|
+
* deadline keeps the default five minutes, because a local backend's agent
|
|
1272
|
+
* boots and registers well after `spawn()` itself returns.
|
|
1273
|
+
*/
|
|
1274
|
+
private stalePruneWindowMs;
|
|
1275
|
+
/**
|
|
1276
|
+
* Remove spawning entries whose `backend.spawn` started longer ago than the
|
|
1277
|
+
* stale window but never registered via WS (e.g., process/container crashed
|
|
1278
|
+
* on startup). Without cleanup, these entries would leak in spawningAgents
|
|
527
1279
|
* forever.
|
|
528
1280
|
*
|
|
529
1281
|
* The window is measured from `spawnStartedAt` (when the throttled spawn
|
|
@@ -532,10 +1284,65 @@ export declare class ScalerManager {
|
|
|
532
1284
|
* reaped mid-queue during a large burst. Recovered orphan entries carry
|
|
533
1285
|
* `spawnStartedAt` set to their persisted enqueue time, so they are subject to
|
|
534
1286
|
* the normal window.
|
|
1287
|
+
*
|
|
1288
|
+
* What happens to the durable row depends on the backend type the spawn
|
|
1289
|
+
* recorded, which the entry carries so a scaler since removed from this
|
|
1290
|
+
* coordinator's config still resolves. A local backend's
|
|
1291
|
+
* compute is pinned to a host this process manages, so a pruned entry means
|
|
1292
|
+
* the spawn is dead and the row goes. An event backend's row is the only
|
|
1293
|
+
* durable pointer at a customer cloud instance, and it is precisely the
|
|
1294
|
+
* reaper's `spawn-timeout` candidate — so the row STAYS, and the reaper
|
|
1295
|
+
* decides. Deleting it here would beat the reaper by the flap grace on any
|
|
1296
|
+
* coordinator serving jobs, so the teardown the row exists to trigger would
|
|
1297
|
+
* never be emitted.
|
|
535
1298
|
*/
|
|
536
1299
|
private pruneStaleSpawningEntries;
|
|
1300
|
+
/**
|
|
1301
|
+
* The spawn row for one agent, as every writer of it must build it.
|
|
1302
|
+
*
|
|
1303
|
+
* Ownership and self-describing columns are assembled only here.
|
|
1304
|
+
* `backend_type` is what `adoptSpawningAgent`'s conditional UPDATE matches
|
|
1305
|
+
* on, so a row without it can never be adopted by any instance — including
|
|
1306
|
+
* the one that spawned it. `owner_instance_id` scopes recovery to that
|
|
1307
|
+
* instance, and `mandatory_labels` carries the platform taint to a
|
|
1308
|
+
* coordinator that has no config entry for this scaler and can therefore
|
|
1309
|
+
* derive it from nothing else. `provisioning_targets` is the same story for
|
|
1310
|
+
* teardown: it is the only address such a coordinator has for the customer's
|
|
1311
|
+
* teardown workflow, and an emit to an empty target list reaches nobody, so
|
|
1312
|
+
* the provision would bill forever. `roles` travels with it so the row stays
|
|
1313
|
+
* a complete description of the spawn.
|
|
1314
|
+
*/
|
|
1315
|
+
private spawningAgentSnapshot;
|
|
1316
|
+
/**
|
|
1317
|
+
* Claim one slot against an event scaler's cluster-wide `maxAgents`.
|
|
1318
|
+
*
|
|
1319
|
+
* The count and the claim share a single advisory-locked transaction, so the
|
|
1320
|
+
* row this writes is already visible to the next coordinator that takes the
|
|
1321
|
+
* lock. A store failure is never an admission: returning false costs a job a
|
|
1322
|
+
* queue wait, while admitting on a count nobody recorded provisions a cloud
|
|
1323
|
+
* instance the cap cannot see. Failing closed is not silent — a refusal
|
|
1324
|
+
* increments the spawn-refusal counter and a store failure its own counter,
|
|
1325
|
+
* labelled so a database outage, a contended lock, and a full cluster are
|
|
1326
|
+
* three distinguishable things rather than one.
|
|
1327
|
+
*
|
|
1328
|
+
* The failure arm bounds the admission, not the row: a transaction that
|
|
1329
|
+
* commits and then loses its acknowledgement leaves `slotClaimed` false, so
|
|
1330
|
+
* neither rollback path fires and the row counts against the cap for an
|
|
1331
|
+
* agent that never spawned. Closing that window needs two-phase commit; the
|
|
1332
|
+
* event-provision reaper is the backstop, clearing the row once the spawn
|
|
1333
|
+
* deadline passes with nothing adopted.
|
|
1334
|
+
*/
|
|
1335
|
+
private claimClusterSlot;
|
|
537
1336
|
private persistSpawningAgent;
|
|
538
1337
|
private deleteSpawningAgentFromStore;
|
|
1338
|
+
/**
|
|
1339
|
+
* Stamp `adopted_by = this instance` on the spawning row of an agent that just
|
|
1340
|
+
* registered here. Reuses the cross-instance conditional UPDATE, so a row some
|
|
1341
|
+
* other instance already claimed is left exactly as it is.
|
|
1342
|
+
*/
|
|
1343
|
+
private markSpawningAgentAdopted;
|
|
1344
|
+
/** Store-side half of the stale prune: drops the row only if nobody adopted it. */
|
|
1345
|
+
private deleteUnadoptedSpawningAgentFromStore;
|
|
539
1346
|
private persistReservation;
|
|
540
1347
|
private deleteReservationFromStore;
|
|
541
1348
|
private persistAgentJob;
|
|
@@ -555,9 +1362,24 @@ export declare class ScalerManager {
|
|
|
555
1362
|
* emitted by the previous coord before correlation are lost (see
|
|
556
1363
|
* wishlist for the rationale).
|
|
557
1364
|
*
|
|
1365
|
+
* Both reads are scoped to this instance's own rows. An unscoped read
|
|
1366
|
+
* hydrates a peer's in-flight spawns and reservations as our own, so our
|
|
1367
|
+
* spawn-timeout reaper destroys agents that peer is still legitimately
|
|
1368
|
+
* waiting on and our caps double-count its reservations. Rows owned by a
|
|
1369
|
+
* dead instance are the leader-gated reaper's business, not ours.
|
|
1370
|
+
*
|
|
558
1371
|
* No-op when no store is wired (unit-test path).
|
|
559
1372
|
*/
|
|
560
1373
|
recoverState(): Promise<ScalerStateRecovery>;
|
|
1374
|
+
/**
|
|
1375
|
+
* Mint a scaler-managed agent id.
|
|
1376
|
+
*
|
|
1377
|
+
* Built from {@link SCALER_AGENT_ID_PREFIX} rather than a second copy of the
|
|
1378
|
+
* literal: the registration guard in `onAgentRegistered` recognises a
|
|
1379
|
+
* scaler-managed agent by that prefix alone when the spawn-record lookup
|
|
1380
|
+
* fails, so a minting prefix that drifts from the constant leaves the guard
|
|
1381
|
+
* matching nothing and failing open.
|
|
1382
|
+
*/
|
|
561
1383
|
private generateAgentId;
|
|
562
1384
|
/**
|
|
563
1385
|
* Start log forwarding for a scaler-managed agent if its backend supports LogCapture.
|
|
@@ -587,6 +1409,13 @@ export declare class ScalerManager {
|
|
|
587
1409
|
* The backend label is resolved from the spawning entry first, then the
|
|
588
1410
|
* managed-agent index — so a late failure after the spawning entry is gone
|
|
589
1411
|
* still buckets under the real backend instead of "unknown".
|
|
1412
|
+
*
|
|
1413
|
+
* `attribution` is the last resort for a failure this coordinator did not
|
|
1414
|
+
* spawn and never registered: the reaper is leader-gated, so the instance that
|
|
1415
|
+
* condemns a stranded external provision routinely has neither a spawning
|
|
1416
|
+
* entry nor a managed-agent index row for it, and every in-memory resolver
|
|
1417
|
+
* above comes back empty. It is read only where the maps are silent, so a live
|
|
1418
|
+
* local entry always wins over a durable row that may be a sweep behind.
|
|
590
1419
|
*/
|
|
591
1420
|
private handleScalerEvent;
|
|
592
1421
|
}
|