deepline 0.3.89 → 0.3.90
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/contracts/install-commands.json +29 -0
- package/dist/bundling-sources/sdk/src/client.ts +120 -37
- package/dist/bundling-sources/sdk/src/index.ts +4 -0
- package/dist/bundling-sources/sdk/src/pagination.ts +25 -0
- package/dist/bundling-sources/sdk/src/plays/bundle-play-file.ts +1 -1
- package/dist/bundling-sources/sdk/src/release.ts +16 -9
- package/dist/bundling-sources/sdk/src/types.ts +23 -0
- package/dist/bundling-sources/shared_libs/monitors/analytics-public.ts +10 -0
- package/dist/bundling-sources/shared_libs/monitors/http-error-response.ts +3 -0
- package/dist/bundling-sources/shared_libs/monitors/inbound-queue.ts +4 -4
- package/dist/bundling-sources/shared_libs/monitors/inventory-reconciliation.ts +59 -0
- package/dist/bundling-sources/shared_libs/monitors/lifecycle-notifications.ts +7 -9
- package/dist/bundling-sources/shared_libs/monitors/package.json +7 -0
- package/dist/bundling-sources/shared_libs/monitors/provider-monitor-batch-operation.ts +44 -2
- package/dist/bundling-sources/shared_libs/monitors/provider-monitor-consumer-health.ts +2 -2
- package/dist/bundling-sources/shared_libs/monitors/provider-monitor-identity-claims.ts +2 -2
- package/dist/bundling-sources/shared_libs/monitors/provider-monitor-public-aliases.ts +1 -1
- package/dist/bundling-sources/shared_libs/monitors/signal-radars/inbound-dead-letter.ts +245 -0
- package/dist/bundling-sources/shared_libs/monitors/signal-radars/inbound-message.ts +38 -0
- package/dist/bundling-sources/shared_libs/monitors/signal-radars/inbound-queue-ack-probe.ts +94 -0
- package/dist/bundling-sources/shared_libs/monitors/signal-radars/inbound-queue-delivery.ts +60 -0
- package/dist/bundling-sources/shared_libs/monitors/signal-radars/inbound-queue.ts +60 -0
- package/dist/bundling-sources/shared_libs/monitors/snitcher-install-validation.ts +1 -1
- package/dist/bundling-sources/shared_libs/monitors/snitcher-ip2company.ts +2 -2
- package/dist/bundling-sources/shared_libs/monitors/snitcher-radar.ts +1 -1
- package/dist/bundling-sources/shared_libs/monitors/tracking-provider-integrations.ts +1 -1
- package/dist/bundling-sources/shared_libs/observability/build-context.ts +13 -0
- package/dist/bundling-sources/shared_libs/observability/log-metadata.ts +160 -0
- package/dist/bundling-sources/shared_libs/observability/request-context.ts +68 -0
- package/dist/bundling-sources/shared_libs/observability/scheduled-jobs.json +654 -0
- package/dist/bundling-sources/shared_libs/observability/scheduled-work-manual.ts +35 -0
- package/dist/bundling-sources/shared_libs/observability/scheduled-work.ts +5 -0
- package/dist/bundling-sources/shared_libs/play-data-plane/neon-constants.ts +9 -0
- package/dist/bundling-sources/shared_libs/play-data-plane/r2.ts +272 -0
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +45 -0
- package/dist/bundling-sources/shared_libs/play-runtime/async-operation.ts +66 -1
- package/dist/bundling-sources/shared_libs/play-runtime/async-play-invocation.ts +0 -1
- package/dist/bundling-sources/shared_libs/play-runtime/bounceban-batching.ts +137 -0
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +779 -140
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +46 -0
- package/dist/bundling-sources/shared_libs/play-runtime/daytona-payload-contract.ts +68 -0
- package/dist/bundling-sources/shared_libs/play-runtime/daytona-sandbox-janitor.ts +103 -0
- package/dist/bundling-sources/shared_libs/play-runtime/daytona-sandbox-reaper.ts +725 -0
- package/dist/bundling-sources/shared_libs/play-runtime/durable-call-cache.ts +29 -0
- package/dist/bundling-sources/shared_libs/play-runtime/execution-capabilities.ts +23 -0
- package/dist/bundling-sources/shared_libs/play-runtime/execution-gateway-invocation-fence.ts +442 -0
- package/dist/bundling-sources/shared_libs/play-runtime/execution-protocol.ts +3 -3
- package/dist/bundling-sources/shared_libs/play-runtime/fixture-behavior.ts +18 -0
- package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +9 -0
- package/dist/bundling-sources/shared_libs/play-runtime/governor/policy.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/governor/rate-state-backend.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/integration-operation-reader-fence/contracts.ts +66 -0
- package/dist/bundling-sources/shared_libs/play-runtime/integration-operation-reader-fence/job.ts +145 -0
- package/dist/bundling-sources/shared_libs/play-runtime/integration-operation-reader-fence/ports.ts +21 -0
- package/dist/bundling-sources/shared_libs/play-runtime/integration-operation-reader-fence/postgres-store.ts +170 -0
- package/dist/bundling-sources/shared_libs/play-runtime/internal-bootstrap-token.ts +1322 -0
- package/dist/bundling-sources/shared_libs/play-runtime/internal-step-ids.ts +2 -2
- package/dist/bundling-sources/shared_libs/play-runtime/lane-retirement-gate.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/launch-source-policy.ts +112 -0
- package/dist/bundling-sources/shared_libs/play-runtime/map-execution-frame.ts +8 -0
- package/dist/bundling-sources/shared_libs/play-runtime/perf-trace.ts +127 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-artifact-runtime-contract.ts +5 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-run-recovery-policy.ts +24 -1
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/admission/contracts.ts +56 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/admission/index.ts +13 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/admission/job.ts +41 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/admission/ports.ts +17 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/admission/postgres-adapter.ts +54 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/attempts/contracts.ts +114 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/attempts/engine-suspension.ts +108 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/attempts/index.ts +36 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/attempts/job.ts +97 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/attempts/ports.ts +104 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/attempts/postgres-fence-adapter.ts +263 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/attempts/worker-terminal-projection.ts +64 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/projection/contracts.ts +29 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/projection/gateway-adapter.ts +56 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/projection/index.ts +15 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/projection/job.ts +65 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/projection/ports.ts +17 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/sandbox-cleanup/contracts.ts +42 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/sandbox-cleanup/index.ts +17 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/sandbox-cleanup/job.ts +142 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/sandbox-cleanup/ports.ts +42 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/sandbox-cleanup/postgres-adapter.ts +102 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/contracts.ts +60 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/index.ts +16 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/job.ts +78 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/ports.ts +54 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/postgres-adapter.ts +71 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runtime-batching-registry.ts +4 -0
- package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/receipt-status.ts +5 -8
- package/dist/bundling-sources/shared_libs/play-runtime/run-failure.ts +3 -5
- package/dist/bundling-sources/shared_libs/play-runtime/run-identity.ts +168 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts +72 -9
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-payload-transport.ts +6 -12
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +19 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/bundle.ts +0 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/runtime-sandbox-reconciliation.ts +14 -44
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +57 -205
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-authority.ts +201 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-env.ts +6 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-operation-contract.ts +67 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-release-environment.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-sandbox-contract.ts +68 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-scheduler-topology.ts +14 -2
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-telemetry.ts +108 -0
- package/dist/bundling-sources/shared_libs/play-runtime/sandbox-runs/contract.ts +4 -0
- package/dist/bundling-sources/shared_libs/play-runtime/sandbox-runs/runner-backend-adapter.ts +3 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +7 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-deadline-reconciliation.ts +88 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-play-deploy-task.ts +124 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-play-run-task.ts +68 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-pool.ts +193 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-queue-lifecycle.ts +269 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-queue-suffix.ts +73 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +6642 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-shared.ts +897 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-worker-metrics.ts +120 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-worker-required-env.ts +76 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd.ts +476 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/gateway-heavy-request-admission.ts +47 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/gateway-progress-registry.ts +147 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/index.ts +27 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/lazy.ts +50 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/play-run-recovery.ts +98 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-budget-state.ts +260 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-pool.ts +1416 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-progress.ts +1255 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts +942 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +14636 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-gateway.ts +888 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-ledger-outbox-recovery.ts +355 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-terminal-transition.ts +46 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runner-push.ts +60 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-api-origin.ts +33 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-receipt-gateway-health.ts +64 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-sandbox-capacity.ts +835 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-sandbox-ledger-reconciliation.ts +1218 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-sandbox-operation.ts +207 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-scheduler-schema.ts +10 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/scheduler-capacity.ts +123 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/scheduler-launch-read-observability.ts +78 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/submit-admission.ts +169 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/terminal-contract.ts +20 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/worker-queue-metrics.ts +34 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/worker-resource-metrics.ts +249 -0
- package/dist/bundling-sources/shared_libs/play-runtime/sheet-attempt-sql.ts +7 -0
- package/dist/bundling-sources/shared_libs/play-runtime/step-progress.ts +296 -0
- package/dist/bundling-sources/shared_libs/play-runtime/tool-call/native-batch.ts +3 -3
- package/dist/bundling-sources/shared_libs/play-runtime/tool-call/runtime-step-receipts-adapter.ts +20 -1
- package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +60 -15
- package/dist/bundling-sources/shared_libs/play-runtime/tool-execution-outcome.ts +20 -0
- package/dist/bundling-sources/shared_libs/play-runtime/transient-service-error.ts +12 -0
- package/dist/bundling-sources/shared_libs/play-runtime/vercel-protection.ts +14 -1
- package/dist/bundling-sources/shared_libs/play-runtime/work-receipt-storage-contract.ts +165 -0
- package/dist/bundling-sources/shared_libs/play-runtime/work-receipts.ts +15 -23
- package/dist/bundling-sources/shared_libs/play-runtime/workflow-executor-auth.ts +169 -0
- package/dist/bundling-sources/shared_libs/play-runtime/zerobounce-batching.ts +122 -0
- package/dist/bundling-sources/shared_libs/plays/artifact-types.ts +2 -0
- package/dist/bundling-sources/shared_libs/plays/contracts.ts +2 -0
- package/dist/bundling-sources/shared_libs/plays/deployment-status.ts +1 -1
- package/dist/bundling-sources/shared_libs/plays/docflow.ts +1 -1
- package/dist/bundling-sources/shared_libs/plays/play-grep-search.ts +39 -0
- package/dist/bundling-sources/shared_libs/plays/share/copy.ts +271 -0
- package/dist/bundling-sources/shared_libs/plays/share/metrics.ts +140 -0
- package/dist/bundling-sources/shared_libs/plays/share/public-read-model.ts +86 -0
- package/dist/bundling-sources/shared_libs/plays/share/seo-quality.ts +181 -0
- package/dist/bundling-sources/shared_libs/plays/share/static-pipeline-summary.ts +658 -0
- package/dist/bundling-sources/shared_libs/plays/share/types.ts +197 -0
- package/dist/bundling-sources/shared_libs/plays/slack-hitl-timeout-error.ts +37 -0
- package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +6 -1
- package/dist/bundling-sources/shared_libs/plays/tool-result-types.ts +1 -1
- package/dist/bundling-sources/shared_libs/product-notifications/configuration.ts +7 -1
- package/dist/bundling-sources/shared_libs/product-notifications/default-policy.ts +3 -2
- package/dist/bundling-sources/shared_libs/product-notifications/overflow.ts +18 -0
- package/dist/bundling-sources/shared_libs/product-notifications/rolling-limit.ts +24 -0
- package/dist/bundling-sources/shared_libs/product-notifications/slack-delivery.ts +624 -0
- package/dist/bundling-sources/shared_libs/product-notifications/slack-payload.ts +140 -0
- package/dist/bundling-sources/shared_libs/product-notifications/slack-transport.ts +75 -0
- package/dist/bundling-sources/shared_libs/runtime-env.ts +6 -0
- package/dist/bundling-sources/shared_libs/security/email-blacklist.ts +42 -0
- package/dist/bundling-sources/shared_libs/security/header-safe-env.ts +10 -3
- package/dist/bundling-sources/shared_libs/security/slack-sanitize.ts +133 -0
- package/dist/cli/getting-started.d.mts +52 -0
- package/dist/cli/getting-started.d.ts +52 -0
- package/dist/cli/getting-started.js +65 -0
- package/dist/cli/getting-started.mjs +36 -0
- package/dist/cli/index.d.mts +9 -0
- package/dist/cli/index.d.ts +9 -0
- package/dist/cli/index.js +545 -264
- package/dist/cli/index.mjs +543 -271
- package/dist/{compiler-manifest-DsLbQaew.d.mts → compiler-manifest-BaJfE-5H.d.mts} +5 -3
- package/dist/{compiler-manifest-DsLbQaew.d.ts → compiler-manifest-BaJfE-5H.d.ts} +5 -3
- package/dist/index.d.mts +81 -148
- package/dist/index.d.ts +81 -148
- package/dist/index.js +83 -26
- package/dist/index.mjs +83 -26
- package/dist/install-integrity.json +130 -2
- package/dist/monitor-fleet-contract.d.mts +138 -0
- package/dist/monitor-fleet-contract.d.ts +138 -0
- package/dist/monitor-fleet-contract.js +404 -0
- package/dist/monitor-fleet-contract.mjs +367 -0
- package/dist/plays/bundle-play-file.d.mts +2 -2
- package/dist/plays/bundle-play-file.d.ts +2 -2
- package/dist/release.d.mts +255 -0
- package/dist/release.d.ts +255 -0
- package/dist/release.js +215 -0
- package/dist/release.mjs +188 -0
- package/package.json +21 -4
|
@@ -0,0 +1,1218 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PostgresSchedulerOptions,
|
|
3
|
+
PostgresSchedulerQueryClient,
|
|
4
|
+
} from './postgres';
|
|
5
|
+
import {
|
|
6
|
+
RuntimeSandboxOperationState,
|
|
7
|
+
RuntimeSandboxProvider,
|
|
8
|
+
} from './runtime-sandbox-operation';
|
|
9
|
+
|
|
10
|
+
const DAYTONA_PROVIDER = RuntimeSandboxProvider.Daytona;
|
|
11
|
+
const DEFAULT_BATCH_SIZE = 500;
|
|
12
|
+
const MAX_BATCH_SIZE = 1_000;
|
|
13
|
+
/** Never classify a very recent resource from a provider snapshot as absent. */
|
|
14
|
+
export const RUNTIME_SANDBOX_LEDGER_RECONCILIATION_MINIMUM_AGE_MS =
|
|
15
|
+
10 * 60 * 1_000;
|
|
16
|
+
|
|
17
|
+
function quoteIdent(value: string): string {
|
|
18
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {
|
|
19
|
+
throw new Error(`Invalid runtime scheduler schema: ${value}`);
|
|
20
|
+
}
|
|
21
|
+
return `"${value}"`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function tables(options?: Pick<PostgresSchedulerOptions, 'schema'>) {
|
|
25
|
+
const schema = quoteIdent(options?.schema ?? 'runtime_scheduler');
|
|
26
|
+
return {
|
|
27
|
+
attempts: `${schema}."attempts"`,
|
|
28
|
+
runs: `${schema}."runs"`,
|
|
29
|
+
operations: `${schema}."runtime_sandbox_operations"`,
|
|
30
|
+
cleanup: `${schema}."sandbox_cleanup_jobs_v2"`,
|
|
31
|
+
cleanupEvents: `${schema}."sandbox_cleanup_events_v2"`,
|
|
32
|
+
leases: `${schema}."runtime_sandbox_capacity_leases"`,
|
|
33
|
+
control: `${schema}."control"`,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A provider inventory item is a physical-sandbox identity, never an id-only
|
|
38
|
+
* guess. Daytona's operation label lets recovery fence a create whose HTTP
|
|
39
|
+
* response was lost after Daytona had already accepted it. */
|
|
40
|
+
export type DaytonaSandboxInventoryEntry = {
|
|
41
|
+
sandboxId: string;
|
|
42
|
+
operationKey: string | null;
|
|
43
|
+
routingDomain: string | null;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function normalizeProviderInventory(
|
|
47
|
+
values: readonly DaytonaSandboxInventoryEntry[],
|
|
48
|
+
): DaytonaSandboxInventoryEntry[] {
|
|
49
|
+
const byIdentity = new Map<string, DaytonaSandboxInventoryEntry>();
|
|
50
|
+
for (const value of values) {
|
|
51
|
+
const sandboxId = value.sandboxId.trim();
|
|
52
|
+
if (!sandboxId)
|
|
53
|
+
throw new Error('Provider inventory sandboxId is required.');
|
|
54
|
+
const operationKey = value.operationKey?.trim() || null;
|
|
55
|
+
const routingDomain = value.routingDomain?.trim() || null;
|
|
56
|
+
byIdentity.set(
|
|
57
|
+
`${sandboxId}\u0000${routingDomain ?? ''}\u0000${operationKey ?? ''}`,
|
|
58
|
+
{ sandboxId, operationKey, routingDomain },
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return [...byIdentity.values()];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function batchSize(value: number | undefined): number {
|
|
65
|
+
if (value === undefined) return DEFAULT_BATCH_SIZE;
|
|
66
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > MAX_BATCH_SIZE) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`batchSize must be an integer from 1 through ${MAX_BATCH_SIZE}.`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type RuntimeSandboxLedgerReconciliationSummary = {
|
|
75
|
+
provider: RuntimeSandboxProvider.Daytona;
|
|
76
|
+
providerSandboxCount: number;
|
|
77
|
+
matchedLiveSandboxes: number;
|
|
78
|
+
unmatchedProviderSandboxIds: string[];
|
|
79
|
+
liveLeasesToSeed: number;
|
|
80
|
+
absentLedgerRows: number;
|
|
81
|
+
seededLeases: number;
|
|
82
|
+
/** Historical or raced leases released because deletion was already durable. */
|
|
83
|
+
releasedTerminalLeases: number;
|
|
84
|
+
markedDeleted: number;
|
|
85
|
+
/** Reserved creates recovered from Daytona's exact operation-key label. */
|
|
86
|
+
recoveredReservedOperations: number;
|
|
87
|
+
hasMoreAbsentLedgerRows: boolean;
|
|
88
|
+
dryRun: boolean;
|
|
89
|
+
diagnostics: readonly RuntimeSandboxLedgerReconciliationDiagnostic[];
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export enum RuntimeSandboxLedgerReconciliationPhase {
|
|
93
|
+
ReleaseTerminalCapacity = 'release_terminal_capacity',
|
|
94
|
+
MatchProviderInventory = 'match_provider_inventory',
|
|
95
|
+
RecoverReservedOperations = 'recover_reserved_operations',
|
|
96
|
+
FindAbsentOperations = 'find_absent_operations',
|
|
97
|
+
InspectCapacityBindings = 'inspect_capacity_bindings',
|
|
98
|
+
SeedCapacityBindings = 'seed_capacity_bindings',
|
|
99
|
+
MarkProviderConfirmedAbsent = 'mark_provider_confirmed_absent',
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export type RuntimeSandboxLedgerReconciliationDiagnostic = {
|
|
103
|
+
phase: RuntimeSandboxLedgerReconciliationPhase;
|
|
104
|
+
durationMs: number;
|
|
105
|
+
rowCount: number;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export type RuntimeSandboxOperationBackfillCursor = {
|
|
109
|
+
/** Exact PostgreSQL timestamptz text; milliseconds are insufficient. */
|
|
110
|
+
updatedAt: string;
|
|
111
|
+
runId: string;
|
|
112
|
+
attempt: number;
|
|
113
|
+
resourceOrdinal: number;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export type RuntimeSandboxOperationBackfillSummary = {
|
|
117
|
+
scannedAttempts: number;
|
|
118
|
+
insertedOperations: number;
|
|
119
|
+
hasMore: boolean;
|
|
120
|
+
nextCursor: RuntimeSandboxOperationBackfillCursor | null;
|
|
121
|
+
/** Redacted, approval-safe resource targets in this bounded page. */
|
|
122
|
+
targets: readonly RuntimeSandboxOperationBackfillTarget[];
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export type RuntimeSandboxOperationBackfillTarget = {
|
|
126
|
+
runId: string;
|
|
127
|
+
attempt: number;
|
|
128
|
+
resourceOrdinal: number;
|
|
129
|
+
provider: RuntimeSandboxProvider;
|
|
130
|
+
sandboxId: string;
|
|
131
|
+
wouldInsert: boolean;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export enum RuntimeSandboxOperationBackfillPhase {
|
|
135
|
+
Pending = 'pending',
|
|
136
|
+
Complete = 'complete',
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const RUNTIME_SANDBOX_OPERATION_BACKFILL_CONTROL_KEY =
|
|
140
|
+
'runtime_sandbox_operation_backfill_v1';
|
|
141
|
+
|
|
142
|
+
type RuntimeSandboxOperationBackfillCheckpoint = {
|
|
143
|
+
phase: RuntimeSandboxOperationBackfillPhase;
|
|
144
|
+
cursor: RuntimeSandboxOperationBackfillCursor | null;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
async function readRuntimeSandboxOperationBackfillCheckpoint(
|
|
148
|
+
client: PostgresSchedulerQueryClient,
|
|
149
|
+
options?: Pick<PostgresSchedulerOptions, 'schema'>,
|
|
150
|
+
): Promise<RuntimeSandboxOperationBackfillCheckpoint> {
|
|
151
|
+
const { control } = tables(options);
|
|
152
|
+
const result = await client.query<{ value: unknown }>(
|
|
153
|
+
`SELECT value FROM ${control} WHERE key = $1`,
|
|
154
|
+
[RUNTIME_SANDBOX_OPERATION_BACKFILL_CONTROL_KEY],
|
|
155
|
+
);
|
|
156
|
+
const value = result.rows[0]?.value;
|
|
157
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
158
|
+
return {
|
|
159
|
+
phase: RuntimeSandboxOperationBackfillPhase.Pending,
|
|
160
|
+
cursor: null,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
const checkpoint = value as { phase?: unknown; cursor?: unknown };
|
|
164
|
+
if (checkpoint.phase === RuntimeSandboxOperationBackfillPhase.Complete) {
|
|
165
|
+
return {
|
|
166
|
+
phase: RuntimeSandboxOperationBackfillPhase.Complete,
|
|
167
|
+
cursor: null,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
const cursor =
|
|
171
|
+
checkpoint.cursor as RuntimeSandboxOperationBackfillCursor | null;
|
|
172
|
+
return {
|
|
173
|
+
phase: RuntimeSandboxOperationBackfillPhase.Pending,
|
|
174
|
+
cursor:
|
|
175
|
+
cursor &&
|
|
176
|
+
typeof cursor.updatedAt === 'string' &&
|
|
177
|
+
typeof cursor.runId === 'string' &&
|
|
178
|
+
typeof cursor.attempt === 'number' &&
|
|
179
|
+
typeof cursor.resourceOrdinal === 'number'
|
|
180
|
+
? cursor
|
|
181
|
+
: null,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export type RuntimeSandboxOperationBackfillProgress = {
|
|
186
|
+
phase: RuntimeSandboxOperationBackfillPhase;
|
|
187
|
+
dryRun: boolean;
|
|
188
|
+
batches: number;
|
|
189
|
+
scannedAttempts: number;
|
|
190
|
+
insertedOperations: number;
|
|
191
|
+
projectedOperations: number;
|
|
192
|
+
targets: readonly RuntimeSandboxOperationBackfillTarget[];
|
|
193
|
+
nextCursor: RuntimeSandboxOperationBackfillCursor | null;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
/** Read one bounded resource-element page without writes. */
|
|
197
|
+
async function previewRuntimeSandboxOperationBackfillPage(
|
|
198
|
+
client: PostgresSchedulerQueryClient,
|
|
199
|
+
input: {
|
|
200
|
+
batchSize?: number;
|
|
201
|
+
cursor: RuntimeSandboxOperationBackfillCursor | null;
|
|
202
|
+
},
|
|
203
|
+
options?: Pick<PostgresSchedulerOptions, 'schema'>,
|
|
204
|
+
): Promise<RuntimeSandboxOperationBackfillSummary> {
|
|
205
|
+
const limit = batchSize(input.batchSize);
|
|
206
|
+
const { attempts, operations } = tables(options);
|
|
207
|
+
const page = await client.query<{
|
|
208
|
+
run_id: string;
|
|
209
|
+
attempt: number;
|
|
210
|
+
updated_at: string;
|
|
211
|
+
resource_ordinal: number;
|
|
212
|
+
provider: RuntimeSandboxProvider;
|
|
213
|
+
sandbox_id: string;
|
|
214
|
+
would_insert: boolean;
|
|
215
|
+
}>(
|
|
216
|
+
`
|
|
217
|
+
WITH candidates AS (
|
|
218
|
+
SELECT attempt.run_id, attempt.attempt, attempt.updated_at,
|
|
219
|
+
resource.ordinal::integer AS resource_ordinal,
|
|
220
|
+
CASE resource.value->>'kind' WHEN 'modal_sandbox' THEN $6 ELSE $7 END AS provider,
|
|
221
|
+
resource.value->>'sandboxId' AS sandbox_id,
|
|
222
|
+
coalesce(CASE resource.value->>'kind'
|
|
223
|
+
WHEN 'modal_sandbox' THEN NULLIF(resource.value->>'modalAppId', '')
|
|
224
|
+
ELSE NULLIF(resource.value->>'daytonaOrganizationId', '') END, '')
|
|
225
|
+
AS routing_domain_key
|
|
226
|
+
FROM ${attempts} AS attempt
|
|
227
|
+
CROSS JOIN LATERAL jsonb_array_elements(
|
|
228
|
+
CASE WHEN jsonb_typeof(attempt.resource_json) = 'array'
|
|
229
|
+
THEN attempt.resource_json ELSE jsonb_build_array(attempt.resource_json) END
|
|
230
|
+
) WITH ORDINALITY AS resource(value, ordinal)
|
|
231
|
+
WHERE attempt.resource_json IS NOT NULL
|
|
232
|
+
AND resource.value->>'kind' IN ('daytona_sandbox', 'modal_sandbox')
|
|
233
|
+
AND NULLIF(resource.value->>'sandboxId', '') IS NOT NULL
|
|
234
|
+
AND ($1::text IS NULL OR
|
|
235
|
+
(attempt.updated_at, attempt.run_id, attempt.attempt, resource.ordinal) >
|
|
236
|
+
($1::timestamptz, $2::text, $3::integer, $4::bigint))
|
|
237
|
+
), page AS (
|
|
238
|
+
SELECT *, row_number() OVER (
|
|
239
|
+
PARTITION BY provider, sandbox_id, routing_domain_key
|
|
240
|
+
ORDER BY updated_at, run_id, attempt, resource_ordinal
|
|
241
|
+
) AS conflict_rank
|
|
242
|
+
FROM candidates
|
|
243
|
+
ORDER BY updated_at, run_id, attempt, resource_ordinal
|
|
244
|
+
LIMIT $5
|
|
245
|
+
)
|
|
246
|
+
SELECT page.run_id, page.attempt, page.updated_at::text AS updated_at,
|
|
247
|
+
page.resource_ordinal, page.provider, page.sandbox_id,
|
|
248
|
+
page.conflict_rank = 1 AND
|
|
249
|
+
NOT EXISTS (
|
|
250
|
+
SELECT 1 FROM ${operations} AS operation
|
|
251
|
+
WHERE operation.provider = page.provider
|
|
252
|
+
AND operation.sandbox_id = page.sandbox_id
|
|
253
|
+
AND operation.routing_domain_key = page.routing_domain_key
|
|
254
|
+
) AS would_insert
|
|
255
|
+
FROM page
|
|
256
|
+
ORDER BY page.updated_at, page.run_id, page.attempt, page.resource_ordinal
|
|
257
|
+
`,
|
|
258
|
+
[
|
|
259
|
+
input.cursor?.updatedAt ?? null,
|
|
260
|
+
input.cursor?.runId ?? '',
|
|
261
|
+
input.cursor?.attempt ?? 0,
|
|
262
|
+
input.cursor?.resourceOrdinal ?? 0,
|
|
263
|
+
limit + 1,
|
|
264
|
+
RuntimeSandboxProvider.Modal,
|
|
265
|
+
RuntimeSandboxProvider.Daytona,
|
|
266
|
+
],
|
|
267
|
+
);
|
|
268
|
+
const hasMore = page.rows.length > limit;
|
|
269
|
+
const items = page.rows.slice(0, limit);
|
|
270
|
+
const last = items.at(-1) ?? null;
|
|
271
|
+
return {
|
|
272
|
+
scannedAttempts: items.length,
|
|
273
|
+
insertedOperations: 0,
|
|
274
|
+
hasMore,
|
|
275
|
+
nextCursor: last
|
|
276
|
+
? {
|
|
277
|
+
updatedAt: last.updated_at,
|
|
278
|
+
runId: last.run_id,
|
|
279
|
+
attempt: Number(last.attempt),
|
|
280
|
+
resourceOrdinal: Number(last.resource_ordinal),
|
|
281
|
+
}
|
|
282
|
+
: null,
|
|
283
|
+
targets: items.map((item) => ({
|
|
284
|
+
runId: item.run_id,
|
|
285
|
+
attempt: Number(item.attempt),
|
|
286
|
+
resourceOrdinal: Number(item.resource_ordinal),
|
|
287
|
+
provider: item.provider,
|
|
288
|
+
sandboxId: item.sandbox_id,
|
|
289
|
+
wouldInsert: item.would_insert,
|
|
290
|
+
})),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Persisted, resumable orchestrator. Dry-run performs no JSON expansion or writes. */
|
|
295
|
+
export async function advanceRuntimeSandboxOperationBackfill(
|
|
296
|
+
client: PostgresSchedulerQueryClient,
|
|
297
|
+
input: { dryRun: boolean; batchSize?: number; maxBatches?: number },
|
|
298
|
+
options?: Pick<PostgresSchedulerOptions, 'schema'>,
|
|
299
|
+
): Promise<RuntimeSandboxOperationBackfillProgress> {
|
|
300
|
+
const checkpoint = await readRuntimeSandboxOperationBackfillCheckpoint(
|
|
301
|
+
client,
|
|
302
|
+
options,
|
|
303
|
+
);
|
|
304
|
+
if (input.dryRun) {
|
|
305
|
+
if (checkpoint.phase === RuntimeSandboxOperationBackfillPhase.Complete) {
|
|
306
|
+
return {
|
|
307
|
+
phase: checkpoint.phase,
|
|
308
|
+
dryRun: true,
|
|
309
|
+
batches: 0,
|
|
310
|
+
scannedAttempts: 0,
|
|
311
|
+
insertedOperations: 0,
|
|
312
|
+
projectedOperations: 0,
|
|
313
|
+
targets: [],
|
|
314
|
+
nextCursor: null,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
const preview = await previewRuntimeSandboxOperationBackfillPage(
|
|
318
|
+
client,
|
|
319
|
+
{ batchSize: input.batchSize, cursor: checkpoint.cursor },
|
|
320
|
+
options,
|
|
321
|
+
);
|
|
322
|
+
return {
|
|
323
|
+
phase: RuntimeSandboxOperationBackfillPhase.Pending,
|
|
324
|
+
dryRun: true,
|
|
325
|
+
batches: preview.scannedAttempts > 0 ? 1 : 0,
|
|
326
|
+
scannedAttempts: preview.scannedAttempts,
|
|
327
|
+
insertedOperations: 0,
|
|
328
|
+
projectedOperations: preview.targets.filter(
|
|
329
|
+
(target) => target.wouldInsert,
|
|
330
|
+
).length,
|
|
331
|
+
targets: preview.targets,
|
|
332
|
+
nextCursor: preview.nextCursor,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
if (checkpoint.phase === RuntimeSandboxOperationBackfillPhase.Complete) {
|
|
336
|
+
return {
|
|
337
|
+
phase: checkpoint.phase,
|
|
338
|
+
dryRun: false,
|
|
339
|
+
batches: 0,
|
|
340
|
+
scannedAttempts: 0,
|
|
341
|
+
insertedOperations: 0,
|
|
342
|
+
projectedOperations: 0,
|
|
343
|
+
targets: [],
|
|
344
|
+
nextCursor: null,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
const maxBatches = Math.max(
|
|
348
|
+
1,
|
|
349
|
+
Math.min(200, Math.floor(input.maxBatches ?? 1)),
|
|
350
|
+
);
|
|
351
|
+
let cursor = checkpoint.cursor;
|
|
352
|
+
let scannedAttempts = 0;
|
|
353
|
+
let insertedOperations = 0;
|
|
354
|
+
for (let batch = 0; batch < maxBatches; batch += 1) {
|
|
355
|
+
const page = await backfillRuntimeSandboxOperationsFromAttemptResources(
|
|
356
|
+
client,
|
|
357
|
+
{ batchSize: input.batchSize, cursor: cursor ?? undefined },
|
|
358
|
+
options,
|
|
359
|
+
);
|
|
360
|
+
scannedAttempts += page.scannedAttempts;
|
|
361
|
+
insertedOperations += page.insertedOperations;
|
|
362
|
+
cursor = page.nextCursor;
|
|
363
|
+
const phase = page.hasMore
|
|
364
|
+
? RuntimeSandboxOperationBackfillPhase.Pending
|
|
365
|
+
: RuntimeSandboxOperationBackfillPhase.Complete;
|
|
366
|
+
await client.query(
|
|
367
|
+
`INSERT INTO ${tables(options).control} (key, value)
|
|
368
|
+
VALUES ($1, $2::jsonb)
|
|
369
|
+
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value`,
|
|
370
|
+
[
|
|
371
|
+
RUNTIME_SANDBOX_OPERATION_BACKFILL_CONTROL_KEY,
|
|
372
|
+
JSON.stringify({
|
|
373
|
+
phase,
|
|
374
|
+
cursor:
|
|
375
|
+
phase === RuntimeSandboxOperationBackfillPhase.Complete
|
|
376
|
+
? null
|
|
377
|
+
: cursor,
|
|
378
|
+
}),
|
|
379
|
+
],
|
|
380
|
+
);
|
|
381
|
+
if (phase === RuntimeSandboxOperationBackfillPhase.Complete) {
|
|
382
|
+
return {
|
|
383
|
+
phase,
|
|
384
|
+
dryRun: false,
|
|
385
|
+
batches: batch + 1,
|
|
386
|
+
scannedAttempts,
|
|
387
|
+
insertedOperations,
|
|
388
|
+
projectedOperations: scannedAttempts,
|
|
389
|
+
targets: [],
|
|
390
|
+
nextCursor: null,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return {
|
|
395
|
+
phase: RuntimeSandboxOperationBackfillPhase.Pending,
|
|
396
|
+
dryRun: false,
|
|
397
|
+
batches: maxBatches,
|
|
398
|
+
scannedAttempts,
|
|
399
|
+
insertedOperations,
|
|
400
|
+
projectedOperations: scannedAttempts,
|
|
401
|
+
targets: [],
|
|
402
|
+
nextCursor: cursor,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* One bounded compatibility migration from the old audit projection.
|
|
408
|
+
*
|
|
409
|
+
* This is intentionally separate from reconciliation: it keyset-scans one
|
|
410
|
+
* indexed attempt page, expands JSON only inside that page, and writes the
|
|
411
|
+
* normalized operation table. Reconciliation itself therefore never starts
|
|
412
|
+
* a full-history JSON expansion. Callers must persist `nextCursor` between
|
|
413
|
+
* batches; a fresh cursor intentionally restarts at the oldest page.
|
|
414
|
+
*/
|
|
415
|
+
export async function backfillRuntimeSandboxOperationsFromAttemptResources(
|
|
416
|
+
client: PostgresSchedulerQueryClient,
|
|
417
|
+
input: { batchSize?: number; cursor?: RuntimeSandboxOperationBackfillCursor },
|
|
418
|
+
options?: Pick<PostgresSchedulerOptions, 'schema'>,
|
|
419
|
+
): Promise<RuntimeSandboxOperationBackfillSummary> {
|
|
420
|
+
const limit = batchSize(input.batchSize);
|
|
421
|
+
const cursor = input.cursor;
|
|
422
|
+
if (
|
|
423
|
+
cursor &&
|
|
424
|
+
(!cursor.updatedAt.trim() ||
|
|
425
|
+
Number.isNaN(Date.parse(cursor.updatedAt)) ||
|
|
426
|
+
!cursor.runId.trim() ||
|
|
427
|
+
!Number.isSafeInteger(cursor.attempt) ||
|
|
428
|
+
cursor.attempt < 1 ||
|
|
429
|
+
!Number.isSafeInteger(cursor.resourceOrdinal) ||
|
|
430
|
+
cursor.resourceOrdinal < 1)
|
|
431
|
+
) {
|
|
432
|
+
throw new Error('Invalid runtime sandbox operation backfill cursor.');
|
|
433
|
+
}
|
|
434
|
+
const { attempts, runs, operations } = tables(options);
|
|
435
|
+
const result = await client.query<{
|
|
436
|
+
run_id: string;
|
|
437
|
+
attempt: number;
|
|
438
|
+
updated_at: string;
|
|
439
|
+
resource_ordinal: number;
|
|
440
|
+
provider: RuntimeSandboxProvider;
|
|
441
|
+
sandbox_id: string;
|
|
442
|
+
would_insert: boolean;
|
|
443
|
+
inserted_operations: number;
|
|
444
|
+
}>(
|
|
445
|
+
`
|
|
446
|
+
WITH resources AS MATERIALIZED (
|
|
447
|
+
SELECT attempt.run_id, attempt.attempt, attempt.updated_at, run.org_id,
|
|
448
|
+
resource.value AS resource, resource.ordinal::integer AS resource_ordinal,
|
|
449
|
+
CASE resource.value->>'kind'
|
|
450
|
+
WHEN 'modal_sandbox' THEN 'modal' ELSE 'daytona' END AS provider,
|
|
451
|
+
COALESCE(NULLIF(resource.value->>'runtimeEnvironment', ''),
|
|
452
|
+
NULLIF(resource.value->>'daytonaEnvironment', ''), 'production') AS environment,
|
|
453
|
+
CASE resource.value->>'kind'
|
|
454
|
+
WHEN 'modal_sandbox' THEN NULLIF(resource.value->>'modalAppId', '')
|
|
455
|
+
ELSE NULLIF(resource.value->>'daytonaOrganizationId', '') END AS routing_domain
|
|
456
|
+
FROM ${attempts} AS attempt
|
|
457
|
+
JOIN ${runs} AS run ON run.run_id = attempt.run_id
|
|
458
|
+
CROSS JOIN LATERAL jsonb_array_elements(
|
|
459
|
+
CASE WHEN jsonb_typeof(attempt.resource_json) = 'array'
|
|
460
|
+
THEN attempt.resource_json ELSE jsonb_build_array(attempt.resource_json) END
|
|
461
|
+
) WITH ORDINALITY AS resource(value, ordinal)
|
|
462
|
+
WHERE attempt.resource_json IS NOT NULL
|
|
463
|
+
AND resource.value->>'kind' IN ('daytona_sandbox', 'modal_sandbox')
|
|
464
|
+
AND NULLIF(resource.value->>'sandboxId', '') IS NOT NULL
|
|
465
|
+
AND (
|
|
466
|
+
$1::text IS NULL
|
|
467
|
+
OR (attempt.updated_at, attempt.run_id, attempt.attempt, resource.ordinal) >
|
|
468
|
+
($1::timestamptz, $2::text, $3::integer, $4::bigint)
|
|
469
|
+
)
|
|
470
|
+
ORDER BY attempt.updated_at, attempt.run_id, attempt.attempt, resource.ordinal
|
|
471
|
+
LIMIT $5
|
|
472
|
+
), ranked_resources AS (
|
|
473
|
+
SELECT resources.*,
|
|
474
|
+
coalesce(routing_domain, '') AS routing_domain_key,
|
|
475
|
+
row_number() OVER (
|
|
476
|
+
PARTITION BY provider, (resource::jsonb)->>'sandboxId', coalesce(routing_domain, '')
|
|
477
|
+
ORDER BY updated_at, run_id, attempt, resource_ordinal
|
|
478
|
+
) AS conflict_rank
|
|
479
|
+
FROM resources
|
|
480
|
+
), inserted AS (
|
|
481
|
+
INSERT INTO ${operations} (
|
|
482
|
+
operation_key, provider, run_id, attempt, org_id, environment,
|
|
483
|
+
sandbox_id, routing_domain, state, provider_created_at
|
|
484
|
+
)
|
|
485
|
+
SELECT
|
|
486
|
+
'backfill:' || provider || ':' || ((resource::jsonb)->>'sandboxId') || ':' || coalesce(routing_domain, ''),
|
|
487
|
+
provider,
|
|
488
|
+
run_id,
|
|
489
|
+
attempt,
|
|
490
|
+
org_id,
|
|
491
|
+
environment,
|
|
492
|
+
(resource::jsonb)->>'sandboxId',
|
|
493
|
+
routing_domain,
|
|
494
|
+
$6,
|
|
495
|
+
updated_at
|
|
496
|
+
FROM ranked_resources
|
|
497
|
+
WHERE conflict_rank = 1
|
|
498
|
+
ON CONFLICT (provider, sandbox_id, routing_domain_key) DO NOTHING
|
|
499
|
+
RETURNING provider, sandbox_id, routing_domain_key
|
|
500
|
+
)
|
|
501
|
+
SELECT
|
|
502
|
+
resource.run_id,
|
|
503
|
+
resource.attempt,
|
|
504
|
+
resource.updated_at::text AS updated_at,
|
|
505
|
+
resource.resource_ordinal,
|
|
506
|
+
resource.provider,
|
|
507
|
+
(resource.resource::jsonb)->>'sandboxId' AS sandbox_id,
|
|
508
|
+
resource.conflict_rank = 1 AND EXISTS (
|
|
509
|
+
SELECT 1
|
|
510
|
+
FROM inserted
|
|
511
|
+
WHERE inserted.provider = resource.provider
|
|
512
|
+
AND inserted.sandbox_id = (resource.resource::jsonb)->>'sandboxId'
|
|
513
|
+
AND inserted.routing_domain_key = resource.routing_domain_key
|
|
514
|
+
) AS would_insert,
|
|
515
|
+
(SELECT count(*)::integer FROM inserted) AS inserted_operations
|
|
516
|
+
FROM ranked_resources AS resource
|
|
517
|
+
ORDER BY resource.updated_at, resource.run_id, resource.attempt, resource.resource_ordinal
|
|
518
|
+
`,
|
|
519
|
+
[
|
|
520
|
+
cursor?.updatedAt ?? null,
|
|
521
|
+
cursor?.runId ?? '',
|
|
522
|
+
cursor?.attempt ?? 0,
|
|
523
|
+
cursor?.resourceOrdinal ?? 0,
|
|
524
|
+
limit,
|
|
525
|
+
RuntimeSandboxOperationState.Live,
|
|
526
|
+
],
|
|
527
|
+
);
|
|
528
|
+
const last = result.rows[0] ?? null;
|
|
529
|
+
const scannedAttempts = result.rows.length;
|
|
530
|
+
// A full page asks callers to fetch once more. This is conservative at an
|
|
531
|
+
// exact page boundary, but never skips a row by mutating an over-fetch.
|
|
532
|
+
const hasMore = scannedAttempts === limit;
|
|
533
|
+
const page = result.rows;
|
|
534
|
+
const next = page.at(-1) ?? null;
|
|
535
|
+
return {
|
|
536
|
+
scannedAttempts: page.length,
|
|
537
|
+
insertedOperations: Number(last?.inserted_operations ?? 0),
|
|
538
|
+
hasMore,
|
|
539
|
+
nextCursor: next
|
|
540
|
+
? {
|
|
541
|
+
updatedAt: next.updated_at,
|
|
542
|
+
runId: next.run_id,
|
|
543
|
+
attempt: Number(next.attempt),
|
|
544
|
+
resourceOrdinal: Number(next.resource_ordinal),
|
|
545
|
+
}
|
|
546
|
+
: null,
|
|
547
|
+
targets: page.map((item) => ({
|
|
548
|
+
runId: item.run_id,
|
|
549
|
+
attempt: Number(item.attempt),
|
|
550
|
+
resourceOrdinal: Number(item.resource_ordinal),
|
|
551
|
+
provider: item.provider,
|
|
552
|
+
sandboxId: item.sandbox_id,
|
|
553
|
+
wouldInsert: item.would_insert,
|
|
554
|
+
})),
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
async function releaseTerminalSandboxCapacityLeases(
|
|
559
|
+
client: PostgresSchedulerQueryClient,
|
|
560
|
+
options?: Pick<PostgresSchedulerOptions, 'schema'>,
|
|
561
|
+
): Promise<number> {
|
|
562
|
+
const { cleanup, leases } = tables(options);
|
|
563
|
+
const released = await client.query<{ lease_id: string }>(
|
|
564
|
+
`
|
|
565
|
+
UPDATE ${leases} AS lease
|
|
566
|
+
SET released_at = clock_timestamp()
|
|
567
|
+
FROM ${cleanup} AS cleanup
|
|
568
|
+
WHERE lease.provider = '${DAYTONA_PROVIDER}'
|
|
569
|
+
AND cleanup.provider = lease.provider
|
|
570
|
+
AND cleanup.sandbox_id = lease.sandbox_id
|
|
571
|
+
AND cleanup.routing_domain IS NOT DISTINCT FROM lease.routing_domain
|
|
572
|
+
AND cleanup.run_id = lease.run_id
|
|
573
|
+
AND cleanup.status = 'deleted'
|
|
574
|
+
AND cleanup.deleted_at IS NOT NULL
|
|
575
|
+
AND lease.released_at IS NULL
|
|
576
|
+
RETURNING lease.lease_id
|
|
577
|
+
`,
|
|
578
|
+
);
|
|
579
|
+
return released.rows.length;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Reconcile durable sandbox records against one complete Daytona inventory.
|
|
584
|
+
*
|
|
585
|
+
* This is intentionally a rollout/migration operation, not a hot-path read:
|
|
586
|
+
* Daytona's label-scoped list is the authority for provider presence. A live
|
|
587
|
+
* provider resource is seeded into the soft-capacity lease table. An older
|
|
588
|
+
* durable resource absent from that inventory is completed in the cleanup
|
|
589
|
+
* ledger with a provider-confirmed-absent audit event; the resource history is
|
|
590
|
+
* never deleted or rewritten.
|
|
591
|
+
*/
|
|
592
|
+
export async function reconcileRuntimeDaytonaSandboxLedger(
|
|
593
|
+
client: PostgresSchedulerQueryClient,
|
|
594
|
+
input: {
|
|
595
|
+
/** Complete source-labelled Daytona inventory, retaining ownership labels. */
|
|
596
|
+
providerSandboxes: readonly DaytonaSandboxInventoryEntry[];
|
|
597
|
+
/**
|
|
598
|
+
* Epoch time immediately after the complete provider inventory finished.
|
|
599
|
+
* Resources recorded later than this snapshot are never classified absent.
|
|
600
|
+
*/
|
|
601
|
+
inventoryCapturedAtMs: number;
|
|
602
|
+
dryRun: boolean;
|
|
603
|
+
batchSize?: number;
|
|
604
|
+
minimumAgeMs?: number;
|
|
605
|
+
},
|
|
606
|
+
options?: Pick<PostgresSchedulerOptions, 'schema'>,
|
|
607
|
+
): Promise<RuntimeSandboxLedgerReconciliationSummary> {
|
|
608
|
+
const providerSandboxes = normalizeProviderInventory(input.providerSandboxes);
|
|
609
|
+
const limit = batchSize(input.batchSize);
|
|
610
|
+
if (
|
|
611
|
+
!Number.isSafeInteger(input.inventoryCapturedAtMs) ||
|
|
612
|
+
input.inventoryCapturedAtMs <= 0
|
|
613
|
+
) {
|
|
614
|
+
throw new Error(
|
|
615
|
+
'inventoryCapturedAtMs must be a positive epoch milliseconds value.',
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
const minimumAgeMs =
|
|
619
|
+
input.minimumAgeMs ?? RUNTIME_SANDBOX_LEDGER_RECONCILIATION_MINIMUM_AGE_MS;
|
|
620
|
+
if (!Number.isSafeInteger(minimumAgeMs) || minimumAgeMs < 0) {
|
|
621
|
+
throw new Error('minimumAgeMs must be a non-negative safe integer.');
|
|
622
|
+
}
|
|
623
|
+
const { operations, cleanup, cleanupEvents, leases } = tables(options);
|
|
624
|
+
const operationStateType = `${quoteIdent(
|
|
625
|
+
options?.schema ?? 'runtime_scheduler',
|
|
626
|
+
)}.${quoteIdent('runtime_sandbox_operation_state')}`;
|
|
627
|
+
const diagnostics: RuntimeSandboxLedgerReconciliationDiagnostic[] = [];
|
|
628
|
+
async function measuredQuery<Row extends Record<string, unknown>>(
|
|
629
|
+
phase: RuntimeSandboxLedgerReconciliationPhase,
|
|
630
|
+
sql: string,
|
|
631
|
+
parameters?: unknown[],
|
|
632
|
+
): Promise<{ rows: Row[] }> {
|
|
633
|
+
const startedAt = performance.now();
|
|
634
|
+
try {
|
|
635
|
+
const result = await client.query<Row>(sql, parameters);
|
|
636
|
+
diagnostics.push({
|
|
637
|
+
phase,
|
|
638
|
+
durationMs: performance.now() - startedAt,
|
|
639
|
+
rowCount: result.rows.length,
|
|
640
|
+
});
|
|
641
|
+
return result;
|
|
642
|
+
} catch (error) {
|
|
643
|
+
diagnostics.push({
|
|
644
|
+
phase,
|
|
645
|
+
durationMs: performance.now() - startedAt,
|
|
646
|
+
rowCount: 0,
|
|
647
|
+
});
|
|
648
|
+
throw error;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
// A previous reconciliation may have observed a sandbox just before its
|
|
652
|
+
// normal cleanup completed. Repair those terminal rows before calculating
|
|
653
|
+
// the next snapshot's capacity state. This is idempotent and is deliberately
|
|
654
|
+
// a migration-only convergence step, never a worker hot-path lock.
|
|
655
|
+
let releasedTerminalLeases = 0;
|
|
656
|
+
if (!input.dryRun) {
|
|
657
|
+
const startedAt = performance.now();
|
|
658
|
+
releasedTerminalLeases = await releaseTerminalSandboxCapacityLeases(
|
|
659
|
+
client,
|
|
660
|
+
options,
|
|
661
|
+
);
|
|
662
|
+
diagnostics.push({
|
|
663
|
+
phase: RuntimeSandboxLedgerReconciliationPhase.ReleaseTerminalCapacity,
|
|
664
|
+
durationMs: performance.now() - startedAt,
|
|
665
|
+
rowCount: releasedTerminalLeases,
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const inventoryJson = JSON.stringify(
|
|
670
|
+
providerSandboxes.map((sandbox) => ({
|
|
671
|
+
sandbox_id: sandbox.sandboxId,
|
|
672
|
+
operation_key: sandbox.operationKey,
|
|
673
|
+
routing_domain: sandbox.routingDomain,
|
|
674
|
+
})),
|
|
675
|
+
);
|
|
676
|
+
const duplicateOperationTokens = new Set<string>();
|
|
677
|
+
const operationTokenCounts = new Map<string, number>();
|
|
678
|
+
for (const sandbox of providerSandboxes) {
|
|
679
|
+
if (sandbox.operationKey) {
|
|
680
|
+
operationTokenCounts.set(
|
|
681
|
+
sandbox.operationKey,
|
|
682
|
+
(operationTokenCounts.get(sandbox.operationKey) ?? 0) + 1,
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
for (const [token, count] of operationTokenCounts) {
|
|
687
|
+
if (count > 1) duplicateOperationTokens.add(token);
|
|
688
|
+
}
|
|
689
|
+
// A Daytona create can commit at the provider and lose its response before
|
|
690
|
+
// the worker records it. The exact operation label is the only selector
|
|
691
|
+
// allowed to recover that state; an id-only inventory item remains unowned
|
|
692
|
+
// and fails closed below. Provider acceptance never proves runner liveness.
|
|
693
|
+
const reservedOwned = providerSandboxes.length
|
|
694
|
+
? await measuredQuery<{
|
|
695
|
+
sandbox_id: string;
|
|
696
|
+
provider_operation_token: string;
|
|
697
|
+
routing_domain: string | null;
|
|
698
|
+
}>(
|
|
699
|
+
RuntimeSandboxLedgerReconciliationPhase.RecoverReservedOperations,
|
|
700
|
+
`
|
|
701
|
+
SELECT item.sandbox_id, operation.provider_operation_token, item.routing_domain
|
|
702
|
+
FROM jsonb_to_recordset($1::jsonb) AS item(
|
|
703
|
+
sandbox_id text, operation_key text, routing_domain text
|
|
704
|
+
)
|
|
705
|
+
JOIN ${operations} AS operation
|
|
706
|
+
ON operation.provider_operation_token = item.operation_key
|
|
707
|
+
AND operation.provider = $2
|
|
708
|
+
AND operation.environment = 'production'
|
|
709
|
+
AND operation.state = $3
|
|
710
|
+
JOIN ${leases} AS lease
|
|
711
|
+
ON lease.lease_id = operation.operation_key
|
|
712
|
+
AND lease.released_at IS NULL
|
|
713
|
+
`,
|
|
714
|
+
[
|
|
715
|
+
inventoryJson,
|
|
716
|
+
DAYTONA_PROVIDER,
|
|
717
|
+
RuntimeSandboxOperationState.Reserved,
|
|
718
|
+
],
|
|
719
|
+
)
|
|
720
|
+
: { rows: [] };
|
|
721
|
+
let recoveredReservedOperations = 0;
|
|
722
|
+
if (!input.dryRun && reservedOwned.rows.length > 0) {
|
|
723
|
+
const recovered = await measuredQuery<{ operation_key: string }>(
|
|
724
|
+
RuntimeSandboxLedgerReconciliationPhase.RecoverReservedOperations,
|
|
725
|
+
`
|
|
726
|
+
WITH inventory AS (
|
|
727
|
+
SELECT item.*
|
|
728
|
+
FROM jsonb_to_recordset($1::jsonb) AS item(
|
|
729
|
+
sandbox_id text, operation_key text, routing_domain text
|
|
730
|
+
)
|
|
731
|
+
WHERE operation_key IS NOT NULL
|
|
732
|
+
AND 1 = (
|
|
733
|
+
SELECT count(*)
|
|
734
|
+
FROM jsonb_to_recordset($1::jsonb) AS sibling(
|
|
735
|
+
sandbox_id text, operation_key text, routing_domain text
|
|
736
|
+
)
|
|
737
|
+
WHERE sibling.operation_key = item.operation_key
|
|
738
|
+
)
|
|
739
|
+
), promoted AS (
|
|
740
|
+
UPDATE ${operations} AS operation
|
|
741
|
+
SET sandbox_id = inventory.sandbox_id,
|
|
742
|
+
routing_domain = inventory.routing_domain,
|
|
743
|
+
state = $3::${operationStateType},
|
|
744
|
+
provider_created_at = clock_timestamp(),
|
|
745
|
+
updated_at = clock_timestamp()
|
|
746
|
+
FROM inventory
|
|
747
|
+
JOIN ${leases} AS lease
|
|
748
|
+
ON TRUE
|
|
749
|
+
WHERE operation.provider_operation_token = inventory.operation_key
|
|
750
|
+
AND operation.provider = $2
|
|
751
|
+
AND operation.environment = 'production'
|
|
752
|
+
AND operation.state = $4
|
|
753
|
+
AND lease.lease_id = operation.operation_key
|
|
754
|
+
AND lease.released_at IS NULL
|
|
755
|
+
RETURNING operation.operation_key, operation.run_id, operation.attempt,
|
|
756
|
+
operation.environment, operation.sandbox_id,
|
|
757
|
+
operation.routing_domain, operation.state
|
|
758
|
+
), resource_fact AS (
|
|
759
|
+
SELECT promoted.*,
|
|
760
|
+
jsonb_build_object(
|
|
761
|
+
'kind', 'daytona_sandbox',
|
|
762
|
+
'sandboxId', promoted.sandbox_id,
|
|
763
|
+
'runtimeEnvironment', promoted.environment,
|
|
764
|
+
'daytonaOrganizationId', promoted.routing_domain,
|
|
765
|
+
'billingStartedAt', extract(epoch FROM clock_timestamp()) * 1000,
|
|
766
|
+
'sandboxCapacityLeaseId', promoted.operation_key,
|
|
767
|
+
'lateAcquired', true
|
|
768
|
+
) AS resource
|
|
769
|
+
FROM promoted
|
|
770
|
+
), recorded_attempt AS (
|
|
771
|
+
-- Preserve the provider fact as a compatibility/audit projection in
|
|
772
|
+
-- the same atomic recovery transition. It is explicitly late: this
|
|
773
|
+
-- path never re-opens execution.
|
|
774
|
+
UPDATE ${tables(options).attempts} AS attempt
|
|
775
|
+
SET resource_json = CASE
|
|
776
|
+
WHEN attempt.resource_json IS NULL THEN jsonb_build_array(resource_fact.resource)
|
|
777
|
+
WHEN jsonb_typeof(attempt.resource_json) = 'array'
|
|
778
|
+
THEN attempt.resource_json || jsonb_build_array(resource_fact.resource)
|
|
779
|
+
ELSE jsonb_build_array(attempt.resource_json, resource_fact.resource)
|
|
780
|
+
END,
|
|
781
|
+
updated_at = clock_timestamp()
|
|
782
|
+
FROM resource_fact
|
|
783
|
+
WHERE attempt.run_id = resource_fact.run_id
|
|
784
|
+
AND attempt.attempt = resource_fact.attempt
|
|
785
|
+
RETURNING attempt.run_id, attempt.attempt
|
|
786
|
+
), bound AS (
|
|
787
|
+
UPDATE ${leases} AS lease
|
|
788
|
+
SET provider = $2,
|
|
789
|
+
sandbox_id = promoted.sandbox_id,
|
|
790
|
+
routing_domain = promoted.routing_domain
|
|
791
|
+
FROM promoted
|
|
792
|
+
JOIN recorded_attempt
|
|
793
|
+
ON recorded_attempt.run_id = promoted.run_id
|
|
794
|
+
AND recorded_attempt.attempt = promoted.attempt
|
|
795
|
+
WHERE lease.lease_id = promoted.operation_key
|
|
796
|
+
AND lease.released_at IS NULL
|
|
797
|
+
RETURNING lease.lease_id
|
|
798
|
+
), cleanup_inserted AS (
|
|
799
|
+
INSERT INTO ${cleanup} (
|
|
800
|
+
provider, sandbox_id, run_id, attempt, environment, routing_domain
|
|
801
|
+
)
|
|
802
|
+
SELECT $2, sandbox_id, run_id, attempt, environment, routing_domain
|
|
803
|
+
FROM promoted
|
|
804
|
+
JOIN bound ON bound.lease_id = promoted.operation_key
|
|
805
|
+
ON CONFLICT (provider, sandbox_id, routing_domain_key) DO NOTHING
|
|
806
|
+
)
|
|
807
|
+
SELECT promoted.operation_key
|
|
808
|
+
FROM promoted
|
|
809
|
+
JOIN bound ON bound.lease_id = promoted.operation_key
|
|
810
|
+
`,
|
|
811
|
+
[
|
|
812
|
+
inventoryJson,
|
|
813
|
+
DAYTONA_PROVIDER,
|
|
814
|
+
RuntimeSandboxOperationState.Deleting,
|
|
815
|
+
RuntimeSandboxOperationState.Reserved,
|
|
816
|
+
],
|
|
817
|
+
);
|
|
818
|
+
recoveredReservedOperations = recovered.rows.length;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
const liveRows = providerSandboxes.length
|
|
822
|
+
? await measuredQuery<{
|
|
823
|
+
sandbox_id: string;
|
|
824
|
+
run_id: string;
|
|
825
|
+
attempt: number;
|
|
826
|
+
org_id: string;
|
|
827
|
+
environment: string;
|
|
828
|
+
routing_domain: string | null;
|
|
829
|
+
operation_key: string;
|
|
830
|
+
}>(
|
|
831
|
+
RuntimeSandboxLedgerReconciliationPhase.MatchProviderInventory,
|
|
832
|
+
`
|
|
833
|
+
SELECT operation.sandbox_id, operation.run_id, operation.attempt,
|
|
834
|
+
operation.org_id, operation.environment, operation.routing_domain,
|
|
835
|
+
operation.operation_key
|
|
836
|
+
FROM jsonb_to_recordset($4::jsonb) AS item(
|
|
837
|
+
sandbox_id text, operation_key text, routing_domain text
|
|
838
|
+
)
|
|
839
|
+
JOIN ${operations} AS operation
|
|
840
|
+
ON operation.sandbox_id = item.sandbox_id
|
|
841
|
+
AND operation.routing_domain IS NOT DISTINCT FROM item.routing_domain
|
|
842
|
+
WHERE operation.provider = $1
|
|
843
|
+
AND operation.environment = 'production'
|
|
844
|
+
AND operation.state IN ($2, $3)
|
|
845
|
+
`,
|
|
846
|
+
[
|
|
847
|
+
DAYTONA_PROVIDER,
|
|
848
|
+
RuntimeSandboxOperationState.Live,
|
|
849
|
+
RuntimeSandboxOperationState.Deleting,
|
|
850
|
+
inventoryJson,
|
|
851
|
+
],
|
|
852
|
+
)
|
|
853
|
+
: { rows: [] };
|
|
854
|
+
const live = liveRows.rows.map((row) => ({
|
|
855
|
+
sandboxId: row.sandbox_id,
|
|
856
|
+
runId: row.run_id,
|
|
857
|
+
attempt: Number(row.attempt),
|
|
858
|
+
orgId: row.org_id,
|
|
859
|
+
environment: row.environment,
|
|
860
|
+
routingDomain: row.routing_domain,
|
|
861
|
+
operationKey: row.operation_key,
|
|
862
|
+
}));
|
|
863
|
+
const matchedIdentities = new Set(
|
|
864
|
+
live.map((row) => `${row.sandboxId}\u0000${row.routingDomain ?? ''}`),
|
|
865
|
+
);
|
|
866
|
+
const unmatchedProviderSandboxIds = providerSandboxes
|
|
867
|
+
.filter(
|
|
868
|
+
(sandbox) =>
|
|
869
|
+
!matchedIdentities.has(
|
|
870
|
+
`${sandbox.sandboxId}\u0000${sandbox.routingDomain ?? ''}`,
|
|
871
|
+
) &&
|
|
872
|
+
!reservedOwned.rows.some(
|
|
873
|
+
(owned) =>
|
|
874
|
+
owned.sandbox_id === sandbox.sandboxId &&
|
|
875
|
+
owned.routing_domain === sandbox.routingDomain &&
|
|
876
|
+
owned.provider_operation_token === sandbox.operationKey &&
|
|
877
|
+
!duplicateOperationTokens.has(sandbox.operationKey ?? ''),
|
|
878
|
+
),
|
|
879
|
+
)
|
|
880
|
+
.map((sandbox) => sandbox.sandboxId);
|
|
881
|
+
|
|
882
|
+
// Do not write an incomplete migration: a listed sandbox lacking durable
|
|
883
|
+
// ownership cannot safely be assigned to an organization for enforcement.
|
|
884
|
+
if (unmatchedProviderSandboxIds.length > 0) {
|
|
885
|
+
return {
|
|
886
|
+
provider: DAYTONA_PROVIDER,
|
|
887
|
+
providerSandboxCount: providerSandboxes.length,
|
|
888
|
+
matchedLiveSandboxes: live.length,
|
|
889
|
+
unmatchedProviderSandboxIds,
|
|
890
|
+
liveLeasesToSeed: 0,
|
|
891
|
+
absentLedgerRows: 0,
|
|
892
|
+
seededLeases: 0,
|
|
893
|
+
releasedTerminalLeases,
|
|
894
|
+
markedDeleted: 0,
|
|
895
|
+
recoveredReservedOperations,
|
|
896
|
+
hasMoreAbsentLedgerRows: false,
|
|
897
|
+
dryRun: input.dryRun,
|
|
898
|
+
diagnostics,
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
const candidates = await measuredQuery<{
|
|
903
|
+
sandbox_id: string;
|
|
904
|
+
run_id: string;
|
|
905
|
+
attempt: number;
|
|
906
|
+
org_id: string;
|
|
907
|
+
environment: string;
|
|
908
|
+
routing_domain: string | null;
|
|
909
|
+
}>(
|
|
910
|
+
RuntimeSandboxLedgerReconciliationPhase.FindAbsentOperations,
|
|
911
|
+
`
|
|
912
|
+
SELECT sandbox_id, run_id, attempt, org_id, environment, routing_domain
|
|
913
|
+
FROM ${operations}
|
|
914
|
+
WHERE provider = $1
|
|
915
|
+
AND environment = 'production'
|
|
916
|
+
AND state = $2
|
|
917
|
+
AND NOT EXISTS (
|
|
918
|
+
SELECT 1
|
|
919
|
+
FROM jsonb_to_recordset($3::jsonb) AS inventory(
|
|
920
|
+
sandbox_id text, operation_key text, routing_domain text
|
|
921
|
+
)
|
|
922
|
+
WHERE inventory.sandbox_id = ${operations}.sandbox_id
|
|
923
|
+
AND inventory.routing_domain IS NOT DISTINCT FROM ${operations}.routing_domain
|
|
924
|
+
)
|
|
925
|
+
AND NOT EXISTS (
|
|
926
|
+
SELECT 1
|
|
927
|
+
FROM ${cleanup} AS existing
|
|
928
|
+
WHERE existing.provider = '${DAYTONA_PROVIDER}'
|
|
929
|
+
AND existing.sandbox_id = ${operations}.sandbox_id
|
|
930
|
+
AND existing.routing_domain IS NOT DISTINCT FROM ${operations}.routing_domain
|
|
931
|
+
AND existing.status = 'deleted'
|
|
932
|
+
)
|
|
933
|
+
-- Provider inventory is a point-in-time observation. Never classify
|
|
934
|
+
-- an operation recorded after it as absent.
|
|
935
|
+
AND provider_created_at <= to_timestamp($4::double precision / 1000.0)
|
|
936
|
+
AND provider_created_at <= clock_timestamp() - ($5::integer * interval '1 millisecond')
|
|
937
|
+
ORDER BY provider_created_at, operation_key
|
|
938
|
+
LIMIT $6
|
|
939
|
+
`,
|
|
940
|
+
[
|
|
941
|
+
DAYTONA_PROVIDER,
|
|
942
|
+
RuntimeSandboxOperationState.Live,
|
|
943
|
+
inventoryJson,
|
|
944
|
+
input.inventoryCapturedAtMs,
|
|
945
|
+
minimumAgeMs,
|
|
946
|
+
limit + 1,
|
|
947
|
+
],
|
|
948
|
+
);
|
|
949
|
+
const hasMoreAbsentLedgerRows = candidates.rows.length > limit;
|
|
950
|
+
const absent = candidates.rows.slice(0, limit).map((row) => ({
|
|
951
|
+
sandboxId: row.sandbox_id,
|
|
952
|
+
runId: row.run_id,
|
|
953
|
+
attempt: Number(row.attempt),
|
|
954
|
+
orgId: row.org_id,
|
|
955
|
+
environment: row.environment,
|
|
956
|
+
routingDomain: row.routing_domain,
|
|
957
|
+
}));
|
|
958
|
+
|
|
959
|
+
const leaseRows = live.length
|
|
960
|
+
? await measuredQuery<{
|
|
961
|
+
sandbox_id: string;
|
|
962
|
+
routing_domain: string | null;
|
|
963
|
+
}>(
|
|
964
|
+
RuntimeSandboxLedgerReconciliationPhase.InspectCapacityBindings,
|
|
965
|
+
`
|
|
966
|
+
SELECT DISTINCT sandbox_id, routing_domain
|
|
967
|
+
FROM ${leases}
|
|
968
|
+
WHERE provider = '${DAYTONA_PROVIDER}'
|
|
969
|
+
AND sandbox_id = ANY($1::text[])
|
|
970
|
+
`,
|
|
971
|
+
[live.map((row) => row.sandboxId)],
|
|
972
|
+
)
|
|
973
|
+
: { rows: [] };
|
|
974
|
+
const existingLeaseIdentities = new Set(
|
|
975
|
+
leaseRows.rows.map(
|
|
976
|
+
(row) => `${row.sandbox_id}\u0000${row.routing_domain ?? ''}`,
|
|
977
|
+
),
|
|
978
|
+
);
|
|
979
|
+
const terminalLiveRows = live.length
|
|
980
|
+
? await measuredQuery<{
|
|
981
|
+
sandbox_id: string;
|
|
982
|
+
routing_domain: string | null;
|
|
983
|
+
}>(
|
|
984
|
+
RuntimeSandboxLedgerReconciliationPhase.InspectCapacityBindings,
|
|
985
|
+
`
|
|
986
|
+
SELECT DISTINCT cleanup.sandbox_id, cleanup.routing_domain
|
|
987
|
+
FROM ${cleanup} AS cleanup
|
|
988
|
+
WHERE cleanup.provider = '${DAYTONA_PROVIDER}'
|
|
989
|
+
AND cleanup.status = 'deleted'
|
|
990
|
+
AND cleanup.deleted_at IS NOT NULL
|
|
991
|
+
AND cleanup.sandbox_id = ANY($1::text[])
|
|
992
|
+
`,
|
|
993
|
+
[live.map((row) => row.sandboxId)],
|
|
994
|
+
)
|
|
995
|
+
: { rows: [] };
|
|
996
|
+
const terminalLiveSandboxIdentities = new Set(
|
|
997
|
+
terminalLiveRows.rows.map(
|
|
998
|
+
(row) => `${row.sandbox_id}\u0000${row.routing_domain ?? ''}`,
|
|
999
|
+
),
|
|
1000
|
+
);
|
|
1001
|
+
const liveLeasesToSeed = live.filter(
|
|
1002
|
+
(row) =>
|
|
1003
|
+
!existingLeaseIdentities.has(
|
|
1004
|
+
`${row.sandboxId}\u0000${row.routingDomain ?? ''}`,
|
|
1005
|
+
) &&
|
|
1006
|
+
!terminalLiveSandboxIdentities.has(
|
|
1007
|
+
`${row.sandboxId}\u0000${row.routingDomain ?? ''}`,
|
|
1008
|
+
),
|
|
1009
|
+
);
|
|
1010
|
+
|
|
1011
|
+
if (input.dryRun) {
|
|
1012
|
+
return {
|
|
1013
|
+
provider: DAYTONA_PROVIDER,
|
|
1014
|
+
providerSandboxCount: providerSandboxes.length,
|
|
1015
|
+
matchedLiveSandboxes: live.length,
|
|
1016
|
+
unmatchedProviderSandboxIds,
|
|
1017
|
+
liveLeasesToSeed: liveLeasesToSeed.length,
|
|
1018
|
+
absentLedgerRows: absent.length,
|
|
1019
|
+
seededLeases: 0,
|
|
1020
|
+
releasedTerminalLeases,
|
|
1021
|
+
markedDeleted: 0,
|
|
1022
|
+
recoveredReservedOperations,
|
|
1023
|
+
hasMoreAbsentLedgerRows,
|
|
1024
|
+
dryRun: true,
|
|
1025
|
+
diagnostics,
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
let seededLeases = 0;
|
|
1030
|
+
if (liveLeasesToSeed.length > 0) {
|
|
1031
|
+
const seeded = await measuredQuery<{ lease_id: string }>(
|
|
1032
|
+
RuntimeSandboxLedgerReconciliationPhase.SeedCapacityBindings,
|
|
1033
|
+
`
|
|
1034
|
+
INSERT INTO ${leases} (
|
|
1035
|
+
lease_id, org_id, run_id, provider, sandbox_id, routing_domain, expires_at, released_at
|
|
1036
|
+
)
|
|
1037
|
+
SELECT
|
|
1038
|
+
item.operation_key,
|
|
1039
|
+
item.org_id,
|
|
1040
|
+
item.run_id,
|
|
1041
|
+
'${DAYTONA_PROVIDER}',
|
|
1042
|
+
item.sandbox_id,
|
|
1043
|
+
item.routing_domain,
|
|
1044
|
+
clock_timestamp(),
|
|
1045
|
+
NULL
|
|
1046
|
+
FROM jsonb_to_recordset($1::jsonb) AS item(
|
|
1047
|
+
operation_key text, sandbox_id text, org_id text, run_id text,
|
|
1048
|
+
routing_domain text
|
|
1049
|
+
)
|
|
1050
|
+
WHERE NOT EXISTS (
|
|
1051
|
+
SELECT 1
|
|
1052
|
+
FROM ${cleanup} AS terminal_cleanup
|
|
1053
|
+
WHERE terminal_cleanup.provider = '${DAYTONA_PROVIDER}'
|
|
1054
|
+
AND terminal_cleanup.sandbox_id = item.sandbox_id
|
|
1055
|
+
AND terminal_cleanup.run_id = item.run_id
|
|
1056
|
+
AND terminal_cleanup.routing_domain IS NOT DISTINCT FROM item.routing_domain
|
|
1057
|
+
AND terminal_cleanup.status = 'deleted'
|
|
1058
|
+
AND terminal_cleanup.deleted_at IS NOT NULL
|
|
1059
|
+
)
|
|
1060
|
+
ON CONFLICT (lease_id) DO UPDATE
|
|
1061
|
+
SET org_id = EXCLUDED.org_id,
|
|
1062
|
+
run_id = EXCLUDED.run_id,
|
|
1063
|
+
provider = EXCLUDED.provider,
|
|
1064
|
+
sandbox_id = EXCLUDED.sandbox_id,
|
|
1065
|
+
routing_domain = EXCLUDED.routing_domain,
|
|
1066
|
+
expires_at = EXCLUDED.expires_at
|
|
1067
|
+
WHERE ${leases}.released_at IS NULL
|
|
1068
|
+
AND NOT EXISTS (
|
|
1069
|
+
SELECT 1
|
|
1070
|
+
FROM ${cleanup} AS terminal_cleanup
|
|
1071
|
+
WHERE terminal_cleanup.provider = '${DAYTONA_PROVIDER}'
|
|
1072
|
+
AND terminal_cleanup.sandbox_id = EXCLUDED.sandbox_id
|
|
1073
|
+
AND terminal_cleanup.run_id = EXCLUDED.run_id
|
|
1074
|
+
AND terminal_cleanup.routing_domain IS NOT DISTINCT FROM EXCLUDED.routing_domain
|
|
1075
|
+
AND terminal_cleanup.status = 'deleted'
|
|
1076
|
+
AND terminal_cleanup.deleted_at IS NOT NULL
|
|
1077
|
+
)
|
|
1078
|
+
RETURNING lease_id
|
|
1079
|
+
`,
|
|
1080
|
+
[
|
|
1081
|
+
JSON.stringify(
|
|
1082
|
+
liveLeasesToSeed.map((row) => ({
|
|
1083
|
+
operation_key: row.operationKey,
|
|
1084
|
+
sandbox_id: row.sandboxId,
|
|
1085
|
+
org_id: row.orgId,
|
|
1086
|
+
run_id: row.runId,
|
|
1087
|
+
routing_domain: row.routingDomain,
|
|
1088
|
+
})),
|
|
1089
|
+
),
|
|
1090
|
+
],
|
|
1091
|
+
);
|
|
1092
|
+
seededLeases = seeded.rows.length;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
const marked = absent.length
|
|
1096
|
+
? await measuredQuery<{ sandbox_id: string }>(
|
|
1097
|
+
RuntimeSandboxLedgerReconciliationPhase.MarkProviderConfirmedAbsent,
|
|
1098
|
+
`
|
|
1099
|
+
WITH candidates AS (
|
|
1100
|
+
SELECT *
|
|
1101
|
+
FROM jsonb_to_recordset($1::jsonb) AS item(
|
|
1102
|
+
sandbox_id text,
|
|
1103
|
+
run_id text,
|
|
1104
|
+
attempt integer,
|
|
1105
|
+
environment text,
|
|
1106
|
+
routing_domain text
|
|
1107
|
+
)
|
|
1108
|
+
),
|
|
1109
|
+
changed AS (
|
|
1110
|
+
INSERT INTO ${cleanup} (
|
|
1111
|
+
provider, sandbox_id, run_id, attempt, environment, routing_domain,
|
|
1112
|
+
status, deleted_at, available_at, updated_at, last_error
|
|
1113
|
+
)
|
|
1114
|
+
SELECT
|
|
1115
|
+
'${DAYTONA_PROVIDER}', sandbox_id, run_id, attempt, environment, routing_domain,
|
|
1116
|
+
'deleted', clock_timestamp(), clock_timestamp(), clock_timestamp(),
|
|
1117
|
+
'reconciled_provider_confirmed_absent'
|
|
1118
|
+
FROM candidates
|
|
1119
|
+
ON CONFLICT (provider, sandbox_id, routing_domain_key) DO UPDATE
|
|
1120
|
+
SET status = 'deleted',
|
|
1121
|
+
deleted_at = clock_timestamp(),
|
|
1122
|
+
lease_owner = NULL,
|
|
1123
|
+
lease_expires_at = NULL,
|
|
1124
|
+
updated_at = clock_timestamp(),
|
|
1125
|
+
last_error = 'reconciled_provider_confirmed_absent'
|
|
1126
|
+
-- A real cleanup worker owns deleting rows. Do not erase its
|
|
1127
|
+
-- lease/fencing state; its normal completion records the outcome.
|
|
1128
|
+
WHERE ${cleanup}.status = 'pending'
|
|
1129
|
+
AND ${cleanup}.lease_owner IS NULL
|
|
1130
|
+
RETURNING sandbox_id, routing_domain, run_id, attempt
|
|
1131
|
+
),
|
|
1132
|
+
events AS (
|
|
1133
|
+
INSERT INTO ${cleanupEvents} (
|
|
1134
|
+
provider, sandbox_id, routing_domain, run_id, attempt,
|
|
1135
|
+
cleanup_attempt, lease_owner, event_type, details_json
|
|
1136
|
+
)
|
|
1137
|
+
SELECT
|
|
1138
|
+
'${DAYTONA_PROVIDER}', sandbox_id, routing_domain, run_id, attempt,
|
|
1139
|
+
0, NULL, 'reconciled_provider_confirmed_absent',
|
|
1140
|
+
jsonb_build_object('source', 'runtime-sandbox-ledger-reconciliation')
|
|
1141
|
+
FROM changed
|
|
1142
|
+
),
|
|
1143
|
+
operation_deleted AS (
|
|
1144
|
+
UPDATE ${operations} AS operation
|
|
1145
|
+
SET state = $2,
|
|
1146
|
+
delete_confirmed_at = COALESCE(
|
|
1147
|
+
operation.delete_confirmed_at,
|
|
1148
|
+
clock_timestamp()
|
|
1149
|
+
),
|
|
1150
|
+
updated_at = clock_timestamp()
|
|
1151
|
+
FROM changed
|
|
1152
|
+
WHERE operation.provider = '${DAYTONA_PROVIDER}'
|
|
1153
|
+
AND operation.sandbox_id = changed.sandbox_id
|
|
1154
|
+
AND operation.routing_domain IS NOT DISTINCT FROM changed.routing_domain
|
|
1155
|
+
AND operation.state = $3
|
|
1156
|
+
RETURNING operation.operation_key
|
|
1157
|
+
),
|
|
1158
|
+
released_leases AS (
|
|
1159
|
+
UPDATE ${leases} AS lease
|
|
1160
|
+
SET released_at = clock_timestamp()
|
|
1161
|
+
FROM changed
|
|
1162
|
+
WHERE lease.provider = '${DAYTONA_PROVIDER}'
|
|
1163
|
+
AND lease.run_id = changed.run_id
|
|
1164
|
+
AND lease.sandbox_id = changed.sandbox_id
|
|
1165
|
+
AND lease.routing_domain IS NOT DISTINCT FROM changed.routing_domain
|
|
1166
|
+
AND lease.released_at IS NULL
|
|
1167
|
+
)
|
|
1168
|
+
SELECT sandbox_id FROM changed
|
|
1169
|
+
`,
|
|
1170
|
+
[
|
|
1171
|
+
JSON.stringify(
|
|
1172
|
+
absent.map((row) => ({
|
|
1173
|
+
sandbox_id: row.sandboxId,
|
|
1174
|
+
run_id: row.runId,
|
|
1175
|
+
attempt: row.attempt,
|
|
1176
|
+
environment: row.environment,
|
|
1177
|
+
routing_domain: row.routingDomain,
|
|
1178
|
+
})),
|
|
1179
|
+
),
|
|
1180
|
+
RuntimeSandboxOperationState.Deleted,
|
|
1181
|
+
RuntimeSandboxOperationState.Live,
|
|
1182
|
+
],
|
|
1183
|
+
)
|
|
1184
|
+
: { rows: [] };
|
|
1185
|
+
|
|
1186
|
+
// Covers the narrow race where a normal cleanup reaches provider-confirmed
|
|
1187
|
+
// deletion after the pre-seed repair but before this reconciliation returns.
|
|
1188
|
+
// The worker path also releases its own leases; this makes the migration
|
|
1189
|
+
// converge even when an older worker completed cleanup first.
|
|
1190
|
+
if (!input.dryRun) {
|
|
1191
|
+
const startedAt = performance.now();
|
|
1192
|
+
releasedTerminalLeases += await releaseTerminalSandboxCapacityLeases(
|
|
1193
|
+
client,
|
|
1194
|
+
options,
|
|
1195
|
+
);
|
|
1196
|
+
diagnostics.push({
|
|
1197
|
+
phase: RuntimeSandboxLedgerReconciliationPhase.ReleaseTerminalCapacity,
|
|
1198
|
+
durationMs: performance.now() - startedAt,
|
|
1199
|
+
rowCount: releasedTerminalLeases,
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
return {
|
|
1204
|
+
provider: DAYTONA_PROVIDER,
|
|
1205
|
+
providerSandboxCount: providerSandboxes.length,
|
|
1206
|
+
matchedLiveSandboxes: live.length,
|
|
1207
|
+
unmatchedProviderSandboxIds,
|
|
1208
|
+
liveLeasesToSeed: liveLeasesToSeed.length,
|
|
1209
|
+
absentLedgerRows: absent.length,
|
|
1210
|
+
seededLeases,
|
|
1211
|
+
releasedTerminalLeases,
|
|
1212
|
+
markedDeleted: marked.rows.length,
|
|
1213
|
+
recoveredReservedOperations,
|
|
1214
|
+
hasMoreAbsentLedgerRows,
|
|
1215
|
+
dryRun: false,
|
|
1216
|
+
diagnostics,
|
|
1217
|
+
};
|
|
1218
|
+
}
|