ultravisor 1.0.22 → 1.0.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/queue-followups/03-config-migration.md +123 -0
- package/docs/queue-followups/04-direct-dispatch-phase-emission.md +128 -0
- package/package.json +3 -1
- package/source/Ultravisor.cjs +4 -0
- package/source/datamodel/Ultravisor-BeaconQueue.json +165 -0
- package/source/services/Ultravisor-Beacon-ActionDefaults.cjs +174 -0
- package/source/services/Ultravisor-Beacon-Coordinator.cjs +382 -6
- package/source/services/Ultravisor-Beacon-RunManager.cjs +169 -0
- package/source/services/Ultravisor-Beacon-Scheduler.cjs +789 -0
- package/source/services/Ultravisor-ExecutionEngine.cjs +242 -6
- package/source/services/Ultravisor-ExecutionManifest.cjs +1 -0
- package/source/services/persistence/Ultravisor-Beacon-QueueStore.cjs +886 -0
- package/source/services/tasks/file-system/Ultravisor-TaskConfigs-FileSystem.cjs +81 -0
- package/source/services/tasks/file-system/definitions/chunked-write.json +38 -0
- package/source/web_server/Ultravisor-API-Server.cjs +354 -0
- package/test/Ultravisor_BeaconQueue_tests.js +502 -0
- package/test/Ultravisor_tests.js +132 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Follow-up: Migrate per-action config into `BeaconActionDefault`
|
|
2
|
+
|
|
3
|
+
**Status:** deferred — table and service are live; rows need populating.
|
|
4
|
+
|
|
5
|
+
## Why this exists
|
|
6
|
+
|
|
7
|
+
Per-action defaults (timeout, retry policy, priority, expected-wait
|
|
8
|
+
baseline) live in scattered Fable settings today. The queueing work
|
|
9
|
+
introduced a durable `BeaconActionDefault` table and a resolver
|
|
10
|
+
([`Ultravisor-Beacon-ActionDefaults.cjs`](../../source/services/Ultravisor-Beacon-ActionDefaults.cjs))
|
|
11
|
+
that an operator can tune live, without a hub restart.
|
|
12
|
+
|
|
13
|
+
The resolver already falls back in this order:
|
|
14
|
+
|
|
15
|
+
1. Per-request `Settings` override (`maxRetries`, `timeoutMs`, etc.)
|
|
16
|
+
2. Per-action row (`Capability`, `Action`)
|
|
17
|
+
3. Per-capability wildcard row (`Capability`, `Action=""`)
|
|
18
|
+
4. Fable-setting compatibility shim
|
|
19
|
+
(`UltravisorBeaconWorkItemTimeoutMs`,
|
|
20
|
+
`UltravisorBeaconHeartbeatMs`)
|
|
21
|
+
5. Hard defaults in `HARD_DEFAULTS`
|
|
22
|
+
|
|
23
|
+
So today everything still works via step 4–5. This follow-up replaces
|
|
24
|
+
the shim with real rows and retires the Fable settings.
|
|
25
|
+
|
|
26
|
+
## What's in the table today
|
|
27
|
+
|
|
28
|
+
Schema at [`Ultravisor-BeaconQueue.json`](../../source/datamodel/Ultravisor-BeaconQueue.json)
|
|
29
|
+
(the `BeaconActionDefault` entry). Columns:
|
|
30
|
+
|
|
31
|
+
| Column | Notes |
|
|
32
|
+
|---|---|
|
|
33
|
+
| `Capability` | required |
|
|
34
|
+
| `Action` | `""` = wildcard for the capability |
|
|
35
|
+
| `TimeoutMs` | overrides `UltravisorBeaconWorkItemTimeoutMs` |
|
|
36
|
+
| `MaxAttempts` | 1 = no retry |
|
|
37
|
+
| `RetryBackoffMs` | multiplied by attempt number |
|
|
38
|
+
| `DefaultPriority` | scheduler sort key |
|
|
39
|
+
| `ExpectedWaitP95Ms` | powers the `queue_wait` dimension of the health score |
|
|
40
|
+
| `HeartbeatExpectedMs` | powers the `event_freshness` dimension |
|
|
41
|
+
| `MinSamplesForBaseline` | min samples before `ActionDefaults.recomputeWaitBaseline` commits a p95 |
|
|
42
|
+
|
|
43
|
+
## Work to do
|
|
44
|
+
|
|
45
|
+
### 1. Inventory today's scattered settings
|
|
46
|
+
|
|
47
|
+
Grep the ultravisor tree for the settings that currently drive
|
|
48
|
+
per-action behavior:
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
grep -rn 'UltravisorBeaconWorkItemTimeoutMs\|UltravisorBeaconHeartbeatMs\|UltravisorBeaconAffinityTTLMs' \
|
|
52
|
+
/Users/steven/Code/retold/modules/apps/ultravisor/source
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Also check the consumer code for per-action constants in task-type
|
|
56
|
+
definitions (`source/services/tasks/**`) and capability providers
|
|
57
|
+
(`source/beacon/providers/**`).
|
|
58
|
+
|
|
59
|
+
### 2. Seed the table at startup
|
|
60
|
+
|
|
61
|
+
Write `source/services/tasks/extension/Ultravisor-BeaconActionDefault-Seed.cjs`
|
|
62
|
+
(or similar) that runs once on hub boot and upserts a row per
|
|
63
|
+
(Capability, Action) pair the hub knows about. Source of truth is
|
|
64
|
+
the action catalog on the coordinator
|
|
65
|
+
([`Ultravisor-Beacon-Coordinator.cjs`](../../source/services/Ultravisor-Beacon-Coordinator.cjs)
|
|
66
|
+
`_ActionCatalog`) — iterate it and call
|
|
67
|
+
`store.upsertActionDefault({ Capability, Action, ...hard_defaults })`
|
|
68
|
+
only when no row exists. Use `getActionDefault` first so operator-set
|
|
69
|
+
rows aren't clobbered on every restart.
|
|
70
|
+
|
|
71
|
+
The seed should **not** overwrite existing rows — it's a bootstrap,
|
|
72
|
+
not a reset.
|
|
73
|
+
|
|
74
|
+
### 3. Admin endpoints (optional, small)
|
|
75
|
+
|
|
76
|
+
If the `/queue` UI wants live knobs, add:
|
|
77
|
+
|
|
78
|
+
- `GET /Beacon/ActionDefaults` → `store.listActionDefaults()`
|
|
79
|
+
- `PUT /Beacon/ActionDefaults/:Capability/:Action` → upsert
|
|
80
|
+
(call `store.upsertActionDefault` + `defaults.invalidate()`)
|
|
81
|
+
|
|
82
|
+
Both gated by `_requireSession`.
|
|
83
|
+
|
|
84
|
+
### 4. Wire up the wait-baseline learner
|
|
85
|
+
|
|
86
|
+
`ActionDefaults.recomputeWaitBaseline(cap, action)` is implemented
|
|
87
|
+
but not called anywhere yet. Trigger it on a slow interval (every 5
|
|
88
|
+
minutes) from the scheduler, iterating capabilities that have seen
|
|
89
|
+
≥ `MinSamplesForBaseline` completions since last recompute. Add a
|
|
90
|
+
counter on the coordinator to track completions per capability so we
|
|
91
|
+
don't recompute on no new data.
|
|
92
|
+
|
|
93
|
+
Rough hook point: end of `UltravisorBeaconScheduler._summaryTick`, or
|
|
94
|
+
a dedicated slower interval.
|
|
95
|
+
|
|
96
|
+
### 5. Retire the Fable-setting shim
|
|
97
|
+
|
|
98
|
+
Once (2) has been in place for at least one production cycle, remove
|
|
99
|
+
the `_fableSettingFallback` in
|
|
100
|
+
[`Ultravisor-Beacon-ActionDefaults.cjs`](../../source/services/Ultravisor-Beacon-ActionDefaults.cjs).
|
|
101
|
+
Grep-clean the retired setting names from the codebase in the same PR
|
|
102
|
+
so nobody reintroduces them. This is the step that makes the
|
|
103
|
+
migration irreversible — don't do it in the same PR as the seeder.
|
|
104
|
+
|
|
105
|
+
## Verification
|
|
106
|
+
|
|
107
|
+
1. Wipe `~/.local/share/ultravisor/beacon/beacon-queue.sqlite` (or
|
|
108
|
+
wherever the store's DB path resolves for the deployment).
|
|
109
|
+
2. Boot the hub. Confirm `BeaconActionDefault` has one row per known
|
|
110
|
+
`Capability:Action` pair with the expected defaults.
|
|
111
|
+
3. `UPDATE BeaconActionDefault SET TimeoutMs = 1 WHERE Capability =
|
|
112
|
+
'Shell'`. Enqueue a Shell work item. The next
|
|
113
|
+
`BeaconActionDefaults._Cache` window (≤ 10s) flips it; next
|
|
114
|
+
work item picks up `TimeoutMs=1` without a hub restart.
|
|
115
|
+
4. Run 30 Shell work items with genuinely varying queue waits; confirm
|
|
116
|
+
`ExpectedWaitP95Ms` updates on its own after the baseline learner
|
|
117
|
+
tick.
|
|
118
|
+
|
|
119
|
+
## Non-goals
|
|
120
|
+
|
|
121
|
+
- UI for editing action defaults (defer until the `/queue` view is
|
|
122
|
+
shipped and operators are asking for it).
|
|
123
|
+
- Multi-tenant defaults (single-ultravisor-instance only).
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Follow-up: Phase emission for `/Beacon/Work/Dispatch` (sync path)
|
|
2
|
+
|
|
3
|
+
**Status:** deferred — the synchronous dispatch path bypasses the
|
|
4
|
+
scheduler and therefore never populates `Settings.QueueMetadata`.
|
|
5
|
+
Workers routed via `/Beacon/Work/Dispatch` still write their own
|
|
6
|
+
phases, but the hub-owned `queue_wait` / `worker_spinup` /
|
|
7
|
+
`asset_capture` records are skipped.
|
|
8
|
+
|
|
9
|
+
## Background
|
|
10
|
+
|
|
11
|
+
There are three dispatch paths in the hub today:
|
|
12
|
+
|
|
13
|
+
| Path | Route | Queue/metadata? | Phases? |
|
|
14
|
+
|---|---|---|---|
|
|
15
|
+
| Async queue | `POST /Beacon/Work/Enqueue` | Yes (via Scheduler) | Yes — beacon-side emits all three |
|
|
16
|
+
| Polling claim | `POST /Beacon/Work/Poll` | Yes (Coordinator stamps metadata in `_sanitizeWorkItemForBeacon`) | Yes — same |
|
|
17
|
+
| **Direct dispatch** | `POST /Beacon/Work/Dispatch` | **No** — calls `Coordinator.dispatchAndWait` which builds the work item inline and blocks the caller on the response | **No** — Settings.QueueMetadata is never set |
|
|
18
|
+
|
|
19
|
+
The direct-dispatch path is used for synchronous RPC-style calls
|
|
20
|
+
where the caller wants the result inline. It's rare but real — a
|
|
21
|
+
handful of retold-labs call sites and any external integration that
|
|
22
|
+
chose the sync contract for simplicity.
|
|
23
|
+
|
|
24
|
+
## Why this is separate from the main work
|
|
25
|
+
|
|
26
|
+
`dispatchAndWait` constructs the work item in a hot request handler
|
|
27
|
+
and resolves to the caller via `_DirectDispatchCallbacks`. It
|
|
28
|
+
intentionally skips the scheduler tick because the caller is
|
|
29
|
+
blocked waiting. Retrofitting it requires either:
|
|
30
|
+
|
|
31
|
+
1. Routing it through the same scheduler pass (adds latency — the
|
|
32
|
+
whole point of direct dispatch is "don't queue"), or
|
|
33
|
+
2. Populating `QueueMetadata` inline in the Coordinator when building
|
|
34
|
+
the work item and calling the beacon-side phase emit path directly.
|
|
35
|
+
|
|
36
|
+
Option 2 is correct. The queue wait for this path is near-zero by
|
|
37
|
+
definition, so the `queue_wait` record will almost always be `0ms`,
|
|
38
|
+
but the `worker_spinup` and `asset_capture` records still have
|
|
39
|
+
real durations worth capturing.
|
|
40
|
+
|
|
41
|
+
## Work to do
|
|
42
|
+
|
|
43
|
+
### 1. Populate `QueueMetadata` inline in `dispatchAndWait`
|
|
44
|
+
|
|
45
|
+
**File:** [`Ultravisor-Beacon-Coordinator.cjs`](../../source/services/Ultravisor-Beacon-Coordinator.cjs)
|
|
46
|
+
|
|
47
|
+
Find `dispatchAndWait` (search for the method definition). Where it
|
|
48
|
+
constructs the work item, set `Settings.QueueMetadata` the same way
|
|
49
|
+
`_sanitizeWorkItemForBeacon` does it now, but with the sync-path
|
|
50
|
+
timestamps:
|
|
51
|
+
|
|
52
|
+
```javascript
|
|
53
|
+
let tmpNowIso = new Date().toISOString();
|
|
54
|
+
tmpWorkItem.EnqueuedAt = tmpNowIso;
|
|
55
|
+
tmpWorkItem.DispatchedAt = tmpNowIso;
|
|
56
|
+
tmpWorkItem.QueueWaitMs = 0;
|
|
57
|
+
tmpWorkItem.AttemptNumber = 1;
|
|
58
|
+
tmpWorkItem.RunID = pWorkItemInfo.RunID || '';
|
|
59
|
+
tmpWorkItem.Settings = tmpWorkItem.Settings || {};
|
|
60
|
+
tmpWorkItem.Settings.QueueMetadata = {
|
|
61
|
+
RunID: tmpWorkItem.RunID,
|
|
62
|
+
WorkItemHash: tmpWorkItem.WorkItemHash,
|
|
63
|
+
EnqueuedAt: tmpNowIso,
|
|
64
|
+
DispatchedAt: tmpNowIso,
|
|
65
|
+
QueueWaitMs: 0,
|
|
66
|
+
AttemptNumber: 1,
|
|
67
|
+
HubInstanceID: (this.fable.settings && this.fable.settings.UltravisorHubInstanceID) || '',
|
|
68
|
+
DispatchPath: 'sync'
|
|
69
|
+
};
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Note the extra `DispatchPath: 'sync'` marker — useful for analytics
|
|
73
|
+
that want to separate sync-call latency from async-queue latency.
|
|
74
|
+
|
|
75
|
+
### 2. Stream the same metadata through `DispatchStream`
|
|
76
|
+
|
|
77
|
+
**File:** [`Ultravisor-API-Server.cjs`](../../source/web_server/Ultravisor-API-Server.cjs)
|
|
78
|
+
|
|
79
|
+
`POST /Beacon/Work/DispatchStream` is the binary-framed variant. It
|
|
80
|
+
calls the coordinator's streaming-dispatch machinery, which also
|
|
81
|
+
builds a work item inline. Same fix pattern: set `QueueMetadata` on
|
|
82
|
+
the work item before the first beacon frame is sent. Look for the
|
|
83
|
+
method that parallels `dispatchAndWait` for streams (grep for
|
|
84
|
+
`_StreamDispatchHandlers`).
|
|
85
|
+
|
|
86
|
+
### 3. Add a `POST /Beacon/Work/Dispatch` body field for `RunID`
|
|
87
|
+
|
|
88
|
+
The sync path doesn't call `/Beacon/Run/Start` before dispatching —
|
|
89
|
+
it's a one-shot. Either:
|
|
90
|
+
|
|
91
|
+
- Accept an optional `RunID` in the request body; if absent, mint one
|
|
92
|
+
on the fly via `RunManager.startRun({ SubmitterTag: 'sync' })` so
|
|
93
|
+
the sync call still lands in `BeaconRun` for observability; **or**
|
|
94
|
+
- Skip run registration for sync calls entirely and emit phases with
|
|
95
|
+
`RunID=""`.
|
|
96
|
+
|
|
97
|
+
The first option keeps the `/queue` UI's event stream complete. Lean
|
|
98
|
+
toward (a).
|
|
99
|
+
|
|
100
|
+
### 4. Accept that retold-labs capability handlers already DTRT
|
|
101
|
+
|
|
102
|
+
Once (1) lands, `Settings.QueueMetadata` is populated on every work
|
|
103
|
+
item regardless of dispatch path.
|
|
104
|
+
[`RetoldLabs-BeaconSetup.cjs`](../../../retold-labs/source/RetoldLabs-BeaconSetup.cjs)
|
|
105
|
+
already checks `pWorkItem.Settings.QueueMetadata` before emitting;
|
|
106
|
+
the sync path will automatically start producing phases with no
|
|
107
|
+
retold-labs changes needed.
|
|
108
|
+
|
|
109
|
+
## Verification
|
|
110
|
+
|
|
111
|
+
1. Issue `POST /Beacon/Work/Dispatch` with `{ Capability: "Shell",
|
|
112
|
+
Action: "Execute", Settings: { Command: "sleep 1" } }`.
|
|
113
|
+
2. The response comes back synchronously (same as today).
|
|
114
|
+
3. In the beacon's staging dir, the resulting `phases.jsonl`
|
|
115
|
+
contains `queue_wait` (duration ≤ 5ms), `worker_spinup`,
|
|
116
|
+
worker-emitted `run_start` / `phase` / `run_end`, and
|
|
117
|
+
`asset_capture`.
|
|
118
|
+
4. The `BeaconRun` table has a new row with `SubmitterTag='sync'`.
|
|
119
|
+
5. The `BeaconWorkItemEvent` table has the `dispatched` event
|
|
120
|
+
with `Payload.Path='sync'` (add this marker in step 1 if you want
|
|
121
|
+
to separate it from poll-path dispatches in analytics).
|
|
122
|
+
|
|
123
|
+
## Non-goals
|
|
124
|
+
|
|
125
|
+
- Retrofitting existing direct-dispatch callers to switch to async
|
|
126
|
+
enqueue. Sync contracts exist for a reason — let callers choose.
|
|
127
|
+
- Differentiating `queue_wait` reason for sync vs. async in the UI.
|
|
128
|
+
0ms queue waits tell the right story on their own.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultravisor",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.24",
|
|
4
4
|
"description": "Cyclic process execution with ai integration.",
|
|
5
5
|
"main": "source/Ultravisor.cjs",
|
|
6
6
|
"bin": {
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"homepage": "https://github.com/stevenvelozo/ultravisor#readme",
|
|
29
29
|
"dependencies": {
|
|
30
|
+
"better-sqlite3": "^11.10.0",
|
|
30
31
|
"cron": "^4.4.0",
|
|
31
32
|
"orator": "^6.0.4",
|
|
32
33
|
"orator-authentication": "^1.0.0",
|
|
@@ -35,6 +36,7 @@
|
|
|
35
36
|
"pict-service-commandlineutility": "^1.0.19",
|
|
36
37
|
"pict-serviceproviderbase": "^1.0.4",
|
|
37
38
|
"ultravisor-beacon": "^0.0.11",
|
|
39
|
+
"ultravisor-file-stream": "^0.0.1",
|
|
38
40
|
"ws": "^8.20.0"
|
|
39
41
|
},
|
|
40
42
|
"devDependencies": {
|
package/source/Ultravisor.cjs
CHANGED
|
@@ -8,6 +8,10 @@ module.exports = (
|
|
|
8
8
|
HypervisorState: require('./services/Ultravisor-Hypervisor-State.cjs'),
|
|
9
9
|
Hypervisor: require('./services/Ultravisor-Hypervisor.cjs'),
|
|
10
10
|
BeaconCoordinator: require('./services/Ultravisor-Beacon-Coordinator.cjs'),
|
|
11
|
+
BeaconQueueStore: require('./services/persistence/Ultravisor-Beacon-QueueStore.cjs'),
|
|
12
|
+
BeaconRunManager: require('./services/Ultravisor-Beacon-RunManager.cjs'),
|
|
13
|
+
BeaconActionDefaults: require('./services/Ultravisor-Beacon-ActionDefaults.cjs'),
|
|
14
|
+
BeaconScheduler: require('./services/Ultravisor-Beacon-Scheduler.cjs'),
|
|
11
15
|
BeaconService: require('ultravisor-beacon'),
|
|
12
16
|
BeaconCapabilityProvider: require('ultravisor-beacon/source/Ultravisor-Beacon-CapabilityProvider.cjs'),
|
|
13
17
|
BeaconProviderRegistry: require('ultravisor-beacon/source/Ultravisor-Beacon-ProviderRegistry.cjs'),
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
{
|
|
2
|
+
"Tables":
|
|
3
|
+
{
|
|
4
|
+
"BeaconRun":
|
|
5
|
+
{
|
|
6
|
+
"TableName": "BeaconRun",
|
|
7
|
+
"Domain": "Default",
|
|
8
|
+
"Description": "Hub-owned run lifecycle record. One row per submitter-visible run; work items link back via RunID.",
|
|
9
|
+
"Columns":
|
|
10
|
+
[
|
|
11
|
+
{ "Column": "IDBeaconRun", "DataType": "ID" },
|
|
12
|
+
{ "Column": "GUIDBeaconRun", "DataType": "GUID", "Size": "36" },
|
|
13
|
+
{ "Column": "CreateDate", "DataType": "DateTime" },
|
|
14
|
+
{ "Column": "CreatingIDUser", "DataType": "Numeric", "Size": "int" },
|
|
15
|
+
{ "Column": "UpdateDate", "DataType": "DateTime" },
|
|
16
|
+
{ "Column": "UpdatingIDUser", "DataType": "Numeric", "Size": "int" },
|
|
17
|
+
{ "Column": "Deleted", "DataType": "Boolean" },
|
|
18
|
+
{ "Column": "DeleteDate", "DataType": "DateTime" },
|
|
19
|
+
{ "Column": "DeletingIDUser", "DataType": "Numeric", "Size": "int" },
|
|
20
|
+
{ "Column": "RunID", "DataType": "String", "Size": "128" },
|
|
21
|
+
{ "Column": "IdempotencyKey", "DataType": "String", "Size": "256" },
|
|
22
|
+
{ "Column": "SubmitterTag", "DataType": "String", "Size": "256" },
|
|
23
|
+
{ "Column": "State", "DataType": "String", "Size": "32" },
|
|
24
|
+
{ "Column": "StartedAt", "DataType": "DateTime" },
|
|
25
|
+
{ "Column": "EndedAt", "DataType": "DateTime" },
|
|
26
|
+
{ "Column": "CanceledAt", "DataType": "DateTime" },
|
|
27
|
+
{ "Column": "CancelReason", "DataType": "String", "Size": "512" },
|
|
28
|
+
{ "Column": "Metadata", "DataType": "String", "Size": "8192" }
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
"BeaconWorkItem":
|
|
32
|
+
{
|
|
33
|
+
"TableName": "BeaconWorkItem",
|
|
34
|
+
"Domain": "Default",
|
|
35
|
+
"Description": "Mutable work item state — one row per dispatchable unit of work. Event history lives in BeaconWorkItemEvent.",
|
|
36
|
+
"Columns":
|
|
37
|
+
[
|
|
38
|
+
{ "Column": "IDBeaconWorkItem", "DataType": "ID" },
|
|
39
|
+
{ "Column": "GUIDBeaconWorkItem", "DataType": "GUID", "Size": "36" },
|
|
40
|
+
{ "Column": "CreateDate", "DataType": "DateTime" },
|
|
41
|
+
{ "Column": "CreatingIDUser", "DataType": "Numeric", "Size": "int" },
|
|
42
|
+
{ "Column": "UpdateDate", "DataType": "DateTime" },
|
|
43
|
+
{ "Column": "UpdatingIDUser", "DataType": "Numeric", "Size": "int" },
|
|
44
|
+
{ "Column": "Deleted", "DataType": "Boolean" },
|
|
45
|
+
{ "Column": "DeleteDate", "DataType": "DateTime" },
|
|
46
|
+
{ "Column": "DeletingIDUser", "DataType": "Numeric", "Size": "int" },
|
|
47
|
+
{ "Column": "WorkItemHash", "DataType": "String", "Size": "256" },
|
|
48
|
+
{ "Column": "RunID", "DataType": "String", "Size": "128" },
|
|
49
|
+
{ "Column": "RunHash", "DataType": "String", "Size": "256" },
|
|
50
|
+
{ "Column": "NodeHash", "DataType": "String", "Size": "256" },
|
|
51
|
+
{ "Column": "OperationHash", "DataType": "String", "Size": "256" },
|
|
52
|
+
{ "Column": "Capability", "DataType": "String", "Size": "128" },
|
|
53
|
+
{ "Column": "Action", "DataType": "String", "Size": "128" },
|
|
54
|
+
{ "Column": "Settings", "DataType": "String", "Size": "65535" },
|
|
55
|
+
{ "Column": "AffinityKey", "DataType": "String", "Size": "256" },
|
|
56
|
+
{ "Column": "Priority", "DataType": "Numeric", "Size": "int" },
|
|
57
|
+
{ "Column": "Status", "DataType": "String", "Size": "32" },
|
|
58
|
+
{ "Column": "AssignedBeaconID", "DataType": "String", "Size": "128" },
|
|
59
|
+
{ "Column": "TimeoutMs", "DataType": "Numeric", "Size": "int" },
|
|
60
|
+
{ "Column": "MaxAttempts", "DataType": "Numeric", "Size": "int" },
|
|
61
|
+
{ "Column": "AttemptNumber", "DataType": "Numeric", "Size": "int" },
|
|
62
|
+
{ "Column": "RetryBackoffMs", "DataType": "Numeric", "Size": "int" },
|
|
63
|
+
{ "Column": "EnqueuedAt", "DataType": "DateTime" },
|
|
64
|
+
{ "Column": "AssignedAt", "DataType": "DateTime" },
|
|
65
|
+
{ "Column": "DispatchedAt", "DataType": "DateTime" },
|
|
66
|
+
{ "Column": "StartedAt", "DataType": "DateTime" },
|
|
67
|
+
{ "Column": "CompletedAt", "DataType": "DateTime" },
|
|
68
|
+
{ "Column": "CanceledAt", "DataType": "DateTime" },
|
|
69
|
+
{ "Column": "CancelRequested", "DataType": "Boolean" },
|
|
70
|
+
{ "Column": "CancelReason", "DataType": "String", "Size": "512" },
|
|
71
|
+
{ "Column": "LastError", "DataType": "String", "Size": "4096" },
|
|
72
|
+
{ "Column": "LastEventAt", "DataType": "DateTime" },
|
|
73
|
+
{ "Column": "QueueWaitMs", "DataType": "Numeric", "Size": "int" },
|
|
74
|
+
{ "Column": "Health", "DataType": "String", "Size": "32" },
|
|
75
|
+
{ "Column": "HealthLabel", "DataType": "String", "Size": "16" },
|
|
76
|
+
{ "Column": "HealthReason", "DataType": "String", "Size": "128" },
|
|
77
|
+
{ "Column": "HealthComputedAt", "DataType": "DateTime" },
|
|
78
|
+
{ "Column": "Result", "DataType": "String", "Size": "65535" }
|
|
79
|
+
]
|
|
80
|
+
},
|
|
81
|
+
"BeaconWorkItemEvent":
|
|
82
|
+
{
|
|
83
|
+
"TableName": "BeaconWorkItemEvent",
|
|
84
|
+
"Domain": "Default",
|
|
85
|
+
"Description": "Append-only audit trail of state transitions for a work item. Each row is one transition.",
|
|
86
|
+
"Columns":
|
|
87
|
+
[
|
|
88
|
+
{ "Column": "IDBeaconWorkItemEvent", "DataType": "ID" },
|
|
89
|
+
{ "Column": "CreateDate", "DataType": "DateTime" },
|
|
90
|
+
{ "Column": "CreatingIDUser", "DataType": "Numeric", "Size": "int" },
|
|
91
|
+
{ "Column": "WorkItemHash", "DataType": "String", "Size": "256" },
|
|
92
|
+
{ "Column": "RunID", "DataType": "String", "Size": "128" },
|
|
93
|
+
{ "Column": "EventType", "DataType": "String", "Size": "32" },
|
|
94
|
+
{ "Column": "FromStatus", "DataType": "String", "Size": "32" },
|
|
95
|
+
{ "Column": "ToStatus", "DataType": "String", "Size": "32" },
|
|
96
|
+
{ "Column": "BeaconID", "DataType": "String", "Size": "128" },
|
|
97
|
+
{ "Column": "Payload", "DataType": "String", "Size": "8192" }
|
|
98
|
+
]
|
|
99
|
+
},
|
|
100
|
+
"BeaconWorkItemAttempt":
|
|
101
|
+
{
|
|
102
|
+
"TableName": "BeaconWorkItemAttempt",
|
|
103
|
+
"Domain": "Default",
|
|
104
|
+
"Description": "One row per execution attempt. On retry a new attempt row is created and WorkItem.AttemptNumber increments.",
|
|
105
|
+
"Columns":
|
|
106
|
+
[
|
|
107
|
+
{ "Column": "IDBeaconWorkItemAttempt", "DataType": "ID" },
|
|
108
|
+
{ "Column": "CreateDate", "DataType": "DateTime" },
|
|
109
|
+
{ "Column": "CreatingIDUser", "DataType": "Numeric", "Size": "int" },
|
|
110
|
+
{ "Column": "UpdateDate", "DataType": "DateTime" },
|
|
111
|
+
{ "Column": "UpdatingIDUser", "DataType": "Numeric", "Size": "int" },
|
|
112
|
+
{ "Column": "WorkItemHash", "DataType": "String", "Size": "256" },
|
|
113
|
+
{ "Column": "AttemptNumber", "DataType": "Numeric", "Size": "int" },
|
|
114
|
+
{ "Column": "BeaconID", "DataType": "String", "Size": "128" },
|
|
115
|
+
{ "Column": "DispatchedAt", "DataType": "DateTime" },
|
|
116
|
+
{ "Column": "StartedAt", "DataType": "DateTime" },
|
|
117
|
+
{ "Column": "CompletedAt", "DataType": "DateTime" },
|
|
118
|
+
{ "Column": "Outcome", "DataType": "String", "Size": "32" },
|
|
119
|
+
{ "Column": "ErrorMessage", "DataType": "String", "Size": "4096" },
|
|
120
|
+
{ "Column": "DurationMs", "DataType": "Numeric", "Size": "int" }
|
|
121
|
+
]
|
|
122
|
+
},
|
|
123
|
+
"BeaconAffinityBinding":
|
|
124
|
+
{
|
|
125
|
+
"TableName": "BeaconAffinityBinding",
|
|
126
|
+
"Domain": "Default",
|
|
127
|
+
"Description": "Affinity lock — routes future work with the same AffinityKey to a specific beacon until TTL expires.",
|
|
128
|
+
"Columns":
|
|
129
|
+
[
|
|
130
|
+
{ "Column": "IDBeaconAffinityBinding", "DataType": "ID" },
|
|
131
|
+
{ "Column": "CreateDate", "DataType": "DateTime" },
|
|
132
|
+
{ "Column": "CreatingIDUser", "DataType": "Numeric", "Size": "int" },
|
|
133
|
+
{ "Column": "UpdateDate", "DataType": "DateTime" },
|
|
134
|
+
{ "Column": "UpdatingIDUser", "DataType": "Numeric", "Size": "int" },
|
|
135
|
+
{ "Column": "AffinityKey", "DataType": "String", "Size": "256" },
|
|
136
|
+
{ "Column": "BeaconID", "DataType": "String", "Size": "128" },
|
|
137
|
+
{ "Column": "ExpiresAt", "DataType": "DateTime" },
|
|
138
|
+
{ "Column": "ClearedAt", "DataType": "DateTime" }
|
|
139
|
+
]
|
|
140
|
+
},
|
|
141
|
+
"BeaconActionDefault":
|
|
142
|
+
{
|
|
143
|
+
"TableName": "BeaconActionDefault",
|
|
144
|
+
"Domain": "Default",
|
|
145
|
+
"Description": "Per-capability/action defaults — timeout, retry, priority, expected-wait baseline. Replaces scattered Fable settings.",
|
|
146
|
+
"Columns":
|
|
147
|
+
[
|
|
148
|
+
{ "Column": "IDBeaconActionDefault", "DataType": "ID" },
|
|
149
|
+
{ "Column": "CreateDate", "DataType": "DateTime" },
|
|
150
|
+
{ "Column": "CreatingIDUser", "DataType": "Numeric", "Size": "int" },
|
|
151
|
+
{ "Column": "UpdateDate", "DataType": "DateTime" },
|
|
152
|
+
{ "Column": "UpdatingIDUser", "DataType": "Numeric", "Size": "int" },
|
|
153
|
+
{ "Column": "Capability", "DataType": "String", "Size": "128" },
|
|
154
|
+
{ "Column": "Action", "DataType": "String", "Size": "128" },
|
|
155
|
+
{ "Column": "TimeoutMs", "DataType": "Numeric", "Size": "int" },
|
|
156
|
+
{ "Column": "MaxAttempts", "DataType": "Numeric", "Size": "int" },
|
|
157
|
+
{ "Column": "RetryBackoffMs", "DataType": "Numeric", "Size": "int" },
|
|
158
|
+
{ "Column": "DefaultPriority", "DataType": "Numeric", "Size": "int" },
|
|
159
|
+
{ "Column": "ExpectedWaitP95Ms", "DataType": "Numeric", "Size": "int" },
|
|
160
|
+
{ "Column": "HeartbeatExpectedMs", "DataType": "Numeric", "Size": "int" },
|
|
161
|
+
{ "Column": "MinSamplesForBaseline", "DataType": "Numeric", "Size": "int" }
|
|
162
|
+
]
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ultravisor Beacon Action Defaults
|
|
3
|
+
*
|
|
4
|
+
* Resolves per-action runtime defaults (timeout, retry policy, priority,
|
|
5
|
+
* expected-wait baseline) from the BeaconActionDefault table. Live
|
|
6
|
+
* config changes land here instead of in Fable settings, which means
|
|
7
|
+
* an operator can tune a slow capability without restarting the hub.
|
|
8
|
+
*
|
|
9
|
+
* Fallback order for any single field: per-request Settings → per-action
|
|
10
|
+
* row → per-capability row (Action="") → Fable setting → hard default.
|
|
11
|
+
* The last two levels are the compatibility shim while we migrate the
|
|
12
|
+
* scattered settings into rows.
|
|
13
|
+
*
|
|
14
|
+
* @module Ultravisor-Beacon-ActionDefaults
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const libPictService = require('pict-serviceproviderbase');
|
|
18
|
+
|
|
19
|
+
const HARD_DEFAULTS = {
|
|
20
|
+
TimeoutMs: 300000,
|
|
21
|
+
MaxAttempts: 1,
|
|
22
|
+
RetryBackoffMs: 5000,
|
|
23
|
+
DefaultPriority: 0,
|
|
24
|
+
ExpectedWaitP95Ms: 0,
|
|
25
|
+
HeartbeatExpectedMs: 60000,
|
|
26
|
+
MinSamplesForBaseline: 20
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
class UltravisorBeaconActionDefaults extends libPictService
|
|
30
|
+
{
|
|
31
|
+
constructor(pPict, pOptions, pServiceHash)
|
|
32
|
+
{
|
|
33
|
+
super(pPict, pOptions, pServiceHash);
|
|
34
|
+
this.serviceType = 'UltravisorBeaconActionDefaults';
|
|
35
|
+
this._Cache = {};
|
|
36
|
+
this._CacheTTLMs = 10000;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
_getStore()
|
|
40
|
+
{
|
|
41
|
+
let tmpMap = this.fable.servicesMap && this.fable.servicesMap.UltravisorBeaconQueueStore;
|
|
42
|
+
if (!tmpMap) return null;
|
|
43
|
+
let tmpStore = Object.values(tmpMap)[0];
|
|
44
|
+
return (tmpStore && tmpStore.isEnabled()) ? tmpStore : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
_cacheKey(pCap, pAction)
|
|
48
|
+
{
|
|
49
|
+
return `${pCap}||${pAction || ''}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
_fableSettingFallback(pKey)
|
|
53
|
+
{
|
|
54
|
+
let tmpSettings = this.fable.settings || {};
|
|
55
|
+
// Legacy settings names kept during the migration period.
|
|
56
|
+
if (pKey === 'TimeoutMs' && tmpSettings.UltravisorBeaconWorkItemTimeoutMs)
|
|
57
|
+
{
|
|
58
|
+
return tmpSettings.UltravisorBeaconWorkItemTimeoutMs;
|
|
59
|
+
}
|
|
60
|
+
if (pKey === 'HeartbeatExpectedMs' && tmpSettings.UltravisorBeaconHeartbeatMs)
|
|
61
|
+
{
|
|
62
|
+
return tmpSettings.UltravisorBeaconHeartbeatMs;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
invalidate()
|
|
68
|
+
{
|
|
69
|
+
this._Cache = {};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
resolve(pCapability, pAction)
|
|
73
|
+
{
|
|
74
|
+
let tmpKey = this._cacheKey(pCapability, pAction);
|
|
75
|
+
let tmpCached = this._Cache[tmpKey];
|
|
76
|
+
let tmpNow = Date.now();
|
|
77
|
+
if (tmpCached && (tmpNow - tmpCached.At) < this._CacheTTLMs)
|
|
78
|
+
{
|
|
79
|
+
return tmpCached.Value;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let tmpStore = this._getStore();
|
|
83
|
+
let tmpSpecific = null;
|
|
84
|
+
let tmpWildcard = null;
|
|
85
|
+
if (tmpStore)
|
|
86
|
+
{
|
|
87
|
+
tmpSpecific = tmpStore.getActionDefault(pCapability, pAction || '');
|
|
88
|
+
if (!tmpSpecific && pAction)
|
|
89
|
+
{
|
|
90
|
+
tmpWildcard = tmpStore.getActionDefault(pCapability, '');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let tmpResolved = {};
|
|
95
|
+
for (let tmpField of Object.keys(HARD_DEFAULTS))
|
|
96
|
+
{
|
|
97
|
+
let tmpVal = null;
|
|
98
|
+
if (tmpSpecific && tmpSpecific[tmpField] != null && tmpSpecific[tmpField] !== 0)
|
|
99
|
+
{
|
|
100
|
+
tmpVal = tmpSpecific[tmpField];
|
|
101
|
+
}
|
|
102
|
+
else if (tmpWildcard && tmpWildcard[tmpField] != null && tmpWildcard[tmpField] !== 0)
|
|
103
|
+
{
|
|
104
|
+
tmpVal = tmpWildcard[tmpField];
|
|
105
|
+
}
|
|
106
|
+
else
|
|
107
|
+
{
|
|
108
|
+
tmpVal = this._fableSettingFallback(tmpField);
|
|
109
|
+
}
|
|
110
|
+
if (tmpVal == null) tmpVal = HARD_DEFAULTS[tmpField];
|
|
111
|
+
tmpResolved[tmpField] = tmpVal;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
this._Cache[tmpKey] = { At: tmpNow, Value: tmpResolved };
|
|
115
|
+
return tmpResolved;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
applyToWorkItem(pWorkItem, pRequestSettings)
|
|
119
|
+
{
|
|
120
|
+
let tmpResolved = this.resolve(pWorkItem.Capability, pWorkItem.Action);
|
|
121
|
+
let tmpSettings = pRequestSettings || {};
|
|
122
|
+
|
|
123
|
+
pWorkItem.TimeoutMs = pWorkItem.TimeoutMs
|
|
124
|
+
|| parseInt(tmpSettings.timeoutMs, 10)
|
|
125
|
+
|| parseInt(tmpSettings.TimeoutMs, 10)
|
|
126
|
+
|| tmpResolved.TimeoutMs;
|
|
127
|
+
|
|
128
|
+
pWorkItem.MaxAttempts = pWorkItem.MaxAttempts
|
|
129
|
+
|| parseInt(tmpSettings.maxRetries, 10)
|
|
130
|
+
|| parseInt(tmpSettings.MaxAttempts, 10)
|
|
131
|
+
|| tmpResolved.MaxAttempts;
|
|
132
|
+
|
|
133
|
+
pWorkItem.RetryBackoffMs = pWorkItem.RetryBackoffMs
|
|
134
|
+
|| parseInt(tmpSettings.retryBackoffMs, 10)
|
|
135
|
+
|| parseInt(tmpSettings.RetryBackoffMs, 10)
|
|
136
|
+
|| tmpResolved.RetryBackoffMs;
|
|
137
|
+
|
|
138
|
+
if (pWorkItem.Priority == null)
|
|
139
|
+
{
|
|
140
|
+
let tmpPri = parseInt(tmpSettings.priority, 10);
|
|
141
|
+
if (isNaN(tmpPri)) tmpPri = parseInt(tmpSettings.Priority, 10);
|
|
142
|
+
if (isNaN(tmpPri)) tmpPri = tmpResolved.DefaultPriority;
|
|
143
|
+
pWorkItem.Priority = tmpPri;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return pWorkItem;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Compute a live p95 estimate from the last N completed samples.
|
|
151
|
+
* Used when no ExpectedWaitP95Ms is set on the row yet. Stored
|
|
152
|
+
* back into the row so future lookups are fast.
|
|
153
|
+
*/
|
|
154
|
+
recomputeWaitBaseline(pCapability, pAction, pMinSamples)
|
|
155
|
+
{
|
|
156
|
+
let tmpStore = this._getStore();
|
|
157
|
+
if (!tmpStore) return null;
|
|
158
|
+
let tmpMin = pMinSamples || HARD_DEFAULTS.MinSamplesForBaseline;
|
|
159
|
+
let tmpSamples = tmpStore.queueWaitSamples(pCapability, pAction || '', 500);
|
|
160
|
+
if (tmpSamples.length < tmpMin) return null;
|
|
161
|
+
tmpSamples.sort((a, b) => a - b);
|
|
162
|
+
let tmpIdx = Math.max(0, Math.floor(tmpSamples.length * 0.95) - 1);
|
|
163
|
+
let tmpP95 = tmpSamples[tmpIdx];
|
|
164
|
+
tmpStore.upsertActionDefault({
|
|
165
|
+
Capability: pCapability,
|
|
166
|
+
Action: pAction || '',
|
|
167
|
+
ExpectedWaitP95Ms: tmpP95
|
|
168
|
+
});
|
|
169
|
+
this.invalidate();
|
|
170
|
+
return tmpP95;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
module.exports = UltravisorBeaconActionDefaults;
|