deepline 0.3.113 → 0.3.115
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 +3 -1
- package/dist/bundling-sources/sdk/src/compat.ts +167 -11
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/sdk/src/skills-version.ts +27 -1
- package/dist/bundling-sources/shared_libs/monitors/inbound-queue.ts +6 -2
- package/dist/bundling-sources/shared_libs/observability/dlq.ts +942 -0
- package/dist/bundling-sources/shared_libs/observability/queue-health.ts +361 -6
- package/dist/bundling-sources/shared_libs/observability/scheduled-jobs.json +24 -24
- package/dist/bundling-sources/shared_libs/observability/scheduled-work-manual.ts +2 -0
- package/dist/bundling-sources/shared_libs/observability/scheduled-work.ts +29 -1
- package/dist/bundling-sources/shared_libs/play-runtime/admission-transport-policy.ts +91 -0
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/direct-admission-deadline.ts +34 -0
- package/dist/bundling-sources/shared_libs/play-runtime/fixture-behavior.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/internal-bootstrap-token.ts +325 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-admission-outcome.ts +134 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/admission/contracts.ts +13 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/admission/index.ts +4 -1
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/admission/postgres-adapter.ts +13 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-webhook-admission-probe.ts +32 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-webhook-identity.ts +28 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +7 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-cron-fire-task.ts +100 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +313 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-shared.ts +111 -3
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd.ts +16 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-pool.ts +67 -8
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +1734 -147
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-ledger-outbox-recovery.ts +74 -13
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-queue-health.ts +245 -81
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/submit-admission.ts +79 -15
- package/dist/bundling-sources/shared_libs/play-runtime/webhook-receipt-retry.ts +21 -21
- package/dist/bundling-sources/shared_libs/plays/bundling/index.ts +4 -1
- package/dist/bundling-sources/shared_libs/product-notifications/catalog.ts +64 -0
- package/dist/bundling-sources/shared_libs/product-notifications/default-policy.ts +1 -1
- package/dist/bundling-sources/shared_libs/product-notifications/events.ts +20 -1
- package/dist/bundling-sources/shared_libs/security/receipt-admission-token.ts +224 -0
- package/dist/cli/index.js +574 -114
- package/dist/cli/index.mjs +592 -133
- package/dist/index.d.mts +54 -1
- package/dist/index.d.ts +54 -1
- package/dist/index.js +8 -3
- package/dist/index.mjs +8 -3
- package/dist/install-integrity.json +8 -0
- package/dist/plays/bundle-play-file.mjs +1 -1
- 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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** Scheduler-neutral queue/workset health contract. */
|
|
2
2
|
|
|
3
3
|
import { measuredValue, type Measured } from './scheduled-work-v2';
|
|
4
|
+
import type { Telemetry, TelemetryFields } from './telemetry';
|
|
4
5
|
export type { Measured } from './scheduled-work-v2';
|
|
5
6
|
export {
|
|
6
7
|
exact,
|
|
@@ -117,6 +118,15 @@ export const OPERATIONS_QUEUE_CATALOG = [
|
|
|
117
118
|
60_000,
|
|
118
119
|
180_000,
|
|
119
120
|
),
|
|
121
|
+
queue(
|
|
122
|
+
'compute-billing-jobs',
|
|
123
|
+
'Compute billing jobs',
|
|
124
|
+
'leased_workset',
|
|
125
|
+
'billing',
|
|
126
|
+
'Settle durable compute billing obligations after a runtime run ends',
|
|
127
|
+
300_000,
|
|
128
|
+
900_000,
|
|
129
|
+
),
|
|
120
130
|
queue(
|
|
121
131
|
'play-webhook-receipts',
|
|
122
132
|
'Play webhook receipts',
|
|
@@ -448,6 +458,7 @@ export type QueueHealth = {
|
|
|
448
458
|
|
|
449
459
|
export const QUEUE_HEALTH_TELEMETRY_EVENT = 'operations.queue.health' as const;
|
|
450
460
|
export const QUEUE_HEALTH_TELEMETRY_SCHEMA_VERSION = 1 as const;
|
|
461
|
+
const UNOBSERVED_SIGNAL_REPEAT_MS = 5 * 60_000;
|
|
451
462
|
|
|
452
463
|
export type QueueHealthSignal = {
|
|
453
464
|
readonly schemaVersion: typeof QUEUE_HEALTH_TELEMETRY_SCHEMA_VERSION;
|
|
@@ -478,9 +489,10 @@ export type QueueHealthSignal = {
|
|
|
478
489
|
* Project a durable observation into the one cross-runtime queue signal.
|
|
479
490
|
* Healthy steady state is deliberately silent. Actionable backlog problems
|
|
480
491
|
* repeat on every observation so a log-based monitor cannot resolve while the
|
|
481
|
-
* queue remains unhealthy
|
|
482
|
-
*
|
|
483
|
-
* healthy observation after a problem emits
|
|
492
|
+
* queue remains unhealthy. Persistent adapter absence is repeated on a
|
|
493
|
+
* five-minute boundary so a log-backed dashboard cannot go blank while an
|
|
494
|
+
* adapter remains absent. The first healthy observation after a problem emits
|
|
495
|
+
* recovery.
|
|
484
496
|
*/
|
|
485
497
|
export function deriveQueueHealthSignal(input: {
|
|
486
498
|
definition: QueueCatalogEntry;
|
|
@@ -500,14 +512,19 @@ export function deriveQueueHealthSignal(input: {
|
|
|
500
512
|
previousHealth.status !== 'healthy';
|
|
501
513
|
if (health.status === 'healthy' && !recovering) return null;
|
|
502
514
|
// A permanently unavailable adapter is a catalog/configuration finding, not
|
|
503
|
-
// a one-minute incident.
|
|
504
|
-
//
|
|
515
|
+
// a one-minute incident. Repeat it only when the five-minute bucket changes;
|
|
516
|
+
// this keeps the state visible without recreating one log per observer tick.
|
|
505
517
|
if (
|
|
506
518
|
health.status === 'unobserved' &&
|
|
507
519
|
previousHealth?.status === 'unobserved' &&
|
|
508
520
|
previousHealth.reason === health.reason
|
|
509
521
|
) {
|
|
510
|
-
|
|
522
|
+
const previousBucket = Math.floor(
|
|
523
|
+
(input.previousObservation?.observedAt ?? 0) /
|
|
524
|
+
UNOBSERVED_SIGNAL_REPEAT_MS,
|
|
525
|
+
);
|
|
526
|
+
const currentBucket = Math.floor(now / UNOBSERVED_SIGNAL_REPEAT_MS);
|
|
527
|
+
if (previousBucket === currentBucket) return null;
|
|
511
528
|
}
|
|
512
529
|
return {
|
|
513
530
|
schemaVersion: QUEUE_HEALTH_TELEMETRY_SCHEMA_VERSION,
|
|
@@ -676,3 +693,341 @@ export function deriveQueueHealth(
|
|
|
676
693
|
...(worstAgeMs === undefined ? {} : { worstAgeMs, worstAgeRatio: ratio }),
|
|
677
694
|
};
|
|
678
695
|
}
|
|
696
|
+
|
|
697
|
+
// Legacy v1 snapshot helpers remain exported for the webhook/cron collectors
|
|
698
|
+
// during the v1 -> v2 observability rollout. They are intentionally isolated
|
|
699
|
+
// from the scheduler-neutral v2 contract above.
|
|
700
|
+
export const QUEUE_HEALTH_SCHEMA_VERSION = 1 as const;
|
|
701
|
+
export type QueueHealthAvailability = 'available' | 'unavailable';
|
|
702
|
+
export type QueueHealthFreshness = 'fresh' | 'stale';
|
|
703
|
+
export type QueueHealthLayer =
|
|
704
|
+
| 'admission'
|
|
705
|
+
| 'delivery'
|
|
706
|
+
| 'execution'
|
|
707
|
+
| 'projection'
|
|
708
|
+
| 'reconciliation'
|
|
709
|
+
| 'other';
|
|
710
|
+
export type QueueHealthAttentionReason =
|
|
711
|
+
| 'source_unavailable'
|
|
712
|
+
| 'snapshot_stale'
|
|
713
|
+
| 'blocked_work'
|
|
714
|
+
| 'dead_letter_work'
|
|
715
|
+
| 'backlog_aged'
|
|
716
|
+
| 'backlog_count_inexact'
|
|
717
|
+
| 'backlog_age_unavailable';
|
|
718
|
+
export type QueueHealthCount = { value: number | null; exact: boolean };
|
|
719
|
+
export type QueueHealthCapacity = {
|
|
720
|
+
active: number | null;
|
|
721
|
+
limit: number | null;
|
|
722
|
+
};
|
|
723
|
+
export type QueueHealthSnapshot = {
|
|
724
|
+
schemaVersion: typeof QUEUE_HEALTH_SCHEMA_VERSION;
|
|
725
|
+
queueId: string;
|
|
726
|
+
owner: string;
|
|
727
|
+
layer: QueueHealthLayer;
|
|
728
|
+
transport: string;
|
|
729
|
+
observedAt: number;
|
|
730
|
+
freshness: { staleAfterMs: number };
|
|
731
|
+
availability: QueueHealthAvailability;
|
|
732
|
+
queued: QueueHealthCount;
|
|
733
|
+
running: QueueHealthCount;
|
|
734
|
+
retryWaiting: QueueHealthCount;
|
|
735
|
+
blocked: QueueHealthCount;
|
|
736
|
+
deadLetter: QueueHealthCount;
|
|
737
|
+
oldestQueuedAgeMs: number | null;
|
|
738
|
+
oldestBlockedAgeMs: number | null;
|
|
739
|
+
capacity: QueueHealthCapacity;
|
|
740
|
+
thresholds: { degradedOldestQueuedAgeMs: number };
|
|
741
|
+
};
|
|
742
|
+
export type QueueHealthSnapshotInput = Omit<
|
|
743
|
+
QueueHealthSnapshot,
|
|
744
|
+
'schemaVersion'
|
|
745
|
+
>;
|
|
746
|
+
type LegacyQueueHealthStatus = 'healthy' | 'degraded' | 'blocked' | 'unknown';
|
|
747
|
+
type LegacyQueueHealthAssessment = {
|
|
748
|
+
status: LegacyQueueHealthStatus;
|
|
749
|
+
freshness: QueueHealthFreshness;
|
|
750
|
+
observedAgeMs: number;
|
|
751
|
+
attentionReasons: readonly QueueHealthAttentionReason[];
|
|
752
|
+
};
|
|
753
|
+
export type QueueHealthAssessment = LegacyQueueHealthAssessment;
|
|
754
|
+
|
|
755
|
+
const legacyLayers = new Set<QueueHealthLayer>([
|
|
756
|
+
'admission',
|
|
757
|
+
'delivery',
|
|
758
|
+
'execution',
|
|
759
|
+
'projection',
|
|
760
|
+
'reconciliation',
|
|
761
|
+
'other',
|
|
762
|
+
]);
|
|
763
|
+
const legacyCountNames = [
|
|
764
|
+
'queued',
|
|
765
|
+
'running',
|
|
766
|
+
'retryWaiting',
|
|
767
|
+
'blocked',
|
|
768
|
+
'deadLetter',
|
|
769
|
+
] as const;
|
|
770
|
+
|
|
771
|
+
export function parseQueueHealthSnapshot(input: unknown): QueueHealthSnapshot {
|
|
772
|
+
const record = legacyRecord(input, 'Queue health snapshot');
|
|
773
|
+
if (record.schemaVersion !== 1)
|
|
774
|
+
throw new Error('Queue health schemaVersion must be 1.');
|
|
775
|
+
const layer = legacyString(record.layer, 'layer') as QueueHealthLayer;
|
|
776
|
+
if (!legacyLayers.has(layer))
|
|
777
|
+
throw new Error('Queue health layer is not supported.');
|
|
778
|
+
const availability = legacyString(
|
|
779
|
+
record.availability,
|
|
780
|
+
'availability',
|
|
781
|
+
) as QueueHealthAvailability;
|
|
782
|
+
if (availability !== 'available' && availability !== 'unavailable')
|
|
783
|
+
throw new Error(
|
|
784
|
+
'Queue health availability must be available or unavailable.',
|
|
785
|
+
);
|
|
786
|
+
const freshness = legacyRecord(record.freshness, 'freshness');
|
|
787
|
+
const capacity = legacyRecord(record.capacity, 'capacity');
|
|
788
|
+
const thresholds = legacyRecord(record.thresholds, 'thresholds');
|
|
789
|
+
const snapshot: QueueHealthSnapshot = {
|
|
790
|
+
schemaVersion: 1,
|
|
791
|
+
queueId: legacyIdentifier(record.queueId, 'queueId'),
|
|
792
|
+
owner: legacyIdentifier(record.owner, 'owner'),
|
|
793
|
+
layer,
|
|
794
|
+
transport: legacyIdentifier(record.transport, 'transport'),
|
|
795
|
+
observedAt: legacyNumber(record.observedAt, 'observedAt'),
|
|
796
|
+
freshness: {
|
|
797
|
+
staleAfterMs: legacyPositive(
|
|
798
|
+
freshness.staleAfterMs,
|
|
799
|
+
'freshness.staleAfterMs',
|
|
800
|
+
),
|
|
801
|
+
},
|
|
802
|
+
availability,
|
|
803
|
+
queued: legacyCount(record.queued, 'queued'),
|
|
804
|
+
running: legacyCount(record.running, 'running'),
|
|
805
|
+
retryWaiting: legacyCount(record.retryWaiting, 'retryWaiting'),
|
|
806
|
+
blocked: legacyCount(record.blocked, 'blocked'),
|
|
807
|
+
deadLetter: legacyCount(record.deadLetter, 'deadLetter'),
|
|
808
|
+
oldestQueuedAgeMs: legacyNullableNumber(
|
|
809
|
+
record.oldestQueuedAgeMs,
|
|
810
|
+
'oldestQueuedAgeMs',
|
|
811
|
+
),
|
|
812
|
+
oldestBlockedAgeMs: legacyNullableNumber(
|
|
813
|
+
record.oldestBlockedAgeMs,
|
|
814
|
+
'oldestBlockedAgeMs',
|
|
815
|
+
),
|
|
816
|
+
capacity: {
|
|
817
|
+
active: legacyNullableNumber(capacity.active, 'capacity.active'),
|
|
818
|
+
limit: legacyNullableNumber(capacity.limit, 'capacity.limit'),
|
|
819
|
+
},
|
|
820
|
+
thresholds: {
|
|
821
|
+
degradedOldestQueuedAgeMs: legacyPositive(
|
|
822
|
+
thresholds.degradedOldestQueuedAgeMs,
|
|
823
|
+
'thresholds.degradedOldestQueuedAgeMs',
|
|
824
|
+
),
|
|
825
|
+
},
|
|
826
|
+
};
|
|
827
|
+
if (snapshot.availability === 'unavailable') {
|
|
828
|
+
for (const name of legacyCountNames) {
|
|
829
|
+
const count = snapshot[name];
|
|
830
|
+
if (count.value !== null || count.exact)
|
|
831
|
+
throw new Error(
|
|
832
|
+
`Queue health ${name} must be unknown when availability is unavailable.`,
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
if (
|
|
836
|
+
snapshot.oldestQueuedAgeMs !== null ||
|
|
837
|
+
snapshot.oldestBlockedAgeMs !== null
|
|
838
|
+
)
|
|
839
|
+
throw new Error(
|
|
840
|
+
'Queue health oldestQueuedAgeMs must be null when availability is unavailable.',
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
if (
|
|
844
|
+
snapshot.queued.exact &&
|
|
845
|
+
snapshot.queued.value === 0 &&
|
|
846
|
+
snapshot.retryWaiting.exact &&
|
|
847
|
+
snapshot.retryWaiting.value === 0 &&
|
|
848
|
+
snapshot.oldestQueuedAgeMs !== null
|
|
849
|
+
) {
|
|
850
|
+
throw new Error(
|
|
851
|
+
'Queue health oldestQueuedAgeMs requires queued or retry-waiting work.',
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
if (
|
|
855
|
+
snapshot.blocked.exact &&
|
|
856
|
+
snapshot.blocked.value === 0 &&
|
|
857
|
+
snapshot.deadLetter.exact &&
|
|
858
|
+
snapshot.deadLetter.value === 0 &&
|
|
859
|
+
snapshot.oldestBlockedAgeMs !== null
|
|
860
|
+
) {
|
|
861
|
+
throw new Error(
|
|
862
|
+
'Queue health oldestBlockedAgeMs requires blocked or dead-letter work.',
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
return snapshot;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
export function createQueueHealthSnapshot(
|
|
869
|
+
input: QueueHealthSnapshotInput,
|
|
870
|
+
): QueueHealthSnapshot {
|
|
871
|
+
return parseQueueHealthSnapshot({ ...input, schemaVersion: 1 });
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
export function assessQueueHealth(
|
|
875
|
+
input: QueueHealthSnapshot,
|
|
876
|
+
now = Date.now(),
|
|
877
|
+
): LegacyQueueHealthAssessment {
|
|
878
|
+
const snapshot = parseQueueHealthSnapshot(input);
|
|
879
|
+
const observedAgeMs = Math.max(0, now - snapshot.observedAt);
|
|
880
|
+
const freshness: QueueHealthFreshness =
|
|
881
|
+
observedAgeMs > snapshot.freshness.staleAfterMs ? 'stale' : 'fresh';
|
|
882
|
+
if (snapshot.availability === 'unavailable')
|
|
883
|
+
return {
|
|
884
|
+
status: 'unknown',
|
|
885
|
+
freshness,
|
|
886
|
+
observedAgeMs,
|
|
887
|
+
attentionReasons: ['source_unavailable'],
|
|
888
|
+
};
|
|
889
|
+
if (freshness === 'stale')
|
|
890
|
+
return {
|
|
891
|
+
status: 'unknown',
|
|
892
|
+
freshness,
|
|
893
|
+
observedAgeMs,
|
|
894
|
+
attentionReasons: ['snapshot_stale'],
|
|
895
|
+
};
|
|
896
|
+
const attentionReasons: QueueHealthAttentionReason[] = [];
|
|
897
|
+
if (knownItems(snapshot.blocked)) attentionReasons.push('blocked_work');
|
|
898
|
+
if (knownItems(snapshot.deadLetter))
|
|
899
|
+
attentionReasons.push('dead_letter_work');
|
|
900
|
+
if (attentionReasons.length)
|
|
901
|
+
return { status: 'blocked', freshness, observedAgeMs, attentionReasons };
|
|
902
|
+
if (
|
|
903
|
+
snapshot.oldestQueuedAgeMs !== null &&
|
|
904
|
+
snapshot.oldestQueuedAgeMs >= snapshot.thresholds.degradedOldestQueuedAgeMs
|
|
905
|
+
)
|
|
906
|
+
return {
|
|
907
|
+
status: 'degraded',
|
|
908
|
+
freshness,
|
|
909
|
+
observedAgeMs,
|
|
910
|
+
attentionReasons: ['backlog_aged'],
|
|
911
|
+
};
|
|
912
|
+
if (
|
|
913
|
+
!snapshot.queued.exact ||
|
|
914
|
+
!snapshot.retryWaiting.exact ||
|
|
915
|
+
!snapshot.blocked.exact ||
|
|
916
|
+
!snapshot.deadLetter.exact
|
|
917
|
+
)
|
|
918
|
+
return {
|
|
919
|
+
status: 'unknown',
|
|
920
|
+
freshness,
|
|
921
|
+
observedAgeMs,
|
|
922
|
+
attentionReasons: ['backlog_count_inexact'],
|
|
923
|
+
};
|
|
924
|
+
if (
|
|
925
|
+
(knownItems(snapshot.queued) || knownItems(snapshot.retryWaiting)) &&
|
|
926
|
+
snapshot.oldestQueuedAgeMs === null
|
|
927
|
+
)
|
|
928
|
+
return {
|
|
929
|
+
status: 'unknown',
|
|
930
|
+
freshness,
|
|
931
|
+
observedAgeMs,
|
|
932
|
+
attentionReasons: ['backlog_age_unavailable'],
|
|
933
|
+
};
|
|
934
|
+
return { status: 'healthy', freshness, observedAgeMs, attentionReasons: [] };
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
export function queueHealthTelemetryFields(
|
|
938
|
+
input: QueueHealthSnapshot,
|
|
939
|
+
assessment = assessQueueHealth(input),
|
|
940
|
+
): TelemetryFields {
|
|
941
|
+
const snapshot = parseQueueHealthSnapshot(input);
|
|
942
|
+
return {
|
|
943
|
+
queueHealthSchemaVersion: snapshot.schemaVersion,
|
|
944
|
+
queueId: snapshot.queueId,
|
|
945
|
+
queueOwner: snapshot.owner,
|
|
946
|
+
queueLayer: snapshot.layer,
|
|
947
|
+
queueTransport: snapshot.transport,
|
|
948
|
+
queueAvailability: snapshot.availability,
|
|
949
|
+
queueObservedAt: snapshot.observedAt,
|
|
950
|
+
queueFreshnessStaleAfterMs: snapshot.freshness.staleAfterMs,
|
|
951
|
+
queueFreshness: assessment.freshness,
|
|
952
|
+
queueHealth: assessment.status,
|
|
953
|
+
queueHealthObservedAgeMs: assessment.observedAgeMs,
|
|
954
|
+
queueQueued: snapshot.queued.value,
|
|
955
|
+
queueQueuedExact: snapshot.queued.exact,
|
|
956
|
+
queueRunning: snapshot.running.value,
|
|
957
|
+
queueRunningExact: snapshot.running.exact,
|
|
958
|
+
queueRetryWaiting: snapshot.retryWaiting.value,
|
|
959
|
+
queueRetryWaitingExact: snapshot.retryWaiting.exact,
|
|
960
|
+
queueBlocked: snapshot.blocked.value,
|
|
961
|
+
queueBlockedExact: snapshot.blocked.exact,
|
|
962
|
+
queueDeadLetter: snapshot.deadLetter.value,
|
|
963
|
+
queueDeadLetterExact: snapshot.deadLetter.exact,
|
|
964
|
+
queueOldestQueuedAgeMs: snapshot.oldestQueuedAgeMs,
|
|
965
|
+
queueOldestBlockedAgeMs: snapshot.oldestBlockedAgeMs,
|
|
966
|
+
queueCapacityActive: snapshot.capacity.active,
|
|
967
|
+
queueCapacityLimit: snapshot.capacity.limit,
|
|
968
|
+
queueDegradedOldestQueuedAgeMs:
|
|
969
|
+
snapshot.thresholds.degradedOldestQueuedAgeMs,
|
|
970
|
+
queueAttentionReasons: assessment.attentionReasons,
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
export async function emitQueueHealthSnapshot(input: {
|
|
975
|
+
telemetry: Pick<Telemetry, 'info' | 'warn'>;
|
|
976
|
+
snapshot: QueueHealthSnapshot;
|
|
977
|
+
now?: number;
|
|
978
|
+
}): Promise<LegacyQueueHealthAssessment> {
|
|
979
|
+
const assessment = assessQueueHealth(input.snapshot, input.now);
|
|
980
|
+
const fields = queueHealthTelemetryFields(input.snapshot, assessment);
|
|
981
|
+
await input.telemetry.info('queue.health.snapshot', fields);
|
|
982
|
+
if (assessment.status !== 'healthy')
|
|
983
|
+
await input.telemetry.warn('queue.health.attention', fields);
|
|
984
|
+
return assessment;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
function legacyRecord(value: unknown, label: string): Record<string, unknown> {
|
|
988
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
989
|
+
throw new Error(`${label} must be an object.`);
|
|
990
|
+
return value as Record<string, unknown>;
|
|
991
|
+
}
|
|
992
|
+
function legacyString(value: unknown, label: string): string {
|
|
993
|
+
if (typeof value !== 'string' || !value.trim())
|
|
994
|
+
throw new Error(`Queue health ${label} must be a non-empty string.`);
|
|
995
|
+
return value.trim();
|
|
996
|
+
}
|
|
997
|
+
function legacyIdentifier(value: unknown, label: string): string {
|
|
998
|
+
const result = legacyString(value, label);
|
|
999
|
+
if (result.length > 96 || !/^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/.test(result))
|
|
1000
|
+
throw new Error(
|
|
1001
|
+
`Queue health ${label} must be a stable low-cardinality identifier.`,
|
|
1002
|
+
);
|
|
1003
|
+
return result;
|
|
1004
|
+
}
|
|
1005
|
+
function legacyNumber(value: unknown, label: string): number {
|
|
1006
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0)
|
|
1007
|
+
throw new Error(
|
|
1008
|
+
`Queue health ${label} must be a finite non-negative number.`,
|
|
1009
|
+
);
|
|
1010
|
+
return value;
|
|
1011
|
+
}
|
|
1012
|
+
function legacyPositive(value: unknown, label: string): number {
|
|
1013
|
+
const result = legacyNumber(value, label);
|
|
1014
|
+
if (result === 0) throw new Error(`Queue health ${label} must be positive.`);
|
|
1015
|
+
return result;
|
|
1016
|
+
}
|
|
1017
|
+
function legacyNullableNumber(value: unknown, label: string): number | null {
|
|
1018
|
+
return value === null ? null : legacyNumber(value, label);
|
|
1019
|
+
}
|
|
1020
|
+
function legacyCount(value: unknown, label: string): QueueHealthCount {
|
|
1021
|
+
const record = legacyRecord(value, label);
|
|
1022
|
+
if (typeof record.exact !== 'boolean')
|
|
1023
|
+
throw new Error(`Queue health ${label}.exact must be a boolean.`);
|
|
1024
|
+
const count = legacyNullableNumber(record.value, `${label}.value`);
|
|
1025
|
+
if (count === null && record.exact)
|
|
1026
|
+
throw new Error(
|
|
1027
|
+
`Queue health ${label}.exact cannot be true when value is unknown.`,
|
|
1028
|
+
);
|
|
1029
|
+
return { value: count, exact: record.exact };
|
|
1030
|
+
}
|
|
1031
|
+
function knownItems(count: QueueHealthCount): boolean {
|
|
1032
|
+
return count.value !== null && count.value > 0;
|
|
1033
|
+
}
|
|
@@ -976,13 +976,13 @@
|
|
|
976
976
|
"domainReportRequired": true
|
|
977
977
|
},
|
|
978
978
|
{
|
|
979
|
-
"id": "
|
|
979
|
+
"id": "convex.recover-play-webhook-receipts",
|
|
980
980
|
"name": "Recover Play webhook receipts",
|
|
981
|
-
"scheduler": "
|
|
982
|
-
"cadence": "
|
|
981
|
+
"scheduler": "convex_cron",
|
|
982
|
+
"cadence": "every 60s",
|
|
983
983
|
"timezone": "UTC",
|
|
984
|
-
"source": "
|
|
985
|
-
"locator": "
|
|
984
|
+
"source": "packages/convex/crons.ts",
|
|
985
|
+
"locator": "recover Play webhook receipts",
|
|
986
986
|
"owner": "runtime",
|
|
987
987
|
"criticality": "critical",
|
|
988
988
|
"purpose": "Recover Play webhook receipts whose admission/dispatch continuation is delayed or stranded.",
|
|
@@ -990,7 +990,25 @@
|
|
|
990
990
|
"failureDefinition": "A receipt cannot be safely recovered, remains stranded, or evidence cannot persist.",
|
|
991
991
|
"startGraceMs": 300000,
|
|
992
992
|
"runTimeoutMs": 1800000,
|
|
993
|
-
"failureAlert": "
|
|
993
|
+
"failureAlert": "convex_action_wrapper",
|
|
994
|
+
"domainReportRequired": true
|
|
995
|
+
},
|
|
996
|
+
{
|
|
997
|
+
"id": "convex.recover-signal-radar-inbound-receipts",
|
|
998
|
+
"name": "Recover Signal Radar inbound receipts",
|
|
999
|
+
"scheduler": "convex_cron",
|
|
1000
|
+
"cadence": "every 60s",
|
|
1001
|
+
"timezone": "UTC",
|
|
1002
|
+
"source": "packages/convex/crons.ts",
|
|
1003
|
+
"locator": "recover Signal Radar inbound receipts",
|
|
1004
|
+
"owner": "monitors",
|
|
1005
|
+
"criticality": "critical",
|
|
1006
|
+
"purpose": "Recover Signal Radar inbound receipts whose processing is delayed or stranded.",
|
|
1007
|
+
"successDefinition": "Every eligible receipt is retried or terminally classified with evidence; no eligible receipts is a valid no-op.",
|
|
1008
|
+
"failureDefinition": "A receipt cannot be safely recovered, remains stranded, or evidence cannot persist.",
|
|
1009
|
+
"startGraceMs": 300000,
|
|
1010
|
+
"runTimeoutMs": 1800000,
|
|
1011
|
+
"failureAlert": "convex_action_wrapper",
|
|
994
1012
|
"domainReportRequired": true
|
|
995
1013
|
},
|
|
996
1014
|
{
|
|
@@ -1011,24 +1029,6 @@
|
|
|
1011
1029
|
"failureAlert": "convex_action_wrapper",
|
|
1012
1030
|
"domainReportRequired": true
|
|
1013
1031
|
},
|
|
1014
|
-
{
|
|
1015
|
-
"id": "github.modal-ci-runner-weekly-proof",
|
|
1016
|
-
"name": "Modal CI runner",
|
|
1017
|
-
"scheduler": "github_actions",
|
|
1018
|
-
"cadence": "23 6 * * 1",
|
|
1019
|
-
"timezone": "UTC",
|
|
1020
|
-
"source": ".github/workflows/modal-ci-runner.yml",
|
|
1021
|
-
"locator": "schedule:",
|
|
1022
|
-
"owner": "platform",
|
|
1023
|
-
"criticality": "maintenance",
|
|
1024
|
-
"purpose": "Lint, test, deploy, and prove the pinned Modal CI runner image and isolated smoke path.",
|
|
1025
|
-
"successDefinition": "Pinned package lint/tests, control-plane/image deploy, and exact-image credential-isolation smoke all pass.",
|
|
1026
|
-
"failureDefinition": "Any lint/test/deploy/health/smoke/cleanup proof fails or image identity is wrong.",
|
|
1027
|
-
"startGraceMs": 600000,
|
|
1028
|
-
"runTimeoutMs": 3600000,
|
|
1029
|
-
"failureAlert": "github_workflow_run",
|
|
1030
|
-
"domainReportRequired": true
|
|
1031
|
-
},
|
|
1032
1032
|
{
|
|
1033
1033
|
"id": "convex.async-usage-recovery-continuation",
|
|
1034
1034
|
"name": "Async usage recovery continuation",
|
|
@@ -18,6 +18,8 @@ export const MANUALLY_RUNNABLE_CONVEX_CRON_JOB_IDS = [
|
|
|
18
18
|
'convex.monitor-deployment-job-tick',
|
|
19
19
|
'convex.provider-monitor-global-audit-daily',
|
|
20
20
|
'convex.recover-notification-delivery-work',
|
|
21
|
+
'convex.recover-play-webhook-receipts',
|
|
22
|
+
'convex.recover-signal-radar-inbound-receipts',
|
|
21
23
|
'convex.process-notification-usage-refreshes',
|
|
22
24
|
'convex.observe-operational-queue-health',
|
|
23
25
|
'convex.cleanup-expired-notification-audit-records',
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
/** The only scheduled-work telemetry envelope emitted after the hard cut. */
|
|
2
|
+
export const SCHEDULED_WORK_OBSERVABILITY_VERSION = 2 as const;
|
|
3
|
+
export const SCHEDULED_WORK_CONTRACT_VERSION = 2 as const;
|
|
2
4
|
|
|
3
5
|
export type ScheduledWorkScheduler =
|
|
4
6
|
| 'convex_cron'
|
|
@@ -11,6 +13,17 @@ export type ScheduledWorkScheduler =
|
|
|
11
13
|
|
|
12
14
|
export type ScheduledWorkOutcome = 'running' | 'succeeded' | 'failed';
|
|
13
15
|
export type ScheduledWorkTrigger = 'scheduled' | 'manual';
|
|
16
|
+
export type ScheduledWorkDomainOutcome = 'success' | 'warning' | 'failure';
|
|
17
|
+
export type ScheduledWorkFailureClass = 'execution' | 'domain';
|
|
18
|
+
|
|
19
|
+
export type ScheduledWorkTelemetryDisposition = {
|
|
20
|
+
/** The occurrence slot this event belongs to, when one exists. */
|
|
21
|
+
occurrenceId?: string;
|
|
22
|
+
expectedAt?: number;
|
|
23
|
+
/** Explicit domain result; absence is a migration gap, not a green run. */
|
|
24
|
+
domainOutcome?: ScheduledWorkDomainOutcome;
|
|
25
|
+
failureClass?: ScheduledWorkFailureClass;
|
|
26
|
+
};
|
|
14
27
|
|
|
15
28
|
export type ScheduledWorkVolume = {
|
|
16
29
|
unit: string;
|
|
@@ -44,9 +57,12 @@ export type ScheduledWorkObservation = {
|
|
|
44
57
|
|
|
45
58
|
export function scheduledWorkTelemetryFields(
|
|
46
59
|
observation: ScheduledWorkObservation,
|
|
60
|
+
disposition: ScheduledWorkTelemetryDisposition = {},
|
|
47
61
|
): Record<string, string | number> {
|
|
48
62
|
return {
|
|
49
63
|
scheduledWorkSchemaVersion: SCHEDULED_WORK_OBSERVABILITY_VERSION,
|
|
64
|
+
// Keep one explicit marker for the hard-cut occurrence/domain contract.
|
|
65
|
+
scheduledWorkContractVersion: SCHEDULED_WORK_CONTRACT_VERSION,
|
|
50
66
|
scheduledWorkJobId: observation.jobId,
|
|
51
67
|
scheduledWorkJobName: observation.jobName,
|
|
52
68
|
scheduledWorkScheduler: observation.scheduler,
|
|
@@ -55,6 +71,18 @@ export function scheduledWorkTelemetryFields(
|
|
|
55
71
|
scheduledWorkOutcome: observation.outcome,
|
|
56
72
|
scheduledWorkInvocationId: observation.invocationId,
|
|
57
73
|
scheduledWorkExpectedWithinMs: observation.expectedWithinMs,
|
|
74
|
+
...(disposition.occurrenceId === undefined
|
|
75
|
+
? {}
|
|
76
|
+
: { scheduledWorkOccurrenceId: disposition.occurrenceId }),
|
|
77
|
+
...(disposition.expectedAt === undefined
|
|
78
|
+
? {}
|
|
79
|
+
: { scheduledWorkExpectedAt: disposition.expectedAt }),
|
|
80
|
+
...(disposition.domainOutcome === undefined
|
|
81
|
+
? {}
|
|
82
|
+
: { scheduledWorkDomainOutcome: disposition.domainOutcome }),
|
|
83
|
+
...(disposition.failureClass === undefined
|
|
84
|
+
? {}
|
|
85
|
+
: { scheduledWorkFailureClass: disposition.failureClass }),
|
|
58
86
|
...(observation.trigger === undefined
|
|
59
87
|
? {}
|
|
60
88
|
: { scheduledWorkTrigger: observation.trigger }),
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { PLAY_WEBHOOK_RECEIPT_LARGE_BATCH_MAX_RECEIPTS } from '../security/receipt-admission-token';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The per-request concurrency budget for moving a durable Play source into
|
|
5
|
+
* the Runtime Postgres + Absurd admission transaction. This is not an
|
|
6
|
+
* execution limit: Absurd owns execution capacity independently.
|
|
7
|
+
*
|
|
8
|
+
* Thirty-two is the private batch's entire scheduler-client budget for a
|
|
9
|
+
* normal 1,000-delivery burst. One client remains reserved below, rather than
|
|
10
|
+
* letting source admission consume every control-plane connection.
|
|
11
|
+
*/
|
|
12
|
+
export const PLAY_ADMISSION_MAX_PARALLELISM = 32;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The private batch owns all 32 serverless scheduler clients, but one remains
|
|
16
|
+
* available for executor-token and other critical runtime control-plane work.
|
|
17
|
+
* Only atomic Runtime Postgres + Absurd submission uses this value.
|
|
18
|
+
* Source-lifecycle preflight has its own bounded budget because it does not
|
|
19
|
+
* consume a scheduler-pool client.
|
|
20
|
+
*/
|
|
21
|
+
export const PLAY_ADMISSION_MAX_ACTIVE_SUBMITS =
|
|
22
|
+
PLAY_ADMISSION_MAX_PARALLELISM - 1;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A batch is allowed to prepare more independent source lifecycles than it
|
|
26
|
+
* may submit to Runtime Postgres at once. Preparation is mostly Convex/CPU
|
|
27
|
+
* work (read the immutable receipt, validate its pinned launch, mint its
|
|
28
|
+
* authority); it must not sit behind a scarce PgBouncer connection. The
|
|
29
|
+
* scheduler submitter remains capped by `PLAY_ADMISSION_MAX_ACTIVE_SUBMITS`.
|
|
30
|
+
*
|
|
31
|
+
* This is deliberately finite. It lets a 1K burst fill the 31 durable-submit
|
|
32
|
+
* slots while prior sources are still doing their non-Postgres preparation,
|
|
33
|
+
* without turning the private route into an unbounded fan-out.
|
|
34
|
+
*/
|
|
35
|
+
export const PLAY_ADMISSION_MAX_ACTIVE_LIFECYCLES = 256;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A private 1K batch may queue behind the bounded scheduler submit cohort for
|
|
39
|
+
* longer than an ordinary single-run request, but never beyond this deadline.
|
|
40
|
+
*/
|
|
41
|
+
export const PLAY_ADMISSION_MAX_ACQUIRE_QUEUE_WAIT_MS = 120_000;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* One managed transport request may carry this many independently durable
|
|
45
|
+
* sources. The Vercel adapter processes one legacy wave at bounded
|
|
46
|
+
* source-lifecycle concurrency; its scheduler submit gate separately reserves
|
|
47
|
+
* critical control-plane capacity.
|
|
48
|
+
*/
|
|
49
|
+
export const PLAY_ADMISSION_BATCH_SIZE = PLAY_ADMISSION_MAX_PARALLELISM;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* `batch_v2` carries one full, bounded ingress burst into a single Vercel
|
|
53
|
+
* invocation. The invocation still submits at most
|
|
54
|
+
* `PLAY_ADMISSION_MAX_ACTIVE_SUBMITS` Runs at a time; this only removes the
|
|
55
|
+
* serial Convex-to-Vercel round trip between 32-receipt waves. The current
|
|
56
|
+
* dispatcher sends one request and lets that request drain its owned tail.
|
|
57
|
+
*/
|
|
58
|
+
export const PLAY_ADMISSION_LARGE_BATCH_SIZE =
|
|
59
|
+
PLAY_WEBHOOK_RECEIPT_LARGE_BATCH_MAX_RECEIPTS;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* `batch_v1` remains during the Convex-before-Vercel rollout window. It
|
|
63
|
+
* drains this many 32-source waves from one fenced Workpool action. New
|
|
64
|
+
* Retained custom-runner v2 actions may take a second large wave while their
|
|
65
|
+
* finite rollout cohort drains. Current scheduled dispatches own one
|
|
66
|
+
* exact wave and do not use this compatibility limit.
|
|
67
|
+
*/
|
|
68
|
+
export const PLAY_ADMISSION_BATCHES_PER_DISPATCH = 32;
|
|
69
|
+
|
|
70
|
+
export const PLAY_ADMISSION_LARGE_BATCHES_PER_DISPATCH = 2;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Upper bound for one direct drain under continuous traffic, not an admission
|
|
74
|
+
* SLO or a debounce. A request stops as soon as its queue is empty. Keep the
|
|
75
|
+
* exported name and persisted deadline compatible with retained dispatches.
|
|
76
|
+
*/
|
|
77
|
+
export const PLAY_ADMISSION_DIRECT_BATCH_COLLECTION_MAX_MS = 60_000;
|
|
78
|
+
|
|
79
|
+
/** Poll/debounce interval for retained legacy dispatch implementations only. */
|
|
80
|
+
export const PLAY_ADMISSION_DIRECT_BATCH_QUIET_MS = 250;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Workpool is recovery, not the normal batch transport. It becomes eligible
|
|
84
|
+
* quickly, but the ownerless direct-dispatch fence keeps it from claiming the
|
|
85
|
+
* same source while the private request drains. That gives a lost action
|
|
86
|
+
* prompt recovery without adding another normal-path batch request.
|
|
87
|
+
*/
|
|
88
|
+
export const PLAY_ADMISSION_DIRECT_FALLBACK_DELAY_MS = 15_000;
|
|
89
|
+
|
|
90
|
+
/** A pending source older than this is actionable queue delay. */
|
|
91
|
+
export const PLAY_ADMISSION_DEGRADED_QUEUE_AGE_MS = 60_000;
|
|
@@ -1123,6 +1123,8 @@ export function resolveToolRuntimeTimeoutMs(
|
|
|
1123
1123
|
normalized === 'deeplineagent_deeplineagent' ||
|
|
1124
1124
|
normalized === 'ai_inference' ||
|
|
1125
1125
|
normalized === 'deeplineagent_ai_inference' ||
|
|
1126
|
+
normalized === 'ai_evaluate' ||
|
|
1127
|
+
normalized === 'deeplineagent_ai_evaluate' ||
|
|
1126
1128
|
normalized === 'aiinference'
|
|
1127
1129
|
? DEEPLINEAGENT_TOOL_RUNTIME_TIMEOUT_MS
|
|
1128
1130
|
: DEFAULT_TOOL_RUNTIME_TIMEOUT_MS;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounds a synchronous direct-admission attempt. The source and its delayed
|
|
3
|
+
* Workpool recovery item already committed before this helper is used, so a
|
|
4
|
+
* timeout is deliberately an ambiguous outcome rather than a failed delivery.
|
|
5
|
+
* The race does not cancel already-started work; a replay must use the same
|
|
6
|
+
* idempotency identity and converge on the existing Runtime Run/Absurd task.
|
|
7
|
+
*/
|
|
8
|
+
export const PLAY_DIRECT_ADMISSION_DEADLINE_MS = 1_500;
|
|
9
|
+
|
|
10
|
+
export class PlayDirectAdmissionDeadlineExceededError extends Error {
|
|
11
|
+
constructor(timeoutMs: number) {
|
|
12
|
+
super(`Direct Play admission exceeded its ${timeoutMs}ms deadline.`);
|
|
13
|
+
this.name = 'PlayDirectAdmissionDeadlineExceededError';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function withPlayDirectAdmissionDeadline<T>(
|
|
18
|
+
fn: () => Promise<T> | T,
|
|
19
|
+
timeoutMs = PLAY_DIRECT_ADMISSION_DEADLINE_MS,
|
|
20
|
+
): Promise<T> {
|
|
21
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
22
|
+
const work = Promise.resolve().then(fn);
|
|
23
|
+
const deadline = new Promise<never>((_resolve, reject) => {
|
|
24
|
+
timer = setTimeout(
|
|
25
|
+
() => reject(new PlayDirectAdmissionDeadlineExceededError(timeoutMs)),
|
|
26
|
+
timeoutMs,
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
try {
|
|
30
|
+
return await Promise.race([work, deadline]);
|
|
31
|
+
} finally {
|
|
32
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
33
|
+
}
|
|
34
|
+
}
|