ultravisor 1.0.24 → 1.0.26
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/_sidebar.md +1 -0
- package/docs/features/persistence-via-databeacon.md +1211 -0
- package/package.json +6 -6
- package/source/cli/Ultravisor-CLIProgram.cjs +80 -0
- package/source/config/Ultravisor-Default-Command-Configuration.cjs +9 -1
- package/source/datamodel/Ultravisor-Fleet.json +66 -0
- package/source/persistence/UltravisorPersistenceSchema.json +240 -0
- package/source/services/Ultravisor-AuthBeaconBridge.cjs +271 -0
- package/source/services/Ultravisor-Beacon-Coordinator.cjs +339 -151
- package/source/services/Ultravisor-Beacon-Scheduler.cjs +65 -29
- package/source/services/Ultravisor-DirectoryDistributor.cjs +280 -0
- package/source/services/Ultravisor-ExecutionManifest.cjs +99 -4
- package/source/services/Ultravisor-FleetManager.cjs +871 -0
- package/source/services/Ultravisor-ManifestStoreBridge.cjs +1134 -0
- package/source/services/Ultravisor-QueuePersistenceBridge.cjs +1336 -0
- package/source/services/persistence/Ultravisor-Beacon-FleetStore.cjs +570 -0
- package/source/web_server/Ultravisor-API-Server.cjs +1185 -90
- package/test/fleetstore-smoke.js +152 -0
- package/webinterface/package.json +1 -0
- package/webinterface/source/Pict-Application-Ultravisor.js +59 -2
- package/webinterface/source/providers/PictRouter-Ultravisor-Configuration.json +12 -0
- package/webinterface/source/views/PictView-Ultravisor-Fleet.js +489 -0
- package/webinterface/source/views/PictView-Ultravisor-Login.js +74 -0
- package/webinterface/source/views/PictView-Ultravisor-TopBar.js +26 -0
- package/webinterface/source/views/PictView-Ultravisor-UserManagement.js +159 -0
|
@@ -113,6 +113,62 @@ class UltravisorBeaconCoordinator extends libPictService
|
|
|
113
113
|
return (tmpStore && tmpStore.isEnabled && tmpStore.isEnabled()) ? tmpStore : null;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Get the queue persistence bridge (lazy). Falls back to direct
|
|
118
|
+
* QueueStore writes when the bridge isn't installed (older
|
|
119
|
+
* deployments, tests).
|
|
120
|
+
*
|
|
121
|
+
* @returns {object|null}
|
|
122
|
+
*/
|
|
123
|
+
_getQueuePersistenceBridge()
|
|
124
|
+
{
|
|
125
|
+
let tmpMap = this.fable.servicesMap && this.fable.servicesMap.UltravisorQueuePersistenceBridge;
|
|
126
|
+
if (!tmpMap) return null;
|
|
127
|
+
return Object.values(tmpMap)[0] || null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Capabilities the bridge skips persistence for. Their own dispatches
|
|
132
|
+
* are themselves work items; recording them would loop:
|
|
133
|
+
* QueuePersistence → QP_AppendEvent → enqueueWorkItem → QP_AppendEvent → ...
|
|
134
|
+
* ManifestStore → MS_UpsertManifest → enqueueWorkItem → MS_UpsertManifest → ...
|
|
135
|
+
* MeadowProxy → MeadowProxy.Request (the bridge's new dispatch
|
|
136
|
+
* backend) → enqueueWorkItem → MeadowProxy.Request → ...
|
|
137
|
+
* DataBeaconSchema → bootstrap-time EnsureSchema/IntrospectSchema; same loop hazard.
|
|
138
|
+
* DataBeaconManagement → bootstrap-time EnableEndpoint/UpdateProxyConfig; same loop hazard.
|
|
139
|
+
* The set lives on the coordinator because it's the only place that
|
|
140
|
+
* decides "is this work item worth persisting?".
|
|
141
|
+
*/
|
|
142
|
+
_isMetaCapability(pCapability)
|
|
143
|
+
{
|
|
144
|
+
return pCapability === 'QueuePersistence'
|
|
145
|
+
|| pCapability === 'ManifestStore'
|
|
146
|
+
|| pCapability === 'MeadowProxy'
|
|
147
|
+
|| pCapability === 'DataBeaconSchema'
|
|
148
|
+
|| pCapability === 'DataBeaconManagement';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Centralized fire-and-forget for bridge calls. Promise rejections
|
|
153
|
+
* land here as warnings instead of unhandled-rejection events,
|
|
154
|
+
* matching the original `try { ... } catch (...)` best-effort
|
|
155
|
+
* shape of every legacy QueueStore call site.
|
|
156
|
+
*/
|
|
157
|
+
_persistBest(pPromise, pLabel)
|
|
158
|
+
{
|
|
159
|
+
if (!pPromise || typeof pPromise.then !== 'function') return;
|
|
160
|
+
pPromise.then((pResult) =>
|
|
161
|
+
{
|
|
162
|
+
if (pResult && pResult.Success === false && pResult.Reason)
|
|
163
|
+
{
|
|
164
|
+
this.log.warn(`BeaconCoordinator: ${pLabel}: ${pResult.Reason}`);
|
|
165
|
+
}
|
|
166
|
+
}).catch((pErr) =>
|
|
167
|
+
{
|
|
168
|
+
this.log.warn(`BeaconCoordinator: ${pLabel} threw: ${(pErr && pErr.message) || pErr}`);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
116
172
|
/**
|
|
117
173
|
* Get the scheduler service (lazy lookup).
|
|
118
174
|
*
|
|
@@ -242,9 +298,19 @@ class UltravisorBeaconCoordinator extends libPictService
|
|
|
242
298
|
{
|
|
243
299
|
let tmpName = pBeaconInfo.Name || 'unnamed';
|
|
244
300
|
|
|
245
|
-
// Check for an existing
|
|
301
|
+
// Check for an existing beacon with the same name to reclaim.
|
|
302
|
+
// Status='Offline' is the original reconnect-after-disconnect case.
|
|
303
|
+
// Status='Online' is the post-enable refreshRegistration case
|
|
304
|
+
// (SDK pushed updated capabilities without disconnecting first).
|
|
305
|
+
// Either way, same name → same beacon — update in place rather
|
|
306
|
+
// than creating a duplicate record (which would orphan the
|
|
307
|
+
// FleetStore installation rows keyed on the old BeaconID and
|
|
308
|
+
// confuse the dispatch filter).
|
|
246
309
|
let tmpExistingBeacon = this.findBeaconByName(tmpName);
|
|
247
|
-
if (tmpExistingBeacon
|
|
310
|
+
if (tmpExistingBeacon
|
|
311
|
+
&& (tmpExistingBeacon.Status === 'Offline'
|
|
312
|
+
|| tmpExistingBeacon.Status === 'Online'
|
|
313
|
+
|| tmpExistingBeacon.Status === 'Busy'))
|
|
248
314
|
{
|
|
249
315
|
tmpExistingBeacon.SessionID = pSessionID || null;
|
|
250
316
|
tmpExistingBeacon.LastHeartbeat = new Date().toISOString();
|
|
@@ -303,6 +369,12 @@ class UltravisorBeaconCoordinator extends libPictService
|
|
|
303
369
|
tmpReachability.onBeaconRegistered(tmpExistingBeacon.BeaconID);
|
|
304
370
|
}
|
|
305
371
|
|
|
372
|
+
// Bootstrap-flush: a reconnect is exactly the case we care
|
|
373
|
+
// about — the beacon was offline, local writes accumulated,
|
|
374
|
+
// now the beacon is back. Fire the same notification path
|
|
375
|
+
// as a fresh register so the bridges sweep their backlog.
|
|
376
|
+
this._notifyPersistenceBridgesOnConnect(tmpExistingBeacon.BeaconID);
|
|
377
|
+
|
|
306
378
|
return tmpExistingBeacon;
|
|
307
379
|
}
|
|
308
380
|
|
|
@@ -352,9 +424,81 @@ class UltravisorBeaconCoordinator extends libPictService
|
|
|
352
424
|
tmpReachability.onBeaconRegistered(tmpBeaconID);
|
|
353
425
|
}
|
|
354
426
|
|
|
427
|
+
// Notify the fleet manager so it can:
|
|
428
|
+
// - auto-push any registered runtimes whose AutoPushOnConnect
|
|
429
|
+
// is true and whose hash on the worker is stale
|
|
430
|
+
// - auto-discover models the worker reports via LWM_Inventory
|
|
431
|
+
// and import them into the fleet table at Source='discovered',
|
|
432
|
+
// EnabledForDispatch=true (so existing dispatches keep working
|
|
433
|
+
// without operator intervention)
|
|
434
|
+
//
|
|
435
|
+
// Critical: defer to the next tick. The WebSocket upgrade handler
|
|
436
|
+
// (which sets `_WorkItemPushHandler`) runs in the SAME tick as
|
|
437
|
+
// `/Beacon/Register`. If we kick off LWM_Inventory or runtime
|
|
438
|
+
// pushes synchronously here, those work items get pre-assigned
|
|
439
|
+
// (via the fleet-push affinity) BUT the WS handler isn't wired
|
|
440
|
+
// yet, so they never deliver — they just fill the beacon's
|
|
441
|
+
// CurrentWorkItems slot and starve every subsequent dispatch.
|
|
442
|
+
// setImmediate buys us "after the WS upgrade settles" without
|
|
443
|
+
// adding a real timer.
|
|
444
|
+
let tmpFleet = this._getService('UltravisorFleetManager');
|
|
445
|
+
if (tmpFleet && typeof tmpFleet.onBeaconConnected === 'function')
|
|
446
|
+
{
|
|
447
|
+
setImmediate(() =>
|
|
448
|
+
{
|
|
449
|
+
try { tmpFleet.onBeaconConnected(tmpBeaconID); }
|
|
450
|
+
catch (pErr)
|
|
451
|
+
{
|
|
452
|
+
this.log.warn(
|
|
453
|
+
`BeaconCoordinator: FleetManager.onBeaconConnected threw: ${pErr.message}`);
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Bootstrap-flush: notify persistence bridges that a beacon
|
|
459
|
+
// just connected so they can flush locally-buffered writes the
|
|
460
|
+
// beacon doesn't have. Each bridge inspects whether the new
|
|
461
|
+
// beacon supplies its capability (QueuePersistence /
|
|
462
|
+
// ManifestStore) and fires a sweep when relevant. setImmediate
|
|
463
|
+
// for the same reason as the FleetManager hook above — the
|
|
464
|
+
// WebSocket upgrade hasn't fully wired the push channel yet.
|
|
465
|
+
this._notifyPersistenceBridgesOnConnect(tmpBeaconID);
|
|
466
|
+
|
|
355
467
|
return tmpBeacon;
|
|
356
468
|
}
|
|
357
469
|
|
|
470
|
+
/**
|
|
471
|
+
* Notify both persistence bridges that a beacon just connected.
|
|
472
|
+
* Each bridge filters by capability internally so this is safe to
|
|
473
|
+
* call for every beacon registration. Fire-and-forget; bridges
|
|
474
|
+
* log their own success/failure. Wrapped in setImmediate so flush
|
|
475
|
+
* dispatches don't preempt the WebSocket upgrade settlement.
|
|
476
|
+
*/
|
|
477
|
+
_notifyPersistenceBridgesOnConnect(pBeaconID)
|
|
478
|
+
{
|
|
479
|
+
let tmpQB = this._getService('UltravisorQueuePersistenceBridge');
|
|
480
|
+
let tmpMB = this._getService('UltravisorManifestStoreBridge');
|
|
481
|
+
setImmediate(() =>
|
|
482
|
+
{
|
|
483
|
+
if (tmpQB && typeof tmpQB.onBeaconConnected === 'function')
|
|
484
|
+
{
|
|
485
|
+
try { tmpQB.onBeaconConnected(pBeaconID); }
|
|
486
|
+
catch (pErr)
|
|
487
|
+
{
|
|
488
|
+
this.log.warn(`BeaconCoordinator: QueuePersistenceBridge.onBeaconConnected threw: ${pErr.message}`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
if (tmpMB && typeof tmpMB.onBeaconConnected === 'function')
|
|
492
|
+
{
|
|
493
|
+
try { tmpMB.onBeaconConnected(pBeaconID); }
|
|
494
|
+
catch (pErr)
|
|
495
|
+
{
|
|
496
|
+
this.log.warn(`BeaconCoordinator: ManifestStoreBridge.onBeaconConnected threw: ${pErr.message}`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
|
|
358
502
|
// ================================================================
|
|
359
503
|
// Action Catalog
|
|
360
504
|
// ================================================================
|
|
@@ -1071,31 +1215,27 @@ class UltravisorBeaconCoordinator extends libPictService
|
|
|
1071
1215
|
}
|
|
1072
1216
|
}
|
|
1073
1217
|
|
|
1074
|
-
// Persist the
|
|
1075
|
-
//
|
|
1076
|
-
//
|
|
1077
|
-
|
|
1078
|
-
|
|
1218
|
+
// Persist via the bridge (beacon when connected, in-process
|
|
1219
|
+
// QueueStore otherwise). Skip QueuePersistence's own dispatches
|
|
1220
|
+
// — recording them would create infinite recursion through the
|
|
1221
|
+
// bridge → coordinator → bridge → ... loop.
|
|
1222
|
+
let tmpBridge = this._getQueuePersistenceBridge();
|
|
1223
|
+
if (tmpBridge && !this._isMetaCapability(tmpWorkItem.Capability))
|
|
1079
1224
|
{
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
AffinityKey: tmpWorkItem.AffinityKey
|
|
1095
|
-
}
|
|
1096
|
-
});
|
|
1097
|
-
}
|
|
1098
|
-
catch (pStoreErr2) { /* best effort */ }
|
|
1225
|
+
this._persistBest(tmpBridge.upsertWorkItem(tmpWorkItem), 'queue upsert');
|
|
1226
|
+
this._persistBest(tmpBridge.appendEvent({
|
|
1227
|
+
WorkItemHash: tmpWorkItem.WorkItemHash,
|
|
1228
|
+
RunID: tmpWorkItem.RunID,
|
|
1229
|
+
EventType: 'enqueued',
|
|
1230
|
+
FromStatus: '',
|
|
1231
|
+
ToStatus: tmpWorkItem.Status,
|
|
1232
|
+
Payload: {
|
|
1233
|
+
Capability: tmpWorkItem.Capability,
|
|
1234
|
+
Action: tmpWorkItem.Action,
|
|
1235
|
+
Priority: tmpWorkItem.Priority,
|
|
1236
|
+
AffinityKey: tmpWorkItem.AffinityKey
|
|
1237
|
+
}
|
|
1238
|
+
}), 'queue enqueued event');
|
|
1099
1239
|
}
|
|
1100
1240
|
|
|
1101
1241
|
// Broadcast the enqueue event. Scheduler owns the ws envelope.
|
|
@@ -1171,6 +1311,31 @@ class UltravisorBeaconCoordinator extends libPictService
|
|
|
1171
1311
|
continue;
|
|
1172
1312
|
}
|
|
1173
1313
|
|
|
1314
|
+
// Fleet-manager dispatch filter (same logic as pollForWork's
|
|
1315
|
+
// pending-pass). System actions / unknown-model dispatches
|
|
1316
|
+
// fall through; per-(beacon, model) gating only applies when
|
|
1317
|
+
// the work item targets a known-installed model.
|
|
1318
|
+
let tmpFleetWS = this._getService('UltravisorFleetManager');
|
|
1319
|
+
if (tmpFleetWS && typeof tmpFleetWS.checkDispatchAllowed === 'function')
|
|
1320
|
+
{
|
|
1321
|
+
let tmpAllowWS;
|
|
1322
|
+
try { tmpAllowWS = tmpFleetWS.checkDispatchAllowed(tmpBeacon.BeaconID, pWorkItem); }
|
|
1323
|
+
catch (pErr)
|
|
1324
|
+
{
|
|
1325
|
+
this.log.warn(
|
|
1326
|
+
`BeaconCoordinator: WS-push fleet filter threw: ${pErr.message} — allowing`);
|
|
1327
|
+
tmpAllowWS = { Allowed: true };
|
|
1328
|
+
}
|
|
1329
|
+
if (tmpAllowWS && tmpAllowWS.Allowed === false)
|
|
1330
|
+
{
|
|
1331
|
+
this.log.info(
|
|
1332
|
+
`BeaconCoordinator: WS-push fleet filter denied beacon [${tmpBeacon.BeaconID}] `
|
|
1333
|
+
+ `for [${pWorkItem.Capability}/${pWorkItem.Action}] `
|
|
1334
|
+
+ `(model=${tmpAllowWS.MatchedModelKey}, reason=${tmpAllowWS.Reason}).`);
|
|
1335
|
+
continue;
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1174
1339
|
// Assign the work item to this beacon
|
|
1175
1340
|
pWorkItem.Status = 'Running';
|
|
1176
1341
|
pWorkItem.AssignedBeaconID = tmpBeacon.BeaconID;
|
|
@@ -1282,37 +1447,35 @@ class UltravisorBeaconCoordinator extends libPictService
|
|
|
1282
1447
|
if (!isNaN(tmpEnqMs)) tmpWorkItem.QueueWaitMs = Math.max(0, Date.now() - tmpEnqMs);
|
|
1283
1448
|
}
|
|
1284
1449
|
tmpWorkItem.AttemptNumber = (tmpWorkItem.AttemptNumber || 0) + 1;
|
|
1285
|
-
|
|
1286
|
-
|
|
1450
|
+
// Affinity-poll dispatch path. Bridge handles beacon-or-local
|
|
1451
|
+
// routing; meta-capability skip mirrors the other write sites.
|
|
1452
|
+
let tmpPollBridgeA = this._getQueuePersistenceBridge();
|
|
1453
|
+
if (tmpPollBridgeA && !this._isMetaCapability(tmpWorkItem.Capability))
|
|
1287
1454
|
{
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
Outcome: 'Dispatched'
|
|
1313
|
-
});
|
|
1314
|
-
}
|
|
1315
|
-
catch (pErr) { /* best effort */ }
|
|
1455
|
+
this._persistBest(tmpPollBridgeA.updateWorkItem(tmpWorkItem.WorkItemHash, {
|
|
1456
|
+
Status: 'Running',
|
|
1457
|
+
StartedAt: tmpWorkItem.StartedAt,
|
|
1458
|
+
DispatchedAt: tmpWorkItem.DispatchedAt,
|
|
1459
|
+
LastEventAt: tmpPollNowIso,
|
|
1460
|
+
QueueWaitMs: tmpWorkItem.QueueWaitMs || 0,
|
|
1461
|
+
AttemptNumber: tmpWorkItem.AttemptNumber
|
|
1462
|
+
}), 'queue dispatch update (affinity)');
|
|
1463
|
+
this._persistBest(tmpPollBridgeA.appendEvent({
|
|
1464
|
+
WorkItemHash: tmpWorkItem.WorkItemHash,
|
|
1465
|
+
RunID: tmpWorkItem.RunID,
|
|
1466
|
+
EventType: 'dispatched',
|
|
1467
|
+
FromStatus: 'Assigned',
|
|
1468
|
+
ToStatus: 'Running',
|
|
1469
|
+
BeaconID: pBeaconID,
|
|
1470
|
+
Payload: { QueueWaitMs: tmpWorkItem.QueueWaitMs || 0, Path: 'poll' }
|
|
1471
|
+
}), 'queue dispatched event (affinity)');
|
|
1472
|
+
this._persistBest(tmpPollBridgeA.insertAttempt({
|
|
1473
|
+
WorkItemHash: tmpWorkItem.WorkItemHash,
|
|
1474
|
+
AttemptNumber: tmpWorkItem.AttemptNumber,
|
|
1475
|
+
BeaconID: pBeaconID,
|
|
1476
|
+
DispatchedAt: tmpWorkItem.DispatchedAt,
|
|
1477
|
+
Outcome: 'Dispatched'
|
|
1478
|
+
}), 'queue insert attempt (affinity)');
|
|
1316
1479
|
}
|
|
1317
1480
|
this.log.info(`BeaconCoordinator: beacon [${pBeaconID}] picked up affinity-assigned work item [${tmpWorkItem.WorkItemHash}].`);
|
|
1318
1481
|
return this._sanitizeWorkItemForBeacon(tmpWorkItem);
|
|
@@ -1341,6 +1504,35 @@ class UltravisorBeaconCoordinator extends libPictService
|
|
|
1341
1504
|
continue;
|
|
1342
1505
|
}
|
|
1343
1506
|
|
|
1507
|
+
// Fleet-manager dispatch filter: gate on per-(beacon, model)
|
|
1508
|
+
// EnabledForDispatch state when the work item targets a
|
|
1509
|
+
// known-installed model. System actions (LabsWorkerManagement,
|
|
1510
|
+
// VideoPipeline/VP_EncodeVideo, etc.) and dispatches with no
|
|
1511
|
+
// extractable model key fall through unchanged. See
|
|
1512
|
+
// FleetManager.checkDispatchAllowed() for policy.
|
|
1513
|
+
let tmpFleetForFilter = this._getService('UltravisorFleetManager');
|
|
1514
|
+
if (tmpFleetForFilter && typeof tmpFleetForFilter.checkDispatchAllowed === 'function')
|
|
1515
|
+
{
|
|
1516
|
+
let tmpAllow;
|
|
1517
|
+
try { tmpAllow = tmpFleetForFilter.checkDispatchAllowed(pBeaconID, tmpWorkItem); }
|
|
1518
|
+
catch (pErr)
|
|
1519
|
+
{
|
|
1520
|
+
this.log.warn(
|
|
1521
|
+
`BeaconCoordinator: fleet filter threw on `
|
|
1522
|
+
+ `[${tmpWorkItem.WorkItemHash}]: ${pErr.message} — allowing`);
|
|
1523
|
+
tmpAllow = { Allowed: true };
|
|
1524
|
+
}
|
|
1525
|
+
if (tmpAllow && tmpAllow.Allowed === false)
|
|
1526
|
+
{
|
|
1527
|
+
this.log.info(
|
|
1528
|
+
`BeaconCoordinator: fleet filter denied beacon [${pBeaconID}] `
|
|
1529
|
+
+ `for [${tmpWorkItem.Capability}/${tmpWorkItem.Action}] `
|
|
1530
|
+
+ `(model=${tmpAllow.MatchedModelKey}, reason=${tmpAllow.Reason}) — `
|
|
1531
|
+
+ `keeping work item pending for an enabled beacon.`);
|
|
1532
|
+
continue;
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1344
1536
|
// Claim this work item
|
|
1345
1537
|
let tmpPollClaimIso = new Date().toISOString();
|
|
1346
1538
|
let tmpPollFromStatus = tmpWorkItem.Status;
|
|
@@ -1357,39 +1549,38 @@ class UltravisorBeaconCoordinator extends libPictService
|
|
|
1357
1549
|
if (!isNaN(tmpEnqMs2)) tmpWorkItem.QueueWaitMs = Math.max(0, Date.now() - tmpEnqMs2);
|
|
1358
1550
|
}
|
|
1359
1551
|
tmpWorkItem.AttemptNumber = (tmpWorkItem.AttemptNumber || 0) + 1;
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
}
|
|
1392
|
-
catch (pErr) { /* best effort */ }
|
|
1552
|
+
// Free-pool poll dispatch path. Same bridge + meta-capability
|
|
1553
|
+
// guard as the affinity poll above; the only difference is
|
|
1554
|
+
// the FromStatus carried into the event row.
|
|
1555
|
+
let tmpPollBridgeB = this._getQueuePersistenceBridge();
|
|
1556
|
+
if (tmpPollBridgeB && !this._isMetaCapability(tmpWorkItem.Capability))
|
|
1557
|
+
{
|
|
1558
|
+
this._persistBest(tmpPollBridgeB.updateWorkItem(tmpWorkItem.WorkItemHash, {
|
|
1559
|
+
Status: 'Running',
|
|
1560
|
+
AssignedBeaconID: pBeaconID,
|
|
1561
|
+
AssignedAt: tmpWorkItem.AssignedAt,
|
|
1562
|
+
DispatchedAt: tmpWorkItem.DispatchedAt,
|
|
1563
|
+
StartedAt: tmpWorkItem.StartedAt,
|
|
1564
|
+
LastEventAt: tmpPollClaimIso,
|
|
1565
|
+
QueueWaitMs: tmpWorkItem.QueueWaitMs || 0,
|
|
1566
|
+
AttemptNumber: tmpWorkItem.AttemptNumber
|
|
1567
|
+
}), 'queue dispatch update (poll)');
|
|
1568
|
+
this._persistBest(tmpPollBridgeB.appendEvent({
|
|
1569
|
+
WorkItemHash: tmpWorkItem.WorkItemHash,
|
|
1570
|
+
RunID: tmpWorkItem.RunID,
|
|
1571
|
+
EventType: 'dispatched',
|
|
1572
|
+
FromStatus: tmpPollFromStatus,
|
|
1573
|
+
ToStatus: 'Running',
|
|
1574
|
+
BeaconID: pBeaconID,
|
|
1575
|
+
Payload: { QueueWaitMs: tmpWorkItem.QueueWaitMs || 0, Path: 'poll' }
|
|
1576
|
+
}), 'queue dispatched event (poll)');
|
|
1577
|
+
this._persistBest(tmpPollBridgeB.insertAttempt({
|
|
1578
|
+
WorkItemHash: tmpWorkItem.WorkItemHash,
|
|
1579
|
+
AttemptNumber: tmpWorkItem.AttemptNumber,
|
|
1580
|
+
BeaconID: pBeaconID,
|
|
1581
|
+
DispatchedAt: tmpWorkItem.DispatchedAt,
|
|
1582
|
+
Outcome: 'Dispatched'
|
|
1583
|
+
}), 'queue insert attempt (poll)');
|
|
1393
1584
|
}
|
|
1394
1585
|
|
|
1395
1586
|
if (tmpBeacon.CurrentWorkItems.indexOf(tmpWorkItem.WorkItemHash) === -1)
|
|
@@ -1580,34 +1771,33 @@ class UltravisorBeaconCoordinator extends libPictService
|
|
|
1580
1771
|
tmpJournal.appendEntry('complete', { WorkItemHash: pWorkItemHash });
|
|
1581
1772
|
}
|
|
1582
1773
|
|
|
1583
|
-
// Persist
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1774
|
+
// Persist via the bridge — beacon-backed when connected,
|
|
1775
|
+
// in-process QueueStore otherwise. Skip meta-capability
|
|
1776
|
+
// dispatches to avoid the QP_*-on-QP_* recursion.
|
|
1777
|
+
let tmpCompleteBridge = this._getQueuePersistenceBridge();
|
|
1778
|
+
if (tmpCompleteBridge && !this._isMetaCapability(tmpWorkItem.Capability))
|
|
1779
|
+
{
|
|
1780
|
+
this._persistBest(tmpCompleteBridge.updateWorkItem(pWorkItemHash, {
|
|
1781
|
+
Status: 'Complete',
|
|
1782
|
+
CompletedAt: tmpWorkItem.CompletedAt,
|
|
1783
|
+
LastEventAt: tmpWorkItem.LastEventAt,
|
|
1784
|
+
Result: pResult || null
|
|
1785
|
+
}), 'queue complete update');
|
|
1786
|
+
this._persistBest(tmpCompleteBridge.appendEvent({
|
|
1787
|
+
WorkItemHash: pWorkItemHash,
|
|
1788
|
+
RunID: tmpWorkItem.RunID,
|
|
1789
|
+
EventType: 'completed',
|
|
1790
|
+
FromStatus: tmpFromStatus,
|
|
1791
|
+
ToStatus: 'Complete',
|
|
1792
|
+
BeaconID: tmpWorkItem.AssignedBeaconID,
|
|
1793
|
+
Payload: { DurationMs: tmpDurationMs }
|
|
1794
|
+
}), 'queue completed event');
|
|
1795
|
+
this._persistBest(tmpCompleteBridge.updateAttemptOutcome(
|
|
1796
|
+
pWorkItemHash, tmpWorkItem.AttemptNumber || 1, {
|
|
1605
1797
|
CompletedAt: tmpWorkItem.CompletedAt,
|
|
1606
1798
|
Outcome: 'Complete',
|
|
1607
1799
|
DurationMs: tmpDurationMs
|
|
1608
|
-
});
|
|
1609
|
-
}
|
|
1610
|
-
catch (pStoreErr) { /* best effort */ }
|
|
1800
|
+
}), 'queue attempt outcome');
|
|
1611
1801
|
}
|
|
1612
1802
|
|
|
1613
1803
|
let tmpScheduler = this._getScheduler();
|
|
@@ -1777,36 +1967,35 @@ class UltravisorBeaconCoordinator extends libPictService
|
|
|
1777
1967
|
tmpJournal.appendEntry('fail', { WorkItemHash: pWorkItemHash });
|
|
1778
1968
|
}
|
|
1779
1969
|
|
|
1780
|
-
// Persist
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1970
|
+
// Persist via the bridge — beacon-backed when connected,
|
|
1971
|
+
// in-process QueueStore otherwise. Skip meta-capability
|
|
1972
|
+
// dispatches to avoid recursion.
|
|
1973
|
+
let tmpFailBridge = this._getQueuePersistenceBridge();
|
|
1974
|
+
if (tmpFailBridge && !this._isMetaCapability(tmpWorkItem.Capability))
|
|
1975
|
+
{
|
|
1976
|
+
this._persistBest(tmpFailBridge.updateWorkItem(pWorkItemHash, {
|
|
1977
|
+
Status: 'Error',
|
|
1978
|
+
CompletedAt: tmpWorkItem.CompletedAt,
|
|
1979
|
+
LastEventAt: tmpWorkItem.LastEventAt,
|
|
1980
|
+
LastError: tmpWorkItem.LastError,
|
|
1981
|
+
Result: tmpWorkItem.Result
|
|
1982
|
+
}), 'queue fail update');
|
|
1983
|
+
this._persistBest(tmpFailBridge.appendEvent({
|
|
1984
|
+
WorkItemHash: pWorkItemHash,
|
|
1985
|
+
RunID: tmpWorkItem.RunID,
|
|
1986
|
+
EventType: 'failed',
|
|
1987
|
+
FromStatus: tmpFromStatus,
|
|
1988
|
+
ToStatus: 'Error',
|
|
1989
|
+
BeaconID: tmpWorkItem.AssignedBeaconID,
|
|
1990
|
+
Payload: { Error: tmpWorkItem.LastError, DurationMs: tmpFailDurationMs }
|
|
1991
|
+
}), 'queue failed event');
|
|
1992
|
+
this._persistBest(tmpFailBridge.updateAttemptOutcome(
|
|
1993
|
+
pWorkItemHash, tmpWorkItem.AttemptNumber || 1, {
|
|
1803
1994
|
CompletedAt: tmpWorkItem.CompletedAt,
|
|
1804
1995
|
Outcome: 'Error',
|
|
1805
1996
|
ErrorMessage: tmpWorkItem.LastError,
|
|
1806
1997
|
DurationMs: tmpFailDurationMs
|
|
1807
|
-
});
|
|
1808
|
-
}
|
|
1809
|
-
catch (pStoreErr) { /* best effort */ }
|
|
1998
|
+
}), 'queue fail attempt outcome');
|
|
1810
1999
|
}
|
|
1811
2000
|
|
|
1812
2001
|
let tmpFailScheduler = this._getScheduler();
|
|
@@ -2168,17 +2357,16 @@ class UltravisorBeaconCoordinator extends libPictService
|
|
|
2168
2357
|
// Mark the item as freshly heard-from for health scoring.
|
|
2169
2358
|
tmpWorkItem.LastEventAt = tmpWorkItem.Progress.UpdatedAt;
|
|
2170
2359
|
if (!tmpWorkItem.StartedAt) tmpWorkItem.StartedAt = tmpWorkItem.Progress.UpdatedAt;
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
}
|
|
2181
|
-
catch (pErr) { /* best effort */ }
|
|
2360
|
+
// Progress updates also land via the bridge so a beacon-backed
|
|
2361
|
+
// persistence backend sees the same heartbeat trail an in-process
|
|
2362
|
+
// store would. Skip meta-capabilities to avoid recursion.
|
|
2363
|
+
let tmpProgressBridge = this._getQueuePersistenceBridge();
|
|
2364
|
+
if (tmpProgressBridge && !this._isMetaCapability(tmpWorkItem.Capability))
|
|
2365
|
+
{
|
|
2366
|
+
this._persistBest(tmpProgressBridge.updateWorkItem(pWorkItemHash, {
|
|
2367
|
+
LastEventAt: tmpWorkItem.LastEventAt,
|
|
2368
|
+
StartedAt: tmpWorkItem.StartedAt
|
|
2369
|
+
}), 'queue progress update');
|
|
2182
2370
|
}
|
|
2183
2371
|
|
|
2184
2372
|
// Accumulate log entries
|