ultravisor 1.0.3 → 1.0.8
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/.claude/launch.json +11 -0
- package/.claude/ultravisor-dev-config.json +3 -0
- package/card-audit.json +1545 -0
- package/docs/_sidebar.md +17 -0
- package/docs/css/docuserve.css +73 -0
- package/docs/features/beacon-authentication.md +209 -0
- package/docs/features/beacon-providers.md +1299 -0
- package/docs/features/beacons.md +530 -0
- package/docs/features/capabilities.md +193 -0
- package/docs/features/llm-model-setup.md +505 -0
- package/docs/features/llm.md +262 -0
- package/docs/features/tasks-content-system.md +110 -0
- package/docs/features/tasks-data-transform.md +233 -0
- package/docs/features/tasks-extension.md +36 -0
- package/docs/features/tasks-file-system.md +203 -0
- package/docs/features/tasks-flow-control.md +125 -0
- package/docs/features/tasks-http-client.md +119 -0
- package/docs/features/tasks-llm.md +118 -0
- package/docs/features/tasks-meadow-api.md +163 -0
- package/docs/features/tasks-user-interaction.md +49 -0
- package/docs/features/tasks.md +14 -0
- package/docs/index.html +1 -1
- package/docs/retold-catalog.json +1 -1
- package/docs/retold-keyword-index.json +17374 -5
- package/operation-library/api-data-pipeline.json +143 -0
- package/operation-library/beacon-remote-command.json +72 -0
- package/operation-library/conditional-file-backup.json +143 -0
- package/operation-library/config-processor.json +136 -0
- package/operation-library/csv-data-analysis.json +228 -0
- package/operation-library/csv-to-json.json +160 -0
- package/operation-library/directory-inventory.json +128 -0
- package/operation-library/expression-calculator.json +114 -0
- package/operation-library/file-search-replace.json +175 -0
- package/operation-library/git-status-report.json +368 -0
- package/operation-library/health-check-ping.json +329 -0
- package/operation-library/json-config-merge.json +313 -0
- package/operation-library/llm-embedding-tool-use.json +314 -0
- package/operation-library/llm-multi-turn-analysis.json +322 -0
- package/operation-library/llm-summarize.json +264 -0
- package/operation-library/log-line-counter.json +185 -0
- package/operation-library/meadow-crud-lifecycle.json +264 -0
- package/operation-library/npm-project-validator.json +193 -0
- package/operation-library/rest-api-orchestrator.json +218 -0
- package/operation-library/shell-system-info.json +137 -0
- package/operation-library/simple-echo.json +95 -0
- package/operation-library/template-processor.json +299 -0
- package/operation-library/text-sanitizer.json +176 -0
- package/package.json +12 -8
- package/source/Ultravisor.cjs +30 -3
- package/source/beacon/Ultravisor-Beacon-CLI.cjs +143 -0
- package/source/beacon/Ultravisor-Beacon-CapabilityProvider.cjs +129 -0
- package/source/beacon/Ultravisor-Beacon-Client.cjs +568 -0
- package/source/beacon/Ultravisor-Beacon-Executor.cjs +500 -0
- package/source/beacon/Ultravisor-Beacon-ProviderRegistry.cjs +330 -0
- package/source/beacon/providers/Ultravisor-Beacon-Provider-FileSystem.cjs +331 -0
- package/source/beacon/providers/Ultravisor-Beacon-Provider-LLM.cjs +966 -0
- package/source/beacon/providers/Ultravisor-Beacon-Provider-Shell.cjs +95 -0
- package/source/cli/Ultravisor-CLIProgram.cjs +44 -22
- package/source/cli/commands/Ultravisor-Command-SingleOperation.cjs +29 -18
- package/source/cli/commands/Ultravisor-Command-SingleTask.cjs +62 -19
- package/source/cli/commands/Ultravisor-Command-UpdateTask.cjs +27 -15
- package/source/config/Ultravisor-Default-Command-Configuration.cjs +12 -3
- package/source/services/Ultravisor-Beacon-Coordinator.cjs +966 -0
- package/source/services/Ultravisor-ExecutionEngine.cjs +1093 -0
- package/source/services/Ultravisor-ExecutionManifest.cjs +629 -0
- package/source/services/Ultravisor-Hypervisor-State.cjs +270 -97
- package/source/services/Ultravisor-Hypervisor.cjs +185 -75
- package/source/services/Ultravisor-Schedule-Persistence-Base.cjs +45 -0
- package/source/services/Ultravisor-StateManager.cjs +253 -0
- package/source/services/Ultravisor-TaskTypeRegistry.cjs +213 -0
- package/source/services/persistence/Ultravisor-Schedule-Persistence-JSONFile.cjs +140 -0
- package/source/services/tasks/Ultravisor-BuiltIn-TaskConfigs.cjs +19 -0
- package/source/services/tasks/Ultravisor-TaskHelper-BeaconDispatch.cjs +107 -0
- package/source/services/tasks/Ultravisor-TaskType-Base.cjs +125 -0
- package/source/services/tasks/content-system/Ultravisor-TaskConfigs-ContentSystem.cjs +90 -0
- package/source/services/tasks/content-system/definitions/content-create-folder.json +24 -0
- package/source/services/tasks/content-system/definitions/content-list-files.json +27 -0
- package/source/services/tasks/content-system/definitions/content-read-file.json +26 -0
- package/source/services/tasks/content-system/definitions/content-save-file.json +26 -0
- package/source/services/tasks/data-transform/Ultravisor-TaskConfigs-DataTransform.cjs +577 -0
- package/source/services/tasks/data-transform/Ultravisor-TaskType-ReplaceString.cjs +80 -0
- package/source/services/tasks/data-transform/Ultravisor-TaskType-SetValues.cjs +74 -0
- package/source/services/tasks/data-transform/Ultravisor-TaskType-StringAppender.cjs +90 -0
- package/source/services/tasks/data-transform/definitions/comprehension-intersect.json +24 -0
- package/source/services/tasks/data-transform/definitions/csv-transform.json +24 -0
- package/source/services/tasks/data-transform/definitions/expression-solver.json +20 -0
- package/source/services/tasks/data-transform/definitions/histogram.json +23 -0
- package/source/services/tasks/data-transform/definitions/parse-csv.json +27 -0
- package/source/services/tasks/data-transform/definitions/replace-string.json +27 -0
- package/source/services/tasks/data-transform/definitions/set-values.json +17 -0
- package/source/services/tasks/data-transform/definitions/string-appender.json +22 -0
- package/source/services/tasks/data-transform/definitions/template-string.json +20 -0
- package/source/services/tasks/extension/Ultravisor-TaskConfigs-Extension.cjs +87 -0
- package/source/services/tasks/extension/definitions/beacon-dispatch.json +32 -0
- package/source/services/tasks/file-system/Ultravisor-TaskConfigs-FileSystem.cjs +509 -0
- package/source/services/tasks/file-system/Ultravisor-TaskType-ReadFile.cjs +96 -0
- package/source/services/tasks/file-system/Ultravisor-TaskType-ReadFileBuffered.cjs +123 -0
- package/source/services/tasks/file-system/Ultravisor-TaskType-WriteFile.cjs +106 -0
- package/source/services/tasks/file-system/definitions/copy-file.json +27 -0
- package/source/services/tasks/file-system/definitions/list-files.json +27 -0
- package/source/services/tasks/file-system/definitions/read-file-buffered.json +31 -0
- package/source/services/tasks/file-system/definitions/read-file.json +26 -0
- package/source/services/tasks/file-system/definitions/read-json.json +23 -0
- package/source/services/tasks/file-system/definitions/write-file.json +29 -0
- package/source/services/tasks/file-system/definitions/write-json.json +30 -0
- package/source/services/tasks/flow-control/Ultravisor-TaskConfigs-FlowControl.cjs +353 -0
- package/source/services/tasks/flow-control/Ultravisor-TaskType-IfConditional.cjs +125 -0
- package/source/services/tasks/flow-control/Ultravisor-TaskType-LaunchOperation.cjs +186 -0
- package/source/services/tasks/flow-control/Ultravisor-TaskType-SplitExecute.cjs +164 -0
- package/source/services/tasks/flow-control/definitions/if-conditional.json +25 -0
- package/source/services/tasks/flow-control/definitions/launch-operation.json +27 -0
- package/source/services/tasks/flow-control/definitions/split-execute.json +32 -0
- package/source/services/tasks/http-client/Ultravisor-TaskConfigs-HttpClient.cjs +281 -0
- package/source/services/tasks/http-client/definitions/get-json.json +26 -0
- package/source/services/tasks/http-client/definitions/get-text.json +26 -0
- package/source/services/tasks/http-client/definitions/rest-request.json +32 -0
- package/source/services/tasks/http-client/definitions/send-json.json +28 -0
- package/source/services/tasks/llm/Ultravisor-TaskConfigs-LLM.cjs +605 -0
- package/source/services/tasks/llm/definitions/llm-chat-completion.json +46 -0
- package/source/services/tasks/llm/definitions/llm-embedding.json +31 -0
- package/source/services/tasks/llm/definitions/llm-tool-use.json +42 -0
- package/source/services/tasks/meadow-api/Ultravisor-TaskConfigs-MeadowApi.cjs +341 -0
- package/source/services/tasks/meadow-api/definitions/meadow-count.json +26 -0
- package/source/services/tasks/meadow-api/definitions/meadow-create.json +26 -0
- package/source/services/tasks/meadow-api/definitions/meadow-delete.json +23 -0
- package/source/services/tasks/meadow-api/definitions/meadow-read.json +26 -0
- package/source/services/tasks/meadow-api/definitions/meadow-reads.json +29 -0
- package/source/services/tasks/meadow-api/definitions/meadow-update.json +26 -0
- package/source/services/tasks/shell/Ultravisor-TaskConfigs-Shell.cjs +63 -0
- package/source/services/tasks/shell/definitions/command.json +29 -0
- package/source/services/tasks/user-interaction/Ultravisor-TaskConfigs-UserInteraction.cjs +64 -0
- package/source/services/tasks/user-interaction/Ultravisor-TaskType-ErrorMessage.cjs +55 -0
- package/source/services/tasks/user-interaction/Ultravisor-TaskType-ValueInput.cjs +47 -0
- package/source/services/tasks/user-interaction/definitions/error-message.json +18 -0
- package/source/services/tasks/user-interaction/definitions/value-input.json +23 -0
- package/source/web_server/Ultravisor-API-Server.cjs +849 -124
- package/test/Ultravisor_browser_tests.js +2226 -0
- package/test/Ultravisor_operation_library_tests.js +894 -0
- package/test/Ultravisor_tests.js +4946 -5044
- package/test/workflows/test-content-concatenate-all.json +339 -0
- package/test/workflows/test-content-full-pipeline.json +159 -0
- package/test/workflows/test-content-list-files.json +76 -0
- package/test/workflows/test-content-save-and-read.json +106 -0
- package/webinterface/css/ultravisor-themes.css +668 -0
- package/webinterface/css/ultravisor.css +37 -14
- package/webinterface/docs/card-help/beacon-dispatch.md +30 -0
- package/webinterface/docs/card-help/command.md +27 -0
- package/webinterface/docs/card-help/comprehension-intersect.md +24 -0
- package/webinterface/docs/card-help/content-create-folder.md +22 -0
- package/webinterface/docs/card-help/content-list-files.md +25 -0
- package/webinterface/docs/card-help/content-read-file.md +24 -0
- package/webinterface/docs/card-help/content-save-file.md +24 -0
- package/webinterface/docs/card-help/copy-file.md +25 -0
- package/webinterface/docs/card-help/csv-transform.md +24 -0
- package/webinterface/docs/card-help/end.md +11 -0
- package/webinterface/docs/card-help/error-message.md +16 -0
- package/webinterface/docs/card-help/expression-solver.md +21 -0
- package/webinterface/docs/card-help/get-json.md +24 -0
- package/webinterface/docs/card-help/get-text.md +24 -0
- package/webinterface/docs/card-help/histogram.md +23 -0
- package/webinterface/docs/card-help/if-conditional.md +23 -0
- package/webinterface/docs/card-help/launch-operation.md +25 -0
- package/webinterface/docs/card-help/list-files.md +25 -0
- package/webinterface/docs/card-help/llm-chat-completion.md +43 -0
- package/webinterface/docs/card-help/llm-embedding.md +24 -0
- package/webinterface/docs/card-help/llm-tool-use.md +39 -0
- package/webinterface/docs/card-help/meadow-count.md +24 -0
- package/webinterface/docs/card-help/meadow-create.md +24 -0
- package/webinterface/docs/card-help/meadow-delete.md +19 -0
- package/webinterface/docs/card-help/meadow-read.md +24 -0
- package/webinterface/docs/card-help/meadow-reads.md +27 -0
- package/webinterface/docs/card-help/meadow-update.md +24 -0
- package/webinterface/docs/card-help/parse-csv.md +27 -0
- package/webinterface/docs/card-help/read-file-buffered.md +29 -0
- package/webinterface/docs/card-help/read-file.md +24 -0
- package/webinterface/docs/card-help/read-json.md +21 -0
- package/webinterface/docs/card-help/replace-string.md +25 -0
- package/webinterface/docs/card-help/rest-request.md +30 -0
- package/webinterface/docs/card-help/send-json.md +26 -0
- package/webinterface/docs/card-help/set-values.md +16 -0
- package/webinterface/docs/card-help/split-execute.md +35 -0
- package/webinterface/docs/card-help/start.md +11 -0
- package/webinterface/docs/card-help/string-appender.md +22 -0
- package/webinterface/docs/card-help/template-string.md +21 -0
- package/webinterface/docs/card-help/value-input.md +24 -0
- package/webinterface/docs/card-help/write-file.md +27 -0
- package/webinterface/docs/card-help/write-json.md +28 -0
- package/webinterface/html/index.html +5 -0
- package/webinterface/package.json +13 -4
- package/webinterface/source/Pict-Application-Ultravisor.js +564 -70
- package/webinterface/source/Ultravisor-TimingUtils.js +99 -0
- package/webinterface/source/card-help-content.js +46 -0
- package/webinterface/source/cards/Ultravisor-BuiltIn-CardConfigs.js +174 -0
- package/webinterface/source/cards/Ultravisor-CardConfigGenerator.js +431 -0
- package/webinterface/source/data/ExampleFlow-CSVPipeline.js +231 -0
- package/webinterface/source/data/ExampleFlow-FileProcessor.js +315 -0
- package/webinterface/source/data/ExampleFlow-MeadowPipeline.js +328 -0
- package/webinterface/source/panels/Ultravisor-CardSettingsPanel.js +902 -0
- package/webinterface/source/providers/PictRouter-Ultravisor-Configuration.json +20 -8
- package/webinterface/source/views/PictView-Ultravisor-BeaconList.js +817 -0
- package/webinterface/source/views/PictView-Ultravisor-BottomBar.js +5 -5
- package/webinterface/source/views/PictView-Ultravisor-Dashboard.js +25 -25
- package/webinterface/source/views/PictView-Ultravisor-Documentation.js +856 -0
- package/webinterface/source/views/PictView-Ultravisor-FlowEditor.js +777 -0
- package/webinterface/source/views/PictView-Ultravisor-ManifestList.js +321 -58
- package/webinterface/source/views/PictView-Ultravisor-OperationEdit.js +36 -91
- package/webinterface/source/views/PictView-Ultravisor-OperationList.js +388 -16
- package/webinterface/source/views/PictView-Ultravisor-PendingInput.js +314 -0
- package/webinterface/source/views/PictView-Ultravisor-Schedule.js +521 -65
- package/webinterface/source/views/PictView-Ultravisor-TimingView.js +333 -71
- package/webinterface/source/views/PictView-Ultravisor-TopBar.js +257 -21
- package/webinterface/theme-sampler.html +645 -0
- package/debug/Harness.js +0 -6
- package/source/services/Ultravisor-Operation-Manifest.cjs +0 -160
- package/source/services/Ultravisor-Operation.cjs +0 -200
- package/source/services/Ultravisor-Task.cjs +0 -349
- package/source/services/events/Ultravisor-Hypervisor-Event-Solver.cjs +0 -11
- package/source/services/tasks/Ultravisor-Task-Base.cjs +0 -264
- package/source/services/tasks/Ultravisor-Task-CollectValues.cjs +0 -188
- package/source/services/tasks/Ultravisor-Task-Command.cjs +0 -65
- package/source/services/tasks/Ultravisor-Task-CommandEach.cjs +0 -190
- package/source/services/tasks/Ultravisor-Task-Conditional.cjs +0 -104
- package/source/services/tasks/Ultravisor-Task-DateWindow.cjs +0 -72
- package/source/services/tasks/Ultravisor-Task-GeneratePagedOperation.cjs +0 -336
- package/source/services/tasks/Ultravisor-Task-LaunchOperation.cjs +0 -143
- package/source/services/tasks/Ultravisor-Task-LaunchTask.cjs +0 -146
- package/source/services/tasks/Ultravisor-Task-LineMatch.cjs +0 -158
- package/source/services/tasks/Ultravisor-Task-Request.cjs +0 -56
- package/source/services/tasks/Ultravisor-Task-Solver.cjs +0 -89
- package/source/services/tasks/Ultravisor-Task-TemplateString.cjs +0 -93
- package/source/services/tasks/rest/Ultravisor-Task-GetBinary.cjs +0 -127
- package/source/services/tasks/rest/Ultravisor-Task-GetJSON.cjs +0 -119
- package/source/services/tasks/rest/Ultravisor-Task-GetText.cjs +0 -109
- package/source/services/tasks/rest/Ultravisor-Task-GetXML.cjs +0 -112
- package/source/services/tasks/rest/Ultravisor-Task-RestRequest.cjs +0 -499
- package/source/services/tasks/rest/Ultravisor-Task-SendJSON.cjs +0 -150
- package/source/services/tasks/stagingfiles/Ultravisor-Task-CopyFile.cjs +0 -110
- package/source/services/tasks/stagingfiles/Ultravisor-Task-ListFiles.cjs +0 -89
- package/source/services/tasks/stagingfiles/Ultravisor-Task-ReadBinary.cjs +0 -87
- package/source/services/tasks/stagingfiles/Ultravisor-Task-ReadJSON.cjs +0 -67
- package/source/services/tasks/stagingfiles/Ultravisor-Task-ReadText.cjs +0 -66
- package/source/services/tasks/stagingfiles/Ultravisor-Task-ReadXML.cjs +0 -69
- package/source/services/tasks/stagingfiles/Ultravisor-Task-WriteBinary.cjs +0 -95
- package/source/services/tasks/stagingfiles/Ultravisor-Task-WriteJSON.cjs +0 -96
- package/source/services/tasks/stagingfiles/Ultravisor-Task-WriteText.cjs +0 -99
- package/source/services/tasks/stagingfiles/Ultravisor-Task-WriteXML.cjs +0 -102
- package/webinterface/source/views/PictView-Ultravisor-TaskEdit.js +0 -220
- package/webinterface/source/views/PictView-Ultravisor-TaskList.js +0 -248
|
@@ -0,0 +1,966 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ultravisor Beacon Coordinator
|
|
3
|
+
*
|
|
4
|
+
* Server-side service that manages Beacon worker nodes, the work queue,
|
|
5
|
+
* and affinity bindings for distributed task execution.
|
|
6
|
+
*
|
|
7
|
+
* The coordinator is transport-agnostic — its internal API (enqueueWorkItem,
|
|
8
|
+
* completeWorkItem, failWorkItem) can be driven by any signaling channel
|
|
9
|
+
* (HTTP polling, WebSocket, MQTT, email/SMS webhooks, etc.).
|
|
10
|
+
*
|
|
11
|
+
* @module Ultravisor-Beacon-Coordinator
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const libPictService = require('pict-serviceproviderbase');
|
|
15
|
+
|
|
16
|
+
class UltravisorBeaconCoordinator extends libPictService
|
|
17
|
+
{
|
|
18
|
+
constructor(pPict, pOptions, pServiceHash)
|
|
19
|
+
{
|
|
20
|
+
super(pPict, pOptions, pServiceHash);
|
|
21
|
+
|
|
22
|
+
// --- Registered Beacons ---
|
|
23
|
+
// Map of BeaconID → Beacon record
|
|
24
|
+
this._Beacons = {};
|
|
25
|
+
|
|
26
|
+
// --- Work Queue ---
|
|
27
|
+
// Map of WorkItemHash → Work item record
|
|
28
|
+
this._WorkQueue = {};
|
|
29
|
+
|
|
30
|
+
// --- Affinity Bindings ---
|
|
31
|
+
// Map of AffinityKey → Binding record
|
|
32
|
+
this._AffinityBindings = {};
|
|
33
|
+
|
|
34
|
+
// --- Timeout Checker ---
|
|
35
|
+
this._TimeoutInterval = null;
|
|
36
|
+
this._TimeoutCheckIntervalMs = 10000; // Check every 10s
|
|
37
|
+
|
|
38
|
+
// --- Direct Dispatch Callbacks ---
|
|
39
|
+
// Map of WorkItemHash → { callback, timeoutHandle }
|
|
40
|
+
// Used by dispatchAndWait() for synchronous HTTP dispatch
|
|
41
|
+
this._DirectDispatchCallbacks = {};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ====================================================================
|
|
45
|
+
// Lifecycle
|
|
46
|
+
// ====================================================================
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Start the timeout checker interval.
|
|
50
|
+
*/
|
|
51
|
+
startTimeoutChecker()
|
|
52
|
+
{
|
|
53
|
+
if (this._TimeoutInterval)
|
|
54
|
+
{
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this._TimeoutInterval = setInterval(
|
|
59
|
+
() =>
|
|
60
|
+
{
|
|
61
|
+
this._checkTimeouts();
|
|
62
|
+
},
|
|
63
|
+
this._TimeoutCheckIntervalMs);
|
|
64
|
+
|
|
65
|
+
this.log.info('BeaconCoordinator: timeout checker started.');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Stop the timeout checker interval.
|
|
70
|
+
*/
|
|
71
|
+
stopTimeoutChecker()
|
|
72
|
+
{
|
|
73
|
+
if (this._TimeoutInterval)
|
|
74
|
+
{
|
|
75
|
+
clearInterval(this._TimeoutInterval);
|
|
76
|
+
this._TimeoutInterval = null;
|
|
77
|
+
this.log.info('BeaconCoordinator: timeout checker stopped.');
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ====================================================================
|
|
82
|
+
// Beacon Registration
|
|
83
|
+
// ====================================================================
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Register a new Beacon worker.
|
|
87
|
+
*
|
|
88
|
+
* If a beacon with the same Name already exists and is Offline,
|
|
89
|
+
* reclaim it (session-aware reconnection) instead of creating a
|
|
90
|
+
* new record.
|
|
91
|
+
*
|
|
92
|
+
* @param {object} pBeaconInfo - { Name, Capabilities, MaxConcurrent?, Tags? }
|
|
93
|
+
* @param {string} pSessionID - Optional session identifier
|
|
94
|
+
* @returns {object} The created or reclaimed Beacon record
|
|
95
|
+
*/
|
|
96
|
+
registerBeacon(pBeaconInfo, pSessionID)
|
|
97
|
+
{
|
|
98
|
+
let tmpName = pBeaconInfo.Name || 'unnamed';
|
|
99
|
+
|
|
100
|
+
// Check for an existing offline beacon with the same name to reclaim
|
|
101
|
+
let tmpExistingBeacon = this.findBeaconByName(tmpName);
|
|
102
|
+
if (tmpExistingBeacon && tmpExistingBeacon.Status === 'Offline')
|
|
103
|
+
{
|
|
104
|
+
tmpExistingBeacon.SessionID = pSessionID || null;
|
|
105
|
+
tmpExistingBeacon.LastHeartbeat = new Date().toISOString();
|
|
106
|
+
tmpExistingBeacon.Status = 'Online';
|
|
107
|
+
|
|
108
|
+
if (Array.isArray(pBeaconInfo.Capabilities))
|
|
109
|
+
{
|
|
110
|
+
tmpExistingBeacon.Capabilities = pBeaconInfo.Capabilities;
|
|
111
|
+
}
|
|
112
|
+
if (pBeaconInfo.MaxConcurrent)
|
|
113
|
+
{
|
|
114
|
+
tmpExistingBeacon.MaxConcurrent = pBeaconInfo.MaxConcurrent;
|
|
115
|
+
}
|
|
116
|
+
if (pBeaconInfo.Tags)
|
|
117
|
+
{
|
|
118
|
+
tmpExistingBeacon.Tags = pBeaconInfo.Tags;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
this.log.info(`BeaconCoordinator: reconnected beacon [${tmpExistingBeacon.BeaconID}] "${tmpName}" with session [${tmpExistingBeacon.SessionID}].`);
|
|
122
|
+
return tmpExistingBeacon;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let tmpTimestamp = Date.now();
|
|
126
|
+
let tmpBeaconID = `bcn-${tmpName.toLowerCase().replace(/[^a-z0-9]/g, '-')}-${tmpTimestamp}`;
|
|
127
|
+
|
|
128
|
+
let tmpBeacon = {
|
|
129
|
+
BeaconID: tmpBeaconID,
|
|
130
|
+
Name: tmpName,
|
|
131
|
+
SessionID: pSessionID || null,
|
|
132
|
+
Capabilities: Array.isArray(pBeaconInfo.Capabilities) ? pBeaconInfo.Capabilities : [],
|
|
133
|
+
Status: 'Online',
|
|
134
|
+
LastHeartbeat: new Date().toISOString(),
|
|
135
|
+
CurrentWorkItems: [],
|
|
136
|
+
MaxConcurrent: pBeaconInfo.MaxConcurrent || 1,
|
|
137
|
+
Tags: pBeaconInfo.Tags || {},
|
|
138
|
+
RegisteredAt: new Date().toISOString()
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
this._Beacons[tmpBeaconID] = tmpBeacon;
|
|
142
|
+
this.log.info(`BeaconCoordinator: registered beacon [${tmpBeaconID}] "${tmpName}" with capabilities [${tmpBeacon.Capabilities.join(', ')}].`);
|
|
143
|
+
|
|
144
|
+
return tmpBeacon;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Find a beacon by its Name property.
|
|
149
|
+
*
|
|
150
|
+
* @param {string} pName - The beacon name to search for
|
|
151
|
+
* @returns {object|null} The first matching Beacon record, or null
|
|
152
|
+
*/
|
|
153
|
+
findBeaconByName(pName)
|
|
154
|
+
{
|
|
155
|
+
let tmpBeaconIDs = Object.keys(this._Beacons);
|
|
156
|
+
for (let i = 0; i < tmpBeaconIDs.length; i++)
|
|
157
|
+
{
|
|
158
|
+
let tmpBeacon = this._Beacons[tmpBeaconIDs[i]];
|
|
159
|
+
if (tmpBeacon.Name === pName)
|
|
160
|
+
{
|
|
161
|
+
return tmpBeacon;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Find a beacon by its SessionID property.
|
|
169
|
+
*
|
|
170
|
+
* @param {string} pSessionID - The session identifier to search for
|
|
171
|
+
* @returns {object|null} The first matching Beacon record, or null
|
|
172
|
+
*/
|
|
173
|
+
findBeaconBySessionID(pSessionID)
|
|
174
|
+
{
|
|
175
|
+
let tmpBeaconIDs = Object.keys(this._Beacons);
|
|
176
|
+
for (let i = 0; i < tmpBeaconIDs.length; i++)
|
|
177
|
+
{
|
|
178
|
+
let tmpBeacon = this._Beacons[tmpBeaconIDs[i]];
|
|
179
|
+
if (tmpBeacon.SessionID === pSessionID)
|
|
180
|
+
{
|
|
181
|
+
return tmpBeacon;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Deregister a Beacon and release any assigned work items.
|
|
189
|
+
*
|
|
190
|
+
* @param {string} pBeaconID
|
|
191
|
+
* @returns {boolean} True if the Beacon was found and removed
|
|
192
|
+
*/
|
|
193
|
+
deregisterBeacon(pBeaconID)
|
|
194
|
+
{
|
|
195
|
+
let tmpBeacon = this._Beacons[pBeaconID];
|
|
196
|
+
|
|
197
|
+
if (!tmpBeacon)
|
|
198
|
+
{
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Release any assigned work items back to Pending
|
|
203
|
+
for (let i = 0; i < tmpBeacon.CurrentWorkItems.length; i++)
|
|
204
|
+
{
|
|
205
|
+
let tmpWorkItem = this._WorkQueue[tmpBeacon.CurrentWorkItems[i]];
|
|
206
|
+
if (tmpWorkItem && (tmpWorkItem.Status === 'Assigned' || tmpWorkItem.Status === 'Running'))
|
|
207
|
+
{
|
|
208
|
+
tmpWorkItem.Status = 'Pending';
|
|
209
|
+
tmpWorkItem.AssignedBeaconID = null;
|
|
210
|
+
tmpWorkItem.ClaimedAt = null;
|
|
211
|
+
this.log.warn(`BeaconCoordinator: released work item [${tmpWorkItem.WorkItemHash}] from deregistered beacon [${pBeaconID}].`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
delete this._Beacons[pBeaconID];
|
|
216
|
+
this.log.info(`BeaconCoordinator: deregistered beacon [${pBeaconID}].`);
|
|
217
|
+
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Process a heartbeat from a Beacon.
|
|
223
|
+
*
|
|
224
|
+
* @param {string} pBeaconID
|
|
225
|
+
* @returns {object|null} Updated Beacon record, or null if not found
|
|
226
|
+
*/
|
|
227
|
+
heartbeat(pBeaconID)
|
|
228
|
+
{
|
|
229
|
+
let tmpBeacon = this._Beacons[pBeaconID];
|
|
230
|
+
|
|
231
|
+
if (!tmpBeacon)
|
|
232
|
+
{
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
tmpBeacon.LastHeartbeat = new Date().toISOString();
|
|
237
|
+
|
|
238
|
+
// Update status based on current work load
|
|
239
|
+
if (tmpBeacon.CurrentWorkItems.length >= tmpBeacon.MaxConcurrent)
|
|
240
|
+
{
|
|
241
|
+
tmpBeacon.Status = 'Busy';
|
|
242
|
+
}
|
|
243
|
+
else
|
|
244
|
+
{
|
|
245
|
+
tmpBeacon.Status = 'Online';
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return tmpBeacon;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* List all registered Beacons.
|
|
253
|
+
*
|
|
254
|
+
* @returns {Array} Array of Beacon records
|
|
255
|
+
*/
|
|
256
|
+
listBeacons()
|
|
257
|
+
{
|
|
258
|
+
return Object.values(this._Beacons);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Get a specific Beacon by ID.
|
|
263
|
+
*
|
|
264
|
+
* @param {string} pBeaconID
|
|
265
|
+
* @returns {object|null} Beacon record or null
|
|
266
|
+
*/
|
|
267
|
+
getBeacon(pBeaconID)
|
|
268
|
+
{
|
|
269
|
+
return this._Beacons[pBeaconID] || null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ====================================================================
|
|
273
|
+
// Work Queue
|
|
274
|
+
// ====================================================================
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Enqueue a work item for Beacon dispatch.
|
|
278
|
+
*
|
|
279
|
+
* Called by the beacon-dispatch task type when a task needs remote execution.
|
|
280
|
+
*
|
|
281
|
+
* @param {object} pWorkItemInfo - Work item details
|
|
282
|
+
* @returns {object} The created work item record
|
|
283
|
+
*/
|
|
284
|
+
enqueueWorkItem(pWorkItemInfo)
|
|
285
|
+
{
|
|
286
|
+
let tmpTimestamp = Date.now();
|
|
287
|
+
let tmpWorkItemHash = `wi-${pWorkItemInfo.RunHash || 'unknown'}-${pWorkItemInfo.NodeHash || 'unknown'}-${tmpTimestamp}`;
|
|
288
|
+
|
|
289
|
+
let tmpDefaultTimeout = this.fable.settings.UltravisorBeaconWorkItemTimeoutMs || 300000;
|
|
290
|
+
|
|
291
|
+
let tmpWorkItem = {
|
|
292
|
+
WorkItemHash: tmpWorkItemHash,
|
|
293
|
+
RunHash: pWorkItemInfo.RunHash || '',
|
|
294
|
+
NodeHash: pWorkItemInfo.NodeHash || '',
|
|
295
|
+
OperationHash: pWorkItemInfo.OperationHash || '',
|
|
296
|
+
Capability: pWorkItemInfo.Capability || 'Shell',
|
|
297
|
+
Action: pWorkItemInfo.Action || 'Execute',
|
|
298
|
+
Settings: pWorkItemInfo.Settings || {},
|
|
299
|
+
AffinityKey: pWorkItemInfo.AffinityKey || '',
|
|
300
|
+
AssignedBeaconID: null,
|
|
301
|
+
Status: 'Pending',
|
|
302
|
+
TimeoutMs: pWorkItemInfo.TimeoutMs || tmpDefaultTimeout,
|
|
303
|
+
CreatedAt: new Date(tmpTimestamp).toISOString(),
|
|
304
|
+
ClaimedAt: null,
|
|
305
|
+
CompletedAt: null,
|
|
306
|
+
Result: null
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
// Check for affinity binding — pre-assign to a specific Beacon
|
|
310
|
+
if (tmpWorkItem.AffinityKey)
|
|
311
|
+
{
|
|
312
|
+
let tmpBinding = this._AffinityBindings[tmpWorkItem.AffinityKey];
|
|
313
|
+
|
|
314
|
+
if (tmpBinding && this._Beacons[tmpBinding.BeaconID])
|
|
315
|
+
{
|
|
316
|
+
// Check if the binding has expired
|
|
317
|
+
if (new Date(tmpBinding.ExpiresAt) > new Date())
|
|
318
|
+
{
|
|
319
|
+
tmpWorkItem.AssignedBeaconID = tmpBinding.BeaconID;
|
|
320
|
+
tmpWorkItem.Status = 'Assigned';
|
|
321
|
+
tmpWorkItem.ClaimedAt = new Date().toISOString();
|
|
322
|
+
|
|
323
|
+
let tmpBeacon = this._Beacons[tmpBinding.BeaconID];
|
|
324
|
+
tmpBeacon.CurrentWorkItems.push(tmpWorkItemHash);
|
|
325
|
+
|
|
326
|
+
this.log.info(`BeaconCoordinator: work item [${tmpWorkItemHash}] pre-assigned to beacon [${tmpBinding.BeaconID}] via affinity [${tmpWorkItem.AffinityKey}].`);
|
|
327
|
+
}
|
|
328
|
+
else
|
|
329
|
+
{
|
|
330
|
+
// Binding expired, clean it up
|
|
331
|
+
delete this._AffinityBindings[tmpWorkItem.AffinityKey];
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
this._WorkQueue[tmpWorkItemHash] = tmpWorkItem;
|
|
337
|
+
this.log.info(`BeaconCoordinator: enqueued work item [${tmpWorkItemHash}] (${tmpWorkItem.Capability}/${tmpWorkItem.Action}) status=${tmpWorkItem.Status}.`);
|
|
338
|
+
|
|
339
|
+
return tmpWorkItem;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Poll for available work matching a Beacon's capabilities.
|
|
344
|
+
*
|
|
345
|
+
* Returns the first matching work item and assigns it to the Beacon.
|
|
346
|
+
* Affinity-assigned items are prioritized for the matching Beacon.
|
|
347
|
+
*
|
|
348
|
+
* @param {string} pBeaconID - The polling Beacon's ID
|
|
349
|
+
* @returns {object|null} A work item, or null if none available
|
|
350
|
+
*/
|
|
351
|
+
pollForWork(pBeaconID)
|
|
352
|
+
{
|
|
353
|
+
let tmpBeacon = this._Beacons[pBeaconID];
|
|
354
|
+
|
|
355
|
+
if (!tmpBeacon)
|
|
356
|
+
{
|
|
357
|
+
this.log.warn(`BeaconCoordinator: poll from unknown beacon [${pBeaconID}].`);
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Update heartbeat on poll
|
|
362
|
+
tmpBeacon.LastHeartbeat = new Date().toISOString();
|
|
363
|
+
|
|
364
|
+
// Check if Beacon is at capacity
|
|
365
|
+
if (tmpBeacon.CurrentWorkItems.length >= tmpBeacon.MaxConcurrent)
|
|
366
|
+
{
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
let tmpWorkItemHashes = Object.keys(this._WorkQueue);
|
|
371
|
+
|
|
372
|
+
// First pass: check for affinity-assigned items for this Beacon
|
|
373
|
+
for (let i = 0; i < tmpWorkItemHashes.length; i++)
|
|
374
|
+
{
|
|
375
|
+
let tmpWorkItem = this._WorkQueue[tmpWorkItemHashes[i]];
|
|
376
|
+
|
|
377
|
+
if (tmpWorkItem.Status === 'Assigned' && tmpWorkItem.AssignedBeaconID === pBeaconID)
|
|
378
|
+
{
|
|
379
|
+
// This item was pre-assigned to us via affinity
|
|
380
|
+
tmpWorkItem.Status = 'Running';
|
|
381
|
+
this.log.info(`BeaconCoordinator: beacon [${pBeaconID}] picked up affinity-assigned work item [${tmpWorkItem.WorkItemHash}].`);
|
|
382
|
+
return this._sanitizeWorkItemForBeacon(tmpWorkItem);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Second pass: check for unassigned items matching capabilities
|
|
387
|
+
for (let i = 0; i < tmpWorkItemHashes.length; i++)
|
|
388
|
+
{
|
|
389
|
+
let tmpWorkItem = this._WorkQueue[tmpWorkItemHashes[i]];
|
|
390
|
+
|
|
391
|
+
if (tmpWorkItem.Status !== 'Pending')
|
|
392
|
+
{
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Skip items assigned to a different Beacon (affinity)
|
|
397
|
+
if (tmpWorkItem.AssignedBeaconID && tmpWorkItem.AssignedBeaconID !== pBeaconID)
|
|
398
|
+
{
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Check capability match
|
|
403
|
+
if (tmpBeacon.Capabilities.indexOf(tmpWorkItem.Capability) === -1)
|
|
404
|
+
{
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Claim this work item
|
|
409
|
+
tmpWorkItem.Status = 'Running';
|
|
410
|
+
tmpWorkItem.AssignedBeaconID = pBeaconID;
|
|
411
|
+
tmpWorkItem.ClaimedAt = new Date().toISOString();
|
|
412
|
+
|
|
413
|
+
if (tmpBeacon.CurrentWorkItems.indexOf(tmpWorkItem.WorkItemHash) === -1)
|
|
414
|
+
{
|
|
415
|
+
tmpBeacon.CurrentWorkItems.push(tmpWorkItem.WorkItemHash);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Create affinity binding if the work item has an AffinityKey
|
|
419
|
+
if (tmpWorkItem.AffinityKey && !this._AffinityBindings[tmpWorkItem.AffinityKey])
|
|
420
|
+
{
|
|
421
|
+
let tmpAffinityTTL = this.fable.settings.UltravisorBeaconAffinityTTLMs || 3600000;
|
|
422
|
+
this._AffinityBindings[tmpWorkItem.AffinityKey] = {
|
|
423
|
+
AffinityKey: tmpWorkItem.AffinityKey,
|
|
424
|
+
BeaconID: pBeaconID,
|
|
425
|
+
RunHash: tmpWorkItem.RunHash,
|
|
426
|
+
CreatedAt: new Date().toISOString(),
|
|
427
|
+
ExpiresAt: new Date(Date.now() + tmpAffinityTTL).toISOString()
|
|
428
|
+
};
|
|
429
|
+
this.log.info(`BeaconCoordinator: created affinity binding [${tmpWorkItem.AffinityKey}] → beacon [${pBeaconID}].`);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
this.log.info(`BeaconCoordinator: beacon [${pBeaconID}] claimed work item [${tmpWorkItem.WorkItemHash}].`);
|
|
433
|
+
return this._sanitizeWorkItemForBeacon(tmpWorkItem);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Return a sanitized work item for the Beacon (only what it needs to execute).
|
|
441
|
+
*
|
|
442
|
+
* @param {object} pWorkItem
|
|
443
|
+
* @returns {object}
|
|
444
|
+
*/
|
|
445
|
+
_sanitizeWorkItemForBeacon(pWorkItem)
|
|
446
|
+
{
|
|
447
|
+
return {
|
|
448
|
+
WorkItemHash: pWorkItem.WorkItemHash,
|
|
449
|
+
Capability: pWorkItem.Capability,
|
|
450
|
+
Action: pWorkItem.Action,
|
|
451
|
+
Settings: pWorkItem.Settings,
|
|
452
|
+
OperationHash: pWorkItem.OperationHash,
|
|
453
|
+
TimeoutMs: pWorkItem.TimeoutMs
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Report successful completion of a work item.
|
|
459
|
+
*
|
|
460
|
+
* Resumes the paused operation via the ExecutionEngine.
|
|
461
|
+
*
|
|
462
|
+
* @param {string} pWorkItemHash
|
|
463
|
+
* @param {object} pResult - { Outputs, Log }
|
|
464
|
+
* @param {function} fCallback - function(pError)
|
|
465
|
+
*/
|
|
466
|
+
completeWorkItem(pWorkItemHash, pResult, fCallback)
|
|
467
|
+
{
|
|
468
|
+
let tmpWorkItem = this._WorkQueue[pWorkItemHash];
|
|
469
|
+
|
|
470
|
+
if (!tmpWorkItem)
|
|
471
|
+
{
|
|
472
|
+
return fCallback(new Error(`BeaconCoordinator: work item [${pWorkItemHash}] not found.`));
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (tmpWorkItem.Status === 'Complete' || tmpWorkItem.Status === 'Error' || tmpWorkItem.Status === 'Timeout')
|
|
476
|
+
{
|
|
477
|
+
return fCallback(new Error(`BeaconCoordinator: work item [${pWorkItemHash}] already finalized (${tmpWorkItem.Status}).`));
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
tmpWorkItem.Status = 'Complete';
|
|
481
|
+
tmpWorkItem.CompletedAt = new Date().toISOString();
|
|
482
|
+
|
|
483
|
+
// Merge accumulated progress logs with the final completion log
|
|
484
|
+
let tmpFinalResult = pResult || {};
|
|
485
|
+
if (tmpWorkItem.AccumulatedLog && tmpWorkItem.AccumulatedLog.length > 0)
|
|
486
|
+
{
|
|
487
|
+
let tmpCompletionLog = tmpFinalResult.Log || [];
|
|
488
|
+
tmpFinalResult.Log = tmpWorkItem.AccumulatedLog.concat(tmpCompletionLog);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
tmpWorkItem.Result = tmpFinalResult;
|
|
492
|
+
|
|
493
|
+
// Remove from Beacon's current work list
|
|
494
|
+
this._removeWorkItemFromBeacon(tmpWorkItem.AssignedBeaconID, pWorkItemHash);
|
|
495
|
+
|
|
496
|
+
this.log.info(`BeaconCoordinator: work item [${pWorkItemHash}] completed by beacon [${tmpWorkItem.AssignedBeaconID}].`);
|
|
497
|
+
|
|
498
|
+
// Check for direct dispatch callback (synchronous HTTP dispatch)
|
|
499
|
+
if (this._resolveDirectDispatch(pWorkItemHash, null, {
|
|
500
|
+
Success: true,
|
|
501
|
+
WorkItemHash: pWorkItemHash,
|
|
502
|
+
Outputs: tmpFinalResult.Outputs || {},
|
|
503
|
+
Log: tmpFinalResult.Log || []
|
|
504
|
+
}))
|
|
505
|
+
{
|
|
506
|
+
// Direct dispatch — clean up and done
|
|
507
|
+
delete this._WorkQueue[pWorkItemHash];
|
|
508
|
+
return fCallback(null);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// No RunHash means this was a standalone work item (not part of an operation)
|
|
512
|
+
if (!tmpWorkItem.RunHash)
|
|
513
|
+
{
|
|
514
|
+
delete this._WorkQueue[pWorkItemHash];
|
|
515
|
+
return fCallback(null);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Resume the paused operation
|
|
519
|
+
let tmpEngine = this._getService('UltravisorExecutionEngine');
|
|
520
|
+
|
|
521
|
+
if (!tmpEngine)
|
|
522
|
+
{
|
|
523
|
+
return fCallback(new Error('BeaconCoordinator: ExecutionEngine service not found.'));
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Build the structured outputs to pass to resumeOperation
|
|
527
|
+
let tmpOutputs = tmpFinalResult.Outputs || {};
|
|
528
|
+
|
|
529
|
+
// Include the BeaconID in the outputs so downstream tasks can reference it
|
|
530
|
+
tmpOutputs.BeaconID = tmpWorkItem.AssignedBeaconID || '';
|
|
531
|
+
|
|
532
|
+
tmpEngine.resumeOperation(tmpWorkItem.RunHash, tmpWorkItem.NodeHash, tmpOutputs,
|
|
533
|
+
(pError, pContext) =>
|
|
534
|
+
{
|
|
535
|
+
if (pError)
|
|
536
|
+
{
|
|
537
|
+
this.log.error(`BeaconCoordinator: error resuming operation for work item [${pWorkItemHash}]: ${pError.message}`);
|
|
538
|
+
return fCallback(pError);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
this.log.info(`BeaconCoordinator: operation [${tmpWorkItem.RunHash}] resumed after work item [${pWorkItemHash}] completion.`);
|
|
542
|
+
|
|
543
|
+
// Clean up the completed work item from the queue
|
|
544
|
+
delete this._WorkQueue[pWorkItemHash];
|
|
545
|
+
|
|
546
|
+
return fCallback(null);
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Report failure of a work item.
|
|
552
|
+
*
|
|
553
|
+
* Resumes the paused operation with an error result so the graph's
|
|
554
|
+
* error path can handle it.
|
|
555
|
+
*
|
|
556
|
+
* @param {string} pWorkItemHash
|
|
557
|
+
* @param {object} pError - { ErrorMessage, Log }
|
|
558
|
+
* @param {function} fCallback - function(pError)
|
|
559
|
+
*/
|
|
560
|
+
failWorkItem(pWorkItemHash, pError, fCallback)
|
|
561
|
+
{
|
|
562
|
+
let tmpWorkItem = this._WorkQueue[pWorkItemHash];
|
|
563
|
+
|
|
564
|
+
if (!tmpWorkItem)
|
|
565
|
+
{
|
|
566
|
+
return fCallback(new Error(`BeaconCoordinator: work item [${pWorkItemHash}] not found.`));
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (tmpWorkItem.Status === 'Complete' || tmpWorkItem.Status === 'Error' || tmpWorkItem.Status === 'Timeout')
|
|
570
|
+
{
|
|
571
|
+
return fCallback(new Error(`BeaconCoordinator: work item [${pWorkItemHash}] already finalized (${tmpWorkItem.Status}).`));
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
tmpWorkItem.Status = 'Error';
|
|
575
|
+
tmpWorkItem.CompletedAt = new Date().toISOString();
|
|
576
|
+
tmpWorkItem.Result = { Error: pError.ErrorMessage || 'Unknown error', Log: pError.Log || [] };
|
|
577
|
+
|
|
578
|
+
// Remove from Beacon's current work list
|
|
579
|
+
this._removeWorkItemFromBeacon(tmpWorkItem.AssignedBeaconID, pWorkItemHash);
|
|
580
|
+
|
|
581
|
+
this.log.warn(`BeaconCoordinator: work item [${pWorkItemHash}] failed: ${pError.ErrorMessage || 'Unknown error'}`);
|
|
582
|
+
|
|
583
|
+
// Check for direct dispatch callback (synchronous HTTP dispatch)
|
|
584
|
+
if (this._resolveDirectDispatch(pWorkItemHash,
|
|
585
|
+
new Error(pError.ErrorMessage || 'Beacon work item failed.'), null))
|
|
586
|
+
{
|
|
587
|
+
// Direct dispatch — clean up and done
|
|
588
|
+
delete this._WorkQueue[pWorkItemHash];
|
|
589
|
+
return fCallback(null);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// No RunHash means this was a standalone work item (not part of an operation)
|
|
593
|
+
if (!tmpWorkItem.RunHash)
|
|
594
|
+
{
|
|
595
|
+
delete this._WorkQueue[pWorkItemHash];
|
|
596
|
+
return fCallback(null);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Resume the operation — the beacon-dispatch task type fires 'Error' event
|
|
600
|
+
// by storing error info in outputs; the ResumeEventName handles routing
|
|
601
|
+
let tmpEngine = this._getService('UltravisorExecutionEngine');
|
|
602
|
+
|
|
603
|
+
if (!tmpEngine)
|
|
604
|
+
{
|
|
605
|
+
return fCallback(new Error('BeaconCoordinator: ExecutionEngine service not found.'));
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
let tmpErrorOutputs = {
|
|
609
|
+
StdOut: pError.ErrorMessage || 'Beacon work item failed.',
|
|
610
|
+
ExitCode: -1,
|
|
611
|
+
BeaconID: tmpWorkItem.AssignedBeaconID || '',
|
|
612
|
+
_BeaconError: true
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
// Resume with 'Error' event by temporarily modifying the waiting task's ResumeEventName
|
|
616
|
+
let tmpManifest = this._getService('UltravisorExecutionManifest');
|
|
617
|
+
if (tmpManifest)
|
|
618
|
+
{
|
|
619
|
+
let tmpContext = tmpManifest.getRun(tmpWorkItem.RunHash);
|
|
620
|
+
if (tmpContext && tmpContext.WaitingTasks[tmpWorkItem.NodeHash])
|
|
621
|
+
{
|
|
622
|
+
tmpContext.WaitingTasks[tmpWorkItem.NodeHash].ResumeEventName = 'Error';
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
tmpEngine.resumeOperation(tmpWorkItem.RunHash, tmpWorkItem.NodeHash, tmpErrorOutputs,
|
|
627
|
+
(pResumeError, pContext) =>
|
|
628
|
+
{
|
|
629
|
+
if (pResumeError)
|
|
630
|
+
{
|
|
631
|
+
this.log.error(`BeaconCoordinator: error resuming operation for failed work item [${pWorkItemHash}]: ${pResumeError.message}`);
|
|
632
|
+
return fCallback(pResumeError);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
this.log.info(`BeaconCoordinator: operation [${tmpWorkItem.RunHash}] resumed with error path after work item [${pWorkItemHash}] failure.`);
|
|
636
|
+
|
|
637
|
+
// Clean up the failed work item
|
|
638
|
+
delete this._WorkQueue[pWorkItemHash];
|
|
639
|
+
|
|
640
|
+
return fCallback(null);
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* List all work items (for admin view).
|
|
646
|
+
*
|
|
647
|
+
* @returns {Array} Array of work item records
|
|
648
|
+
*/
|
|
649
|
+
listWorkItems()
|
|
650
|
+
{
|
|
651
|
+
return Object.values(this._WorkQueue);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Get a specific work item by hash.
|
|
656
|
+
*
|
|
657
|
+
* @param {string} pWorkItemHash
|
|
658
|
+
* @returns {object|null}
|
|
659
|
+
*/
|
|
660
|
+
getWorkItem(pWorkItemHash)
|
|
661
|
+
{
|
|
662
|
+
return this._WorkQueue[pWorkItemHash] || null;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// ====================================================================
|
|
666
|
+
// Direct Dispatch (synchronous HTTP dispatch)
|
|
667
|
+
// ====================================================================
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Dispatch a work item and wait for completion.
|
|
671
|
+
*
|
|
672
|
+
* Used by the POST /Beacon/Work/Dispatch endpoint to provide
|
|
673
|
+
* synchronous request/response semantics for external consumers
|
|
674
|
+
* (e.g. retold-remote). The HTTP connection stays open until the
|
|
675
|
+
* beacon completes the work item.
|
|
676
|
+
*
|
|
677
|
+
* @param {object} pWorkItemInfo - Work item details (no RunHash/NodeHash needed)
|
|
678
|
+
* @param {function} fCallback - function(pError, pResult) called when work completes
|
|
679
|
+
*/
|
|
680
|
+
dispatchAndWait(pWorkItemInfo, fCallback)
|
|
681
|
+
{
|
|
682
|
+
// Ensure no RunHash — direct dispatch bypasses the operation graph
|
|
683
|
+
pWorkItemInfo.RunHash = '';
|
|
684
|
+
pWorkItemInfo.NodeHash = '';
|
|
685
|
+
pWorkItemInfo.OperationHash = '';
|
|
686
|
+
|
|
687
|
+
// Enqueue the work item
|
|
688
|
+
let tmpWorkItem = this.enqueueWorkItem(pWorkItemInfo);
|
|
689
|
+
|
|
690
|
+
let tmpTimeoutMs = pWorkItemInfo.TimeoutMs || this.fable.settings.UltravisorBeaconWorkItemTimeoutMs || 300000;
|
|
691
|
+
|
|
692
|
+
// Register completion callback
|
|
693
|
+
let tmpTimeoutHandle = setTimeout(
|
|
694
|
+
() =>
|
|
695
|
+
{
|
|
696
|
+
// Timeout — clean up and call back with error
|
|
697
|
+
let tmpEntry = this._DirectDispatchCallbacks[tmpWorkItem.WorkItemHash];
|
|
698
|
+
if (tmpEntry)
|
|
699
|
+
{
|
|
700
|
+
delete this._DirectDispatchCallbacks[tmpWorkItem.WorkItemHash];
|
|
701
|
+
|
|
702
|
+
// Clean up the work item from the queue
|
|
703
|
+
let tmpWI = this._WorkQueue[tmpWorkItem.WorkItemHash];
|
|
704
|
+
if (tmpWI)
|
|
705
|
+
{
|
|
706
|
+
this._removeWorkItemFromBeacon(tmpWI.AssignedBeaconID, tmpWorkItem.WorkItemHash);
|
|
707
|
+
delete this._WorkQueue[tmpWorkItem.WorkItemHash];
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
this.log.warn(`BeaconCoordinator: direct dispatch [${tmpWorkItem.WorkItemHash}] timed out after ${tmpTimeoutMs}ms.`);
|
|
711
|
+
tmpEntry.callback(new Error(`Direct dispatch timed out after ${tmpTimeoutMs}ms.`));
|
|
712
|
+
}
|
|
713
|
+
},
|
|
714
|
+
tmpTimeoutMs);
|
|
715
|
+
|
|
716
|
+
this._DirectDispatchCallbacks[tmpWorkItem.WorkItemHash] =
|
|
717
|
+
{
|
|
718
|
+
callback: fCallback,
|
|
719
|
+
timeoutHandle: tmpTimeoutHandle
|
|
720
|
+
};
|
|
721
|
+
|
|
722
|
+
this.log.info(`BeaconCoordinator: direct dispatch [${tmpWorkItem.WorkItemHash}] waiting for completion (timeout: ${tmpTimeoutMs}ms).`);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Check if a work item has a direct dispatch callback registered.
|
|
727
|
+
* If so, call it and return true. Otherwise return false.
|
|
728
|
+
*
|
|
729
|
+
* @param {string} pWorkItemHash
|
|
730
|
+
* @param {object|null} pError - Error object (for fail path)
|
|
731
|
+
* @param {object|null} pResult - Result object (for complete path)
|
|
732
|
+
* @returns {boolean} True if a direct dispatch callback was found and called
|
|
733
|
+
*/
|
|
734
|
+
_resolveDirectDispatch(pWorkItemHash, pError, pResult)
|
|
735
|
+
{
|
|
736
|
+
let tmpEntry = this._DirectDispatchCallbacks[pWorkItemHash];
|
|
737
|
+
|
|
738
|
+
if (!tmpEntry)
|
|
739
|
+
{
|
|
740
|
+
return false;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// Clear the timeout
|
|
744
|
+
clearTimeout(tmpEntry.timeoutHandle);
|
|
745
|
+
delete this._DirectDispatchCallbacks[pWorkItemHash];
|
|
746
|
+
|
|
747
|
+
if (pError)
|
|
748
|
+
{
|
|
749
|
+
tmpEntry.callback(pError);
|
|
750
|
+
}
|
|
751
|
+
else
|
|
752
|
+
{
|
|
753
|
+
tmpEntry.callback(null, pResult);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
return true;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// ====================================================================
|
|
760
|
+
// Progress Reporting
|
|
761
|
+
// ====================================================================
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* Update progress for a running work item.
|
|
765
|
+
*
|
|
766
|
+
* Called by the Beacon during execution to report progress and
|
|
767
|
+
* stream log entries. Progress data is stored on the work item
|
|
768
|
+
* record and reflected in the manifest's WaitingTasks so it
|
|
769
|
+
* surfaces via GET /Manifest/:RunHash.
|
|
770
|
+
*
|
|
771
|
+
* @param {string} pWorkItemHash
|
|
772
|
+
* @param {object} pProgressData - { Percent?, Message?, Step?, TotalSteps?, Log? }
|
|
773
|
+
* @returns {boolean} true if progress was updated
|
|
774
|
+
*/
|
|
775
|
+
updateProgress(pWorkItemHash, pProgressData)
|
|
776
|
+
{
|
|
777
|
+
let tmpWorkItem = this._WorkQueue[pWorkItemHash];
|
|
778
|
+
|
|
779
|
+
if (!tmpWorkItem)
|
|
780
|
+
{
|
|
781
|
+
return false;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
if (tmpWorkItem.Status !== 'Running' && tmpWorkItem.Status !== 'Assigned')
|
|
785
|
+
{
|
|
786
|
+
return false;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// Update progress fields on the work item
|
|
790
|
+
tmpWorkItem.Progress = {
|
|
791
|
+
Percent: (pProgressData.Percent !== undefined) ? pProgressData.Percent : (tmpWorkItem.Progress ? tmpWorkItem.Progress.Percent : undefined),
|
|
792
|
+
Message: pProgressData.Message || (tmpWorkItem.Progress ? tmpWorkItem.Progress.Message : ''),
|
|
793
|
+
Step: (pProgressData.Step !== undefined) ? pProgressData.Step : (tmpWorkItem.Progress ? tmpWorkItem.Progress.Step : undefined),
|
|
794
|
+
TotalSteps: (pProgressData.TotalSteps !== undefined) ? pProgressData.TotalSteps : (tmpWorkItem.Progress ? tmpWorkItem.Progress.TotalSteps : undefined),
|
|
795
|
+
UpdatedAt: new Date().toISOString()
|
|
796
|
+
};
|
|
797
|
+
|
|
798
|
+
// Accumulate log entries
|
|
799
|
+
if (Array.isArray(pProgressData.Log) && pProgressData.Log.length > 0)
|
|
800
|
+
{
|
|
801
|
+
if (!tmpWorkItem.AccumulatedLog)
|
|
802
|
+
{
|
|
803
|
+
tmpWorkItem.AccumulatedLog = [];
|
|
804
|
+
}
|
|
805
|
+
for (let i = 0; i < pProgressData.Log.length; i++)
|
|
806
|
+
{
|
|
807
|
+
tmpWorkItem.AccumulatedLog.push(pProgressData.Log[i]);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// Update the manifest's WaitingTasks entry so progress surfaces
|
|
812
|
+
// via GET /Manifest/:RunHash
|
|
813
|
+
let tmpManifest = this._getService('UltravisorExecutionManifest');
|
|
814
|
+
if (tmpManifest)
|
|
815
|
+
{
|
|
816
|
+
let tmpContext = tmpManifest.getRun(tmpWorkItem.RunHash);
|
|
817
|
+
if (tmpContext && tmpContext.WaitingTasks[tmpWorkItem.NodeHash])
|
|
818
|
+
{
|
|
819
|
+
tmpContext.WaitingTasks[tmpWorkItem.NodeHash].Progress = tmpWorkItem.Progress;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
return true;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// ====================================================================
|
|
827
|
+
// Affinity
|
|
828
|
+
// ====================================================================
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* List all active affinity bindings.
|
|
832
|
+
*
|
|
833
|
+
* @returns {Array}
|
|
834
|
+
*/
|
|
835
|
+
listAffinityBindings()
|
|
836
|
+
{
|
|
837
|
+
return Object.values(this._AffinityBindings);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Clear a specific affinity binding.
|
|
842
|
+
*
|
|
843
|
+
* @param {string} pAffinityKey
|
|
844
|
+
* @returns {boolean}
|
|
845
|
+
*/
|
|
846
|
+
clearAffinityBinding(pAffinityKey)
|
|
847
|
+
{
|
|
848
|
+
if (this._AffinityBindings[pAffinityKey])
|
|
849
|
+
{
|
|
850
|
+
delete this._AffinityBindings[pAffinityKey];
|
|
851
|
+
return true;
|
|
852
|
+
}
|
|
853
|
+
return false;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// ====================================================================
|
|
857
|
+
// Internal Helpers
|
|
858
|
+
// ====================================================================
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* Get a service by type name.
|
|
862
|
+
*/
|
|
863
|
+
_getService(pTypeName)
|
|
864
|
+
{
|
|
865
|
+
return this.fable.servicesMap[pTypeName]
|
|
866
|
+
? Object.values(this.fable.servicesMap[pTypeName])[0]
|
|
867
|
+
: null;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* Remove a work item hash from a Beacon's CurrentWorkItems array.
|
|
872
|
+
*/
|
|
873
|
+
_removeWorkItemFromBeacon(pBeaconID, pWorkItemHash)
|
|
874
|
+
{
|
|
875
|
+
if (!pBeaconID)
|
|
876
|
+
{
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
let tmpBeacon = this._Beacons[pBeaconID];
|
|
881
|
+
|
|
882
|
+
if (!tmpBeacon)
|
|
883
|
+
{
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
let tmpIndex = tmpBeacon.CurrentWorkItems.indexOf(pWorkItemHash);
|
|
888
|
+
if (tmpIndex > -1)
|
|
889
|
+
{
|
|
890
|
+
tmpBeacon.CurrentWorkItems.splice(tmpIndex, 1);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// Update status
|
|
894
|
+
if (tmpBeacon.CurrentWorkItems.length < tmpBeacon.MaxConcurrent)
|
|
895
|
+
{
|
|
896
|
+
tmpBeacon.Status = 'Online';
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
/**
|
|
901
|
+
* Check for timed-out work items and dead Beacons.
|
|
902
|
+
*/
|
|
903
|
+
_checkTimeouts()
|
|
904
|
+
{
|
|
905
|
+
let tmpNow = Date.now();
|
|
906
|
+
|
|
907
|
+
// Check work item timeouts
|
|
908
|
+
let tmpWorkItemHashes = Object.keys(this._WorkQueue);
|
|
909
|
+
for (let i = 0; i < tmpWorkItemHashes.length; i++)
|
|
910
|
+
{
|
|
911
|
+
let tmpWorkItem = this._WorkQueue[tmpWorkItemHashes[i]];
|
|
912
|
+
|
|
913
|
+
if (tmpWorkItem.Status !== 'Running' && tmpWorkItem.Status !== 'Assigned')
|
|
914
|
+
{
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
let tmpClaimedTime = tmpWorkItem.ClaimedAt ? new Date(tmpWorkItem.ClaimedAt).getTime() : tmpNow;
|
|
919
|
+
let tmpElapsed = tmpNow - tmpClaimedTime;
|
|
920
|
+
|
|
921
|
+
if (tmpElapsed > tmpWorkItem.TimeoutMs)
|
|
922
|
+
{
|
|
923
|
+
this.log.warn(`BeaconCoordinator: work item [${tmpWorkItem.WorkItemHash}] timed out after ${tmpElapsed}ms.`);
|
|
924
|
+
this.failWorkItem(tmpWorkItem.WorkItemHash,
|
|
925
|
+
{ ErrorMessage: `Work item timed out after ${tmpElapsed}ms.`, Log: ['Timeout'] },
|
|
926
|
+
(pError) =>
|
|
927
|
+
{
|
|
928
|
+
if (pError)
|
|
929
|
+
{
|
|
930
|
+
this.log.error(`BeaconCoordinator: error handling timeout for [${tmpWorkItem.WorkItemHash}]: ${pError.message}`);
|
|
931
|
+
}
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// Check Beacon heartbeat timeouts
|
|
937
|
+
let tmpHeartbeatTimeout = this.fable.settings.UltravisorBeaconHeartbeatTimeoutMs || 60000;
|
|
938
|
+
let tmpBeaconIDs = Object.keys(this._Beacons);
|
|
939
|
+
for (let i = 0; i < tmpBeaconIDs.length; i++)
|
|
940
|
+
{
|
|
941
|
+
let tmpBeacon = this._Beacons[tmpBeaconIDs[i]];
|
|
942
|
+
let tmpLastHeartbeat = new Date(tmpBeacon.LastHeartbeat).getTime();
|
|
943
|
+
let tmpElapsed = tmpNow - tmpLastHeartbeat;
|
|
944
|
+
|
|
945
|
+
if (tmpElapsed > tmpHeartbeatTimeout && tmpBeacon.Status !== 'Offline')
|
|
946
|
+
{
|
|
947
|
+
this.log.warn(`BeaconCoordinator: beacon [${tmpBeacon.BeaconID}] missed heartbeat (${tmpElapsed}ms since last), marking Offline.`);
|
|
948
|
+
tmpBeacon.Status = 'Offline';
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// Clean up expired affinity bindings
|
|
953
|
+
let tmpAffinityKeys = Object.keys(this._AffinityBindings);
|
|
954
|
+
for (let i = 0; i < tmpAffinityKeys.length; i++)
|
|
955
|
+
{
|
|
956
|
+
let tmpBinding = this._AffinityBindings[tmpAffinityKeys[i]];
|
|
957
|
+
if (new Date(tmpBinding.ExpiresAt).getTime() < tmpNow)
|
|
958
|
+
{
|
|
959
|
+
delete this._AffinityBindings[tmpAffinityKeys[i]];
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
module.exports = UltravisorBeaconCoordinator;
|
|
966
|
+
module.exports.default_configuration = {};
|