deepline 0.3.201 → 0.3.203
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/client.ts +62 -1
- package/dist/bundling-sources/sdk/src/http.ts +55 -6
- package/dist/bundling-sources/sdk/src/index.ts +1 -0
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/sdk/src/types.ts +8 -0
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +4 -2
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +460 -96
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +11 -1
- package/dist/bundling-sources/shared_libs/play-runtime/execution-gateway-invocation-fence.ts +6 -2
- package/dist/bundling-sources/shared_libs/play-runtime/fixture-behavior.ts +401 -2
- package/dist/bundling-sources/shared_libs/play-runtime/output-size-limits.ts +458 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-no-progress.ts +40 -0
- package/dist/bundling-sources/shared_libs/play-runtime/receipt-sql.ts +7 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +25 -3
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +119 -171
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-work-counters.ts +37 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-pool.ts +238 -204
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts +16 -7
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +136 -25
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-gateway.ts +21 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-receipt-value-storage.ts +64 -15
- package/dist/bundling-sources/shared_libs/play-runtime/sheet-attempt-sql.ts +5 -5
- package/dist/bundling-sources/shared_libs/play-runtime/tool-call/index.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/tool-call/result-receipt-policy.ts +46 -0
- package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +14 -5
- package/dist/bundling-sources/shared_libs/play-runtime/tool-http-errors.ts +5 -1
- package/dist/bundling-sources/shared_libs/play-runtime/work-receipt-state-machine.ts +3 -9
- package/dist/bundling-sources/shared_libs/product-notifications/catalog.ts +1 -1
- package/dist/cli/index.js +362 -55
- package/dist/cli/index.mjs +365 -57
- package/dist/index.d.mts +129 -1
- package/dist/index.d.ts +129 -1
- package/dist/index.js +64 -7
- package/dist/index.mjs +64 -7
- package/dist/install-integrity.json +3 -0
- package/dist/release.d.mts +1 -1
- package/dist/release.d.ts +1 -1
- package/dist/release.js +1 -1
- package/dist/release.mjs +1 -1
- package/package.json +1 -1
|
@@ -307,7 +307,7 @@ type BreakerState = {
|
|
|
307
307
|
consecutiveFailures: number;
|
|
308
308
|
openedAt: number | null;
|
|
309
309
|
lastFailureMessage: string;
|
|
310
|
-
|
|
310
|
+
halfOpenProbe: symbol | null;
|
|
311
311
|
};
|
|
312
312
|
|
|
313
313
|
export type RuntimeSchedulerPoolPriority = 'ordinary' | 'critical';
|
|
@@ -645,6 +645,9 @@ function getPoolEntry(options: RuntimeSchedulerPoolOptions): PoolEntry {
|
|
|
645
645
|
maxAcquireQueueDepth,
|
|
646
646
|
maxAcquireQueueWaitMs,
|
|
647
647
|
);
|
|
648
|
+
// A pool created by the old breaker used a boolean claim. The new token
|
|
649
|
+
// does not inherit a stranded half-open claim across a hot reload.
|
|
650
|
+
existing.breaker.halfOpenProbe ??= null;
|
|
648
651
|
return existing;
|
|
649
652
|
}
|
|
650
653
|
|
|
@@ -671,7 +674,7 @@ function getPoolEntry(options: RuntimeSchedulerPoolOptions): PoolEntry {
|
|
|
671
674
|
consecutiveFailures: 0,
|
|
672
675
|
openedAt: null,
|
|
673
676
|
lastFailureMessage: '',
|
|
674
|
-
|
|
677
|
+
halfOpenProbe: null,
|
|
675
678
|
},
|
|
676
679
|
stats: {
|
|
677
680
|
acquires: 0,
|
|
@@ -698,31 +701,39 @@ function getPoolEntry(options: RuntimeSchedulerPoolOptions): PoolEntry {
|
|
|
698
701
|
return entry;
|
|
699
702
|
}
|
|
700
703
|
|
|
701
|
-
function breakerGate(entry: PoolEntry):
|
|
704
|
+
function breakerGate(entry: PoolEntry): symbol | null {
|
|
702
705
|
const { breaker } = entry;
|
|
703
|
-
if (breaker.openedAt === null) return;
|
|
706
|
+
if (breaker.openedAt === null) return null;
|
|
704
707
|
const elapsed = Date.now() - breaker.openedAt;
|
|
705
708
|
if (elapsed < RUNTIME_SCHEDULER_POOL_DEFAULTS.breaker.openMs) {
|
|
706
709
|
entry.stats.breakerRejections += 1;
|
|
707
710
|
throw new RuntimeSchedulerCircuitOpenError(breaker.lastFailureMessage);
|
|
708
711
|
}
|
|
709
712
|
// Half-open: let exactly one caller through as the trial.
|
|
710
|
-
if (breaker.
|
|
713
|
+
if (breaker.halfOpenProbe !== null) {
|
|
711
714
|
entry.stats.breakerRejections += 1;
|
|
712
715
|
throw new RuntimeSchedulerCircuitOpenError(breaker.lastFailureMessage);
|
|
713
716
|
}
|
|
714
|
-
|
|
717
|
+
const probe = Symbol('runtime-scheduler-half-open-probe');
|
|
718
|
+
breaker.halfOpenProbe = probe;
|
|
719
|
+
return probe;
|
|
715
720
|
}
|
|
716
721
|
|
|
717
722
|
function recordBreakerSuccess(entry: PoolEntry): void {
|
|
718
723
|
entry.breaker.consecutiveFailures = 0;
|
|
719
724
|
entry.breaker.openedAt = null;
|
|
720
|
-
entry.breaker.
|
|
725
|
+
entry.breaker.halfOpenProbe = null;
|
|
721
726
|
}
|
|
722
727
|
|
|
723
|
-
function recordBreakerFailure(
|
|
728
|
+
function recordBreakerFailure(
|
|
729
|
+
entry: PoolEntry,
|
|
730
|
+
error: unknown,
|
|
731
|
+
probe: symbol | null,
|
|
732
|
+
): void {
|
|
724
733
|
const { breaker } = entry;
|
|
725
|
-
breaker.
|
|
734
|
+
if (probe !== null && breaker.halfOpenProbe === probe) {
|
|
735
|
+
breaker.halfOpenProbe = null;
|
|
736
|
+
}
|
|
726
737
|
breaker.consecutiveFailures += 1;
|
|
727
738
|
breaker.lastFailureMessage =
|
|
728
739
|
error instanceof Error ? error.message : String(error);
|
|
@@ -798,190 +809,46 @@ export async function withRuntimeSchedulerClient<T>(
|
|
|
798
809
|
throwIfSchedulerAborted(signal);
|
|
799
810
|
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
800
811
|
throwIfSchedulerAborted(signal);
|
|
801
|
-
breakerGate(entry);
|
|
802
|
-
|
|
803
|
-
entry.stats.saturationRejections += 1;
|
|
804
|
-
throw new RuntimeSchedulerSaturatedError(entry.pool.waitingCount);
|
|
805
|
-
}
|
|
806
|
-
const acquireStart = performance.now();
|
|
807
|
-
entry.stats.peakTotalClients = Math.max(
|
|
808
|
-
entry.stats.peakTotalClients,
|
|
809
|
-
entry.pool.totalCount,
|
|
810
|
-
);
|
|
811
|
-
entry.stats.peakWaitingClients = Math.max(
|
|
812
|
-
entry.stats.peakWaitingClients,
|
|
813
|
-
entry.pool.waitingCount,
|
|
814
|
-
);
|
|
815
|
-
let poolClient;
|
|
812
|
+
const halfOpenProbe = breakerGate(entry);
|
|
813
|
+
let capacityObserved = false;
|
|
816
814
|
try {
|
|
817
|
-
|
|
818
|
-
} catch (error) {
|
|
819
|
-
if (isRuntimeSchedulerLocalAcquireTimeout(error)) {
|
|
815
|
+
if (entry.pool.waitingCount >= entry.maxAcquireQueueDepth) {
|
|
820
816
|
entry.stats.saturationRejections += 1;
|
|
821
|
-
console.warn('[runtime-scheduler-pool.local_acquire_saturated]', {
|
|
822
|
-
maxClients: entry.maxClients,
|
|
823
|
-
totalClients: entry.pool.totalCount,
|
|
824
|
-
idleClients: entry.pool.idleCount,
|
|
825
|
-
waitingClients: entry.pool.waitingCount,
|
|
826
|
-
});
|
|
827
817
|
throw new RuntimeSchedulerSaturatedError(entry.pool.waitingCount);
|
|
828
818
|
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
);
|
|
834
|
-
}
|
|
835
|
-
recordBreakerFailure(entry, error);
|
|
836
|
-
entry.stats.connectionErrors += 1;
|
|
837
|
-
lastError = error;
|
|
838
|
-
if (attempt + 1 < maxAttempts) {
|
|
839
|
-
entry.stats.retries += 1;
|
|
840
|
-
await sleep(jitteredBackoffMs(attempt));
|
|
841
|
-
throwIfSchedulerAborted(signal);
|
|
842
|
-
continue;
|
|
843
|
-
}
|
|
844
|
-
throw error;
|
|
845
|
-
}
|
|
846
|
-
entry.stats.acquires += 1;
|
|
847
|
-
const acquireWaitMs = performance.now() - acquireStart;
|
|
848
|
-
entry.stats.peakTotalClients = Math.max(
|
|
849
|
-
entry.stats.peakTotalClients,
|
|
850
|
-
entry.pool.totalCount,
|
|
851
|
-
);
|
|
852
|
-
pushSample(entry.stats.acquireWait, acquireWaitMs);
|
|
853
|
-
|
|
854
|
-
// A checked-out pg client emits 'error' (not via the pool) when its TCP
|
|
855
|
-
// connection dies while idle — e.g. a worker holding a client across a
|
|
856
|
-
// long play execution. With no listener that is an uncaught exception
|
|
857
|
-
// that kills the process (observed: worker crash mid-150k-row run).
|
|
858
|
-
// Swallow it here; the next query on the dead client fails normally and
|
|
859
|
-
// flows through the connection-error retry/poison path below.
|
|
860
|
-
let checkedOutError: unknown = null;
|
|
861
|
-
const onCheckedOutError = (error: Error) => {
|
|
862
|
-
checkedOutError = error;
|
|
863
|
-
entry.stats.connectionErrors += 1;
|
|
864
|
-
console.warn('[runtime-scheduler-pool.checked_out_client_error]', {
|
|
865
|
-
error: error.message,
|
|
866
|
-
});
|
|
867
|
-
};
|
|
868
|
-
poolClient.on('error', onCheckedOutError);
|
|
869
|
-
let clientReleased = false;
|
|
870
|
-
const releasePoolClient = (error?: Error) => {
|
|
871
|
-
if (clientReleased) return;
|
|
872
|
-
clientReleased = true;
|
|
873
|
-
poolClient.release(error);
|
|
874
|
-
};
|
|
875
|
-
const onAbort = () => releasePoolClient(schedulerAbortReason(signal!));
|
|
876
|
-
signal?.addEventListener('abort', onAbort, { once: true });
|
|
877
|
-
|
|
878
|
-
let queryCount = 0;
|
|
879
|
-
let queryMs = 0;
|
|
880
|
-
try {
|
|
881
|
-
throwIfSchedulerAborted(signal);
|
|
882
|
-
const result = await fn({
|
|
883
|
-
query: async (sql, params) => {
|
|
884
|
-
throwIfSchedulerAborted(signal);
|
|
885
|
-
const queryStartedAt = performance.now();
|
|
886
|
-
queryCount += 1;
|
|
887
|
-
try {
|
|
888
|
-
try {
|
|
889
|
-
const queryResult = await poolClient.query(sql, params);
|
|
890
|
-
throwIfSchedulerAborted(signal);
|
|
891
|
-
return { rows: queryResult.rows };
|
|
892
|
-
} catch (error) {
|
|
893
|
-
// node-postgres' query timeout and an AbortSignal reject the
|
|
894
|
-
// promise, but the server may still be processing the SQL.
|
|
895
|
-
// Never return that socket to the pool: doing so lets the
|
|
896
|
-
// next retry overlap the first statement on the same backend.
|
|
897
|
-
if (isRuntimeSchedulerQueryCancellationError(error)) {
|
|
898
|
-
releasePoolClient(
|
|
899
|
-
error instanceof Error ? error : new Error(String(error)),
|
|
900
|
-
);
|
|
901
|
-
}
|
|
902
|
-
throw error;
|
|
903
|
-
}
|
|
904
|
-
} finally {
|
|
905
|
-
queryMs += performance.now() - queryStartedAt;
|
|
906
|
-
}
|
|
907
|
-
},
|
|
908
|
-
});
|
|
909
|
-
throwIfSchedulerAborted(signal);
|
|
910
|
-
recordBreakerSuccess(entry);
|
|
911
|
-
signal?.removeEventListener('abort', onAbort);
|
|
912
|
-
poolClient.removeListener('error', onCheckedOutError);
|
|
913
|
-
// The connection died mid-fn but fn happened to finish without another
|
|
914
|
-
// query — destroy the dead connection rather than pooling it.
|
|
915
|
-
releasePoolClient(
|
|
916
|
-
checkedOutError instanceof Error ? checkedOutError : undefined,
|
|
819
|
+
const acquireStart = performance.now();
|
|
820
|
+
entry.stats.peakTotalClients = Math.max(
|
|
821
|
+
entry.stats.peakTotalClients,
|
|
822
|
+
entry.pool.totalCount,
|
|
917
823
|
);
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
attempt: attempt + 1,
|
|
945
|
-
priority: trace.priority ?? 'ordinary',
|
|
946
|
-
admissionWaitMs,
|
|
947
|
-
acquireWaitMs,
|
|
948
|
-
queryCount,
|
|
949
|
-
queryMs,
|
|
950
|
-
totalMs: performance.now() - operationStartedAt,
|
|
951
|
-
errorName: error instanceof Error ? error.name : 'UnknownError',
|
|
952
|
-
}),
|
|
953
|
-
);
|
|
954
|
-
}
|
|
955
|
-
signal?.removeEventListener('abort', onAbort);
|
|
956
|
-
poolClient.removeListener('error', onCheckedOutError);
|
|
957
|
-
if (signal?.aborted) {
|
|
958
|
-
const cancellation = schedulerAbortReason(signal);
|
|
959
|
-
releasePoolClient(cancellation);
|
|
960
|
-
throw cancellation;
|
|
961
|
-
}
|
|
962
|
-
if (isPgBouncerCapacityError(error)) {
|
|
963
|
-
recordCapacityRejection(entry, error);
|
|
964
|
-
releasePoolClient();
|
|
965
|
-
throw new RuntimeSchedulerPgBouncerCapacityError(
|
|
966
|
-
error instanceof Error ? error.message : String(error),
|
|
967
|
-
);
|
|
968
|
-
}
|
|
969
|
-
if (isRuntimeSchedulerQueryCancellationError(error)) {
|
|
970
|
-
entry.stats.queryErrors += 1;
|
|
971
|
-
recordBreakerSuccess(entry);
|
|
972
|
-
releasePoolClient(
|
|
973
|
-
error instanceof Error ? error : new Error(String(error)),
|
|
974
|
-
);
|
|
975
|
-
throw error;
|
|
976
|
-
}
|
|
977
|
-
if (isConnectionCategoryError(error)) {
|
|
978
|
-
recordBreakerFailure(entry, error);
|
|
824
|
+
entry.stats.peakWaitingClients = Math.max(
|
|
825
|
+
entry.stats.peakWaitingClients,
|
|
826
|
+
entry.pool.waitingCount,
|
|
827
|
+
);
|
|
828
|
+
let poolClient;
|
|
829
|
+
try {
|
|
830
|
+
poolClient = await entry.pool.connect();
|
|
831
|
+
} catch (error) {
|
|
832
|
+
if (isRuntimeSchedulerLocalAcquireTimeout(error)) {
|
|
833
|
+
entry.stats.saturationRejections += 1;
|
|
834
|
+
console.warn('[runtime-scheduler-pool.local_acquire_saturated]', {
|
|
835
|
+
maxClients: entry.maxClients,
|
|
836
|
+
totalClients: entry.pool.totalCount,
|
|
837
|
+
idleClients: entry.pool.idleCount,
|
|
838
|
+
waitingClients: entry.pool.waitingCount,
|
|
839
|
+
});
|
|
840
|
+
throw new RuntimeSchedulerSaturatedError(entry.pool.waitingCount);
|
|
841
|
+
}
|
|
842
|
+
if (isPgBouncerCapacityError(error)) {
|
|
843
|
+
recordCapacityRejection(entry, error);
|
|
844
|
+
capacityObserved = true;
|
|
845
|
+
throw new RuntimeSchedulerPgBouncerCapacityError(
|
|
846
|
+
error instanceof Error ? error.message : String(error),
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
recordBreakerFailure(entry, error, halfOpenProbe);
|
|
979
850
|
entry.stats.connectionErrors += 1;
|
|
980
851
|
lastError = error;
|
|
981
|
-
// Destroy the broken connection instead of returning it to the pool.
|
|
982
|
-
releasePoolClient(
|
|
983
|
-
error instanceof Error ? error : new Error(String(error)),
|
|
984
|
-
);
|
|
985
852
|
if (attempt + 1 < maxAttempts) {
|
|
986
853
|
entry.stats.retries += 1;
|
|
987
854
|
await sleep(jitteredBackoffMs(attempt));
|
|
@@ -990,30 +857,197 @@ export async function withRuntimeSchedulerClient<T>(
|
|
|
990
857
|
}
|
|
991
858
|
throw error;
|
|
992
859
|
}
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
entry.
|
|
860
|
+
entry.stats.acquires += 1;
|
|
861
|
+
const acquireWaitMs = performance.now() - acquireStart;
|
|
862
|
+
entry.stats.peakTotalClients = Math.max(
|
|
863
|
+
entry.stats.peakTotalClients,
|
|
864
|
+
entry.pool.totalCount,
|
|
865
|
+
);
|
|
866
|
+
pushSample(entry.stats.acquireWait, acquireWaitMs);
|
|
867
|
+
|
|
868
|
+
// A checked-out pg client emits 'error' (not via the pool) when its TCP
|
|
869
|
+
// connection dies while idle — e.g. a worker holding a client across a
|
|
870
|
+
// long play execution. With no listener that is an uncaught exception
|
|
871
|
+
// that kills the process (observed: worker crash mid-150k-row run).
|
|
872
|
+
// Swallow it here; the next query on the dead client fails normally and
|
|
873
|
+
// flows through the connection-error retry/poison path below.
|
|
874
|
+
let checkedOutError: unknown = null;
|
|
875
|
+
const onCheckedOutError = (error: Error) => {
|
|
876
|
+
checkedOutError = error;
|
|
877
|
+
entry.stats.connectionErrors += 1;
|
|
878
|
+
console.warn('[runtime-scheduler-pool.checked_out_client_error]', {
|
|
879
|
+
error: error.message,
|
|
880
|
+
});
|
|
881
|
+
};
|
|
882
|
+
poolClient.on('error', onCheckedOutError);
|
|
883
|
+
let clientReleased = false;
|
|
884
|
+
const releasePoolClient = (error?: Error) => {
|
|
885
|
+
if (clientReleased) return;
|
|
886
|
+
clientReleased = true;
|
|
887
|
+
poolClient.release(error);
|
|
888
|
+
};
|
|
889
|
+
const onAbort = () => releasePoolClient(schedulerAbortReason(signal!));
|
|
890
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
891
|
+
|
|
892
|
+
let queryCount = 0;
|
|
893
|
+
let queryMs = 0;
|
|
894
|
+
try {
|
|
895
|
+
throwIfSchedulerAborted(signal);
|
|
896
|
+
const result = await fn({
|
|
897
|
+
query: async (sql, params) => {
|
|
898
|
+
throwIfSchedulerAborted(signal);
|
|
899
|
+
const queryStartedAt = performance.now();
|
|
900
|
+
queryCount += 1;
|
|
901
|
+
try {
|
|
902
|
+
try {
|
|
903
|
+
const queryResult = await poolClient.query(sql, params);
|
|
904
|
+
throwIfSchedulerAborted(signal);
|
|
905
|
+
return { rows: queryResult.rows };
|
|
906
|
+
} catch (error) {
|
|
907
|
+
// node-postgres' query timeout and an AbortSignal reject the
|
|
908
|
+
// promise, but the server may still be processing the SQL.
|
|
909
|
+
// Never return that socket to the pool: doing so lets the
|
|
910
|
+
// next retry overlap the first statement on the same backend.
|
|
911
|
+
if (isRuntimeSchedulerQueryCancellationError(error)) {
|
|
912
|
+
releasePoolClient(
|
|
913
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
throw error;
|
|
917
|
+
}
|
|
918
|
+
} finally {
|
|
919
|
+
queryMs += performance.now() - queryStartedAt;
|
|
920
|
+
}
|
|
921
|
+
},
|
|
922
|
+
});
|
|
923
|
+
throwIfSchedulerAborted(signal);
|
|
998
924
|
recordBreakerSuccess(entry);
|
|
999
|
-
|
|
925
|
+
signal?.removeEventListener('abort', onAbort);
|
|
926
|
+
poolClient.removeListener('error', onCheckedOutError);
|
|
927
|
+
// The connection died mid-fn but fn happened to finish without another
|
|
928
|
+
// query — destroy the dead connection rather than pooling it.
|
|
1000
929
|
releasePoolClient(
|
|
1001
|
-
|
|
930
|
+
checkedOutError instanceof Error ? checkedOutError : undefined,
|
|
1002
931
|
);
|
|
1003
|
-
if (
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
932
|
+
if (trace) {
|
|
933
|
+
console.info(
|
|
934
|
+
'[runtime-scheduler-pool.operation] ' +
|
|
935
|
+
JSON.stringify({
|
|
936
|
+
runId: trace.runId,
|
|
937
|
+
phase: trace.phase,
|
|
938
|
+
outcome: 'success',
|
|
939
|
+
attempt: attempt + 1,
|
|
940
|
+
priority: trace.priority ?? 'ordinary',
|
|
941
|
+
admissionWaitMs,
|
|
942
|
+
acquireWaitMs,
|
|
943
|
+
queryCount,
|
|
944
|
+
queryMs,
|
|
945
|
+
totalMs: performance.now() - operationStartedAt,
|
|
946
|
+
}),
|
|
947
|
+
);
|
|
1008
948
|
}
|
|
949
|
+
return result;
|
|
950
|
+
} catch (error) {
|
|
951
|
+
if (trace) {
|
|
952
|
+
console.info(
|
|
953
|
+
'[runtime-scheduler-pool.operation] ' +
|
|
954
|
+
JSON.stringify({
|
|
955
|
+
runId: trace.runId,
|
|
956
|
+
phase: trace.phase,
|
|
957
|
+
outcome: 'failed',
|
|
958
|
+
attempt: attempt + 1,
|
|
959
|
+
priority: trace.priority ?? 'ordinary',
|
|
960
|
+
admissionWaitMs,
|
|
961
|
+
acquireWaitMs,
|
|
962
|
+
queryCount,
|
|
963
|
+
queryMs,
|
|
964
|
+
totalMs: performance.now() - operationStartedAt,
|
|
965
|
+
errorName:
|
|
966
|
+
error instanceof Error ? error.name : 'UnknownError',
|
|
967
|
+
}),
|
|
968
|
+
);
|
|
969
|
+
}
|
|
970
|
+
signal?.removeEventListener('abort', onAbort);
|
|
971
|
+
poolClient.removeListener('error', onCheckedOutError);
|
|
972
|
+
if (signal?.aborted) {
|
|
973
|
+
const cancellation = schedulerAbortReason(signal);
|
|
974
|
+
releasePoolClient(cancellation);
|
|
975
|
+
throw cancellation;
|
|
976
|
+
}
|
|
977
|
+
if (isPgBouncerCapacityError(error)) {
|
|
978
|
+
recordCapacityRejection(entry, error);
|
|
979
|
+
capacityObserved = true;
|
|
980
|
+
releasePoolClient();
|
|
981
|
+
throw new RuntimeSchedulerPgBouncerCapacityError(
|
|
982
|
+
error instanceof Error ? error.message : String(error),
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
if (isRuntimeSchedulerQueryCancellationError(error)) {
|
|
986
|
+
entry.stats.queryErrors += 1;
|
|
987
|
+
recordBreakerSuccess(entry);
|
|
988
|
+
releasePoolClient(
|
|
989
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
990
|
+
);
|
|
991
|
+
throw error;
|
|
992
|
+
}
|
|
993
|
+
if (isConnectionCategoryError(error)) {
|
|
994
|
+
recordBreakerFailure(entry, error, halfOpenProbe);
|
|
995
|
+
entry.stats.connectionErrors += 1;
|
|
996
|
+
lastError = error;
|
|
997
|
+
// Destroy the broken connection instead of returning it to the pool.
|
|
998
|
+
releasePoolClient(
|
|
999
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
1000
|
+
);
|
|
1001
|
+
if (attempt + 1 < maxAttempts) {
|
|
1002
|
+
entry.stats.retries += 1;
|
|
1003
|
+
await sleep(jitteredBackoffMs(attempt));
|
|
1004
|
+
throwIfSchedulerAborted(signal);
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
1007
|
+
throw error;
|
|
1008
|
+
}
|
|
1009
|
+
if (isRuntimeSchedulerRetryableTransactionError(error)) {
|
|
1010
|
+
// PostgreSQL has aborted the transaction, so never return this
|
|
1011
|
+
// client to the pool. Re-run the idempotent scheduler operation on a
|
|
1012
|
+
// new session without opening the connection-failure breaker.
|
|
1013
|
+
entry.stats.queryErrors += 1;
|
|
1014
|
+
recordBreakerSuccess(entry);
|
|
1015
|
+
lastError = error;
|
|
1016
|
+
releasePoolClient(
|
|
1017
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
1018
|
+
);
|
|
1019
|
+
if (attempt + 1 < maxAttempts) {
|
|
1020
|
+
entry.stats.retries += 1;
|
|
1021
|
+
await sleep(jitteredBackoffMs(attempt));
|
|
1022
|
+
throwIfSchedulerAborted(signal);
|
|
1023
|
+
continue;
|
|
1024
|
+
}
|
|
1025
|
+
throw error;
|
|
1026
|
+
}
|
|
1027
|
+
// Query-level failure on a healthy connection: count it, return the
|
|
1028
|
+
// connection to the pool, surface the error without retry.
|
|
1029
|
+
entry.stats.queryErrors += 1;
|
|
1030
|
+
recordBreakerSuccess(entry);
|
|
1031
|
+
releasePoolClient();
|
|
1009
1032
|
throw error;
|
|
1010
1033
|
}
|
|
1011
|
-
|
|
1012
|
-
//
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1034
|
+
} finally {
|
|
1035
|
+
// A canceled, locally saturated, or capacity-rejected trial did not
|
|
1036
|
+
// call either breaker recorder. Never let it own the probe forever.
|
|
1037
|
+
// Only the trial that claimed this token may release it: another
|
|
1038
|
+
// operation can close and reopen the breaker while this one awaits.
|
|
1039
|
+
if (
|
|
1040
|
+
halfOpenProbe !== null &&
|
|
1041
|
+
entry.breaker.halfOpenProbe === halfOpenProbe
|
|
1042
|
+
) {
|
|
1043
|
+
entry.breaker.halfOpenProbe = null;
|
|
1044
|
+
if (capacityObserved && entry.breaker.openedAt !== null) {
|
|
1045
|
+
// Capacity is not a connection failure. Keep the existing open
|
|
1046
|
+
// state and try once more after the ordinary cooldown instead of
|
|
1047
|
+
// probing the still-full database on every request.
|
|
1048
|
+
entry.breaker.openedAt = Date.now();
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1017
1051
|
}
|
|
1018
1052
|
}
|
|
1019
1053
|
throw lastError instanceof Error
|
package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts
CHANGED
|
@@ -3080,6 +3080,13 @@ async function penalizeRateStateStatements(
|
|
|
3080
3080
|
GREATEST(h.retry_at_ms, COALESCE($5::bigint, nv.now_ms + $2::bigint))
|
|
3081
3081
|
- COALESCE(h.rate_observation_started_at_ms, nv.now_ms)
|
|
3082
3082
|
END) AS cycle_ms,
|
|
3083
|
+
CASE
|
|
3084
|
+
WHEN $5::bigint IS NOT NULL AND h.rate_successes <= 0 THEN
|
|
3085
|
+
CASE WHEN h.seen_backpressure THEN
|
|
3086
|
+
LEAST(60000, GREATEST(1000, h.rate_recovery_interval_ms * 2))
|
|
3087
|
+
ELSE 1000 END
|
|
3088
|
+
ELSE NULL
|
|
3089
|
+
END AS no_progress_probe_interval_ms,
|
|
3083
3090
|
nv.now_ms
|
|
3084
3091
|
FROM held h CROSS JOIN now_value nv
|
|
3085
3092
|
), throttle_ceiling AS (
|
|
@@ -3087,12 +3094,12 @@ async function penalizeRateStateStatements(
|
|
|
3087
3094
|
GREATEST(${MIN_ADAPTIVE_RPS_FLOOR},
|
|
3088
3095
|
CASE WHEN e.seen_backpressure AND e.rate_successes = 0 THEN
|
|
3089
3096
|
CASE WHEN $5::bigint IS NOT NULL
|
|
3090
|
-
--
|
|
3091
|
-
--
|
|
3092
|
-
--
|
|
3093
|
-
--
|
|
3094
|
-
--
|
|
3095
|
-
THEN
|
|
3097
|
+
-- A fresh provider deadline is not a throughput measurement.
|
|
3098
|
+
-- Repeated probes with no successful work are evidence that
|
|
3099
|
+
-- the scope is still unavailable, however. Space subsequent
|
|
3100
|
+
-- probes exponentially (up to one per minute) across every
|
|
3101
|
+
-- run sharing this provider credential.
|
|
3102
|
+
THEN 1000.0 / e.no_progress_probe_interval_ms
|
|
3096
3103
|
ELSE e.refill_before END
|
|
3097
3104
|
ELSE e.refill_before / 2 END
|
|
3098
3105
|
) AS next_ceiling_rps
|
|
@@ -3115,7 +3122,9 @@ async function penalizeRateStateStatements(
|
|
|
3115
3122
|
rate_throttle_ceiling_rps = CASE WHEN e.new_episode
|
|
3116
3123
|
THEN e.next_ceiling_rps ELSE e.rate_throttle_ceiling_rps END,
|
|
3117
3124
|
rate_observation_started_at_ms = CASE WHEN e.new_episode THEN NULL ELSE e.rate_observation_started_at_ms END,
|
|
3118
|
-
rate_recovery_interval_ms = CASE WHEN e.new_episode
|
|
3125
|
+
rate_recovery_interval_ms = CASE WHEN e.new_episode
|
|
3126
|
+
THEN COALESCE(e.no_progress_probe_interval_ms, e.cycle_ms)
|
|
3127
|
+
ELSE e.rate_recovery_interval_ms END,
|
|
3119
3128
|
last_ramp_at_ms = CASE WHEN e.new_episode THEN e.deadline ELSE e.last_ramp_at_ms END,
|
|
3120
3129
|
-- All pre-429 permits are revalidated against this durable hold
|
|
3121
3130
|
-- before dispatch. Their obsolete schedule debt must not add a
|