deepline 0.3.323 → 0.3.325
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/sdk/src/types.ts +6 -0
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +454 -129
- package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +407 -37
- package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +18 -4
- package/dist/bundling-sources/shared_libs/play-runtime/governor/rate-state-backend.ts +16 -1
- package/dist/bundling-sources/shared_libs/play-runtime/map-execution-frame.ts +6 -2
- package/dist/bundling-sources/shared_libs/play-runtime/play-http-request.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/gradient2-concurrency-controller.ts +15 -5
- package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/provider-capacity-policy.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/provider-concurrency-policy.ts +7 -2
- package/dist/bundling-sources/shared_libs/play-runtime/provider-row-retry-queue.ts +65 -0
- package/dist/bundling-sources/shared_libs/play-runtime/resource-governor.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +7 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-actions.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +5 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-flow.ts +34 -5
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-map-row-feed.ts +116 -15
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-scope.ts +59 -6
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts +294 -37
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +73 -0
- package/dist/bundling-sources/shared_libs/play-runtime/suspension.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +41 -3
- package/dist/bundling-sources/shared_libs/plays/artifact-types.ts +9 -0
- package/dist/cli/{auth-4TYSEEPE.mjs → auth-PIZESRPC.mjs} +4 -4
- package/dist/cli/auth-RXCD4GOV.js +31 -0
- package/dist/cli/{billing-HYX3OWXB.js → billing-DQKFCQV5.js} +70 -70
- package/dist/cli/{billing-QDER3JLM.mjs → billing-HSL6VTCM.mjs} +4 -4
- package/dist/cli/{chunk-VN433ENM.mjs → chunk-5FHU7TV4.mjs} +2 -2
- package/dist/cli/{chunk-ZF6WOSMW.mjs → chunk-5TYU3UIS.mjs} +1 -1
- package/dist/cli/{chunk-YY6EASRE.mjs → chunk-6RMKHN7S.mjs} +4 -4
- package/dist/cli/{chunk-CXJND6CJ.mjs → chunk-6VZDSBK6.mjs} +2 -2
- package/dist/cli/{chunk-JGTV5QNG.mjs → chunk-7MD453WV.mjs} +6 -6
- package/dist/cli/{chunk-3OTHDXTV.mjs → chunk-AQUNQJ5Y.mjs} +1 -1
- package/dist/cli/{chunk-FNKBVKQX.js → chunk-CV5GDCKN.js} +15 -15
- package/dist/cli/{chunk-WOG5IKUF.js → chunk-DLW7X2UK.js} +220 -220
- package/dist/cli/{chunk-BC3E7GEW.mjs → chunk-H5TJBRSC.mjs} +5 -5
- package/dist/cli/{chunk-LXUY7LRE.js → chunk-HPTQEWCF.js} +60 -60
- package/dist/cli/{chunk-GUPKB4UE.js → chunk-HUKWYHBG.js} +2 -2
- package/dist/cli/{chunk-BEG62AQW.js → chunk-K7FC5IUU.js} +1 -1
- package/dist/cli/{chunk-I7TAO4XH.js → chunk-KZNFYOHX.js} +6 -6
- package/dist/cli/{chunk-UMUH2FVZ.js → chunk-MNKDSR2J.js} +6 -6
- package/dist/cli/{chunk-UE55ECMI.mjs → chunk-NWBARG3F.mjs} +1 -1
- package/dist/cli/{chunk-KLIHE4X5.js → chunk-OOPSX57I.js} +6 -6
- package/dist/cli/{chunk-3I4KDUKY.mjs → chunk-OPERWJR6.mjs} +3 -3
- package/dist/cli/{chunk-HUQVA5MR.js → chunk-PBNZXZCW.js} +48 -48
- package/dist/cli/{chunk-HSZD4VBE.js → chunk-PVDQ55PS.js} +4 -4
- package/dist/cli/{chunk-HJN5KR42.mjs → chunk-PWV2M4LW.mjs} +1 -1
- package/dist/cli/{chunk-M2WAAIM4.js → chunk-QQ2FG5W3.js} +6 -6
- package/dist/cli/{chunk-OQPG3ND3.js → chunk-UYFWTIKF.js} +26 -26
- package/dist/cli/{chunk-2HYJDTQT.mjs → chunk-WW4XZIVM.mjs} +2 -2
- package/dist/cli/{chunk-FGD2M25K.mjs → chunk-XIVUB36F.mjs} +1 -1
- package/dist/cli/{chunk-W2V624T6.mjs → chunk-YE3O2UHM.mjs} +1 -1
- package/dist/cli/{chunk-EWFKMQFO.js → chunk-YXRPWMGN.js} +50 -50
- package/dist/cli/client-562MULXK.js +20 -0
- package/dist/cli/{client-CEX7EHUM.mjs → client-57SR4KVM.mjs} +3 -3
- package/dist/cli/{csv-GBPRJ6YU.js → csv-5DJSC6BD.js} +12 -12
- package/dist/cli/{csv-LR5KWOIK.mjs → csv-G2FRMOXL.mjs} +3 -3
- package/dist/cli/{db-VFKXAHQA.js → db-NVO7P4VO.js} +20 -20
- package/dist/cli/{db-4BMCUU34.mjs → db-WWZEXM3N.mjs} +4 -4
- package/dist/cli/{describe-VTLU5KX5.mjs → describe-26OMF3A4.mjs} +8 -8
- package/dist/cli/{describe-BYWGWALB.js → describe-M26LHDBQ.js} +15 -15
- package/dist/cli/{executions-NGREVSY6.mjs → executions-2LLHY3IL.mjs} +4 -4
- package/dist/cli/{executions-DU7I6YEU.js → executions-KI47LPKE.js} +6 -6
- package/dist/cli/{feedback-PHPAWVHC.js → feedback-2Y26C2BZ.js} +6 -6
- package/dist/cli/{feedback-GUY2CCGF.mjs → feedback-O26VL2OF.mjs} +3 -3
- package/dist/cli/git-KGPX5YPD.js +23 -0
- package/dist/cli/{git-22YV3DFJ.mjs → git-UVPGZN3K.mjs} +4 -4
- package/dist/cli/index.js +75 -75
- package/dist/cli/index.mjs +31 -31
- package/dist/cli/{legacy-noops-V72GCF5R.js → legacy-noops-H6Y3SEVB.js} +7 -7
- package/dist/cli/{legacy-noops-YEQDVI6U.mjs → legacy-noops-IK5CH72U.mjs} +4 -4
- package/dist/cli/{monitors-MVJBQDQG.js → monitors-JL6TTQCI.js} +85 -85
- package/dist/cli/{monitors-QOCAMBRV.mjs → monitors-SFVVMAJT.mjs} +4 -4
- package/dist/cli/{org-DMIOMJK6.js → org-OVMAZCQS.js} +60 -60
- package/dist/cli/{org-A53XV2UQ.mjs → org-SSDONL57.mjs} +4 -4
- package/dist/cli/play-3DB4MSHN.js +152 -0
- package/dist/cli/{play-73L74CUW.mjs → play-X6NPACUO.mjs} +7 -7
- package/dist/cli/{providers-PEOAZYM6.js → providers-E24JDWSR.js} +7 -7
- package/dist/cli/{providers-6LDD5X5G.mjs → providers-ONUBYYY4.mjs} +4 -4
- package/dist/cli/{quickstart-PXMC66UG.mjs → quickstart-B7NIXACV.mjs} +3 -3
- package/dist/cli/{quickstart-33IPDK2I.js → quickstart-TB7C3DLG.js} +5 -5
- package/dist/cli/{search-FJEJDKPA.mjs → search-N3QUIATW.mjs} +5 -5
- package/dist/cli/search-NJ637YGQ.js +26 -0
- package/dist/cli/{secrets-3SCAG3T7.js → secrets-CPCVFDYB.js} +13 -13
- package/dist/cli/{secrets-Q3ZWHE5S.mjs → secrets-V7Z4M67T.mjs} +4 -4
- package/dist/cli/{sessions-QJB4LSNZ.js → sessions-E4FIZLQP.js} +5 -5
- package/dist/cli/{sessions-RY3SCVN7.mjs → sessions-PDOBKAKO.mjs} +4 -4
- package/dist/cli/{settings-VTYJKMI6.mjs → settings-QYHYHZDI.mjs} +7 -7
- package/dist/cli/{settings-DLAE6POM.js → settings-UVM2WDE4.js} +74 -74
- package/dist/cli/{setup-LM2VWSFE.js → setup-3JFAXQLJ.js} +41 -41
- package/dist/cli/{setup-UYQ4V4GP.mjs → setup-4RLAETDW.mjs} +6 -6
- package/dist/cli/skills-BYRYG64R.js +28 -0
- package/dist/cli/{skills-2OKHLQF7.mjs → skills-ZX2JSWOZ.mjs} +4 -4
- package/dist/cli/{tools-F5RYANCG.mjs → tools-EGXCVIPJ.mjs} +6 -6
- package/dist/cli/tools-TAWOAY5X.js +38 -0
- package/dist/cli/{update-5FDHGSNE.mjs → update-7FEKRQFE.mjs} +6 -6
- package/dist/cli/update-H24GEBXM.js +28 -0
- package/dist/cli/{workflow-EQMKPCUV.js → workflow-54VCKIG2.js} +15 -15
- package/dist/cli/{workflow-TWQH53ZO.mjs → workflow-7V7TTTKK.mjs} +5 -5
- package/dist/{client-BV68GMPo.d.ts → client-Bb4Jjzmj.d.ts} +7 -1
- package/dist/{client-D9ACjiRC.d.mts → client-COa-3GHK.d.mts} +7 -1
- package/dist/{compiler-manifest-BXcZ4P4W.d.mts → compiler-manifest-CsnWj-AK.d.mts} +6 -0
- package/dist/{compiler-manifest-BXcZ4P4W.d.ts → compiler-manifest-CsnWj-AK.d.ts} +6 -0
- package/dist/index.d.mts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +6 -6
- package/dist/index.mjs +3 -3
- package/dist/install-integrity.json +79 -78
- package/dist/plays/bundle-play-file.d.mts +2 -2
- package/dist/plays/bundle-play-file.d.ts +2 -2
- package/dist/plays/run-request.d.mts +2 -2
- package/dist/plays/run-request.d.ts +2 -2
- package/dist/release.d.mts +1 -1
- package/dist/release.d.ts +1 -1
- package/dist/release.js +2 -2
- package/dist/release.mjs +1 -1
- package/package.json +1 -1
- package/dist/cli/auth-KWPQ4JVV.js +0 -31
- package/dist/cli/client-YMQ2QFV7.js +0 -20
- package/dist/cli/git-W5NEI2BP.js +0 -23
- package/dist/cli/play-NBFCGFVC.js +0 -152
- package/dist/cli/search-FPUK2H6Y.js +0 -26
- package/dist/cli/skills-5NH6JX5H.js +0 -28
- package/dist/cli/tools-ZPGDY3I2.js +0 -38
- package/dist/cli/update-W5KBXMKC.js +0 -28
package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
import {
|
|
11
11
|
DEFAULT_ADVISORY_PROVIDER_MAX_RPS,
|
|
12
12
|
DEFAULT_UNKNOWN_PROVIDER_COLD_START_RPS,
|
|
13
|
+
DEFAULT_UNKNOWN_PROVIDER_FAST_RECOVERY_RPS,
|
|
13
14
|
DEFAULT_UNKNOWN_PROVIDER_MIN_RPS,
|
|
14
15
|
PROVIDER_AIMD_COOLDOWN_CAP_MS,
|
|
15
16
|
type PacingRule,
|
|
@@ -34,6 +35,7 @@ const RATE_RESERVATION_BUCKET_EXPIRY_INDEX =
|
|
|
34
35
|
'runtime_scheduler_rate_reservation_bucket_expiry_idx';
|
|
35
36
|
const RATE_REQUESTER_TABLE = 'rate_requesters';
|
|
36
37
|
const RATE_SCOPE_HOLD_TABLE = 'rate_scope_holds';
|
|
38
|
+
const PHYSICAL_DISPATCH_RULE_ID = 'runtime:physical-dispatch';
|
|
37
39
|
|
|
38
40
|
const MIN_CONCURRENCY_WAIT_MS = 10;
|
|
39
41
|
/**
|
|
@@ -53,6 +55,10 @@ const FAIR_SHARE_REQUESTER_IDLE_WINDOWS = 2;
|
|
|
53
55
|
const FAIR_SHARE_REQUESTER_CLEANUP_BATCH_SIZE = 64;
|
|
54
56
|
/** Conservative warm-start width for a previously unobserved provider scope. */
|
|
55
57
|
const INITIAL_CONCURRENCY_DISCOVERY_LIMIT = 8;
|
|
58
|
+
// Rate spacing cannot bound requests delayed between admission and the
|
|
59
|
+
// physical provider. Unknown scopes discover with at most two shared leases
|
|
60
|
+
// until a productive rate epoch establishes that the provider can sustain more.
|
|
61
|
+
const UNKNOWN_PROVIDER_CONCURRENCY_DISCOVERY_LIMIT = 2;
|
|
56
62
|
/** A transport batch must stay bounded even when a runner completes huge maps. */
|
|
57
63
|
const MAX_CONCURRENCY_OBSERVATIONS_PER_ACQUIRE = 4_096;
|
|
58
64
|
/** Keep retry identities long enough for failed runtime work to recover. */
|
|
@@ -68,6 +74,7 @@ const MIN_ADAPTIVE_RPS_FLOOR = 5 / 60;
|
|
|
68
74
|
const MIN_NO_PROGRESS_PROBE_RPS = 1 / 60;
|
|
69
75
|
// Used only to keep wait-time division finite for persisted legacy state.
|
|
70
76
|
const MIN_SCHEDULE_RPS = 1 / 86_400;
|
|
77
|
+
const PRODUCTIVE_UNKNOWN_PROVIDER_DECREASE_FACTOR = 0.9;
|
|
71
78
|
/**
|
|
72
79
|
* Horizon a granted=0 caller is allowed to sleep toward a WHOLE block instead of
|
|
73
80
|
* a single token. Waiting for one token means one permit per store round trip,
|
|
@@ -115,8 +122,8 @@ export type PostgresRateStatePenaltyTelemetry = {
|
|
|
115
122
|
* persisting a learned capacity would make every warm acquire look like drift
|
|
116
123
|
* and re-seed the row. The bucket the grant actually spends is therefore
|
|
117
124
|
* derived, not stored: one second of `refill_per_sec` before a throttle and
|
|
118
|
-
*
|
|
119
|
-
* after a throttle only a single probe is the floor.
|
|
125
|
+
* a short bounded slice afterwards. Before a throttle the declared burst is a
|
|
126
|
+
* floor; after a throttle only a single probe is the floor.
|
|
120
127
|
* Retaining the catalog burst after learning a slow rate lets a fresh runtime
|
|
121
128
|
* reserve minutes of future slots before another run can register demand.
|
|
122
129
|
*
|
|
@@ -128,12 +135,17 @@ export type PostgresRateStatePenaltyTelemetry = {
|
|
|
128
135
|
* calls/s no matter how high `refill_per_sec` climbed.
|
|
129
136
|
*
|
|
130
137
|
* The burst stays bounded by the learned rate and `max_rps`. The dispatch
|
|
131
|
-
* cursor still spaces every permit at 1/`refill_per_sec
|
|
132
|
-
*
|
|
133
|
-
*
|
|
138
|
+
* cursor still spaces every permit at 1/`refill_per_sec`. The shorter
|
|
139
|
+
* post-throttle horizon limits fixed-window boundary overshoot while retaining
|
|
140
|
+
* small batches on fast lanes.
|
|
134
141
|
*/
|
|
135
142
|
const ADAPTIVE_BURST_HORIZON_MS = 1_000;
|
|
136
143
|
const BACKPRESSURED_BURST_HORIZON_MS = 100;
|
|
144
|
+
// Unknown providers recover near 10 RPS, where a 100ms slice leases only one
|
|
145
|
+
// permit. A shared admission round trip over 100ms would then cap actual work
|
|
146
|
+
// below the recovered target. Lease half a second of scheduled permits after
|
|
147
|
+
// the first probe; their dispatch times and the final shared hold still apply.
|
|
148
|
+
const UNKNOWN_PROVIDER_BACKPRESSURED_BURST_HORIZON_MS = 500;
|
|
137
149
|
|
|
138
150
|
/**
|
|
139
151
|
* Effective (spendable) bucket capacity for a rate-row alias. Derived per
|
|
@@ -142,7 +154,11 @@ const BACKPRESSURED_BURST_HORIZON_MS = 100;
|
|
|
142
154
|
function effectiveCapacitySql(
|
|
143
155
|
alias: string,
|
|
144
156
|
refillColumn = 'refill_per_sec',
|
|
157
|
+
unknownProvider = false,
|
|
145
158
|
): string {
|
|
159
|
+
const backpressuredBurstHorizonMs = unknownProvider
|
|
160
|
+
? UNKNOWN_PROVIDER_BACKPRESSURED_BURST_HORIZON_MS
|
|
161
|
+
: BACKPRESSURED_BURST_HORIZON_MS;
|
|
146
162
|
// After a provider 429, Retry-After marks the earliest safe probe time; it
|
|
147
163
|
// does not authorize banking a full rate-sized burst while the hold runs.
|
|
148
164
|
// rate_observation_started_at_ms stays NULL until the first post-hold grant,
|
|
@@ -158,7 +174,7 @@ function effectiveCapacitySql(
|
|
|
158
174
|
ELSE GREATEST(
|
|
159
175
|
CASE WHEN ${alias}.seen_backpressure OR ${alias}.${refillColumn} < ${alias}.base_refill_per_sec THEN 1 ELSE ${alias}.capacity END,
|
|
160
176
|
${alias}.${refillColumn} * CASE
|
|
161
|
-
WHEN ${alias}.seen_backpressure THEN ${
|
|
177
|
+
WHEN ${alias}.seen_backpressure THEN ${backpressuredBurstHorizonMs}
|
|
162
178
|
ELSE ${ADAPTIVE_BURST_HORIZON_MS}
|
|
163
179
|
END / 1000.0
|
|
164
180
|
)
|
|
@@ -753,6 +769,7 @@ async function seedBucketRows(
|
|
|
753
769
|
AND existing.capacity = sc.capacity
|
|
754
770
|
) AS unlearned_unknown_provider
|
|
755
771
|
FROM ${bucketTableName(input)} WHERE bucket_id = $1
|
|
772
|
+
AND rule_id <> '${PHYSICAL_DISPATCH_RULE_ID}'
|
|
756
773
|
), seed_scope_hold AS (
|
|
757
774
|
-- Preserve actual holds, but never import poisoned per-rule learning.
|
|
758
775
|
-- A legacy throttled scope restarts at at most one probe/second.
|
|
@@ -1279,6 +1296,9 @@ async function acquirePureRate(
|
|
|
1279
1296
|
requested: number,
|
|
1280
1297
|
): Promise<InternalAcquireOutcome> {
|
|
1281
1298
|
const ruleIds = rules.map((rule) => rule.ruleId);
|
|
1299
|
+
const unknownProvider = rules.some(
|
|
1300
|
+
(rule) => rule.ruleId === 'default:provider-scope',
|
|
1301
|
+
);
|
|
1282
1302
|
const requesterId = normalizeRequesterId(input);
|
|
1283
1303
|
const fair = requesterId
|
|
1284
1304
|
? fairShareSql(input, {
|
|
@@ -1325,6 +1345,7 @@ async function acquirePureRate(
|
|
|
1325
1345
|
scopeHoldTableName(input),
|
|
1326
1346
|
'$5',
|
|
1327
1347
|
minimumRateSqlForRules(rules),
|
|
1348
|
+
unknownProvider,
|
|
1328
1349
|
)},
|
|
1329
1350
|
reclaim_reservations AS (
|
|
1330
1351
|
DELETE FROM ${reservationTableName(input)}
|
|
@@ -1374,10 +1395,10 @@ async function acquirePureRate(
|
|
|
1374
1395
|
-- Spendable burst: one second of the rate in force, floored at the
|
|
1375
1396
|
-- declared window burst. The normalized row is what this acquire
|
|
1376
1397
|
-- spends; the stored row remains available for drift reporting.
|
|
1377
|
-
${effectiveCapacitySql('normalized')} AS effective_capacity,
|
|
1378
|
-
${effectiveCapacitySql('normalized', 'next_refill_per_sec')} AS scheduled_capacity,
|
|
1398
|
+
${effectiveCapacitySql('normalized', 'refill_per_sec', unknownProvider)} AS effective_capacity,
|
|
1399
|
+
${effectiveCapacitySql('normalized', 'next_refill_per_sec', unknownProvider)} AS scheduled_capacity,
|
|
1379
1400
|
LEAST(
|
|
1380
|
-
${effectiveCapacitySql('normalized')},
|
|
1401
|
+
${effectiveCapacitySql('normalized', 'refill_per_sec', unknownProvider)},
|
|
1381
1402
|
ph.tokens_after_refill,
|
|
1382
1403
|
LEAST(b.tokens, sc.capacity)
|
|
1383
1404
|
+ GREATEST(0, nv.now_ms - b.refilled_at_ms) / 1000.0
|
|
@@ -1479,7 +1500,7 @@ async function acquirePureRate(
|
|
|
1479
1500
|
GREATEST(1, FLOOR(MIN(next_refill_per_sec) / 100.0))::bigint AS burst_size
|
|
1480
1501
|
FROM refreshed
|
|
1481
1502
|
),
|
|
1482
|
-
${updateRateScopeSql(scopeHoldTableName(input))},
|
|
1503
|
+
${updateRateScopeSql(scopeHoldTableName(input), unknownProvider)},
|
|
1483
1504
|
updated AS (
|
|
1484
1505
|
UPDATE ${bucketTableName(input)} b
|
|
1485
1506
|
SET
|
|
@@ -1508,8 +1529,9 @@ async function acquirePureRate(
|
|
|
1508
1529
|
-- AIMD ramp, folded in. Reaching this refill proves a local lease
|
|
1509
1530
|
-- block was exhausted; only a confirmed provider success can turn
|
|
1510
1531
|
-- that demand into a rate increase. Slow-start doubles until the
|
|
1511
|
-
-- first 429
|
|
1512
|
-
--
|
|
1532
|
+
-- first 429. Known-provider recovery uses 25% steps; unknown scopes
|
|
1533
|
+
-- rediscover below their seed and probe gently above it. Never
|
|
1534
|
+
-- exceeds max_rps.
|
|
1513
1535
|
refill_per_sec = LEAST(r.max_rps, (SELECT next_refill_per_sec FROM provider_hold)),
|
|
1514
1536
|
last_ramp_at_ms = (SELECT next_ramp_at_ms FROM provider_hold)
|
|
1515
1537
|
FROM refreshed r
|
|
@@ -1702,8 +1724,10 @@ async function acquirePureRate(
|
|
|
1702
1724
|
* Mixed / concurrency-bearing acquire. The token bucket still governs the
|
|
1703
1725
|
* request rate; the durable lease table governs the simultaneous-in-flight cap.
|
|
1704
1726
|
* Expired-lease reclaim + active count + rate refill + grant + token deduction +
|
|
1705
|
-
* lease minting all execute in
|
|
1706
|
-
*
|
|
1727
|
+
* lease minting all execute in one multi-CTE statement after taking the scope
|
|
1728
|
+
* lock in a separate READ COMMITTED statement. Waiting for that lock must finish
|
|
1729
|
+
* before the lease count takes its snapshot, or concurrent acquires miss leases
|
|
1730
|
+
* committed during their wait. A crash cannot deduct tokens without minting the matching
|
|
1707
1731
|
* lease rows. Lease ids are generated in SQL (gen_random_uuid) and returned to
|
|
1708
1732
|
* the client, one per granted permit, releasable independently. With a
|
|
1709
1733
|
* requester identity the grant is also bounded by that run's fair share of
|
|
@@ -1716,8 +1740,44 @@ async function acquireWithConcurrency(
|
|
|
1716
1740
|
rules: AcquirePacingRule[],
|
|
1717
1741
|
seeds: RateBucketSeed[],
|
|
1718
1742
|
requested: number,
|
|
1743
|
+
): Promise<InternalAcquireOutcome> {
|
|
1744
|
+
await client.query('BEGIN ISOLATION LEVEL READ COMMITTED');
|
|
1745
|
+
try {
|
|
1746
|
+
await client.query(
|
|
1747
|
+
`SELECT bucket_id FROM ${scopeHoldTableName(input)}
|
|
1748
|
+
WHERE bucket_id = $1 FOR UPDATE`,
|
|
1749
|
+
[input.bucketId],
|
|
1750
|
+
);
|
|
1751
|
+
const outcome = await grantWithConcurrency(
|
|
1752
|
+
client,
|
|
1753
|
+
input,
|
|
1754
|
+
rules,
|
|
1755
|
+
seeds,
|
|
1756
|
+
requested,
|
|
1757
|
+
);
|
|
1758
|
+
await client.query('COMMIT');
|
|
1759
|
+
return outcome;
|
|
1760
|
+
} catch (error) {
|
|
1761
|
+
try {
|
|
1762
|
+
await client.query('ROLLBACK');
|
|
1763
|
+
} catch {
|
|
1764
|
+
// Preserve the original admission/storage error.
|
|
1765
|
+
}
|
|
1766
|
+
throw error;
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
async function grantWithConcurrency(
|
|
1771
|
+
client: PostgresSchedulerQueryClient,
|
|
1772
|
+
input: RateStateAcquireInput,
|
|
1773
|
+
rules: AcquirePacingRule[],
|
|
1774
|
+
seeds: RateBucketSeed[],
|
|
1775
|
+
requested: number,
|
|
1719
1776
|
): Promise<InternalAcquireOutcome> {
|
|
1720
1777
|
const ruleIds = rules.map((rule) => rule.ruleId);
|
|
1778
|
+
const unknownProvider = rules.some(
|
|
1779
|
+
(rule) => rule.ruleId === 'default:provider-scope',
|
|
1780
|
+
);
|
|
1721
1781
|
const concurrencyRules = rules.filter((rule) => rule.maxConcurrency != null);
|
|
1722
1782
|
const concurrencyRuleIds = concurrencyRules.map((rule) => rule.ruleId);
|
|
1723
1783
|
const requesterId = normalizeRequesterId(input);
|
|
@@ -1741,10 +1801,10 @@ async function acquireWithConcurrency(
|
|
|
1741
1801
|
// One multi-CTE statement: reclaim expired leases, refill the rate rows,
|
|
1742
1802
|
// count live leases, compute the grant bounded by rate AND free concurrency
|
|
1743
1803
|
// slots, deduct the tokens, AND mint the lease rows — atomically. Grant +
|
|
1744
|
-
// deduction + lease minting share
|
|
1804
|
+
// deduction + lease minting share the scope-lock transaction, so a crash can
|
|
1745
1805
|
// never deduct tokens without the matching lease rows (which would otherwise
|
|
1746
|
-
// over-admit the provider until the phantom leases "expired"). PgBouncer
|
|
1747
|
-
//
|
|
1806
|
+
// over-admit the provider until the phantom leases "expired"). PgBouncer's
|
|
1807
|
+
// transaction pooling keeps the scope lock through this fresh snapshot.
|
|
1748
1808
|
const result = await client.query<{
|
|
1749
1809
|
granted: string | number;
|
|
1750
1810
|
wait_ms: string | number;
|
|
@@ -1769,6 +1829,7 @@ async function acquireWithConcurrency(
|
|
|
1769
1829
|
scopeHoldTableName(input),
|
|
1770
1830
|
'$10',
|
|
1771
1831
|
minimumRateSqlForRules(rules),
|
|
1832
|
+
unknownProvider,
|
|
1772
1833
|
)},
|
|
1773
1834
|
reclaim_reservations AS (
|
|
1774
1835
|
DELETE FROM ${reservationTableName(input)}
|
|
@@ -1823,6 +1884,7 @@ async function acquireWithConcurrency(
|
|
|
1823
1884
|
b.concurrency_minimum_rtt_ms,
|
|
1824
1885
|
ph.cool_until_ms,
|
|
1825
1886
|
ph.last_ramp_at_ms,
|
|
1887
|
+
ph.rate_observation_started_at_ms,
|
|
1826
1888
|
GREATEST(b.next_dispatch_at_ms, ph.next_dispatch_at_ms) AS next_dispatch_at_ms,
|
|
1827
1889
|
ph.rate_recovery_interval_ms,
|
|
1828
1890
|
ph.observation_was_idle AS observation_was_idle,
|
|
@@ -1838,10 +1900,10 @@ async function acquireWithConcurrency(
|
|
|
1838
1900
|
ph.retry_at_ms AS scope_retry_at_ms,
|
|
1839
1901
|
-- Same derived burst as the pure-rate path. The declared
|
|
1840
1902
|
-- maxConcurrency is untouched: free_slots still bounds the grant.
|
|
1841
|
-
${effectiveCapacitySql('normalized')} AS effective_capacity,
|
|
1842
|
-
${effectiveCapacitySql('normalized', 'next_refill_per_sec')} AS scheduled_capacity,
|
|
1903
|
+
${effectiveCapacitySql('normalized', 'refill_per_sec', unknownProvider)} AS effective_capacity,
|
|
1904
|
+
${effectiveCapacitySql('normalized', 'next_refill_per_sec', unknownProvider)} AS scheduled_capacity,
|
|
1843
1905
|
LEAST(
|
|
1844
|
-
${effectiveCapacitySql('normalized')},
|
|
1906
|
+
${effectiveCapacitySql('normalized', 'refill_per_sec', unknownProvider)},
|
|
1845
1907
|
ph.tokens_after_refill,
|
|
1846
1908
|
LEAST(b.tokens, sc.capacity)
|
|
1847
1909
|
+ GREATEST(0, nv.now_ms - b.refilled_at_ms) / 1000.0
|
|
@@ -1891,10 +1953,17 @@ async function acquireWithConcurrency(
|
|
|
1891
1953
|
lim.rule_id,
|
|
1892
1954
|
LEAST(
|
|
1893
1955
|
lim.max_concurrency,
|
|
1956
|
+
CASE WHEN bucket.rule_id = 'default:provider-scope'
|
|
1957
|
+
AND NOT bucket.seen_backpressure
|
|
1958
|
+
AND bucket.last_ramp_at_ms <= COALESCE(bucket.rate_observation_started_at_ms, bucket.now_ms)
|
|
1959
|
+
THEN ${UNKNOWN_PROVIDER_CONCURRENCY_DISCOVERY_LIMIT}
|
|
1960
|
+
ELSE lim.max_concurrency END,
|
|
1894
1961
|
CEIL(
|
|
1895
1962
|
COALESCE(
|
|
1896
1963
|
bucket.learned_concurrency_limit,
|
|
1897
|
-
LEAST(lim.max_concurrency,
|
|
1964
|
+
LEAST(lim.max_concurrency, CASE WHEN bucket.rule_id = 'default:provider-scope'
|
|
1965
|
+
THEN ${UNKNOWN_PROVIDER_CONCURRENCY_DISCOVERY_LIMIT}
|
|
1966
|
+
ELSE ${INITIAL_CONCURRENCY_DISCOVERY_LIMIT} END)
|
|
1898
1967
|
)
|
|
1899
1968
|
)
|
|
1900
1969
|
)::int AS max_concurrency
|
|
@@ -1999,7 +2068,7 @@ async function acquireWithConcurrency(
|
|
|
1999
2068
|
GREATEST(1, FLOOR(MIN(next_refill_per_sec) / 100.0))::bigint AS burst_size
|
|
2000
2069
|
FROM refreshed
|
|
2001
2070
|
),
|
|
2002
|
-
${updateRateScopeSql(scopeHoldTableName(input))},
|
|
2071
|
+
${updateRateScopeSql(scopeHoldTableName(input), unknownProvider)},
|
|
2003
2072
|
updated AS (
|
|
2004
2073
|
UPDATE ${bucketTableName(input)} b
|
|
2005
2074
|
SET
|
|
@@ -2035,6 +2104,7 @@ async function acquireWithConcurrency(
|
|
|
2035
2104
|
AND b.rule_id = r.rule_id
|
|
2036
2105
|
RETURNING
|
|
2037
2106
|
r.tokens_after_refill,
|
|
2107
|
+
r.effective_capacity AS capacity,
|
|
2038
2108
|
b.refill_per_sec AS new_refill_per_sec
|
|
2039
2109
|
),
|
|
2040
2110
|
lease_tokens AS (
|
|
@@ -2177,7 +2247,19 @@ async function acquireWithConcurrency(
|
|
|
2177
2247
|
WHEN (SELECT free FROM free_slots) <= 0 THEN $7::bigint
|
|
2178
2248
|
ELSE (
|
|
2179
2249
|
SELECT CEIL(
|
|
2180
|
-
MAX(
|
|
2250
|
+
MAX(
|
|
2251
|
+
(
|
|
2252
|
+
GREATEST(
|
|
2253
|
+
1.0,
|
|
2254
|
+
LEAST(
|
|
2255
|
+
$4::numeric,
|
|
2256
|
+
FLOOR(capacity),
|
|
2257
|
+
FLOOR(new_refill_per_sec * ${BLOCK_WAIT_HORIZON_MS} / 1000.0),
|
|
2258
|
+
(SELECT free FROM free_slots)
|
|
2259
|
+
)
|
|
2260
|
+
) - tokens_after_refill
|
|
2261
|
+
) / GREATEST(new_refill_per_sec, ${MIN_SCHEDULE_RPS}) * 1000
|
|
2262
|
+
)
|
|
2181
2263
|
)::bigint
|
|
2182
2264
|
FROM updated
|
|
2183
2265
|
)
|
|
@@ -2320,6 +2402,124 @@ export async function validatePostgresRateStatePermit(
|
|
|
2320
2402
|
});
|
|
2321
2403
|
}
|
|
2322
2404
|
|
|
2405
|
+
/**
|
|
2406
|
+
* Space unknown-provider sends after app preparation, where network/charge
|
|
2407
|
+
* delays can collapse previously spaced permits. The existing scope remains
|
|
2408
|
+
* the only learner; this internal rule row stores only the physical cadence.
|
|
2409
|
+
* Legacy/uninitialized and declared scopes retain hold-only validation.
|
|
2410
|
+
*/
|
|
2411
|
+
export async function claimPostgresProviderDispatch(
|
|
2412
|
+
client: PostgresSchedulerQueryClient,
|
|
2413
|
+
input: PlayRunnerRateStateValidateInput & { nowMs?: number },
|
|
2414
|
+
): Promise<
|
|
2415
|
+
PlayRunnerRateStateValidateResult & { physicalCadenceWait: boolean }
|
|
2416
|
+
> {
|
|
2417
|
+
return withRateStateAvailabilityGuard(input, async () => {
|
|
2418
|
+
const result = await client.query<{
|
|
2419
|
+
server_now_ms: string | number;
|
|
2420
|
+
retry_at_ms: string | number;
|
|
2421
|
+
allowed: boolean;
|
|
2422
|
+
cadence_wait: boolean;
|
|
2423
|
+
}>(
|
|
2424
|
+
`
|
|
2425
|
+
WITH now_value AS (
|
|
2426
|
+
SELECT COALESCE($2::bigint,
|
|
2427
|
+
FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint) AS now_ms
|
|
2428
|
+
), scope_rows AS (
|
|
2429
|
+
SELECT h.retry_at_ms, h.cool_until_ms, h.seen_backpressure,
|
|
2430
|
+
CASE WHEN EXISTS (
|
|
2431
|
+
SELECT 1 FROM ${bucketTableName(input)} b
|
|
2432
|
+
WHERE b.bucket_id = h.bucket_id AND b.rule_id = 'default:provider-scope'
|
|
2433
|
+
) THEN h.refill_per_sec ELSE NULL END AS physical_rps
|
|
2434
|
+
FROM ${scopeHoldTableName(input)} h WHERE h.bucket_id = $1
|
|
2435
|
+
FOR SHARE OF h
|
|
2436
|
+
), cadence AS (
|
|
2437
|
+
SELECT nv.now_ms,
|
|
2438
|
+
GREATEST(COALESCE(s.retry_at_ms, 0), COALESCE(s.cool_until_ms, 0)) AS hold_until_ms,
|
|
2439
|
+
s.physical_rps,
|
|
2440
|
+
COALESCE(s.seen_backpressure, false) AS preserve_cadence_phase,
|
|
2441
|
+
GREATEST(1, FLOOR(s.physical_rps / 100))::int AS burst_size,
|
|
2442
|
+
GREATEST(1, CEIL(1000.0 / s.physical_rps))::bigint AS spacing_ms
|
|
2443
|
+
FROM now_value nv LEFT JOIN scope_rows s ON true
|
|
2444
|
+
), cursor AS (
|
|
2445
|
+
INSERT INTO ${bucketTableName(input)} AS existing (
|
|
2446
|
+
bucket_id, rule_id, tokens, capacity, refill_per_sec,
|
|
2447
|
+
base_refill_per_sec, min_rps, max_rps, refilled_at_ms,
|
|
2448
|
+
next_dispatch_at_ms, cool_until_ms
|
|
2449
|
+
)
|
|
2450
|
+
SELECT $1, '${PHYSICAL_DISPATCH_RULE_ID}', c.burst_size - 1,
|
|
2451
|
+
c.burst_size, c.physical_rps, c.physical_rps,
|
|
2452
|
+
${MIN_SCHEDULE_RPS}, c.physical_rps, c.now_ms,
|
|
2453
|
+
c.now_ms + c.spacing_ms * c.burst_size, 0
|
|
2454
|
+
FROM cadence c
|
|
2455
|
+
WHERE c.hold_until_ms <= c.now_ms AND c.physical_rps > 0
|
|
2456
|
+
ON CONFLICT (bucket_id, rule_id) DO UPDATE SET
|
|
2457
|
+
-- Always return the locked current cursor, including a denial.
|
|
2458
|
+
-- A snapshot read after a blocked upsert could miss a concurrent
|
|
2459
|
+
-- insert and report the wrong retry deadline.
|
|
2460
|
+
tokens = CASE
|
|
2461
|
+
WHEN existing.next_dispatch_at_ms <= EXCLUDED.refilled_at_ms
|
|
2462
|
+
THEN EXCLUDED.capacity - 1
|
|
2463
|
+
WHEN LEAST(existing.tokens, EXCLUDED.capacity) >= 1
|
|
2464
|
+
THEN LEAST(existing.tokens, EXCLUDED.capacity) - 1
|
|
2465
|
+
ELSE existing.tokens END,
|
|
2466
|
+
cool_until_ms = CASE
|
|
2467
|
+
WHEN existing.next_dispatch_at_ms <= EXCLUDED.refilled_at_ms
|
|
2468
|
+
OR LEAST(existing.tokens, EXCLUDED.capacity) >= 1 THEN 0
|
|
2469
|
+
ELSE LEAST(existing.next_dispatch_at_ms, EXCLUDED.next_dispatch_at_ms) END,
|
|
2470
|
+
-- Persist debt decay at the new pace so repeated probes converge
|
|
2471
|
+
-- to this deadline instead of moving it forward on every denial.
|
|
2472
|
+
next_dispatch_at_ms = CASE
|
|
2473
|
+
WHEN existing.next_dispatch_at_ms <= EXCLUDED.refilled_at_ms
|
|
2474
|
+
-- Keep the cadence phase when a request reaches its slot
|
|
2475
|
+
-- late. Reanchoring every grant to now adds query/network
|
|
2476
|
+
-- jitter to every interval and lowers sustained throughput.
|
|
2477
|
+
-- Expire missed slots and retain only the first future slot,
|
|
2478
|
+
-- so idle time never creates a catch-up burst.
|
|
2479
|
+
THEN CASE WHEN (SELECT preserve_cadence_phase FROM cadence)
|
|
2480
|
+
THEN (existing.next_dispatch_at_ms::numeric
|
|
2481
|
+
+ (
|
|
2482
|
+
FLOOR(
|
|
2483
|
+
(EXCLUDED.refilled_at_ms::numeric - existing.next_dispatch_at_ms::numeric)
|
|
2484
|
+
/ (EXCLUDED.next_dispatch_at_ms - EXCLUDED.refilled_at_ms)
|
|
2485
|
+
) + 1
|
|
2486
|
+
) * (EXCLUDED.next_dispatch_at_ms - EXCLUDED.refilled_at_ms))::bigint
|
|
2487
|
+
-- Cold discovery retains strict spacing until provider
|
|
2488
|
+
-- backpressure establishes an adaptive cadence.
|
|
2489
|
+
ELSE EXCLUDED.next_dispatch_at_ms END
|
|
2490
|
+
ELSE LEAST(existing.next_dispatch_at_ms, EXCLUDED.next_dispatch_at_ms) END,
|
|
2491
|
+
capacity = EXCLUDED.capacity,
|
|
2492
|
+
refill_per_sec = EXCLUDED.refill_per_sec,
|
|
2493
|
+
base_refill_per_sec = EXCLUDED.base_refill_per_sec,
|
|
2494
|
+
max_rps = EXCLUDED.max_rps,
|
|
2495
|
+
refilled_at_ms = EXCLUDED.refilled_at_ms
|
|
2496
|
+
RETURNING cool_until_ms
|
|
2497
|
+
)
|
|
2498
|
+
SELECT c.now_ms AS server_now_ms,
|
|
2499
|
+
GREATEST(c.hold_until_ms, COALESCE(cursor.cool_until_ms, 0)) AS retry_at_ms,
|
|
2500
|
+
c.hold_until_ms <= c.now_ms AND (
|
|
2501
|
+
c.physical_rps IS NULL OR cursor.cool_until_ms = 0
|
|
2502
|
+
) AS allowed,
|
|
2503
|
+
c.hold_until_ms <= c.now_ms AND COALESCE(cursor.cool_until_ms > c.now_ms, false) AS cadence_wait
|
|
2504
|
+
FROM cadence c LEFT JOIN cursor ON true
|
|
2505
|
+
`,
|
|
2506
|
+
[input.bucketId, input.nowMs ?? null],
|
|
2507
|
+
);
|
|
2508
|
+
const row = result.rows[0];
|
|
2509
|
+
if (!row || typeof row.allowed !== 'boolean') {
|
|
2510
|
+
throw new Error(
|
|
2511
|
+
`Physical provider admission returned no decision for ${input.bucketId}.`,
|
|
2512
|
+
);
|
|
2513
|
+
}
|
|
2514
|
+
return {
|
|
2515
|
+
allowed: row.allowed,
|
|
2516
|
+
physicalCadenceWait: row.cadence_wait,
|
|
2517
|
+
serverNowMs: Number(row.server_now_ms),
|
|
2518
|
+
retryAtMs: Number(row.retry_at_ms),
|
|
2519
|
+
};
|
|
2520
|
+
});
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2323
2523
|
type ConcurrencyObservation = Gradient2Observation & {
|
|
2324
2524
|
kind: 'success' | 'throttled';
|
|
2325
2525
|
};
|
|
@@ -2723,7 +2923,10 @@ async function applyConcurrencyObservations(
|
|
|
2723
2923
|
maximumLimit,
|
|
2724
2924
|
Math.max(
|
|
2725
2925
|
1,
|
|
2726
|
-
row.learned_concurrency_limit ??
|
|
2926
|
+
row.learned_concurrency_limit ??
|
|
2927
|
+
(rule.ruleId === 'default:provider-scope'
|
|
2928
|
+
? UNKNOWN_PROVIDER_CONCURRENCY_DISCOVERY_LIMIT
|
|
2929
|
+
: INITIAL_CONCURRENCY_DISCOVERY_LIMIT),
|
|
2727
2930
|
),
|
|
2728
2931
|
);
|
|
2729
2932
|
limitsBefore.push(initialLimit);
|
|
@@ -2731,6 +2934,7 @@ async function applyConcurrencyObservations(
|
|
|
2731
2934
|
providerConcurrencyControllerOptions({
|
|
2732
2935
|
initialLimit,
|
|
2733
2936
|
maximumLimit,
|
|
2937
|
+
unknownProvider: rule.ruleId === 'default:provider-scope',
|
|
2734
2938
|
}),
|
|
2735
2939
|
row.learned_concurrency_limit == null
|
|
2736
2940
|
? undefined
|
|
@@ -2745,7 +2949,7 @@ async function applyConcurrencyObservations(
|
|
|
2745
2949
|
),
|
|
2746
2950
|
windowStartedAtLimit: Math.max(
|
|
2747
2951
|
1,
|
|
2748
|
-
|
|
2952
|
+
row.concurrency_window_limit ?? initialLimit,
|
|
2749
2953
|
),
|
|
2750
2954
|
hasObservedThrottle:
|
|
2751
2955
|
row.concurrency_has_observed_throttle === true,
|
|
@@ -2826,7 +3030,8 @@ async function acquireRateStateStatements(
|
|
|
2826
3030
|
: acquirePureRate(client, grantInput, rules, seeds, requested);
|
|
2827
3031
|
|
|
2828
3032
|
// Cold path only: seed missing rows / refresh changed configuration. A warm
|
|
2829
|
-
// acquire is exactly one statement.
|
|
3033
|
+
// pure-rate acquire is exactly one statement. Concurrency-bearing acquires
|
|
3034
|
+
// take the scope lock before the lease-count statement gets its snapshot.
|
|
2830
3035
|
let seeded = false;
|
|
2831
3036
|
if (!warmRateBuckets.has(warmKey)) {
|
|
2832
3037
|
await seedBucketRows(client, input, seeds, nowMs);
|
|
@@ -3067,7 +3272,7 @@ async function penalizeRateStateStatements(
|
|
|
3067
3272
|
// one-minute probe interval so the five-RPM floor cannot turn into a retry
|
|
3068
3273
|
// loop.
|
|
3069
3274
|
// - refill_per_sec: multiplicative decrease, but ONLY once per active
|
|
3070
|
-
// cooldown window (
|
|
3275
|
+
// cooldown window (skip if already cooling), floored at
|
|
3071
3276
|
// min_rps.
|
|
3072
3277
|
// - seen_backpressure flips the ramp from slow-start (x2) to additive.
|
|
3073
3278
|
// - tokens drained: no more grants until refill re-accrues past the cooldown.
|
|
@@ -3095,9 +3300,37 @@ async function penalizeRateStateStatements(
|
|
|
3095
3300
|
) AS now_ms
|
|
3096
3301
|
), held AS (
|
|
3097
3302
|
SELECT * FROM ${scopeHoldTableName(input)} WHERE bucket_id = $1 FOR UPDATE
|
|
3303
|
+
), seeded_hold AS (
|
|
3304
|
+
SELECT h.*, CASE WHEN cardinality($6::text[]) > 0
|
|
3305
|
+
THEN 'default:provider-scope' = ANY($6::text[])
|
|
3306
|
+
AND NOT EXISTS (
|
|
3307
|
+
SELECT 1 FROM unnest($6::text[]) AS active(rule_id)
|
|
3308
|
+
WHERE active.rule_id NOT LIKE 'default:%'
|
|
3309
|
+
)
|
|
3310
|
+
ELSE EXISTS (
|
|
3311
|
+
SELECT 1 FROM ${bucketTableName(input)} b
|
|
3312
|
+
WHERE b.bucket_id = h.bucket_id
|
|
3313
|
+
AND b.rule_id = 'default:provider-scope'
|
|
3314
|
+
) AND NOT EXISTS (
|
|
3315
|
+
SELECT 1 FROM ${bucketTableName(input)} b
|
|
3316
|
+
WHERE b.bucket_id = h.bucket_id
|
|
3317
|
+
AND b.rule_id NOT LIKE 'default:%'
|
|
3318
|
+
AND b.rule_id <> '${PHYSICAL_DISPATCH_RULE_ID}'
|
|
3319
|
+
) END AS unknown_provider
|
|
3320
|
+
FROM held h
|
|
3098
3321
|
), evidence AS (
|
|
3099
3322
|
SELECT h.*,
|
|
3100
3323
|
COALESCE(h.refill_per_sec, 1) AS refill_before,
|
|
3324
|
+
-- A productive unknown-provider probe establishes useful capacity.
|
|
3325
|
+
-- Preserve 90% of its target instead of discarding half after each
|
|
3326
|
+
-- exploratory overshoot. Failed or sparse probes keep the stronger
|
|
3327
|
+
-- decrease and no-progress policy. Prefer the attempt's active rules.
|
|
3328
|
+
-- Early app feedback carries no rules: only an exclusively unknown
|
|
3329
|
+
-- durable seed can select this policy, not an obsolete default row
|
|
3330
|
+
-- beside declared operation rows.
|
|
3331
|
+
h.unknown_provider AND h.refill_per_sec >= ${DEFAULT_UNKNOWN_PROVIDER_FAST_RECOVERY_RPS}
|
|
3332
|
+
AND h.rate_successes >= ${DEFAULT_UNKNOWN_PROVIDER_COLD_START_RPS}
|
|
3333
|
+
AS productive_unknown_probe,
|
|
3101
3334
|
GREATEST(h.retry_at_ms, COALESCE($5::bigint, nv.now_ms + $2::bigint)) AS deadline,
|
|
3102
3335
|
(
|
|
3103
3336
|
COALESCE($5::bigint, nv.now_ms + $2::bigint) > nv.now_ms
|
|
@@ -3122,17 +3355,25 @@ async function penalizeRateStateStatements(
|
|
|
3122
3355
|
- COALESCE(h.rate_observation_started_at_ms, nv.now_ms)
|
|
3123
3356
|
END) AS cycle_ms,
|
|
3124
3357
|
CASE
|
|
3125
|
-
|
|
3358
|
+
-- A first unknown-provider rejection establishes its rate floor,
|
|
3359
|
+
-- not a failed probe streak. Respect its explicit deadline even
|
|
3360
|
+
-- when it expires in transit; the reduced dispatch budget still
|
|
3361
|
+
-- paces the next request. Only a subsequent admitted probe with
|
|
3362
|
+
-- no successes justifies the shared exponential pause.
|
|
3363
|
+
WHEN h.rate_successes <= 0
|
|
3364
|
+
AND ($5::bigint IS NOT NULL OR h.seen_backpressure)
|
|
3365
|
+
AND (NOT h.unknown_provider OR h.seen_backpressure) THEN
|
|
3126
3366
|
CASE WHEN h.seen_backpressure THEN
|
|
3127
3367
|
LEAST(60000, GREATEST(1000, h.rate_recovery_interval_ms * 2))
|
|
3128
3368
|
ELSE 1000 END
|
|
3129
3369
|
ELSE NULL
|
|
3130
3370
|
END AS no_progress_probe_interval_ms,
|
|
3131
3371
|
nv.now_ms
|
|
3132
|
-
FROM
|
|
3372
|
+
FROM seeded_hold h CROSS JOIN now_value nv
|
|
3133
3373
|
), throttle_ceiling AS (
|
|
3134
3374
|
SELECT e.*,
|
|
3135
|
-
GREATEST(
|
|
3375
|
+
GREATEST(CASE WHEN e.unknown_provider
|
|
3376
|
+
THEN ${DEFAULT_UNKNOWN_PROVIDER_MIN_RPS} ELSE ${minimumRateRps} END,
|
|
3136
3377
|
CASE WHEN e.seen_backpressure AND e.rate_successes = 0 THEN
|
|
3137
3378
|
CASE WHEN e.no_progress_probe_interval_ms IS NOT NULL
|
|
3138
3379
|
-- A fresh provider deadline is not a throughput measurement.
|
|
@@ -3143,6 +3384,7 @@ async function penalizeRateStateStatements(
|
|
|
3143
3384
|
THEN GREATEST(${noProgressProbeRateRps},
|
|
3144
3385
|
1000.0 / e.no_progress_probe_interval_ms)
|
|
3145
3386
|
ELSE e.refill_before END
|
|
3387
|
+
WHEN e.productive_unknown_probe THEN e.refill_before * ${PRODUCTIVE_UNKNOWN_PROVIDER_DECREASE_FACTOR}
|
|
3146
3388
|
ELSE e.refill_before / 2 END
|
|
3147
3389
|
) AS next_ceiling_rps
|
|
3148
3390
|
FROM evidence e
|
|
@@ -3151,11 +3393,12 @@ async function penalizeRateStateStatements(
|
|
|
3151
3393
|
CASE WHEN e.new_episode THEN GREATEST(
|
|
3152
3394
|
CASE WHEN e.seen_backpressure AND e.rate_successes = 0
|
|
3153
3395
|
THEN ${noProgressProbeRateRps}
|
|
3154
|
-
ELSE
|
|
3155
|
-
|
|
3396
|
+
ELSE CASE WHEN e.unknown_provider
|
|
3397
|
+
THEN ${DEFAULT_UNKNOWN_PROVIDER_MIN_RPS} ELSE ${minimumRateRps} END END,
|
|
3398
|
+
CASE WHEN e.productive_unknown_probe THEN e.next_ceiling_rps ELSE LEAST(
|
|
3156
3399
|
e.next_ceiling_rps,
|
|
3157
3400
|
GREATEST(1, e.rate_successes) * 1000.0 / e.cycle_ms
|
|
3158
|
-
)) ELSE e.refill_before END AS refill_after
|
|
3401
|
+
) END) ELSE e.refill_before END AS refill_after
|
|
3159
3402
|
FROM throttle_ceiling e
|
|
3160
3403
|
), provider_hold AS (
|
|
3161
3404
|
UPDATE ${scopeHoldTableName(input)} h SET
|
|
@@ -3165,21 +3408,33 @@ async function penalizeRateStateStatements(
|
|
|
3165
3408
|
-- A repeated no-success throttle can impose a slower local probe
|
|
3166
3409
|
-- interval than the normal adaptive floor. Keep that pause separate
|
|
3167
3410
|
-- from retry_at_ms, which remains the provider's absolute deadline.
|
|
3168
|
-
|
|
3411
|
+
-- Only a newly admitted probe can start another backoff episode.
|
|
3412
|
+
-- Gateway/runner copies and late in-flight siblings still extend
|
|
3413
|
+
-- retry_at_ms, but must not slide the local no-progress pause.
|
|
3414
|
+
cool_until_ms = CASE WHEN e.new_episode THEN GREATEST(e.cool_until_ms, LEAST(
|
|
3169
3415
|
e.now_ms + $4::bigint,
|
|
3170
3416
|
GREATEST(
|
|
3171
3417
|
e.deadline,
|
|
3172
|
-
|
|
3418
|
+
-- cooldownMs was captured before transport/pool admission. When
|
|
3419
|
+
-- an absolute deadline exists, adding that stale duration to
|
|
3420
|
+
-- the database clock would restart part of the same hold.
|
|
3421
|
+
-- Legacy duration-only callers still start their hold here.
|
|
3422
|
+
CASE WHEN $5::bigint IS NULL
|
|
3423
|
+
THEN e.now_ms + LEAST($2::bigint, $4::bigint)
|
|
3424
|
+
ELSE e.now_ms END,
|
|
3173
3425
|
e.now_ms + COALESCE(e.no_progress_probe_interval_ms, 0)
|
|
3174
3426
|
)
|
|
3175
|
-
)),
|
|
3427
|
+
)) ELSE e.cool_until_ms END,
|
|
3176
3428
|
tokens = CASE WHEN e.new_episode THEN 0 ELSE e.tokens END,
|
|
3177
3429
|
rate_successes = e.rate_successes,
|
|
3178
3430
|
rate_throttle_ceiling_rps = CASE WHEN e.new_episode
|
|
3179
3431
|
THEN e.next_ceiling_rps ELSE e.rate_throttle_ceiling_rps END,
|
|
3180
3432
|
rate_observation_started_at_ms = CASE WHEN e.new_episode THEN NULL ELSE e.rate_observation_started_at_ms END,
|
|
3181
3433
|
rate_recovery_interval_ms = CASE WHEN e.new_episode
|
|
3182
|
-
THEN
|
|
3434
|
+
THEN CASE WHEN e.unknown_provider AND e.rate_successes > 0
|
|
3435
|
+
-- Retry-After is a deadline, not time spent proving capacity.
|
|
3436
|
+
THEN GREATEST(1000, e.now_ms - COALESCE(e.rate_observation_started_at_ms, e.now_ms))
|
|
3437
|
+
ELSE COALESCE(e.no_progress_probe_interval_ms, e.cycle_ms) END
|
|
3183
3438
|
ELSE e.rate_recovery_interval_ms END,
|
|
3184
3439
|
last_ramp_at_ms = CASE WHEN e.new_episode THEN e.deadline ELSE e.last_ramp_at_ms END,
|
|
3185
3440
|
-- All pre-429 permits are revalidated against this durable hold
|
|
@@ -3193,6 +3448,7 @@ async function penalizeRateStateStatements(
|
|
|
3193
3448
|
SELECT b.bucket_id, b.rule_id, b.refill_per_sec AS refill_before
|
|
3194
3449
|
FROM ${bucketTableName(input)} b
|
|
3195
3450
|
JOIN provider_hold h ON h.bucket_id = b.bucket_id
|
|
3451
|
+
WHERE b.rule_id <> '${PHYSICAL_DISPATCH_RULE_ID}'
|
|
3196
3452
|
ORDER BY b.rule_id FOR UPDATE OF b
|
|
3197
3453
|
), updated AS (
|
|
3198
3454
|
-- Compatibility projections only: no per-rule learning history is
|
|
@@ -3235,6 +3491,7 @@ async function penalizeRateStateStatements(
|
|
|
3235
3491
|
input.nowMs ?? null,
|
|
3236
3492
|
PROVIDER_AIMD_COOLDOWN_CAP_MS,
|
|
3237
3493
|
retryAtMs,
|
|
3494
|
+
rules.map((rule) => rule.ruleId),
|
|
3238
3495
|
],
|
|
3239
3496
|
);
|
|
3240
3497
|
const rateRow = rateStateResult.rows[0];
|