ultravisor 1.0.22 → 1.0.24
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/docs/queue-followups/03-config-migration.md +123 -0
- package/docs/queue-followups/04-direct-dispatch-phase-emission.md +128 -0
- package/package.json +3 -1
- package/source/Ultravisor.cjs +4 -0
- package/source/datamodel/Ultravisor-BeaconQueue.json +165 -0
- package/source/services/Ultravisor-Beacon-ActionDefaults.cjs +174 -0
- package/source/services/Ultravisor-Beacon-Coordinator.cjs +382 -6
- package/source/services/Ultravisor-Beacon-RunManager.cjs +169 -0
- package/source/services/Ultravisor-Beacon-Scheduler.cjs +789 -0
- package/source/services/Ultravisor-ExecutionEngine.cjs +242 -6
- package/source/services/Ultravisor-ExecutionManifest.cjs +1 -0
- package/source/services/persistence/Ultravisor-Beacon-QueueStore.cjs +886 -0
- package/source/services/tasks/file-system/Ultravisor-TaskConfigs-FileSystem.cjs +81 -0
- package/source/services/tasks/file-system/definitions/chunked-write.json +38 -0
- package/source/web_server/Ultravisor-API-Server.cjs +354 -0
- package/test/Ultravisor_BeaconQueue_tests.js +502 -0
- package/test/Ultravisor_tests.js +132 -0
|
@@ -0,0 +1,886 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ultravisor Beacon Queue Store
|
|
3
|
+
*
|
|
4
|
+
* SQLite-backed persistence for the beacon work queue, runs, affinity
|
|
5
|
+
* bindings, per-attempt history, and action defaults. Replaces the
|
|
6
|
+
* former JSONL journal as the durable store of record.
|
|
7
|
+
*
|
|
8
|
+
* Schema is driven by the stricture intermediate model at
|
|
9
|
+
* source/datamodel/Ultravisor-BeaconQueue.json. DDL is generated
|
|
10
|
+
* directly (CREATE TABLE IF NOT EXISTS + ALTER TABLE ADD COLUMN for
|
|
11
|
+
* forward-only drift), avoiding the meadow bootstrap chain.
|
|
12
|
+
*
|
|
13
|
+
* SQLite WAL mode is enabled on open — crash-safe, concurrent reader
|
|
14
|
+
* support, no separate journal file to reason about.
|
|
15
|
+
*
|
|
16
|
+
* @module Ultravisor-Beacon-QueueStore
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const libPictService = require('pict-serviceproviderbase');
|
|
20
|
+
const libFS = require('fs');
|
|
21
|
+
const libPath = require('path');
|
|
22
|
+
|
|
23
|
+
let libBetterSqlite = null;
|
|
24
|
+
try
|
|
25
|
+
{
|
|
26
|
+
libBetterSqlite = require('better-sqlite3');
|
|
27
|
+
}
|
|
28
|
+
catch (pError)
|
|
29
|
+
{
|
|
30
|
+
// better-sqlite3 is a hard requirement at runtime but we defer the
|
|
31
|
+
// failure to initialize() so unit tests that don't touch persistence
|
|
32
|
+
// can still load the module.
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const TYPE_INTEGER_RE = /^(Integer|Int|FK|AutoID|AutoIdentity|ID|CreateIDUser|UpdateIDUser|DeleteIDUser|Boolean|Deleted)$/i;
|
|
36
|
+
const TYPE_NUMERIC_INT_SIZES = new Set(['int', 'integer', 'smallint', 'bigint', 'tinyint']);
|
|
37
|
+
|
|
38
|
+
class UltravisorBeaconQueueStore extends libPictService
|
|
39
|
+
{
|
|
40
|
+
constructor(pPict, pOptions, pServiceHash)
|
|
41
|
+
{
|
|
42
|
+
super(pPict, pOptions, pServiceHash);
|
|
43
|
+
|
|
44
|
+
this.serviceType = 'UltravisorBeaconQueueStore';
|
|
45
|
+
|
|
46
|
+
this._DBPath = '';
|
|
47
|
+
this._DB = null;
|
|
48
|
+
this._Schema = null;
|
|
49
|
+
this._Initialized = false;
|
|
50
|
+
this._Enabled = false;
|
|
51
|
+
|
|
52
|
+
// Prepared statement cache — compiled once, reused per call.
|
|
53
|
+
this._Prepared = {};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ====================================================================
|
|
57
|
+
// Lifecycle
|
|
58
|
+
// ====================================================================
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Initialize the store: open SQLite, apply WAL, provision tables.
|
|
62
|
+
*
|
|
63
|
+
* @param {string} pStorePath - Base storage directory
|
|
64
|
+
* @returns {boolean} true on success
|
|
65
|
+
*/
|
|
66
|
+
initialize(pStorePath)
|
|
67
|
+
{
|
|
68
|
+
if (this._Initialized)
|
|
69
|
+
{
|
|
70
|
+
return this._Enabled;
|
|
71
|
+
}
|
|
72
|
+
this._Initialized = true;
|
|
73
|
+
|
|
74
|
+
if (!pStorePath)
|
|
75
|
+
{
|
|
76
|
+
this.log.warn('BeaconQueueStore: no store path; persistence disabled.');
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!libBetterSqlite)
|
|
81
|
+
{
|
|
82
|
+
this.log.error('BeaconQueueStore: better-sqlite3 not installed; persistence disabled.');
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let tmpDir = libPath.join(pStorePath, 'beacon');
|
|
87
|
+
try
|
|
88
|
+
{
|
|
89
|
+
if (!libFS.existsSync(tmpDir))
|
|
90
|
+
{
|
|
91
|
+
libFS.mkdirSync(tmpDir, { recursive: true });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch (pError)
|
|
95
|
+
{
|
|
96
|
+
this.log.error(`BeaconQueueStore: failed to create [${tmpDir}]: ${pError.message}`);
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
this._DBPath = libPath.join(tmpDir, 'beacon-queue.sqlite');
|
|
101
|
+
|
|
102
|
+
try
|
|
103
|
+
{
|
|
104
|
+
this._DB = new libBetterSqlite(this._DBPath);
|
|
105
|
+
this._DB.pragma('journal_mode = WAL');
|
|
106
|
+
this._DB.pragma('synchronous = NORMAL');
|
|
107
|
+
this._DB.pragma('foreign_keys = ON');
|
|
108
|
+
}
|
|
109
|
+
catch (pError)
|
|
110
|
+
{
|
|
111
|
+
this.log.error(`BeaconQueueStore: failed to open [${this._DBPath}]: ${pError.message}`);
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let tmpSchemaPath = libPath.join(__dirname, '..', '..', 'datamodel', 'Ultravisor-BeaconQueue.json');
|
|
116
|
+
try
|
|
117
|
+
{
|
|
118
|
+
this._Schema = JSON.parse(libFS.readFileSync(tmpSchemaPath, 'utf8'));
|
|
119
|
+
}
|
|
120
|
+
catch (pError)
|
|
121
|
+
{
|
|
122
|
+
this.log.error(`BeaconQueueStore: failed to load schema [${tmpSchemaPath}]: ${pError.message}`);
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
try
|
|
127
|
+
{
|
|
128
|
+
this._provisionTables();
|
|
129
|
+
}
|
|
130
|
+
catch (pError)
|
|
131
|
+
{
|
|
132
|
+
this.log.error(`BeaconQueueStore: schema provisioning failed: ${pError.message}`);
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
this._Enabled = true;
|
|
137
|
+
this.log.info(`BeaconQueueStore: ready at [${this._DBPath}].`);
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
isEnabled()
|
|
142
|
+
{
|
|
143
|
+
return this._Enabled;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
close()
|
|
147
|
+
{
|
|
148
|
+
if (this._DB)
|
|
149
|
+
{
|
|
150
|
+
try { this._DB.close(); } catch (pError) { /* ignore */ }
|
|
151
|
+
this._DB = null;
|
|
152
|
+
}
|
|
153
|
+
this._Enabled = false;
|
|
154
|
+
this._Prepared = {};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ====================================================================
|
|
158
|
+
// Schema provisioning
|
|
159
|
+
// ====================================================================
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Translate a stricture column spec to a SQLite column DDL fragment.
|
|
163
|
+
* Forward-only drift uses the same mapping for ALTER TABLE ADD COLUMN.
|
|
164
|
+
*/
|
|
165
|
+
_columnToSql(pCol, pIsPrimary)
|
|
166
|
+
{
|
|
167
|
+
let tmpType = pCol.DataType || pCol.Type || 'String';
|
|
168
|
+
let tmpSize = (pCol.Size || '').toString().toLowerCase();
|
|
169
|
+
|
|
170
|
+
let tmpSqlType = 'TEXT';
|
|
171
|
+
if (/^ID$/i.test(tmpType))
|
|
172
|
+
{
|
|
173
|
+
tmpSqlType = 'INTEGER';
|
|
174
|
+
}
|
|
175
|
+
else if (/^Numeric$/i.test(tmpType))
|
|
176
|
+
{
|
|
177
|
+
tmpSqlType = TYPE_NUMERIC_INT_SIZES.has(tmpSize) ? 'INTEGER' : 'REAL';
|
|
178
|
+
}
|
|
179
|
+
else if (TYPE_INTEGER_RE.test(tmpType))
|
|
180
|
+
{
|
|
181
|
+
tmpSqlType = 'INTEGER';
|
|
182
|
+
}
|
|
183
|
+
else if (/^(DateTime|Date)$/i.test(tmpType))
|
|
184
|
+
{
|
|
185
|
+
tmpSqlType = 'TEXT';
|
|
186
|
+
}
|
|
187
|
+
else
|
|
188
|
+
{
|
|
189
|
+
tmpSqlType = 'TEXT';
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let tmpDDL = `"${pCol.Column}" ${tmpSqlType}`;
|
|
193
|
+
if (pIsPrimary)
|
|
194
|
+
{
|
|
195
|
+
tmpDDL += ' PRIMARY KEY AUTOINCREMENT';
|
|
196
|
+
}
|
|
197
|
+
return tmpDDL;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
_provisionTables()
|
|
201
|
+
{
|
|
202
|
+
let tmpTables = this._Schema.Tables || {};
|
|
203
|
+
for (let tmpName of Object.keys(tmpTables))
|
|
204
|
+
{
|
|
205
|
+
let tmpTable = tmpTables[tmpName];
|
|
206
|
+
let tmpCols = tmpTable.Columns || [];
|
|
207
|
+
if (tmpCols.length === 0) continue;
|
|
208
|
+
|
|
209
|
+
// Convention: first ID column is the primary key.
|
|
210
|
+
let tmpPrimaryIdx = tmpCols.findIndex((c) => /^ID$/i.test(c.DataType || c.Type || ''));
|
|
211
|
+
let tmpDDLCols = tmpCols.map((c, idx) =>
|
|
212
|
+
this._columnToSql(c, idx === tmpPrimaryIdx));
|
|
213
|
+
|
|
214
|
+
let tmpCreate = `CREATE TABLE IF NOT EXISTS "${tmpTable.TableName}" (${tmpDDLCols.join(', ')})`;
|
|
215
|
+
this._DB.exec(tmpCreate);
|
|
216
|
+
|
|
217
|
+
// Forward-only ADD COLUMN migration
|
|
218
|
+
let tmpExisting = {};
|
|
219
|
+
let tmpInfo = this._DB.prepare(`PRAGMA table_info("${tmpTable.TableName}")`).all();
|
|
220
|
+
for (let r of tmpInfo) tmpExisting[r.name] = r;
|
|
221
|
+
|
|
222
|
+
for (let c of tmpCols)
|
|
223
|
+
{
|
|
224
|
+
if (tmpExisting[c.Column]) continue;
|
|
225
|
+
let tmpAdd = this._columnToSql(c, false);
|
|
226
|
+
try
|
|
227
|
+
{
|
|
228
|
+
this._DB.exec(`ALTER TABLE "${tmpTable.TableName}" ADD COLUMN ${tmpAdd}`);
|
|
229
|
+
this.log.info(`BeaconQueueStore: migrated ${tmpTable.TableName}.${c.Column}`);
|
|
230
|
+
}
|
|
231
|
+
catch (pAlterErr)
|
|
232
|
+
{
|
|
233
|
+
if (!/duplicate column/i.test(pAlterErr.message))
|
|
234
|
+
{
|
|
235
|
+
throw pAlterErr;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Useful indices on hot lookup paths.
|
|
242
|
+
let tmpIndices = [
|
|
243
|
+
'CREATE INDEX IF NOT EXISTS idx_workitem_hash ON BeaconWorkItem(WorkItemHash)',
|
|
244
|
+
'CREATE INDEX IF NOT EXISTS idx_workitem_status ON BeaconWorkItem(Status)',
|
|
245
|
+
'CREATE INDEX IF NOT EXISTS idx_workitem_runid ON BeaconWorkItem(RunID)',
|
|
246
|
+
'CREATE INDEX IF NOT EXISTS idx_workitem_assigned ON BeaconWorkItem(AssignedBeaconID, Status)',
|
|
247
|
+
'CREATE INDEX IF NOT EXISTS idx_workitem_priority ON BeaconWorkItem(Status, Priority, EnqueuedAt)',
|
|
248
|
+
'CREATE INDEX IF NOT EXISTS idx_run_runid ON BeaconRun(RunID)',
|
|
249
|
+
'CREATE INDEX IF NOT EXISTS idx_run_idempotency ON BeaconRun(IdempotencyKey)',
|
|
250
|
+
'CREATE INDEX IF NOT EXISTS idx_affinity_key ON BeaconAffinityBinding(AffinityKey)',
|
|
251
|
+
'CREATE INDEX IF NOT EXISTS idx_event_hash ON BeaconWorkItemEvent(WorkItemHash)',
|
|
252
|
+
'CREATE INDEX IF NOT EXISTS idx_action_default ON BeaconActionDefault(Capability, Action)'
|
|
253
|
+
];
|
|
254
|
+
for (let tmpSql of tmpIndices)
|
|
255
|
+
{
|
|
256
|
+
this._DB.exec(tmpSql);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ====================================================================
|
|
261
|
+
// Internal helpers
|
|
262
|
+
// ====================================================================
|
|
263
|
+
|
|
264
|
+
_prepare(pKey, pSql)
|
|
265
|
+
{
|
|
266
|
+
if (!this._Prepared[pKey])
|
|
267
|
+
{
|
|
268
|
+
this._Prepared[pKey] = this._DB.prepare(pSql);
|
|
269
|
+
}
|
|
270
|
+
return this._Prepared[pKey];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
_nowIso()
|
|
274
|
+
{
|
|
275
|
+
return new Date().toISOString();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
_serializeJSON(pValue)
|
|
279
|
+
{
|
|
280
|
+
if (pValue == null) return null;
|
|
281
|
+
if (typeof pValue === 'string') return pValue;
|
|
282
|
+
try { return JSON.stringify(pValue); } catch (pErr) { return null; }
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
_parseJSON(pValue, pFallback)
|
|
286
|
+
{
|
|
287
|
+
if (pValue == null || pValue === '') return (pFallback !== undefined) ? pFallback : null;
|
|
288
|
+
if (typeof pValue !== 'string') return pValue;
|
|
289
|
+
try { return JSON.parse(pValue); } catch (pErr) { return (pFallback !== undefined) ? pFallback : null; }
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ====================================================================
|
|
293
|
+
// Run lifecycle
|
|
294
|
+
// ====================================================================
|
|
295
|
+
|
|
296
|
+
insertRun(pRun)
|
|
297
|
+
{
|
|
298
|
+
if (!this._Enabled) return null;
|
|
299
|
+
let tmpStmt = this._prepare('insertRun', `
|
|
300
|
+
INSERT INTO BeaconRun
|
|
301
|
+
(GUIDBeaconRun, CreateDate, UpdateDate, Deleted, RunID, IdempotencyKey,
|
|
302
|
+
SubmitterTag, State, StartedAt, EndedAt, CanceledAt, CancelReason, Metadata)
|
|
303
|
+
VALUES (@GUIDBeaconRun, @CreateDate, @UpdateDate, 0, @RunID, @IdempotencyKey,
|
|
304
|
+
@SubmitterTag, @State, @StartedAt, @EndedAt, @CanceledAt, @CancelReason, @Metadata)
|
|
305
|
+
`);
|
|
306
|
+
let tmpNow = this._nowIso();
|
|
307
|
+
let tmpRec = {
|
|
308
|
+
GUIDBeaconRun: pRun.GUIDBeaconRun || '',
|
|
309
|
+
CreateDate: tmpNow,
|
|
310
|
+
UpdateDate: tmpNow,
|
|
311
|
+
RunID: pRun.RunID,
|
|
312
|
+
IdempotencyKey: pRun.IdempotencyKey || '',
|
|
313
|
+
SubmitterTag: pRun.SubmitterTag || '',
|
|
314
|
+
State: pRun.State || 'Active',
|
|
315
|
+
StartedAt: pRun.StartedAt || tmpNow,
|
|
316
|
+
EndedAt: pRun.EndedAt || null,
|
|
317
|
+
CanceledAt: pRun.CanceledAt || null,
|
|
318
|
+
CancelReason: pRun.CancelReason || '',
|
|
319
|
+
Metadata: this._serializeJSON(pRun.Metadata)
|
|
320
|
+
};
|
|
321
|
+
tmpStmt.run(tmpRec);
|
|
322
|
+
return this.getRunByRunID(pRun.RunID);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
getRunByRunID(pRunID)
|
|
326
|
+
{
|
|
327
|
+
if (!this._Enabled) return null;
|
|
328
|
+
let tmpStmt = this._prepare('getRunByRunID',
|
|
329
|
+
'SELECT * FROM BeaconRun WHERE RunID = ? AND Deleted = 0 LIMIT 1');
|
|
330
|
+
let tmpRow = tmpStmt.get(pRunID);
|
|
331
|
+
return tmpRow ? this._hydrateRun(tmpRow) : null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
getRunByIdempotencyKey(pKey)
|
|
335
|
+
{
|
|
336
|
+
if (!this._Enabled || !pKey) return null;
|
|
337
|
+
let tmpStmt = this._prepare('getRunByIdempotency',
|
|
338
|
+
'SELECT * FROM BeaconRun WHERE IdempotencyKey = ? AND Deleted = 0 ORDER BY IDBeaconRun DESC LIMIT 1');
|
|
339
|
+
let tmpRow = tmpStmt.get(pKey);
|
|
340
|
+
return tmpRow ? this._hydrateRun(tmpRow) : null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
updateRunState(pRunID, pState, pExtras)
|
|
344
|
+
{
|
|
345
|
+
if (!this._Enabled) return;
|
|
346
|
+
let tmpExtras = pExtras || {};
|
|
347
|
+
let tmpStmt = this._prepare('updateRunState', `
|
|
348
|
+
UPDATE BeaconRun
|
|
349
|
+
SET State = @State,
|
|
350
|
+
EndedAt = COALESCE(@EndedAt, EndedAt),
|
|
351
|
+
CanceledAt = COALESCE(@CanceledAt, CanceledAt),
|
|
352
|
+
CancelReason = COALESCE(@CancelReason, CancelReason),
|
|
353
|
+
UpdateDate = @UpdateDate
|
|
354
|
+
WHERE RunID = @RunID
|
|
355
|
+
`);
|
|
356
|
+
tmpStmt.run({
|
|
357
|
+
RunID: pRunID,
|
|
358
|
+
State: pState,
|
|
359
|
+
EndedAt: tmpExtras.EndedAt || null,
|
|
360
|
+
CanceledAt: tmpExtras.CanceledAt || null,
|
|
361
|
+
CancelReason: tmpExtras.CancelReason || null,
|
|
362
|
+
UpdateDate: this._nowIso()
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
_hydrateRun(pRow)
|
|
367
|
+
{
|
|
368
|
+
return {
|
|
369
|
+
IDBeaconRun: pRow.IDBeaconRun,
|
|
370
|
+
GUIDBeaconRun: pRow.GUIDBeaconRun,
|
|
371
|
+
RunID: pRow.RunID,
|
|
372
|
+
IdempotencyKey: pRow.IdempotencyKey,
|
|
373
|
+
SubmitterTag: pRow.SubmitterTag,
|
|
374
|
+
State: pRow.State,
|
|
375
|
+
StartedAt: pRow.StartedAt,
|
|
376
|
+
EndedAt: pRow.EndedAt,
|
|
377
|
+
CanceledAt: pRow.CanceledAt,
|
|
378
|
+
CancelReason: pRow.CancelReason,
|
|
379
|
+
Metadata: this._parseJSON(pRow.Metadata, {}),
|
|
380
|
+
CreateDate: pRow.CreateDate,
|
|
381
|
+
UpdateDate: pRow.UpdateDate
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// ====================================================================
|
|
386
|
+
// Work item CRUD
|
|
387
|
+
// ====================================================================
|
|
388
|
+
|
|
389
|
+
upsertWorkItem(pItem)
|
|
390
|
+
{
|
|
391
|
+
if (!this._Enabled) return null;
|
|
392
|
+
let tmpExisting = this.getWorkItemByHash(pItem.WorkItemHash);
|
|
393
|
+
let tmpNow = this._nowIso();
|
|
394
|
+
|
|
395
|
+
if (!tmpExisting)
|
|
396
|
+
{
|
|
397
|
+
let tmpInsert = this._prepare('insertWorkItem', `
|
|
398
|
+
INSERT INTO BeaconWorkItem
|
|
399
|
+
(GUIDBeaconWorkItem, CreateDate, UpdateDate, Deleted,
|
|
400
|
+
WorkItemHash, RunID, RunHash, NodeHash, OperationHash,
|
|
401
|
+
Capability, Action, Settings, AffinityKey, Priority,
|
|
402
|
+
Status, AssignedBeaconID, TimeoutMs, MaxAttempts, AttemptNumber,
|
|
403
|
+
RetryBackoffMs, EnqueuedAt, AssignedAt, DispatchedAt, StartedAt,
|
|
404
|
+
CompletedAt, CanceledAt, CancelRequested, CancelReason,
|
|
405
|
+
LastError, LastEventAt, QueueWaitMs,
|
|
406
|
+
Health, HealthLabel, HealthReason, HealthComputedAt, Result)
|
|
407
|
+
VALUES
|
|
408
|
+
(@GUIDBeaconWorkItem, @CreateDate, @UpdateDate, 0,
|
|
409
|
+
@WorkItemHash, @RunID, @RunHash, @NodeHash, @OperationHash,
|
|
410
|
+
@Capability, @Action, @Settings, @AffinityKey, @Priority,
|
|
411
|
+
@Status, @AssignedBeaconID, @TimeoutMs, @MaxAttempts, @AttemptNumber,
|
|
412
|
+
@RetryBackoffMs, @EnqueuedAt, @AssignedAt, @DispatchedAt, @StartedAt,
|
|
413
|
+
@CompletedAt, @CanceledAt, @CancelRequested, @CancelReason,
|
|
414
|
+
@LastError, @LastEventAt, @QueueWaitMs,
|
|
415
|
+
@Health, @HealthLabel, @HealthReason, @HealthComputedAt, @Result)
|
|
416
|
+
`);
|
|
417
|
+
tmpInsert.run({
|
|
418
|
+
GUIDBeaconWorkItem: pItem.GUIDBeaconWorkItem || '',
|
|
419
|
+
CreateDate: tmpNow,
|
|
420
|
+
UpdateDate: tmpNow,
|
|
421
|
+
WorkItemHash: pItem.WorkItemHash,
|
|
422
|
+
RunID: pItem.RunID || '',
|
|
423
|
+
RunHash: pItem.RunHash || '',
|
|
424
|
+
NodeHash: pItem.NodeHash || '',
|
|
425
|
+
OperationHash: pItem.OperationHash || '',
|
|
426
|
+
Capability: pItem.Capability || 'Shell',
|
|
427
|
+
Action: pItem.Action || '',
|
|
428
|
+
Settings: this._serializeJSON(pItem.Settings || {}),
|
|
429
|
+
AffinityKey: pItem.AffinityKey || '',
|
|
430
|
+
Priority: pItem.Priority || 0,
|
|
431
|
+
Status: pItem.Status || 'Queued',
|
|
432
|
+
AssignedBeaconID: pItem.AssignedBeaconID || '',
|
|
433
|
+
TimeoutMs: pItem.TimeoutMs || 300000,
|
|
434
|
+
MaxAttempts: pItem.MaxAttempts || 1,
|
|
435
|
+
AttemptNumber: pItem.AttemptNumber || 0,
|
|
436
|
+
RetryBackoffMs: pItem.RetryBackoffMs || 5000,
|
|
437
|
+
EnqueuedAt: pItem.EnqueuedAt || tmpNow,
|
|
438
|
+
AssignedAt: pItem.AssignedAt || null,
|
|
439
|
+
DispatchedAt: pItem.DispatchedAt || null,
|
|
440
|
+
StartedAt: pItem.StartedAt || null,
|
|
441
|
+
CompletedAt: pItem.CompletedAt || null,
|
|
442
|
+
CanceledAt: pItem.CanceledAt || null,
|
|
443
|
+
CancelRequested: pItem.CancelRequested ? 1 : 0,
|
|
444
|
+
CancelReason: pItem.CancelReason || '',
|
|
445
|
+
LastError: pItem.LastError || '',
|
|
446
|
+
LastEventAt: pItem.LastEventAt || tmpNow,
|
|
447
|
+
QueueWaitMs: pItem.QueueWaitMs || 0,
|
|
448
|
+
Health: (pItem.Health == null) ? null : String(pItem.Health),
|
|
449
|
+
HealthLabel: pItem.HealthLabel || 'Unknown',
|
|
450
|
+
HealthReason: pItem.HealthReason || '',
|
|
451
|
+
HealthComputedAt: pItem.HealthComputedAt || null,
|
|
452
|
+
Result: this._serializeJSON(pItem.Result)
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
else
|
|
456
|
+
{
|
|
457
|
+
this.updateWorkItem(pItem.WorkItemHash, pItem);
|
|
458
|
+
}
|
|
459
|
+
return this.getWorkItemByHash(pItem.WorkItemHash);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
updateWorkItem(pHash, pPatch)
|
|
463
|
+
{
|
|
464
|
+
if (!this._Enabled) return;
|
|
465
|
+
let tmpFields = [];
|
|
466
|
+
let tmpParams = { WorkItemHash: pHash, UpdateDate: this._nowIso() };
|
|
467
|
+
let tmpAllowed = [
|
|
468
|
+
'RunID', 'RunHash', 'NodeHash', 'OperationHash', 'Capability', 'Action',
|
|
469
|
+
'Settings', 'AffinityKey', 'Priority', 'Status', 'AssignedBeaconID',
|
|
470
|
+
'TimeoutMs', 'MaxAttempts', 'AttemptNumber', 'RetryBackoffMs',
|
|
471
|
+
'EnqueuedAt', 'AssignedAt', 'DispatchedAt', 'StartedAt', 'CompletedAt',
|
|
472
|
+
'CanceledAt', 'CancelRequested', 'CancelReason', 'LastError',
|
|
473
|
+
'LastEventAt', 'QueueWaitMs', 'Health', 'HealthLabel', 'HealthReason',
|
|
474
|
+
'HealthComputedAt', 'Result'
|
|
475
|
+
];
|
|
476
|
+
for (let tmpKey of tmpAllowed)
|
|
477
|
+
{
|
|
478
|
+
if (!(tmpKey in pPatch)) continue;
|
|
479
|
+
let tmpVal = pPatch[tmpKey];
|
|
480
|
+
if (tmpKey === 'Settings' || tmpKey === 'Result')
|
|
481
|
+
{
|
|
482
|
+
tmpVal = this._serializeJSON(tmpVal);
|
|
483
|
+
}
|
|
484
|
+
else if (tmpKey === 'Health' && tmpVal != null)
|
|
485
|
+
{
|
|
486
|
+
tmpVal = String(tmpVal);
|
|
487
|
+
}
|
|
488
|
+
else if (tmpKey === 'CancelRequested')
|
|
489
|
+
{
|
|
490
|
+
tmpVal = tmpVal ? 1 : 0;
|
|
491
|
+
}
|
|
492
|
+
tmpFields.push(`"${tmpKey}" = @${tmpKey}`);
|
|
493
|
+
tmpParams[tmpKey] = tmpVal;
|
|
494
|
+
}
|
|
495
|
+
if (tmpFields.length === 0) return;
|
|
496
|
+
tmpFields.push('UpdateDate = @UpdateDate');
|
|
497
|
+
|
|
498
|
+
let tmpSql = `UPDATE BeaconWorkItem SET ${tmpFields.join(', ')} WHERE WorkItemHash = @WorkItemHash`;
|
|
499
|
+
// Not cached — field list varies per call.
|
|
500
|
+
this._DB.prepare(tmpSql).run(tmpParams);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
getWorkItemByHash(pHash)
|
|
504
|
+
{
|
|
505
|
+
if (!this._Enabled) return null;
|
|
506
|
+
let tmpStmt = this._prepare('getWorkItemByHash',
|
|
507
|
+
'SELECT * FROM BeaconWorkItem WHERE WorkItemHash = ? AND Deleted = 0 LIMIT 1');
|
|
508
|
+
let tmpRow = tmpStmt.get(pHash);
|
|
509
|
+
return tmpRow ? this._hydrateWorkItem(tmpRow) : null;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
listWorkItems(pFilter)
|
|
513
|
+
{
|
|
514
|
+
if (!this._Enabled) return [];
|
|
515
|
+
let tmpFilter = pFilter || {};
|
|
516
|
+
let tmpWhere = ['Deleted = 0'];
|
|
517
|
+
let tmpParams = {};
|
|
518
|
+
|
|
519
|
+
if (tmpFilter.Status)
|
|
520
|
+
{
|
|
521
|
+
if (Array.isArray(tmpFilter.Status))
|
|
522
|
+
{
|
|
523
|
+
let tmpKeys = tmpFilter.Status.map((s, i) => `@status_${i}`);
|
|
524
|
+
tmpWhere.push(`Status IN (${tmpKeys.join(',')})`);
|
|
525
|
+
tmpFilter.Status.forEach((s, i) => { tmpParams[`status_${i}`] = s; });
|
|
526
|
+
}
|
|
527
|
+
else
|
|
528
|
+
{
|
|
529
|
+
tmpWhere.push('Status = @Status');
|
|
530
|
+
tmpParams.Status = tmpFilter.Status;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
if (tmpFilter.AssignedBeaconID)
|
|
534
|
+
{
|
|
535
|
+
tmpWhere.push('AssignedBeaconID = @AssignedBeaconID');
|
|
536
|
+
tmpParams.AssignedBeaconID = tmpFilter.AssignedBeaconID;
|
|
537
|
+
}
|
|
538
|
+
if (tmpFilter.RunID)
|
|
539
|
+
{
|
|
540
|
+
tmpWhere.push('RunID = @RunID');
|
|
541
|
+
tmpParams.RunID = tmpFilter.RunID;
|
|
542
|
+
}
|
|
543
|
+
if (tmpFilter.Capability)
|
|
544
|
+
{
|
|
545
|
+
tmpWhere.push('Capability = @Capability');
|
|
546
|
+
tmpParams.Capability = tmpFilter.Capability;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
let tmpOrder = tmpFilter.OrderBy || 'EnqueuedAt ASC';
|
|
550
|
+
let tmpLimit = Math.max(1, Math.min(parseInt(tmpFilter.Limit, 10) || 500, 5000));
|
|
551
|
+
|
|
552
|
+
let tmpSql = `SELECT * FROM BeaconWorkItem WHERE ${tmpWhere.join(' AND ')} ORDER BY ${tmpOrder} LIMIT ${tmpLimit}`;
|
|
553
|
+
let tmpRows = this._DB.prepare(tmpSql).all(tmpParams);
|
|
554
|
+
return tmpRows.map((r) => this._hydrateWorkItem(r));
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
countByStatus()
|
|
558
|
+
{
|
|
559
|
+
if (!this._Enabled) return {};
|
|
560
|
+
let tmpStmt = this._prepare('countByStatus',
|
|
561
|
+
'SELECT Status, COUNT(*) as Count FROM BeaconWorkItem WHERE Deleted = 0 GROUP BY Status');
|
|
562
|
+
let tmpRows = tmpStmt.all();
|
|
563
|
+
let tmpOut = {};
|
|
564
|
+
for (let r of tmpRows) tmpOut[r.Status] = r.Count;
|
|
565
|
+
return tmpOut;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
_hydrateWorkItem(pRow)
|
|
569
|
+
{
|
|
570
|
+
let tmpHealth = null;
|
|
571
|
+
if (pRow.Health != null && pRow.Health !== '')
|
|
572
|
+
{
|
|
573
|
+
let tmpParsed = parseFloat(pRow.Health);
|
|
574
|
+
if (!isNaN(tmpParsed)) tmpHealth = tmpParsed;
|
|
575
|
+
}
|
|
576
|
+
return {
|
|
577
|
+
IDBeaconWorkItem: pRow.IDBeaconWorkItem,
|
|
578
|
+
GUIDBeaconWorkItem: pRow.GUIDBeaconWorkItem,
|
|
579
|
+
WorkItemHash: pRow.WorkItemHash,
|
|
580
|
+
RunID: pRow.RunID,
|
|
581
|
+
RunHash: pRow.RunHash,
|
|
582
|
+
NodeHash: pRow.NodeHash,
|
|
583
|
+
OperationHash: pRow.OperationHash,
|
|
584
|
+
Capability: pRow.Capability,
|
|
585
|
+
Action: pRow.Action,
|
|
586
|
+
Settings: this._parseJSON(pRow.Settings, {}),
|
|
587
|
+
AffinityKey: pRow.AffinityKey,
|
|
588
|
+
Priority: pRow.Priority,
|
|
589
|
+
Status: pRow.Status,
|
|
590
|
+
AssignedBeaconID: pRow.AssignedBeaconID || null,
|
|
591
|
+
TimeoutMs: pRow.TimeoutMs,
|
|
592
|
+
MaxAttempts: pRow.MaxAttempts,
|
|
593
|
+
AttemptNumber: pRow.AttemptNumber,
|
|
594
|
+
RetryBackoffMs: pRow.RetryBackoffMs,
|
|
595
|
+
EnqueuedAt: pRow.EnqueuedAt,
|
|
596
|
+
AssignedAt: pRow.AssignedAt,
|
|
597
|
+
DispatchedAt: pRow.DispatchedAt,
|
|
598
|
+
StartedAt: pRow.StartedAt,
|
|
599
|
+
CompletedAt: pRow.CompletedAt,
|
|
600
|
+
CanceledAt: pRow.CanceledAt,
|
|
601
|
+
CancelRequested: !!pRow.CancelRequested,
|
|
602
|
+
CancelReason: pRow.CancelReason,
|
|
603
|
+
LastError: pRow.LastError,
|
|
604
|
+
LastEventAt: pRow.LastEventAt,
|
|
605
|
+
QueueWaitMs: pRow.QueueWaitMs,
|
|
606
|
+
Health: tmpHealth,
|
|
607
|
+
HealthLabel: pRow.HealthLabel || 'Unknown',
|
|
608
|
+
HealthReason: pRow.HealthReason,
|
|
609
|
+
HealthComputedAt: pRow.HealthComputedAt,
|
|
610
|
+
Result: this._parseJSON(pRow.Result)
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// ====================================================================
|
|
615
|
+
// Events
|
|
616
|
+
// ====================================================================
|
|
617
|
+
|
|
618
|
+
appendEvent(pEvent)
|
|
619
|
+
{
|
|
620
|
+
if (!this._Enabled) return;
|
|
621
|
+
let tmpStmt = this._prepare('appendEvent', `
|
|
622
|
+
INSERT INTO BeaconWorkItemEvent
|
|
623
|
+
(CreateDate, WorkItemHash, RunID, EventType, FromStatus, ToStatus, BeaconID, Payload)
|
|
624
|
+
VALUES (@CreateDate, @WorkItemHash, @RunID, @EventType, @FromStatus, @ToStatus, @BeaconID, @Payload)
|
|
625
|
+
`);
|
|
626
|
+
tmpStmt.run({
|
|
627
|
+
CreateDate: this._nowIso(),
|
|
628
|
+
WorkItemHash: pEvent.WorkItemHash || '',
|
|
629
|
+
RunID: pEvent.RunID || '',
|
|
630
|
+
EventType: pEvent.EventType || '',
|
|
631
|
+
FromStatus: pEvent.FromStatus || '',
|
|
632
|
+
ToStatus: pEvent.ToStatus || '',
|
|
633
|
+
BeaconID: pEvent.BeaconID || '',
|
|
634
|
+
Payload: this._serializeJSON(pEvent.Payload)
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
listEventsForWorkItem(pHash, pLimit)
|
|
639
|
+
{
|
|
640
|
+
if (!this._Enabled) return [];
|
|
641
|
+
let tmpLimit = Math.max(1, Math.min(parseInt(pLimit, 10) || 200, 2000));
|
|
642
|
+
let tmpStmt = this._DB.prepare(
|
|
643
|
+
`SELECT * FROM BeaconWorkItemEvent WHERE WorkItemHash = ? ORDER BY IDBeaconWorkItemEvent ASC LIMIT ${tmpLimit}`);
|
|
644
|
+
return tmpStmt.all(pHash).map((r) => ({
|
|
645
|
+
IDBeaconWorkItemEvent: r.IDBeaconWorkItemEvent,
|
|
646
|
+
CreateDate: r.CreateDate,
|
|
647
|
+
WorkItemHash: r.WorkItemHash,
|
|
648
|
+
RunID: r.RunID,
|
|
649
|
+
EventType: r.EventType,
|
|
650
|
+
FromStatus: r.FromStatus,
|
|
651
|
+
ToStatus: r.ToStatus,
|
|
652
|
+
BeaconID: r.BeaconID,
|
|
653
|
+
Payload: this._parseJSON(r.Payload)
|
|
654
|
+
}));
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// ====================================================================
|
|
658
|
+
// Attempts
|
|
659
|
+
// ====================================================================
|
|
660
|
+
|
|
661
|
+
insertAttempt(pAttempt)
|
|
662
|
+
{
|
|
663
|
+
if (!this._Enabled) return;
|
|
664
|
+
let tmpStmt = this._prepare('insertAttempt', `
|
|
665
|
+
INSERT INTO BeaconWorkItemAttempt
|
|
666
|
+
(CreateDate, UpdateDate, WorkItemHash, AttemptNumber, BeaconID,
|
|
667
|
+
DispatchedAt, StartedAt, CompletedAt, Outcome, ErrorMessage, DurationMs)
|
|
668
|
+
VALUES (@CreateDate, @UpdateDate, @WorkItemHash, @AttemptNumber, @BeaconID,
|
|
669
|
+
@DispatchedAt, @StartedAt, @CompletedAt, @Outcome, @ErrorMessage, @DurationMs)
|
|
670
|
+
`);
|
|
671
|
+
let tmpNow = this._nowIso();
|
|
672
|
+
tmpStmt.run({
|
|
673
|
+
CreateDate: tmpNow,
|
|
674
|
+
UpdateDate: tmpNow,
|
|
675
|
+
WorkItemHash: pAttempt.WorkItemHash,
|
|
676
|
+
AttemptNumber: pAttempt.AttemptNumber || 1,
|
|
677
|
+
BeaconID: pAttempt.BeaconID || '',
|
|
678
|
+
DispatchedAt: pAttempt.DispatchedAt || tmpNow,
|
|
679
|
+
StartedAt: pAttempt.StartedAt || null,
|
|
680
|
+
CompletedAt: pAttempt.CompletedAt || null,
|
|
681
|
+
Outcome: pAttempt.Outcome || 'Dispatched',
|
|
682
|
+
ErrorMessage: pAttempt.ErrorMessage || '',
|
|
683
|
+
DurationMs: pAttempt.DurationMs || 0
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
updateAttemptOutcome(pHash, pAttemptNumber, pPatch)
|
|
688
|
+
{
|
|
689
|
+
if (!this._Enabled) return;
|
|
690
|
+
let tmpStmt = this._prepare('updateAttemptOutcome', `
|
|
691
|
+
UPDATE BeaconWorkItemAttempt
|
|
692
|
+
SET StartedAt = COALESCE(@StartedAt, StartedAt),
|
|
693
|
+
CompletedAt = COALESCE(@CompletedAt, CompletedAt),
|
|
694
|
+
Outcome = COALESCE(@Outcome, Outcome),
|
|
695
|
+
ErrorMessage = COALESCE(@ErrorMessage, ErrorMessage),
|
|
696
|
+
DurationMs = COALESCE(@DurationMs, DurationMs),
|
|
697
|
+
UpdateDate = @UpdateDate
|
|
698
|
+
WHERE WorkItemHash = @WorkItemHash AND AttemptNumber = @AttemptNumber
|
|
699
|
+
`);
|
|
700
|
+
tmpStmt.run({
|
|
701
|
+
WorkItemHash: pHash,
|
|
702
|
+
AttemptNumber: pAttemptNumber,
|
|
703
|
+
StartedAt: pPatch.StartedAt || null,
|
|
704
|
+
CompletedAt: pPatch.CompletedAt || null,
|
|
705
|
+
Outcome: pPatch.Outcome || null,
|
|
706
|
+
ErrorMessage: pPatch.ErrorMessage || null,
|
|
707
|
+
DurationMs: pPatch.DurationMs || null,
|
|
708
|
+
UpdateDate: this._nowIso()
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// ====================================================================
|
|
713
|
+
// Affinity bindings
|
|
714
|
+
// ====================================================================
|
|
715
|
+
|
|
716
|
+
upsertAffinityBinding(pBinding)
|
|
717
|
+
{
|
|
718
|
+
if (!this._Enabled) return;
|
|
719
|
+
let tmpExisting = this.getAffinityBinding(pBinding.AffinityKey);
|
|
720
|
+
let tmpNow = this._nowIso();
|
|
721
|
+
if (!tmpExisting)
|
|
722
|
+
{
|
|
723
|
+
let tmpStmt = this._prepare('insertAffinity', `
|
|
724
|
+
INSERT INTO BeaconAffinityBinding
|
|
725
|
+
(CreateDate, UpdateDate, AffinityKey, BeaconID, ExpiresAt, ClearedAt)
|
|
726
|
+
VALUES (@CreateDate, @UpdateDate, @AffinityKey, @BeaconID, @ExpiresAt, @ClearedAt)
|
|
727
|
+
`);
|
|
728
|
+
tmpStmt.run({
|
|
729
|
+
CreateDate: tmpNow,
|
|
730
|
+
UpdateDate: tmpNow,
|
|
731
|
+
AffinityKey: pBinding.AffinityKey,
|
|
732
|
+
BeaconID: pBinding.BeaconID,
|
|
733
|
+
ExpiresAt: pBinding.ExpiresAt || null,
|
|
734
|
+
ClearedAt: null
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
else
|
|
738
|
+
{
|
|
739
|
+
let tmpStmt = this._prepare('updateAffinity', `
|
|
740
|
+
UPDATE BeaconAffinityBinding
|
|
741
|
+
SET BeaconID = @BeaconID, ExpiresAt = @ExpiresAt, ClearedAt = NULL, UpdateDate = @UpdateDate
|
|
742
|
+
WHERE AffinityKey = @AffinityKey
|
|
743
|
+
`);
|
|
744
|
+
tmpStmt.run({
|
|
745
|
+
AffinityKey: pBinding.AffinityKey,
|
|
746
|
+
BeaconID: pBinding.BeaconID,
|
|
747
|
+
ExpiresAt: pBinding.ExpiresAt || null,
|
|
748
|
+
UpdateDate: tmpNow
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
getAffinityBinding(pKey)
|
|
754
|
+
{
|
|
755
|
+
if (!this._Enabled) return null;
|
|
756
|
+
let tmpStmt = this._prepare('getAffinity', `
|
|
757
|
+
SELECT * FROM BeaconAffinityBinding
|
|
758
|
+
WHERE AffinityKey = ? AND (ClearedAt IS NULL OR ClearedAt = '')
|
|
759
|
+
ORDER BY IDBeaconAffinityBinding DESC LIMIT 1
|
|
760
|
+
`);
|
|
761
|
+
let tmpRow = tmpStmt.get(pKey);
|
|
762
|
+
return tmpRow ? {
|
|
763
|
+
AffinityKey: tmpRow.AffinityKey,
|
|
764
|
+
BeaconID: tmpRow.BeaconID,
|
|
765
|
+
ExpiresAt: tmpRow.ExpiresAt,
|
|
766
|
+
ClearedAt: tmpRow.ClearedAt,
|
|
767
|
+
CreateDate: tmpRow.CreateDate,
|
|
768
|
+
UpdateDate: tmpRow.UpdateDate
|
|
769
|
+
} : null;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
clearAffinityBinding(pKey)
|
|
773
|
+
{
|
|
774
|
+
if (!this._Enabled) return;
|
|
775
|
+
let tmpStmt = this._prepare('clearAffinity', `
|
|
776
|
+
UPDATE BeaconAffinityBinding
|
|
777
|
+
SET ClearedAt = @ClearedAt, UpdateDate = @UpdateDate
|
|
778
|
+
WHERE AffinityKey = @AffinityKey
|
|
779
|
+
`);
|
|
780
|
+
let tmpNow = this._nowIso();
|
|
781
|
+
tmpStmt.run({ AffinityKey: pKey, ClearedAt: tmpNow, UpdateDate: tmpNow });
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
listActiveAffinityBindings()
|
|
785
|
+
{
|
|
786
|
+
if (!this._Enabled) return [];
|
|
787
|
+
let tmpStmt = this._prepare('listAffinities', `
|
|
788
|
+
SELECT * FROM BeaconAffinityBinding
|
|
789
|
+
WHERE ClearedAt IS NULL OR ClearedAt = ''
|
|
790
|
+
`);
|
|
791
|
+
return tmpStmt.all();
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// ====================================================================
|
|
795
|
+
// Action defaults
|
|
796
|
+
// ====================================================================
|
|
797
|
+
|
|
798
|
+
upsertActionDefault(pEntry)
|
|
799
|
+
{
|
|
800
|
+
if (!this._Enabled) return;
|
|
801
|
+
let tmpExisting = this.getActionDefault(pEntry.Capability, pEntry.Action);
|
|
802
|
+
let tmpNow = this._nowIso();
|
|
803
|
+
if (!tmpExisting)
|
|
804
|
+
{
|
|
805
|
+
let tmpStmt = this._prepare('insertActionDefault', `
|
|
806
|
+
INSERT INTO BeaconActionDefault
|
|
807
|
+
(CreateDate, UpdateDate, Capability, Action, TimeoutMs, MaxAttempts,
|
|
808
|
+
RetryBackoffMs, DefaultPriority, ExpectedWaitP95Ms, HeartbeatExpectedMs, MinSamplesForBaseline)
|
|
809
|
+
VALUES (@CreateDate, @UpdateDate, @Capability, @Action, @TimeoutMs, @MaxAttempts,
|
|
810
|
+
@RetryBackoffMs, @DefaultPriority, @ExpectedWaitP95Ms, @HeartbeatExpectedMs, @MinSamplesForBaseline)
|
|
811
|
+
`);
|
|
812
|
+
tmpStmt.run({
|
|
813
|
+
CreateDate: tmpNow,
|
|
814
|
+
UpdateDate: tmpNow,
|
|
815
|
+
Capability: pEntry.Capability,
|
|
816
|
+
Action: pEntry.Action || '',
|
|
817
|
+
TimeoutMs: pEntry.TimeoutMs || 300000,
|
|
818
|
+
MaxAttempts: pEntry.MaxAttempts || 1,
|
|
819
|
+
RetryBackoffMs: pEntry.RetryBackoffMs || 5000,
|
|
820
|
+
DefaultPriority: pEntry.DefaultPriority || 0,
|
|
821
|
+
ExpectedWaitP95Ms: pEntry.ExpectedWaitP95Ms || 0,
|
|
822
|
+
HeartbeatExpectedMs: pEntry.HeartbeatExpectedMs || 0,
|
|
823
|
+
MinSamplesForBaseline: pEntry.MinSamplesForBaseline || 20
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
else
|
|
827
|
+
{
|
|
828
|
+
let tmpStmt = this._prepare('updateActionDefault', `
|
|
829
|
+
UPDATE BeaconActionDefault
|
|
830
|
+
SET TimeoutMs = @TimeoutMs, MaxAttempts = @MaxAttempts,
|
|
831
|
+
RetryBackoffMs = @RetryBackoffMs, DefaultPriority = @DefaultPriority,
|
|
832
|
+
ExpectedWaitP95Ms = @ExpectedWaitP95Ms,
|
|
833
|
+
HeartbeatExpectedMs = @HeartbeatExpectedMs,
|
|
834
|
+
MinSamplesForBaseline = @MinSamplesForBaseline,
|
|
835
|
+
UpdateDate = @UpdateDate
|
|
836
|
+
WHERE Capability = @Capability AND Action = @Action
|
|
837
|
+
`);
|
|
838
|
+
tmpStmt.run({
|
|
839
|
+
Capability: pEntry.Capability,
|
|
840
|
+
Action: pEntry.Action || '',
|
|
841
|
+
TimeoutMs: pEntry.TimeoutMs || tmpExisting.TimeoutMs,
|
|
842
|
+
MaxAttempts: pEntry.MaxAttempts || tmpExisting.MaxAttempts,
|
|
843
|
+
RetryBackoffMs: pEntry.RetryBackoffMs || tmpExisting.RetryBackoffMs,
|
|
844
|
+
DefaultPriority: (pEntry.DefaultPriority != null) ? pEntry.DefaultPriority : tmpExisting.DefaultPriority,
|
|
845
|
+
ExpectedWaitP95Ms: (pEntry.ExpectedWaitP95Ms != null) ? pEntry.ExpectedWaitP95Ms : tmpExisting.ExpectedWaitP95Ms,
|
|
846
|
+
HeartbeatExpectedMs: (pEntry.HeartbeatExpectedMs != null) ? pEntry.HeartbeatExpectedMs : tmpExisting.HeartbeatExpectedMs,
|
|
847
|
+
MinSamplesForBaseline: (pEntry.MinSamplesForBaseline != null) ? pEntry.MinSamplesForBaseline : tmpExisting.MinSamplesForBaseline,
|
|
848
|
+
UpdateDate: tmpNow
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
getActionDefault(pCapability, pAction)
|
|
854
|
+
{
|
|
855
|
+
if (!this._Enabled) return null;
|
|
856
|
+
let tmpStmt = this._prepare('getActionDefault', `
|
|
857
|
+
SELECT * FROM BeaconActionDefault
|
|
858
|
+
WHERE Capability = ? AND Action = ? LIMIT 1
|
|
859
|
+
`);
|
|
860
|
+
return tmpStmt.get(pCapability, pAction || '') || null;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
listActionDefaults()
|
|
864
|
+
{
|
|
865
|
+
if (!this._Enabled) return [];
|
|
866
|
+
return this._DB.prepare('SELECT * FROM BeaconActionDefault').all();
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// ====================================================================
|
|
870
|
+
// Aggregate queries for observability
|
|
871
|
+
// ====================================================================
|
|
872
|
+
|
|
873
|
+
queueWaitSamples(pCapability, pAction, pLimit)
|
|
874
|
+
{
|
|
875
|
+
if (!this._Enabled) return [];
|
|
876
|
+
let tmpLimit = Math.max(1, Math.min(parseInt(pLimit, 10) || 200, 2000));
|
|
877
|
+
let tmpStmt = this._DB.prepare(`
|
|
878
|
+
SELECT QueueWaitMs FROM BeaconWorkItem
|
|
879
|
+
WHERE Capability = ? AND Action = ? AND QueueWaitMs > 0 AND Deleted = 0
|
|
880
|
+
ORDER BY IDBeaconWorkItem DESC LIMIT ${tmpLimit}
|
|
881
|
+
`);
|
|
882
|
+
return tmpStmt.all(pCapability, pAction || '').map((r) => r.QueueWaitMs);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
module.exports = UltravisorBeaconQueueStore;
|