deepline 0.3.332 → 0.3.334
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/play.ts +1 -1
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +200 -43
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +4 -0
- package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +8 -2
- package/dist/bundling-sources/shared_libs/play-runtime/execution-gateway-invocation-fence.ts +16 -0
- package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +30 -9
- package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +6 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +11 -7
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-deadline.ts +14 -14
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +12 -5
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-shared.ts +6 -1
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-scope.ts +95 -24
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts +132 -37
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +13 -5
- package/dist/bundling-sources/shared_libs/play-runtime/suspension.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +87 -3
- package/dist/bundling-sources/shared_libs/plays/authoring-contract.ts +7 -0
- package/dist/cli/{auth-V4IWMXTG.mjs → auth-PAEBUPRS.mjs} +4 -4
- package/dist/cli/auth-WR3XFOHN.js +31 -0
- package/dist/cli/{billing-GYW4BCPG.mjs → billing-QGQJUM5G.mjs} +4 -4
- package/dist/cli/{billing-BW37QW4H.js → billing-V746QMQC.js} +71 -71
- package/dist/cli/{chunk-GOH5BRGN.js → chunk-6466HVYR.js} +26 -26
- package/dist/cli/{chunk-JHF3GWKX.js → chunk-6MVMTDL7.js} +4 -4
- package/dist/cli/{chunk-TTJ7KELW.js → chunk-7XXNMV6G.js} +45 -45
- package/dist/cli/{chunk-WQI642QX.mjs → chunk-A7S4PRK2.mjs} +1 -1
- package/dist/cli/{chunk-HKQMCTEE.mjs → chunk-EJLUQARV.mjs} +4 -4
- package/dist/cli/{chunk-SBPJWA6Y.mjs → chunk-EJVAWHQ7.mjs} +6 -6
- package/dist/cli/{chunk-GDBJYM5N.js → chunk-FLMBKUHB.js} +60 -60
- package/dist/cli/{chunk-U2B5KDNH.js → chunk-GNHI4WZC.js} +15 -15
- package/dist/cli/{chunk-KYQPEIKA.mjs → chunk-GUMAWNWZ.mjs} +5 -5
- package/dist/cli/{chunk-CYLZE6RJ.js → chunk-GX2YGFQV.js} +1 -1
- package/dist/cli/{chunk-X6MY7BC2.js → chunk-HIPRSI5H.js} +225 -225
- package/dist/cli/{chunk-AF6XLGZT.js → chunk-NLHRXDIT.js} +2 -2
- package/dist/cli/{chunk-UMKN2BZK.mjs → chunk-O564PHDU.mjs} +1 -1
- package/dist/cli/{chunk-MXJVBJYC.mjs → chunk-O64RSXDZ.mjs} +2 -2
- package/dist/cli/{chunk-3E7P62NA.mjs → chunk-P3ZX3NSE.mjs} +1 -1
- package/dist/cli/{chunk-ERPCUHOQ.js → chunk-R6O3YVIG.js} +8 -8
- package/dist/cli/{chunk-PK4EE6T3.js → chunk-SABEM6FL.js} +48 -48
- package/dist/cli/{chunk-J5Z5BQGE.mjs → chunk-SBEWKGUA.mjs} +1 -1
- package/dist/cli/{chunk-4RLN2DHZ.mjs → chunk-TT2CLWMK.mjs} +1 -1
- package/dist/cli/{chunk-B6R2V3TH.mjs → chunk-TVIWEQ4V.mjs} +3 -3
- package/dist/cli/{chunk-MSQK2FLJ.mjs → chunk-U724V33M.mjs} +2 -2
- package/dist/cli/{chunk-T5TGWH4J.js → chunk-UUB3FTKZ.js} +6 -6
- package/dist/cli/{chunk-LAJ56FLL.mjs → chunk-VB7TNIUB.mjs} +1 -1
- package/dist/cli/{chunk-ZZWCK6F3.js → chunk-XXZSLV7B.js} +6 -6
- package/dist/cli/{chunk-5FHGKEAD.mjs → chunk-Y2VN54ME.mjs} +2 -2
- package/dist/cli/{chunk-XON4H3CO.js → chunk-Z6VHC643.js} +9 -9
- package/dist/cli/client-6OBIO6GC.js +20 -0
- package/dist/cli/{client-TBCDCJ7T.mjs → client-JIZWAGVW.mjs} +3 -3
- package/dist/cli/{csv-O3NEPSU3.mjs → csv-7DCY7Z6R.mjs} +3 -3
- package/dist/cli/{csv-EFQ7UL3M.js → csv-KAFKXAFU.js} +12 -12
- package/dist/cli/{db-2FQ3T4LT.js → db-5LJPO2VA.js} +29 -29
- package/dist/cli/{db-RUWCFINP.mjs → db-KJBWUFPL.mjs} +4 -4
- package/dist/cli/{describe-4KSVRYPM.mjs → describe-CPF4KDKZ.mjs} +8 -8
- package/dist/cli/{describe-YJFDN54P.js → describe-VJEEVOH6.js} +15 -15
- package/dist/cli/{executions-MG63LYMV.mjs → executions-LJN5QOK7.mjs} +4 -4
- package/dist/cli/{executions-O7IIWVRV.js → executions-LPVCV5SC.js} +6 -6
- package/dist/cli/{feedback-WMYRDK2J.mjs → feedback-3HYS6ECC.mjs} +3 -3
- package/dist/cli/{feedback-SR2X2H4N.js → feedback-K26WP7HJ.js} +6 -6
- package/dist/cli/{git-NIGE3SIJ.mjs → git-U4VBLXSS.mjs} +4 -4
- package/dist/cli/git-VMQTXKJQ.js +23 -0
- package/dist/cli/index.js +79 -79
- package/dist/cli/index.mjs +31 -31
- package/dist/cli/{legacy-noops-TXSBNOG6.js → legacy-noops-32YL3VF4.js} +7 -7
- package/dist/cli/{legacy-noops-YXYYCDSR.mjs → legacy-noops-ORHX2FMA.mjs} +4 -4
- package/dist/cli/{monitors-QYAGRL6V.mjs → monitors-5JJBEJS7.mjs} +4 -4
- package/dist/cli/{monitors-ZZUQMDUA.js → monitors-NFE4ICED.js} +85 -85
- package/dist/cli/{org-QQSCJJMM.js → org-7CJS35TF.js} +60 -60
- package/dist/cli/{org-6NUUN2P7.mjs → org-RJYSDJ3W.mjs} +4 -4
- package/dist/cli/play-FUZYMPHF.js +153 -0
- package/dist/cli/{play-3G2A53MD.mjs → play-H2QUTJR2.mjs} +7 -7
- package/dist/cli/{providers-IGD3ZYTA.mjs → providers-TEVRE4Q6.mjs} +4 -4
- package/dist/cli/{providers-AASOAAWT.js → providers-VUKRZWZY.js} +7 -7
- package/dist/cli/{quickstart-RRCLOSHQ.mjs → quickstart-2MIHVSZO.mjs} +3 -3
- package/dist/cli/{quickstart-NDUT5BVF.js → quickstart-36OGYQ2P.js} +5 -5
- package/dist/cli/search-DFSHYVZ5.js +26 -0
- package/dist/cli/{search-37MO75IS.mjs → search-IRRQSN6R.mjs} +5 -5
- package/dist/cli/{secrets-3OTOG64E.mjs → secrets-3W6OVDVE.mjs} +4 -4
- package/dist/cli/{secrets-QXY6DCJA.js → secrets-K67ZKZZM.js} +13 -13
- package/dist/cli/{sessions-FML35Y64.mjs → sessions-AOMOV4HG.mjs} +4 -4
- package/dist/cli/{sessions-3P6DOM3H.js → sessions-SLQ2QFLK.js} +5 -5
- package/dist/cli/{settings-SD6MDI67.js → settings-2X7H6O4I.js} +74 -74
- package/dist/cli/{settings-MWFWIGHW.mjs → settings-L4IFYF5D.mjs} +7 -7
- package/dist/cli/{setup-HY35HR26.js → setup-2RY5APQS.js} +41 -41
- package/dist/cli/{setup-HDYPYPFU.mjs → setup-CZYQTZ2K.mjs} +6 -6
- package/dist/cli/{skills-TGEDPYTP.mjs → skills-D2QQH5S7.mjs} +4 -4
- package/dist/cli/skills-RIFYRYWL.js +28 -0
- package/dist/cli/tools-M7LTH6ZT.js +40 -0
- package/dist/cli/{tools-VBARDH32.mjs → tools-TYIEROJJ.mjs} +6 -6
- package/dist/cli/{update-OIDLOWGB.mjs → update-7YCSUWFP.mjs} +6 -6
- package/dist/cli/update-HC62SVSQ.js +28 -0
- package/dist/cli/{workflow-K2SJUC5Z.js → workflow-42IFLYPN.js} +15 -15
- package/dist/cli/{workflow-ACHWLHPT.mjs → workflow-GGRUCLCI.mjs} +5 -5
- package/dist/{client-l-IoLh8Z.d.ts → client-DkoxT4wF.d.ts} +1 -1
- package/dist/{client-D5XOQKsA.d.mts → client-PDAfbylh.d.mts} +1 -1
- package/dist/{compiler-manifest-82q4jtfQ.d.ts → compiler-manifest-CButyeZ8.d.mts} +7 -0
- package/dist/{compiler-manifest-82q4jtfQ.d.mts → compiler-manifest-CButyeZ8.d.ts} +7 -0
- package/dist/{index-BPbW2XHF.d.mts → index-BMsNUoL5.d.mts} +1 -1
- package/dist/{index-k0rquHIm.d.ts → index-LDRzwo09.d.ts} +1 -1
- package/dist/index.d.mts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +6 -6
- package/dist/index.mjs +3 -3
- package/dist/install-integrity.json +80 -80
- package/dist/plays/bundle-play-file.d.mts +3 -3
- package/dist/plays/bundle-play-file.d.ts +3 -3
- 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-6SVPO5HS.js +0 -31
- package/dist/cli/client-BPQKKACG.js +0 -20
- package/dist/cli/git-ZZU4LXWX.js +0 -23
- package/dist/cli/play-LLC44DQW.js +0 -153
- package/dist/cli/search-JWUKJSNY.js +0 -26
- package/dist/cli/skills-T3F4A4BQ.js +0 -28
- package/dist/cli/tools-PXC5VIRU.js +0 -40
- package/dist/cli/update-PIT4RK74.js +0 -28
|
@@ -16,6 +16,7 @@ export interface PlayRunnerRuntimeDeadlineOptions {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export interface PlayRunnerRuntimeDeadlineHandle {
|
|
19
|
+
drainAtMs: number;
|
|
19
20
|
/** Claim normal settlement. False means the deadline already won. */
|
|
20
21
|
claimCompletion: () => boolean;
|
|
21
22
|
/** Settles only if the deadline callback unexpectedly returns or rejects. */
|
|
@@ -60,6 +61,10 @@ export function startPlayRunnerRuntimeDeadline(
|
|
|
60
61
|
options: PlayRunnerRuntimeDeadlineOptions,
|
|
61
62
|
): PlayRunnerRuntimeDeadlineHandle {
|
|
62
63
|
const limitMs = Math.max(1, Math.floor(options.limitMs));
|
|
64
|
+
const drainDelayMs = options.onDrain
|
|
65
|
+
? Math.max(1, limitMs - Math.min(options.drainLeadMs ?? 4_000, limitMs / 2))
|
|
66
|
+
: limitMs;
|
|
67
|
+
const drainAtMs = Date.now() + drainDelayMs;
|
|
63
68
|
const setTimeoutImpl =
|
|
64
69
|
options.setTimeoutImpl ??
|
|
65
70
|
((callback, delayMs) => setTimeout(callback, delayMs));
|
|
@@ -73,20 +78,14 @@ export function startPlayRunnerRuntimeDeadline(
|
|
|
73
78
|
settleDeadline = resolve;
|
|
74
79
|
});
|
|
75
80
|
const drainHandle = options.onDrain
|
|
76
|
-
? setTimeoutImpl(
|
|
77
|
-
()
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
},
|
|
85
|
-
Math.max(
|
|
86
|
-
1,
|
|
87
|
-
limitMs - Math.min(options.drainLeadMs ?? 4_000, limitMs / 2),
|
|
88
|
-
),
|
|
89
|
-
)
|
|
81
|
+
? setTimeoutImpl(() => {
|
|
82
|
+
if (fired || completionClaimed) return;
|
|
83
|
+
try {
|
|
84
|
+
options.onDrain?.();
|
|
85
|
+
} catch (error) {
|
|
86
|
+
options.onError?.(error);
|
|
87
|
+
}
|
|
88
|
+
}, drainDelayMs)
|
|
90
89
|
: null;
|
|
91
90
|
const handle = setTimeoutImpl(() => {
|
|
92
91
|
if (fired || completionClaimed) return;
|
|
@@ -99,6 +98,7 @@ export function startPlayRunnerRuntimeDeadline(
|
|
|
99
98
|
}, limitMs);
|
|
100
99
|
|
|
101
100
|
return {
|
|
101
|
+
drainAtMs,
|
|
102
102
|
claimCompletion: () => {
|
|
103
103
|
if (fired || completionClaimed) return false;
|
|
104
104
|
completionClaimed = true;
|
package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts
CHANGED
|
@@ -736,7 +736,7 @@ export class AbsurdExecutionOperationTimeoutError extends Error {
|
|
|
736
736
|
* park/resume seam without a real engine. */
|
|
737
737
|
export type AbsurdSuspensionContext = Pick<
|
|
738
738
|
TaskContext,
|
|
739
|
-
'sleepFor' | 'awaitEvent' | 'heartbeat'
|
|
739
|
+
'sleepFor' | 'sleepUntil' | 'awaitEvent' | 'heartbeat'
|
|
740
740
|
>;
|
|
741
741
|
|
|
742
742
|
/** Injected DB + app-runtime seams so the handler is testable against real SQL
|
|
@@ -1393,10 +1393,17 @@ export async function parkAndResolveSuspension(input: {
|
|
|
1393
1393
|
|
|
1394
1394
|
if (suspension.kind === 'sleep') {
|
|
1395
1395
|
// Throws SuspendTask on the first leg; returns once the deadline elapsed.
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1396
|
+
if (suspension.wakeAtMs !== undefined) {
|
|
1397
|
+
await ctx.sleepUntil(
|
|
1398
|
+
suspension.boundaryId,
|
|
1399
|
+
new Date(suspension.wakeAtMs),
|
|
1400
|
+
);
|
|
1401
|
+
} else {
|
|
1402
|
+
await ctx.sleepFor(
|
|
1403
|
+
suspension.boundaryId,
|
|
1404
|
+
Math.max(0, suspension.delayMs) / 1000,
|
|
1405
|
+
);
|
|
1406
|
+
}
|
|
1400
1407
|
return absurdCheckpointWithSleep(
|
|
1401
1408
|
checkpoint,
|
|
1402
1409
|
suspension.boundaryId,
|
|
@@ -818,7 +818,12 @@ export function absurdCheckpointWithSleep(
|
|
|
818
818
|
...base,
|
|
819
819
|
resolvedBoundaries: {
|
|
820
820
|
...resolvedBoundaries(base),
|
|
821
|
-
[boundaryId]: {
|
|
821
|
+
[boundaryId]: {
|
|
822
|
+
...resolvedBoundaries(base)[boundaryId],
|
|
823
|
+
kind: 'sleep',
|
|
824
|
+
delayMs,
|
|
825
|
+
completedAt: Date.now(),
|
|
826
|
+
},
|
|
822
827
|
},
|
|
823
828
|
};
|
|
824
829
|
}
|
package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-scope.ts
CHANGED
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
* learn from copies of the same provider feedback. These CTEs are shared by
|
|
5
5
|
* pure-rate and concurrency-bearing admission, in the same atomic grant SQL.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
DEFAULT_UNKNOWN_PROVIDER_FAST_RECOVERY_RPS,
|
|
9
|
+
DEFAULT_UNKNOWN_PROVIDER_MIN_RPS,
|
|
10
|
+
} from '../governor/rate-state-backend';
|
|
8
11
|
|
|
9
12
|
const AIMD_RECOVERY_STEP_MS = 1_000;
|
|
10
13
|
// Increase relative to the learned rate, not by a fixed RPS amount: a long
|
|
@@ -14,15 +17,17 @@ const AIMD_RECOVERY_STEP_MS = 1_000;
|
|
|
14
17
|
const AIMD_ADDITIVE_STEP_FRACTION = 0.25;
|
|
15
18
|
// Keep upward probes small enough to retain headroom after a productive
|
|
16
19
|
// decrease. Sparse successes use the earlier observation window; sustained
|
|
17
|
-
// useful work can establish another small probe
|
|
20
|
+
// useful work at nearly the current target can establish another small probe
|
|
21
|
+
// each second. Demand or historical successes cannot certify the next target.
|
|
18
22
|
const UNKNOWN_PROVIDER_PROBE_STEP_RPS = 0.1;
|
|
19
23
|
const UNKNOWN_PROVIDER_PROBE_INTERVAL_CAP_MS = 5_000;
|
|
20
|
-
const UNKNOWN_PROVIDER_PRODUCTIVE_RATE_FRACTION = 0.
|
|
24
|
+
const UNKNOWN_PROVIDER_PRODUCTIVE_RATE_FRACTION = 0.95;
|
|
21
25
|
export function acquireRateScopeSql(
|
|
22
26
|
table: string,
|
|
23
27
|
successesParameter: string,
|
|
24
28
|
minimumRateSql = 'NULL::float8',
|
|
25
29
|
unknownProvider = false,
|
|
30
|
+
probeBaselineSupported = true,
|
|
26
31
|
): string {
|
|
27
32
|
const refillRate = `CASE WHEN ${minimumRateSql} IS NULL
|
|
28
33
|
THEN h.refill_per_sec
|
|
@@ -35,11 +40,29 @@ export function acquireRateScopeSql(
|
|
|
35
40
|
// Success counts are already applied once by the feedback writer. Measure
|
|
36
41
|
// this post-hold epoch, excluding Retry-After and earlier idle history. A
|
|
37
42
|
// productive second justifies a small new probe; a lone completion does not.
|
|
43
|
+
const probeStartedAtMs = probeBaselineSupported
|
|
44
|
+
? 'GREATEST(h.rate_probe_started_at_ms, h.rate_observation_started_at_ms)'
|
|
45
|
+
: 'h.rate_observation_started_at_ms';
|
|
46
|
+
const probeSuccesses = probeBaselineSupported
|
|
47
|
+
? 'GREATEST(0, h.rate_successes - h.rate_probe_successes_at_ramp)'
|
|
48
|
+
: 'h.rate_successes';
|
|
49
|
+
const productiveRateFraction = probeBaselineSupported
|
|
50
|
+
? UNKNOWN_PROVIDER_PRODUCTIVE_RATE_FRACTION
|
|
51
|
+
: 0.8;
|
|
52
|
+
const storedRecoveryInterval = `LEAST(${UNKNOWN_PROVIDER_PROBE_INTERVAL_CAP_MS}, GREATEST(${AIMD_RECOVERY_STEP_MS}, h.rate_recovery_interval_ms))`;
|
|
53
|
+
const sparseRecoveryInterval = probeBaselineSupported
|
|
54
|
+
? `CASE WHEN ${refillRate} > ${DEFAULT_UNKNOWN_PROVIDER_FAST_RECOVERY_RPS}
|
|
55
|
+
THEN ${UNKNOWN_PROVIDER_PROBE_INTERVAL_CAP_MS} ELSE ${storedRecoveryInterval} END`
|
|
56
|
+
: storedRecoveryInterval;
|
|
57
|
+
const floorRecoveryInterval = probeBaselineSupported
|
|
58
|
+
? `GREATEST(${AIMD_RECOVERY_STEP_MS}, h.rate_recovery_interval_ms)`
|
|
59
|
+
: 'h.rate_recovery_interval_ms';
|
|
38
60
|
const productiveRecovery = `h.rate_observation_started_at_ms IS NOT NULL
|
|
39
|
-
AND
|
|
40
|
-
AND
|
|
41
|
-
|
|
42
|
-
|
|
61
|
+
${probeBaselineSupported ? 'AND h.rate_probe_started_at_ms >= h.last_ramp_at_ms AND h.rate_probe_started_at_ms >= h.rate_observation_started_at_ms' : ''}
|
|
62
|
+
AND nv.now_ms - ${probeStartedAtMs} >= ${AIMD_RECOVERY_STEP_MS}
|
|
63
|
+
AND ${probeSuccesses} * 1000.0
|
|
64
|
+
/ GREATEST(${AIMD_RECOVERY_STEP_MS}, nv.now_ms - ${probeStartedAtMs})
|
|
65
|
+
>= ${refillRate} * ${productiveRateFraction}`;
|
|
43
66
|
// Below useful seed capacity, a confirmed success can prove the next probe
|
|
44
67
|
// after one dispatch interval at the current rate. Keeping the floor's 500ms
|
|
45
68
|
// interval at 4/8 RPS waits through additional clean sends unnecessarily.
|
|
@@ -47,9 +70,29 @@ export function acquireRateScopeSql(
|
|
|
47
70
|
const backpressuredRecoveryInterval = unknownProvider
|
|
48
71
|
? `CASE WHEN ${refillRate} >= ${DEFAULT_UNKNOWN_PROVIDER_FAST_RECOVERY_RPS}
|
|
49
72
|
THEN CASE WHEN ${productiveRecovery} THEN ${AIMD_RECOVERY_STEP_MS}
|
|
50
|
-
ELSE
|
|
51
|
-
ELSE LEAST(
|
|
73
|
+
ELSE ${sparseRecoveryInterval} END
|
|
74
|
+
ELSE LEAST(${floorRecoveryInterval}, CEIL(1000.0 / ${refillRate})) END`
|
|
52
75
|
: `LEAST(h.rate_recovery_interval_ms, ${AIMD_RECOVERY_STEP_MS})`;
|
|
76
|
+
// A local no-progress pause remains active after the provider's shorter
|
|
77
|
+
// Retry-After. Only time beyond every outstanding dispatch/hold is idle.
|
|
78
|
+
const observationWasIdle = `${successesParameter}::int = 0
|
|
79
|
+
AND nv.now_ms - GREATEST(h.refilled_at_ms, h.next_dispatch_at_ms, h.retry_at_ms, h.cool_until_ms)
|
|
80
|
+
> GREATEST(1000, 2000.0 / ${refillRate})`;
|
|
81
|
+
// A denied refill may fold a completion before the next floor interval is
|
|
82
|
+
// due. Keep that fresh completion available for one later ramp rather than
|
|
83
|
+
// requiring another provider call at the old pace. Its baseline advances on
|
|
84
|
+
// every floor ramp, so time alone cannot reuse it for another increase.
|
|
85
|
+
const recoverySuccess =
|
|
86
|
+
unknownProvider && probeBaselineSupported
|
|
87
|
+
? `(${successesParameter}::int > 0 OR (
|
|
88
|
+
h.seen_backpressure
|
|
89
|
+
AND ${refillRate} < ${DEFAULT_UNKNOWN_PROVIDER_FAST_RECOVERY_RPS}
|
|
90
|
+
AND h.rate_probe_started_at_ms >= h.last_ramp_at_ms
|
|
91
|
+
AND h.rate_probe_started_at_ms >= h.rate_observation_started_at_ms
|
|
92
|
+
AND h.rate_successes > h.rate_probe_successes_at_ramp
|
|
93
|
+
AND NOT (${observationWasIdle})
|
|
94
|
+
))`
|
|
95
|
+
: `${successesParameter}::int > 0`;
|
|
53
96
|
return `provider_hold_rows AS (
|
|
54
97
|
SELECT * FROM ${table} WHERE bucket_id = $1 FOR UPDATE
|
|
55
98
|
), provider_hold AS (
|
|
@@ -58,12 +101,8 @@ export function acquireRateScopeSql(
|
|
|
58
101
|
GREATEST(CASE WHEN h.seen_backpressure THEN 1 ELSE h.capacity END, ${refillRate}),
|
|
59
102
|
h.tokens + GREATEST(0, nv.now_ms - h.refilled_at_ms) * ${refillRate} / 1000.0
|
|
60
103
|
) AS tokens_after_refill,
|
|
61
|
-
${
|
|
62
|
-
|
|
63
|
-
-- even after the provider's shorter Retry-After has elapsed.
|
|
64
|
-
AND nv.now_ms - GREATEST(h.refilled_at_ms, h.next_dispatch_at_ms, h.retry_at_ms, h.cool_until_ms)
|
|
65
|
-
> GREATEST(1000, 2000.0 / ${refillRate}) AS observation_was_idle,
|
|
66
|
-
CASE WHEN ${successesParameter}::int > 0
|
|
104
|
+
${observationWasIdle} AS observation_was_idle,
|
|
105
|
+
CASE WHEN ${recoverySuccess}
|
|
67
106
|
AND h.rate_observation_started_at_ms IS NOT NULL
|
|
68
107
|
AND nv.now_ms - h.last_ramp_at_ms >= CASE WHEN h.seen_backpressure
|
|
69
108
|
THEN ${backpressuredRecoveryInterval}
|
|
@@ -73,7 +112,7 @@ export function acquireRateScopeSql(
|
|
|
73
112
|
THEN ${recoveryRate}
|
|
74
113
|
ELSE ${refillRate} * 2 END)
|
|
75
114
|
ELSE ${refillRate} END AS next_refill_per_sec,
|
|
76
|
-
CASE WHEN ${
|
|
115
|
+
CASE WHEN ${recoverySuccess}
|
|
77
116
|
AND h.rate_observation_started_at_ms IS NOT NULL
|
|
78
117
|
AND nv.now_ms - h.last_ramp_at_ms >= CASE WHEN h.seen_backpressure
|
|
79
118
|
THEN ${backpressuredRecoveryInterval}
|
|
@@ -87,13 +126,24 @@ export function acquireRateScopeSql(
|
|
|
87
126
|
export function updateRateScopeSql(
|
|
88
127
|
table: string,
|
|
89
128
|
unknownProvider = false,
|
|
129
|
+
probeBaselineSupported = true,
|
|
90
130
|
): string {
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
131
|
+
// Keep the throttle epoch's productive evidence while measuring each upward
|
|
132
|
+
// probe from a new counter baseline. Losing the earlier useful work would
|
|
133
|
+
// turn an exploratory rejection into a zero-progress failure.
|
|
94
134
|
const recoveredObservation = `${unknownProvider} AND ph.seen_backpressure
|
|
95
135
|
AND ph.refill_per_sec < ${DEFAULT_UNKNOWN_PROVIDER_FAST_RECOVERY_RPS}
|
|
96
136
|
AND ph.next_refill_per_sec >= ${DEFAULT_UNKNOWN_PROVIDER_FAST_RECOVERY_RPS}`;
|
|
137
|
+
const probedObservation = `${unknownProvider} AND ph.seen_backpressure
|
|
138
|
+
AND ph.refill_per_sec >= ${DEFAULT_UNKNOWN_PROVIDER_FAST_RECOVERY_RPS}
|
|
139
|
+
AND ph.next_refill_per_sec > ph.refill_per_sec`;
|
|
140
|
+
const floorRampedObservation = `${unknownProvider} AND ph.seen_backpressure
|
|
141
|
+
AND ph.refill_per_sec < ${DEFAULT_UNKNOWN_PROVIDER_FAST_RECOVERY_RPS}
|
|
142
|
+
AND ph.next_refill_per_sec > ph.refill_per_sec`;
|
|
143
|
+
const idleFloorObservation = `ph.observation_was_idle
|
|
144
|
+
AND ph.refill_per_sec < ${DEFAULT_UNKNOWN_PROVIDER_FAST_RECOVERY_RPS}`;
|
|
145
|
+
const rebaseProbe =
|
|
146
|
+
'ph.rate_probe_started_at_ms < ph.last_ramp_at_ms OR ph.rate_probe_started_at_ms < COALESCE(ph.rate_observation_started_at_ms, 0) OR ph.rate_probe_successes_at_ramp > ph.rate_successes';
|
|
97
147
|
return `updated_scope AS (
|
|
98
148
|
UPDATE ${table} h SET
|
|
99
149
|
tokens = ph.tokens_after_refill - (SELECT granted FROM grant_calc),
|
|
@@ -112,13 +162,34 @@ export function updateRateScopeSql(
|
|
|
112
162
|
AND ph.rate_observation_started_at_ms IS NULL THEN 0
|
|
113
163
|
WHEN (SELECT granted FROM grant_calc) > 0 AND ph.observation_was_idle THEN 0
|
|
114
164
|
ELSE ph.rate_successes END,
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
165
|
+
${
|
|
166
|
+
unknownProvider && probeBaselineSupported
|
|
167
|
+
? `rate_probe_successes_at_ramp = CASE
|
|
168
|
+
WHEN ${recoveredObservation} THEN 0
|
|
169
|
+
WHEN (SELECT granted FROM grant_calc) > 0
|
|
170
|
+
AND (ph.rate_observation_started_at_ms IS NULL OR ph.observation_was_idle) THEN 0
|
|
171
|
+
WHEN ${idleFloorObservation} THEN ph.rate_successes
|
|
172
|
+
WHEN ${floorRampedObservation} OR ${probedObservation} OR (${rebaseProbe}) THEN ph.rate_successes
|
|
173
|
+
ELSE ph.rate_probe_successes_at_ramp END,
|
|
174
|
+
rate_probe_started_at_ms = CASE
|
|
175
|
+
WHEN ${idleFloorObservation} OR ${recoveredObservation} OR ${floorRampedObservation} OR ${probedObservation} OR (${rebaseProbe}) THEN nv.now_ms
|
|
176
|
+
WHEN (SELECT granted FROM grant_calc) > 0
|
|
177
|
+
AND (ph.rate_observation_started_at_ms IS NULL OR ph.observation_was_idle) THEN nv.now_ms
|
|
178
|
+
ELSE ph.rate_probe_started_at_ms END,`
|
|
179
|
+
: ''
|
|
180
|
+
}
|
|
181
|
+
-- At the floor, a new successful epoch must not inherit failed-probe
|
|
182
|
+
-- backoff. Above the floor, retain its useful measurement duration for
|
|
183
|
+
-- sparse upward probes; that rate already proves successful recovery.
|
|
118
184
|
rate_recovery_interval_ms = CASE
|
|
119
185
|
WHEN ${unknownProvider} AND (SELECT granted FROM grant_calc) > 0
|
|
120
|
-
AND ph.
|
|
121
|
-
|
|
186
|
+
AND ph.rate_successes > 0
|
|
187
|
+
AND ph.next_refill_per_sec <= ${DEFAULT_UNKNOWN_PROVIDER_MIN_RPS}
|
|
188
|
+
AND (ph.observation_was_idle
|
|
189
|
+
OR ph.rate_observation_started_at_ms IS NULL)
|
|
190
|
+
-- Zero is an unmeasured new episode, not a failed-probe interval.
|
|
191
|
+
-- A real failure establishes the interval before it can double.
|
|
192
|
+
THEN ${probeBaselineSupported ? 0 : AIMD_RECOVERY_STEP_MS}
|
|
122
193
|
ELSE ph.rate_recovery_interval_ms END,
|
|
123
194
|
rate_throttle_ceiling_rps = CASE
|
|
124
195
|
WHEN (SELECT granted FROM grant_calc) > 0
|
package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts
CHANGED
|
@@ -288,6 +288,36 @@ function isMissingRateThrottleCeilingColumnError(error: unknown): boolean {
|
|
|
288
288
|
);
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
+
/** Run the existing rate operation until the deploy-owned probe expansion lands. */
|
|
292
|
+
function isMissingProbeBaselineColumnError(error: unknown): boolean {
|
|
293
|
+
const candidate = error as { code?: unknown; message?: unknown } | null;
|
|
294
|
+
return (
|
|
295
|
+
candidate?.code === '42703' &&
|
|
296
|
+
typeof candidate.message === 'string' &&
|
|
297
|
+
/\b(rate_probe_successes_at_ramp|rate_probe_started_at_ms)\b/.test(
|
|
298
|
+
candidate.message,
|
|
299
|
+
)
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function queryRateStateWithProbeBaseline<
|
|
304
|
+
Row extends Record<string, unknown>,
|
|
305
|
+
>(
|
|
306
|
+
client: PostgresSchedulerQueryClient,
|
|
307
|
+
sql: (probeBaselineSupported: boolean) => string,
|
|
308
|
+
parameters: unknown[],
|
|
309
|
+
) {
|
|
310
|
+
try {
|
|
311
|
+
return await client.query<Row>(sql(true), parameters);
|
|
312
|
+
} catch (error) {
|
|
313
|
+
if (!isMissingProbeBaselineColumnError(error)) throw error;
|
|
314
|
+
// PostgreSQL rejects the statement before any grant or reservation writes.
|
|
315
|
+
// Retry only that statement; previously accepted success feedback is not
|
|
316
|
+
// applied twice. No DDL, migration preflight, or global schema floor.
|
|
317
|
+
return await client.query<Row>(sql(false), parameters);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
291
321
|
/**
|
|
292
322
|
* v43 added `rate_requesters` and the lease `requester_id` column. Only an
|
|
293
323
|
* acquire that carries a requester identity touches them, so during the
|
|
@@ -565,6 +595,8 @@ export async function ensurePostgresRateStateSchema(
|
|
|
565
595
|
ADD COLUMN IF NOT EXISTS last_ramp_at_ms bigint NOT NULL DEFAULT 0,
|
|
566
596
|
ADD COLUMN IF NOT EXISTS rate_observation_started_at_ms bigint,
|
|
567
597
|
ADD COLUMN IF NOT EXISTS rate_successes bigint NOT NULL DEFAULT 0,
|
|
598
|
+
ADD COLUMN IF NOT EXISTS rate_probe_successes_at_ramp bigint NOT NULL DEFAULT 0,
|
|
599
|
+
ADD COLUMN IF NOT EXISTS rate_probe_started_at_ms bigint NOT NULL DEFAULT 0,
|
|
568
600
|
ADD COLUMN IF NOT EXISTS rate_recovery_interval_ms bigint NOT NULL DEFAULT 1000,
|
|
569
601
|
ADD COLUMN IF NOT EXISTS rate_throttle_ceiling_rps float8
|
|
570
602
|
`);
|
|
@@ -916,6 +948,8 @@ type InternalAcquireOutcome = PlayRunnerRateStateAcquireResult & {
|
|
|
916
948
|
observedRuleRows: number;
|
|
917
949
|
/** True when a seed upsert would still change a stored row (config drift). */
|
|
918
950
|
configDrift: boolean;
|
|
951
|
+
/** Local monotonic receipt time; never crosses the runtime boundary. */
|
|
952
|
+
serverClockReceivedAtMs?: number;
|
|
919
953
|
};
|
|
920
954
|
|
|
921
955
|
function acquireResultOf(
|
|
@@ -924,9 +958,17 @@ function acquireResultOf(
|
|
|
924
958
|
const result: PlayRunnerRateStateAcquireResult & {
|
|
925
959
|
observedRuleRows?: number;
|
|
926
960
|
configDrift?: boolean;
|
|
961
|
+
serverClockReceivedAtMs?: number;
|
|
927
962
|
} = { ...outcome };
|
|
928
963
|
delete result.observedRuleRows;
|
|
929
964
|
delete result.configDrift;
|
|
965
|
+
delete result.serverClockReceivedAtMs;
|
|
966
|
+
if (outcome.serverClockReceivedAtMs != null) {
|
|
967
|
+
result.serverClockElapsedMs = Math.max(
|
|
968
|
+
0,
|
|
969
|
+
Math.floor(performance.now() - outcome.serverClockReceivedAtMs),
|
|
970
|
+
);
|
|
971
|
+
}
|
|
930
972
|
return result;
|
|
931
973
|
}
|
|
932
974
|
|
|
@@ -1307,7 +1349,7 @@ async function acquirePureRate(
|
|
|
1307
1349
|
concurrency: null,
|
|
1308
1350
|
})
|
|
1309
1351
|
: null;
|
|
1310
|
-
const result = await
|
|
1352
|
+
const result = await queryRateStateWithProbeBaseline<{
|
|
1311
1353
|
granted: string | number;
|
|
1312
1354
|
wait_ms: string | number;
|
|
1313
1355
|
observed_rule_rows: string | number;
|
|
@@ -1319,7 +1361,8 @@ async function acquirePureRate(
|
|
|
1319
1361
|
reservation_ids: string[] | null;
|
|
1320
1362
|
effective_rps: string | number;
|
|
1321
1363
|
}>(
|
|
1322
|
-
|
|
1364
|
+
client,
|
|
1365
|
+
(probeBaselineSupported) => `
|
|
1323
1366
|
WITH now_value AS (
|
|
1324
1367
|
SELECT COALESCE(
|
|
1325
1368
|
$3::bigint,
|
|
@@ -1346,6 +1389,7 @@ async function acquirePureRate(
|
|
|
1346
1389
|
'$5',
|
|
1347
1390
|
minimumRateSqlForRules(rules),
|
|
1348
1391
|
unknownProvider,
|
|
1392
|
+
probeBaselineSupported,
|
|
1349
1393
|
)},
|
|
1350
1394
|
reclaim_reservations AS (
|
|
1351
1395
|
DELETE FROM ${reservationTableName(input)}
|
|
@@ -1500,7 +1544,7 @@ async function acquirePureRate(
|
|
|
1500
1544
|
GREATEST(1, FLOOR(MIN(next_refill_per_sec) / 100.0))::bigint AS burst_size
|
|
1501
1545
|
FROM refreshed
|
|
1502
1546
|
),
|
|
1503
|
-
${updateRateScopeSql(scopeHoldTableName(input), unknownProvider)},
|
|
1547
|
+
${updateRateScopeSql(scopeHoldTableName(input), unknownProvider, probeBaselineSupported)},
|
|
1504
1548
|
updated AS (
|
|
1505
1549
|
UPDATE ${bucketTableName(input)} b
|
|
1506
1550
|
SET
|
|
@@ -1669,6 +1713,8 @@ async function acquirePureRate(
|
|
|
1669
1713
|
...(requesterId ? [requesterId] : []),
|
|
1670
1714
|
],
|
|
1671
1715
|
);
|
|
1716
|
+
const serverClockReceivedAtMs =
|
|
1717
|
+
input.nowMs == null ? performance.now() : undefined;
|
|
1672
1718
|
const row = result.rows[0];
|
|
1673
1719
|
if (!row) {
|
|
1674
1720
|
// Seeding raced away or rows vanished; the caller re-seeds and retries.
|
|
@@ -1709,6 +1755,7 @@ async function acquirePureRate(
|
|
|
1709
1755
|
granted,
|
|
1710
1756
|
waitMs,
|
|
1711
1757
|
serverNowMs,
|
|
1758
|
+
serverClockReceivedAtMs,
|
|
1712
1759
|
observedRuleRows,
|
|
1713
1760
|
configDrift,
|
|
1714
1761
|
leaseIds: [],
|
|
@@ -1741,29 +1788,39 @@ async function acquireWithConcurrency(
|
|
|
1741
1788
|
seeds: RateBucketSeed[],
|
|
1742
1789
|
requested: number,
|
|
1743
1790
|
): Promise<InternalAcquireOutcome> {
|
|
1744
|
-
|
|
1745
|
-
|
|
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) {
|
|
1791
|
+
let probeBaselineSupported = true;
|
|
1792
|
+
while (true) {
|
|
1793
|
+
await client.query('BEGIN ISOLATION LEVEL READ COMMITTED');
|
|
1761
1794
|
try {
|
|
1762
|
-
await client.query(
|
|
1763
|
-
|
|
1764
|
-
|
|
1795
|
+
await client.query(
|
|
1796
|
+
`SELECT bucket_id FROM ${scopeHoldTableName(input)}
|
|
1797
|
+
WHERE bucket_id = $1 FOR UPDATE`,
|
|
1798
|
+
[input.bucketId],
|
|
1799
|
+
);
|
|
1800
|
+
const outcome = await grantWithConcurrency(
|
|
1801
|
+
client,
|
|
1802
|
+
input,
|
|
1803
|
+
rules,
|
|
1804
|
+
seeds,
|
|
1805
|
+
requested,
|
|
1806
|
+
probeBaselineSupported,
|
|
1807
|
+
);
|
|
1808
|
+
await client.query('COMMIT');
|
|
1809
|
+
return outcome;
|
|
1810
|
+
} catch (error) {
|
|
1811
|
+
try {
|
|
1812
|
+
await client.query('ROLLBACK');
|
|
1813
|
+
} catch {
|
|
1814
|
+
// Preserve the original admission/storage error.
|
|
1815
|
+
}
|
|
1816
|
+
if (probeBaselineSupported && isMissingProbeBaselineColumnError(error)) {
|
|
1817
|
+
// The failed grant aborts its transaction. Reacquire the same scope lock
|
|
1818
|
+
// with a fresh snapshot; only the grant is repeated, never its feedback.
|
|
1819
|
+
probeBaselineSupported = false;
|
|
1820
|
+
continue;
|
|
1821
|
+
}
|
|
1822
|
+
throw error;
|
|
1765
1823
|
}
|
|
1766
|
-
throw error;
|
|
1767
1824
|
}
|
|
1768
1825
|
}
|
|
1769
1826
|
|
|
@@ -1773,6 +1830,7 @@ async function grantWithConcurrency(
|
|
|
1773
1830
|
rules: AcquirePacingRule[],
|
|
1774
1831
|
seeds: RateBucketSeed[],
|
|
1775
1832
|
requested: number,
|
|
1833
|
+
probeBaselineSupported: boolean,
|
|
1776
1834
|
): Promise<InternalAcquireOutcome> {
|
|
1777
1835
|
const ruleIds = rules.map((rule) => rule.ruleId);
|
|
1778
1836
|
const unknownProvider = rules.some(
|
|
@@ -1830,6 +1888,7 @@ async function grantWithConcurrency(
|
|
|
1830
1888
|
'$10',
|
|
1831
1889
|
minimumRateSqlForRules(rules),
|
|
1832
1890
|
unknownProvider,
|
|
1891
|
+
probeBaselineSupported,
|
|
1833
1892
|
)},
|
|
1834
1893
|
reclaim_reservations AS (
|
|
1835
1894
|
DELETE FROM ${reservationTableName(input)}
|
|
@@ -2068,7 +2127,7 @@ async function grantWithConcurrency(
|
|
|
2068
2127
|
GREATEST(1, FLOOR(MIN(next_refill_per_sec) / 100.0))::bigint AS burst_size
|
|
2069
2128
|
FROM refreshed
|
|
2070
2129
|
),
|
|
2071
|
-
${updateRateScopeSql(scopeHoldTableName(input), unknownProvider)},
|
|
2130
|
+
${updateRateScopeSql(scopeHoldTableName(input), unknownProvider, probeBaselineSupported)},
|
|
2072
2131
|
updated AS (
|
|
2073
2132
|
UPDATE ${bucketTableName(input)} b
|
|
2074
2133
|
SET
|
|
@@ -2283,6 +2342,8 @@ async function grantWithConcurrency(
|
|
|
2283
2342
|
...(requesterId ? [requesterId] : []),
|
|
2284
2343
|
],
|
|
2285
2344
|
);
|
|
2345
|
+
const serverClockReceivedAtMs =
|
|
2346
|
+
input.nowMs == null ? performance.now() : undefined;
|
|
2286
2347
|
const row = result.rows[0];
|
|
2287
2348
|
if (!row) {
|
|
2288
2349
|
return {
|
|
@@ -2327,6 +2388,7 @@ async function grantWithConcurrency(
|
|
|
2327
2388
|
granted,
|
|
2328
2389
|
waitMs,
|
|
2329
2390
|
serverNowMs,
|
|
2391
|
+
serverClockReceivedAtMs,
|
|
2330
2392
|
observedRuleRows,
|
|
2331
2393
|
configDrift,
|
|
2332
2394
|
leaseIds: granted > 0 ? leaseIds : [],
|
|
@@ -2410,9 +2472,15 @@ export async function validatePostgresRateStatePermit(
|
|
|
2410
2472
|
*/
|
|
2411
2473
|
export async function claimPostgresProviderDispatch(
|
|
2412
2474
|
client: PostgresSchedulerQueryClient,
|
|
2413
|
-
input: PlayRunnerRateStateValidateInput & {
|
|
2475
|
+
input: PlayRunnerRateStateValidateInput & {
|
|
2476
|
+
nowMs?: number;
|
|
2477
|
+
retryAtMsAtGrant?: number;
|
|
2478
|
+
},
|
|
2414
2479
|
): Promise<
|
|
2415
|
-
PlayRunnerRateStateValidateResult & {
|
|
2480
|
+
PlayRunnerRateStateValidateResult & {
|
|
2481
|
+
physicalCadenceWait: boolean;
|
|
2482
|
+
stalePermit?: boolean;
|
|
2483
|
+
}
|
|
2416
2484
|
> {
|
|
2417
2485
|
return withRateStateAvailabilityGuard(input, async () => {
|
|
2418
2486
|
const result = await client.query<{
|
|
@@ -2420,6 +2488,7 @@ export async function claimPostgresProviderDispatch(
|
|
|
2420
2488
|
retry_at_ms: string | number;
|
|
2421
2489
|
allowed: boolean;
|
|
2422
2490
|
cadence_wait: boolean;
|
|
2491
|
+
stale_permit: boolean;
|
|
2423
2492
|
}>(
|
|
2424
2493
|
`
|
|
2425
2494
|
WITH now_value AS (
|
|
@@ -2438,6 +2507,7 @@ export async function claimPostgresProviderDispatch(
|
|
|
2438
2507
|
GREATEST(COALESCE(s.retry_at_ms, 0), COALESCE(s.cool_until_ms, 0)) AS hold_until_ms,
|
|
2439
2508
|
s.physical_rps,
|
|
2440
2509
|
COALESCE(s.seen_backpressure, false) AS preserve_cadence_phase,
|
|
2510
|
+
COALESCE(s.retry_at_ms > $3::bigint, false) AS stale_permit,
|
|
2441
2511
|
GREATEST(1, FLOOR(s.physical_rps / 100))::int AS burst_size,
|
|
2442
2512
|
GREATEST(1, CEIL(1000.0 / s.physical_rps))::bigint AS spacing_ms
|
|
2443
2513
|
FROM now_value nv LEFT JOIN scope_rows s ON true
|
|
@@ -2453,6 +2523,7 @@ export async function claimPostgresProviderDispatch(
|
|
|
2453
2523
|
c.now_ms + c.spacing_ms * c.burst_size, 0
|
|
2454
2524
|
FROM cadence c
|
|
2455
2525
|
WHERE c.hold_until_ms <= c.now_ms AND c.physical_rps > 0
|
|
2526
|
+
AND NOT c.stale_permit
|
|
2456
2527
|
ON CONFLICT (bucket_id, rule_id) DO UPDATE SET
|
|
2457
2528
|
-- Always return the locked current cursor, including a denial.
|
|
2458
2529
|
-- A snapshot read after a blocked upsert could miss a concurrent
|
|
@@ -2466,9 +2537,16 @@ export async function claimPostgresProviderDispatch(
|
|
|
2466
2537
|
cool_until_ms = CASE
|
|
2467
2538
|
WHEN existing.next_dispatch_at_ms <= EXCLUDED.refilled_at_ms
|
|
2468
2539
|
OR LEAST(existing.tokens, EXCLUDED.capacity) >= 1 THEN 0
|
|
2469
|
-
ELSE
|
|
2470
|
-
|
|
2471
|
-
|
|
2540
|
+
ELSE CASE WHEN EXCLUDED.refill_per_sec > existing.refill_per_sec
|
|
2541
|
+
THEN LEAST(existing.next_dispatch_at_ms,
|
|
2542
|
+
GREATEST(existing.refilled_at_ms, EXCLUDED.refilled_at_ms)
|
|
2543
|
+
+ (EXCLUDED.next_dispatch_at_ms - EXCLUDED.refilled_at_ms))
|
|
2544
|
+
ELSE existing.next_dispatch_at_ms END END,
|
|
2545
|
+
-- Only a faster rate shortens existing debt. A contender can
|
|
2546
|
+
-- sample the clock before waiting behind a newer cursor grant;
|
|
2547
|
+
-- its denial must not rewind that grant at the same pace. Anchor
|
|
2548
|
+
-- recovery at the latest cursor clock, then persist its deadline
|
|
2549
|
+
-- so repeated probes converge without consuming another slot.
|
|
2472
2550
|
next_dispatch_at_ms = CASE
|
|
2473
2551
|
WHEN existing.next_dispatch_at_ms <= EXCLUDED.refilled_at_ms
|
|
2474
2552
|
-- Keep the cadence phase when a request reaches its slot
|
|
@@ -2487,23 +2565,30 @@ export async function claimPostgresProviderDispatch(
|
|
|
2487
2565
|
-- Cold discovery retains strict spacing until provider
|
|
2488
2566
|
-- backpressure establishes an adaptive cadence.
|
|
2489
2567
|
ELSE EXCLUDED.next_dispatch_at_ms END
|
|
2490
|
-
ELSE
|
|
2568
|
+
ELSE CASE WHEN EXCLUDED.refill_per_sec > existing.refill_per_sec
|
|
2569
|
+
THEN LEAST(existing.next_dispatch_at_ms,
|
|
2570
|
+
GREATEST(existing.refilled_at_ms, EXCLUDED.refilled_at_ms)
|
|
2571
|
+
+ (EXCLUDED.next_dispatch_at_ms - EXCLUDED.refilled_at_ms))
|
|
2572
|
+
ELSE existing.next_dispatch_at_ms END END,
|
|
2491
2573
|
capacity = EXCLUDED.capacity,
|
|
2492
2574
|
refill_per_sec = EXCLUDED.refill_per_sec,
|
|
2493
2575
|
base_refill_per_sec = EXCLUDED.base_refill_per_sec,
|
|
2494
2576
|
max_rps = EXCLUDED.max_rps,
|
|
2495
|
-
refilled_at_ms = EXCLUDED.refilled_at_ms
|
|
2577
|
+
refilled_at_ms = GREATEST(existing.refilled_at_ms, EXCLUDED.refilled_at_ms)
|
|
2496
2578
|
RETURNING cool_until_ms
|
|
2497
2579
|
)
|
|
2498
2580
|
SELECT c.now_ms AS server_now_ms,
|
|
2499
|
-
GREATEST(c.hold_until_ms, COALESCE(cursor.cool_until_ms, 0)
|
|
2500
|
-
|
|
2581
|
+
GREATEST(c.hold_until_ms, COALESCE(cursor.cool_until_ms, 0),
|
|
2582
|
+
CASE WHEN c.stale_permit THEN c.now_ms ELSE 0 END) AS retry_at_ms,
|
|
2583
|
+
NOT c.stale_permit AND c.hold_until_ms <= c.now_ms AND (
|
|
2501
2584
|
c.physical_rps IS NULL OR cursor.cool_until_ms = 0
|
|
2502
2585
|
) AS allowed,
|
|
2503
|
-
|
|
2586
|
+
NOT c.stale_permit AND c.hold_until_ms <= c.now_ms
|
|
2587
|
+
AND COALESCE(cursor.cool_until_ms > c.now_ms, false) AS cadence_wait,
|
|
2588
|
+
c.stale_permit
|
|
2504
2589
|
FROM cadence c LEFT JOIN cursor ON true
|
|
2505
2590
|
`,
|
|
2506
|
-
[input.bucketId, input.nowMs ?? null],
|
|
2591
|
+
[input.bucketId, input.nowMs ?? null, input.retryAtMsAtGrant ?? null],
|
|
2507
2592
|
);
|
|
2508
2593
|
const row = result.rows[0];
|
|
2509
2594
|
if (!row || typeof row.allowed !== 'boolean') {
|
|
@@ -2516,6 +2601,9 @@ export async function claimPostgresProviderDispatch(
|
|
|
2516
2601
|
physicalCadenceWait: row.cadence_wait,
|
|
2517
2602
|
serverNowMs: Number(row.server_now_ms),
|
|
2518
2603
|
retryAtMs: Number(row.retry_at_ms),
|
|
2604
|
+
...(input.retryAtMsAtGrant !== undefined
|
|
2605
|
+
? { stalePermit: row.stale_permit }
|
|
2606
|
+
: {}),
|
|
2519
2607
|
};
|
|
2520
2608
|
});
|
|
2521
2609
|
}
|
|
@@ -3362,7 +3450,13 @@ async function penalizeRateStateStatements(
|
|
|
3362
3450
|
-- no successes justifies the shared exponential pause.
|
|
3363
3451
|
WHEN h.rate_successes <= 0
|
|
3364
3452
|
AND ($5::bigint IS NOT NULL OR h.seen_backpressure)
|
|
3365
|
-
|
|
3453
|
+
-- An unknown scope can only recover above its floor after
|
|
3454
|
+
-- successful work. A cleared counter at that new epoch does
|
|
3455
|
+
-- not turn its measurement duration into consecutive failures.
|
|
3456
|
+
AND (NOT h.unknown_provider
|
|
3457
|
+
OR h.refill_per_sec <= ${DEFAULT_UNKNOWN_PROVIDER_MIN_RPS})
|
|
3458
|
+
AND (NOT h.unknown_provider OR (h.seen_backpressure
|
|
3459
|
+
AND h.rate_recovery_interval_ms > 0)) THEN
|
|
3366
3460
|
CASE WHEN h.seen_backpressure THEN
|
|
3367
3461
|
LEAST(60000, GREATEST(1000, h.rate_recovery_interval_ms * 2))
|
|
3368
3462
|
ELSE 1000 END
|
|
@@ -3392,6 +3486,7 @@ async function penalizeRateStateStatements(
|
|
|
3392
3486
|
SELECT e.*,
|
|
3393
3487
|
CASE WHEN e.new_episode THEN GREATEST(
|
|
3394
3488
|
CASE WHEN e.seen_backpressure AND e.rate_successes = 0
|
|
3489
|
+
AND e.no_progress_probe_interval_ms IS NOT NULL
|
|
3395
3490
|
THEN ${noProgressProbeRateRps}
|
|
3396
3491
|
ELSE CASE WHEN e.unknown_provider
|
|
3397
3492
|
THEN ${DEFAULT_UNKNOWN_PROVIDER_MIN_RPS} ELSE ${minimumRateRps} END END,
|