@cat-factory/worker 0.171.2 → 0.173.0
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/index.d.ts.map +1 -1
- package/dist/index.js +58 -15
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/container-assembly.d.ts.map +1 -1
- package/dist/infrastructure/container-assembly.js +25 -6
- package/dist/infrastructure/container-assembly.js.map +1 -1
- package/dist/infrastructure/container.d.ts.map +1 -1
- package/dist/infrastructure/container.js +1 -0
- package/dist/infrastructure/container.js.map +1 -1
- package/dist/infrastructure/repositories/D1NotificationWebhookRepository.d.ts +9 -7
- package/dist/infrastructure/repositories/D1NotificationWebhookRepository.d.ts.map +1 -1
- package/dist/infrastructure/repositories/D1NotificationWebhookRepository.js +61 -27
- package/dist/infrastructure/repositories/D1NotificationWebhookRepository.js.map +1 -1
- package/dist/infrastructure/workflows/sweeper.d.ts +55 -12
- package/dist/infrastructure/workflows/sweeper.d.ts.map +1 -1
- package/dist/infrastructure/workflows/sweeper.js +168 -20
- package/dist/infrastructure/workflows/sweeper.js.map +1 -1
- package/migrations/0085_notification_webhook_collection.sql +37 -0
- package/package.json +18 -18
|
@@ -1,38 +1,55 @@
|
|
|
1
1
|
import { parseNotificationWebhookTypes, parsePlatformAlertEvents, parseRunLifecycleEvents, } from '@cat-factory/server';
|
|
2
2
|
/**
|
|
3
|
-
* A workspace's outbound
|
|
4
|
-
* 0072, `alert_events` in 0080). All three JSON filter
|
|
5
|
-
* parsers the Drizzle repo uses, so the columns can't drift
|
|
3
|
+
* A workspace's outbound webhooks, keyed by (workspace, endpoint id) (migration 0061; `run_events`
|
|
4
|
+
* added in 0072, `alert_events` in 0080, the named-collection key in 0085). All three JSON filter
|
|
5
|
+
* columns are decoded through the SHARED parsers the Drizzle repo uses, so the columns can't drift
|
|
6
|
+
* between runtimes.
|
|
6
7
|
*/
|
|
7
8
|
export class D1NotificationWebhookRepository {
|
|
8
9
|
db;
|
|
9
10
|
constructor({ db }) {
|
|
10
11
|
this.db = db;
|
|
11
12
|
}
|
|
12
|
-
async get(workspaceId) {
|
|
13
|
+
async get(workspaceId, id) {
|
|
13
14
|
const row = await this.db
|
|
14
|
-
.prepare(`SELECT * FROM notification_webhooks WHERE workspace_id = ?`)
|
|
15
|
-
.bind(workspaceId)
|
|
15
|
+
.prepare(`SELECT * FROM notification_webhooks WHERE workspace_id = ? AND id = ?`)
|
|
16
|
+
.bind(workspaceId, id)
|
|
16
17
|
.first();
|
|
17
|
-
|
|
18
|
-
return null;
|
|
19
|
-
return {
|
|
20
|
-
workspaceId: row.workspace_id,
|
|
21
|
-
url: row.url,
|
|
22
|
-
types: parseNotificationWebhookTypes(row.types),
|
|
23
|
-
runEvents: parseRunLifecycleEvents(row.run_events),
|
|
24
|
-
alertEvents: parsePlatformAlertEvents(row.alert_events),
|
|
25
|
-
enabled: row.enabled === 1,
|
|
26
|
-
secretSealed: row.secret_sealed,
|
|
27
|
-
updatedAt: row.updated_at,
|
|
28
|
-
};
|
|
18
|
+
return row ? toRecord(row) : null;
|
|
29
19
|
}
|
|
30
|
-
async
|
|
31
|
-
|
|
20
|
+
async list(workspaceId) {
|
|
21
|
+
// `COLLATE BINARY` is SQLite's default for a text column and is spelled out only to name the
|
|
22
|
+
// order this port promises: the Drizzle mirror has to ask Postgres for `COLLATE "C"` to match,
|
|
23
|
+
// because its database-locale default would sort `web-hook` after `webhook` by ignoring the
|
|
24
|
+
// punctuation. Byte order on both, so the two runtimes return one sequence.
|
|
25
|
+
const { results } = await this.db
|
|
26
|
+
.prepare(`SELECT * FROM notification_webhooks WHERE workspace_id = ? ORDER BY id COLLATE BINARY`)
|
|
27
|
+
.bind(workspaceId)
|
|
28
|
+
.all();
|
|
29
|
+
return (results ?? []).map(toRecord);
|
|
30
|
+
}
|
|
31
|
+
async put(record, limit) {
|
|
32
|
+
// ONE statement, so the cap and the write cannot be separated by another writer. The row
|
|
33
|
+
// source is a `SELECT ... WHERE` rather than `VALUES` precisely so the admission test rides
|
|
34
|
+
// inside the insert: SQLite serializes statements, so a concurrent enrolment either sees this
|
|
35
|
+
// row or is seen by it, and can never read the same free slot twice.
|
|
36
|
+
//
|
|
37
|
+
// The predicate admits an existing endpoint unconditionally (`EXISTS`) before it consults the
|
|
38
|
+
// count, because the count includes that row: without the first half, a workspace sitting
|
|
39
|
+
// exactly at the limit could no longer edit, disable or re-point what it had already
|
|
40
|
+
// registered, which are the only ways back under it.
|
|
41
|
+
//
|
|
42
|
+
// The `WHERE` also disambiguates the parse. SQLite cannot otherwise tell an upsert's `ON
|
|
43
|
+
// CONFLICT` from a join's `ON` in an `INSERT ... SELECT`, and its documented fix is exactly a
|
|
44
|
+
// `WHERE` clause on the SELECT, which this needs anyway.
|
|
45
|
+
const result = await this.db
|
|
32
46
|
.prepare(`INSERT INTO notification_webhooks
|
|
33
|
-
(workspace_id, url, types, run_events, alert_events, enabled, secret_sealed, updated_at)
|
|
34
|
-
|
|
35
|
-
|
|
47
|
+
(workspace_id, id, name, url, types, run_events, alert_events, enabled, secret_sealed, updated_at)
|
|
48
|
+
SELECT ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
|
49
|
+
WHERE EXISTS (SELECT 1 FROM notification_webhooks WHERE workspace_id = ? AND id = ?)
|
|
50
|
+
OR (SELECT COUNT(*) FROM notification_webhooks WHERE workspace_id = ?) < ?
|
|
51
|
+
ON CONFLICT (workspace_id, id) DO UPDATE SET
|
|
52
|
+
name = excluded.name,
|
|
36
53
|
url = excluded.url,
|
|
37
54
|
types = excluded.types,
|
|
38
55
|
run_events = excluded.run_events,
|
|
@@ -40,14 +57,31 @@ export class D1NotificationWebhookRepository {
|
|
|
40
57
|
enabled = excluded.enabled,
|
|
41
58
|
secret_sealed = excluded.secret_sealed,
|
|
42
59
|
updated_at = excluded.updated_at`)
|
|
43
|
-
.bind(record.workspaceId, record.url, JSON.stringify(record.types), JSON.stringify(record.runEvents), JSON.stringify(record.alertEvents), record.enabled ? 1 : 0, record.secretSealed, record.updatedAt)
|
|
60
|
+
.bind(record.workspaceId, record.id, record.name, record.url, JSON.stringify(record.types), JSON.stringify(record.runEvents), JSON.stringify(record.alertEvents), record.enabled ? 1 : 0, record.secretSealed, record.updatedAt, record.workspaceId, record.id, record.workspaceId, limit)
|
|
44
61
|
.run();
|
|
62
|
+
// A filtered-out row writes nothing, which is the refusal. An admitted one always reports a
|
|
63
|
+
// change, whether it inserted or took the conflict branch.
|
|
64
|
+
return (result.meta?.changes ?? 0) > 0 ? 'stored' : 'limit_reached';
|
|
45
65
|
}
|
|
46
|
-
async delete(workspaceId) {
|
|
66
|
+
async delete(workspaceId, id) {
|
|
47
67
|
await this.db
|
|
48
|
-
.prepare(`DELETE FROM notification_webhooks WHERE workspace_id = ?`)
|
|
49
|
-
.bind(workspaceId)
|
|
68
|
+
.prepare(`DELETE FROM notification_webhooks WHERE workspace_id = ? AND id = ?`)
|
|
69
|
+
.bind(workspaceId, id)
|
|
50
70
|
.run();
|
|
51
71
|
}
|
|
52
72
|
}
|
|
73
|
+
function toRecord(row) {
|
|
74
|
+
return {
|
|
75
|
+
workspaceId: row.workspace_id,
|
|
76
|
+
id: row.id,
|
|
77
|
+
name: row.name,
|
|
78
|
+
url: row.url,
|
|
79
|
+
types: parseNotificationWebhookTypes(row.types),
|
|
80
|
+
runEvents: parseRunLifecycleEvents(row.run_events),
|
|
81
|
+
alertEvents: parsePlatformAlertEvents(row.alert_events),
|
|
82
|
+
enabled: row.enabled === 1,
|
|
83
|
+
secretSealed: row.secret_sealed,
|
|
84
|
+
updatedAt: row.updated_at,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
53
87
|
//# sourceMappingURL=D1NotificationWebhookRepository.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"D1NotificationWebhookRepository.js","sourceRoot":"","sources":["../../../src/infrastructure/repositories/D1NotificationWebhookRepository.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"D1NotificationWebhookRepository.js","sourceRoot":"","sources":["../../../src/infrastructure/repositories/D1NotificationWebhookRepository.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,35 +2,66 @@ import type { AgentRunRef, AgentRunRepository, Clock, EnvironmentTestRunRecord,
|
|
|
2
2
|
import type { Workflow } from '@cloudflare/workers-types';
|
|
3
3
|
/**
|
|
4
4
|
* A run's durable instance, as the sweeper needs to classify it:
|
|
5
|
-
* - `alive`
|
|
6
|
-
* - `terminal`
|
|
5
|
+
* - `alive` : running/queued/waiting/paused/waitingForPause; leave it.
|
|
6
|
+
* - `terminal` : the instance exists but completed/errored/terminated. It can NOT
|
|
7
7
|
* be recreated (instance ids are unique), so a re-drive via
|
|
8
|
-
* `create` is a silent no-op
|
|
9
|
-
* - `missing`
|
|
8
|
+
* `create` is a silent no-op and the run must be FINALIZED instead.
|
|
9
|
+
* - `missing` : no instance for this id; safe to (re-)create via `redrive`.
|
|
10
|
+
* - `unknown` : the probe could not classify the instance. The Workflows API refused the
|
|
11
|
+
* lookup, the binding for this run's kind is unconfigured, or Workflows
|
|
12
|
+
* itself answered `unknown`. Every disposition the sweeper has is DESTRUCTIVE
|
|
13
|
+
* against a live run (a re-drive of a run that is fine is at best wasted, a
|
|
14
|
+
* finalize kills it outright), so this state buys the run one more tick
|
|
15
|
+
* instead of guessing. It exists because the two swallows this replaced both
|
|
16
|
+
* answered `missing`, which made a Workflows outage read as every stale run
|
|
17
|
+
* having lost its instance at once, and re-drove the whole fleet with no log
|
|
18
|
+
* line to say why.
|
|
10
19
|
*/
|
|
11
|
-
export type InstanceState = 'alive' | 'terminal' | 'missing';
|
|
20
|
+
export type InstanceState = 'alive' | 'terminal' | 'missing' | 'unknown';
|
|
21
|
+
/** What the probe learned about a run's durable instance. */
|
|
22
|
+
export interface InstanceProbe {
|
|
23
|
+
state: InstanceState;
|
|
24
|
+
/**
|
|
25
|
+
* What the probe learned beyond the state, already scrubbed and capped for a surface a
|
|
26
|
+
* person reads:
|
|
27
|
+
* - on `terminal`, the instance's OWN error (`InstanceStatus.error`) when Workflows
|
|
28
|
+
* reported one. It is the only account of why the driver died that survives the
|
|
29
|
+
* instance, and the run is about to be stopped with a fixed sentence that says nothing.
|
|
30
|
+
* - on `unknown`, why the lookup could not classify.
|
|
31
|
+
*
|
|
32
|
+
* Absent when there is nothing to say, which is not the same as an empty string: a terminal
|
|
33
|
+
* instance that Workflows reports no error for ended on purpose.
|
|
34
|
+
*/
|
|
35
|
+
detail?: string;
|
|
36
|
+
}
|
|
12
37
|
/** Tells the sweeper the state of a run's durable instance. */
|
|
13
38
|
interface WorkflowLookup {
|
|
14
|
-
instanceState(runId: string): Promise<
|
|
39
|
+
instanceState(runId: string): Promise<InstanceProbe>;
|
|
15
40
|
}
|
|
16
41
|
/** WorkflowLookup over a Cloudflare Workflows binding. */
|
|
17
42
|
export declare class WorkflowsLookup implements WorkflowLookup {
|
|
18
43
|
private readonly workflow;
|
|
19
|
-
|
|
20
|
-
|
|
44
|
+
private readonly log;
|
|
45
|
+
constructor(workflow: Workflow, log?: Logger);
|
|
46
|
+
instanceState(runId: string): Promise<InstanceProbe>;
|
|
21
47
|
}
|
|
22
48
|
export interface SweepDeps {
|
|
23
49
|
agentRunRepository: AgentRunRepository;
|
|
24
50
|
/** State of the durable instance backing this run (by kind). */
|
|
25
|
-
instanceState(ref: AgentRunRef): Promise<
|
|
51
|
+
instanceState(ref: AgentRunRef): Promise<InstanceProbe>;
|
|
26
52
|
/** (Re-)create the durable driver for a run whose instance is `missing`. */
|
|
27
53
|
redrive(ref: AgentRunRef): Promise<void>;
|
|
28
54
|
/**
|
|
29
55
|
* Finalize a run whose instance is `terminal` (so it can't be re-created): mark it
|
|
30
56
|
* stopped/failed and reclaim any leftover container, routed by kind. Without this,
|
|
31
57
|
* such a run would show as `running` forever (the re-drive is a silent no-op).
|
|
58
|
+
*
|
|
59
|
+
* `cause` is the dead instance's own account of itself ({@link InstanceProbe.detail}), when
|
|
60
|
+
* Workflows kept one. It is the only thing that distinguishes the runs this branch settles,
|
|
61
|
+
* all of which otherwise carry the same fixed sentence, so it belongs in the stop reason
|
|
62
|
+
* rather than only in a log line the operator has to go and find.
|
|
32
63
|
*/
|
|
33
|
-
finalizeOrphan(ref: AgentRunRef): Promise<void>;
|
|
64
|
+
finalizeOrphan(ref: AgentRunRef, cause?: string): Promise<void>;
|
|
34
65
|
/**
|
|
35
66
|
* Fail an execution run whose instance stayed `missing` past the hard-stall deadline —
|
|
36
67
|
* re-driving isn't resurrecting it, so flag it `stalled` (loud banner + retry) instead of
|
|
@@ -72,6 +103,12 @@ export interface SweepResult {
|
|
|
72
103
|
finalized: number;
|
|
73
104
|
/** Runs failed `stalled` (instance missing past the hard-stall deadline). */
|
|
74
105
|
stalled: number;
|
|
106
|
+
/**
|
|
107
|
+
* Runs the probe could not classify, so the sweep left them alone. Reported apart from the
|
|
108
|
+
* three dispositions because it is not one: a rising number here means the sweeper is
|
|
109
|
+
* BLIND, which reads in every other number as a fleet that suddenly went quiet.
|
|
110
|
+
*/
|
|
111
|
+
unknown: number;
|
|
75
112
|
}
|
|
76
113
|
/**
|
|
77
114
|
* Backstop for runs that are still `running` in storage but whose Workflows
|
|
@@ -82,6 +119,10 @@ export interface SweepResult {
|
|
|
82
119
|
* recreated under the same id, so re-driving is a no-op — without
|
|
83
120
|
* this the run would be stuck `running` forever).
|
|
84
121
|
* - `alive` → leave it.
|
|
122
|
+
* - `unknown` → leave it, count it, and forget its orphan clock. The sweep acts on what it
|
|
123
|
+
* KNOWS; an unclassifiable instance costs the run one tick of recovery
|
|
124
|
+
* latency, where guessing costs a live run its container or an orphan its
|
|
125
|
+
* place in the queue for as long as the outage lasts.
|
|
85
126
|
* Pure orchestration over its ports so it is unit-testable with fakes.
|
|
86
127
|
*/
|
|
87
128
|
export declare function sweepStuckRuns({ agentRunRepository, instanceState, redrive, finalizeOrphan, failStalled, clock, leaseMs, hardStallMs, orphanedSince, metrics, logger, }: SweepDeps): Promise<SweepResult>;
|
|
@@ -90,14 +131,15 @@ export interface EnvTestSweepDeps {
|
|
|
90
131
|
listStale(cutoffMs: number): Promise<EnvironmentTestRunRecord[]>;
|
|
91
132
|
};
|
|
92
133
|
/** State of the run's EnvironmentTestWorkflow instance. */
|
|
93
|
-
instanceState(runId: string): Promise<
|
|
134
|
+
instanceState(runId: string): Promise<InstanceProbe>;
|
|
94
135
|
/** (Re-)create the durable driver for a run whose instance is `missing`. */
|
|
95
136
|
redrive(workspaceId: string, runId: string): Promise<void>;
|
|
96
137
|
/**
|
|
97
138
|
* Finalize a run whose instance is `terminal` (it can't be re-created under the same
|
|
98
139
|
* id): best-effort cleanup + mark it failed, via `EnvironmentTestService.expire`.
|
|
140
|
+
* `cause` is the dead instance's own error when Workflows kept one (see {@link SweepDeps}).
|
|
99
141
|
*/
|
|
100
|
-
finalizeOrphan(workspaceId: string, runId: string): Promise<void>;
|
|
142
|
+
finalizeOrphan(workspaceId: string, runId: string, cause?: string): Promise<void>;
|
|
101
143
|
clock: Clock;
|
|
102
144
|
/** A run is considered stuck if its record hasn't been touched in this many ms. */
|
|
103
145
|
leaseMs: number;
|
|
@@ -115,6 +157,7 @@ export interface EnvTestSweepDeps {
|
|
|
115
157
|
export declare function sweepStuckEnvTests({ repository, instanceState, redrive, finalizeOrphan, clock, leaseMs, }: EnvTestSweepDeps): Promise<{
|
|
116
158
|
redriven: number;
|
|
117
159
|
finalized: number;
|
|
160
|
+
unknown: number;
|
|
118
161
|
}>;
|
|
119
162
|
export {};
|
|
120
163
|
//# sourceMappingURL=sweeper.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sweeper.d.ts","sourceRoot":"","sources":["../../../src/infrastructure/workflows/sweeper.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,WAAW,EACX,kBAAkB,EAClB,KAAK,EACL,wBAAwB,EACxB,MAAM,EACN,kBAAkB,EACnB,MAAM,qBAAqB,CAAA;
|
|
1
|
+
{"version":3,"file":"sweeper.d.ts","sourceRoot":"","sources":["../../../src/infrastructure/workflows/sweeper.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,32 +1,156 @@
|
|
|
1
|
-
import { noopLogger, noopOperationalMetrics, runBestEffort } from '@cat-factory/kernel';
|
|
1
|
+
import { describeError, noopLogger, noopOperationalMetrics, redactSecrets, runBestEffort, } from '@cat-factory/kernel';
|
|
2
|
+
/**
|
|
3
|
+
* How much of an instance's own error text is carried onto the run's stop reason. It is
|
|
4
|
+
* rendered in the run's details, so it is a sentence or two of context, not a transcript.
|
|
5
|
+
*/
|
|
6
|
+
const INSTANCE_DETAIL_CAP = 400;
|
|
7
|
+
/**
|
|
8
|
+
* Workflows' code for "no instance with this id". It rides the error MESSAGE
|
|
9
|
+
* (`(instance.not_found) Instance does not exist` from the API, a bare `instance.not_found`
|
|
10
|
+
* from the local binding); the binding's own `WorkflowError.code` is a NUMBER, so this string
|
|
11
|
+
* is the only spelling stable across both.
|
|
12
|
+
*/
|
|
13
|
+
const INSTANCE_NOT_FOUND_CODE = 'instance.not_found';
|
|
14
|
+
/** How far down a `cause` chain the classification below looks before giving up. */
|
|
15
|
+
const CAUSE_DEPTH = 4;
|
|
16
|
+
/**
|
|
17
|
+
* The message a thrown value carries, whatever shape it arrived in.
|
|
18
|
+
*
|
|
19
|
+
* Reading `.message` only off an `Error` is the trap: `Workflow.get` is declared to reject with
|
|
20
|
+
* a plain `WorkflowError` (`{ code?: number; message: string }`), which is not an `Error`, so an
|
|
21
|
+
* `instanceof` test falls through to `String(error)` and renders it `[object Object]`. That
|
|
22
|
+
* silently makes `missing` unreachable, and with it the ENTIRE stale-run backstop: every probe
|
|
23
|
+
* answers `unknown`, which by design takes no action at all. Empty when the value carries no
|
|
24
|
+
* message, so a caller can tell "nothing said" from "said nothing useful".
|
|
25
|
+
*/
|
|
26
|
+
function messageOf(error) {
|
|
27
|
+
if (typeof error === 'string')
|
|
28
|
+
return error;
|
|
29
|
+
if (error instanceof Error)
|
|
30
|
+
return error.message;
|
|
31
|
+
if (typeof error === 'object' && error !== null) {
|
|
32
|
+
const { message } = error;
|
|
33
|
+
if (typeof message === 'string')
|
|
34
|
+
return message;
|
|
35
|
+
}
|
|
36
|
+
return '';
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Whether a lookup throw means "no instance with this id" (the state the sweeper acts on)
|
|
40
|
+
* rather than "the lookup failed" (which says nothing about the run).
|
|
41
|
+
*
|
|
42
|
+
* Matching the CODE rather than the prose keeps it stable across Workflows' two spellings, and
|
|
43
|
+
* walking the `cause` chain keeps it stable across a binding shim that re-throws wrapping the
|
|
44
|
+
* original. Anything else (a quota rejection, an unbound namespace, an API outage) is a lookup
|
|
45
|
+
* failure and must NOT be reported as a lost instance.
|
|
46
|
+
*
|
|
47
|
+
* A misclassification here is invisible in the sweep's dispositions, which is what
|
|
48
|
+
* `sweep.run_state_unknown` is counted for: a sweeper that can no longer recognise the code
|
|
49
|
+
* reports every stale run as unclassifiable rather than re-driving none of them quietly.
|
|
50
|
+
*/
|
|
51
|
+
function isInstanceNotFound(error) {
|
|
52
|
+
let current = error;
|
|
53
|
+
for (let depth = 0; depth < CAUSE_DEPTH && current != null; depth++) {
|
|
54
|
+
if (messageOf(current).includes(INSTANCE_NOT_FOUND_CODE))
|
|
55
|
+
return true;
|
|
56
|
+
current = current.cause;
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Prepare free text for {@link InstanceProbe.detail}: scrub, trim, cap. Workflows echoes the
|
|
62
|
+
* step's own throw, which routinely carries a URL or a provider response, and this lands on a
|
|
63
|
+
* run surface a person reads. Empty in ⇒ empty out, so the caller can tell "nothing to say"
|
|
64
|
+
* from "said nothing useful".
|
|
65
|
+
*/
|
|
66
|
+
function probeDetail(text) {
|
|
67
|
+
const scrubbed = (redactSecrets(text.trim()) ?? '').trim();
|
|
68
|
+
return scrubbed.length > INSTANCE_DETAIL_CAP
|
|
69
|
+
? `${scrubbed.slice(0, INSTANCE_DETAIL_CAP)}…`
|
|
70
|
+
: scrubbed;
|
|
71
|
+
}
|
|
72
|
+
/** The instance's own error, scrubbed and capped; empty when Workflows reported none. */
|
|
73
|
+
function describeInstanceError(error) {
|
|
74
|
+
if (!error)
|
|
75
|
+
return '';
|
|
76
|
+
return probeDetail([error.name, error.message].filter(Boolean).join(': '));
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Why a lookup could not answer, scrubbed and capped. Reads the message off whatever shape the
|
|
80
|
+
* throw arrived in (see {@link messageOf}) rather than stringifying it, since the plain
|
|
81
|
+
* `WorkflowError` the binding is declared to reject with renders as `[object Object]` and would
|
|
82
|
+
* put a run's only account of an outage as literally nothing.
|
|
83
|
+
*/
|
|
84
|
+
function describeLookupFailure(error) {
|
|
85
|
+
const message = messageOf(error);
|
|
86
|
+
if (!message)
|
|
87
|
+
return probeDetail(safeStringify(error));
|
|
88
|
+
const name = error instanceof Error ? error.name : '';
|
|
89
|
+
return probeDetail(name ? `${name}: ${message}` : message);
|
|
90
|
+
}
|
|
91
|
+
/** A last-resort rendering of a throw that carries no message. Never throws itself. */
|
|
92
|
+
function safeStringify(error) {
|
|
93
|
+
try {
|
|
94
|
+
return typeof error === 'object' && error !== null ? JSON.stringify(error) : String(error);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// silent-catch-ok: the value is unserialisable (a cycle, a throwing getter); its type is
|
|
98
|
+
// the only honest thing left to say about it, and this is already the fallback path.
|
|
99
|
+
return Object.prototype.toString.call(error);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
2
102
|
/** WorkflowLookup over a Cloudflare Workflows binding. */
|
|
3
103
|
export class WorkflowsLookup {
|
|
4
104
|
workflow;
|
|
5
|
-
|
|
105
|
+
log;
|
|
106
|
+
constructor(workflow, log = noopLogger) {
|
|
6
107
|
this.workflow = workflow;
|
|
108
|
+
this.log = log;
|
|
7
109
|
}
|
|
8
110
|
async instanceState(runId) {
|
|
9
111
|
let instance;
|
|
10
112
|
try {
|
|
11
113
|
instance = await this.workflow.get(runId);
|
|
12
114
|
}
|
|
13
|
-
catch {
|
|
14
|
-
|
|
15
|
-
|
|
115
|
+
catch (error) {
|
|
116
|
+
if (isInstanceNotFound(error))
|
|
117
|
+
return { state: 'missing' };
|
|
118
|
+
// The lookup itself failed. Reporting `missing` here is how one Workflows outage became
|
|
119
|
+
// a fleet-wide re-drive; report that we do not know instead, and name the cause.
|
|
120
|
+
this.log.warn('workflow instance lookup failed', { runId, ...describeError(error) });
|
|
121
|
+
return { state: 'unknown', detail: describeLookupFailure(error) };
|
|
16
122
|
}
|
|
17
123
|
try {
|
|
18
|
-
const { status } = await instance.status();
|
|
19
|
-
|
|
124
|
+
const { status, error } = await instance.status();
|
|
125
|
+
if (status === 'running' ||
|
|
20
126
|
status === 'queued' ||
|
|
21
127
|
status === 'waiting' ||
|
|
22
|
-
status === 'paused'
|
|
23
|
-
|
|
24
|
-
|
|
128
|
+
status === 'paused' ||
|
|
129
|
+
// Finishing the current work before parking: still driving the run, so leaving it
|
|
130
|
+
// alone is right. Absent from the alive set it fell through to `terminal`, and the
|
|
131
|
+
// sweeper force-stopped a run that was pausing exactly as it was asked to.
|
|
132
|
+
status === 'waitingForPause') {
|
|
133
|
+
return { state: 'alive' };
|
|
134
|
+
}
|
|
135
|
+
// Workflows' OWN "I cannot tell you" answer. It shares the fall-through with the genuine
|
|
136
|
+
// terminal states, so it used to finalize the run, the most destructive disposition
|
|
137
|
+
// available, taken on the one answer that carries no information.
|
|
138
|
+
if (status === 'unknown') {
|
|
139
|
+
this.log.warn('workflow instance status is unknown', { runId });
|
|
140
|
+
return { state: 'unknown', detail: 'Workflows reported the instance status as unknown.' };
|
|
141
|
+
}
|
|
142
|
+
const detail = describeInstanceError(error);
|
|
143
|
+
return { state: 'terminal', ...(detail ? { detail } : {}) };
|
|
25
144
|
}
|
|
26
|
-
catch {
|
|
27
|
-
// The
|
|
28
|
-
//
|
|
29
|
-
|
|
145
|
+
catch (error) {
|
|
146
|
+
// The handle `get` hands back is LAZY: it resolves nothing, so "no instance with this id"
|
|
147
|
+
// routinely surfaces here rather than from `get`. It is the same fact and takes the same
|
|
148
|
+
// classification, or a genuinely lost instance reads as an unreadable one and is never
|
|
149
|
+
// re-created.
|
|
150
|
+
if (isInstanceNotFound(error))
|
|
151
|
+
return { state: 'missing' };
|
|
152
|
+
this.log.warn('workflow instance status unreadable', { runId, ...describeError(error) });
|
|
153
|
+
return { state: 'unknown', detail: describeLookupFailure(error) };
|
|
30
154
|
}
|
|
31
155
|
}
|
|
32
156
|
}
|
|
@@ -39,6 +163,10 @@ export class WorkflowsLookup {
|
|
|
39
163
|
* recreated under the same id, so re-driving is a no-op — without
|
|
40
164
|
* this the run would be stuck `running` forever).
|
|
41
165
|
* - `alive` → leave it.
|
|
166
|
+
* - `unknown` → leave it, count it, and forget its orphan clock. The sweep acts on what it
|
|
167
|
+
* KNOWS; an unclassifiable instance costs the run one tick of recovery
|
|
168
|
+
* latency, where guessing costs a live run its container or an orphan its
|
|
169
|
+
* place in the queue for as long as the outage lasts.
|
|
42
170
|
* Pure orchestration over its ports so it is unit-testable with fakes.
|
|
43
171
|
*/
|
|
44
172
|
export async function sweepStuckRuns({ agentRunRepository, instanceState, redrive, finalizeOrphan, failStalled, clock, leaseMs, hardStallMs, orphanedSince = new Map(), metrics = noopOperationalMetrics, logger = noopLogger, }) {
|
|
@@ -47,19 +175,31 @@ export async function sweepStuckRuns({ agentRunRepository, instanceState, redriv
|
|
|
47
175
|
let redriven = 0;
|
|
48
176
|
let finalized = 0;
|
|
49
177
|
let stalled = 0;
|
|
178
|
+
let unknown = 0;
|
|
50
179
|
// Which runs were observed still-orphaned (`missing`) THIS tick — used to prune the
|
|
51
180
|
// per-process clock of any run that recovered or went terminal so its deadline restarts
|
|
52
181
|
// if it ever stalls again.
|
|
53
182
|
const stillOrphaned = new Set();
|
|
54
183
|
for (const ref of stale) {
|
|
55
|
-
const state = await instanceState(ref);
|
|
184
|
+
const { state, detail } = await instanceState(ref);
|
|
56
185
|
if (state === 'alive') {
|
|
57
186
|
orphanedSince.delete(ref.id);
|
|
58
187
|
continue;
|
|
59
188
|
}
|
|
189
|
+
if (state === 'unknown') {
|
|
190
|
+
// Forget the orphan clock rather than carrying it: the run was NOT observed orphaned
|
|
191
|
+
// this tick, and letting an outage age a deadline nobody could measure is how a
|
|
192
|
+
// Workflows incident would come out the far side as a batch of `stalled` runs. The
|
|
193
|
+
// deadline restarts from the next observation that actually saw something, which also
|
|
194
|
+
// guarantees the run is re-driven at least once before it can be given up on.
|
|
195
|
+
orphanedSince.delete(ref.id);
|
|
196
|
+
unknown++;
|
|
197
|
+
metrics.increment('sweep.run_state_unknown', { kind: ref.kind });
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
60
200
|
if (state === 'terminal') {
|
|
61
201
|
orphanedSince.delete(ref.id);
|
|
62
|
-
await finalizeOrphan(ref);
|
|
202
|
+
await finalizeOrphan(ref, detail);
|
|
63
203
|
finalized++;
|
|
64
204
|
// The run KIND is a bounded enum and the split that matters: bootstrap runs and
|
|
65
205
|
// execution runs are lost for different reasons and fixed in different places.
|
|
@@ -99,7 +239,7 @@ export async function sweepStuckRuns({ agentRunRepository, instanceState, redriv
|
|
|
99
239
|
if (!stillOrphaned.has(id))
|
|
100
240
|
orphanedSince.delete(id);
|
|
101
241
|
}
|
|
102
|
-
return { redriven, finalized, stalled };
|
|
242
|
+
return { redriven, finalized, stalled, unknown };
|
|
103
243
|
}
|
|
104
244
|
/**
|
|
105
245
|
* The env-test sibling of {@link sweepStuckRuns}: self-test runs live in their own
|
|
@@ -115,18 +255,26 @@ export async function sweepStuckEnvTests({ repository, instanceState, redrive, f
|
|
|
115
255
|
const stale = await repository.listStale(clock.now() - leaseMs);
|
|
116
256
|
let redriven = 0;
|
|
117
257
|
let finalized = 0;
|
|
258
|
+
let unknown = 0;
|
|
118
259
|
for (const run of stale) {
|
|
119
|
-
const state = await instanceState(run.id);
|
|
260
|
+
const { state, detail } = await instanceState(run.id);
|
|
120
261
|
if (state === 'alive')
|
|
121
262
|
continue;
|
|
263
|
+
// Unclassifiable: leave it for the next tick, exactly like the run sweep. Re-driving a
|
|
264
|
+
// test whose instance is alive would be harmless, but expiring one that is provisioning
|
|
265
|
+
// real infrastructure is not, and neither disposition is safe to take blind.
|
|
266
|
+
if (state === 'unknown') {
|
|
267
|
+
unknown++;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
122
270
|
if (state === 'terminal') {
|
|
123
|
-
await finalizeOrphan(run.workspaceId, run.id);
|
|
271
|
+
await finalizeOrphan(run.workspaceId, run.id, detail);
|
|
124
272
|
finalized++;
|
|
125
273
|
continue;
|
|
126
274
|
}
|
|
127
275
|
await redrive(run.workspaceId, run.id);
|
|
128
276
|
redriven++;
|
|
129
277
|
}
|
|
130
|
-
return { redriven, finalized };
|
|
278
|
+
return { redriven, finalized, unknown };
|
|
131
279
|
}
|
|
132
280
|
//# sourceMappingURL=sweeper.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sweeper.js","sourceRoot":"","sources":["../../../src/infrastructure/workflows/sweeper.ts"],"names":[],"mappings":"AAQA,OAAO,
|
|
1
|
+
{"version":3,"file":"sweeper.js","sourceRoot":"","sources":["../../../src/infrastructure/workflows/sweeper.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
-- Several NAMED outbound webhooks per workspace, replacing the one-row-per-workspace shape from
|
|
2
|
+
-- migration 0061. One endpoint per workspace made a second integration's enrolment a hostile act:
|
|
3
|
+
-- registering it overwrote whatever was already there, and the only symptom was that the previous
|
|
4
|
+
-- receiver went quiet. Each row now carries a caller-chosen `id` (the key, alongside the workspace)
|
|
5
|
+
-- and an operator-facing `name`.
|
|
6
|
+
--
|
|
7
|
+
-- SQLite cannot re-key a table in place, so this is the standard rebuild. Existing rows migrate to
|
|
8
|
+
-- the id `default`, which is exactly what the singular `/api/v1/notification-webhook` routes now
|
|
9
|
+
-- address: an already-registered endpoint keeps delivering, and the caller that registered it keeps
|
|
10
|
+
-- addressing it through the same route.
|
|
11
|
+
|
|
12
|
+
CREATE TABLE notification_webhooks_new (
|
|
13
|
+
workspace_id TEXT NOT NULL,
|
|
14
|
+
id TEXT NOT NULL,
|
|
15
|
+
name TEXT NOT NULL,
|
|
16
|
+
url TEXT NOT NULL,
|
|
17
|
+
types TEXT NOT NULL DEFAULT '[]',
|
|
18
|
+
run_events TEXT NOT NULL DEFAULT '[]',
|
|
19
|
+
alert_events TEXT NOT NULL DEFAULT '[]',
|
|
20
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
21
|
+
secret_sealed TEXT,
|
|
22
|
+
updated_at INTEGER NOT NULL,
|
|
23
|
+
PRIMARY KEY (workspace_id, id)
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
INSERT INTO notification_webhooks_new
|
|
27
|
+
(workspace_id, id, name, url, types, run_events, alert_events, enabled, secret_sealed, updated_at)
|
|
28
|
+
SELECT
|
|
29
|
+
workspace_id, 'default', 'Default', url, types, run_events, alert_events, enabled, secret_sealed, updated_at
|
|
30
|
+
FROM notification_webhooks;
|
|
31
|
+
|
|
32
|
+
DROP TABLE notification_webhooks;
|
|
33
|
+
|
|
34
|
+
ALTER TABLE notification_webhooks_new RENAME TO notification_webhooks;
|
|
35
|
+
|
|
36
|
+
-- No extra index on `workspace_id`: it is the leading column of the composite primary key, so the
|
|
37
|
+
-- per-workspace list every delivery reads is already served by that index.
|