@hotmeshio/hotmesh 0.25.5 → 0.25.6
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/ISSUE.md +53 -42
- package/RESPONSE-0.25.6.md +77 -0
- package/build/package.json +1 -1
- package/build/services/activities/hook.js +22 -0
- package/build/services/escalations/client.d.ts +13 -7
- package/build/services/escalations/client.js +28 -17
- package/build/services/store/providers/postgres/postgres.d.ts +22 -7
- package/build/services/store/providers/postgres/postgres.js +205 -142
- package/build/services/stream/providers/postgres/kvtables.js +76 -14
- package/build/services/stream/providers/postgres/messages.js +6 -3
- package/build/services/stream/providers/postgres/procedures.js +6 -4
- package/build/types/hmsh_escalations.d.ts +7 -8
- package/package.json +1 -1
- package/wedge-capture-20260706.csv +19 -0
package/ISSUE.md
CHANGED
|
@@ -1,53 +1,64 @@
|
|
|
1
|
-
|
|
1
|
+
## EIGHTH ITEM — a claimed row's post-signal settle can fail, stranding pending state that contradicts the workflow (dashboard-visible)
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Two order trails, same signature (hike-mono, 0.25.5/0.7.5):
|
|
4
|
+
- run A order `a0e68dba`: every station row resolved EXCEPT quality-control —
|
|
5
|
+
yet the QC stationWorker delivered (shipping minted 5s later, order
|
|
6
|
+
COMPLETED). The QC row sits `pending` + claimed forever.
|
|
7
|
+
- run B order `89942462`: identical, at post-print-qa (claimed 07:34:44,
|
|
8
|
+
gluing resolved 07:34:54, order COMPLETED 07:35:11; the qa row still
|
|
9
|
+
pending+claimed 20+ min later).
|
|
5
10
|
|
|
6
|
-
|
|
11
|
+
Trigger context (our side, fixed by us): two associates raced a shallow pond
|
|
12
|
+
and BOTH booked the same order in the same second (audit rows at 07:34:44) —
|
|
13
|
+
so a concurrent claim write was interleaved with the resolve.
|
|
7
14
|
|
|
8
|
-
|
|
9
|
-
|
|
15
|
+
The library-side gap: the resolve path delivered the SIGNAL (workflow
|
|
16
|
+
advanced, next station minted) but the row's own transition to `resolved`
|
|
17
|
+
was lost against the concurrent claim — no error surfaced anywhere. The
|
|
18
|
+
result is dashboard-visible corruption: a live, claimable, countdown-bearing
|
|
19
|
+
work item asserting QA is owed on a COMPLETED order (real associates would
|
|
20
|
+
work it). Whatever branch handled this (atomic signal_key resolve or the
|
|
21
|
+
legacy handle.signal → ltResolveEscalation), signal delivery and row settle
|
|
22
|
+
must be atomic — or the settle must retry until the row cannot lie.
|
|
10
23
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
already scanning the pond and grabbed it instantly).
|
|
15
|
-
- 15+ minutes later — the awaiting child is still `status > 0`, never woke;
|
|
16
|
-
its parent's `Promise.all` wedged with it.
|
|
24
|
+
Repro shape: claim a signal_key escalation from principal A, and within the
|
|
25
|
+
same second re-claim/book from principal B while principal C resolves by
|
|
26
|
+
metadata. Expect: signal delivered AND row resolved, always.
|
|
17
27
|
|
|
18
|
-
|
|
28
|
+
# RE-FILED — two reports lost when this file was replaced with your 0.25.6 reply (both found the same evening, before it)
|
|
19
29
|
|
|
20
|
-
|
|
21
|
-
sub-second after the conditionLT park** (33 ms, 232 ms, ~600 ms). Slow resolves
|
|
22
|
-
(seconds or minutes after park) have never lost a wake across hundreds of
|
|
23
|
-
station-worker resolutions tonight.
|
|
30
|
+
## NINTH ITEM (re-filed seventh) — a timeout wake KILLS the collator of a collated wait set; parent hangs forever
|
|
24
31
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
32
|
+
`Promise.all(21 × condition(signalId, {timeout:'1320s'}))` in one parent
|
|
33
|
+
(hike-mono simulationDirector, 0.25.5). Signal-path delivery through the
|
|
34
|
+
collation worked (seat 18 resolved cleanly at 04:55:05, its collator_waiter
|
|
35
|
+
slot consumed). At 05:07:25 UTC the FIRST batch of SLA expirations landed
|
|
36
|
+
(vm0..vm4 rows flipped 'expired' in the same second) and the collator job
|
|
37
|
+
(`HfSVaQSgUaYu3XGsCCjyQWy$C`) went **status -1 in that same second**
|
|
38
|
+
(context={}, is_live=t). Every subsequent expiry wake then hit
|
|
39
|
+
`Inactive job …$C` at CollatorService.assertJobActive (collator/index.js:13)
|
|
40
|
+
via Hook.processTimeHookEvent (hook.js:650) and was dropped. End state,
|
|
41
|
+
stable 10+ min: 20 live collator_waiter registry rows, all rows 'expired',
|
|
42
|
+
zero pending engine_streams for the collator, parent wedged in Promise.all
|
|
43
|
+
forever with no surfaced error.
|
|
30
44
|
|
|
31
|
-
|
|
45
|
+
Repro: `await Promise.all([condition('k1',{timeout:'30s'}), condition('k2',{timeout:'30s'})])`,
|
|
46
|
+
no resolver — let both expire. Expect collator -1 on first expiry, parent wedged.
|
|
32
47
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
3. Observe whether A ever wakes. In our runs, a same-process consumer beating
|
|
37
|
-
the registration window wedges A reliably enough to hit twice in one evening.
|
|
48
|
+
Our standing mitigation: each timeout-bearing wait moved into its own child
|
|
49
|
+
workflow (inline wait), parent collates ExecChild completions — works, but
|
|
50
|
+
collated waits + SLA should compose.
|
|
38
51
|
|
|
39
|
-
##
|
|
52
|
+
## TENTH ITEM (re-filed follow-up) — second of two near-simultaneous sibling wakes lost (no crash, no restart)
|
|
40
53
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
since 0.25.4) and treat the row's persisted resolver payload as authoritative.
|
|
53
|
-
The parent catches the child's terminate rejection. Ugly but wedge-proof.
|
|
54
|
+
Clean floor, 10 orders: per order, TWO sibling printJobWorkers under one
|
|
55
|
+
dispatcher park signal_key waits; a machine resolves BOTH escalations
|
|
56
|
+
seconds apart. 3-for-3 wedged orders: LEFT worker woke and completed; RIGHT
|
|
57
|
+
worker (resolved moments later) never woke — job row untouched since
|
|
58
|
+
creation, 10+ min, stored-pending-signal path did not recover it. 14/14
|
|
59
|
+
wakes delivered for the 7 orders whose resolves were spread out. Process up
|
|
60
|
+
the whole time (distinct from the reclaim case). Plausibly fixed by commit
|
|
61
|
+
3's atomic wake INSERT ("no interleave window") — flagging so it gets a
|
|
62
|
+
sibling-burst regression test: resolve two waits of one parent within the
|
|
63
|
+
same tick, both must wake. Our mitigation: 0–4s jitter between sibling
|
|
64
|
+
resolves (also more realistic for print hardware).
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# hotmesh 0.25.6 — fixes for the wake-loss and reservation reports (branch: fix/atomic-cancel-wake, three commits)
|
|
2
|
+
|
|
3
|
+
Everything below is implemented and fail-first tested (each fix was watched failing
|
|
4
|
+
before its change); the branch is in final full-suite verification before merge.
|
|
5
|
+
|
|
6
|
+
## Commit 1 — cancellation wakes are durable
|
|
7
|
+
|
|
8
|
+
**Your second addendum (3 of 6 pills slept after the 400ms cancel wave).** Your
|
|
9
|
+
watchers settle waits via `cancel()`, and cancel's wake was a post-commit,
|
|
10
|
+
best-effort publish whose failure was silently discarded. Now the cancellation
|
|
11
|
+
wake commits inside the cancel transaction: a cancel that is durable always
|
|
12
|
+
resumes its `condition()` with `null`. A native six-pill burst test pins your
|
|
13
|
+
wave shape.
|
|
14
|
+
|
|
15
|
+
Also adds a permanent regression test for your converge topology
|
|
16
|
+
(`Promise.all([executeChild x4, sleep branch])`): the sleep's timehook mints and
|
|
17
|
+
the converge settles.
|
|
18
|
+
|
|
19
|
+
## Commit 2 — dead reservations reclaim; resolved waits disarm their timers
|
|
20
|
+
|
|
21
|
+
**Your fifth data point (reservations never reclaimed after process death) — the
|
|
22
|
+
highest-leverage fix.** The fallback poller's discovery query filtered
|
|
23
|
+
`reserved_at IS NULL`, so a dead holder's reservation was invisible forever.
|
|
24
|
+
It now surfaces stale reservations (threshold tracks the configured window;
|
|
25
|
+
heartbeating work never matches), through dedicated index-backed branches, and
|
|
26
|
+
the fixed function re-deploys to existing databases on boot. A crash or deploy
|
|
27
|
+
now costs ≤ ~90 seconds of redelivery latency instead of a permanent wedge —
|
|
28
|
+
and this most plausibly explains your third data point too (wake messages
|
|
29
|
+
claimed by churning per-principal clients that died were never reclaimed).
|
|
30
|
+
|
|
31
|
+
**Your sixth item (armed-timeout drizzle).** When the signal wins an SLA-gated
|
|
32
|
+
wait, the armed timeout is now disarmed with the settled wait — root and cycled
|
|
33
|
+
dimensions both covered. New timers are jid-stamped so job-scoped purges
|
|
34
|
+
(terminate) remove them as well. Timers armed before the upgrade drain as the
|
|
35
|
+
one-shot benign events you observed.
|
|
36
|
+
|
|
37
|
+
Also hardens the pending-signal handoff and cancel wake into single atomic
|
|
38
|
+
statements (safe on pooled connections, no interleave window; an in-window
|
|
39
|
+
wake refreshed by a newer signal delivers the newest payload).
|
|
40
|
+
|
|
41
|
+
## Commit 3 — resolve paths made single-statement; bulk resolve protects waiters
|
|
42
|
+
|
|
43
|
+
- `resolve()` and `resolveByMetadata()` now use the same single-statement
|
|
44
|
+
atomic idiom as cancel: row lock, status guard, and the wake INSERT commit
|
|
45
|
+
as one unit on any connection type.
|
|
46
|
+
- `resolveMany()` now excludes rows backing a live `condition()` waiter — bulk
|
|
47
|
+
resolution carries no wake, so those rows stay pending for a targeted
|
|
48
|
+
`resolve()`/`cancel()` instead of silently stranding the workflow.
|
|
49
|
+
- Secured-mode worker responses stamp `jid`, so job-scoped purges address them.
|
|
50
|
+
|
|
51
|
+
## Status of the remaining items
|
|
52
|
+
|
|
53
|
+
- **Lost `sleep()` timer (your timer forensics):** still not reproduced — your
|
|
54
|
+
exact topology mints and converges cleanly under test, and every registration
|
|
55
|
+
path checks out atomic. The reclaim fix (commit 2) may retire this too if the
|
|
56
|
+
collator's driving message was claimed by a dying client. The decisive
|
|
57
|
+
forensic if it recurs: the wedged director's collator job
|
|
58
|
+
(`SELECT * FROM durable.jobs WHERE key LIKE '%$C'`) plus that collator jid's
|
|
59
|
+
`engine_streams` rows, no filters — it records exactly which item-cycle
|
|
60
|
+
stopped.
|
|
61
|
+
- **`createClient` construction churn (your fourth data point):** your
|
|
62
|
+
per-principal memoization is the right mitigation; an upstream answer
|
|
63
|
+
(internal per-auth memoization or an explicit `client.close()`) is under
|
|
64
|
+
design as its own change.
|
|
65
|
+
- **Crash windows on interrupt's parent notification** (found by our internal
|
|
66
|
+
audit, same family as your wake fixes): being engineered in a dedicated
|
|
67
|
+
follow-up PR — the straightforward implementation measurably slowed interrupt
|
|
68
|
+
dispatch, and interrupts race against live completions by design, so it is
|
|
69
|
+
getting the unhurried version.
|
|
70
|
+
|
|
71
|
+
## After upgrading
|
|
72
|
+
|
|
73
|
+
- Restarting workers drains any orphaned-reservation backlog automatically.
|
|
74
|
+
- The settlement guard can be retired again — cancel and resolve wakes are both
|
|
75
|
+
transactional now, and dead reservations self-heal.
|
|
76
|
+
- Watch for `hook-signal-pending-redelivered` (in-window wake handled) and the
|
|
77
|
+
reclaim redeliveries after any crash: both are the system working as intended.
|
package/build/package.json
CHANGED
|
@@ -575,6 +575,9 @@ class Hook extends activity_1.Activity {
|
|
|
575
575
|
this.context.metadata.jid = jobId;
|
|
576
576
|
this.context.metadata.gid = gId;
|
|
577
577
|
this.context.metadata.dad = dad;
|
|
578
|
+
//captured for the timeout disarm below: processEvent's state
|
|
579
|
+
//hydration replaces context.metadata before the disarm runs
|
|
580
|
+
const waiterJid = jobId;
|
|
578
581
|
// Inline retry for FORBIDDEN: Leg2 arrived in the window between
|
|
579
582
|
// setHookSignal (standalone) and Leg1 transaction.exec(). The 100B
|
|
580
583
|
// ledger digit is not yet visible. Leg1 needs only milliseconds to
|
|
@@ -589,6 +592,25 @@ class Hook extends activity_1.Activity {
|
|
|
589
592
|
await this.processEvent(status, code, 'hook');
|
|
590
593
|
if (code === 200) {
|
|
591
594
|
await taskService.deleteWebHookSignal(this.config.hook.topic, data);
|
|
595
|
+
//signal won the race: disarm the SLA timeout leg (the mirror
|
|
596
|
+
//of the timeout-won path, which deletes the signal). The
|
|
597
|
+
//armed timehook may not survive to fire against the settled
|
|
598
|
+
//wait. Addressed by the SIGNAL's jid + dimensional address —
|
|
599
|
+
//processEvent has since rehydrated this.context.
|
|
600
|
+
if (this.config.sleep) {
|
|
601
|
+
try {
|
|
602
|
+
await this.store.expireTimeHook?.(waiterJid, this.metadata.aid);
|
|
603
|
+
}
|
|
604
|
+
catch (error) {
|
|
605
|
+
//best-effort: a surviving timer fires once and is
|
|
606
|
+
//consumed as an inactive-job event
|
|
607
|
+
this.logger.warn('hook-timeout-disarm-error', {
|
|
608
|
+
jid: waiterJid,
|
|
609
|
+
aid: this.metadata.aid,
|
|
610
|
+
error,
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
}
|
|
592
614
|
//clean up orphan pending on the sibling signal topic
|
|
593
615
|
// wfs.wait delivered → remove wfs.signal pending
|
|
594
616
|
// wfs.signal delivered → remove wfs.wait pending
|
|
@@ -73,9 +73,9 @@ export declare class EscalationClientService {
|
|
|
73
73
|
private _hashConnection;
|
|
74
74
|
private _deliverEscalationSignal;
|
|
75
75
|
/**
|
|
76
|
-
* Builds the wake
|
|
77
|
-
* INSIDE the resolve transaction — the wake becomes durable
|
|
78
|
-
*
|
|
76
|
+
* Builds the wake as a webhook message so the store can commit it
|
|
77
|
+
* INSIDE the resolve/cancel transaction — the wake becomes durable
|
|
78
|
+
* with the status change, closing the crash window between commit and
|
|
79
79
|
* post-commit signal delivery. Mirrors `_deliverEscalationSignal`'s
|
|
80
80
|
* topic fallback chain; returns null when no hook rule is deployed
|
|
81
81
|
* for any candidate topic (the caller then keeps post-commit
|
|
@@ -132,10 +132,10 @@ export declare class EscalationClientService {
|
|
|
132
132
|
/**
|
|
133
133
|
* Cancels a pending escalation and delivers a cancellation signal to the
|
|
134
134
|
* waiting workflow so that `condition()` returns `null`. Terminal rows
|
|
135
|
-
* return `already-terminal`.
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
135
|
+
* return `already-terminal`. The cancellation wake commits inside the
|
|
136
|
+
* cancel transaction (same durability contract as `resolve()`); when no
|
|
137
|
+
* hook rule is deployed for any candidate topic, delivery falls back to
|
|
138
|
+
* a best-effort post-commit publish.
|
|
139
139
|
*/
|
|
140
140
|
cancel(id: string, namespace?: string): Promise<CancelEscalationResult>;
|
|
141
141
|
/**
|
|
@@ -207,6 +207,12 @@ export declare class EscalationClientService {
|
|
|
207
207
|
* (still-pending) row's GIN-indexed `metadata` in the single atomic UPDATE.
|
|
208
208
|
* See {@link resolve}.
|
|
209
209
|
*/
|
|
210
|
+
/**
|
|
211
|
+
* Bulk-resolves standalone escalations (rows with `signal_key = null`).
|
|
212
|
+
* Rows backing a live `condition()` waiter are excluded — they stay
|
|
213
|
+
* pending so a targeted `resolve()`/`cancel()` can deliver their wake;
|
|
214
|
+
* bulk resolution carries no wake and would strand the workflow.
|
|
215
|
+
*/
|
|
210
216
|
resolveMany(params: ResolveManyParams): Promise<EscalationEntry[]>;
|
|
211
217
|
/**
|
|
212
218
|
* Returns dashboard-ready escalation counts. `period` controls the window
|
|
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.EscalationClientService = void 0;
|
|
4
4
|
const enums_1 = require("../../modules/enums");
|
|
5
5
|
const utils_1 = require("../../modules/utils");
|
|
6
|
-
const key_1 = require("../../modules/key");
|
|
7
6
|
const hotmesh_1 = require("../hotmesh");
|
|
8
7
|
const types_1 = require("../../types");
|
|
9
8
|
const stream_1 = require("../../types/stream");
|
|
@@ -148,9 +147,9 @@ class EscalationClientService {
|
|
|
148
147
|
return delivered;
|
|
149
148
|
}
|
|
150
149
|
/**
|
|
151
|
-
* Builds the wake
|
|
152
|
-
* INSIDE the resolve transaction — the wake becomes durable
|
|
153
|
-
*
|
|
150
|
+
* Builds the wake as a webhook message so the store can commit it
|
|
151
|
+
* INSIDE the resolve/cancel transaction — the wake becomes durable
|
|
152
|
+
* with the status change, closing the crash window between commit and
|
|
154
153
|
* post-commit signal delivery. Mirrors `_deliverEscalationSignal`'s
|
|
155
154
|
* topic fallback chain; returns null when no hook rule is deployed
|
|
156
155
|
* for any candidate topic (the caller then keeps post-commit
|
|
@@ -173,13 +172,10 @@ class EscalationClientService {
|
|
|
173
172
|
metadata: { guid: (0, utils_1.guid)(), aid, topic: candidate },
|
|
174
173
|
data: { id: signalKey, data },
|
|
175
174
|
};
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
JSON.stringify(streamData),
|
|
181
|
-
]);
|
|
182
|
-
return { forSignalKey: signalKey, sql, params };
|
|
175
|
+
return {
|
|
176
|
+
forSignalKey: signalKey,
|
|
177
|
+
message: JSON.stringify(streamData),
|
|
178
|
+
};
|
|
183
179
|
}
|
|
184
180
|
catch {
|
|
185
181
|
/* candidate not deployed — try the next topic */
|
|
@@ -286,18 +282,27 @@ class EscalationClientService {
|
|
|
286
282
|
/**
|
|
287
283
|
* Cancels a pending escalation and delivers a cancellation signal to the
|
|
288
284
|
* waiting workflow so that `condition()` returns `null`. Terminal rows
|
|
289
|
-
* return `already-terminal`.
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
285
|
+
* return `already-terminal`. The cancellation wake commits inside the
|
|
286
|
+
* cancel transaction (same durability contract as `resolve()`); when no
|
|
287
|
+
* hook rule is deployed for any candidate topic, delivery falls back to
|
|
288
|
+
* a best-effort post-commit publish.
|
|
293
289
|
*/
|
|
294
290
|
async cancel(id, namespace) {
|
|
295
291
|
const ns = namespace ?? factory_1.APP_ID;
|
|
296
292
|
const hm = await this._engine(null, ns);
|
|
297
|
-
const
|
|
293
|
+
const store = hm.engine.store;
|
|
294
|
+
//pre-build the cancellation wake so it commits INSIDE the cancel
|
|
295
|
+
//transaction — condition() resumes with null even if the process
|
|
296
|
+
//dies the instant the cancel commits
|
|
297
|
+
let wakeCommand = null;
|
|
298
|
+
const preview = await store.getEscalation(id, namespace);
|
|
299
|
+
if (preview?.signal_key) {
|
|
300
|
+
wakeCommand = await this._buildWakeCommand(ns, preview.topic, preview.signal_key, { __escalation_cancelled: true });
|
|
301
|
+
}
|
|
302
|
+
const result = await store.cancelEscalation(id, namespace, wakeCommand ?? undefined);
|
|
298
303
|
if (result.ok === true) {
|
|
299
304
|
this._emit('cancelled', result.entry);
|
|
300
|
-
if (result.entry.signal_key) {
|
|
305
|
+
if (result.entry.signal_key && !result.wakeEnqueued) {
|
|
301
306
|
await this._deliverEscalationSignal(ns, result.entry.topic, {
|
|
302
307
|
id: result.entry.signal_key,
|
|
303
308
|
data: { __escalation_cancelled: true },
|
|
@@ -444,6 +449,12 @@ class EscalationClientService {
|
|
|
444
449
|
* (still-pending) row's GIN-indexed `metadata` in the single atomic UPDATE.
|
|
445
450
|
* See {@link resolve}.
|
|
446
451
|
*/
|
|
452
|
+
/**
|
|
453
|
+
* Bulk-resolves standalone escalations (rows with `signal_key = null`).
|
|
454
|
+
* Rows backing a live `condition()` waiter are excluded — they stay
|
|
455
|
+
* pending so a targeted `resolve()`/`cancel()` can deliver their wake;
|
|
456
|
+
* bulk resolution carries no wake and would strand the workflow.
|
|
457
|
+
*/
|
|
447
458
|
async resolveMany(params) {
|
|
448
459
|
const hm = await this._engine(null, params.namespace);
|
|
449
460
|
const entries = await hm.engine.store.resolveManyEscalations(params);
|
|
@@ -162,6 +162,18 @@ declare class PostgresStoreService extends StoreService<ProviderClient, Provider
|
|
|
162
162
|
* picks it up — no intermediate table, no polling, fully transactional.
|
|
163
163
|
*/
|
|
164
164
|
registerTimeHook(jobId: string, gId: string, activityId: string, _type: WorkListTaskType, deletionTime: number, dad: string, transaction?: ProviderTransaction): Promise<void>;
|
|
165
|
+
/**
|
|
166
|
+
* Disarms a scheduled timehook (soft delete) for one activity of a
|
|
167
|
+
* job — the mirror of registerTimeHook, called when the SIGNAL wins
|
|
168
|
+
* an SLA-gated wait so the armed timeout cannot fire against the
|
|
169
|
+
* settled workflow. Scoped by the jid index, then narrowed to the
|
|
170
|
+
* activity via the message metadata. Dimensional addressing is
|
|
171
|
+
* deliberately NOT matched: a job's waits on one activity are
|
|
172
|
+
* sequential (cycle N settles before cycle N+1 arms), so at most one
|
|
173
|
+
* timer per (jid, aid) is armed at a time, and the signal composite's
|
|
174
|
+
* address can differ from the stored one at cycle offsets.
|
|
175
|
+
*/
|
|
176
|
+
expireTimeHook(jobId: string, activityId: string): Promise<number>;
|
|
165
177
|
getNextTask(_listKey?: string): Promise<[
|
|
166
178
|
listKey: string,
|
|
167
179
|
jobId: string,
|
|
@@ -274,13 +286,14 @@ declare class PostgresStoreService extends StoreService<ProviderClient, Provider
|
|
|
274
286
|
claimEscalationByMetadata(params: import('../../../../types/hmsh_escalations').ClaimByMetadataParams): Promise<import('../../../../types/hmsh_escalations').ClaimByMetadataResult>;
|
|
275
287
|
releaseEscalation(params: import('../../../../types/hmsh_escalations').ReleaseEscalationParams): Promise<import('../../../../types/hmsh_escalations').ReleaseEscalationResult>;
|
|
276
288
|
/**
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
289
|
+
* Composes the wake as a data-modifying CTE appended to a settle
|
|
290
|
+
* statement (resolve/cancel). The INSERT fires iff the settle CTE
|
|
291
|
+
* (`fromCTE`) produced a row whose signal_key matches the wake — one
|
|
292
|
+
* atomic statement, safe on any connection type (Client or Pool),
|
|
293
|
+
* with no transaction window for unrelated queries to interleave
|
|
294
|
+
* into. Appends the wake params to `params` in place.
|
|
282
295
|
*/
|
|
283
|
-
private
|
|
296
|
+
private composeEscalationWakeCTE;
|
|
284
297
|
resolveEscalation(params: import('../../../../types/hmsh_escalations').ResolveEscalationParams, wakeCommand?: import('../../../../types/hmsh_escalations').EscalationWakeCommand): Promise<import('../../../../types/hmsh_escalations').ResolveEscalationResult & {
|
|
285
298
|
signalKey?: string | null;
|
|
286
299
|
topic?: string | null;
|
|
@@ -302,7 +315,9 @@ declare class PostgresStoreService extends StoreService<ProviderClient, Provider
|
|
|
302
315
|
topic?: string | null;
|
|
303
316
|
wakeEnqueued?: boolean;
|
|
304
317
|
}>;
|
|
305
|
-
cancelEscalation(id: string, namespace?: string): Promise<import('../../../../types/hmsh_escalations').CancelEscalationResult
|
|
318
|
+
cancelEscalation(id: string, namespace?: string, wakeCommand?: import('../../../../types/hmsh_escalations').EscalationWakeCommand): Promise<import('../../../../types/hmsh_escalations').CancelEscalationResult & {
|
|
319
|
+
wakeEnqueued?: boolean;
|
|
320
|
+
}>;
|
|
306
321
|
escalateEscalationToRole(params: import('../../../../types/hmsh_escalations').EscalateToRoleParams): Promise<import('../../../../types/hmsh_escalations').EscalationEntry | null>;
|
|
307
322
|
updateEscalation(params: import('../../../../types/hmsh_escalations').UpdateEscalationParams): Promise<import('../../../../types/hmsh_escalations').EscalationEntry | null>;
|
|
308
323
|
appendEscalationMilestones(params: import('../../../../types/hmsh_escalations').AppendMilestonesParams): Promise<import('../../../../types/hmsh_escalations').EscalationEntry | null>;
|
|
@@ -814,43 +814,52 @@ class PostgresStoreService extends __1.StoreService {
|
|
|
814
814
|
const wasInsert = lockResult.rows[0]?.inserted;
|
|
815
815
|
const isPending = captured?.startsWith('$pending::');
|
|
816
816
|
if (isPending && redelivery) {
|
|
817
|
-
//consume the marker and commit its redelivery as ONE
|
|
818
|
-
//pending wake becomes a durable engine stream message in
|
|
819
|
-
//same
|
|
820
|
-
//survives a crash at any instant.
|
|
821
|
-
//
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
metadata: {
|
|
828
|
-
guid: (0, utils_1.guid)(),
|
|
829
|
-
aid: redelivery.aid,
|
|
830
|
-
topic: redelivery.topic,
|
|
831
|
-
},
|
|
832
|
-
data: JSON.parse(pendingData),
|
|
833
|
-
});
|
|
817
|
+
//consume the marker and commit its redelivery as ONE statement:
|
|
818
|
+
//the pending wake becomes a durable engine stream message in
|
|
819
|
+
//the same atomic unit that destroys the marker, so the wake
|
|
820
|
+
//survives a crash at any instant. Single-statement composition
|
|
821
|
+
//is safe on any connection type (Client or Pool) and cannot
|
|
822
|
+
//interleave with unrelated queries. The consume is guarded on
|
|
823
|
+
//the value still being a pending marker, and the redelivered
|
|
824
|
+
//payload is read from the row itself — a marker refreshed by a
|
|
825
|
+
//newer signal between capture and consume delivers the NEWER
|
|
826
|
+
//payload rather than a stale copy.
|
|
834
827
|
const schemaName = this.kvsql().safeName(this.appId);
|
|
835
|
-
await this.pgClient.query(
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
828
|
+
const res = await this.pgClient.query(`WITH prev AS (
|
|
829
|
+
SELECT value FROM ${tableName}
|
|
830
|
+
WHERE key = $1 FOR UPDATE
|
|
831
|
+
),
|
|
832
|
+
consumed AS (
|
|
833
|
+
UPDATE ${tableName} t
|
|
834
|
+
SET value = $2, expiry = NOW() + INTERVAL '${delay} seconds'
|
|
835
|
+
FROM prev
|
|
836
|
+
WHERE t.key = $1 AND prev.value LIKE '$pending::%'
|
|
837
|
+
RETURNING prev.value AS pending_value
|
|
838
|
+
)
|
|
839
|
+
INSERT INTO ${schemaName}.engine_streams
|
|
841
840
|
(stream_name, message, priority)
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
841
|
+
SELECT $3,
|
|
842
|
+
json_build_object(
|
|
843
|
+
'type', 'webhook',
|
|
844
|
+
'status', 'success',
|
|
845
|
+
'code', 200,
|
|
846
|
+
'metadata', json_build_object(
|
|
847
|
+
'guid', $4::text, 'aid', $5::text, 'topic', $6::text),
|
|
848
|
+
'data', substr(consumed.pending_value, 11)::json
|
|
849
|
+
)::text,
|
|
850
|
+
5
|
|
851
|
+
FROM consumed
|
|
852
|
+
RETURNING id`, [storedKey, jobId, this.appId, (0, utils_1.guid)(), redelivery.aid, redelivery.topic]);
|
|
853
|
+
if ((res.rowCount ?? 0) > 0) {
|
|
854
|
+
this.logger.warn('hook-signal-pending-redelivered', {
|
|
855
|
+
key: signalKey,
|
|
856
|
+
topic: redelivery.topic,
|
|
857
|
+
});
|
|
858
|
+
return { success: true };
|
|
848
859
|
}
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
});
|
|
853
|
-
return { success: true };
|
|
860
|
+
//the marker vanished between capture and consume (a concurrent
|
|
861
|
+
//duplicate Leg1 consumed it) — idempotent no-op
|
|
862
|
+
return { success: false };
|
|
854
863
|
}
|
|
855
864
|
if (!wasInsert) {
|
|
856
865
|
//step 2: row existed — overwrite with hook value
|
|
@@ -1022,10 +1031,12 @@ class PostgresStoreService extends __1.StoreService {
|
|
|
1022
1031
|
metadata: { guid: (0, utils_1.guid)(), jid: jobId, gid: gId, dad: msgDad, aid },
|
|
1023
1032
|
data: { timestamp: Date.now() },
|
|
1024
1033
|
});
|
|
1034
|
+
//jid column stamped so job-scoped expiry (interrupt purge, timeout
|
|
1035
|
+
//disarm) can address armed timers through the partial jid index
|
|
1025
1036
|
const sql = `INSERT INTO ${schemaName}.engine_streams
|
|
1026
|
-
(stream_name, message, priority, visible_at)
|
|
1027
|
-
VALUES ($1, $2, 5, NOW() + INTERVAL '${Math.max(delayMs, 0)} milliseconds')`;
|
|
1028
|
-
const params = [this.appId, message];
|
|
1037
|
+
(stream_name, jid, message, priority, visible_at)
|
|
1038
|
+
VALUES ($1, $2, $3, 5, NOW() + INTERVAL '${Math.max(delayMs, 0)} milliseconds')`;
|
|
1039
|
+
const params = [this.appId, jobId, message];
|
|
1029
1040
|
if (transaction && typeof transaction.addCommand === 'function') {
|
|
1030
1041
|
transaction.addCommand(sql, params);
|
|
1031
1042
|
}
|
|
@@ -1033,6 +1044,26 @@ class PostgresStoreService extends __1.StoreService {
|
|
|
1033
1044
|
await this.pgClient.query(sql, params);
|
|
1034
1045
|
}
|
|
1035
1046
|
}
|
|
1047
|
+
/**
|
|
1048
|
+
* Disarms a scheduled timehook (soft delete) for one activity of a
|
|
1049
|
+
* job — the mirror of registerTimeHook, called when the SIGNAL wins
|
|
1050
|
+
* an SLA-gated wait so the armed timeout cannot fire against the
|
|
1051
|
+
* settled workflow. Scoped by the jid index, then narrowed to the
|
|
1052
|
+
* activity via the message metadata. Dimensional addressing is
|
|
1053
|
+
* deliberately NOT matched: a job's waits on one activity are
|
|
1054
|
+
* sequential (cycle N settles before cycle N+1 arms), so at most one
|
|
1055
|
+
* timer per (jid, aid) is armed at a time, and the signal composite's
|
|
1056
|
+
* address can differ from the stored one at cycle offsets.
|
|
1057
|
+
*/
|
|
1058
|
+
async expireTimeHook(jobId, activityId) {
|
|
1059
|
+
const schemaName = this.kvsql().safeName(this.appId);
|
|
1060
|
+
const res = await this.pgClient.query(`UPDATE ${schemaName}.engine_streams
|
|
1061
|
+
SET expired_at = NOW()
|
|
1062
|
+
WHERE jid = $1 AND expired_at IS NULL AND visible_at > NOW()
|
|
1063
|
+
AND (message::jsonb ->> 'type') = 'timehook'
|
|
1064
|
+
AND (message::jsonb -> 'metadata' ->> 'aid') = $2`, [jobId, activityId]);
|
|
1065
|
+
return res.rowCount ?? 0;
|
|
1066
|
+
}
|
|
1036
1067
|
async getNextTask(_listKey) {
|
|
1037
1068
|
//no-op: time hooks are now engine_streams messages with future visible_at.
|
|
1038
1069
|
//the engine's normal dequeue handles them when they become visible.
|
|
@@ -1832,85 +1863,95 @@ class PostgresStoreService extends __1.StoreService {
|
|
|
1832
1863
|
return { ok: true, entry: row.entry_json };
|
|
1833
1864
|
}
|
|
1834
1865
|
/**
|
|
1835
|
-
*
|
|
1836
|
-
*
|
|
1837
|
-
*
|
|
1838
|
-
*
|
|
1839
|
-
*
|
|
1866
|
+
* Composes the wake as a data-modifying CTE appended to a settle
|
|
1867
|
+
* statement (resolve/cancel). The INSERT fires iff the settle CTE
|
|
1868
|
+
* (`fromCTE`) produced a row whose signal_key matches the wake — one
|
|
1869
|
+
* atomic statement, safe on any connection type (Client or Pool),
|
|
1870
|
+
* with no transaction window for unrelated queries to interleave
|
|
1871
|
+
* into. Appends the wake params to `params` in place.
|
|
1840
1872
|
*/
|
|
1841
|
-
|
|
1842
|
-
if (!wakeCommand
|
|
1843
|
-
return
|
|
1844
|
-
}
|
|
1845
|
-
await this.pgClient.query('SAVEPOINT escalation_wake');
|
|
1846
|
-
try {
|
|
1847
|
-
await this.pgClient.query(wakeCommand.sql, wakeCommand.params);
|
|
1848
|
-
return true;
|
|
1849
|
-
}
|
|
1850
|
-
catch (error) {
|
|
1851
|
-
await this.pgClient.query('ROLLBACK TO SAVEPOINT escalation_wake');
|
|
1852
|
-
this.logger.warn('escalation-wake-enqueue-error', {
|
|
1853
|
-
escalationId,
|
|
1854
|
-
error: error.message,
|
|
1855
|
-
});
|
|
1856
|
-
return false;
|
|
1873
|
+
composeEscalationWakeCTE(wakeCommand, params, fromCTE) {
|
|
1874
|
+
if (!wakeCommand) {
|
|
1875
|
+
return { wakeCTE: '', wakeCount: '0::int AS wake_count' };
|
|
1857
1876
|
}
|
|
1877
|
+
const schemaName = this.kvsql().safeName(this.appId);
|
|
1878
|
+
const base = params.length;
|
|
1879
|
+
params.push(this.appId, wakeCommand.message, wakeCommand.forSignalKey);
|
|
1880
|
+
return {
|
|
1881
|
+
wakeCTE: `,
|
|
1882
|
+
wake AS (
|
|
1883
|
+
INSERT INTO ${schemaName}.engine_streams (stream_name, message, priority)
|
|
1884
|
+
SELECT $${base + 1}, $${base + 2}, 5 FROM ${fromCTE}
|
|
1885
|
+
WHERE ${fromCTE}.signal_key = $${base + 3}
|
|
1886
|
+
RETURNING id
|
|
1887
|
+
)`,
|
|
1888
|
+
wakeCount: '(SELECT COUNT(*) FROM wake)::int AS wake_count',
|
|
1889
|
+
};
|
|
1858
1890
|
}
|
|
1859
1891
|
async resolveEscalation(params, wakeCommand) {
|
|
1860
1892
|
const { id, namespace, resolverPayload, metadata } = params;
|
|
1861
1893
|
const payloadJson = resolverPayload ? JSON.stringify(resolverPayload) : null;
|
|
1862
|
-
// metaJson is bound at a fixed index; the CASE no-ops when null so resolution
|
|
1863
|
-
// can merge into the GIN-indexed metadata without disturbing the param layout.
|
|
1864
1894
|
const metaJson = metadata ? JSON.stringify(metadata) : null;
|
|
1865
|
-
//
|
|
1866
|
-
//
|
|
1867
|
-
//
|
|
1868
|
-
//
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1895
|
+
//single statement: FOR UPDATE lock, TOCTOU-guarded UPDATE, and the
|
|
1896
|
+
//wake INSERT are one atomic unit. The wake commits WITH the
|
|
1897
|
+
//resolve — a crash after the statement leaves both the resolved row
|
|
1898
|
+
//and its wake message durable; before, neither. The engine_streams
|
|
1899
|
+
//INSERT trigger emits the delivery NOTIFY on commit. Safe on any
|
|
1900
|
+
//connection type; no window for unrelated queries to interleave.
|
|
1901
|
+
const sqlParams = namespace
|
|
1902
|
+
? [id, payloadJson, metaJson, namespace]
|
|
1903
|
+
: [id, payloadJson, metaJson];
|
|
1904
|
+
const { wakeCTE, wakeCount } = this.composeEscalationWakeCTE(wakeCommand, sqlParams, 'resolved');
|
|
1905
|
+
const result = await this.pgClient.query(`
|
|
1906
|
+
WITH target AS MATERIALIZED (
|
|
1907
|
+
SELECT id, signal_key, topic, status FROM public.hmsh_escalations
|
|
1908
|
+
WHERE id = $1 ${namespace ? 'AND namespace = $4' : ''}
|
|
1909
|
+
LIMIT 1 FOR UPDATE
|
|
1910
|
+
),
|
|
1911
|
+
resolved AS (
|
|
1912
|
+
UPDATE public.hmsh_escalations e
|
|
1913
|
+
SET status = 'resolved', resolved_at = NOW(), resolver_payload = $2,
|
|
1914
|
+
metadata = CASE WHEN $3::jsonb IS NOT NULL
|
|
1915
|
+
THEN COALESCE(e.metadata, '{}'::jsonb) || $3::jsonb
|
|
1916
|
+
ELSE e.metadata END,
|
|
1917
|
+
updated_at = NOW()
|
|
1918
|
+
FROM target
|
|
1919
|
+
WHERE e.id = target.id AND target.status = 'pending'
|
|
1920
|
+
RETURNING e.*
|
|
1921
|
+
)${wakeCTE}
|
|
1922
|
+
SELECT t.id, t.status AS prior_status, t.signal_key, t.topic,
|
|
1923
|
+
CASE
|
|
1924
|
+
WHEN r.id IS NOT NULL THEN 'resolved'
|
|
1925
|
+
WHEN t.id IS NULL THEN 'not-found'
|
|
1926
|
+
ELSE 'blocked'
|
|
1927
|
+
END AS outcome,
|
|
1928
|
+
row_to_json(r.*) AS entry_json,
|
|
1929
|
+
${wakeCount}
|
|
1930
|
+
FROM (SELECT * FROM target) t
|
|
1931
|
+
FULL OUTER JOIN (SELECT * FROM resolved) r ON r.id = t.id
|
|
1932
|
+
`, sqlParams);
|
|
1933
|
+
const row = result.rows[0];
|
|
1934
|
+
if (!row || row.outcome === 'not-found')
|
|
1935
|
+
return { ok: false, reason: 'not-found' };
|
|
1936
|
+
if (row.outcome === 'blocked') {
|
|
1937
|
+
if (row.prior_status === 'cancelled')
|
|
1880
1938
|
return { ok: false, reason: 'already-cancelled' };
|
|
1881
|
-
|
|
1882
|
-
if (status === 'expired') {
|
|
1939
|
+
if (row.prior_status === 'expired') {
|
|
1883
1940
|
// The wait's SLA timer fired first and the workflow resumed with
|
|
1884
1941
|
// false — the resolver payload has nowhere to go. Name it, so the
|
|
1885
1942
|
// operator learns the deadline passed rather than believing the
|
|
1886
1943
|
// resolution landed.
|
|
1887
|
-
await this.pgClient.query('ROLLBACK');
|
|
1888
1944
|
return { ok: false, reason: 'already-expired' };
|
|
1889
1945
|
}
|
|
1890
|
-
|
|
1891
|
-
SET status = 'resolved', resolved_at = NOW(), resolver_payload = $2,
|
|
1892
|
-
metadata = CASE WHEN $3::jsonb IS NOT NULL
|
|
1893
|
-
THEN COALESCE(metadata, '{}'::jsonb) || $3::jsonb
|
|
1894
|
-
ELSE metadata END,
|
|
1895
|
-
updated_at = NOW()
|
|
1896
|
-
WHERE id = $1 ${namespace ? 'AND namespace = $4' : ''} AND status = 'pending'
|
|
1897
|
-
RETURNING *`, namespace ? [id, payloadJson, metaJson, namespace] : [id, payloadJson, metaJson]);
|
|
1898
|
-
if (!updateResult.rows[0]) {
|
|
1899
|
-
await this.pgClient.query('ROLLBACK');
|
|
1900
|
-
return { ok: false, reason: 'already-resolved' };
|
|
1901
|
-
}
|
|
1902
|
-
//the wake commits WITH the resolve: a crash after COMMIT leaves both
|
|
1903
|
-
//the resolved row and its wake message durable; a crash before
|
|
1904
|
-
//leaves neither. The engine_streams INSERT trigger emits the
|
|
1905
|
-
//delivery NOTIFY on commit.
|
|
1906
|
-
const wakeEnqueued = await this.enqueueEscalationWake(wakeCommand, signal_key, id);
|
|
1907
|
-
await this.pgClient.query('COMMIT');
|
|
1908
|
-
return { ok: true, entry: updateResult.rows[0], signalKey: signal_key, topic, wakeEnqueued };
|
|
1909
|
-
}
|
|
1910
|
-
catch (e) {
|
|
1911
|
-
await this.pgClient.query('ROLLBACK');
|
|
1912
|
-
throw e;
|
|
1946
|
+
return { ok: false, reason: 'already-resolved' };
|
|
1913
1947
|
}
|
|
1948
|
+
return {
|
|
1949
|
+
ok: true,
|
|
1950
|
+
entry: row.entry_json,
|
|
1951
|
+
signalKey: row.signal_key,
|
|
1952
|
+
topic: row.topic,
|
|
1953
|
+
wakeEnqueued: row.wake_count > 0,
|
|
1954
|
+
};
|
|
1914
1955
|
}
|
|
1915
1956
|
/**
|
|
1916
1957
|
* Non-locking preview of the row `resolveEscalationByMetadata` would
|
|
@@ -1938,46 +1979,66 @@ class PostgresStoreService extends __1.StoreService {
|
|
|
1938
1979
|
const filter = JSON.stringify({ [key]: value });
|
|
1939
1980
|
const payloadJson = resolverPayload ? JSON.stringify(resolverPayload) : null;
|
|
1940
1981
|
const metaJson = metadata ? JSON.stringify(metadata) : null;
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1982
|
+
//single statement — see resolveEscalation for the atomicity contract
|
|
1983
|
+
const sqlParams = namespace
|
|
1984
|
+
? [filter, roles ?? null, payloadJson, metaJson, namespace]
|
|
1985
|
+
: [filter, roles ?? null, payloadJson, metaJson];
|
|
1986
|
+
const { wakeCTE, wakeCount } = this.composeEscalationWakeCTE(wakeCommand, sqlParams, 'resolved');
|
|
1987
|
+
const result = await this.pgClient.query(`
|
|
1988
|
+
WITH target AS MATERIALIZED (
|
|
1989
|
+
SELECT id, signal_key, topic, status FROM public.hmsh_escalations
|
|
1990
|
+
WHERE ${namespace ? 'namespace = $5 AND' : ''}
|
|
1991
|
+
metadata @> $1::jsonb
|
|
1992
|
+
AND ($2::text[] IS NULL OR role = ANY($2::text[]))
|
|
1993
|
+
AND status IN ('pending', 'cancelled')
|
|
1994
|
+
ORDER BY priority ASC, created_at ASC
|
|
1995
|
+
LIMIT 1 FOR UPDATE
|
|
1996
|
+
),
|
|
1997
|
+
resolved AS (
|
|
1998
|
+
UPDATE public.hmsh_escalations e
|
|
1999
|
+
SET status = 'resolved', resolved_at = NOW(), resolver_payload = $3,
|
|
2000
|
+
metadata = CASE WHEN $4::jsonb IS NOT NULL
|
|
2001
|
+
THEN COALESCE(e.metadata, '{}'::jsonb) || $4::jsonb
|
|
2002
|
+
ELSE e.metadata END,
|
|
2003
|
+
updated_at = NOW()
|
|
2004
|
+
FROM target
|
|
2005
|
+
WHERE e.id = target.id AND target.status = 'pending'
|
|
2006
|
+
RETURNING e.*
|
|
2007
|
+
)${wakeCTE}
|
|
2008
|
+
SELECT t.id, t.status AS prior_status, t.signal_key, t.topic,
|
|
2009
|
+
CASE
|
|
2010
|
+
WHEN r.id IS NOT NULL THEN 'resolved'
|
|
2011
|
+
WHEN t.id IS NULL THEN 'not-found'
|
|
2012
|
+
ELSE 'blocked'
|
|
2013
|
+
END AS outcome,
|
|
2014
|
+
row_to_json(r.*) AS entry_json,
|
|
2015
|
+
${wakeCount}
|
|
2016
|
+
FROM (SELECT * FROM target) t
|
|
2017
|
+
FULL OUTER JOIN (SELECT * FROM resolved) r ON r.id = t.id
|
|
2018
|
+
`, sqlParams);
|
|
2019
|
+
const row = result.rows[0];
|
|
2020
|
+
if (!row || row.outcome === 'not-found')
|
|
2021
|
+
return { ok: false, reason: 'not-found' };
|
|
2022
|
+
if (row.outcome === 'blocked') {
|
|
2023
|
+
if (row.prior_status === 'cancelled')
|
|
1957
2024
|
return { ok: false, reason: 'already-cancelled' };
|
|
1958
|
-
}
|
|
1959
|
-
const updateResult = await this.pgClient.query(`UPDATE public.hmsh_escalations
|
|
1960
|
-
SET status = 'resolved', resolved_at = NOW(), resolver_payload = $2,
|
|
1961
|
-
metadata = CASE WHEN $3::jsonb IS NOT NULL
|
|
1962
|
-
THEN COALESCE(metadata, '{}'::jsonb) || $3::jsonb
|
|
1963
|
-
ELSE metadata END,
|
|
1964
|
-
updated_at = NOW()
|
|
1965
|
-
WHERE id = $1 AND status = 'pending'
|
|
1966
|
-
RETURNING *`, [id, payloadJson, metaJson]);
|
|
1967
|
-
if (!updateResult.rows[0]) {
|
|
1968
|
-
await this.pgClient.query('ROLLBACK');
|
|
1969
|
-
return { ok: false, reason: 'already-resolved' };
|
|
1970
|
-
}
|
|
1971
|
-
const wakeEnqueued = await this.enqueueEscalationWake(wakeCommand, signal_key, id);
|
|
1972
|
-
await this.pgClient.query('COMMIT');
|
|
1973
|
-
return { ok: true, entry: updateResult.rows[0], signalKey: signal_key, topic, wakeEnqueued };
|
|
1974
|
-
}
|
|
1975
|
-
catch (e) {
|
|
1976
|
-
await this.pgClient.query('ROLLBACK');
|
|
1977
|
-
throw e;
|
|
2025
|
+
return { ok: false, reason: 'already-resolved' };
|
|
1978
2026
|
}
|
|
2027
|
+
return {
|
|
2028
|
+
ok: true,
|
|
2029
|
+
entry: row.entry_json,
|
|
2030
|
+
signalKey: row.signal_key,
|
|
2031
|
+
topic: row.topic,
|
|
2032
|
+
wakeEnqueued: row.wake_count > 0,
|
|
2033
|
+
};
|
|
1979
2034
|
}
|
|
1980
|
-
async cancelEscalation(id, namespace) {
|
|
2035
|
+
async cancelEscalation(id, namespace, wakeCommand) {
|
|
2036
|
+
//single statement: the cancellation wake is a data-modifying CTE
|
|
2037
|
+
//that fires iff the cancel lands AND the row's signal_key matches
|
|
2038
|
+
//the wake — atomic on any connection type (Client or Pool), with
|
|
2039
|
+
//no BEGIN/COMMIT window for unrelated queries to interleave into
|
|
2040
|
+
const params = namespace ? [id, namespace] : [id];
|
|
2041
|
+
const { wakeCTE, wakeCount } = this.composeEscalationWakeCTE(wakeCommand, params, 'cancelled');
|
|
1981
2042
|
const result = await this.pgClient.query(`
|
|
1982
2043
|
WITH target AS MATERIALIZED (
|
|
1983
2044
|
SELECT id, status FROM public.hmsh_escalations
|
|
@@ -1991,23 +2052,24 @@ class PostgresStoreService extends __1.StoreService {
|
|
|
1991
2052
|
WHERE public.hmsh_escalations.id = target.id
|
|
1992
2053
|
AND target.status = 'pending'
|
|
1993
2054
|
RETURNING public.hmsh_escalations.*
|
|
1994
|
-
)
|
|
2055
|
+
)${wakeCTE}
|
|
1995
2056
|
SELECT t.id, t.status AS prior_status,
|
|
1996
2057
|
CASE
|
|
1997
2058
|
WHEN c.id IS NOT NULL THEN 'cancelled'
|
|
1998
2059
|
WHEN t.id IS NULL THEN 'not-found'
|
|
1999
2060
|
ELSE 'already-terminal'
|
|
2000
2061
|
END AS outcome,
|
|
2001
|
-
row_to_json(c.*) AS entry_json
|
|
2062
|
+
row_to_json(c.*) AS entry_json,
|
|
2063
|
+
${wakeCount}
|
|
2002
2064
|
FROM (SELECT * FROM target) t
|
|
2003
2065
|
FULL OUTER JOIN (SELECT * FROM cancelled) c ON c.id = t.id
|
|
2004
|
-
`,
|
|
2066
|
+
`, params);
|
|
2005
2067
|
if (!result.rows[0] || result.rows[0].outcome === 'not-found')
|
|
2006
2068
|
return { ok: false, reason: 'not-found' };
|
|
2007
2069
|
if (result.rows[0].outcome === 'already-terminal')
|
|
2008
2070
|
return { ok: false, reason: 'already-terminal' };
|
|
2009
2071
|
const entry = result.rows[0].entry_json;
|
|
2010
|
-
return { ok: true, entry };
|
|
2072
|
+
return { ok: true, entry, wakeEnqueued: result.rows[0].wake_count > 0 };
|
|
2011
2073
|
}
|
|
2012
2074
|
async escalateEscalationToRole(params) {
|
|
2013
2075
|
const { id, targetRole, namespace } = params;
|
|
@@ -2156,6 +2218,7 @@ class PostgresStoreService extends __1.StoreService {
|
|
|
2156
2218
|
WHERE id = ANY($2::uuid[])
|
|
2157
2219
|
${namespace ? 'AND namespace = $4' : ''}
|
|
2158
2220
|
AND status = 'pending'
|
|
2221
|
+
AND signal_key IS NULL
|
|
2159
2222
|
RETURNING *`, namespace ? [payloadJson, ids, metaJson, namespace] : [payloadJson, ids, metaJson]);
|
|
2160
2223
|
return result.rows;
|
|
2161
2224
|
}
|
|
@@ -35,6 +35,10 @@ async function deploySchema(streamClient, appId, logger) {
|
|
|
35
35
|
await ensureIndexes(client, schemaName);
|
|
36
36
|
await ensureProcedures(client, schemaName);
|
|
37
37
|
await ensureStatementLevelTriggers(client, schemaName);
|
|
38
|
+
// Re-deploy the fallback poller's discovery function so existing
|
|
39
|
+
// databases receive predicate changes (v0.25.6: stale-reservation
|
|
40
|
+
// reclaim — see getNotifyVisibleMessagesSQL)
|
|
41
|
+
await client.query(getNotifyVisibleMessagesSQL(schemaName));
|
|
38
42
|
}
|
|
39
43
|
finally {
|
|
40
44
|
await client.query('SELECT pg_advisory_unlock($1)', [lockId]);
|
|
@@ -177,6 +181,19 @@ async function ensureIndexes(client, schemaName) {
|
|
|
177
181
|
CREATE INDEX IF NOT EXISTS idx_worker_streams_message_fetch
|
|
178
182
|
ON ${workerTable} (stream_name, priority DESC, id)
|
|
179
183
|
WHERE expired_at IS NULL;
|
|
184
|
+
`);
|
|
185
|
+
// v0.25.6: in-flight partial indexes — bounded to currently reserved
|
|
186
|
+
// rows (≈ concurrent executions) — serve the fallback poller's
|
|
187
|
+
// stale-reservation reclaim branch (see getNotifyVisibleMessagesSQL)
|
|
188
|
+
await client.query(`
|
|
189
|
+
CREATE INDEX IF NOT EXISTS idx_engine_streams_inflight
|
|
190
|
+
ON ${engineTable} (stream_name, reserved_at)
|
|
191
|
+
WHERE expired_at IS NULL AND reserved_at IS NOT NULL;
|
|
192
|
+
`);
|
|
193
|
+
await client.query(`
|
|
194
|
+
CREATE INDEX IF NOT EXISTS idx_worker_streams_inflight
|
|
195
|
+
ON ${workerTable} (stream_name, reserved_at)
|
|
196
|
+
WHERE expired_at IS NULL AND reserved_at IS NOT NULL;
|
|
180
197
|
`);
|
|
181
198
|
// v0.18.0: add jid column to engine_streams for job tracing
|
|
182
199
|
await client.query(`ALTER TABLE ${engineTable} ADD COLUMN IF NOT EXISTS jid TEXT NOT NULL DEFAULT ''`);
|
|
@@ -461,7 +478,34 @@ async function createNotificationTriggers(client, schemaName) {
|
|
|
461
478
|
EXECUTE FUNCTION ${schemaName}.notify_new_worker_stream_message();
|
|
462
479
|
`);
|
|
463
480
|
// ---- Visibility timeout notification function (queries both tables) ----
|
|
464
|
-
await client.query(
|
|
481
|
+
await client.query(getNotifyVisibleMessagesSQL(schemaName));
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* The fallback poller's discovery function. Surfaces streams that have
|
|
485
|
+
* deliverable work: unreserved visible messages AND stale reservations
|
|
486
|
+
* whose holder died (heartbeats refresh reserved_at every ~15s while an
|
|
487
|
+
* activity runs, so any reservation older than 60s has a dead holder or
|
|
488
|
+
* is already claimable). Stale-reservation discovery is the ONLY path
|
|
489
|
+
* that reclaims orphaned in-flight work on a quiet stream — without it,
|
|
490
|
+
* a process death wedges its reserved messages forever. A premature
|
|
491
|
+
* surface (reservation window configured above 60s on a provider with
|
|
492
|
+
* static leases) costs one empty fetch; the claim query enforces the
|
|
493
|
+
* real window.
|
|
494
|
+
*
|
|
495
|
+
* Deployed on fresh schemas AND re-deployed on every boot (see
|
|
496
|
+
* deploySchema) so existing databases receive predicate changes.
|
|
497
|
+
*/
|
|
498
|
+
function getNotifyVisibleMessagesSQL(schemaName) {
|
|
499
|
+
const engineTable = `${schemaName}.engine_streams`;
|
|
500
|
+
const workerTable = `${schemaName}.worker_streams`;
|
|
501
|
+
//stale threshold tracks the configured base window: heartbeats refresh
|
|
502
|
+
//reserved_at every base/2 (15s default), so live holders never look
|
|
503
|
+
//stale; 2x base keeps static-lease holders (secured workers, one
|
|
504
|
+
//adaptive doubling) from surfacing as false positives. Baked at
|
|
505
|
+
//deploy time from the deploying node's env — a premature surface
|
|
506
|
+
//costs one empty fetch (the claim query enforces the real window).
|
|
507
|
+
const staleSeconds = Math.max(60, enums_1.HMSH_RESERVATION_TIMEOUT_S * 2);
|
|
508
|
+
return `
|
|
465
509
|
CREATE OR REPLACE FUNCTION ${schemaName}.notify_visible_messages()
|
|
466
510
|
RETURNS INTEGER AS $$
|
|
467
511
|
DECLARE
|
|
@@ -470,13 +514,23 @@ async function createNotificationTriggers(client, schemaName) {
|
|
|
470
514
|
payload JSON;
|
|
471
515
|
notification_count INTEGER := 0;
|
|
472
516
|
BEGIN
|
|
473
|
-
-- Engine streams
|
|
517
|
+
-- Engine streams: visible unreserved work (active_messages
|
|
518
|
+
-- partial index) UNION stale reservations whose holder died
|
|
519
|
+
-- (inflight partial index) — each branch index-matched
|
|
474
520
|
FOR msg IN
|
|
475
|
-
SELECT DISTINCT stream_name
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
521
|
+
SELECT DISTINCT u.stream_name FROM (
|
|
522
|
+
(SELECT stream_name FROM ${engineTable}
|
|
523
|
+
WHERE visible_at <= NOW()
|
|
524
|
+
AND reserved_at IS NULL
|
|
525
|
+
AND expired_at IS NULL
|
|
526
|
+
LIMIT 50)
|
|
527
|
+
UNION ALL
|
|
528
|
+
(SELECT stream_name FROM ${engineTable}
|
|
529
|
+
WHERE expired_at IS NULL
|
|
530
|
+
AND reserved_at IS NOT NULL
|
|
531
|
+
AND reserved_at < NOW() - INTERVAL '${staleSeconds} seconds'
|
|
532
|
+
LIMIT 50)
|
|
533
|
+
) u
|
|
480
534
|
LIMIT 50
|
|
481
535
|
LOOP
|
|
482
536
|
channel_name := 'eng_' || msg.stream_name;
|
|
@@ -493,13 +547,21 @@ async function createNotificationTriggers(client, schemaName) {
|
|
|
493
547
|
notification_count := notification_count + 1;
|
|
494
548
|
END LOOP;
|
|
495
549
|
|
|
496
|
-
-- Worker streams
|
|
550
|
+
-- Worker streams: same two index-matched branches
|
|
497
551
|
FOR msg IN
|
|
498
|
-
SELECT DISTINCT stream_name
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
552
|
+
SELECT DISTINCT u.stream_name FROM (
|
|
553
|
+
(SELECT stream_name FROM ${workerTable}
|
|
554
|
+
WHERE visible_at <= NOW()
|
|
555
|
+
AND reserved_at IS NULL
|
|
556
|
+
AND expired_at IS NULL
|
|
557
|
+
LIMIT 50)
|
|
558
|
+
UNION ALL
|
|
559
|
+
(SELECT stream_name FROM ${workerTable}
|
|
560
|
+
WHERE expired_at IS NULL
|
|
561
|
+
AND reserved_at IS NOT NULL
|
|
562
|
+
AND reserved_at < NOW() - INTERVAL '${staleSeconds} seconds'
|
|
563
|
+
LIMIT 50)
|
|
564
|
+
) u
|
|
503
565
|
LIMIT 50
|
|
504
566
|
LOOP
|
|
505
567
|
channel_name := 'wrk_' || msg.stream_name;
|
|
@@ -519,7 +581,7 @@ async function createNotificationTriggers(client, schemaName) {
|
|
|
519
581
|
RETURN notification_count;
|
|
520
582
|
END;
|
|
521
583
|
$$ LANGUAGE plpgsql;
|
|
522
|
-
|
|
584
|
+
`;
|
|
523
585
|
}
|
|
524
586
|
function getNotificationChannelName(streamName, isEngine) {
|
|
525
587
|
const prefix = isEngine ? 'eng_' : 'wrk_';
|
|
@@ -296,12 +296,15 @@ async function dropDeadJobMessages(client, tableName, streamName, rows, liveness
|
|
|
296
296
|
}
|
|
297
297
|
}
|
|
298
298
|
catch (error) {
|
|
299
|
-
if (error?.code === '42P01') {
|
|
300
|
-
//jobs table is not visible
|
|
301
|
-
//run here — interrupt-time
|
|
299
|
+
if (error?.code === '42P01' || error?.code === '42501') {
|
|
300
|
+
//jobs table is not visible (42P01) or not readable (42501) from
|
|
301
|
+
//this connection; the guard cannot run here — interrupt-time
|
|
302
|
+
//purging (expireJobMessages) still applies. Self-disable so the
|
|
303
|
+
//hot path stops issuing a failing cross-table query per fetch.
|
|
302
304
|
liveness.enabled = false;
|
|
303
305
|
logger.info('postgres-stream-liveness-guard-disabled', {
|
|
304
306
|
jobsTable: liveness.jobsTable,
|
|
307
|
+
code: error.code,
|
|
305
308
|
});
|
|
306
309
|
}
|
|
307
310
|
else {
|
|
@@ -150,17 +150,19 @@ function getCreateProceduresSQL(schemaName) {
|
|
|
150
150
|
-- Engine stream_name is the schema/appId name
|
|
151
151
|
engine_stream_name := '${schemaName}';
|
|
152
152
|
|
|
153
|
+
-- jid stamped from the message metadata (parity with the raw
|
|
154
|
+
-- publish path) so job-scoped expiry can address these rows
|
|
153
155
|
IF p_max_retry_attempts IS NOT NULL THEN
|
|
154
156
|
INSERT INTO ${engineTable}
|
|
155
|
-
(stream_name, message, priority, max_retry_attempts, backoff_coefficient, maximum_interval_seconds, visible_at, retry_attempt)
|
|
157
|
+
(stream_name, jid, message, priority, max_retry_attempts, backoff_coefficient, maximum_interval_seconds, visible_at, retry_attempt)
|
|
156
158
|
VALUES
|
|
157
|
-
(engine_stream_name, p_message, p_priority, p_max_retry_attempts, p_backoff_coefficient, p_maximum_interval_seconds, p_visible_at, p_retry_attempt)
|
|
159
|
+
(engine_stream_name, COALESCE(p_message::jsonb -> 'metadata' ->> 'jid', ''), p_message, p_priority, p_max_retry_attempts, p_backoff_coefficient, p_maximum_interval_seconds, p_visible_at, p_retry_attempt)
|
|
158
160
|
RETURNING id INTO new_id;
|
|
159
161
|
ELSE
|
|
160
162
|
INSERT INTO ${engineTable}
|
|
161
|
-
(stream_name, message, priority, visible_at, retry_attempt)
|
|
163
|
+
(stream_name, jid, message, priority, visible_at, retry_attempt)
|
|
162
164
|
VALUES
|
|
163
|
-
(engine_stream_name, p_message, p_priority, p_visible_at, p_retry_attempt)
|
|
165
|
+
(engine_stream_name, COALESCE(p_message::jsonb -> 'metadata' ->> 'jid', ''), p_message, p_priority, p_visible_at, p_retry_attempt)
|
|
164
166
|
RETURNING id INTO new_id;
|
|
165
167
|
END IF;
|
|
166
168
|
|
|
@@ -106,17 +106,16 @@ export type ResolveEscalationResult = {
|
|
|
106
106
|
reason: 'not-found' | 'already-resolved' | 'already-cancelled' | 'already-expired';
|
|
107
107
|
};
|
|
108
108
|
/**
|
|
109
|
-
* A pre-built wake
|
|
110
|
-
* awaiting workflow's wake
|
|
111
|
-
*
|
|
112
|
-
* pins the
|
|
113
|
-
* when the
|
|
114
|
-
* to post-commit delivery).
|
|
109
|
+
* A pre-built wake message, committed INSIDE the resolve/cancel transaction
|
|
110
|
+
* so the awaiting workflow's wake is durable with the status change. The
|
|
111
|
+
* client composes the webhook message; the store owns the stream INSERT.
|
|
112
|
+
* `forSignalKey` pins the wake to the row it was built for — it is written
|
|
113
|
+
* only when the affected row's `signal_key` matches (a mismatched row falls
|
|
114
|
+
* back to post-commit delivery).
|
|
115
115
|
*/
|
|
116
116
|
export interface EscalationWakeCommand {
|
|
117
117
|
forSignalKey: string;
|
|
118
|
-
|
|
119
|
-
params: unknown[];
|
|
118
|
+
message: string;
|
|
120
119
|
}
|
|
121
120
|
export type ReleaseEscalationResult = {
|
|
122
121
|
ok: true;
|
package/package.json
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
jid,reserved_at,retry_attempt,created_at,message
|
|
2
|
+
simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm3,2026-07-06 20:19:07.953447+00,0,2026-07-06 20:18:07.945876+00,"{""metadata"":{""guid"":""HxfFFVmLVwtk37194ysP1qQ"",""jid"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm3"",""gid"":""HAXgFtL8ZicUsOywHCKUauE"",""dad"":"",0,15,0,0"",""aid"":""proxyer"",""topic"":""virtual-farm-activity"",""wfn"":""durable.execute""},""data"":{""activityName"":""printerBatch"",""arguments"":[{""machineId"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm3"",""operatorId"":""0dfa0000-0000-4000-8000-000000000004"",""printSeconds"":2,""idleTickSeconds"":3,""maxIterations"":25,""runsSoFar"":6,""plan"":[],""advertId"":""0a4af7d1-2dc4-4e06-9d43-791acf3c49e5""}],""workflowDimension"":""$1"",""index"":5,""originJobId"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG"",""parentWorkflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm3-$printerBatch$1-5"",""workflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm3-$printerBatch$1-5"",""workflowTopic"":""virtual-farm-activity"",""expire"":2592000,""backoffCoefficient"":5,""initialInterval"":1,""maximumAttempts"":3,""maximumInterval"":120}}"
|
|
3
|
+
simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm4,2026-07-06 20:19:10.889217+00,0,2026-07-06 20:18:10.880925+00,"{""metadata"":{""guid"":""HXZnTUGkTix4uF7q14PuP0f"",""jid"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm4"",""gid"":""HO9SAZEoZGiycaugLuwuUXv"",""dad"":"",0,15,0,0"",""aid"":""proxyer"",""topic"":""virtual-farm-activity"",""wfn"":""durable.execute""},""data"":{""activityName"":""printerBatch"",""arguments"":[{""machineId"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm4"",""operatorId"":""0dfa0000-0000-4000-8000-000000000005"",""printSeconds"":2,""idleTickSeconds"":3,""maxIterations"":25,""runsSoFar"":6,""plan"":[],""advertId"":""a1d6d90a-a2cf-4621-a74f-e0ed1013cbc6""}],""workflowDimension"":""$1"",""index"":5,""originJobId"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG"",""parentWorkflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm4-$printerBatch$1-5"",""workflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm4-$printerBatch$1-5"",""workflowTopic"":""virtual-farm-activity"",""expire"":2592000,""backoffCoefficient"":5,""initialInterval"":1,""maximumAttempts"":3,""maximumInterval"":120}}"
|
|
4
|
+
simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm5,2026-07-06 20:19:16.082786+00,0,2026-07-06 20:18:16.072927+00,"{""metadata"":{""guid"":""HE7nFSVKoh2oFe6-lYtCJWV"",""jid"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm5"",""gid"":""H697gK8YvdS-_waP4NLiL-L"",""dad"":"",0,15,0,0"",""aid"":""proxyer"",""topic"":""virtual-farm-activity"",""wfn"":""durable.execute""},""data"":{""activityName"":""printerBatch"",""arguments"":[{""machineId"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm5"",""operatorId"":""0dfa0000-0000-4000-8000-000000000006"",""printSeconds"":2,""idleTickSeconds"":3,""maxIterations"":25,""runsSoFar"":4,""plan"":[],""advertId"":""df441cd6-1811-41f0-b646-3c794ebf71d4""}],""workflowDimension"":""$1"",""index"":5,""originJobId"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG"",""parentWorkflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm5-$printerBatch$1-5"",""workflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm5-$printerBatch$1-5"",""workflowTopic"":""virtual-farm-activity"",""expire"":2592000,""backoffCoefficient"":5,""initialInterval"":1,""maximumAttempts"":3,""maximumInterval"":120}}"
|
|
5
|
+
simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm1,2026-07-06 20:19:05.227375+00,0,2026-07-06 20:18:20.216145+00,"{""metadata"":{""guid"":""HTFIOwV5q0HYi8z04GwX9qj"",""jid"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm1"",""gid"":""HSR3wD1ryGQ6f5vzzGcO8eV"",""dad"":"",0,15,0,0"",""aid"":""proxyer"",""topic"":""virtual-farm-activity"",""wfn"":""durable.execute""},""data"":{""activityName"":""printerBatch"",""arguments"":[{""machineId"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm1"",""operatorId"":""0dfa0000-0000-4000-8000-000000000002"",""printSeconds"":2,""idleTickSeconds"":3,""maxIterations"":25,""runsSoFar"":13,""plan"":[],""advertId"":""e8ee662b-6de6-4a57-b8d6-ee9ed76e1f99""}],""workflowDimension"":""$1"",""index"":5,""originJobId"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG"",""parentWorkflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm1-$printerBatch$1-5"",""workflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm1-$printerBatch$1-5"",""workflowTopic"":""virtual-farm-activity"",""expire"":2592000,""backoffCoefficient"":5,""initialInterval"":1,""maximumAttempts"":3,""maximumInterval"":120}}"
|
|
6
|
+
simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm0,2026-07-06 20:19:09.068044+00,0,2026-07-06 20:18:39.059327+00,"{""metadata"":{""guid"":""HcFhO_I7Fqn_5ftBd4uUTuZ"",""jid"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm0"",""gid"":""HyVUxkj0OkvEO2r0sdgtTan"",""dad"":"",0,15,0,0"",""aid"":""proxyer"",""topic"":""virtual-farm-activity"",""wfn"":""durable.execute""},""data"":{""activityName"":""printerBatch"",""arguments"":[{""machineId"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm0"",""operatorId"":""0dfa0000-0000-4000-8000-000000000001"",""printSeconds"":2,""idleTickSeconds"":3,""maxIterations"":25,""runsSoFar"":12,""plan"":[],""advertId"":""53bd1b3a-8ec3-4c35-a839-da93d9e23bc0""}],""workflowDimension"":""$1"",""index"":5,""originJobId"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG"",""parentWorkflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm0-$printerBatch$1-5"",""workflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm0-$printerBatch$1-5"",""workflowTopic"":""virtual-farm-activity"",""expire"":2592000,""backoffCoefficient"":5,""initialInterval"":1,""maximumAttempts"":3,""maximumInterval"":120}}"
|
|
7
|
+
simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm2,2026-07-06 20:19:18.195319+00,0,2026-07-06 20:19:18.191286+00,"{""metadata"":{""guid"":""HWpXdbtZ-Bx5iqINxvAa6wf"",""jid"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm2"",""gid"":""HWzNcP3ToTg27Qc5u5MljTT"",""dad"":"",0,16,0,0"",""aid"":""proxyer"",""topic"":""virtual-farm-activity"",""wfn"":""durable.execute""},""data"":{""activityName"":""printerBatch"",""arguments"":[{""machineId"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm2"",""operatorId"":""0dfa0000-0000-4000-8000-000000000003"",""printSeconds"":2,""idleTickSeconds"":3,""maxIterations"":25,""runsSoFar"":8,""plan"":[],""advertId"":""a0a84553-5031-456d-ae02-5d9746b606fc""}],""workflowDimension"":""$1"",""index"":6,""originJobId"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG"",""parentWorkflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm2-$printerBatch$1-6"",""workflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm2-$printerBatch$1-6"",""workflowTopic"":""virtual-farm-activity"",""expire"":2592000,""backoffCoefficient"":5,""initialInterval"":1,""maximumAttempts"":3,""maximumInterval"":120}}"
|
|
8
|
+
simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-manager,2026-07-06 20:19:14.76557+00,0,2026-07-06 20:18:29.759824+00,"{""metadata"":{""guid"":""HzkL-DZTtXyIyDV8oZNZJxI"",""jid"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-manager"",""gid"":""HCGavoqirvD8FwdwQOWgcov"",""dad"":"",0,15,0,0"",""aid"":""proxyer"",""topic"":""actor-crew-activity"",""wfn"":""durable.execute""},""data"":{""activityName"":""managerBatch"",""arguments"":[{""laneId"":""9a93b820-dde3-425b-a320-260b91c08ec2"",""reviewSeconds"":3,""moveSeconds"":3,""idleTickSeconds"":3,""maxIterations"":25,""actorEmail"":""sim.manager.marco@sim.hike.local""}],""workflowDimension"":""$1"",""index"":5,""originJobId"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG"",""parentWorkflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-manager-$managerBatch$1-5"",""workflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-manager-$managerBatch$1-5"",""workflowTopic"":""actor-crew-activity"",""expire"":2592000,""backoffCoefficient"":5,""initialInterval"":1,""maximumAttempts"":3,""maximumInterval"":120}}"
|
|
9
|
+
simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_quality_control-0,2026-07-06 20:19:12.094467+00,0,2026-07-06 20:18:42.089308+00,"{""metadata"":{""guid"":""H2MwGXqBeP8-8nPX-T6ndnU"",""jid"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_quality_control-0"",""gid"":""H7nf5o8VvELz6NHWGWGNsXe"",""dad"":"",0,15,0,0"",""aid"":""proxyer"",""topic"":""actor-crew-activity"",""wfn"":""durable.execute""},""data"":{""activityName"":""associateBatch"",""arguments"":[{""station"":""NEEDS_QUALITY_CONTROL"",""role"":""quality-inspector"",""toStatus"":""NEEDS_SHIPPING"",""dwellSeconds"":4,""betweenSeconds"":2,""idleTickSeconds"":3,""maxIterations"":25,""touchesSoFar"":7,""unparkedRejects"":[],""troubledOrders"":[""931a1e99-2db6-4280-b034-57e3c42a9aba""],""actorEmail"":""sim.inspector.ines@sim.hike.local""}],""workflowDimension"":""$1"",""index"":5,""originJobId"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG"",""parentWorkflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_quality_control-0-$associateBatch$1-5"",""workflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_quality_control-0-$associateBatch$1-5"",""workflowTopic"":""actor-crew-activity"",""expire"":2592000,""backoffCoefficient"":5,""initialInterval"":1,""maximumAttempts"":3,""maximumInterval"":120}}"
|
|
10
|
+
simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_quality_control-1,2026-07-06 20:19:12.096894+00,0,2026-07-06 20:18:42.093836+00,"{""metadata"":{""guid"":""H4pQ4lScxaW8wUEiP8BQh8E"",""jid"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_quality_control-1"",""gid"":""HQUqSMYqzsvODyjMvRNPtvl"",""dad"":"",0,15,0,0"",""aid"":""proxyer"",""topic"":""actor-crew-activity"",""wfn"":""durable.execute""},""data"":{""activityName"":""associateBatch"",""arguments"":[{""station"":""NEEDS_QUALITY_CONTROL"",""role"":""quality-inspector"",""toStatus"":""NEEDS_SHIPPING"",""dwellSeconds"":4,""betweenSeconds"":2,""idleTickSeconds"":3,""maxIterations"":25,""touchesSoFar"":7,""unparkedRejects"":[],""troubledOrders"":[""931a1e99-2db6-4280-b034-57e3c42a9aba""],""actorEmail"":""sim.inspector.tomas@sim.hike.local""}],""workflowDimension"":""$1"",""index"":5,""originJobId"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG"",""parentWorkflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_quality_control-1-$associateBatch$1-5"",""workflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_quality_control-1-$associateBatch$1-5"",""workflowTopic"":""actor-crew-activity"",""expire"":2592000,""backoffCoefficient"":5,""initialInterval"":1,""maximumAttempts"":3,""maximumInterval"":120}}"
|
|
11
|
+
simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-draft,2026-07-06 20:19:16.150935+00,0,2026-07-06 20:18:46.146153+00,"{""metadata"":{""guid"":""HgtJMxOb1oYv6jJb2uErF0_"",""jid"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-draft"",""gid"":""HN6b23imRu-wgDHA-DpxzLV"",""dad"":"",0,14,0,0"",""aid"":""proxyer"",""topic"":""actor-crew-activity"",""wfn"":""durable.execute""},""data"":{""activityName"":""associateBatch"",""arguments"":[{""station"":""DRAFT"",""role"":""insole-designer"",""toStatus"":""NEEDS_MANUFACTURING"",""dwellSeconds"":4,""betweenSeconds"":2,""idleTickSeconds"":3,""maxIterations"":25,""touchesSoFar"":24,""unparkedRejects"":[],""troubledOrders"":[""2b294913-f8ca-4639-9928-3abd53ea9616""],""actorEmail"":""sim.designer.dana@sim.hike.local"",""worklistStation"":""design-approval""}],""workflowDimension"":""$1"",""index"":4,""originJobId"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG"",""parentWorkflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-draft-$associateBatch$1-4"",""workflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-draft-$associateBatch$1-4"",""workflowTopic"":""actor-crew-activity"",""expire"":2592000,""backoffCoefficient"":5,""initialInterval"":1,""maximumAttempts"":3,""maximumInterval"":120}}"
|
|
12
|
+
simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_finishing-1,2026-07-06 20:19:04.938838+00,0,2026-07-06 20:18:49.932504+00,"{""metadata"":{""guid"":""HQD-nuRyxBmH431uJ8noMBS"",""jid"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_finishing-1"",""gid"":""HWM3gVxjxz9_K-EnQpCeFjY"",""dad"":"",0,15,0,0"",""aid"":""proxyer"",""topic"":""actor-crew-activity"",""wfn"":""durable.execute""},""data"":{""activityName"":""associateBatch"",""arguments"":[{""station"":""NEEDS_FINISHING"",""role"":""finisher"",""toStatus"":""NEEDS_QUALITY_CONTROL"",""dwellSeconds"":4,""betweenSeconds"":2,""idleTickSeconds"":3,""maxIterations"":25,""touchesSoFar"":9,""unparkedRejects"":[],""troubledOrders"":[],""actorEmail"":""sim.finisher.kofi@sim.hike.local""}],""workflowDimension"":""$1"",""index"":5,""originJobId"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG"",""parentWorkflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_finishing-1-$associateBatch$1-5"",""workflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_finishing-1-$associateBatch$1-5"",""workflowTopic"":""actor-crew-activity"",""expire"":2592000,""backoffCoefficient"":5,""initialInterval"":1,""maximumAttempts"":3,""maximumInterval"":120}}"
|
|
13
|
+
simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_finishing-0,2026-07-06 20:19:14.016307+00,0,2026-07-06 20:18:59.008759+00,"{""metadata"":{""guid"":""HHBHdHpX2Wear_cVJim70Ps"",""jid"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_finishing-0"",""gid"":""HkZyt5-JhLoCNX26Q4cyqbq"",""dad"":"",0,15,0,0"",""aid"":""proxyer"",""topic"":""actor-crew-activity"",""wfn"":""durable.execute""},""data"":{""activityName"":""associateBatch"",""arguments"":[{""station"":""NEEDS_FINISHING"",""role"":""finisher"",""toStatus"":""NEEDS_QUALITY_CONTROL"",""dwellSeconds"":4,""betweenSeconds"":2,""idleTickSeconds"":3,""maxIterations"":25,""touchesSoFar"":11,""unparkedRejects"":[],""troubledOrders"":[],""actorEmail"":""sim.finisher.ada@sim.hike.local""}],""workflowDimension"":""$1"",""index"":5,""originJobId"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG"",""parentWorkflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_finishing-0-$associateBatch$1-5"",""workflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_finishing-0-$associateBatch$1-5"",""workflowTopic"":""actor-crew-activity"",""expire"":2592000,""backoffCoefficient"":5,""initialInterval"":1,""maximumAttempts"":3,""maximumInterval"":120}}"
|
|
14
|
+
simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_shipping,2026-07-06 20:19:15.654293+00,0,2026-07-06 20:19:00.646061+00,"{""metadata"":{""guid"":""HffnP49JTwmHAwk3BNRn22l"",""jid"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_shipping"",""gid"":""HuK-iSfDdYXXBJWo2WwhCDF"",""dad"":"",0,15,0,0"",""aid"":""proxyer"",""topic"":""actor-crew-activity"",""wfn"":""durable.execute""},""data"":{""activityName"":""associateBatch"",""arguments"":[{""station"":""NEEDS_SHIPPING"",""role"":""shipping-associate"",""toStatus"":""COMPLETED"",""dwellSeconds"":4,""betweenSeconds"":2,""idleTickSeconds"":3,""maxIterations"":25,""touchesSoFar"":12,""unparkedRejects"":[],""troubledOrders"":[],""actorEmail"":""sim.shipper.june@sim.hike.local""}],""workflowDimension"":""$1"",""index"":5,""originJobId"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG"",""parentWorkflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_shipping-$associateBatch$1-5"",""workflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_shipping-$associateBatch$1-5"",""workflowTopic"":""actor-crew-activity"",""expire"":2592000,""backoffCoefficient"":5,""initialInterval"":1,""maximumAttempts"":3,""maximumInterval"":120}}"
|
|
15
|
+
simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_gluing-1,2026-07-06 20:19:05.381804+00,0,2026-07-06 20:19:05.3792+00,"{""metadata"":{""guid"":""Hx82JQ7g0Y4XT7nqPFK4km9"",""jid"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_gluing-1"",""gid"":""HoT3fZZg04_z7BKimsTs-Ei"",""dad"":"",0,15,0,0"",""aid"":""proxyer"",""topic"":""actor-crew-activity"",""wfn"":""durable.execute""},""data"":{""activityName"":""associateBatch"",""arguments"":[{""station"":""NEEDS_GLUING"",""role"":""gluer"",""toStatus"":""NEEDS_FINISHING"",""dwellSeconds"":4,""betweenSeconds"":2,""idleTickSeconds"":3,""maxIterations"":25,""touchesSoFar"":13,""unparkedRejects"":[],""troubledOrders"":[],""actorEmail"":""sim.gluer.dev@sim.hike.local""}],""workflowDimension"":""$1"",""index"":5,""originJobId"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG"",""parentWorkflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_gluing-1-$associateBatch$1-5"",""workflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_gluing-1-$associateBatch$1-5"",""workflowTopic"":""actor-crew-activity"",""expire"":2592000,""backoffCoefficient"":5,""initialInterval"":1,""maximumAttempts"":3,""maximumInterval"":120}}"
|
|
16
|
+
simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_gluing-0,2026-07-06 20:19:05.386589+00,0,2026-07-06 20:19:05.384446+00,"{""metadata"":{""guid"":""HvNFncRHNbqR_OSOOiQ0M12"",""jid"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_gluing-0"",""gid"":""Hk7Wy8HO98wh45Clgp5-sUo"",""dad"":"",0,15,0,0"",""aid"":""proxyer"",""topic"":""actor-crew-activity"",""wfn"":""durable.execute""},""data"":{""activityName"":""associateBatch"",""arguments"":[{""station"":""NEEDS_GLUING"",""role"":""gluer"",""toStatus"":""NEEDS_FINISHING"",""dwellSeconds"":4,""betweenSeconds"":2,""idleTickSeconds"":3,""maxIterations"":25,""touchesSoFar"":13,""unparkedRejects"":[],""troubledOrders"":[],""actorEmail"":""sim.gluer.rosa@sim.hike.local""}],""workflowDimension"":""$1"",""index"":5,""originJobId"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG"",""parentWorkflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_gluing-0-$associateBatch$1-5"",""workflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_gluing-0-$associateBatch$1-5"",""workflowTopic"":""actor-crew-activity"",""expire"":2592000,""backoffCoefficient"":5,""initialInterval"":1,""maximumAttempts"":3,""maximumInterval"":120}}"
|
|
17
|
+
simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-post_print_qa-1,2026-07-06 20:19:06.832852+00,0,2026-07-06 20:19:06.829652+00,"{""metadata"":{""guid"":""Hg1xKYket-ULnS-HdHaNuiD"",""jid"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-post_print_qa-1"",""gid"":""HLjxCrhPCUyqBaWbwSE0pxp"",""dad"":"",0,15,0,0"",""aid"":""proxyer"",""topic"":""actor-crew-activity"",""wfn"":""durable.execute""},""data"":{""activityName"":""associateBatch"",""arguments"":[{""station"":""POST_PRINT_QA"",""role"":""post-print-qa"",""toStatus"":""NEEDS_GLUING"",""dwellSeconds"":4,""betweenSeconds"":2,""idleTickSeconds"":3,""maxIterations"":25,""touchesSoFar"":14,""rejectRate"":{""pct"":15,""targetStation"":""PRINTING"",""reasons"":[""Layer separation on the arch — reprint"",""Surface finish below spec"",""Edge delamination at the heel cup"",""Warping on the medial side""],""leftQuantity"":1,""rightQuantity"":0},""unparkedRejects"":[],""troubledOrders"":[],""actorEmail"":""sim.qa.elena@sim.hike.local""}],""workflowDimension"":""$1"",""index"":5,""originJobId"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG"",""parentWorkflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-post_print_qa-1-$associateBatch$1-5"",""workflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-post_print_qa-1-$associateBatch$1-5"",""workflowTopic"":""actor-crew-activity"",""expire"":2592000,""backoffCoefficient"":5,""initialInterval"":1,""maximumAttempts"":3,""maximumInterval"":120}}"
|
|
18
|
+
simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-post_print_qa-0,2026-07-06 20:19:12.746444+00,0,2026-07-06 20:19:12.744474+00,"{""metadata"":{""guid"":""HTl4tkkOMhgso2QMlQlKYvn"",""jid"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-post_print_qa-0"",""gid"":""HY9mpKnlDbC3a1o3B4R-53h"",""dad"":"",0,15,0,0"",""aid"":""proxyer"",""topic"":""actor-crew-activity"",""wfn"":""durable.execute""},""data"":{""activityName"":""associateBatch"",""arguments"":[{""station"":""POST_PRINT_QA"",""role"":""post-print-qa"",""toStatus"":""NEEDS_GLUING"",""dwellSeconds"":4,""betweenSeconds"":2,""idleTickSeconds"":3,""maxIterations"":25,""touchesSoFar"":16,""rejectRate"":{""pct"":15,""targetStation"":""PRINTING"",""reasons"":[""Layer separation on the arch — reprint"",""Surface finish below spec"",""Edge delamination at the heel cup"",""Warping on the medial side""],""leftQuantity"":1,""rightQuantity"":0},""unparkedRejects"":[],""troubledOrders"":[],""actorEmail"":""sim.qa.marcus@sim.hike.local""}],""workflowDimension"":""$1"",""index"":5,""originJobId"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG"",""parentWorkflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-post_print_qa-0-$associateBatch$1-5"",""workflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-post_print_qa-0-$associateBatch$1-5"",""workflowTopic"":""actor-crew-activity"",""expire"":2592000,""backoffCoefficient"":5,""initialInterval"":1,""maximumAttempts"":3,""maximumInterval"":120}}"
|
|
19
|
+
simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-design,2026-07-06 20:19:10.594054+00,0,2026-07-06 20:18:55.585455+00,"{""metadata"":{""guid"":""HeSdgdXIms_1Lhh2nNgc4EP"",""jid"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-design"",""gid"":""HU01R82t5rnplNLKWryLJPz"",""dad"":"",0,40,0,0"",""aid"":""proxyer"",""topic"":""simulation-activity"",""wfn"":""durable.execute""},""data"":{""activityName"":""designTwinBatch"",""arguments"":[{""user"":""sim.taika.iris@sim.hike.local"",""designSeconds"":2,""idleTickSeconds"":3,""maxIterations"":10,""concurrency"":4,""designedOrders"":[""0f596ab9-f449-485d-b06d-e74864750ae7"",""a5cedbc2-f726-4411-9ac3-10fd113532d2"",""c1a09bd7-bd4b-4eb1-bd3e-91d6b45b479f"",""94d74e0e-e4ce-4db7-91f3-02c1c081c2ba"",""0ed99be7-83b6-4a91-9aa5-560d5c110c6b"",""33b987bb-9253-483f-baa4-37d4ffd0903c"",""2c4bc240-1335-423d-96af-e5855b6ccc0f"",""0e7f24bb-3fba-40e5-a5a8-666f5c91fd49"",""ad0c05b6-4528-4f2a-9569-d06c76589b54"",""de2a02e2-33ca-4c0c-843c-5ad422c7c517"",""c008abd0-8b3b-44f7-9f1c-7d314f72f587"",""04a28a34-20f3-4d10-a636-3cb8f504f56e"",""52295fa8-452b-48ca-862b-64cf3ff8cae6"",""cb1dd956-639d-4ff8-9f56-c12f9c5278e8"",""fca4a3f0-fc49-47cd-8285-bf7e11c60dbe"",""bcd42ec0-f047-4acc-8e70-33575ae7535a"",""3b73fb95-2366-4498-ac9a-3f2d9eb4d1c0"",""3c9d174f-7928-42e7-9edb-2f3b38ba0ddc"",""1e61d445-77ba-4013-af89-1a6de07ff407"",""0b066160-06ab-4681-af20-8c36866c9987"",""fa9f996c-2327-4f30-8fdc-e63239483f23"",""ff31ec18-c7e3-4907-85f0-5c4eda42841f"",""f17a825c-8905-4166-b687-54015ddeae6c"",""931a1e99-2db6-4280-b034-57e3c42a9aba"",""2b294913-f8ca-4639-9928-3abd53ea9616"",""04caa323-662a-4c28-8de0-3655d435f206"",""b2eac688-d074-45d9-8a60-e87719b18c81"",""a11092fe-31e5-4cfe-815c-230a09f2165e"",""176e3d03-0813-4816-83fd-a785798a83bb"",""e1d887bd-1ad4-4436-b728-d75e7df2c39a"",""c8a0ffdc-91b0-443a-a8f9-c01c2e3935c0"",""83cbe6eb-ea9b-492e-8657-4f138e3df05e"",""e8e56685-82a3-4f88-a804-b6a8c21f0a4c"",""89d231bf-600b-40ed-8161-e58dbcc7157c"",""6904b329-7a63-465f-aa6a-82a1095638fb"",""9ca2abe9-ae5f-459a-a003-9ea4ad5cafc0"",""cae10b57-439a-4e98-b0d0-1bb172a9d79f"",""f90af7cb-401b-4e68-9f22-a45d34581d64"",""86a1d1b2-7287-48ab-94b3-544a743fda41"",""76793c77-67a0-4148-b83b-fa50f900111c""]}],""workflowDimension"":""$3"",""index"":8,""originJobId"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG"",""parentWorkflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-design-$designTwinBatch$3-8"",""workflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-design-$designTwinBatch$3-8"",""workflowTopic"":""simulation-activity"",""expire"":2592000,""backoffCoefficient"":5,""initialInterval"":1,""maximumAttempts"":3,""maximumInterval"":120}}"
|