ultravisor 1.0.25 → 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 +62 -0
- package/source/config/Ultravisor-Default-Command-Configuration.cjs +9 -1
- package/source/persistence/UltravisorPersistenceSchema.json +240 -0
- package/source/services/Ultravisor-AuthBeaconBridge.cjs +271 -0
- package/source/services/Ultravisor-Beacon-Coordinator.cjs +242 -149
- package/source/services/Ultravisor-Beacon-Scheduler.cjs +65 -29
- package/source/services/Ultravisor-ExecutionManifest.cjs +99 -4
- package/source/services/Ultravisor-FleetManager.cjs +19 -1
- package/source/services/Ultravisor-ManifestStoreBridge.cjs +1134 -0
- package/source/services/Ultravisor-QueuePersistenceBridge.cjs +1336 -0
- package/source/web_server/Ultravisor-API-Server.cjs +951 -90
- package/webinterface/package.json +1 -0
- package/webinterface/source/Pict-Application-Ultravisor.js +57 -2
- package/webinterface/source/providers/PictRouter-Ultravisor-Configuration.json +8 -0
- package/webinterface/source/views/PictView-Ultravisor-Login.js +74 -0
- package/webinterface/source/views/PictView-Ultravisor-TopBar.js +25 -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
|
*
|
|
@@ -313,6 +369,12 @@ class UltravisorBeaconCoordinator extends libPictService
|
|
|
313
369
|
tmpReachability.onBeaconRegistered(tmpExistingBeacon.BeaconID);
|
|
314
370
|
}
|
|
315
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
|
+
|
|
316
378
|
return tmpExistingBeacon;
|
|
317
379
|
}
|
|
318
380
|
|
|
@@ -393,9 +455,50 @@ class UltravisorBeaconCoordinator extends libPictService
|
|
|
393
455
|
});
|
|
394
456
|
}
|
|
395
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
|
+
|
|
396
467
|
return tmpBeacon;
|
|
397
468
|
}
|
|
398
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
|
+
|
|
399
502
|
// ================================================================
|
|
400
503
|
// Action Catalog
|
|
401
504
|
// ================================================================
|
|
@@ -1112,31 +1215,27 @@ class UltravisorBeaconCoordinator extends libPictService
|
|
|
1112
1215
|
}
|
|
1113
1216
|
}
|
|
1114
1217
|
|
|
1115
|
-
// Persist the
|
|
1116
|
-
//
|
|
1117
|
-
//
|
|
1118
|
-
|
|
1119
|
-
|
|
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))
|
|
1120
1224
|
{
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
AffinityKey: tmpWorkItem.AffinityKey
|
|
1136
|
-
}
|
|
1137
|
-
});
|
|
1138
|
-
}
|
|
1139
|
-
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');
|
|
1140
1239
|
}
|
|
1141
1240
|
|
|
1142
1241
|
// Broadcast the enqueue event. Scheduler owns the ws envelope.
|
|
@@ -1348,37 +1447,35 @@ class UltravisorBeaconCoordinator extends libPictService
|
|
|
1348
1447
|
if (!isNaN(tmpEnqMs)) tmpWorkItem.QueueWaitMs = Math.max(0, Date.now() - tmpEnqMs);
|
|
1349
1448
|
}
|
|
1350
1449
|
tmpWorkItem.AttemptNumber = (tmpWorkItem.AttemptNumber || 0) + 1;
|
|
1351
|
-
|
|
1352
|
-
|
|
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))
|
|
1353
1454
|
{
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
Outcome: 'Dispatched'
|
|
1379
|
-
});
|
|
1380
|
-
}
|
|
1381
|
-
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)');
|
|
1382
1479
|
}
|
|
1383
1480
|
this.log.info(`BeaconCoordinator: beacon [${pBeaconID}] picked up affinity-assigned work item [${tmpWorkItem.WorkItemHash}].`);
|
|
1384
1481
|
return this._sanitizeWorkItemForBeacon(tmpWorkItem);
|
|
@@ -1452,39 +1549,38 @@ class UltravisorBeaconCoordinator extends libPictService
|
|
|
1452
1549
|
if (!isNaN(tmpEnqMs2)) tmpWorkItem.QueueWaitMs = Math.max(0, Date.now() - tmpEnqMs2);
|
|
1453
1550
|
}
|
|
1454
1551
|
tmpWorkItem.AttemptNumber = (tmpWorkItem.AttemptNumber || 0) + 1;
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
}
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
}
|
|
1487
|
-
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)');
|
|
1488
1584
|
}
|
|
1489
1585
|
|
|
1490
1586
|
if (tmpBeacon.CurrentWorkItems.indexOf(tmpWorkItem.WorkItemHash) === -1)
|
|
@@ -1675,34 +1771,33 @@ class UltravisorBeaconCoordinator extends libPictService
|
|
|
1675
1771
|
tmpJournal.appendEntry('complete', { WorkItemHash: pWorkItemHash });
|
|
1676
1772
|
}
|
|
1677
1773
|
|
|
1678
|
-
// Persist
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
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, {
|
|
1700
1797
|
CompletedAt: tmpWorkItem.CompletedAt,
|
|
1701
1798
|
Outcome: 'Complete',
|
|
1702
1799
|
DurationMs: tmpDurationMs
|
|
1703
|
-
});
|
|
1704
|
-
}
|
|
1705
|
-
catch (pStoreErr) { /* best effort */ }
|
|
1800
|
+
}), 'queue attempt outcome');
|
|
1706
1801
|
}
|
|
1707
1802
|
|
|
1708
1803
|
let tmpScheduler = this._getScheduler();
|
|
@@ -1872,36 +1967,35 @@ class UltravisorBeaconCoordinator extends libPictService
|
|
|
1872
1967
|
tmpJournal.appendEntry('fail', { WorkItemHash: pWorkItemHash });
|
|
1873
1968
|
}
|
|
1874
1969
|
|
|
1875
|
-
// Persist
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
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, {
|
|
1898
1994
|
CompletedAt: tmpWorkItem.CompletedAt,
|
|
1899
1995
|
Outcome: 'Error',
|
|
1900
1996
|
ErrorMessage: tmpWorkItem.LastError,
|
|
1901
1997
|
DurationMs: tmpFailDurationMs
|
|
1902
|
-
});
|
|
1903
|
-
}
|
|
1904
|
-
catch (pStoreErr) { /* best effort */ }
|
|
1998
|
+
}), 'queue fail attempt outcome');
|
|
1905
1999
|
}
|
|
1906
2000
|
|
|
1907
2001
|
let tmpFailScheduler = this._getScheduler();
|
|
@@ -2263,17 +2357,16 @@ class UltravisorBeaconCoordinator extends libPictService
|
|
|
2263
2357
|
// Mark the item as freshly heard-from for health scoring.
|
|
2264
2358
|
tmpWorkItem.LastEventAt = tmpWorkItem.Progress.UpdatedAt;
|
|
2265
2359
|
if (!tmpWorkItem.StartedAt) tmpWorkItem.StartedAt = tmpWorkItem.Progress.UpdatedAt;
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
}
|
|
2276
|
-
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');
|
|
2277
2370
|
}
|
|
2278
2371
|
|
|
2279
2372
|
// Accumulate log entries
|
|
@@ -82,6 +82,42 @@ class UltravisorBeaconScheduler extends libPictService
|
|
|
82
82
|
return (tmpStore && tmpStore.isEnabled()) ? tmpStore : null;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Persistence bridge — beacon-or-local fallback. Same shape the
|
|
87
|
+
* Coordinator uses; the scheduler funnels every write through it
|
|
88
|
+
* so a connected QueuePersistence beacon sees the full event log
|
|
89
|
+
* (dispatched, canceled, health updates, reorders).
|
|
90
|
+
*/
|
|
91
|
+
_getBridge()
|
|
92
|
+
{
|
|
93
|
+
let tmpMap = this.fable.servicesMap && this.fable.servicesMap.UltravisorQueuePersistenceBridge;
|
|
94
|
+
if (!tmpMap) return null;
|
|
95
|
+
return Object.values(tmpMap)[0] || null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
_isMetaCapability(pCapability)
|
|
99
|
+
{
|
|
100
|
+
// Mirror the coordinator's gate. Kept as a per-class method
|
|
101
|
+
// rather than a shared module so both classes can independently
|
|
102
|
+
// add their own meta-capabilities later.
|
|
103
|
+
return pCapability === 'QueuePersistence' || pCapability === 'ManifestStore';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
_persistBest(pPromise, pLabel)
|
|
107
|
+
{
|
|
108
|
+
if (!pPromise || typeof pPromise.then !== 'function') return;
|
|
109
|
+
pPromise.then((pResult) =>
|
|
110
|
+
{
|
|
111
|
+
if (pResult && pResult.Success === false && pResult.Reason)
|
|
112
|
+
{
|
|
113
|
+
this.log.warn(`BeaconScheduler: ${pLabel}: ${pResult.Reason}`);
|
|
114
|
+
}
|
|
115
|
+
}).catch((pErr) =>
|
|
116
|
+
{
|
|
117
|
+
this.log.warn(`BeaconScheduler: ${pLabel} threw: ${(pErr && pErr.message) || pErr}`);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
85
121
|
_getDefaults()
|
|
86
122
|
{
|
|
87
123
|
let tmpMap = this.fable.servicesMap && this.fable.servicesMap.UltravisorBeaconActionDefaults;
|
|
@@ -256,10 +292,10 @@ class UltravisorBeaconScheduler extends libPictService
|
|
|
256
292
|
pBeacon.CurrentWorkItems.push(pItem.WorkItemHash);
|
|
257
293
|
}
|
|
258
294
|
|
|
259
|
-
let
|
|
260
|
-
if (
|
|
295
|
+
let tmpDispatchBridge = this._getBridge();
|
|
296
|
+
if (tmpDispatchBridge && !this._isMetaCapability(pItem.Capability))
|
|
261
297
|
{
|
|
262
|
-
|
|
298
|
+
this._persistBest(tmpDispatchBridge.updateWorkItem(pItem.WorkItemHash, {
|
|
263
299
|
Status: 'Dispatched',
|
|
264
300
|
AssignedBeaconID: pBeacon.BeaconID,
|
|
265
301
|
AssignedAt: pItem.AssignedAt,
|
|
@@ -268,8 +304,8 @@ class UltravisorBeaconScheduler extends libPictService
|
|
|
268
304
|
AttemptNumber: pItem.AttemptNumber,
|
|
269
305
|
LastEventAt: pItem.LastEventAt,
|
|
270
306
|
Settings: pItem.Settings
|
|
271
|
-
});
|
|
272
|
-
|
|
307
|
+
}), 'dispatch update');
|
|
308
|
+
this._persistBest(tmpDispatchBridge.appendEvent({
|
|
273
309
|
WorkItemHash: pItem.WorkItemHash,
|
|
274
310
|
RunID: pItem.RunID,
|
|
275
311
|
EventType: 'dispatched',
|
|
@@ -277,14 +313,14 @@ class UltravisorBeaconScheduler extends libPictService
|
|
|
277
313
|
ToStatus: 'Dispatched',
|
|
278
314
|
BeaconID: pBeacon.BeaconID,
|
|
279
315
|
Payload: { QueueWaitMs: tmpQueueWaitMs }
|
|
280
|
-
});
|
|
281
|
-
|
|
316
|
+
}), 'dispatched event');
|
|
317
|
+
this._persistBest(tmpDispatchBridge.insertAttempt({
|
|
282
318
|
WorkItemHash: pItem.WorkItemHash,
|
|
283
319
|
AttemptNumber: pItem.AttemptNumber,
|
|
284
320
|
BeaconID: pBeacon.BeaconID,
|
|
285
321
|
DispatchedAt: pItem.DispatchedAt,
|
|
286
322
|
Outcome: 'Dispatched'
|
|
287
|
-
});
|
|
323
|
+
}), 'dispatch attempt');
|
|
288
324
|
}
|
|
289
325
|
|
|
290
326
|
this.log.info(`BeaconScheduler: dispatched [${pItem.WorkItemHash}] to beacon [${pBeacon.BeaconID}] (queue_wait=${tmpQueueWaitMs}ms, attempt=${pItem.AttemptNumber}).`);
|
|
@@ -327,23 +363,23 @@ class UltravisorBeaconScheduler extends libPictService
|
|
|
327
363
|
pItem.CancelReason = pItem.CancelReason || pReason || '';
|
|
328
364
|
pItem.LastEventAt = tmpNowIso;
|
|
329
365
|
|
|
330
|
-
let
|
|
331
|
-
if (
|
|
366
|
+
let tmpCancelBridge = this._getBridge();
|
|
367
|
+
if (tmpCancelBridge && !this._isMetaCapability(pItem.Capability))
|
|
332
368
|
{
|
|
333
|
-
|
|
369
|
+
this._persistBest(tmpCancelBridge.updateWorkItem(pItem.WorkItemHash, {
|
|
334
370
|
Status: 'Canceled',
|
|
335
371
|
CanceledAt: pItem.CanceledAt,
|
|
336
372
|
CancelReason: pItem.CancelReason,
|
|
337
373
|
LastEventAt: tmpNowIso
|
|
338
|
-
});
|
|
339
|
-
|
|
374
|
+
}), 'cancel update');
|
|
375
|
+
this._persistBest(tmpCancelBridge.appendEvent({
|
|
340
376
|
WorkItemHash: pItem.WorkItemHash,
|
|
341
377
|
RunID: pItem.RunID,
|
|
342
378
|
EventType: 'canceled',
|
|
343
379
|
FromStatus: tmpFromStatus,
|
|
344
380
|
ToStatus: 'Canceled',
|
|
345
381
|
Payload: { Reason: pItem.CancelReason }
|
|
346
|
-
});
|
|
382
|
+
}), 'canceled event');
|
|
347
383
|
}
|
|
348
384
|
|
|
349
385
|
this._broadcast('queue.canceled', {
|
|
@@ -489,15 +525,15 @@ class UltravisorBeaconScheduler extends libPictService
|
|
|
489
525
|
pItem.HealthReason = tmpHealth.Reason;
|
|
490
526
|
pItem.HealthComputedAt = tmpNowIso;
|
|
491
527
|
|
|
492
|
-
let
|
|
493
|
-
if (
|
|
528
|
+
let tmpHealthBridge = this._getBridge();
|
|
529
|
+
if (tmpHealthBridge && !this._isMetaCapability(pItem.Capability))
|
|
494
530
|
{
|
|
495
|
-
|
|
531
|
+
this._persistBest(tmpHealthBridge.updateWorkItem(pItem.WorkItemHash, {
|
|
496
532
|
Health: tmpHealth.Score,
|
|
497
533
|
HealthLabel: tmpHealth.Label,
|
|
498
534
|
HealthReason: tmpHealth.Reason,
|
|
499
535
|
HealthComputedAt: tmpNowIso
|
|
500
|
-
});
|
|
536
|
+
}), 'health update');
|
|
501
537
|
}
|
|
502
538
|
|
|
503
539
|
// Only broadcast on label change or meaningful score delta.
|
|
@@ -642,21 +678,21 @@ class UltravisorBeaconScheduler extends libPictService
|
|
|
642
678
|
{
|
|
643
679
|
tmpItem.CancelRequested = true;
|
|
644
680
|
tmpItem.CancelReason = pReason || 'cancel requested';
|
|
645
|
-
let
|
|
646
|
-
if (
|
|
681
|
+
let tmpCancelReqBridge = this._getBridge();
|
|
682
|
+
if (tmpCancelReqBridge && !this._isMetaCapability(tmpItem.Capability))
|
|
647
683
|
{
|
|
648
|
-
|
|
684
|
+
this._persistBest(tmpCancelReqBridge.updateWorkItem(pWorkItemHash, {
|
|
649
685
|
CancelRequested: true,
|
|
650
686
|
CancelReason: tmpItem.CancelReason
|
|
651
|
-
});
|
|
652
|
-
|
|
687
|
+
}), 'cancel-requested update');
|
|
688
|
+
this._persistBest(tmpCancelReqBridge.appendEvent({
|
|
653
689
|
WorkItemHash: pWorkItemHash,
|
|
654
690
|
RunID: tmpItem.RunID,
|
|
655
691
|
EventType: 'cancel_requested',
|
|
656
692
|
FromStatus: tmpItem.Status,
|
|
657
693
|
ToStatus: tmpItem.Status,
|
|
658
694
|
Payload: { Reason: tmpItem.CancelReason }
|
|
659
|
-
});
|
|
695
|
+
}), 'cancel-requested event');
|
|
660
696
|
}
|
|
661
697
|
this._broadcast('queue.cancel_requested', {
|
|
662
698
|
WorkItemHash: pWorkItemHash,
|
|
@@ -728,18 +764,18 @@ class UltravisorBeaconScheduler extends libPictService
|
|
|
728
764
|
: ((tmpNeighbor.Priority || 0) - 1);
|
|
729
765
|
tmpItem.Priority = tmpNewPriority;
|
|
730
766
|
|
|
731
|
-
let
|
|
732
|
-
if (
|
|
767
|
+
let tmpReorderBridge = this._getBridge();
|
|
768
|
+
if (tmpReorderBridge && !this._isMetaCapability(tmpItem.Capability))
|
|
733
769
|
{
|
|
734
|
-
|
|
735
|
-
|
|
770
|
+
this._persistBest(tmpReorderBridge.updateWorkItem(pWorkItemHash, { Priority: tmpNewPriority }), 'reorder update');
|
|
771
|
+
this._persistBest(tmpReorderBridge.appendEvent({
|
|
736
772
|
WorkItemHash: pWorkItemHash,
|
|
737
773
|
RunID: tmpItem.RunID,
|
|
738
774
|
EventType: 'reordered',
|
|
739
775
|
FromStatus: tmpItem.Status,
|
|
740
776
|
ToStatus: tmpItem.Status,
|
|
741
777
|
Payload: { Direction: pDirection, OldPriority: tmpOldPriority, NewPriority: tmpNewPriority }
|
|
742
|
-
});
|
|
778
|
+
}), 'reordered event');
|
|
743
779
|
}
|
|
744
780
|
|
|
745
781
|
// Two broadcasts: a targeted reorder event so the UI can update
|