deepline 0.3.105 → 0.3.107
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/shared_libs/observability/queue-health.ts +678 -0
- package/dist/bundling-sources/shared_libs/observability/scheduled-jobs.json +424 -70
- package/dist/bundling-sources/shared_libs/observability/scheduled-work-manual.ts +1 -0
- package/dist/bundling-sources/shared_libs/observability/scheduled-work-v2.ts +380 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-queue-health.ts +718 -0
- package/dist/cli/index.js +24 -8
- package/dist/cli/index.mjs +24 -8
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- 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
|
@@ -18,6 +18,7 @@ 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.observe-operational-queue-health',
|
|
21
22
|
'convex.cleanup-expired-notification-audit-records',
|
|
22
23
|
'convex.fail-stale-play-deployments',
|
|
23
24
|
] as const;
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scheduler-neutral scheduled-work contract.
|
|
3
|
+
*
|
|
4
|
+
* This is deliberately separate from scheduled-work.ts. That module is the
|
|
5
|
+
* v1 wire contract and is consumed by existing adapters. New adapters should
|
|
6
|
+
* use this module: all domain counters and declared condition codes are
|
|
7
|
+
* required, and no result-shape inference is performed.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const SCHEDULED_WORK_CONTRACT_VERSION = 2 as const;
|
|
11
|
+
|
|
12
|
+
const SCHEDULED_WORK_BOILERPLATE = [
|
|
13
|
+
'publish its declared domain outcome',
|
|
14
|
+
'Declared work completes with valid accounting',
|
|
15
|
+
'the trigger is missed, execution is stuck, reporting is invalid',
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
export type ScheduledCatalogEntry = {
|
|
19
|
+
readonly id: string;
|
|
20
|
+
readonly name: string;
|
|
21
|
+
readonly scheduler: string;
|
|
22
|
+
readonly cadence: string;
|
|
23
|
+
readonly timezone: string;
|
|
24
|
+
readonly source: string;
|
|
25
|
+
readonly locator: string;
|
|
26
|
+
readonly owner: string;
|
|
27
|
+
readonly criticality: string;
|
|
28
|
+
readonly purpose: string;
|
|
29
|
+
readonly successDefinition: string;
|
|
30
|
+
readonly failureDefinition: string;
|
|
31
|
+
/** Every root must report a concrete domain result, not only scheduler liveness. */
|
|
32
|
+
readonly domainReportRequired: true;
|
|
33
|
+
readonly startGraceMs: number;
|
|
34
|
+
readonly runTimeoutMs: number;
|
|
35
|
+
readonly failureAlert: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export function validateScheduledWorkCatalog(
|
|
39
|
+
catalog: readonly ScheduledCatalogEntry[],
|
|
40
|
+
): void {
|
|
41
|
+
const ids = new Set<string>();
|
|
42
|
+
for (const entry of catalog) {
|
|
43
|
+
for (const field of [
|
|
44
|
+
'id',
|
|
45
|
+
'name',
|
|
46
|
+
'scheduler',
|
|
47
|
+
'cadence',
|
|
48
|
+
'timezone',
|
|
49
|
+
'source',
|
|
50
|
+
'locator',
|
|
51
|
+
'owner',
|
|
52
|
+
'criticality',
|
|
53
|
+
'purpose',
|
|
54
|
+
'successDefinition',
|
|
55
|
+
'failureDefinition',
|
|
56
|
+
'failureAlert',
|
|
57
|
+
] as const)
|
|
58
|
+
requireText(
|
|
59
|
+
entry[field],
|
|
60
|
+
`catalog entry ${entry.id || '<unknown>'}: ${field}`,
|
|
61
|
+
);
|
|
62
|
+
if (entry.domainReportRequired !== true) {
|
|
63
|
+
throw new Error(`${entry.id}: domainReportRequired must be true`);
|
|
64
|
+
}
|
|
65
|
+
for (const field of [
|
|
66
|
+
'purpose',
|
|
67
|
+
'successDefinition',
|
|
68
|
+
'failureDefinition',
|
|
69
|
+
] as const) {
|
|
70
|
+
if (
|
|
71
|
+
SCHEDULED_WORK_BOILERPLATE.some((phrase) =>
|
|
72
|
+
entry[field].includes(phrase),
|
|
73
|
+
)
|
|
74
|
+
) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`${entry.id}: ${field} contains scheduled-work boilerplate`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (ids.has(entry.id))
|
|
81
|
+
throw new Error(`duplicate scheduled-work id: ${entry.id}`);
|
|
82
|
+
ids.add(entry.id);
|
|
83
|
+
if (!Number.isFinite(entry.startGraceMs) || entry.startGraceMs < 0) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`${entry.id}: startGraceMs must be finite and non-negative`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
if (!Number.isFinite(entry.runTimeoutMs) || entry.runTimeoutMs <= 0) {
|
|
89
|
+
throw new Error(`${entry.id}: runTimeoutMs must be finite and positive`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export type Measured<T> =
|
|
95
|
+
| { readonly kind: 'exact'; readonly value: T }
|
|
96
|
+
| { readonly kind: 'lower_bound'; readonly value: T }
|
|
97
|
+
| { readonly kind: 'not_applicable'; readonly reason: string }
|
|
98
|
+
| { readonly kind: 'unavailable'; readonly reason: string };
|
|
99
|
+
|
|
100
|
+
export function exact<T>(value: T): Measured<T> {
|
|
101
|
+
return { kind: 'exact', value };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function lowerBound<T>(value: T): Measured<T> {
|
|
105
|
+
return { kind: 'lower_bound', value };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function notApplicable<T = never>(reason: string): Measured<T> {
|
|
109
|
+
return { kind: 'not_applicable', reason };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function unavailable<T = never>(reason: string): Measured<T> {
|
|
113
|
+
return { kind: 'unavailable', reason };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function measuredValue<T>(measurement: Measured<T>): T | undefined {
|
|
117
|
+
return measurement.kind === 'exact' || measurement.kind === 'lower_bound'
|
|
118
|
+
? measurement.value
|
|
119
|
+
: undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export type ScheduledConditionSeverity = 'warning' | 'failure';
|
|
123
|
+
export type ScheduledConditionOwner = 'customer' | 'provider' | 'deepline';
|
|
124
|
+
|
|
125
|
+
export type ScheduledCondition = {
|
|
126
|
+
readonly severity: ScheduledConditionSeverity;
|
|
127
|
+
readonly owner: ScheduledConditionOwner;
|
|
128
|
+
readonly meaning: string;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export type ScheduledTrigger =
|
|
132
|
+
| {
|
|
133
|
+
readonly kind: 'periodic';
|
|
134
|
+
readonly cadence: string;
|
|
135
|
+
readonly startGraceMs: number;
|
|
136
|
+
}
|
|
137
|
+
| { readonly kind: 'on_demand' };
|
|
138
|
+
|
|
139
|
+
export type ScheduledWorkDefinition<
|
|
140
|
+
W extends string = string,
|
|
141
|
+
F extends string = string,
|
|
142
|
+
> = {
|
|
143
|
+
readonly id: string;
|
|
144
|
+
readonly owner: string;
|
|
145
|
+
readonly purpose: string;
|
|
146
|
+
readonly unit: string;
|
|
147
|
+
readonly trigger: ScheduledTrigger;
|
|
148
|
+
readonly runTimeoutMs: number;
|
|
149
|
+
readonly conditions: Readonly<Record<W | F, ScheduledCondition>>;
|
|
150
|
+
/** Type-only witnesses make the intended condition sets visible to TS. */
|
|
151
|
+
readonly warningCodes?: readonly W[];
|
|
152
|
+
readonly failureCodes?: readonly F[];
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export type ScheduledWorkReport<
|
|
156
|
+
W extends string = string,
|
|
157
|
+
F extends string = string,
|
|
158
|
+
> = {
|
|
159
|
+
readonly examined: number;
|
|
160
|
+
readonly actionable: number;
|
|
161
|
+
readonly dispositions: {
|
|
162
|
+
readonly succeeded: number;
|
|
163
|
+
readonly skipped: number;
|
|
164
|
+
readonly deferred: number;
|
|
165
|
+
readonly failures: Readonly<Record<F, number>>;
|
|
166
|
+
};
|
|
167
|
+
readonly warnings: Readonly<Record<W, number>>;
|
|
168
|
+
readonly failures: Readonly<Record<F, number>>;
|
|
169
|
+
readonly remaining: Measured<number>;
|
|
170
|
+
readonly noop?: { readonly reason: string };
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
type WarningCodes<C extends Record<string, ScheduledCondition>> = {
|
|
174
|
+
[K in keyof C]: C[K] extends { readonly severity: 'warning' } ? K : never;
|
|
175
|
+
}[keyof C] &
|
|
176
|
+
string;
|
|
177
|
+
|
|
178
|
+
type FailureCodes<C extends Record<string, ScheduledCondition>> = {
|
|
179
|
+
[K in keyof C]: C[K] extends { readonly severity: 'failure' } ? K : never;
|
|
180
|
+
}[keyof C] &
|
|
181
|
+
string;
|
|
182
|
+
|
|
183
|
+
export type ScheduledWorkOccurrence = {
|
|
184
|
+
readonly occurrenceId: string;
|
|
185
|
+
readonly invocationId?: string;
|
|
186
|
+
readonly expectedAt: number;
|
|
187
|
+
readonly startedAt?: number;
|
|
188
|
+
readonly heartbeatAt?: number;
|
|
189
|
+
readonly completedAt?: number;
|
|
190
|
+
readonly outcome?: 'succeeded' | 'warning' | 'failed';
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
export type ScheduledWorkHealthStatus =
|
|
194
|
+
| 'waiting'
|
|
195
|
+
| 'running'
|
|
196
|
+
| 'healthy'
|
|
197
|
+
| 'warning'
|
|
198
|
+
| 'failed'
|
|
199
|
+
| 'missed_trigger'
|
|
200
|
+
| 'stuck'
|
|
201
|
+
| 'on_demand'
|
|
202
|
+
| 'unobserved';
|
|
203
|
+
|
|
204
|
+
export type ScheduledWorkHealth = {
|
|
205
|
+
readonly status: ScheduledWorkHealthStatus;
|
|
206
|
+
readonly reason:
|
|
207
|
+
| 'before_start_deadline'
|
|
208
|
+
| 'started'
|
|
209
|
+
| 'completed'
|
|
210
|
+
| 'completed_with_warnings'
|
|
211
|
+
| 'completed_with_failures'
|
|
212
|
+
| 'missed_trigger'
|
|
213
|
+
| 'runtime_timeout'
|
|
214
|
+
| 'heartbeat_timeout'
|
|
215
|
+
| 'on_demand_idle'
|
|
216
|
+
| 'missing_observation';
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
export function defineScheduledWork<
|
|
220
|
+
const C extends Readonly<Record<string, ScheduledCondition>>,
|
|
221
|
+
>(
|
|
222
|
+
definition: Omit<
|
|
223
|
+
ScheduledWorkDefinition<WarningCodes<C>, FailureCodes<C>>,
|
|
224
|
+
'conditions'
|
|
225
|
+
> & { readonly conditions: C },
|
|
226
|
+
) {
|
|
227
|
+
validateScheduledWorkDefinition(definition);
|
|
228
|
+
return {
|
|
229
|
+
definition,
|
|
230
|
+
report(
|
|
231
|
+
report: ScheduledWorkReport<WarningCodes<C>, FailureCodes<C>>,
|
|
232
|
+
): ScheduledWorkReport<WarningCodes<C>, FailureCodes<C>> {
|
|
233
|
+
validateScheduledWorkReport(definition, report);
|
|
234
|
+
return report;
|
|
235
|
+
},
|
|
236
|
+
} as const;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function validateScheduledWorkDefinition(
|
|
240
|
+
definition: ScheduledWorkDefinition,
|
|
241
|
+
): void {
|
|
242
|
+
requireText(definition.id, 'id');
|
|
243
|
+
requireText(definition.owner, 'owner');
|
|
244
|
+
requireText(definition.purpose, 'purpose');
|
|
245
|
+
requireText(definition.unit, 'unit');
|
|
246
|
+
if (
|
|
247
|
+
!Number.isFinite(definition.runTimeoutMs) ||
|
|
248
|
+
definition.runTimeoutMs <= 0
|
|
249
|
+
) {
|
|
250
|
+
throw new Error(`${definition.id}: runTimeoutMs must be positive`);
|
|
251
|
+
}
|
|
252
|
+
if (definition.trigger.kind === 'periodic') {
|
|
253
|
+
requireText(definition.trigger.cadence, `${definition.id}: cadence`);
|
|
254
|
+
if (
|
|
255
|
+
!Number.isFinite(definition.trigger.startGraceMs) ||
|
|
256
|
+
definition.trigger.startGraceMs < 0
|
|
257
|
+
) {
|
|
258
|
+
throw new Error(`${definition.id}: startGraceMs must be non-negative`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const codes = Object.entries(definition.conditions);
|
|
262
|
+
if (codes.length === 0) {
|
|
263
|
+
throw new Error(`${definition.id}: declare at least one condition code`);
|
|
264
|
+
}
|
|
265
|
+
const codeSet = new Set<string>();
|
|
266
|
+
for (const [code, condition] of codes) {
|
|
267
|
+
requireText(code, `${definition.id}: condition code`);
|
|
268
|
+
if (codeSet.has(code))
|
|
269
|
+
throw new Error(`${definition.id}: duplicate condition ${code}`);
|
|
270
|
+
codeSet.add(code);
|
|
271
|
+
requireText(condition.meaning, `${definition.id}: ${code}.meaning`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function validateScheduledWorkReport<W extends string, F extends string>(
|
|
276
|
+
definition: ScheduledWorkDefinition<W, F>,
|
|
277
|
+
report: ScheduledWorkReport<W, F>,
|
|
278
|
+
): void {
|
|
279
|
+
for (const [name, value] of [
|
|
280
|
+
['examined', report.examined],
|
|
281
|
+
['actionable', report.actionable],
|
|
282
|
+
['succeeded', report.dispositions.succeeded],
|
|
283
|
+
['skipped', report.dispositions.skipped],
|
|
284
|
+
['deferred', report.dispositions.deferred],
|
|
285
|
+
] as const)
|
|
286
|
+
requireCount(definition.id, name, value);
|
|
287
|
+
if (report.actionable > report.examined) {
|
|
288
|
+
throw new Error(`${definition.id}: actionable cannot exceed examined`);
|
|
289
|
+
}
|
|
290
|
+
const conditionCodes = Object.keys(definition.conditions);
|
|
291
|
+
for (const code of conditionCodes) {
|
|
292
|
+
const condition = (
|
|
293
|
+
definition.conditions as Readonly<Record<string, ScheduledCondition>>
|
|
294
|
+
)[code]!;
|
|
295
|
+
const value =
|
|
296
|
+
condition.severity === 'warning'
|
|
297
|
+
? report.warnings[code as W]
|
|
298
|
+
: report.failures[code as F];
|
|
299
|
+
requireCount(definition.id, `${code} condition`, value);
|
|
300
|
+
}
|
|
301
|
+
for (const [code, value] of Object.entries(report.dispositions.failures)) {
|
|
302
|
+
requireCount(definition.id, `disposition ${code}`, value as number);
|
|
303
|
+
}
|
|
304
|
+
if (
|
|
305
|
+
report.remaining.kind === 'exact' ||
|
|
306
|
+
report.remaining.kind === 'lower_bound'
|
|
307
|
+
) {
|
|
308
|
+
requireCount(definition.id, 'remaining', report.remaining.value);
|
|
309
|
+
} else
|
|
310
|
+
requireText(report.remaining.reason, `${definition.id}: remaining.reason`);
|
|
311
|
+
const totals =
|
|
312
|
+
report.dispositions.succeeded +
|
|
313
|
+
report.dispositions.skipped +
|
|
314
|
+
report.dispositions.deferred +
|
|
315
|
+
(Object.values(report.dispositions.failures) as number[]).reduce(
|
|
316
|
+
(sum, n) => sum + n,
|
|
317
|
+
0,
|
|
318
|
+
);
|
|
319
|
+
if (totals > report.actionable) {
|
|
320
|
+
throw new Error(`${definition.id}: dispositions exceed actionable work`);
|
|
321
|
+
}
|
|
322
|
+
if (report.actionable === 0 && !report.noop) {
|
|
323
|
+
throw new Error(
|
|
324
|
+
`${definition.id}: zero actionable work requires noop.reason`,
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
if (report.noop)
|
|
328
|
+
requireText(report.noop.reason, `${definition.id}: noop.reason`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export function deriveScheduledWorkHealth(
|
|
332
|
+
definition: ScheduledWorkDefinition,
|
|
333
|
+
occurrence: ScheduledWorkOccurrence | undefined,
|
|
334
|
+
now = Date.now(),
|
|
335
|
+
): ScheduledWorkHealth {
|
|
336
|
+
if (definition.trigger.kind === 'on_demand' && !occurrence) {
|
|
337
|
+
return { status: 'on_demand', reason: 'on_demand_idle' };
|
|
338
|
+
}
|
|
339
|
+
if (!occurrence)
|
|
340
|
+
return { status: 'unobserved', reason: 'missing_observation' };
|
|
341
|
+
const startDeadline =
|
|
342
|
+
occurrence.expectedAt +
|
|
343
|
+
(definition.trigger.kind === 'periodic'
|
|
344
|
+
? definition.trigger.startGraceMs
|
|
345
|
+
: 0);
|
|
346
|
+
if (!occurrence.startedAt) {
|
|
347
|
+
return now <= startDeadline
|
|
348
|
+
? { status: 'waiting', reason: 'before_start_deadline' }
|
|
349
|
+
: { status: 'missed_trigger', reason: 'missed_trigger' };
|
|
350
|
+
}
|
|
351
|
+
if (occurrence.completedAt !== undefined) {
|
|
352
|
+
return occurrence.outcome === 'failed'
|
|
353
|
+
? { status: 'failed', reason: 'completed_with_failures' }
|
|
354
|
+
: occurrence.outcome === 'warning'
|
|
355
|
+
? { status: 'warning', reason: 'completed_with_warnings' }
|
|
356
|
+
: { status: 'healthy', reason: 'completed' };
|
|
357
|
+
}
|
|
358
|
+
const lastAlive = occurrence.heartbeatAt ?? occurrence.startedAt;
|
|
359
|
+
const timeoutAt = occurrence.startedAt + definition.runTimeoutMs;
|
|
360
|
+
if (now > timeoutAt) return { status: 'stuck', reason: 'runtime_timeout' };
|
|
361
|
+
if (now - lastAlive > Math.min(definition.runTimeoutMs, 5 * 60_000)) {
|
|
362
|
+
return { status: 'stuck', reason: 'heartbeat_timeout' };
|
|
363
|
+
}
|
|
364
|
+
return { status: 'running', reason: 'started' };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function requireText(value: string | undefined, label: string): void {
|
|
368
|
+
if (typeof value !== 'string' || value.trim() === '')
|
|
369
|
+
throw new Error(`${label} is required`);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function requireCount(
|
|
373
|
+
id: string,
|
|
374
|
+
label: string,
|
|
375
|
+
value: number | undefined,
|
|
376
|
+
): void {
|
|
377
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
|
|
378
|
+
throw new Error(`${id}: ${label} must be a finite non-negative number`);
|
|
379
|
+
}
|
|
380
|
+
}
|