@memberjunction/scheduling-engine 6.1.0-edge.1 → 6.1.0-edge.3
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/LICENSE +180 -4
- package/README.md +27 -1
- package/dist/ScheduledJobEngine.d.ts.map +1 -1
- package/dist/ScheduledJobEngine.js +19 -7
- package/dist/ScheduledJobEngine.js.map +1 -1
- package/dist/drivers/ActionLogRetentionScheduledJobDriver.d.ts +106 -0
- package/dist/drivers/ActionLogRetentionScheduledJobDriver.d.ts.map +1 -0
- package/dist/drivers/ActionLogRetentionScheduledJobDriver.js +224 -0
- package/dist/drivers/ActionLogRetentionScheduledJobDriver.js.map +1 -0
- package/dist/drivers/AgentScheduledJobDriver.d.ts.map +1 -1
- package/dist/drivers/AgentScheduledJobDriver.js +16 -0
- package/dist/drivers/AgentScheduledJobDriver.js.map +1 -1
- package/dist/drivers/MaterializationRefreshScheduledJobDriver.d.ts +25 -0
- package/dist/drivers/MaterializationRefreshScheduledJobDriver.d.ts.map +1 -0
- package/dist/drivers/MaterializationRefreshScheduledJobDriver.js +103 -0
- package/dist/drivers/MaterializationRefreshScheduledJobDriver.js.map +1 -0
- package/dist/drivers/UserRoutineDispatcherDriver.d.ts +10 -0
- package/dist/drivers/UserRoutineDispatcherDriver.d.ts.map +1 -1
- package/dist/drivers/UserRoutineDispatcherDriver.js +132 -4
- package/dist/drivers/UserRoutineDispatcherDriver.js.map +1 -1
- package/dist/drivers/index.d.ts +2 -0
- package/dist/drivers/index.d.ts.map +1 -1
- package/dist/drivers/index.js +2 -0
- package/dist/drivers/index.js.map +1 -1
- package/package.json +19 -18
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Driver for the bounded action-execution-log purge.
|
|
3
|
+
* @module @memberjunction/scheduling-engine
|
|
4
|
+
*/
|
|
5
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
6
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
7
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
8
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
9
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
10
|
+
};
|
|
11
|
+
import { RegisterClass } from '@memberjunction/global';
|
|
12
|
+
import { BaseScheduledJob } from '../BaseScheduledJob.js';
|
|
13
|
+
import { LogError, Metadata, RunView, ValidationErrorInfo, ValidationErrorType, ValidationResult, } from '@memberjunction/core';
|
|
14
|
+
/** How many rows one run will delete before stopping, when the job does not say. */
|
|
15
|
+
export const DEFAULT_MAX_DELETES_PER_RUN = 5000;
|
|
16
|
+
/**
|
|
17
|
+
* Turns the distinct retention values present into the buckets a purge query needs.
|
|
18
|
+
*
|
|
19
|
+
* Bucketing rather than per-row date math is what keeps this a small number of indexable range
|
|
20
|
+
* queries instead of a scan: retention is stamped from the action, so the distinct set is roughly
|
|
21
|
+
* "one per configured action", not one per log row. Pure and exported so the arithmetic is testable
|
|
22
|
+
* without a clock or a database.
|
|
23
|
+
*
|
|
24
|
+
* @param now the reference instant, passed in rather than read, so a test can pin it.
|
|
25
|
+
*/
|
|
26
|
+
export function BuildRetentionBuckets(retentionDays, now) {
|
|
27
|
+
const distinct = [...new Set(retentionDays.filter((d) => Number.isFinite(d) && d > 0))].sort((a, b) => a - b);
|
|
28
|
+
return distinct.map((RetentionDays) => ({
|
|
29
|
+
RetentionDays,
|
|
30
|
+
Cutoff: new Date(now.getTime() - RetentionDays * 24 * 60 * 60 * 1000),
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Purges expired `MJ: Action Execution Logs` rows, bounded per run.
|
|
35
|
+
*
|
|
36
|
+
* **Retention is decided at write time, not here.** `StartActionLog` stamps each row's
|
|
37
|
+
* `RetentionPeriod` from its action's, so this job reads only the row's own value — which is what
|
|
38
|
+
* makes an edit to `Action.RetentionPeriod` a going-forward change rather than a retroactive
|
|
39
|
+
* deletion of history written under the previous policy. A row with NULL retention is kept forever
|
|
40
|
+
* unless the job is explicitly configured with `DefaultRetentionDays`.
|
|
41
|
+
*
|
|
42
|
+
* **Deletion goes through `BaseEntity.Delete()`, one row at a time.** A `DELETE` statement would be
|
|
43
|
+
* faster and is a sanctioned use of raw DML in principle — but only for an entity that has opted in
|
|
44
|
+
* via `AllowDirectSQLDelete`, and this one has not. Honouring the platform's own gate matters more
|
|
45
|
+
* here than throughput, because the job is bounded anyway: the cap exists so a first run against a
|
|
46
|
+
* long-neglected table cannot become an unbounded transaction, and raising it is a configuration
|
|
47
|
+
* change rather than a code change.
|
|
48
|
+
*
|
|
49
|
+
* Opt-in like every other maintenance driver: shipping the job type activates nothing until someone
|
|
50
|
+
* creates a `MJ: Scheduled Job` of this type with a cron expression.
|
|
51
|
+
*/
|
|
52
|
+
let ActionLogRetentionScheduledJobDriver = class ActionLogRetentionScheduledJobDriver extends BaseScheduledJob {
|
|
53
|
+
async Execute(context) {
|
|
54
|
+
const config = this.readConfiguration(context.Schedule);
|
|
55
|
+
const maxDeletes = config.MaxDeletesPerRun ?? DEFAULT_MAX_DELETES_PER_RUN;
|
|
56
|
+
try {
|
|
57
|
+
const expired = await this.findExpiredLogs(config, maxDeletes, context.ContextUser);
|
|
58
|
+
const { Deleted, Failed } = await this.deleteBounded(expired);
|
|
59
|
+
return {
|
|
60
|
+
Success: true,
|
|
61
|
+
Details: {
|
|
62
|
+
Deleted,
|
|
63
|
+
Failed,
|
|
64
|
+
Examined: expired.length,
|
|
65
|
+
// Says plainly that the run stopped at its ceiling rather than because it was
|
|
66
|
+
// finished — otherwise "Deleted: 5000" reads as "all clean" when it is not.
|
|
67
|
+
ReachedCap: expired.length >= maxDeletes,
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
return { Success: false, ErrorMessage: `Action-log purge failed: ${e instanceof Error ? e.message : String(e)}` };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The expired rows, oldest first, capped at `maxDeletes`.
|
|
77
|
+
*
|
|
78
|
+
* Oldest-first is not cosmetic: a capped run that took an arbitrary slice would leave the same
|
|
79
|
+
* ancient rows behind every night while deleting newer ones, so the backlog would never drain.
|
|
80
|
+
*/
|
|
81
|
+
async findExpiredLogs(config, maxDeletes, user) {
|
|
82
|
+
const filter = await this.buildExpiredFilter(config, user);
|
|
83
|
+
if (!filter) {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
// A scheduled maintenance sweep is a server-global task, so the global default provider is
|
|
87
|
+
// the correct source here (not a per-request/per-tenant provider).
|
|
88
|
+
const rv = RunView.FromMetadataProvider(Metadata.Provider); // global-provider-ok: scheduled maintenance sweep is a server-global task, not per-request/per-tenant
|
|
89
|
+
const result = await rv.RunView({
|
|
90
|
+
EntityName: 'MJ: Action Execution Logs',
|
|
91
|
+
ExtraFilter: filter,
|
|
92
|
+
OrderBy: 'StartedAt ASC',
|
|
93
|
+
MaxRows: maxDeletes,
|
|
94
|
+
ResultType: 'entity_object',
|
|
95
|
+
}, user);
|
|
96
|
+
if (!result.Success) {
|
|
97
|
+
throw new Error(result.ErrorMessage ?? 'could not read the action execution log');
|
|
98
|
+
}
|
|
99
|
+
return result.Results ?? [];
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The WHERE clause selecting expired rows, or null when nothing can expire.
|
|
103
|
+
*
|
|
104
|
+
* Returning null rather than a clause that matches nothing is deliberate — it lets the caller
|
|
105
|
+
* skip the query entirely on an instance where no action has ever configured retention, which is
|
|
106
|
+
* the common case and should cost nothing nightly.
|
|
107
|
+
*/
|
|
108
|
+
async buildExpiredFilter(config, user) {
|
|
109
|
+
const now = new Date();
|
|
110
|
+
const clauses = [];
|
|
111
|
+
const buckets = BuildRetentionBuckets(await this.distinctRetentionDays(user), now);
|
|
112
|
+
for (const bucket of buckets) {
|
|
113
|
+
clauses.push(`(RetentionPeriod = ${bucket.RetentionDays} AND StartedAt < '${bucket.Cutoff.toISOString()}')`);
|
|
114
|
+
}
|
|
115
|
+
const fallback = config.DefaultRetentionDays;
|
|
116
|
+
if (fallback != null && Number.isFinite(fallback) && fallback > 0) {
|
|
117
|
+
const cutoff = new Date(now.getTime() - fallback * 24 * 60 * 60 * 1000);
|
|
118
|
+
clauses.push(`(RetentionPeriod IS NULL AND StartedAt < '${cutoff.toISOString()}')`);
|
|
119
|
+
}
|
|
120
|
+
return clauses.length > 0 ? clauses.join(' OR ') : null;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The retention lifetimes actually in use, read from the actions that stamped them.
|
|
124
|
+
*
|
|
125
|
+
* Read from `MJ: Actions` rather than by scanning the log for distinct values, because the log
|
|
126
|
+
* is the large table and the actions table is not — and the two agree by construction, since the
|
|
127
|
+
* stamp comes from here.
|
|
128
|
+
*/
|
|
129
|
+
async distinctRetentionDays(user) {
|
|
130
|
+
const rv = RunView.FromMetadataProvider(Metadata.Provider); // global-provider-ok: scheduled maintenance sweep is a server-global task, not per-request/per-tenant
|
|
131
|
+
const result = await rv.RunView({
|
|
132
|
+
EntityName: 'MJ: Actions',
|
|
133
|
+
ExtraFilter: 'RetentionPeriod IS NOT NULL',
|
|
134
|
+
Fields: ['RetentionPeriod'],
|
|
135
|
+
ResultType: 'simple',
|
|
136
|
+
}, user);
|
|
137
|
+
if (!result.Success) {
|
|
138
|
+
throw new Error(result.ErrorMessage ?? 'could not read action retention settings');
|
|
139
|
+
}
|
|
140
|
+
return (result.Results ?? []).map((r) => Number(r.RetentionPeriod)).filter((d) => Number.isFinite(d));
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Deletes the given rows, counting rather than aborting on a failure.
|
|
144
|
+
*
|
|
145
|
+
* One bad row — a FK an extension added, a permission gap — must not strand the rest of the
|
|
146
|
+
* purge behind it every night. The count is reported so the failure is visible.
|
|
147
|
+
*/
|
|
148
|
+
async deleteBounded(rows) {
|
|
149
|
+
let Deleted = 0;
|
|
150
|
+
let Failed = 0;
|
|
151
|
+
for (const row of rows) {
|
|
152
|
+
if (await row.Delete()) {
|
|
153
|
+
Deleted++;
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
Failed++;
|
|
157
|
+
LogError(`ActionLogRetention: could not delete action execution log ${row.ID}: ` +
|
|
158
|
+
`${row.LatestResult?.CompleteMessage ?? 'unknown error'}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return { Deleted, Failed };
|
|
162
|
+
}
|
|
163
|
+
ValidateConfiguration(schedule) {
|
|
164
|
+
const result = new ValidationResult();
|
|
165
|
+
const config = this.readConfiguration(schedule);
|
|
166
|
+
for (const [field, value] of [
|
|
167
|
+
['DefaultRetentionDays', config.DefaultRetentionDays],
|
|
168
|
+
['MaxDeletesPerRun', config.MaxDeletesPerRun],
|
|
169
|
+
]) {
|
|
170
|
+
if (value != null && (typeof value !== 'number' || !Number.isFinite(value) || value <= 0)) {
|
|
171
|
+
result.Errors.push(new ValidationErrorInfo(`Configuration.${field}`, `${field} must be a positive number when provided`, value, ValidationErrorType.Failure));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
result.Success = result.Errors.length === 0;
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
FormatNotification(context, result) {
|
|
178
|
+
const deleted = result.Details?.['Deleted'] ?? 0;
|
|
179
|
+
const failed = result.Details?.['Failed'] ?? 0;
|
|
180
|
+
const reachedCap = result.Details?.['ReachedCap'] ?? false;
|
|
181
|
+
if (!result.Success) {
|
|
182
|
+
return {
|
|
183
|
+
Subject: `Action-log purge failed: ${context.Schedule.Name}`,
|
|
184
|
+
Body: `The scheduled action-log purge "${context.Schedule.Name}" failed.\n\nError: ${result.ErrorMessage ?? 'unknown'}`,
|
|
185
|
+
Priority: 'High',
|
|
186
|
+
Metadata: { Deleted: deleted, Failed: failed },
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
const capNote = reachedCap
|
|
190
|
+
? '\n\nThe run stopped at its per-run ceiling, so expired rows remain — it will continue on the next run.'
|
|
191
|
+
: '';
|
|
192
|
+
return {
|
|
193
|
+
Subject: `Action-log purge: ${deleted} expired row(s) removed`,
|
|
194
|
+
Body: `The scheduled action-log purge "${context.Schedule.Name}" removed ${deleted} expired log row(s)`
|
|
195
|
+
+ `${failed > 0 ? `, and could not remove ${failed}` : ''}.${capNote}`,
|
|
196
|
+
Priority: failed > 0 ? 'High' : deleted > 0 ? 'Normal' : 'Low',
|
|
197
|
+
Metadata: { Deleted: deleted, Failed: failed, ReachedCap: reachedCap },
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Tolerant parse — the purge needs no configuration, so empty/missing JSON yields defaults.
|
|
202
|
+
*
|
|
203
|
+
* Named apart from the base's `parseConfiguration`, which throws on an empty Configuration
|
|
204
|
+
* because most job types genuinely require one. This job's every setting is optional, and a job
|
|
205
|
+
* that refused to run because nobody filled in a box would be a retention policy that quietly
|
|
206
|
+
* never applies.
|
|
207
|
+
*/
|
|
208
|
+
readConfiguration(schedule) {
|
|
209
|
+
if (!schedule.Configuration) {
|
|
210
|
+
return {};
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
return JSON.parse(schedule.Configuration);
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
return {};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
ActionLogRetentionScheduledJobDriver = __decorate([
|
|
221
|
+
RegisterClass(BaseScheduledJob, 'ActionLogRetentionScheduledJobDriver')
|
|
222
|
+
], ActionLogRetentionScheduledJobDriver);
|
|
223
|
+
export { ActionLogRetentionScheduledJobDriver };
|
|
224
|
+
//# sourceMappingURL=ActionLogRetentionScheduledJobDriver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ActionLogRetentionScheduledJobDriver.js","sourceRoot":"","sources":["../../src/drivers/ActionLogRetentionScheduledJobDriver.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AgentScheduledJobDriver.d.ts","sourceRoot":"","sources":["../../src/drivers/AgentScheduledJobDriver.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,gBAAgB,EAAE,4BAA4B,EAAE,MAAM,qBAAqB,CAAC;AACrF,OAAO,EAAE,gBAAgB,EAAmF,MAAM,sBAAsB,CAAC;AAGzI,OAAO,EACH,kBAAkB,EAClB,mBAAmB,EAEtB,MAAM,uCAAuC,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBACa,uBAAwB,SAAQ,gBAAgB;IAC5C,OAAO,CAAC,OAAO,EAAE,4BAA4B,GAAG,OAAO,CAAC,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"AgentScheduledJobDriver.d.ts","sourceRoot":"","sources":["../../src/drivers/AgentScheduledJobDriver.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,gBAAgB,EAAE,4BAA4B,EAAE,MAAM,qBAAqB,CAAC;AACrF,OAAO,EAAE,gBAAgB,EAAmF,MAAM,sBAAsB,CAAC;AAGzI,OAAO,EACH,kBAAkB,EAClB,mBAAmB,EAEtB,MAAM,uCAAuC,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBACa,uBAAwB,SAAQ,gBAAgB;IAC5C,OAAO,CAAC,OAAO,EAAE,4BAA4B,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAqEjF,qBAAqB,CAAC,QAAQ,EAAE,GAAG,GAAG,gBAAgB;IA4CtD,kBAAkB,CACrB,OAAO,EAAE,4BAA4B,EACrC,MAAM,EAAE,kBAAkB,GAC3B,mBAAmB;YA8BR,SAAS;CAW1B"}
|
|
@@ -39,6 +39,22 @@ let AgentScheduledJobDriver = class AgentScheduledJobDriver extends BaseSchedule
|
|
|
39
39
|
const config = this.parseConfiguration(context.Schedule);
|
|
40
40
|
// Load the agent entity
|
|
41
41
|
const agent = await this.loadAgent(config.AgentID, context.ContextUser);
|
|
42
|
+
// A workflow SUBMITS and detaches — it returns a handle as soon as its steps are scheduled,
|
|
43
|
+
// not a result. A scheduled job that ran one would record success the moment the graph was
|
|
44
|
+
// written, before any of the work happened: the job history would show a clean run every
|
|
45
|
+
// time regardless of whether the workflow later succeeded, failed or stalled. That is worse
|
|
46
|
+
// than not running it, because it looks like it worked.
|
|
47
|
+
//
|
|
48
|
+
// Refused here rather than at submission because this is where the caller's expectation
|
|
49
|
+
// lives: the job expects a completed unit of work, and nothing downstream of this point can
|
|
50
|
+
// supply one until an await path exists.
|
|
51
|
+
if ((agent.Type ?? '').trim().toLowerCase() === 'flow') {
|
|
52
|
+
const message = `'${agent.Name}' is a workflow and cannot be run on a schedule yet. A workflow returns ` +
|
|
53
|
+
`as soon as its steps are scheduled, so this job would report success before any of the ` +
|
|
54
|
+
`work had happened. Schedule the steps individually, or start the workflow by hand.`;
|
|
55
|
+
this.log(message);
|
|
56
|
+
return { Success: false, ErrorMessage: message, Details: { AgentID: config.AgentID } };
|
|
57
|
+
}
|
|
42
58
|
// Verbose-only: the engine's always-on "▶️ Starting / ✅ Completed" pair already frames this
|
|
43
59
|
// run. This per-agent detail is redundant noise on routine (often no-op) scheduled runs.
|
|
44
60
|
this.log(`Executing agent: ${agent.Name}`, true);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AgentScheduledJobDriver.js","sourceRoot":"","sources":["../../src/drivers/AgentScheduledJobDriver.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"AgentScheduledJobDriver.js","sourceRoot":"","sources":["../../src/drivers/AgentScheduledJobDriver.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Scheduled-job driver that refreshes due materialized results on a cron schedule —
|
|
3
|
+
* sibling to RecordProcessScheduledJobDriver / ActionScheduledJobDriver. Delegates the actual
|
|
4
|
+
* rebuild to the @memberjunction/materialization engine.
|
|
5
|
+
* @module @memberjunction/scheduling-engine
|
|
6
|
+
*/
|
|
7
|
+
import { ValidationResult } from '@memberjunction/core';
|
|
8
|
+
import { BaseScheduledJob, ScheduledJobExecutionContext } from '../BaseScheduledJob.js';
|
|
9
|
+
import { ScheduledJobResult, NotificationContent } from '@memberjunction/scheduling-base-types';
|
|
10
|
+
/**
|
|
11
|
+
* Driver for the materialization refresh sweep (materialization plan §11.1).
|
|
12
|
+
*
|
|
13
|
+
* Unlike most drivers, this one has **no per-job configuration** — it processes the entire due queue:
|
|
14
|
+
* every non-disabled, scheduled `MJ: Materialized Results` row whose `NextRefreshAt` is at/before now
|
|
15
|
+
* (or null). Each is refreshed via the engine (full rebuild + atomic wrapper-view swap) and its
|
|
16
|
+
* `NextRefreshAt` is advanced from its own `RefreshSchedule` cron. Per-materialization cadence lives
|
|
17
|
+
* on the rows; this job just needs to run often enough to honor the smallest cadence.
|
|
18
|
+
*/
|
|
19
|
+
export declare class MaterializationRefreshScheduledJobDriver extends BaseScheduledJob {
|
|
20
|
+
Execute(context: ScheduledJobExecutionContext): Promise<ScheduledJobResult>;
|
|
21
|
+
/** No per-job configuration — the driver always sweeps the whole due queue. */
|
|
22
|
+
ValidateConfiguration(): ValidationResult;
|
|
23
|
+
FormatNotification(_context: ScheduledJobExecutionContext, result: ScheduledJobResult): NotificationContent;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=MaterializationRefreshScheduledJobDriver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MaterializationRefreshScheduledJobDriver.d.ts","sourceRoot":"","sources":["../../src/drivers/MaterializationRefreshScheduledJobDriver.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,gBAAgB,EAAwC,MAAM,sBAAsB,CAAC;AAG9F,OAAO,EAAE,gBAAgB,EAAE,4BAA4B,EAAE,MAAM,qBAAqB,CAAC;AACrF,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,uCAAuC,CAAC;AAGhG;;;;;;;;GAQG;AACH,qBACa,wCAAyC,SAAQ,gBAAgB;IAC7D,OAAO,CAAC,OAAO,EAAE,4BAA4B,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAmExF,+EAA+E;IACxE,qBAAqB,IAAI,gBAAgB;IAIzC,kBAAkB,CAAC,QAAQ,EAAE,4BAA4B,EAAE,MAAM,EAAE,kBAAkB,GAAG,mBAAmB;CAQrH"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Scheduled-job driver that refreshes due materialized results on a cron schedule —
|
|
3
|
+
* sibling to RecordProcessScheduledJobDriver / ActionScheduledJobDriver. Delegates the actual
|
|
4
|
+
* rebuild to the @memberjunction/materialization engine.
|
|
5
|
+
* @module @memberjunction/scheduling-engine
|
|
6
|
+
*/
|
|
7
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
8
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
10
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
11
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
12
|
+
};
|
|
13
|
+
import { RegisterClass } from '@memberjunction/global';
|
|
14
|
+
import { ValidationResult, RunView, Metadata } from '@memberjunction/core';
|
|
15
|
+
import { MaterializationRefresher } from '@memberjunction/materialization';
|
|
16
|
+
import { BaseScheduledJob } from '../BaseScheduledJob.js';
|
|
17
|
+
import { CronExpressionHelper } from '../CronExpressionHelper.js';
|
|
18
|
+
/**
|
|
19
|
+
* Driver for the materialization refresh sweep (materialization plan §11.1).
|
|
20
|
+
*
|
|
21
|
+
* Unlike most drivers, this one has **no per-job configuration** — it processes the entire due queue:
|
|
22
|
+
* every non-disabled, scheduled `MJ: Materialized Results` row whose `NextRefreshAt` is at/before now
|
|
23
|
+
* (or null). Each is refreshed via the engine (full rebuild + atomic wrapper-view swap) and its
|
|
24
|
+
* `NextRefreshAt` is advanced from its own `RefreshSchedule` cron. Per-materialization cadence lives
|
|
25
|
+
* on the rows; this job just needs to run often enough to honor the smallest cadence.
|
|
26
|
+
*/
|
|
27
|
+
let MaterializationRefreshScheduledJobDriver = class MaterializationRefreshScheduledJobDriver extends BaseScheduledJob {
|
|
28
|
+
async Execute(context) {
|
|
29
|
+
const provider = Metadata.Provider; // global-provider-ok: scheduled job runs in the server's single default-provider context
|
|
30
|
+
// Candidate queue: scheduled materializations that are neither disabled nor held for drift
|
|
31
|
+
// review (§13/§17.2 — DriftHold rows stop refreshing until a human resolves the drift).
|
|
32
|
+
// Due-selection is done in JS (engine.filterDue) so the date comparison stays engine-agnostic.
|
|
33
|
+
const rv = new RunView();
|
|
34
|
+
const candidates = await rv.RunView({
|
|
35
|
+
EntityName: 'MJ: Materialized Results',
|
|
36
|
+
ExtraFilter: `Status NOT IN ('Disabled', 'DriftHold') AND RefreshSchedule IS NOT NULL`,
|
|
37
|
+
ResultType: 'entity_object',
|
|
38
|
+
}, context.ContextUser);
|
|
39
|
+
// RunView returns Success:false (never throws) on failure. Without this check a failed candidate
|
|
40
|
+
// load yields an empty due set and the sweep reports GREEN while silently refreshing nothing —
|
|
41
|
+
// masking a total refresh outage. Surface it as a job failure instead.
|
|
42
|
+
if (!candidates.Success) {
|
|
43
|
+
return {
|
|
44
|
+
Success: false,
|
|
45
|
+
ErrorMessage: `Failed to load due materializations: ${candidates.ErrorMessage ?? 'unknown error'}`,
|
|
46
|
+
Details: { DueCount: 0, Refreshed: 0, Failed: 0, Items: [] },
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const now = new Date();
|
|
50
|
+
const due = MaterializationRefresher.filterDue(candidates.Results ?? [], now);
|
|
51
|
+
const refresher = new MaterializationRefresher();
|
|
52
|
+
let refreshed = 0;
|
|
53
|
+
let failed = 0;
|
|
54
|
+
const items = [];
|
|
55
|
+
for (const mr of due) {
|
|
56
|
+
void context.heartbeat?.();
|
|
57
|
+
// Advance NextRefreshAt from this row's own cron (UTC). Candidates are pre-filtered to
|
|
58
|
+
// RefreshSchedule IS NOT NULL, so a throw here means a MALFORMED cron. In that case we must
|
|
59
|
+
// NOT refresh: RefreshOne would set NextRefreshAt=null, which filterDue treats as due, so the
|
|
60
|
+
// expensive full-rebuild-with-swap would run on EVERY sweep forever. Instead surface it as a
|
|
61
|
+
// failed item and skip — the row stays selected but untouched until the cron is corrected.
|
|
62
|
+
let nextRefreshAt;
|
|
63
|
+
try {
|
|
64
|
+
nextRefreshAt = CronExpressionHelper.GetNextRunTime(mr.RefreshSchedule, 'UTC', now);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
failed++;
|
|
68
|
+
items.push({ ID: mr.ID, TableName: mr.TableName, Success: false, Error: `Invalid RefreshSchedule cron "${mr.RefreshSchedule}" — skipped (not refreshed) until corrected.` });
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const result = await refresher.RefreshOne(mr, context.ContextUser, provider, { nextRefreshAt });
|
|
72
|
+
if (result.Success) {
|
|
73
|
+
refreshed++;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
failed++;
|
|
77
|
+
}
|
|
78
|
+
items.push({ ID: mr.ID, TableName: mr.TableName, Success: result.Success, RowCount: result.RowCount, Error: result.ErrorMessage });
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
Success: failed === 0,
|
|
82
|
+
ErrorMessage: failed > 0 ? `${failed} materialization(s) failed to refresh` : undefined,
|
|
83
|
+
Details: { DueCount: due.length, Refreshed: refreshed, Failed: failed, Items: items },
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/** No per-job configuration — the driver always sweeps the whole due queue. */
|
|
87
|
+
ValidateConfiguration() {
|
|
88
|
+
return new ValidationResult();
|
|
89
|
+
}
|
|
90
|
+
FormatNotification(_context, result) {
|
|
91
|
+
const d = result.Details ?? {};
|
|
92
|
+
return {
|
|
93
|
+
Subject: `Materialization refresh — ${result.Success ? 'OK' : 'errors'}`,
|
|
94
|
+
Body: `Refreshed ${d.Refreshed ?? 0} of ${d.DueCount ?? 0} due materialization(s)${(d.Failed ?? 0) > 0 ? `; ${d.Failed} failed` : ''}.`,
|
|
95
|
+
Priority: result.Success ? 'Normal' : 'High',
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
MaterializationRefreshScheduledJobDriver = __decorate([
|
|
100
|
+
RegisterClass(BaseScheduledJob, 'MaterializationRefreshScheduledJobDriver')
|
|
101
|
+
], MaterializationRefreshScheduledJobDriver);
|
|
102
|
+
export { MaterializationRefreshScheduledJobDriver };
|
|
103
|
+
//# sourceMappingURL=MaterializationRefreshScheduledJobDriver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MaterializationRefreshScheduledJobDriver.js","sourceRoot":"","sources":["../../src/drivers/MaterializationRefreshScheduledJobDriver.ts"],"names":[],"mappings":"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"}
|
|
@@ -64,8 +64,18 @@ export declare class UserRoutineDispatcherDriver extends BaseScheduledJob {
|
|
|
64
64
|
* Execute one claimed routine end-to-end: run row → target execution → run/routine
|
|
65
65
|
* bookkeeping → notification decision + delivery. Never throws for target failures —
|
|
66
66
|
* those are recorded on the run row; only run-row creation failures propagate.
|
|
67
|
+
*
|
|
68
|
+
* **Once the run row exists, this drives it to a TERMINAL status on every path.** The inner
|
|
69
|
+
* guard around `executeTarget` only ever covered target failures, so a throw from the
|
|
70
|
+
* bookkeeping that follows it — `finalizeRunRow`, `updateRoutineAfterRun`, notification
|
|
71
|
+
* delivery — escaped to the sweep's per-routine catch, which records `Failed` in its in-memory
|
|
72
|
+
* summary and returns. The row on disk stays `Running` forever: nothing reconciles it, the
|
|
73
|
+
* sweep reports success, and the routine looks like it is still executing long after the
|
|
74
|
+
* process that owned it exited. A run row that outlives its executor has to say so.
|
|
67
75
|
*/
|
|
68
76
|
private executeRoutine;
|
|
77
|
+
/** The body of {@link executeRoutine}, from target execution through notification delivery. */
|
|
78
|
+
private runAndRecord;
|
|
69
79
|
/** Create the `MJ: User Routine Runs` row in its initial Running state. */
|
|
70
80
|
private createRunRow;
|
|
71
81
|
/** Persist the run's terminal state + linkage. Telemetry stays on the linked records. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UserRoutineDispatcherDriver.d.ts","sourceRoot":"","sources":["../../src/drivers/UserRoutineDispatcherDriver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,EAAkD,QAAQ,EAA4C,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAC5J,OAAO,EAGH,oBAAoB,EACpB,mBAAmB,EAKtB,MAAM,+BAA+B,CAAC;AAQvC,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,yBAAyB,EAAE,MAAM,uCAAuC,CAAC;AAC3H,OAAO,EAAE,gBAAgB,EAAE,4BAA4B,EAAE,MAAM,qBAAqB,CAAC;AAYrF;;;GAGG;AACH,MAAM,WAAW,kCAAmC,SAAQ,yBAAyB;IACjF,0EAA0E;IAC1E,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAClC;
|
|
1
|
+
{"version":3,"file":"UserRoutineDispatcherDriver.d.ts","sourceRoot":"","sources":["../../src/drivers/UserRoutineDispatcherDriver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,EAAkD,QAAQ,EAA4C,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAC5J,OAAO,EAGH,oBAAoB,EACpB,mBAAmB,EAKtB,MAAM,+BAA+B,CAAC;AAQvC,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,yBAAyB,EAAE,MAAM,uCAAuC,CAAC;AAC3H,OAAO,EAAE,gBAAgB,EAAE,4BAA4B,EAAE,MAAM,qBAAqB,CAAC;AAYrF;;;GAGG;AACH,MAAM,WAAW,kCAAmC,SAAQ,yBAAyB;IACjF,0EAA0E;IAC1E,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAClC;AAyED;;;;;;;;GAQG;AACH,qBACa,2BAA4B,SAAQ,gBAAgB;IAC7D;;;OAGG;IACH,IAAoB,uBAAuB,IAAI,OAAO,CAErD;IAEY,OAAO,CAAC,OAAO,EAAE,4BAA4B,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAmDjF,qBAAqB,CAAC,QAAQ,EAAE,oBAAoB,GAAG,gBAAgB;IAgBvE,kBAAkB,CAAC,OAAO,EAAE,4BAA4B,EAAE,MAAM,EAAE,kBAAkB,GAAG,mBAAmB;IAwBjH,6GAA6G;IAC7G,OAAO,CAAC,4BAA4B;IAOpC;;;OAGG;YACW,qBAAqB;IAcnC,oFAAoF;YACtE,aAAa;IAc3B;;;;;;;;OAQG;YACW,YAAY;IAmB1B;;;;;;;;;;;;OAYG;YACW,cAAc;IAiD5B,+FAA+F;YACjF,YAAY;IA4C1B,2EAA2E;YAC7D,YAAY;IAkB1B,yFAAyF;YAC3E,cAAc;IA0E5B,6FAA6F;YAC/E,qBAAqB;IAUnC,0FAA0F;IAC1F,OAAO,CAAC,kBAAkB;IAW1B;;;OAGG;YACW,aAAa;IAkB3B;;;;;;;;;;OAUG;YACW,kBAAkB;IAsChC;;;;;;;;;;;;;OAaG;IACU,yBAAyB,CAAC,OAAO,EAAE,mBAAmB,EAAE,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IA2C7G,6HAA6H;YAC/G,uBAAuB;IAUrC,6GAA6G;IAC7G,OAAO,CAAC,sBAAsB;IAY9B,2FAA2F;YAC7E,mBAAmB;IAyBjC,iGAAiG;YACnF,mBAAmB;IA6BjC,8FAA8F;IAC9F,OAAO,CAAC,iBAAiB;IAgBzB;;;;;;;;;OASG;YACW,wBAAwB;IAsDtC,2FAA2F;YAC7E,cAAc;IAe5B;;;;OAIG;YACW,yBAAyB;IAqCvC,sGAAsG;IACtG,OAAO,CAAC,2BAA2B;IAWnC,wEAAwE;IACxE,OAAO,CAAC,qBAAqB;IAS7B;;;;;;;;OAQG;YACW,YAAY;IA6B1B,uFAAuF;YACzE,eAAe;IA4B7B;;;;;;;OAOG;IACH,OAAO,CAAC,kBAAkB;CAa7B"}
|
|
@@ -28,8 +28,44 @@ import { TemplateEngineServer } from '@memberjunction/templates';
|
|
|
28
28
|
import { NotificationEngine } from '@memberjunction/notifications';
|
|
29
29
|
import { BaseScheduledJob } from '../BaseScheduledJob.js';
|
|
30
30
|
import { ComputeRoutineNextRunAt, ComputeResultHash, EvaluateNotifyCondition, IsRoutineDue, RoutineNeedsSeeding, BuildDueRoutineFilter, SortRecipientsBySequence, RunWithBoundedConcurrency, } from '../UserRoutineProcessor.js';
|
|
31
|
+
/**
|
|
32
|
+
* Attempts to re-attach a run's execution linkage after the first save was rejected.
|
|
33
|
+
*
|
|
34
|
+
* The linkage columns are foreign keys to rows written fire-and-forget by the action/agent/prompt
|
|
35
|
+
* engines, so a rejection usually means "not inserted yet" rather than "does not exist". A few
|
|
36
|
+
* short, escalating waits cover that window without putting the sweep on the DB's critical path.
|
|
37
|
+
*/
|
|
38
|
+
const RUN_LINKAGE_ATTEMPTS = 3;
|
|
39
|
+
const RUN_LINKAGE_RETRY_MS = 50;
|
|
31
40
|
/** Default bound on concurrent routine executions within a single dispatcher sweep. */
|
|
32
41
|
const DEFAULT_MAX_CONCURRENT_ROUTINES = 3;
|
|
42
|
+
/**
|
|
43
|
+
* The run's status **as last persisted** — never the in-memory value.
|
|
44
|
+
*
|
|
45
|
+
* `BaseEntity.finalizeSave` runs only when the save returned a row, and it calls
|
|
46
|
+
* `SetMany(data, …, replaceOldValues = true)`. So `OldValue` holds the value on disk: it advances
|
|
47
|
+
* to 'Success'/'Failed' when the write lands, and stays 'Running' when it does not.
|
|
48
|
+
*
|
|
49
|
+
* The distinction is the whole point. `finalizeRunRow` assigns `Status = 'Success'` BEFORE its
|
|
50
|
+
* first save, so after a rejected save the in-memory `Status` says 'Success' while the row still
|
|
51
|
+
* says 'Running'. A caller that reads `run.Status` to decide "was an outcome already committed?"
|
|
52
|
+
* gets 'Success' and leaves the row stuck at 'Running' forever — the exact defect this module's
|
|
53
|
+
* tests were written to eliminate, reintroduced at the seam between the two layers that each fix
|
|
54
|
+
* one half of it.
|
|
55
|
+
*
|
|
56
|
+
* Falls back to the in-memory value only when the field cannot be resolved, which cannot happen
|
|
57
|
+
* for a real `MJUserRoutineRunEntity` (`Status` is a column); it keeps this total rather than
|
|
58
|
+
* throwing inside an error handler.
|
|
59
|
+
*/
|
|
60
|
+
function persistedRunStatus(run) {
|
|
61
|
+
const persisted = run.GetFieldByName?.('Status')?.OldValue;
|
|
62
|
+
// `OldValue` is untyped on EntityField, so this is the one narrowing boundary. The column is
|
|
63
|
+
// CHECK-constrained to the same value list as the property, so a string read from it IS that
|
|
64
|
+
// union — asserting anything wider here would just push the cast to every caller.
|
|
65
|
+
return typeof persisted === 'string'
|
|
66
|
+
? persisted
|
|
67
|
+
: run.Status;
|
|
68
|
+
}
|
|
33
69
|
/** Name of the metadata-seeded default notification template (resolved BY NAME, never hardcoded ID). */
|
|
34
70
|
const DEFAULT_NOTIFICATION_TEMPLATE_NAME = 'User Routine Notification - Default';
|
|
35
71
|
/** Name of the metadata-seeded `MJ: User Notification Types` row used for in-app delivery. */
|
|
@@ -201,11 +237,57 @@ let UserRoutineDispatcherDriver = class UserRoutineDispatcherDriver extends Base
|
|
|
201
237
|
* Execute one claimed routine end-to-end: run row → target execution → run/routine
|
|
202
238
|
* bookkeeping → notification decision + delivery. Never throws for target failures —
|
|
203
239
|
* those are recorded on the run row; only run-row creation failures propagate.
|
|
240
|
+
*
|
|
241
|
+
* **Once the run row exists, this drives it to a TERMINAL status on every path.** The inner
|
|
242
|
+
* guard around `executeTarget` only ever covered target failures, so a throw from the
|
|
243
|
+
* bookkeeping that follows it — `finalizeRunRow`, `updateRoutineAfterRun`, notification
|
|
244
|
+
* delivery — escaped to the sweep's per-routine catch, which records `Failed` in its in-memory
|
|
245
|
+
* summary and returns. The row on disk stays `Running` forever: nothing reconciles it, the
|
|
246
|
+
* sweep reports success, and the routine looks like it is still executing long after the
|
|
247
|
+
* process that owned it exited. A run row that outlives its executor has to say so.
|
|
204
248
|
*/
|
|
205
249
|
async executeRoutine(routine, context) {
|
|
206
250
|
const contextUser = context.ContextUser;
|
|
207
251
|
const run = await this.createRunRow(routine, contextUser);
|
|
208
252
|
this.log(`Running routine "${routine.Name}" (${routine.TargetType} target)`, true);
|
|
253
|
+
try {
|
|
254
|
+
return await this.runAndRecord(routine, run, context);
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
258
|
+
this.logError(`Routine "${routine.Name}" (${routine.ID}) failed after its run row was created`, error);
|
|
259
|
+
// ONLY drive a row that is still non-terminal. Everything after `finalizeRunRow` runs
|
|
260
|
+
// inside this scope — `updateRoutineAfterRun`, the notification save — so a throw from
|
|
261
|
+
// there arrives with `Status` already committed as `Success` alongside its
|
|
262
|
+
// ResultSummary and ResultHash, and `MJUserRoutine.LastRunStatus` possibly already
|
|
263
|
+
// agreeing. Overwriting it with `Failed` would rewrite a run that genuinely succeeded
|
|
264
|
+
// and put the run and its routine into disagreement — inventing the very status/disk
|
|
265
|
+
// mismatch this catch was added to eliminate.
|
|
266
|
+
//
|
|
267
|
+
// ASK THE ROW, NOT THE OBJECT. This must read the PERSISTED status, because the other
|
|
268
|
+
// way into this catch is `finalizeRunRow` itself throwing — and it throws having
|
|
269
|
+
// already set `Status = 'Success'` in memory on an object whose saves all failed.
|
|
270
|
+
// Reading `run.Status` there would report a committed success for a row that is still
|
|
271
|
+
// 'Running' on disk, which is precisely the bug the throw exists to signal.
|
|
272
|
+
const committed = persistedRunStatus(run);
|
|
273
|
+
if (committed !== 'Running') {
|
|
274
|
+
this.logError(`Run ${run.ID} already recorded '${committed}'; leaving it as-is. The failure above ` +
|
|
275
|
+
`happened in the bookkeeping AFTER the outcome was committed.`);
|
|
276
|
+
return { RoutineID: routine.ID, RunStatus: committed, Notified: false };
|
|
277
|
+
}
|
|
278
|
+
run.Status = 'Failed';
|
|
279
|
+
run.ErrorMessage = `Run bookkeeping failed: ${message}`;
|
|
280
|
+
run.CompletedAt = new Date();
|
|
281
|
+
if (!(await run.Save())) {
|
|
282
|
+
this.logError(`Run ${run.ID} is STUCK at Running — the failure could not be recorded: ` +
|
|
283
|
+
`${run.LatestResult?.CompleteMessage ?? 'unknown'}`);
|
|
284
|
+
}
|
|
285
|
+
return { RoutineID: routine.ID, RunStatus: 'Failed', Notified: false };
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
/** The body of {@link executeRoutine}, from target execution through notification delivery. */
|
|
289
|
+
async runAndRecord(routine, run, context) {
|
|
290
|
+
const contextUser = context.ContextUser;
|
|
209
291
|
let outcome;
|
|
210
292
|
try {
|
|
211
293
|
outcome = await this.executeTarget(routine, contextUser, context.heartbeat);
|
|
@@ -263,10 +345,56 @@ let UserRoutineDispatcherDriver = class UserRoutineDispatcherDriver extends Base
|
|
|
263
345
|
run.AgentRunID = outcome.AgentRunID;
|
|
264
346
|
run.PromptRunID = outcome.PromptRunID;
|
|
265
347
|
run.ActionExecutionLogID = outcome.ActionExecutionLogID;
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
348
|
+
if (await run.Save()) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
// The run's own status must not be hostage to an OPTIONAL linkage. The three ID columns
|
|
352
|
+
// above are foreign keys to artifacts of the execution — the action log, the agent run, the
|
|
353
|
+
// prompt run — and any of them can fail to resolve (a log row cleaned up by an unrelated
|
|
354
|
+
// process, a cross-session write not yet visible). When that FK is what rejected the save,
|
|
355
|
+
// the previous behaviour logged and carried on: `Status` stayed 'Success' IN MEMORY, the
|
|
356
|
+
// caller reported success from that in-memory value, and the row on disk stayed 'Running'
|
|
357
|
+
// forever with nothing to reconcile it. Losing a link is a diagnostic loss; losing the
|
|
358
|
+
// status is a correctness one — so drop the links and record the outcome.
|
|
359
|
+
const firstFailure = run.LatestResult?.CompleteMessage ?? 'unknown';
|
|
360
|
+
// The usual cause is a RACE, not a bad ID. Action-execution logging is fire-and-forget:
|
|
361
|
+
// `ActionEngine` client-generates the log row's PK and returns it immediately, then INSERTs
|
|
362
|
+
// it off the critical path. So `outcome.ActionExecutionLogID` is a valid identifier for a
|
|
363
|
+
// row that may not exist YET — and writing it into a foreign key can lose that race, which
|
|
364
|
+
// is why this only shows up under load. Land the STATUS first (the correctness-bearing
|
|
365
|
+
// part), then re-attach the linkage once the INSERT has had a chance to land.
|
|
366
|
+
const linkage = {
|
|
367
|
+
AgentRunID: run.AgentRunID,
|
|
368
|
+
PromptRunID: run.PromptRunID,
|
|
369
|
+
ActionExecutionLogID: run.ActionExecutionLogID,
|
|
370
|
+
};
|
|
371
|
+
run.AgentRunID = null;
|
|
372
|
+
run.PromptRunID = null;
|
|
373
|
+
run.ActionExecutionLogID = null;
|
|
374
|
+
if (!(await run.Save())) {
|
|
375
|
+
throw new Error(`Run ${run.ID} could not be finalized and is stuck at 'Running': ${run.LatestResult?.CompleteMessage ?? firstFailure}`);
|
|
376
|
+
}
|
|
377
|
+
for (let attempt = 1; attempt <= RUN_LINKAGE_ATTEMPTS; attempt++) {
|
|
378
|
+
await new Promise((resolve) => setTimeout(resolve, RUN_LINKAGE_RETRY_MS * attempt));
|
|
379
|
+
run.AgentRunID = linkage.AgentRunID;
|
|
380
|
+
run.PromptRunID = linkage.PromptRunID;
|
|
381
|
+
run.ActionExecutionLogID = linkage.ActionExecutionLogID;
|
|
382
|
+
if (await run.Save()) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
// Re-null so the next attempt starts from the persisted (unlinked) state rather than
|
|
386
|
+
// stacking a second rejected write on top of a dirty entity.
|
|
387
|
+
run.AgentRunID = null;
|
|
388
|
+
run.PromptRunID = null;
|
|
389
|
+
run.ActionExecutionLogID = null;
|
|
390
|
+
}
|
|
391
|
+
// Status is recorded; only the diagnostic linkage is missing. Say so and move on — losing a
|
|
392
|
+
// link is an observability loss, and blocking the sweep on it would be the worse trade.
|
|
393
|
+
run.ErrorMessage = [outcome.ErrorMessage, `execution linkage not attached: ${firstFailure}`]
|
|
394
|
+
.filter(Boolean).join(' | ');
|
|
395
|
+
const noted = await run.Save();
|
|
396
|
+
this.logError(`Run ${run.ID} finalized WITHOUT execution linkage after ${RUN_LINKAGE_ATTEMPTS} attempt(s): ${firstFailure}` +
|
|
397
|
+
(noted ? '' : ` — and the note could not be written: ${run.LatestResult?.CompleteMessage ?? 'unknown'}`));
|
|
270
398
|
}
|
|
271
399
|
/** Roll the run outcome up onto the routine (LastRunAt / LastRunStatus / LastResultHash). */
|
|
272
400
|
async updateRoutineAfterRun(routine, run) {
|