deepline 0.3.115 → 0.3.117
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 +356 -12
- package/dist/bundling-sources/sdk/src/index.ts +12 -1
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/shared_libs/monitors/billing/state.ts +36 -6
- package/dist/bundling-sources/shared_libs/monitors/billing/types.ts +0 -1
- package/dist/bundling-sources/shared_libs/monitors/deactivation-reasons.ts +6 -3
- package/dist/bundling-sources/shared_libs/monitors/fleets/verdict.ts +52 -15
- package/dist/bundling-sources/shared_libs/monitors/monitor-job-state.ts +44 -0
- package/dist/bundling-sources/shared_libs/monitors/package.json +2 -0
- package/dist/bundling-sources/shared_libs/monitors/provider-monitor-batch-operation.ts +76 -3
- package/dist/bundling-sources/shared_libs/monitors/provider-monitor-state.ts +81 -6
- package/dist/bundling-sources/shared_libs/monitors/provider-monitor-status.ts +26 -10
- package/dist/bundling-sources/shared_libs/observability/queue-health.ts +128 -0
- package/dist/bundling-sources/shared_libs/observability/scheduled-jobs.json +18 -0
- package/dist/bundling-sources/shared_libs/observability/scheduled-work-manual.ts +1 -0
- package/dist/bundling-sources/shared_libs/plays/share/static-pipeline-summary.ts +2 -2
- package/dist/cli/index.js +413 -68
- package/dist/cli/index.mjs +413 -68
- package/dist/index.d.mts +155 -13
- package/dist/index.d.ts +155 -13
- package/dist/index.js +137 -2
- package/dist/index.mjs +134 -2
- package/dist/install-integrity.json +1 -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
|
@@ -8,23 +8,32 @@
|
|
|
8
8
|
* the fact; this is the single reading of them.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
11
|
+
import type { ProviderMonitorCustomerState } from '@deepline/monitors/provider-monitor-state';
|
|
12
|
+
|
|
13
|
+
export const FLEET_CONVERGENCE_STATES = [
|
|
14
|
+
'pending',
|
|
15
|
+
'submitting',
|
|
16
|
+
'rate_limited',
|
|
17
|
+
'settled',
|
|
18
|
+
'blocked',
|
|
19
|
+
'failed',
|
|
20
|
+
] as const;
|
|
21
|
+
|
|
22
|
+
export type FleetConvergenceState = (typeof FLEET_CONVERGENCE_STATES)[number];
|
|
23
|
+
|
|
24
|
+
export type FleetConvergenceCounts = Record<FleetConvergenceState, number>;
|
|
19
25
|
|
|
20
26
|
/** The lifecycle a fleet row records. */
|
|
21
|
-
export
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
export const FLEET_LIFECYCLE_STATUSES = [
|
|
28
|
+
'active',
|
|
29
|
+
'deactivating',
|
|
30
|
+
'deactivated',
|
|
31
|
+
'billing_suspending',
|
|
32
|
+
'billing_suspended',
|
|
33
|
+
'billing_reactivating',
|
|
34
|
+
] as const;
|
|
35
|
+
|
|
36
|
+
export type FleetLifecycleStatus = (typeof FLEET_LIFECYCLE_STATUSES)[number];
|
|
28
37
|
|
|
29
38
|
/** What a caller is told the fleet is doing. */
|
|
30
39
|
export type FleetStatusVerdict =
|
|
@@ -37,6 +46,34 @@ export type FleetStatusVerdict =
|
|
|
37
46
|
| 'billing_suspended'
|
|
38
47
|
| 'billing_reactivating';
|
|
39
48
|
|
|
49
|
+
/**
|
|
50
|
+
* The Fleet's stable customer state. Convergence and lifecycle transitions are
|
|
51
|
+
* reported by the durable Fleet operation, not by growing this union.
|
|
52
|
+
*/
|
|
53
|
+
export function fleetCustomerState(input: {
|
|
54
|
+
lifecycle: FleetLifecycleStatus;
|
|
55
|
+
verdict: FleetStatusVerdict;
|
|
56
|
+
convergedGeneration: number | null;
|
|
57
|
+
}): ProviderMonitorCustomerState {
|
|
58
|
+
switch (input.lifecycle) {
|
|
59
|
+
case 'billing_suspending':
|
|
60
|
+
case 'billing_suspended':
|
|
61
|
+
case 'billing_reactivating':
|
|
62
|
+
return 'billing_suspended';
|
|
63
|
+
case 'deactivating':
|
|
64
|
+
case 'deactivated':
|
|
65
|
+
return 'disabled';
|
|
66
|
+
case 'active':
|
|
67
|
+
// A new Fleet is not active merely because its desired lifecycle says
|
|
68
|
+
// so. It becomes active after one generation has been provider-proven.
|
|
69
|
+
// A later convergence may keep serving that last proven generation.
|
|
70
|
+
return input.verdict !== 'degraded' &&
|
|
71
|
+
(input.verdict === 'converged' || input.convergedGeneration !== null)
|
|
72
|
+
? 'active'
|
|
73
|
+
: 'disabled';
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
40
77
|
const ZERO_FLEET_COUNTS: FleetConvergenceCounts = {
|
|
41
78
|
pending: 0,
|
|
42
79
|
submitting: 0,
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared vocabulary for the public Monitor Job projection.
|
|
3
|
+
*
|
|
4
|
+
* A job reports progress toward provider truth. None of these values are a
|
|
5
|
+
* monitor lifecycle state, and no cron/admin path may invent another job
|
|
6
|
+
* vocabulary. Convex validators repeat these literals at the storage ABI
|
|
7
|
+
* boundary; the contract tests keep that deliberate mirror aligned.
|
|
8
|
+
*/
|
|
9
|
+
export const MONITOR_JOB_STATES = [
|
|
10
|
+
'queued',
|
|
11
|
+
'running',
|
|
12
|
+
'cancelling',
|
|
13
|
+
'succeeded',
|
|
14
|
+
'failed',
|
|
15
|
+
'cancelled',
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
export type MonitorJobState = (typeof MONITOR_JOB_STATES)[number];
|
|
19
|
+
|
|
20
|
+
export const MONITOR_JOB_PHASES = [
|
|
21
|
+
'queued',
|
|
22
|
+
'provider_create',
|
|
23
|
+
'provider_readback',
|
|
24
|
+
'reconciliation',
|
|
25
|
+
'completed',
|
|
26
|
+
] as const;
|
|
27
|
+
|
|
28
|
+
export type MonitorJobPhase = (typeof MONITOR_JOB_PHASES)[number];
|
|
29
|
+
|
|
30
|
+
export const MONITOR_JOB_RECONCILIATION_STATES = [
|
|
31
|
+
'pending',
|
|
32
|
+
'aligned',
|
|
33
|
+
'partial',
|
|
34
|
+
'mismatched',
|
|
35
|
+
] as const;
|
|
36
|
+
|
|
37
|
+
export type MonitorJobReconciliationState =
|
|
38
|
+
(typeof MONITOR_JOB_RECONCILIATION_STATES)[number];
|
|
39
|
+
|
|
40
|
+
export const TERMINAL_MONITOR_JOB_STATES = [
|
|
41
|
+
'succeeded',
|
|
42
|
+
'failed',
|
|
43
|
+
'cancelled',
|
|
44
|
+
] as const satisfies readonly MonitorJobState[];
|
|
@@ -53,6 +53,8 @@
|
|
|
53
53
|
"./lifecycle-notifications.js": "./lifecycle-notifications.ts",
|
|
54
54
|
"./monitor-fleet-limits": "./monitor-fleet-limits.ts",
|
|
55
55
|
"./monitor-fleet-limits.js": "./monitor-fleet-limits.ts",
|
|
56
|
+
"./monitor-job-state": "./monitor-job-state.ts",
|
|
57
|
+
"./monitor-job-state.js": "./monitor-job-state.ts",
|
|
56
58
|
"./org-monitor-limits": "./org-monitor-limits.ts",
|
|
57
59
|
"./org-monitor-limits.js": "./org-monitor-limits.ts",
|
|
58
60
|
"./provider-callback-verification": "./provider-callback-verification.ts",
|
|
@@ -21,6 +21,10 @@ export const PROVIDER_MONITOR_BATCH_OPERATION_KINDS = [
|
|
|
21
21
|
// operation is the durable, observable hand-off to the fenced Fleet
|
|
22
22
|
// executor; it is not a second lifecycle verb or a raw provider create.
|
|
23
23
|
'converge',
|
|
24
|
+
// A named Batch owns a bounded mixed plan (sync/start/stop) under one
|
|
25
|
+
// durable operation. It is intentionally distinct from manifest deploy:
|
|
26
|
+
// omission/removal semantics belong to the Batch desired-set owner.
|
|
27
|
+
'named_batch',
|
|
24
28
|
] as const;
|
|
25
29
|
|
|
26
30
|
export type ProviderMonitorBatchOperationKind =
|
|
@@ -66,12 +70,25 @@ export type ProviderMonitorBatchScope =
|
|
|
66
70
|
| { kind: 'target'; target: ProviderMonitorBatchTarget }
|
|
67
71
|
| { kind: 'fleet'; fleetId: string }
|
|
68
72
|
| ProviderMonitorBatchManifestScope
|
|
73
|
+
| {
|
|
74
|
+
kind: 'named_batch';
|
|
75
|
+
batchId: string;
|
|
76
|
+
generation: number;
|
|
77
|
+
intent: 'sync' | 'start' | 'stop';
|
|
78
|
+
}
|
|
69
79
|
| ProviderMonitorBatchGlobalAuditScope;
|
|
70
80
|
|
|
71
81
|
export type ProviderMonitorBatchOperationRequest = {
|
|
72
82
|
kind: ProviderMonitorBatchOperationKind;
|
|
73
83
|
scope: ProviderMonitorBatchScope;
|
|
74
84
|
dryRun: boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Optional ownership mode for exact customer lifecycle requests. Omitted
|
|
87
|
+
* means the system/policy runner may operate the selected definitions. The
|
|
88
|
+
* field is deliberately additive so older operation rows and callers keep
|
|
89
|
+
* their original fingerprint and behavior.
|
|
90
|
+
*/
|
|
91
|
+
control?: 'customer_exact' | 'system';
|
|
75
92
|
/**
|
|
76
93
|
* The application deployment that accepted this operation. A provider
|
|
77
94
|
* callback is part of the create side effect, so a continuation must retain
|
|
@@ -93,6 +110,17 @@ export type ProviderMonitorBatchOperationState =
|
|
|
93
110
|
| 'failed'
|
|
94
111
|
| 'cancelled';
|
|
95
112
|
|
|
113
|
+
/** Per-monitor evidence written by a durable batch operation. */
|
|
114
|
+
export const PROVIDER_MONITOR_BATCH_RESULT_STATES = [
|
|
115
|
+
'done',
|
|
116
|
+
'retry',
|
|
117
|
+
'blocked',
|
|
118
|
+
'superseded',
|
|
119
|
+
] as const;
|
|
120
|
+
|
|
121
|
+
export type ProviderMonitorBatchResultState =
|
|
122
|
+
(typeof PROVIDER_MONITOR_BATCH_RESULT_STATES)[number];
|
|
123
|
+
|
|
96
124
|
const IDEMPOTENCY_KEY = /^[A-Za-z0-9][A-Za-z0-9._-]{0,95}$/;
|
|
97
125
|
|
|
98
126
|
function canonicalJson(value: unknown): string {
|
|
@@ -176,14 +204,40 @@ export function validateProviderMonitorBatchOperationRequest(
|
|
|
176
204
|
if (!isProviderMonitorBatchOperationKind(request.kind)) {
|
|
177
205
|
throw new Error(`Unknown monitor batch operation '${request.kind}'.`);
|
|
178
206
|
}
|
|
207
|
+
if (
|
|
208
|
+
request.control !== undefined &&
|
|
209
|
+
request.control !== 'customer_exact' &&
|
|
210
|
+
request.control !== 'system'
|
|
211
|
+
) {
|
|
212
|
+
throw new Error(`Unknown monitor batch control '${request.control}'.`);
|
|
213
|
+
}
|
|
179
214
|
const callbackBaseUrl = normalizeCallbackBaseUrl(request.callbackBaseUrl);
|
|
180
|
-
if (
|
|
181
|
-
|
|
215
|
+
if (
|
|
216
|
+
callbackBaseUrl !== undefined &&
|
|
217
|
+
request.kind !== 'deploy' &&
|
|
218
|
+
request.kind !== 'named_batch'
|
|
219
|
+
) {
|
|
220
|
+
throw new Error(
|
|
221
|
+
'callbackBaseUrl is only valid for a batch deploy or named Batch sync.',
|
|
222
|
+
);
|
|
182
223
|
}
|
|
183
224
|
if (!request.scope || typeof request.scope !== 'object') {
|
|
184
225
|
throw new Error('A monitor batch operation requires exactly one scope.');
|
|
185
226
|
}
|
|
186
227
|
const scope = request.scope;
|
|
228
|
+
if (request.kind === 'named_batch' && scope.kind !== 'named_batch') {
|
|
229
|
+
throw new Error('A named_batch operation requires a named Batch scope.');
|
|
230
|
+
}
|
|
231
|
+
if (
|
|
232
|
+
callbackBaseUrl !== undefined &&
|
|
233
|
+
request.kind === 'named_batch' &&
|
|
234
|
+
scope.kind === 'named_batch' &&
|
|
235
|
+
scope.intent !== 'sync'
|
|
236
|
+
) {
|
|
237
|
+
throw new Error(
|
|
238
|
+
'callbackBaseUrl is only valid for a named Batch sync operation.',
|
|
239
|
+
);
|
|
240
|
+
}
|
|
187
241
|
if (scope.kind === 'keys') {
|
|
188
242
|
const keys = [...new Set(scope.monitorKeys.map((key) => key.trim()))];
|
|
189
243
|
if (
|
|
@@ -294,6 +348,23 @@ export function validateProviderMonitorBatchOperationRequest(
|
|
|
294
348
|
...(callbackBaseUrl === undefined ? {} : { callbackBaseUrl }),
|
|
295
349
|
};
|
|
296
350
|
}
|
|
351
|
+
if (scope.kind === 'named_batch') {
|
|
352
|
+
const batchId = scope.batchId.trim();
|
|
353
|
+
if (!batchId || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(batchId)) {
|
|
354
|
+
throw new Error('A named Batch scope requires a valid batch id.');
|
|
355
|
+
}
|
|
356
|
+
if (!Number.isSafeInteger(scope.generation) || scope.generation < 1) {
|
|
357
|
+
throw new Error('A named Batch scope requires a positive generation.');
|
|
358
|
+
}
|
|
359
|
+
if (request.kind !== 'named_batch') {
|
|
360
|
+
throw new Error('A named Batch scope is only valid for named_batch.');
|
|
361
|
+
}
|
|
362
|
+
return {
|
|
363
|
+
...request,
|
|
364
|
+
scope: { ...scope, batchId },
|
|
365
|
+
...(callbackBaseUrl === undefined ? {} : { callbackBaseUrl }),
|
|
366
|
+
};
|
|
367
|
+
}
|
|
297
368
|
if (scope.kind === 'global_audit') {
|
|
298
369
|
const runId = scope.runId.trim();
|
|
299
370
|
const monitorKey = scope.monitorKey?.trim();
|
|
@@ -304,7 +375,9 @@ export function validateProviderMonitorBatchOperationRequest(
|
|
|
304
375
|
throw new Error('A global-audit scope is only valid for repair.');
|
|
305
376
|
}
|
|
306
377
|
if (scope.monitorKey !== undefined && !monitorKey) {
|
|
307
|
-
throw new Error(
|
|
378
|
+
throw new Error(
|
|
379
|
+
'A global-audit monitor key, when supplied, must be non-empty.',
|
|
380
|
+
);
|
|
308
381
|
}
|
|
309
382
|
return {
|
|
310
383
|
...request,
|
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The customer-visible
|
|
3
|
-
*
|
|
2
|
+
* The complete customer-visible lifecycle of a monitor.
|
|
3
|
+
*
|
|
4
|
+
* Provider work, retries, and audit evidence intentionally live on separate
|
|
5
|
+
* axes. They must never become additional values in this union.
|
|
6
|
+
*/
|
|
7
|
+
export type ProviderMonitorCustomerState =
|
|
8
|
+
| 'active'
|
|
9
|
+
| 'billing_suspended'
|
|
10
|
+
| 'disabled';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The old two-value desired-state type. Keep it for control-plane callers and
|
|
14
|
+
* the deprecated `status` response alias while clients migrate to `state`.
|
|
4
15
|
*/
|
|
5
16
|
export type ProviderMonitorState = 'active' | 'inactive';
|
|
6
17
|
|
|
18
|
+
export type ProviderMonitorStatusAlias = ProviderMonitorState;
|
|
19
|
+
|
|
7
20
|
declare const providerMonitorResourceIdBrand: unique symbol;
|
|
8
21
|
|
|
9
22
|
/**
|
|
@@ -64,7 +77,9 @@ export function providerMonitorOperationalState(input: {
|
|
|
64
77
|
desired: ProviderMonitorState;
|
|
65
78
|
providerResourceId: string | null | undefined;
|
|
66
79
|
}): OperationalProviderMonitorState {
|
|
67
|
-
const providerResourceId = providerMonitorResourceId(
|
|
80
|
+
const providerResourceId = providerMonitorResourceId(
|
|
81
|
+
input.providerResourceId,
|
|
82
|
+
);
|
|
68
83
|
if (input.desired === 'active' && providerResourceId !== null) {
|
|
69
84
|
return {
|
|
70
85
|
state: 'active',
|
|
@@ -81,6 +96,62 @@ export function providerMonitorOperationalState(input: {
|
|
|
81
96
|
};
|
|
82
97
|
}
|
|
83
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Project untrusted persisted billing state onto the only billing lifecycle
|
|
101
|
+
* values that are meaningful to a customer. Cleanup progress is a job detail;
|
|
102
|
+
* an unresolved billing admission is unavailable, not a new public state.
|
|
103
|
+
*/
|
|
104
|
+
export function providerMonitorCustomerStateForBillingState(
|
|
105
|
+
billingState: unknown,
|
|
106
|
+
): ProviderMonitorCustomerState {
|
|
107
|
+
switch (billingState) {
|
|
108
|
+
case 'active':
|
|
109
|
+
return 'active';
|
|
110
|
+
case 'billing_suspended':
|
|
111
|
+
case 'upstream_disable_pending':
|
|
112
|
+
case 'upstream_disable_failed':
|
|
113
|
+
return 'billing_suspended';
|
|
114
|
+
case 'disabled':
|
|
115
|
+
case 'billing_pending':
|
|
116
|
+
case 'preflight_denied':
|
|
117
|
+
default:
|
|
118
|
+
return 'disabled';
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The single public monitor-state projection. A billing suspension owns the
|
|
124
|
+
* off state. Otherwise `active` is operational proof: a committed active
|
|
125
|
+
* lifecycle and a non-empty provider resource binding. Internal billing
|
|
126
|
+
* bookkeeping such as an unresolved usage charge must not make a usable
|
|
127
|
+
* provider resource look disabled.
|
|
128
|
+
*/
|
|
129
|
+
export function providerMonitorCustomerState(input: {
|
|
130
|
+
lifecycleStatus?: string | null;
|
|
131
|
+
billingState?: unknown;
|
|
132
|
+
providerResourceId?: string | null | undefined;
|
|
133
|
+
}): ProviderMonitorCustomerState {
|
|
134
|
+
const billingState = providerMonitorCustomerStateForBillingState(
|
|
135
|
+
input.billingState,
|
|
136
|
+
);
|
|
137
|
+
if (billingState === 'billing_suspended') return billingState;
|
|
138
|
+
|
|
139
|
+
const providerResourceId = providerMonitorResourceId(
|
|
140
|
+
input.providerResourceId,
|
|
141
|
+
);
|
|
142
|
+
if (input.lifecycleStatus === 'active' && providerResourceId !== null) {
|
|
143
|
+
return 'active';
|
|
144
|
+
}
|
|
145
|
+
return 'disabled';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Deprecated compatibility projection for the old `status`/`active` keys. */
|
|
149
|
+
export function providerMonitorStatusAlias(
|
|
150
|
+
state: ProviderMonitorCustomerState,
|
|
151
|
+
): ProviderMonitorStatusAlias {
|
|
152
|
+
return state === 'active' ? 'active' : 'inactive';
|
|
153
|
+
}
|
|
154
|
+
|
|
84
155
|
/**
|
|
85
156
|
* Runtime boundary for code that is about to publish or administer an active
|
|
86
157
|
* resource. Its return type is the same proof exposed to callers: an active
|
|
@@ -102,9 +173,13 @@ export function requireActiveProviderMonitorBinding(input: {
|
|
|
102
173
|
}
|
|
103
174
|
|
|
104
175
|
/**
|
|
105
|
-
*
|
|
106
|
-
* compatibility boundary.
|
|
107
|
-
*
|
|
176
|
+
* Recover desired intent from a historical control-record lifecycle label at
|
|
177
|
+
* the sole compatibility boundary. This does not prove that a monitor is
|
|
178
|
+
* operationally active: customer projections must still require a committed
|
|
179
|
+
* provider resource through {@link providerMonitorCustomerState}.
|
|
180
|
+
*
|
|
181
|
+
* New code persists `desired`; this mapping exists only while the online
|
|
182
|
+
* backfill drains existing rows.
|
|
108
183
|
*/
|
|
109
184
|
export function providerMonitorStateForLegacyStatus(
|
|
110
185
|
status: string | undefined,
|
|
@@ -1,10 +1,26 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
/** Current values allowed in providerMonitorDefinitions.status. */
|
|
2
|
+
export const PROVIDER_MONITOR_PERSISTED_STATUSES = [
|
|
3
|
+
'active',
|
|
4
|
+
'disabled',
|
|
5
|
+
'deleted',
|
|
6
|
+
'deleting',
|
|
7
|
+
'error',
|
|
8
|
+
'pending_create',
|
|
9
|
+
] as const;
|
|
10
|
+
|
|
11
|
+
/** Compatibility-only inputs normalized by current writers before storage. */
|
|
12
|
+
export const PROVIDER_MONITOR_LEGACY_REPLACEMENT_STATUSES = [
|
|
13
|
+
'replacement_pending',
|
|
14
|
+
'replacement_active_old_cleanup_pending',
|
|
15
|
+
'replacement_cleanup_failed',
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
export const PROVIDER_MONITOR_STATUSES = [
|
|
19
|
+
...PROVIDER_MONITOR_LEGACY_REPLACEMENT_STATUSES,
|
|
20
|
+
...PROVIDER_MONITOR_PERSISTED_STATUSES,
|
|
21
|
+
] as const;
|
|
22
|
+
|
|
23
|
+
export type ProviderMonitorStatus = (typeof PROVIDER_MONITOR_STATUSES)[number];
|
|
24
|
+
|
|
25
|
+
export type PersistedProviderMonitorStatus =
|
|
26
|
+
(typeof PROVIDER_MONITOR_PERSISTED_STATUSES)[number];
|
|
@@ -456,6 +456,26 @@ export type QueueHealth = {
|
|
|
456
456
|
readonly worstAgeRatio?: number;
|
|
457
457
|
};
|
|
458
458
|
|
|
459
|
+
/**
|
|
460
|
+
* Small operator-facing state set for the dashboard and admin CLI.
|
|
461
|
+
*
|
|
462
|
+
* This is deliberately derived from the source-owned depth buckets. It is
|
|
463
|
+
* not a replacement for the queue's durable state machine and therefore does
|
|
464
|
+
* not create a new persisted state or a new retry policy.
|
|
465
|
+
*/
|
|
466
|
+
export type QueueOperatorState =
|
|
467
|
+
| 'draining'
|
|
468
|
+
| 'stalled'
|
|
469
|
+
| 'held'
|
|
470
|
+
| 'quarantined'
|
|
471
|
+
| 'clear'
|
|
472
|
+
| 'unknown';
|
|
473
|
+
|
|
474
|
+
export type QueueOperatorStateSnapshot = {
|
|
475
|
+
readonly state: QueueOperatorState;
|
|
476
|
+
readonly reason: string;
|
|
477
|
+
};
|
|
478
|
+
|
|
459
479
|
export const QUEUE_HEALTH_TELEMETRY_EVENT = 'operations.queue.health' as const;
|
|
460
480
|
export const QUEUE_HEALTH_TELEMETRY_SCHEMA_VERSION = 1 as const;
|
|
461
481
|
const UNOBSERVED_SIGNAL_REPEAT_MS = 5 * 60_000;
|
|
@@ -475,6 +495,8 @@ export type QueueHealthSignal = {
|
|
|
475
495
|
readonly adapter: string;
|
|
476
496
|
readonly observedAt: number;
|
|
477
497
|
readonly health: QueueHealth;
|
|
498
|
+
readonly operatorState: QueueOperatorState;
|
|
499
|
+
readonly operatorReason: string;
|
|
478
500
|
readonly previousHealth?: QueueHealth;
|
|
479
501
|
readonly warningDefinition: string;
|
|
480
502
|
readonly failureDefinition: string;
|
|
@@ -503,6 +525,11 @@ export function deriveQueueHealthSignal(input: {
|
|
|
503
525
|
}): QueueHealthSignal | null {
|
|
504
526
|
const now = input.now ?? input.observation.observedAt;
|
|
505
527
|
const health = deriveQueueHealth(input.definition, input.observation, now);
|
|
528
|
+
const operator = deriveQueueOperatorState(
|
|
529
|
+
input.definition,
|
|
530
|
+
input.observation,
|
|
531
|
+
now,
|
|
532
|
+
);
|
|
506
533
|
const previousHealth = input.previousObservation
|
|
507
534
|
? deriveQueueHealth(input.definition, input.previousObservation, now)
|
|
508
535
|
: undefined;
|
|
@@ -546,6 +573,8 @@ export function deriveQueueHealthSignal(input: {
|
|
|
546
573
|
adapter: input.observation.adapter,
|
|
547
574
|
observedAt: input.observation.observedAt,
|
|
548
575
|
health,
|
|
576
|
+
operatorState: operator.state,
|
|
577
|
+
operatorReason: operator.reason,
|
|
549
578
|
...(previousHealth === undefined ? {} : { previousHealth }),
|
|
550
579
|
warningDefinition: input.definition.warningDefinition,
|
|
551
580
|
failureDefinition: input.definition.failureDefinition,
|
|
@@ -694,6 +723,105 @@ export function deriveQueueHealth(
|
|
|
694
723
|
};
|
|
695
724
|
}
|
|
696
725
|
|
|
726
|
+
/**
|
|
727
|
+
* Collapse the detailed queue buckets into a small, actionable operator
|
|
728
|
+
* vocabulary. The precedence is intentional: quarantined and held work must
|
|
729
|
+
* not be hidden by a large ready count, while an old ready-only queue is
|
|
730
|
+
* called stalled only when no in-flight work is observed.
|
|
731
|
+
*/
|
|
732
|
+
export function deriveQueueOperatorState(
|
|
733
|
+
definition: QueueDefinition,
|
|
734
|
+
observation: QueueObservation | undefined,
|
|
735
|
+
now = Date.now(),
|
|
736
|
+
): QueueOperatorStateSnapshot {
|
|
737
|
+
const health = deriveQueueHealth(definition, observation, now);
|
|
738
|
+
if (
|
|
739
|
+
observation === undefined ||
|
|
740
|
+
health.status === 'unobserved' ||
|
|
741
|
+
health.reason === 'invalid_observation'
|
|
742
|
+
) {
|
|
743
|
+
return {
|
|
744
|
+
state: 'unknown',
|
|
745
|
+
reason: 'Queue observation is missing or stale.',
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const ready = measuredValue(observation.depth.ready);
|
|
750
|
+
const delayed = measuredValue(observation.depth.delayed);
|
|
751
|
+
const inFlight = measuredValue(observation.depth.inFlight);
|
|
752
|
+
const blocked = measuredValue(observation.depth.blocked);
|
|
753
|
+
const deadLettered = measuredValue(observation.depth.deadLettered);
|
|
754
|
+
const oldestReadyMs = measuredValue(observation.oldestAgeMs.ready);
|
|
755
|
+
|
|
756
|
+
if (deadLettered !== undefined && deadLettered > 0) {
|
|
757
|
+
return {
|
|
758
|
+
state: 'quarantined',
|
|
759
|
+
reason: 'Dead-lettered work requires an owner recovery path.',
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
if (blocked !== undefined && blocked > 0) {
|
|
763
|
+
return {
|
|
764
|
+
state: 'held',
|
|
765
|
+
reason: 'Blocked work is intentionally held for owner review.',
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (
|
|
770
|
+
Object.values(observation.depth).some(
|
|
771
|
+
(measurement) =>
|
|
772
|
+
measurement.kind === 'lower_bound' && measurement.value === 0,
|
|
773
|
+
)
|
|
774
|
+
) {
|
|
775
|
+
return {
|
|
776
|
+
state: 'unknown',
|
|
777
|
+
reason: 'A bounded read cannot prove that the queue is clear.',
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
if (
|
|
781
|
+
Object.values(observation.depth).some(
|
|
782
|
+
(measurement) => measurement.kind === 'unavailable',
|
|
783
|
+
)
|
|
784
|
+
) {
|
|
785
|
+
return {
|
|
786
|
+
state: 'unknown',
|
|
787
|
+
reason: 'A depth bucket is unavailable; queue state cannot be proven.',
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
if (
|
|
792
|
+
ready !== undefined &&
|
|
793
|
+
ready > 0 &&
|
|
794
|
+
inFlight === 0 &&
|
|
795
|
+
oldestReadyMs !== undefined &&
|
|
796
|
+
oldestReadyMs >= definition.ageSlo.failMs
|
|
797
|
+
) {
|
|
798
|
+
return {
|
|
799
|
+
state: 'stalled',
|
|
800
|
+
reason:
|
|
801
|
+
'Ready work is past its failure age SLO and no in-flight work is observed.',
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
if (
|
|
806
|
+
(ready !== undefined && ready > 0) ||
|
|
807
|
+
(delayed !== undefined && delayed > 0) ||
|
|
808
|
+
(inFlight !== undefined && inFlight > 0)
|
|
809
|
+
) {
|
|
810
|
+
return {
|
|
811
|
+
state: 'draining',
|
|
812
|
+
reason:
|
|
813
|
+
health.reason === 'age_breached'
|
|
814
|
+
? 'Work is active, but its oldest item is past the age SLO.'
|
|
815
|
+
: 'Work is eligible, scheduled, or currently in flight.',
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
return {
|
|
820
|
+
state: 'clear',
|
|
821
|
+
reason: 'No active, held, or quarantined work is observed.',
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
|
|
697
825
|
// Legacy v1 snapshot helpers remain exported for the webhook/cron collectors
|
|
698
826
|
// during the v1 -> v2 observability rollout. They are intentionally isolated
|
|
699
827
|
// from the scheduler-neutral v2 contract above.
|
|
@@ -975,6 +975,24 @@
|
|
|
975
975
|
"failureAlert": "convex_action_wrapper",
|
|
976
976
|
"domainReportRequired": true
|
|
977
977
|
},
|
|
978
|
+
{
|
|
979
|
+
"id": "convex.monitor-provider-repair-recovery-tick",
|
|
980
|
+
"name": "Monitor provider repair recovery tick",
|
|
981
|
+
"scheduler": "convex_cron",
|
|
982
|
+
"cadence": "every 60s",
|
|
983
|
+
"timezone": "UTC",
|
|
984
|
+
"source": "packages/convex/crons.ts",
|
|
985
|
+
"locator": "monitor provider repair recovery tick",
|
|
986
|
+
"owner": "monitors",
|
|
987
|
+
"criticality": "critical",
|
|
988
|
+
"purpose": "Reclaim and wake durable provider monitor repair operations without delaying ordinary Fleet convergence.",
|
|
989
|
+
"successDefinition": "Due repair operations are re-issued or durably accounted; no due repairs is a valid no-op.",
|
|
990
|
+
"failureDefinition": "A repair lease or continuation is stranded, or its recovery state cannot persist.",
|
|
991
|
+
"startGraceMs": 120000,
|
|
992
|
+
"runTimeoutMs": 1800000,
|
|
993
|
+
"failureAlert": "convex_action_wrapper",
|
|
994
|
+
"domainReportRequired": true
|
|
995
|
+
},
|
|
978
996
|
{
|
|
979
997
|
"id": "convex.recover-play-webhook-receipts",
|
|
980
998
|
"name": "Recover Play webhook receipts",
|
|
@@ -15,6 +15,7 @@ export const MANUALLY_RUNNABLE_CONVEX_CRON_JOB_IDS = [
|
|
|
15
15
|
'convex.cleanup-expired-domain-health-cache',
|
|
16
16
|
'convex.salesforce-oauth-refresh-maintenance',
|
|
17
17
|
'convex.monitor-fleet-convergence-tick',
|
|
18
|
+
'convex.monitor-provider-repair-recovery-tick',
|
|
18
19
|
'convex.monitor-deployment-job-tick',
|
|
19
20
|
'convex.provider-monitor-global-audit-daily',
|
|
20
21
|
'convex.recover-notification-delivery-work',
|
|
@@ -328,8 +328,8 @@ function previewDetailFor(substep: PlayStaticSubstep): string | null {
|
|
|
328
328
|
? `${substep.steps.length} fallbacks`
|
|
329
329
|
: 'first match wins';
|
|
330
330
|
case 'step_suite': {
|
|
331
|
-
const count =
|
|
332
|
-
return
|
|
331
|
+
const count = effectiveStepCount(substep);
|
|
332
|
+
return `${count} step${count === 1 ? '' : 's'}`;
|
|
333
333
|
}
|
|
334
334
|
case 'control_flow': {
|
|
335
335
|
const count = substep.steps.reduce(
|