deepline 0.3.89 → 0.3.91
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 +221 -43
- package/dist/bundling-sources/sdk/src/index.ts +4 -0
- package/dist/bundling-sources/sdk/src/monitor-fleet-contract.ts +2 -556
- package/dist/bundling-sources/sdk/src/monitor-fleets.ts +2 -2
- 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 +11 -9
- package/dist/bundling-sources/sdk/src/types.ts +29 -1
- package/dist/bundling-sources/shared_libs/integration-contracts/monitor-fleet-contract.ts +549 -0
- package/dist/bundling-sources/shared_libs/monitors/analytics-public.ts +10 -0
- package/dist/bundling-sources/shared_libs/monitors/fleets/errors.ts +96 -0
- package/dist/bundling-sources/shared_libs/monitors/fleets/limits.ts +8 -0
- package/dist/bundling-sources/shared_libs/monitors/fleets/plan.ts +320 -0
- package/dist/bundling-sources/shared_libs/monitors/fleets/verdict.ts +106 -0
- package/dist/bundling-sources/shared_libs/monitors/http-error-response.ts +8 -1
- 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/monitor-fleet-limits.ts +2 -12
- package/dist/bundling-sources/shared_libs/monitors/package.json +113 -4
- package/dist/bundling-sources/shared_libs/monitors/provider-monitor-batch-operation.ts +56 -9
- 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 +690 -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 +46 -1
- 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/db-session-lease-store.ts +578 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/commands/roll-execution-relay.ts +521 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/commands/roll-receipt-gateway.ts +1378 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/commands/runtime-topology.ts +1024 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-gateway-route-probe.ts +193 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-machine-lifecycle.ts +104 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-machines.ts +1844 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-registry.ts +21 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/process-tree.ts +40 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-gateway-drain.ts +262 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-surface.ts +98 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-topology.ts +303 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-tuple-publication-guardian.ts +70 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-tuple-recovery.ts +519 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/vercel-fly-rollback-alignment.ts +280 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/vercel-production-gate.ts +159 -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/memory-store.ts +114 -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/run-ledger.ts +44 -5
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/child-play-resolver.ts +26 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +3662 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/progress-sidecar.ts +47 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-deadline.ts +84 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-gateway-transport.ts +91 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-heartbeat.ts +424 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-progress-push.ts +321 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-sheet-persistence-retry.ts +110 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-terminal-push.ts +625 -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 +103 -208
- 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/webhook-receipt-retry.ts +50 -0
- 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/bundling/repository-adapter.ts +24 -0
- package/dist/bundling-sources/shared_libs/plays/child-run-projection.ts +174 -0
- package/dist/bundling-sources/shared_libs/plays/contracts.ts +2 -0
- package/dist/bundling-sources/shared_libs/plays/deploy/api-client.ts +267 -0
- package/dist/bundling-sources/shared_libs/plays/deploy/control-plane-types.ts +73 -0
- package/dist/bundling-sources/shared_libs/plays/deploy/manifest.ts +162 -0
- package/dist/bundling-sources/shared_libs/plays/deploy/project-serializer.ts +87 -0
- package/dist/bundling-sources/shared_libs/plays/deploy/source-file-path.ts +26 -0
- package/dist/bundling-sources/shared_libs/plays/deploy/target-paths.ts +36 -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/launch/input-contract.ts +400 -0
- package/dist/bundling-sources/shared_libs/plays/launch/prepare.ts +96 -0
- package/dist/bundling-sources/shared_libs/plays/local-file-discovery.ts +385 -0
- package/dist/bundling-sources/shared_libs/plays/play-grep-search.ts +112 -0
- package/dist/bundling-sources/shared_libs/plays/play-reference.ts +117 -0
- package/dist/bundling-sources/shared_libs/plays/run-input-summary.ts +260 -0
- package/dist/bundling-sources/shared_libs/plays/run-summary-types.ts +49 -0
- package/dist/bundling-sources/shared_libs/plays/secrets/errors.ts +19 -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/sheet-filter.ts +347 -0
- package/dist/bundling-sources/shared_libs/plays/slack-hitl-timeout-error.ts +37 -0
- package/dist/bundling-sources/shared_libs/plays/standard-webhooks.ts +117 -0
- package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +6 -1
- package/dist/bundling-sources/shared_libs/plays/status-result.ts +388 -0
- package/dist/bundling-sources/shared_libs/plays/tool-result-types.ts +1 -1
- package/dist/bundling-sources/shared_libs/plays/transform-factor-structure.ts +377 -0
- package/dist/bundling-sources/shared_libs/plays/webhook-payload.ts +113 -0
- 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 +895 -407
- package/dist/cli/index.mjs +893 -414
- package/dist/{compiler-manifest-DsLbQaew.d.mts → compiler-manifest-x14KL83E.d.mts} +4 -2
- package/dist/{compiler-manifest-DsLbQaew.d.ts → compiler-manifest-x14KL83E.d.ts} +4 -2
- package/dist/index.d.mts +124 -154
- package/dist/index.d.ts +124 -154
- package/dist/index.js +132 -28
- package/dist/index.mjs +132 -28
- package/dist/install-integrity.json +182 -2
- package/dist/monitor-fleet-contract.d.mts +131 -0
- package/dist/monitor-fleet-contract.d.ts +131 -0
- package/dist/monitor-fleet-contract.js +406 -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 +210 -0
- package/dist/release.mjs +183 -0
- package/package.json +21 -4
package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts
ADDED
|
@@ -0,0 +1,942 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PlayRunnerRateStateAcquireInput,
|
|
3
|
+
PlayRunnerRateStateAcquireResult,
|
|
4
|
+
PlayRunnerRateStatePenalizeInput,
|
|
5
|
+
PlayRunnerRateStateReleaseInput,
|
|
6
|
+
} from '../protocol';
|
|
7
|
+
import type { PacingRule } from '../governor/rate-state-backend';
|
|
8
|
+
import type { PostgresSchedulerQueryClient } from './postgres';
|
|
9
|
+
import { resolveRuntimeSchedulerSchemaEnv } from './runtime-scheduler-schema';
|
|
10
|
+
import { stringifyPostgresJson } from '../postgres-json';
|
|
11
|
+
|
|
12
|
+
const DEFAULT_SCHEMA = 'runtime_scheduler';
|
|
13
|
+
const RATE_BUCKET_TABLE = 'rate_bucket';
|
|
14
|
+
const RATE_CONCURRENCY_LEASE_TABLE = 'rate_concurrency_leases';
|
|
15
|
+
const MIN_CONCURRENCY_WAIT_MS = 10;
|
|
16
|
+
const ACTIVE_LEASE_TTL_MS = 5 * 60 * 1000;
|
|
17
|
+
const MIN_ADAPTIVE_RPS_FLOOR = 0.01;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Internal cooldown cap. A 429 report (penalize) can carry an arbitrarily large
|
|
21
|
+
* server Retry-After; we store that verbatim in `claimed_retry_at_ms` for
|
|
22
|
+
* observability but cap the pacer's own cooldown at 45 minutes so a stuck
|
|
23
|
+
* provider is re-probed at least this often instead of freezing the bucket for
|
|
24
|
+
* the whole (possibly hours-long) Retry-After.
|
|
25
|
+
*/
|
|
26
|
+
const MAX_INTERNAL_COOLDOWN_MS = 45 * 60 * 1000;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Additive-ramp multiplier applied to undeclared default-pacing buckets.
|
|
30
|
+
* Declared rules stay pinned at base unless they carry an explicit adaptive
|
|
31
|
+
* ceiling.
|
|
32
|
+
*/
|
|
33
|
+
const DEFAULT_PACING_MAX_RPS_MULTIPLIER = 25;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Low-rate provider requests cross the sandbox -> gateway -> app boundary after
|
|
37
|
+
* their permit wakes. Small network-latency differences can compress two
|
|
38
|
+
* otherwise exact 100ms/200ms intervals at the provider and turn a mathematically
|
|
39
|
+
* valid edge into a rolling-window 429. Keep a 20% arrival margin below 100
|
|
40
|
+
* RPS. High-rate providers retain their microburst schedule and full rate.
|
|
41
|
+
*/
|
|
42
|
+
const LOW_RATE_DISPATCH_HEADROOM_FACTOR = 0.8;
|
|
43
|
+
const LOW_RATE_DISPATCH_HEADROOM_THRESHOLD_RPS = 100;
|
|
44
|
+
|
|
45
|
+
function quoteIdent(value: string): string {
|
|
46
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function schemaName(options?: {
|
|
50
|
+
schema?: string | null;
|
|
51
|
+
schedulerSchema?: string | null;
|
|
52
|
+
}): string {
|
|
53
|
+
return (
|
|
54
|
+
options?.schema?.trim() ||
|
|
55
|
+
options?.schedulerSchema?.trim() ||
|
|
56
|
+
resolveRuntimeSchedulerSchemaEnv() ||
|
|
57
|
+
DEFAULT_SCHEMA
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function bucketTableName(options?: {
|
|
62
|
+
schema?: string | null;
|
|
63
|
+
schedulerSchema?: string | null;
|
|
64
|
+
}): string {
|
|
65
|
+
return `${quoteIdent(schemaName(options))}.${quoteIdent(RATE_BUCKET_TABLE)}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function concurrencyLeaseTableName(options?: {
|
|
69
|
+
schema?: string | null;
|
|
70
|
+
schedulerSchema?: string | null;
|
|
71
|
+
}): string {
|
|
72
|
+
return `${quoteIdent(schemaName(options))}.${quoteIdent(RATE_CONCURRENCY_LEASE_TABLE)}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Acquire input rules may carry an optional `adaptiveMaxRps` the client can send
|
|
77
|
+
* to widen a bucket's recovery ceiling above its declared base rate. When
|
|
78
|
+
* absent, `default:*` rules get base * 25 and declared providers pin at base.
|
|
79
|
+
*/
|
|
80
|
+
type AcquirePacingRule = PacingRule & {
|
|
81
|
+
adaptiveMaxRps?: number | null;
|
|
82
|
+
adaptiveMaxConfigured?: boolean;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
function normalizeRules(
|
|
86
|
+
rules: readonly AcquirePacingRule[],
|
|
87
|
+
): AcquirePacingRule[] {
|
|
88
|
+
return [...rules]
|
|
89
|
+
.map((rule) => {
|
|
90
|
+
const isDefaultPacing = rule.ruleId.startsWith('default:');
|
|
91
|
+
return {
|
|
92
|
+
ruleId: rule.ruleId,
|
|
93
|
+
// Rates are fractional; only dispatched requests must be whole.
|
|
94
|
+
requestsPerWindow: rule.requestsPerWindow,
|
|
95
|
+
windowMs: Math.max(0, Math.trunc(rule.windowMs)),
|
|
96
|
+
maxConcurrency:
|
|
97
|
+
rule.maxConcurrency == null
|
|
98
|
+
? null
|
|
99
|
+
: Math.max(0, Math.trunc(rule.maxConcurrency)),
|
|
100
|
+
adaptiveMaxRps:
|
|
101
|
+
rule.adaptiveMaxRps == null || !Number.isFinite(rule.adaptiveMaxRps)
|
|
102
|
+
? null
|
|
103
|
+
: Math.max(0, rule.adaptiveMaxRps),
|
|
104
|
+
adaptiveMaxConfigured:
|
|
105
|
+
isDefaultPacing ||
|
|
106
|
+
Object.prototype.hasOwnProperty.call(rule, 'adaptiveMaxRps'),
|
|
107
|
+
};
|
|
108
|
+
})
|
|
109
|
+
.filter(
|
|
110
|
+
(rule) =>
|
|
111
|
+
rule.ruleId &&
|
|
112
|
+
Number.isFinite(rule.requestsPerWindow) &&
|
|
113
|
+
rule.requestsPerWindow > 0 &&
|
|
114
|
+
rule.windowMs > 0,
|
|
115
|
+
)
|
|
116
|
+
.sort((a, b) => a.ruleId.localeCompare(b.ruleId));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Seed parameters for a rule row. `base_refill` is the declared rate in tokens
|
|
121
|
+
* per second; `capacity` is one window of burst. The adaptive `[min,max]` range
|
|
122
|
+
* is derived value-first so the same wire shape drives both declared and
|
|
123
|
+
* default-pacing buckets:
|
|
124
|
+
* - min_rps = min(base, max(0.01, base/8))
|
|
125
|
+
* (low-RPM rules must still be able to back down below their base)
|
|
126
|
+
* - max_rps = base (declared: recovery climbs back TO base only)
|
|
127
|
+
* - max_rps = base * 25 (undeclared default:* buckets, or explicit
|
|
128
|
+
* adaptiveMaxRps override)
|
|
129
|
+
*/
|
|
130
|
+
function seedForRule(rule: AcquirePacingRule): {
|
|
131
|
+
rule_id: string;
|
|
132
|
+
capacity: number;
|
|
133
|
+
base_refill_per_sec: number;
|
|
134
|
+
min_rps: number;
|
|
135
|
+
max_rps: number;
|
|
136
|
+
adaptive_max_configured: boolean;
|
|
137
|
+
} {
|
|
138
|
+
// Even a sub-one-request window must accumulate one complete permit.
|
|
139
|
+
// Preserve its fractional refill rate, with dispatch times resolved in ms.
|
|
140
|
+
const capacity = Math.max(1, rule.requestsPerWindow);
|
|
141
|
+
const base = (rule.requestsPerWindow / rule.windowMs) * 1000;
|
|
142
|
+
const minRps = Math.min(base, Math.max(MIN_ADAPTIVE_RPS_FLOOR, base / 8));
|
|
143
|
+
const isDefaultPacing = rule.ruleId.startsWith('default:');
|
|
144
|
+
const maxRps =
|
|
145
|
+
rule.adaptiveMaxRps != null && rule.adaptiveMaxRps > base
|
|
146
|
+
? rule.adaptiveMaxRps
|
|
147
|
+
: isDefaultPacing
|
|
148
|
+
? base * DEFAULT_PACING_MAX_RPS_MULTIPLIER
|
|
149
|
+
: base;
|
|
150
|
+
return {
|
|
151
|
+
rule_id: rule.ruleId,
|
|
152
|
+
capacity,
|
|
153
|
+
base_refill_per_sec: base,
|
|
154
|
+
min_rps: minRps,
|
|
155
|
+
max_rps: Math.max(maxRps, base),
|
|
156
|
+
adaptive_max_configured: rule.adaptiveMaxConfigured === true,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function positiveInt(value: number, label: string): number {
|
|
161
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
162
|
+
throw new Error(`${label} must be a positive finite number.`);
|
|
163
|
+
}
|
|
164
|
+
return Math.trunc(value);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function ensurePostgresRateStateSchema(
|
|
168
|
+
client: PostgresSchedulerQueryClient,
|
|
169
|
+
options?: { schema?: string | null },
|
|
170
|
+
): Promise<void> {
|
|
171
|
+
// Token-bucket + AIMD-in-the-row state. One row per (bucket_id, rule_id). The
|
|
172
|
+
// hot path is a single autocommit statement. It locks only the selected
|
|
173
|
+
// bucket rows, in rule-id order, while it refills, grants, schedules, deducts,
|
|
174
|
+
// and folds the ramp into the same UPDATE.
|
|
175
|
+
await client.query(`
|
|
176
|
+
CREATE TABLE IF NOT EXISTS ${bucketTableName(options)} (
|
|
177
|
+
bucket_id text NOT NULL,
|
|
178
|
+
rule_id text NOT NULL,
|
|
179
|
+
tokens float8 NOT NULL,
|
|
180
|
+
capacity float8 NOT NULL,
|
|
181
|
+
refill_per_sec float8 NOT NULL,
|
|
182
|
+
base_refill_per_sec float8 NOT NULL,
|
|
183
|
+
min_rps float8 NOT NULL,
|
|
184
|
+
max_rps float8 NOT NULL,
|
|
185
|
+
seen_backpressure boolean NOT NULL DEFAULT false,
|
|
186
|
+
cool_until_ms bigint NOT NULL DEFAULT 0,
|
|
187
|
+
claimed_retry_at_ms bigint NOT NULL DEFAULT 0,
|
|
188
|
+
last_ramp_at_ms bigint NOT NULL DEFAULT 0,
|
|
189
|
+
refilled_at_ms bigint NOT NULL,
|
|
190
|
+
PRIMARY KEY (bucket_id, rule_id)
|
|
191
|
+
)
|
|
192
|
+
`);
|
|
193
|
+
await client.query(`
|
|
194
|
+
ALTER TABLE ${bucketTableName(options)}
|
|
195
|
+
ADD COLUMN IF NOT EXISTS next_dispatch_at_ms bigint NOT NULL DEFAULT 0
|
|
196
|
+
`);
|
|
197
|
+
await client.query(`
|
|
198
|
+
ALTER TABLE ${bucketTableName(options)}
|
|
199
|
+
ADD COLUMN IF NOT EXISTS adaptive_max_configured boolean NOT NULL DEFAULT false
|
|
200
|
+
`);
|
|
201
|
+
await client.query(`
|
|
202
|
+
CREATE TABLE IF NOT EXISTS ${concurrencyLeaseTableName(options)} (
|
|
203
|
+
bucket_id text NOT NULL,
|
|
204
|
+
rule_id text NOT NULL,
|
|
205
|
+
lease_id text NOT NULL,
|
|
206
|
+
permits integer NOT NULL CHECK (permits > 0),
|
|
207
|
+
expires_at_ms bigint NOT NULL,
|
|
208
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
209
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
210
|
+
PRIMARY KEY (bucket_id, rule_id, lease_id)
|
|
211
|
+
)
|
|
212
|
+
`);
|
|
213
|
+
await client.query(`
|
|
214
|
+
CREATE INDEX IF NOT EXISTS runtime_scheduler_rate_concurrency_expiry_idx
|
|
215
|
+
ON ${concurrencyLeaseTableName(options)} (expires_at_ms)
|
|
216
|
+
`);
|
|
217
|
+
await client.query(`
|
|
218
|
+
CREATE INDEX IF NOT EXISTS runtime_scheduler_rate_bucket_bucket_idx
|
|
219
|
+
ON ${bucketTableName(options)} (bucket_id)
|
|
220
|
+
`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Seed missing rule rows and refresh changed configuration without erasing
|
|
225
|
+
* learned AIMD state. The conflict WHERE keeps the warm path a no-op, while a
|
|
226
|
+
* newly deployed adaptive ceiling reaches existing production buckets.
|
|
227
|
+
*/
|
|
228
|
+
async function seedBucketRows(
|
|
229
|
+
client: PostgresSchedulerQueryClient,
|
|
230
|
+
input: { bucketId: string } & {
|
|
231
|
+
schema?: string | null;
|
|
232
|
+
schedulerSchema?: string | null;
|
|
233
|
+
},
|
|
234
|
+
rules: AcquirePacingRule[],
|
|
235
|
+
nowMs: number,
|
|
236
|
+
): Promise<void> {
|
|
237
|
+
const seeds = rules.map(seedForRule);
|
|
238
|
+
await client.query(
|
|
239
|
+
`
|
|
240
|
+
INSERT INTO ${bucketTableName(input)} AS existing (
|
|
241
|
+
bucket_id,
|
|
242
|
+
rule_id,
|
|
243
|
+
tokens,
|
|
244
|
+
capacity,
|
|
245
|
+
refill_per_sec,
|
|
246
|
+
base_refill_per_sec,
|
|
247
|
+
min_rps,
|
|
248
|
+
max_rps,
|
|
249
|
+
adaptive_max_configured,
|
|
250
|
+
seen_backpressure,
|
|
251
|
+
cool_until_ms,
|
|
252
|
+
claimed_retry_at_ms,
|
|
253
|
+
last_ramp_at_ms,
|
|
254
|
+
refilled_at_ms,
|
|
255
|
+
next_dispatch_at_ms
|
|
256
|
+
)
|
|
257
|
+
SELECT
|
|
258
|
+
$1,
|
|
259
|
+
seed.rule_id,
|
|
260
|
+
seed.capacity,
|
|
261
|
+
seed.capacity,
|
|
262
|
+
seed.base_refill_per_sec,
|
|
263
|
+
seed.base_refill_per_sec,
|
|
264
|
+
seed.min_rps,
|
|
265
|
+
seed.max_rps,
|
|
266
|
+
seed.adaptive_max_configured,
|
|
267
|
+
false,
|
|
268
|
+
0,
|
|
269
|
+
0,
|
|
270
|
+
0,
|
|
271
|
+
$3::bigint,
|
|
272
|
+
$3::bigint
|
|
273
|
+
FROM jsonb_to_recordset($2::jsonb) AS seed(
|
|
274
|
+
rule_id text,
|
|
275
|
+
capacity float8,
|
|
276
|
+
base_refill_per_sec float8,
|
|
277
|
+
min_rps float8,
|
|
278
|
+
max_rps float8,
|
|
279
|
+
adaptive_max_configured boolean
|
|
280
|
+
)
|
|
281
|
+
ON CONFLICT (bucket_id, rule_id) DO UPDATE SET
|
|
282
|
+
tokens = LEAST(existing.tokens, EXCLUDED.capacity),
|
|
283
|
+
capacity = EXCLUDED.capacity,
|
|
284
|
+
refill_per_sec = LEAST(
|
|
285
|
+
EXCLUDED.max_rps,
|
|
286
|
+
GREATEST(EXCLUDED.min_rps, existing.refill_per_sec)
|
|
287
|
+
),
|
|
288
|
+
base_refill_per_sec = EXCLUDED.base_refill_per_sec,
|
|
289
|
+
min_rps = EXCLUDED.min_rps,
|
|
290
|
+
max_rps = CASE
|
|
291
|
+
WHEN EXCLUDED.adaptive_max_configured THEN EXCLUDED.max_rps
|
|
292
|
+
ELSE existing.max_rps
|
|
293
|
+
END,
|
|
294
|
+
adaptive_max_configured =
|
|
295
|
+
existing.adaptive_max_configured OR EXCLUDED.adaptive_max_configured
|
|
296
|
+
WHERE existing.capacity IS DISTINCT FROM EXCLUDED.capacity
|
|
297
|
+
OR existing.base_refill_per_sec IS DISTINCT FROM EXCLUDED.base_refill_per_sec
|
|
298
|
+
OR existing.min_rps IS DISTINCT FROM EXCLUDED.min_rps
|
|
299
|
+
OR (
|
|
300
|
+
EXCLUDED.adaptive_max_configured
|
|
301
|
+
AND existing.max_rps IS DISTINCT FROM EXCLUDED.max_rps
|
|
302
|
+
)
|
|
303
|
+
`,
|
|
304
|
+
[input.bucketId, stringifyPostgresJson(seeds), nowMs],
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Pure-rate (no maxConcurrency) acquire. A deterministic row lock inside one
|
|
310
|
+
* autocommit statement makes every contender reserve from the latest committed
|
|
311
|
+
* token and dispatch cursor instead of reusing a stale CTE snapshot. It refills
|
|
312
|
+
* every rule row against clock_timestamp(), grants the integer floor of the min
|
|
313
|
+
* available tokens across the bucket's rules (multi-rule = most constrained),
|
|
314
|
+
* deducts, and folds the AIMD ramp in. Returns the granted count and, when
|
|
315
|
+
* granted is 0, the wait until one more token accrues on the binding rule (or
|
|
316
|
+
* the cooldown clears).
|
|
317
|
+
*/
|
|
318
|
+
async function acquirePureRate(
|
|
319
|
+
client: PostgresSchedulerQueryClient,
|
|
320
|
+
input: PlayRunnerRateStateAcquireInput & { nowMs?: number },
|
|
321
|
+
rules: AcquirePacingRule[],
|
|
322
|
+
requested: number,
|
|
323
|
+
): Promise<PlayRunnerRateStateAcquireResult> {
|
|
324
|
+
const ruleIds = rules.map((rule) => rule.ruleId);
|
|
325
|
+
const result = await client.query<{
|
|
326
|
+
granted: string | number;
|
|
327
|
+
wait_ms: string | number;
|
|
328
|
+
server_now_ms: string | number;
|
|
329
|
+
cool_until_ms: string | number;
|
|
330
|
+
claimed_retry_at_ms: string | number;
|
|
331
|
+
scheduled_at_ms: Array<string | number> | null;
|
|
332
|
+
effective_rps: string | number;
|
|
333
|
+
}>(
|
|
334
|
+
`
|
|
335
|
+
WITH now_value AS (
|
|
336
|
+
SELECT COALESCE(
|
|
337
|
+
$3::bigint,
|
|
338
|
+
FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint
|
|
339
|
+
) AS now_ms
|
|
340
|
+
),
|
|
341
|
+
refreshed AS (
|
|
342
|
+
SELECT
|
|
343
|
+
b.bucket_id,
|
|
344
|
+
b.rule_id,
|
|
345
|
+
b.capacity,
|
|
346
|
+
b.min_rps,
|
|
347
|
+
b.max_rps,
|
|
348
|
+
b.seen_backpressure,
|
|
349
|
+
b.base_refill_per_sec,
|
|
350
|
+
b.refill_per_sec,
|
|
351
|
+
b.cool_until_ms,
|
|
352
|
+
b.last_ramp_at_ms,
|
|
353
|
+
b.next_dispatch_at_ms,
|
|
354
|
+
nv.now_ms AS now_ms,
|
|
355
|
+
LEAST(
|
|
356
|
+
b.capacity,
|
|
357
|
+
b.tokens + GREATEST(0, nv.now_ms - b.refilled_at_ms) / 1000.0 * b.refill_per_sec
|
|
358
|
+
) AS tokens_after_refill
|
|
359
|
+
FROM ${bucketTableName(input)} b
|
|
360
|
+
CROSS JOIN now_value nv
|
|
361
|
+
WHERE b.bucket_id = $1
|
|
362
|
+
AND b.rule_id = ANY($2::text[])
|
|
363
|
+
ORDER BY b.rule_id
|
|
364
|
+
FOR UPDATE OF b
|
|
365
|
+
),
|
|
366
|
+
cooling AS (
|
|
367
|
+
SELECT COALESCE(MAX(cool_until_ms), 0) AS cool_until_ms
|
|
368
|
+
FROM refreshed
|
|
369
|
+
),
|
|
370
|
+
grant_calc AS (
|
|
371
|
+
SELECT
|
|
372
|
+
-- Cooling buckets grant nothing; otherwise the integer floor of the
|
|
373
|
+
-- minimum post-refill tokens across every rule row (most constrained).
|
|
374
|
+
CASE
|
|
375
|
+
WHEN (SELECT cool_until_ms FROM cooling) > (SELECT now_ms FROM refreshed LIMIT 1)
|
|
376
|
+
THEN 0
|
|
377
|
+
ELSE GREATEST(0, LEAST($4::int, FLOOR(MIN(tokens_after_refill))::int))
|
|
378
|
+
END AS granted
|
|
379
|
+
FROM refreshed
|
|
380
|
+
),
|
|
381
|
+
schedule_calc AS (
|
|
382
|
+
SELECT
|
|
383
|
+
GREATEST(
|
|
384
|
+
MAX(now_ms),
|
|
385
|
+
MAX(cool_until_ms),
|
|
386
|
+
MAX(next_dispatch_at_ms)
|
|
387
|
+
)::bigint AS first_at_ms,
|
|
388
|
+
CEIL(MAX(
|
|
389
|
+
1000.0 / GREATEST(
|
|
390
|
+
refill_per_sec * CASE
|
|
391
|
+
WHEN refill_per_sec < ${LOW_RATE_DISPATCH_HEADROOM_THRESHOLD_RPS}
|
|
392
|
+
THEN ${LOW_RATE_DISPATCH_HEADROOM_FACTOR}
|
|
393
|
+
ELSE 1.0
|
|
394
|
+
END,
|
|
395
|
+
0.0001
|
|
396
|
+
)
|
|
397
|
+
))::bigint AS spacing_ms,
|
|
398
|
+
GREATEST(1, FLOOR(MIN(refill_per_sec) / 100.0))::bigint AS burst_size
|
|
399
|
+
FROM refreshed
|
|
400
|
+
),
|
|
401
|
+
updated AS (
|
|
402
|
+
UPDATE ${bucketTableName(input)} b
|
|
403
|
+
SET
|
|
404
|
+
tokens = r.tokens_after_refill - (SELECT granted FROM grant_calc),
|
|
405
|
+
refilled_at_ms = r.now_ms,
|
|
406
|
+
next_dispatch_at_ms = CASE
|
|
407
|
+
WHEN (SELECT granted FROM grant_calc) > 0
|
|
408
|
+
THEN (SELECT first_at_ms FROM schedule_calc)
|
|
409
|
+
+ CEIL(
|
|
410
|
+
(SELECT granted FROM grant_calc)::numeric
|
|
411
|
+
/ (SELECT burst_size FROM schedule_calc)
|
|
412
|
+
)::bigint
|
|
413
|
+
* (SELECT spacing_ms FROM schedule_calc)
|
|
414
|
+
* (SELECT burst_size FROM schedule_calc)
|
|
415
|
+
ELSE b.next_dispatch_at_ms
|
|
416
|
+
END,
|
|
417
|
+
-- AIMD ramp, folded in. Only after confirmed provider success,
|
|
418
|
+
-- under real demand, not cooling, and at least 1s since the last
|
|
419
|
+
-- ramp. Slow-start doubles until the first 429, then adds
|
|
420
|
+
-- max(1, base/10) per step. Never exceeds max_rps.
|
|
421
|
+
refill_per_sec = CASE
|
|
422
|
+
WHEN $5::int > 0
|
|
423
|
+
AND r.now_ms - r.last_ramp_at_ms >= 1000
|
|
424
|
+
AND r.cool_until_ms <= r.now_ms
|
|
425
|
+
AND r.tokens_after_refill < r.capacity / 2
|
|
426
|
+
THEN LEAST(
|
|
427
|
+
r.max_rps,
|
|
428
|
+
CASE
|
|
429
|
+
WHEN r.seen_backpressure
|
|
430
|
+
THEN r.refill_per_sec + GREATEST(1, r.base_refill_per_sec / 10)
|
|
431
|
+
ELSE r.refill_per_sec * 2
|
|
432
|
+
END
|
|
433
|
+
)
|
|
434
|
+
ELSE r.refill_per_sec
|
|
435
|
+
END,
|
|
436
|
+
last_ramp_at_ms = CASE
|
|
437
|
+
WHEN $5::int > 0
|
|
438
|
+
AND r.now_ms - r.last_ramp_at_ms >= 1000
|
|
439
|
+
AND r.cool_until_ms <= r.now_ms
|
|
440
|
+
AND r.tokens_after_refill < r.capacity / 2
|
|
441
|
+
THEN r.now_ms
|
|
442
|
+
ELSE r.last_ramp_at_ms
|
|
443
|
+
END
|
|
444
|
+
FROM refreshed r
|
|
445
|
+
WHERE b.bucket_id = r.bucket_id
|
|
446
|
+
AND b.rule_id = r.rule_id
|
|
447
|
+
RETURNING
|
|
448
|
+
b.rule_id,
|
|
449
|
+
r.tokens_after_refill,
|
|
450
|
+
b.refill_per_sec AS new_refill_per_sec,
|
|
451
|
+
r.now_ms AS now_ms
|
|
452
|
+
)
|
|
453
|
+
SELECT
|
|
454
|
+
(SELECT granted FROM grant_calc) AS granted,
|
|
455
|
+
(SELECT now_ms FROM refreshed LIMIT 1) AS server_now_ms,
|
|
456
|
+
(SELECT cool_until_ms FROM cooling) AS cool_until_ms,
|
|
457
|
+
(SELECT COALESCE(MAX(claimed_retry_at_ms), 0) FROM ${bucketTableName(input)}
|
|
458
|
+
WHERE bucket_id = $1 AND rule_id = ANY($2::text[])) AS claimed_retry_at_ms,
|
|
459
|
+
(SELECT COALESCE(MIN(new_refill_per_sec), 0) FROM updated) AS effective_rps,
|
|
460
|
+
CASE
|
|
461
|
+
WHEN (SELECT granted FROM grant_calc) > 0 THEN ARRAY(
|
|
462
|
+
SELECT
|
|
463
|
+
(SELECT first_at_ms FROM schedule_calc)
|
|
464
|
+
+ FLOOR(
|
|
465
|
+
(permit_index - 1)::numeric
|
|
466
|
+
/ (SELECT burst_size FROM schedule_calc)
|
|
467
|
+
)::bigint
|
|
468
|
+
* (SELECT spacing_ms FROM schedule_calc)
|
|
469
|
+
* (SELECT burst_size FROM schedule_calc)
|
|
470
|
+
FROM generate_series(1, (SELECT granted FROM grant_calc)) AS permit_index
|
|
471
|
+
)
|
|
472
|
+
ELSE '{}'::bigint[]
|
|
473
|
+
END AS scheduled_at_ms,
|
|
474
|
+
-- Wait until either the cooldown clears or one token accrues on the
|
|
475
|
+
-- most-constrained rule (whichever a granted=0 caller must sleep for).
|
|
476
|
+
CASE
|
|
477
|
+
WHEN (SELECT granted FROM grant_calc) > 0 THEN 0
|
|
478
|
+
WHEN (SELECT cool_until_ms FROM cooling) > (SELECT now_ms FROM refreshed LIMIT 1)
|
|
479
|
+
THEN (SELECT cool_until_ms FROM cooling) - (SELECT now_ms FROM refreshed LIMIT 1)
|
|
480
|
+
ELSE (
|
|
481
|
+
SELECT CEIL(
|
|
482
|
+
MAX((1.0 - tokens_after_refill) / GREATEST(new_refill_per_sec, 0.0001) * 1000)
|
|
483
|
+
)::bigint
|
|
484
|
+
FROM updated
|
|
485
|
+
)
|
|
486
|
+
END AS wait_ms
|
|
487
|
+
`,
|
|
488
|
+
[
|
|
489
|
+
input.bucketId,
|
|
490
|
+
ruleIds,
|
|
491
|
+
input.nowMs ?? null,
|
|
492
|
+
requested,
|
|
493
|
+
Math.max(0, Math.trunc(input.observedSuccesses ?? 0)),
|
|
494
|
+
],
|
|
495
|
+
);
|
|
496
|
+
const row = result.rows[0];
|
|
497
|
+
if (!row) {
|
|
498
|
+
// Seeding raced away or rows vanished; treat as a short retryable wait.
|
|
499
|
+
return { granted: 0, waitMs: MIN_CONCURRENCY_WAIT_MS, leaseIds: [] };
|
|
500
|
+
}
|
|
501
|
+
const granted = Math.max(0, Math.trunc(Number(row.granted)));
|
|
502
|
+
const serverNowMs = Math.max(0, Number(row.server_now_ms));
|
|
503
|
+
const coolUntilMs = Math.max(0, Number(row.cool_until_ms));
|
|
504
|
+
const claimedRetryAtMs = Math.max(0, Number(row.claimed_retry_at_ms));
|
|
505
|
+
const scheduledAtMs = Array.isArray(row.scheduled_at_ms)
|
|
506
|
+
? row.scheduled_at_ms.map(Number)
|
|
507
|
+
: [];
|
|
508
|
+
const effectiveRequestsPerSecond = Math.max(0, Number(row.effective_rps));
|
|
509
|
+
const waitMs = granted > 0 ? 0 : Math.max(1, Math.trunc(Number(row.wait_ms)));
|
|
510
|
+
return {
|
|
511
|
+
granted,
|
|
512
|
+
waitMs,
|
|
513
|
+
serverNowMs,
|
|
514
|
+
leaseIds: [],
|
|
515
|
+
scheduledAtMs,
|
|
516
|
+
effectiveRequestsPerSecond,
|
|
517
|
+
coolUntilMs,
|
|
518
|
+
claimedRetryAtMs,
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Mixed / concurrency-bearing acquire. The token bucket still governs the
|
|
524
|
+
* request rate; the durable lease table governs the simultaneous-in-flight cap.
|
|
525
|
+
* Expired-lease reclaim + active count + rate refill + grant + token deduction +
|
|
526
|
+
* lease minting all execute in ONE multi-CTE statement (one implicit
|
|
527
|
+
* transaction), so a crash can never deduct tokens without minting the matching
|
|
528
|
+
* lease rows. Lease ids are generated in SQL (gen_random_uuid) and returned to
|
|
529
|
+
* the client, one per granted permit, releasable independently.
|
|
530
|
+
*/
|
|
531
|
+
async function acquireWithConcurrency(
|
|
532
|
+
client: PostgresSchedulerQueryClient,
|
|
533
|
+
input: PlayRunnerRateStateAcquireInput & { nowMs?: number },
|
|
534
|
+
rules: AcquirePacingRule[],
|
|
535
|
+
requested: number,
|
|
536
|
+
): Promise<PlayRunnerRateStateAcquireResult> {
|
|
537
|
+
const ruleIds = rules.map((rule) => rule.ruleId);
|
|
538
|
+
const concurrencyRules = rules.filter((rule) => rule.maxConcurrency != null);
|
|
539
|
+
const concurrencyRuleIds = concurrencyRules.map((rule) => rule.ruleId);
|
|
540
|
+
// Per-rule limits, NOT collapsed to a single min. When a bucket carries more
|
|
541
|
+
// than one concurrency rule, each rule mints its own lease row per permit, so
|
|
542
|
+
// SUM(permits) over all rules double-counts a single in-flight permit. The
|
|
543
|
+
// grant must instead be bounded by the MIN free slots computed PER RULE
|
|
544
|
+
// (limit_r - active_r), matching the per-rule enforcement on origin/main.
|
|
545
|
+
const concurrencyLimits = concurrencyRules.map((rule) => ({
|
|
546
|
+
rule_id: rule.ruleId,
|
|
547
|
+
max_concurrency: rule.maxConcurrency as number,
|
|
548
|
+
}));
|
|
549
|
+
|
|
550
|
+
// One multi-CTE statement: reclaim expired leases, refill the rate rows,
|
|
551
|
+
// count live leases, compute the grant bounded by rate AND free concurrency
|
|
552
|
+
// slots, deduct the tokens, AND mint the lease rows — atomically. Grant +
|
|
553
|
+
// deduction + lease minting share one implicit transaction, so a crash can
|
|
554
|
+
// never deduct tokens without the matching lease rows (which would otherwise
|
|
555
|
+
// over-admit the provider until the phantom leases "expired"). PgBouncer-safe:
|
|
556
|
+
// autocommit, deterministic row locking, no lock_timeout, no session state.
|
|
557
|
+
const result = await client.query<{
|
|
558
|
+
granted: string | number;
|
|
559
|
+
wait_ms: string | number;
|
|
560
|
+
server_now_ms: string | number;
|
|
561
|
+
cool_until_ms: string | number;
|
|
562
|
+
claimed_retry_at_ms: string | number;
|
|
563
|
+
lease_ids: string[] | null;
|
|
564
|
+
scheduled_at_ms: Array<string | number> | null;
|
|
565
|
+
effective_rps: string | number;
|
|
566
|
+
}>(
|
|
567
|
+
`
|
|
568
|
+
WITH now_value AS (
|
|
569
|
+
SELECT COALESCE(
|
|
570
|
+
$3::bigint,
|
|
571
|
+
FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint
|
|
572
|
+
) AS now_ms
|
|
573
|
+
),
|
|
574
|
+
reclaim AS (
|
|
575
|
+
DELETE FROM ${concurrencyLeaseTableName(input)}
|
|
576
|
+
WHERE bucket_id = $1
|
|
577
|
+
AND rule_id = ANY($5::text[])
|
|
578
|
+
AND expires_at_ms <= (SELECT now_ms FROM now_value)
|
|
579
|
+
RETURNING 1
|
|
580
|
+
),
|
|
581
|
+
limits AS (
|
|
582
|
+
-- One row per concurrency rule with its declared cap.
|
|
583
|
+
SELECT rule_id, max_concurrency
|
|
584
|
+
FROM jsonb_to_recordset($9::jsonb) AS lim(
|
|
585
|
+
rule_id text,
|
|
586
|
+
max_concurrency int
|
|
587
|
+
)
|
|
588
|
+
),
|
|
589
|
+
active_by_rule AS (
|
|
590
|
+
-- Live in-flight permits PER RULE. A CTE SELECT sees the pre-statement
|
|
591
|
+
-- snapshot, so expired rows the reclaim DELETE removes are still visible
|
|
592
|
+
-- here; filter them out explicitly by expiry so the free-slot count
|
|
593
|
+
-- matches the post-reclaim table. Grouping by rule_id (not SUM across
|
|
594
|
+
-- all rules) is what keeps a single permit — which mints one lease row
|
|
595
|
+
-- per concurrency rule — from being counted once per rule.
|
|
596
|
+
SELECT l.rule_id,
|
|
597
|
+
COALESCE((
|
|
598
|
+
SELECT SUM(cl.permits)
|
|
599
|
+
FROM ${concurrencyLeaseTableName(input)} cl
|
|
600
|
+
WHERE cl.bucket_id = $1
|
|
601
|
+
AND cl.rule_id = l.rule_id
|
|
602
|
+
AND cl.expires_at_ms > (SELECT now_ms FROM now_value)
|
|
603
|
+
), 0)::int AS active_count
|
|
604
|
+
FROM limits l
|
|
605
|
+
),
|
|
606
|
+
free_slots AS (
|
|
607
|
+
-- Most-constrained free concurrency across all rules of the bucket.
|
|
608
|
+
SELECT COALESCE(
|
|
609
|
+
MIN(l.max_concurrency - a.active_count),
|
|
610
|
+
$6::int
|
|
611
|
+
)::int AS free
|
|
612
|
+
FROM limits l
|
|
613
|
+
JOIN active_by_rule a ON a.rule_id = l.rule_id
|
|
614
|
+
),
|
|
615
|
+
refreshed AS (
|
|
616
|
+
SELECT
|
|
617
|
+
b.rule_id,
|
|
618
|
+
b.capacity,
|
|
619
|
+
b.max_rps,
|
|
620
|
+
b.seen_backpressure,
|
|
621
|
+
b.base_refill_per_sec,
|
|
622
|
+
b.cool_until_ms,
|
|
623
|
+
b.last_ramp_at_ms,
|
|
624
|
+
b.next_dispatch_at_ms,
|
|
625
|
+
nv.now_ms AS now_ms,
|
|
626
|
+
LEAST(
|
|
627
|
+
b.capacity,
|
|
628
|
+
b.tokens + GREATEST(0, nv.now_ms - b.refilled_at_ms) / 1000.0 * b.refill_per_sec
|
|
629
|
+
) AS tokens_after_refill,
|
|
630
|
+
b.refill_per_sec
|
|
631
|
+
FROM ${bucketTableName(input)} b
|
|
632
|
+
CROSS JOIN now_value nv
|
|
633
|
+
WHERE b.bucket_id = $1
|
|
634
|
+
AND b.rule_id = ANY($2::text[])
|
|
635
|
+
ORDER BY b.rule_id
|
|
636
|
+
FOR UPDATE OF b
|
|
637
|
+
),
|
|
638
|
+
cooling AS (
|
|
639
|
+
SELECT COALESCE(MAX(cool_until_ms), 0) AS cool_until_ms FROM refreshed
|
|
640
|
+
),
|
|
641
|
+
grant_calc AS (
|
|
642
|
+
SELECT
|
|
643
|
+
CASE
|
|
644
|
+
WHEN (SELECT cool_until_ms FROM cooling) > (SELECT now_ms FROM refreshed LIMIT 1)
|
|
645
|
+
THEN 0
|
|
646
|
+
ELSE GREATEST(
|
|
647
|
+
0,
|
|
648
|
+
LEAST(
|
|
649
|
+
$4::int,
|
|
650
|
+
FLOOR(MIN(tokens_after_refill))::int,
|
|
651
|
+
(SELECT free FROM free_slots)
|
|
652
|
+
)
|
|
653
|
+
)
|
|
654
|
+
END AS granted
|
|
655
|
+
FROM refreshed
|
|
656
|
+
),
|
|
657
|
+
schedule_calc AS (
|
|
658
|
+
SELECT
|
|
659
|
+
GREATEST(
|
|
660
|
+
MAX(now_ms),
|
|
661
|
+
MAX(cool_until_ms),
|
|
662
|
+
MAX(next_dispatch_at_ms)
|
|
663
|
+
)::bigint AS first_at_ms,
|
|
664
|
+
CEIL(MAX(
|
|
665
|
+
1000.0 / GREATEST(
|
|
666
|
+
refill_per_sec * CASE
|
|
667
|
+
WHEN refill_per_sec < ${LOW_RATE_DISPATCH_HEADROOM_THRESHOLD_RPS}
|
|
668
|
+
THEN ${LOW_RATE_DISPATCH_HEADROOM_FACTOR}
|
|
669
|
+
ELSE 1.0
|
|
670
|
+
END,
|
|
671
|
+
0.0001
|
|
672
|
+
)
|
|
673
|
+
))::bigint AS spacing_ms,
|
|
674
|
+
GREATEST(1, FLOOR(MIN(refill_per_sec) / 100.0))::bigint AS burst_size
|
|
675
|
+
FROM refreshed
|
|
676
|
+
),
|
|
677
|
+
updated AS (
|
|
678
|
+
UPDATE ${bucketTableName(input)} b
|
|
679
|
+
SET
|
|
680
|
+
tokens = r.tokens_after_refill - (SELECT granted FROM grant_calc),
|
|
681
|
+
refilled_at_ms = r.now_ms,
|
|
682
|
+
next_dispatch_at_ms = CASE
|
|
683
|
+
WHEN (SELECT granted FROM grant_calc) > 0
|
|
684
|
+
THEN (SELECT first_at_ms FROM schedule_calc)
|
|
685
|
+
+ CEIL(
|
|
686
|
+
(SELECT granted FROM grant_calc)::numeric
|
|
687
|
+
/ (SELECT burst_size FROM schedule_calc)
|
|
688
|
+
)::bigint
|
|
689
|
+
* (SELECT spacing_ms FROM schedule_calc)
|
|
690
|
+
* (SELECT burst_size FROM schedule_calc)
|
|
691
|
+
ELSE b.next_dispatch_at_ms
|
|
692
|
+
END,
|
|
693
|
+
-- Match the pure-rate path: concurrency leases change admission
|
|
694
|
+
-- accounting, not the AIMD learning contract.
|
|
695
|
+
refill_per_sec = CASE
|
|
696
|
+
WHEN $10::int > 0
|
|
697
|
+
AND r.now_ms - r.last_ramp_at_ms >= 1000
|
|
698
|
+
AND r.cool_until_ms <= r.now_ms
|
|
699
|
+
AND r.tokens_after_refill < r.capacity / 2
|
|
700
|
+
THEN LEAST(
|
|
701
|
+
r.max_rps,
|
|
702
|
+
CASE
|
|
703
|
+
WHEN r.seen_backpressure
|
|
704
|
+
THEN r.refill_per_sec + GREATEST(1, r.base_refill_per_sec / 10)
|
|
705
|
+
ELSE r.refill_per_sec * 2
|
|
706
|
+
END
|
|
707
|
+
)
|
|
708
|
+
ELSE r.refill_per_sec
|
|
709
|
+
END,
|
|
710
|
+
last_ramp_at_ms = CASE
|
|
711
|
+
WHEN $10::int > 0
|
|
712
|
+
AND r.now_ms - r.last_ramp_at_ms >= 1000
|
|
713
|
+
AND r.cool_until_ms <= r.now_ms
|
|
714
|
+
AND r.tokens_after_refill < r.capacity / 2
|
|
715
|
+
THEN r.now_ms
|
|
716
|
+
ELSE r.last_ramp_at_ms
|
|
717
|
+
END
|
|
718
|
+
FROM refreshed r
|
|
719
|
+
WHERE b.bucket_id = $1
|
|
720
|
+
AND b.rule_id = r.rule_id
|
|
721
|
+
RETURNING
|
|
722
|
+
r.tokens_after_refill,
|
|
723
|
+
b.refill_per_sec AS new_refill_per_sec
|
|
724
|
+
),
|
|
725
|
+
lease_tokens AS (
|
|
726
|
+
-- One uuid per granted permit. A WITH query is evaluated once (volatile
|
|
727
|
+
-- functions included), so each lease_id below stays stable across the
|
|
728
|
+
-- per-rule cross join in minted — every concurrency rule row shares the
|
|
729
|
+
-- same lease_id for a given permit, matching release semantics.
|
|
730
|
+
SELECT gen_random_uuid()::text AS lease_id
|
|
731
|
+
FROM generate_series(1, (SELECT granted FROM grant_calc))
|
|
732
|
+
),
|
|
733
|
+
minted AS (
|
|
734
|
+
-- Mint the lease rows from grant_calc (NOT by re-reading the lease
|
|
735
|
+
-- table: all CTEs share the pre-statement snapshot). Same-statement =
|
|
736
|
+
-- same transaction as the token deduction above.
|
|
737
|
+
INSERT INTO ${concurrencyLeaseTableName(input)} (
|
|
738
|
+
bucket_id,
|
|
739
|
+
rule_id,
|
|
740
|
+
lease_id,
|
|
741
|
+
permits,
|
|
742
|
+
expires_at_ms
|
|
743
|
+
)
|
|
744
|
+
SELECT
|
|
745
|
+
$1,
|
|
746
|
+
rule_id,
|
|
747
|
+
lt.lease_id,
|
|
748
|
+
1,
|
|
749
|
+
(SELECT now_ms FROM now_value) + $8::bigint
|
|
750
|
+
FROM unnest($5::text[]) AS rule_id
|
|
751
|
+
CROSS JOIN lease_tokens lt
|
|
752
|
+
RETURNING lease_id
|
|
753
|
+
)
|
|
754
|
+
SELECT
|
|
755
|
+
(SELECT granted FROM grant_calc) AS granted,
|
|
756
|
+
(SELECT now_ms FROM refreshed LIMIT 1) AS server_now_ms,
|
|
757
|
+
(SELECT cool_until_ms FROM cooling) AS cool_until_ms,
|
|
758
|
+
(SELECT COALESCE(MAX(claimed_retry_at_ms), 0) FROM ${bucketTableName(input)}
|
|
759
|
+
WHERE bucket_id = $1 AND rule_id = ANY($2::text[])) AS claimed_retry_at_ms,
|
|
760
|
+
(SELECT COALESCE(MIN(new_refill_per_sec), 0) FROM updated) AS effective_rps,
|
|
761
|
+
CASE
|
|
762
|
+
WHEN (SELECT granted FROM grant_calc) > 0 THEN ARRAY(
|
|
763
|
+
SELECT
|
|
764
|
+
(SELECT first_at_ms FROM schedule_calc)
|
|
765
|
+
+ FLOOR(
|
|
766
|
+
(permit_index - 1)::numeric
|
|
767
|
+
/ (SELECT burst_size FROM schedule_calc)
|
|
768
|
+
)::bigint
|
|
769
|
+
* (SELECT spacing_ms FROM schedule_calc)
|
|
770
|
+
* (SELECT burst_size FROM schedule_calc)
|
|
771
|
+
FROM generate_series(1, (SELECT granted FROM grant_calc)) AS permit_index
|
|
772
|
+
)
|
|
773
|
+
ELSE '{}'::bigint[]
|
|
774
|
+
END AS scheduled_at_ms,
|
|
775
|
+
(SELECT COALESCE(ARRAY_AGG(DISTINCT lease_id), '{}') FROM minted) AS lease_ids,
|
|
776
|
+
CASE
|
|
777
|
+
WHEN (SELECT granted FROM grant_calc) > 0 THEN 0
|
|
778
|
+
WHEN (SELECT cool_until_ms FROM cooling) > (SELECT now_ms FROM refreshed LIMIT 1)
|
|
779
|
+
THEN (SELECT cool_until_ms FROM cooling) - (SELECT now_ms FROM refreshed LIMIT 1)
|
|
780
|
+
WHEN (SELECT free FROM free_slots) <= 0 THEN $7::bigint
|
|
781
|
+
ELSE (
|
|
782
|
+
SELECT CEIL(
|
|
783
|
+
MAX((1.0 - tokens_after_refill) / GREATEST(new_refill_per_sec, 0.0001) * 1000)
|
|
784
|
+
)::bigint
|
|
785
|
+
FROM updated
|
|
786
|
+
)
|
|
787
|
+
END AS wait_ms
|
|
788
|
+
`,
|
|
789
|
+
[
|
|
790
|
+
input.bucketId,
|
|
791
|
+
ruleIds,
|
|
792
|
+
input.nowMs ?? null,
|
|
793
|
+
requested,
|
|
794
|
+
concurrencyRuleIds,
|
|
795
|
+
// $6 is only the free_slots COALESCE fallback (no concurrency rows). Use
|
|
796
|
+
// the tightest declared cap so it can never widen the grant.
|
|
797
|
+
Math.min(...concurrencyLimits.map((limit) => limit.max_concurrency)),
|
|
798
|
+
MIN_CONCURRENCY_WAIT_MS,
|
|
799
|
+
ACTIVE_LEASE_TTL_MS,
|
|
800
|
+
stringifyPostgresJson(concurrencyLimits),
|
|
801
|
+
Math.max(0, Math.trunc(input.observedSuccesses ?? 0)),
|
|
802
|
+
],
|
|
803
|
+
);
|
|
804
|
+
const row = result.rows[0];
|
|
805
|
+
if (!row) {
|
|
806
|
+
return { granted: 0, waitMs: MIN_CONCURRENCY_WAIT_MS, leaseIds: [] };
|
|
807
|
+
}
|
|
808
|
+
const granted = Math.max(0, Math.trunc(Number(row.granted)));
|
|
809
|
+
const serverNowMs = Math.max(0, Number(row.server_now_ms));
|
|
810
|
+
const coolUntilMs = Math.max(0, Number(row.cool_until_ms));
|
|
811
|
+
const claimedRetryAtMs = Math.max(0, Number(row.claimed_retry_at_ms));
|
|
812
|
+
const scheduledAtMs = Array.isArray(row.scheduled_at_ms)
|
|
813
|
+
? row.scheduled_at_ms.map(Number)
|
|
814
|
+
: [];
|
|
815
|
+
const effectiveRequestsPerSecond = Math.max(0, Number(row.effective_rps));
|
|
816
|
+
const waitMs = granted > 0 ? 0 : Math.max(1, Math.trunc(Number(row.wait_ms)));
|
|
817
|
+
const leaseIds = Array.isArray(row.lease_ids)
|
|
818
|
+
? row.lease_ids.filter(
|
|
819
|
+
(leaseId): leaseId is string =>
|
|
820
|
+
typeof leaseId === 'string' && leaseId.length > 0,
|
|
821
|
+
)
|
|
822
|
+
: [];
|
|
823
|
+
if (granted > 0 && leaseIds.length !== granted) {
|
|
824
|
+
throw new Error(
|
|
825
|
+
`Rate-state lease minting drift for ${input.bucketId}: granted ${granted} with ${leaseIds.length} lease tokens.`,
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
return {
|
|
829
|
+
granted,
|
|
830
|
+
waitMs,
|
|
831
|
+
serverNowMs,
|
|
832
|
+
leaseIds: granted > 0 ? leaseIds : [],
|
|
833
|
+
scheduledAtMs,
|
|
834
|
+
effectiveRequestsPerSecond,
|
|
835
|
+
coolUntilMs,
|
|
836
|
+
claimedRetryAtMs,
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
export async function acquirePostgresRateState(
|
|
841
|
+
client: PostgresSchedulerQueryClient,
|
|
842
|
+
input: PlayRunnerRateStateAcquireInput & { nowMs?: number },
|
|
843
|
+
): Promise<PlayRunnerRateStateAcquireResult> {
|
|
844
|
+
const rules = normalizeRules(input.rules as AcquirePacingRule[]);
|
|
845
|
+
if (rules.length === 0) return { granted: 0, waitMs: 0, leaseIds: [] };
|
|
846
|
+
const requested = positiveInt(input.requested, 'requested permits');
|
|
847
|
+
const nowMs =
|
|
848
|
+
input.nowMs ??
|
|
849
|
+
Number(
|
|
850
|
+
(
|
|
851
|
+
await client.query<{ now_ms: string | number }>(
|
|
852
|
+
`SELECT FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint AS now_ms`,
|
|
853
|
+
)
|
|
854
|
+
).rows[0]?.now_ms ?? Date.now(),
|
|
855
|
+
);
|
|
856
|
+
// Cold path: seed missing rows (ON CONFLICT DO NOTHING, no-op when warm).
|
|
857
|
+
await seedBucketRows(client, input, rules, nowMs);
|
|
858
|
+
const hasConcurrency = rules.some((rule) => rule.maxConcurrency != null);
|
|
859
|
+
if (!hasConcurrency) {
|
|
860
|
+
return acquirePureRate(client, { ...input, nowMs }, rules, requested);
|
|
861
|
+
}
|
|
862
|
+
return acquireWithConcurrency(client, { ...input, nowMs }, rules, requested);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
export async function releasePostgresRateState(
|
|
866
|
+
client: PostgresSchedulerQueryClient,
|
|
867
|
+
input: PlayRunnerRateStateReleaseInput & { nowMs?: number },
|
|
868
|
+
): Promise<void> {
|
|
869
|
+
const rules = normalizeRules(input.rules as AcquirePacingRule[]).filter(
|
|
870
|
+
(rule) => rule.maxConcurrency != null,
|
|
871
|
+
);
|
|
872
|
+
if (rules.length === 0) return;
|
|
873
|
+
const leaseIds = [
|
|
874
|
+
...new Set(
|
|
875
|
+
input.leaseIds
|
|
876
|
+
.filter((leaseId): leaseId is string => typeof leaseId === 'string')
|
|
877
|
+
.map((leaseId) => leaseId.trim())
|
|
878
|
+
.filter(Boolean),
|
|
879
|
+
),
|
|
880
|
+
];
|
|
881
|
+
if (leaseIds.length === 0) return;
|
|
882
|
+
// DELETE by holder token is naturally idempotent, including HTTP retries.
|
|
883
|
+
await client.query(
|
|
884
|
+
`
|
|
885
|
+
DELETE FROM ${concurrencyLeaseTableName(input)}
|
|
886
|
+
WHERE bucket_id = $1
|
|
887
|
+
AND rule_id = ANY($2::text[])
|
|
888
|
+
AND lease_id = ANY($3::text[])
|
|
889
|
+
`,
|
|
890
|
+
[input.bucketId, rules.map((rule) => rule.ruleId), leaseIds],
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
export async function penalizePostgresRateState(
|
|
895
|
+
client: PostgresSchedulerQueryClient,
|
|
896
|
+
input: PlayRunnerRateStatePenalizeInput & { nowMs?: number },
|
|
897
|
+
): Promise<void> {
|
|
898
|
+
if (!Number.isFinite(input.cooldownMs) || input.cooldownMs <= 0) return;
|
|
899
|
+
// Single autocommit statement applied to every rule row of the bucket:
|
|
900
|
+
// - claimed_retry_at_ms: server Retry-After stored verbatim (observability).
|
|
901
|
+
// - cool_until_ms: extended by the cooldown, internally capped at 45min.
|
|
902
|
+
// - refill_per_sec: multiplicative decrease, but ONLY once per active
|
|
903
|
+
// cooldown window (halve-once guard: skip if already cooling), floored at
|
|
904
|
+
// min_rps.
|
|
905
|
+
// - seen_backpressure flips the ramp from slow-start (x2) to additive.
|
|
906
|
+
// - tokens drained: no more grants until refill re-accrues past the cooldown.
|
|
907
|
+
await client.query(
|
|
908
|
+
`
|
|
909
|
+
WITH now_value AS (
|
|
910
|
+
SELECT COALESCE(
|
|
911
|
+
$3::bigint,
|
|
912
|
+
FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint
|
|
913
|
+
) AS now_ms
|
|
914
|
+
)
|
|
915
|
+
UPDATE ${bucketTableName(input)} b
|
|
916
|
+
SET
|
|
917
|
+
claimed_retry_at_ms = (SELECT now_ms FROM now_value) + $2::bigint,
|
|
918
|
+
refill_per_sec = CASE
|
|
919
|
+
WHEN (SELECT now_ms FROM now_value) >= b.cool_until_ms
|
|
920
|
+
THEN GREATEST(b.min_rps, b.refill_per_sec / 2)
|
|
921
|
+
ELSE b.refill_per_sec
|
|
922
|
+
END,
|
|
923
|
+
cool_until_ms = GREATEST(
|
|
924
|
+
b.cool_until_ms,
|
|
925
|
+
(SELECT now_ms FROM now_value) + LEAST($2::bigint, $4::bigint)
|
|
926
|
+
),
|
|
927
|
+
seen_backpressure = true,
|
|
928
|
+
tokens = 0,
|
|
929
|
+
next_dispatch_at_ms = GREATEST(
|
|
930
|
+
b.next_dispatch_at_ms,
|
|
931
|
+
(SELECT now_ms FROM now_value) + LEAST($2::bigint, $4::bigint)
|
|
932
|
+
)
|
|
933
|
+
WHERE b.bucket_id = $1
|
|
934
|
+
`,
|
|
935
|
+
[
|
|
936
|
+
input.bucketId,
|
|
937
|
+
Math.ceil(input.cooldownMs),
|
|
938
|
+
input.nowMs ?? null,
|
|
939
|
+
MAX_INTERNAL_COOLDOWN_MS,
|
|
940
|
+
],
|
|
941
|
+
);
|
|
942
|
+
}
|