@hatchet-dev/typescript-sdk 1.30.0 → 1.31.0
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/clients/dispatcher/action-listener.d.ts +1 -1
- package/clients/dispatcher/action-listener.js +6 -0
- package/clients/listeners/durable-listener/durable-listener-client.d.ts +6 -1
- package/clients/listeners/durable-listener/durable-listener-client.js +124 -37
- package/clients/rest/generated/Api.d.ts +6 -1
- package/clients/rest/generated/Api.js +1 -1
- package/clients/rest/generated/data-contracts.d.ts +9 -1
- package/clients/rest/generated/data-contracts.js +1 -0
- package/package.json +1 -1
- package/protoc/v1/workflows.d.ts +15 -0
- package/protoc/v1/workflows.js +62 -2
- package/v1/client/worker/context.d.ts +2 -0
- package/v1/client/worker/context.js +26 -8
- package/v1/client/worker/eviction/eviction-manager.d.ts +1 -1
- package/v1/client/worker/eviction/eviction-manager.js +12 -5
- package/v1/client/worker/worker-internal.d.ts +15 -0
- package/v1/client/worker/worker-internal.js +66 -29
- package/v1/declaration.d.ts +3 -1
- package/v1/embedded.d.ts +36 -2
- package/v1/embedded.js +35 -8
- package/v1/examples/concurrency_dynamic/workflow.d.ts +10 -0
- package/v1/examples/concurrency_dynamic/workflow.js +32 -0
- package/v1/examples/concurrency_shared/workflow.d.ts +17 -0
- package/v1/examples/concurrency_shared/workflow.js +70 -0
- package/v1/examples/durable_callback_ordering/workflow.d.ts +42 -0
- package/v1/examples/durable_callback_ordering/workflow.js +95 -0
- package/v1/examples/durable_eviction/workflow.d.ts +5 -0
- package/v1/examples/durable_eviction/workflow.js +15 -1
- package/v1/examples/e2e-worker.js +97 -88
- package/v1/task.d.ts +24 -3
- package/version.d.ts +1 -1
- package/version.js +1 -1
|
@@ -7,7 +7,7 @@ declare enum ListenStrategy {
|
|
|
7
7
|
LISTEN_STRATEGY_V1 = 1,
|
|
8
8
|
LISTEN_STRATEGY_V2 = 2
|
|
9
9
|
}
|
|
10
|
-
export type ActionKey = `${string}/${number}`;
|
|
10
|
+
export type ActionKey = `${string}/${number}` | `${string}/${number}/${number}`;
|
|
11
11
|
export type Action = AssignedAction & {
|
|
12
12
|
readonly key: ActionKey;
|
|
13
13
|
};
|
|
@@ -90,6 +90,12 @@ function createAction(assignedAction) {
|
|
|
90
90
|
const action = assignedAction;
|
|
91
91
|
Object.defineProperty(action, 'key', {
|
|
92
92
|
get() {
|
|
93
|
+
// Durable task invocations each get a distinct key so a restored
|
|
94
|
+
// invocation never collides with the one it replaces in contexts,
|
|
95
|
+
// futures, or eviction state.
|
|
96
|
+
if (this.durableTaskInvocationCount !== undefined) {
|
|
97
|
+
return `${this.taskRunExternalId}/${this.retryCount}/${this.durableTaskInvocationCount}`;
|
|
98
|
+
}
|
|
93
99
|
return `${this.taskRunExternalId}/${this.retryCount}`;
|
|
94
100
|
},
|
|
95
101
|
enumerable: true,
|
|
@@ -65,7 +65,7 @@ export declare class DurableListenerClient {
|
|
|
65
65
|
private _requestNotify;
|
|
66
66
|
private _pendingEventAcks;
|
|
67
67
|
private _pendingCallbacks;
|
|
68
|
-
private
|
|
68
|
+
private _orderedCompletions;
|
|
69
69
|
private _pendingEvictionAcks;
|
|
70
70
|
private _receiveAbort;
|
|
71
71
|
private _statusInterval;
|
|
@@ -90,6 +90,11 @@ export declare class DurableListenerClient {
|
|
|
90
90
|
sendEvent(durableTaskExternalId: string, invocationCount: number, event: RunChildrenEvent): Promise<DurableTaskEventRunAck>;
|
|
91
91
|
sendEvent(durableTaskExternalId: string, invocationCount: number, event: WaitForEvent): Promise<DurableTaskEventWaitForAck>;
|
|
92
92
|
sendEvent(durableTaskExternalId: string, invocationCount: number, event: MemoEvent): Promise<DurableTaskEventMemoAck>;
|
|
93
|
+
private _drainsInProgress;
|
|
94
|
+
private _drainRerunRequests;
|
|
95
|
+
private _drainOrderedCompletions;
|
|
96
|
+
private _drainLoop;
|
|
97
|
+
consumeCallbackWithoutBlocking(durableTaskExternalId: string, invocationCount: number, branchId: number, nodeId: number): void;
|
|
93
98
|
waitForCallback(durableTaskExternalId: string, invocationCount: number, branchId: number, nodeId: number, opts?: {
|
|
94
99
|
signal?: AbortSignal;
|
|
95
100
|
}): Promise<DurableTaskEventLogEntryResult>;
|
|
@@ -38,6 +38,7 @@ const abort_controller_x_1 = require("abort-controller-x");
|
|
|
38
38
|
const hatchet_error_1 = require("../../../util/errors/hatchet-error");
|
|
39
39
|
const dispatcher_1 = require("../../../protoc/v1/dispatcher");
|
|
40
40
|
const non_determinism_error_1 = require("../../../util/errors/non-determinism-error");
|
|
41
|
+
const task_run_terminated_error_1 = require("../../../util/errors/task-run-terminated-error");
|
|
41
42
|
const abort_error_1 = require("../../../util/abort-error");
|
|
42
43
|
const sleep_1 = __importDefault(require("../../../util/sleep"));
|
|
43
44
|
const listener_severity_1 = require("../../dispatcher/listener-severity");
|
|
@@ -110,6 +111,9 @@ function eventLogEntryResultFromProto(proto) {
|
|
|
110
111
|
function ackKey(taskExtId, invocationCount) {
|
|
111
112
|
return `${taskExtId}:${invocationCount}`;
|
|
112
113
|
}
|
|
114
|
+
function completionOrderKey(taskExtId, invocationCount) {
|
|
115
|
+
return `${taskExtId}:${invocationCount}`;
|
|
116
|
+
}
|
|
113
117
|
function callbackKey(taskExtId, invocationCount, branchId, nodeId) {
|
|
114
118
|
return `${taskExtId}:${invocationCount}:${branchId}:${nodeId}`;
|
|
115
119
|
}
|
|
@@ -131,13 +135,15 @@ class DurableListenerClient {
|
|
|
131
135
|
this._requestQueue = [];
|
|
132
136
|
this._pendingEventAcks = new Map();
|
|
133
137
|
this._pendingCallbacks = new Map();
|
|
134
|
-
// Completions
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
this.
|
|
138
|
+
// Completions held in server delivery order until their waiters can
|
|
139
|
+
// consume them without overtaking an earlier-delivered completion.
|
|
140
|
+
// The TTL only garbage-collects queues for invocations that died
|
|
141
|
+
// without consuming everything; the server stall-evicts long before.
|
|
142
|
+
this._orderedCompletions = new TTLMap(300000);
|
|
139
143
|
this._pendingEvictionAcks = new Map();
|
|
140
144
|
this._consecutiveFailures = 0;
|
|
145
|
+
this._drainsInProgress = new Set();
|
|
146
|
+
this._drainRerunRequests = new Set();
|
|
141
147
|
this.config = config;
|
|
142
148
|
this.client = factory.create(dispatcher_1.V1DispatcherDefinition, channel);
|
|
143
149
|
this.logger = config.logger(`DurableListener`, config.log_level);
|
|
@@ -185,7 +191,7 @@ class DurableListenerClient {
|
|
|
185
191
|
this._receiveAbort.abort();
|
|
186
192
|
}
|
|
187
193
|
this._failPendingAcks(new Error('DurableListener stopped'));
|
|
188
|
-
this.
|
|
194
|
+
this._orderedCompletions.destroy();
|
|
189
195
|
});
|
|
190
196
|
}
|
|
191
197
|
_connect() {
|
|
@@ -332,10 +338,10 @@ class DurableListenerClient {
|
|
|
332
338
|
d.reject(exc);
|
|
333
339
|
}
|
|
334
340
|
this._pendingCallbacks.clear();
|
|
335
|
-
this.
|
|
341
|
+
this._orderedCompletions.clear();
|
|
336
342
|
}
|
|
337
343
|
_handleResponse(response) {
|
|
338
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2;
|
|
344
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4, _5;
|
|
339
345
|
if (response.registerWorker) {
|
|
340
346
|
// registration acknowledged
|
|
341
347
|
}
|
|
@@ -396,14 +402,28 @@ class DurableListenerClient {
|
|
|
396
402
|
const { ref } = completed;
|
|
397
403
|
const key = callbackKey((_o = ref === null || ref === void 0 ? void 0 : ref.durableTaskExternalId) !== null && _o !== void 0 ? _o : '', (_p = ref === null || ref === void 0 ? void 0 : ref.invocationCount) !== null && _p !== void 0 ? _p : 0, (_q = ref === null || ref === void 0 ? void 0 : ref.branchId) !== null && _q !== void 0 ? _q : 0, (_r = ref === null || ref === void 0 ? void 0 : ref.nodeId) !== null && _r !== void 0 ? _r : 0);
|
|
398
404
|
const result = eventLogEntryResultFromProto(completed);
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
pending
|
|
402
|
-
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
|
|
405
|
+
const orderKey = completionOrderKey((_s = ref === null || ref === void 0 ? void 0 : ref.durableTaskExternalId) !== null && _s !== void 0 ? _s : '', (_t = ref === null || ref === void 0 ? void 0 : ref.invocationCount) !== null && _t !== void 0 ? _t : 0);
|
|
406
|
+
const queue = (_u = this._orderedCompletions.get(orderKey)) !== null && _u !== void 0 ? _u : {
|
|
407
|
+
pending: [],
|
|
408
|
+
delivered: new Set(),
|
|
409
|
+
};
|
|
410
|
+
if (!queue.delivered.has(key)) {
|
|
411
|
+
queue.delivered.add(key);
|
|
412
|
+
queue.pending.push({ key, result });
|
|
413
|
+
}
|
|
414
|
+
else if (queue.pending.every((entry) => entry.key !== key)) {
|
|
415
|
+
// Re-delivery of a completion that already drained (reconnect,
|
|
416
|
+
// worker-status re-send, or a repeated wait on a node deduped by
|
|
417
|
+
// child key). Its satisfied order was released before anything
|
|
418
|
+
// still queued, so handing it straight to a waiter keeps order.
|
|
419
|
+
const redelivered = this._pendingCallbacks.get(key);
|
|
420
|
+
if (redelivered) {
|
|
421
|
+
this._pendingCallbacks.delete(key);
|
|
422
|
+
redelivered.resolve(result);
|
|
423
|
+
}
|
|
406
424
|
}
|
|
425
|
+
this._orderedCompletions.set(orderKey, queue);
|
|
426
|
+
this._drainOrderedCompletions(orderKey);
|
|
407
427
|
}
|
|
408
428
|
else if (response.evictionAck) {
|
|
409
429
|
const ack = response.evictionAck;
|
|
@@ -418,34 +438,36 @@ class DurableListenerClient {
|
|
|
418
438
|
const evict = response.serverEvict;
|
|
419
439
|
this.logger.info(`received server eviction notification for task ${evict.durableTaskExternalId} ` +
|
|
420
440
|
`invocation ${evict.invocationCount}: ${evict.reason}`);
|
|
421
|
-
|
|
441
|
+
// onServerEvict aborts the run first so waiters settle as aborted
|
|
442
|
+
// (eviction) rather than with cleanup's generic rejection.
|
|
422
443
|
if (this.onServerEvict) {
|
|
423
444
|
this.onServerEvict(evict.durableTaskExternalId, evict.invocationCount);
|
|
424
445
|
}
|
|
446
|
+
this.cleanupTaskState(evict.durableTaskExternalId, evict.invocationCount);
|
|
425
447
|
}
|
|
426
448
|
else if (response.error) {
|
|
427
449
|
const { error } = response;
|
|
428
450
|
const { ref } = error;
|
|
429
451
|
let exc;
|
|
430
452
|
if (error.errorType === dispatcher_1.DurableTaskErrorType.DURABLE_TASK_ERROR_TYPE_NONDETERMINISM) {
|
|
431
|
-
exc = new non_determinism_error_1.NonDeterminismError((
|
|
453
|
+
exc = new non_determinism_error_1.NonDeterminismError((_v = ref === null || ref === void 0 ? void 0 : ref.durableTaskExternalId) !== null && _v !== void 0 ? _v : '', (_w = ref === null || ref === void 0 ? void 0 : ref.invocationCount) !== null && _w !== void 0 ? _w : 0, (_x = ref === null || ref === void 0 ? void 0 : ref.nodeId) !== null && _x !== void 0 ? _x : 0, error.errorMessage);
|
|
432
454
|
}
|
|
433
455
|
else {
|
|
434
456
|
exc = new Error(`Unspecified durable task error: ${error.errorMessage} (type: ${error.errorType})`);
|
|
435
457
|
}
|
|
436
|
-
const eAckKey = ackKey((
|
|
458
|
+
const eAckKey = ackKey((_y = ref === null || ref === void 0 ? void 0 : ref.durableTaskExternalId) !== null && _y !== void 0 ? _y : '', (_z = ref === null || ref === void 0 ? void 0 : ref.invocationCount) !== null && _z !== void 0 ? _z : 0);
|
|
437
459
|
const pendingAck = this._pendingEventAcks.get(eAckKey);
|
|
438
460
|
if (pendingAck) {
|
|
439
461
|
pendingAck.reject(exc);
|
|
440
462
|
this._pendingEventAcks.delete(eAckKey);
|
|
441
463
|
}
|
|
442
|
-
const eCbKey = callbackKey((
|
|
464
|
+
const eCbKey = callbackKey((_0 = ref === null || ref === void 0 ? void 0 : ref.durableTaskExternalId) !== null && _0 !== void 0 ? _0 : '', (_1 = ref === null || ref === void 0 ? void 0 : ref.invocationCount) !== null && _1 !== void 0 ? _1 : 0, (_2 = ref === null || ref === void 0 ? void 0 : ref.branchId) !== null && _2 !== void 0 ? _2 : 0, (_3 = ref === null || ref === void 0 ? void 0 : ref.nodeId) !== null && _3 !== void 0 ? _3 : 0);
|
|
443
465
|
const pendingCb = this._pendingCallbacks.get(eCbKey);
|
|
444
466
|
if (pendingCb) {
|
|
445
467
|
pendingCb.reject(exc);
|
|
446
468
|
this._pendingCallbacks.delete(eCbKey);
|
|
447
469
|
}
|
|
448
|
-
const eEvKey = evictionKey((
|
|
470
|
+
const eEvKey = evictionKey((_4 = ref === null || ref === void 0 ? void 0 : ref.durableTaskExternalId) !== null && _4 !== void 0 ? _4 : '', (_5 = ref === null || ref === void 0 ? void 0 : ref.invocationCount) !== null && _5 !== void 0 ? _5 : 0);
|
|
449
471
|
const pendingEv = this._pendingEvictionAcks.get(eEvKey);
|
|
450
472
|
if (pendingEv) {
|
|
451
473
|
pendingEv.reject(exc);
|
|
@@ -498,26 +520,87 @@ class DurableListenerClient {
|
|
|
498
520
|
return d.promise;
|
|
499
521
|
});
|
|
500
522
|
}
|
|
523
|
+
// Hands queued completions to their waiters strictly in delivery order.
|
|
524
|
+
// Stops at the first completion whose waiter has not registered yet:
|
|
525
|
+
// releasing a later completion first would resume its coroutine ahead of
|
|
526
|
+
// the recorded order and diverge the re-emitted event sequence.
|
|
527
|
+
_drainOrderedCompletions(orderKey) {
|
|
528
|
+
if (this._drainsInProgress.has(orderKey)) {
|
|
529
|
+
this._drainRerunRequests.add(orderKey);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
this._drainsInProgress.add(orderKey);
|
|
533
|
+
void this._drainLoop(orderKey).finally(() => {
|
|
534
|
+
this._drainsInProgress.delete(orderKey);
|
|
535
|
+
if (this._drainRerunRequests.delete(orderKey)) {
|
|
536
|
+
this._drainOrderedCompletions(orderKey);
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
_drainLoop(orderKey) {
|
|
541
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
542
|
+
let released = false;
|
|
543
|
+
while (true) {
|
|
544
|
+
if (released) {
|
|
545
|
+
// A macrotask boundary between releases lets the just-resumed waiter
|
|
546
|
+
// run to its next suspension point (emitting its next durable event)
|
|
547
|
+
// before the following completion is released. Resolving several
|
|
548
|
+
// deferreds back-to-back would instead resume them in the order their
|
|
549
|
+
// continuations happened to be attached, not in release order.
|
|
550
|
+
yield new Promise((resolve) => {
|
|
551
|
+
setImmediate(resolve);
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
const queue = this._orderedCompletions.get(orderKey);
|
|
555
|
+
if (!queue || queue.pending.length === 0)
|
|
556
|
+
return;
|
|
557
|
+
const [head] = queue.pending;
|
|
558
|
+
const waiter = this._pendingCallbacks.get(head.key);
|
|
559
|
+
if (!waiter)
|
|
560
|
+
return;
|
|
561
|
+
queue.pending.shift();
|
|
562
|
+
this._pendingCallbacks.delete(head.key);
|
|
563
|
+
waiter.resolve(head.result);
|
|
564
|
+
released = true;
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
// A memo's value arrives in its ack and its completion is never awaited,
|
|
569
|
+
// but the server still delivers that completion on replay, in recorded
|
|
570
|
+
// order. Register a waiter that nothing blocks on so the drain can hand the
|
|
571
|
+
// completion through instead of stalling the queue at it forever.
|
|
572
|
+
consumeCallbackWithoutBlocking(durableTaskExternalId, invocationCount, branchId, nodeId) {
|
|
573
|
+
const key = callbackKey(durableTaskExternalId, invocationCount, branchId, nodeId);
|
|
574
|
+
if (this._pendingCallbacks.has(key))
|
|
575
|
+
return;
|
|
576
|
+
const d = deferred();
|
|
577
|
+
d.promise.catch(() => { });
|
|
578
|
+
this._pendingCallbacks.set(key, d);
|
|
579
|
+
this._drainOrderedCompletions(completionOrderKey(durableTaskExternalId, invocationCount));
|
|
580
|
+
}
|
|
501
581
|
waitForCallback(durableTaskExternalId, invocationCount, branchId, nodeId, opts) {
|
|
502
582
|
return __awaiter(this, void 0, void 0, function* () {
|
|
503
|
-
const
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
this._bufferedCompletions.delete(key);
|
|
507
|
-
return early;
|
|
583
|
+
const signal = opts === null || opts === void 0 ? void 0 : opts.signal;
|
|
584
|
+
if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
|
|
585
|
+
return Promise.reject((0, abort_error_1.createAbortError)('Operation cancelled by AbortSignal'));
|
|
508
586
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
587
|
+
const key = callbackKey(durableTaskExternalId, invocationCount, branchId, nodeId);
|
|
588
|
+
let d = this._pendingCallbacks.get(key);
|
|
589
|
+
if (!d) {
|
|
590
|
+
d = deferred();
|
|
591
|
+
// A deferred can outlive its waiters (eviction aborts the caller while
|
|
592
|
+
// the entry stays registered); a later cleanupTaskState rejection must
|
|
593
|
+
// not crash the process as an unhandled rejection.
|
|
594
|
+
d.promise.catch(() => { });
|
|
595
|
+
this._pendingCallbacks.set(key, d);
|
|
596
|
+
this._drainOrderedCompletions(completionOrderKey(durableTaskExternalId, invocationCount));
|
|
597
|
+
if (this._pendingCallbacks.has(key)) {
|
|
598
|
+
this._pollWorkerStatus();
|
|
599
|
+
}
|
|
512
600
|
}
|
|
513
|
-
const d = this._pendingCallbacks.get(key);
|
|
514
|
-
const signal = opts === null || opts === void 0 ? void 0 : opts.signal;
|
|
515
601
|
if (!signal) {
|
|
516
602
|
return d.promise;
|
|
517
603
|
}
|
|
518
|
-
if (signal.aborted) {
|
|
519
|
-
return Promise.reject((0, abort_error_1.createAbortError)('Operation cancelled by AbortSignal'));
|
|
520
|
-
}
|
|
521
604
|
return new Promise((resolve, reject) => {
|
|
522
605
|
let settled = false;
|
|
523
606
|
const onAbort = () => {
|
|
@@ -544,24 +627,28 @@ class DurableListenerClient {
|
|
|
544
627
|
});
|
|
545
628
|
}
|
|
546
629
|
cleanupTaskState(durableTaskExternalId, invocationCount) {
|
|
630
|
+
// Rejecting with TaskRunTerminatedError marks any coroutine still blocked
|
|
631
|
+
// on this invocation's state as evicted rather than failed, matching
|
|
632
|
+
// Python's future cancellation on cleanup.
|
|
633
|
+
const evicted = () => new task_run_terminated_error_1.TaskRunTerminatedError('evicted', 'task state cleaned up');
|
|
547
634
|
for (const [k, d] of this._pendingCallbacks) {
|
|
548
635
|
const parts = k.split(':');
|
|
549
636
|
if (parts[0] === durableTaskExternalId && parseInt(parts[1], 10) <= invocationCount) {
|
|
550
|
-
d.reject(
|
|
637
|
+
d.reject(evicted());
|
|
551
638
|
this._pendingCallbacks.delete(k);
|
|
552
639
|
}
|
|
553
640
|
}
|
|
554
641
|
for (const [k, d] of this._pendingEventAcks) {
|
|
555
642
|
const parts = k.split(':');
|
|
556
643
|
if (parts[0] === durableTaskExternalId && parseInt(parts[1], 10) <= invocationCount) {
|
|
557
|
-
d.reject(
|
|
644
|
+
d.reject(evicted());
|
|
558
645
|
this._pendingEventAcks.delete(k);
|
|
559
646
|
}
|
|
560
647
|
}
|
|
561
|
-
for (const k of this.
|
|
648
|
+
for (const k of this._orderedCompletions.keys()) {
|
|
562
649
|
const parts = k.split(':');
|
|
563
650
|
if (parts[0] === durableTaskExternalId && parseInt(parts[1], 10) <= invocationCount) {
|
|
564
|
-
this.
|
|
651
|
+
this._orderedCompletions.delete(k);
|
|
565
652
|
}
|
|
566
653
|
}
|
|
567
654
|
}
|
|
@@ -350,7 +350,10 @@ export declare class Api<SecurityDataType = unknown> extends HttpClient<Security
|
|
|
350
350
|
* @request GET:/api/v1/stable/workflow-runs/{v1-workflow-run}
|
|
351
351
|
* @secure
|
|
352
352
|
*/
|
|
353
|
-
v1WorkflowRunGet: (v1WorkflowRun: string,
|
|
353
|
+
v1WorkflowRunGet: (v1WorkflowRun: string, query?: {
|
|
354
|
+
/** Whether to include the DAG orchestrator's task events, which are hidden by default */
|
|
355
|
+
includeOrchestratorEvents?: boolean;
|
|
356
|
+
}, params?: RequestParams) => Promise<import("axios").AxiosResponse<V1WorkflowRunDetails, any, {}>>;
|
|
354
357
|
/**
|
|
355
358
|
* @description Get the status of a workflow run.
|
|
356
359
|
*
|
|
@@ -2024,6 +2027,8 @@ export declare class Api<SecurityDataType = unknown> extends HttpClient<Security
|
|
|
2024
2027
|
statuses?: WorkerStatus[];
|
|
2025
2028
|
/** Filter by worker labels */
|
|
2026
2029
|
labels?: string[];
|
|
2030
|
+
/** Whether to include engine-managed operator workers, which are hidden by default */
|
|
2031
|
+
includeOperators?: boolean;
|
|
2027
2032
|
}, params?: RequestParams) => Promise<import("axios").AxiosResponse<WorkerList, any, {}>>;
|
|
2028
2033
|
/**
|
|
2029
2034
|
* @description Update a worker
|
|
@@ -175,7 +175,7 @@ class Api extends http_client_1.HttpClient {
|
|
|
175
175
|
* @request GET:/api/v1/stable/workflow-runs/{v1-workflow-run}
|
|
176
176
|
* @secure
|
|
177
177
|
*/
|
|
178
|
-
this.v1WorkflowRunGet = (v1WorkflowRun, params = {}) => this.request(Object.assign({ path: `/api/v1/stable/workflow-runs/${v1WorkflowRun}`, method: 'GET', secure: true, format: 'json' }, params));
|
|
178
|
+
this.v1WorkflowRunGet = (v1WorkflowRun, query, params = {}) => this.request(Object.assign({ path: `/api/v1/stable/workflow-runs/${v1WorkflowRun}`, method: 'GET', query: query, secure: true, format: 'json' }, params));
|
|
179
179
|
/**
|
|
180
180
|
* @description Get the status of a workflow run.
|
|
181
181
|
*
|
|
@@ -25,7 +25,8 @@ export declare enum PullRequestState {
|
|
|
25
25
|
export declare enum FeatureFlagId {
|
|
26
26
|
TenantLogWorkflowFilterEnabled = "tenant-log-workflow-filter-enabled",
|
|
27
27
|
TraceMinimapEnabled = "trace-minimap-enabled",
|
|
28
|
-
OrganizationSsoEnabled = "organization-sso-enabled"
|
|
28
|
+
OrganizationSsoEnabled = "organization-sso-enabled",
|
|
29
|
+
OperatorDetailsEnabled = "operator-details-enabled"
|
|
29
30
|
}
|
|
30
31
|
export declare enum WebhookWorkerRequestMethod {
|
|
31
32
|
GET = "GET",
|
|
@@ -2328,6 +2329,13 @@ export interface Worker {
|
|
|
2328
2329
|
*/
|
|
2329
2330
|
webhookId?: string;
|
|
2330
2331
|
runtimeInfo?: WorkerRuntimeInfo;
|
|
2332
|
+
/**
|
|
2333
|
+
* The id of the operator that owns this worker, if it is an engine-managed operator worker.
|
|
2334
|
+
* @format uuid
|
|
2335
|
+
*/
|
|
2336
|
+
operatorId?: string;
|
|
2337
|
+
/** The number of durable task runs owned by this operator that are currently evicted while waiting on durable events. Evicted runs hold no slots. Only set for operator workers. */
|
|
2338
|
+
evictedDurableTaskCount?: number;
|
|
2331
2339
|
}
|
|
2332
2340
|
export interface WorkerList {
|
|
2333
2341
|
pagination?: PaginationResponse;
|
|
@@ -46,6 +46,7 @@ var FeatureFlagId;
|
|
|
46
46
|
FeatureFlagId["TenantLogWorkflowFilterEnabled"] = "tenant-log-workflow-filter-enabled";
|
|
47
47
|
FeatureFlagId["TraceMinimapEnabled"] = "trace-minimap-enabled";
|
|
48
48
|
FeatureFlagId["OrganizationSsoEnabled"] = "organization-sso-enabled";
|
|
49
|
+
FeatureFlagId["OperatorDetailsEnabled"] = "operator-details-enabled";
|
|
49
50
|
})(FeatureFlagId || (exports.FeatureFlagId = FeatureFlagId = {}));
|
|
50
51
|
var WebhookWorkerRequestMethod;
|
|
51
52
|
(function (WebhookWorkerRequestMethod) {
|
package/package.json
CHANGED
package/protoc/v1/workflows.d.ts
CHANGED
|
@@ -170,6 +170,15 @@ export interface DefaultFilter {
|
|
|
170
170
|
/** (optional) the payload for the filter, if any. A JSON object as a string. */
|
|
171
171
|
payload?: Uint8Array | undefined;
|
|
172
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* Concurrency declares one entry in a concurrency chain. Entries are processed in array
|
|
175
|
+
* order, so a tenant-scoped entry may come before or after a workflow-scoped one.
|
|
176
|
+
*
|
|
177
|
+
* A tenant-scoped entry (is_tenant_scoped = true) defines (or updates in place) a strategy
|
|
178
|
+
* shared across workflows, keyed by name: every task declaring the same name consumes the
|
|
179
|
+
* same concurrency limit. Registrations whose chains order the same tenant-scoped
|
|
180
|
+
* strategies inconsistently are rejected, since inconsistent orders can deadlock.
|
|
181
|
+
*/
|
|
173
182
|
export interface Concurrency {
|
|
174
183
|
/** (required) the expression to use for concurrency */
|
|
175
184
|
expression: string;
|
|
@@ -177,6 +186,12 @@ export interface Concurrency {
|
|
|
177
186
|
maxRuns?: number | undefined;
|
|
178
187
|
/** (optional) the strategy to use when the concurrency limit is reached, default CANCEL_IN_PROGRESS */
|
|
179
188
|
limitStrategy?: ConcurrencyLimitStrategy | undefined;
|
|
189
|
+
/** (required when is_tenant_scoped) the strategy name; unique per tenant for tenant-scoped strategies */
|
|
190
|
+
name?: string | undefined;
|
|
191
|
+
/** (optional) when true, the entry is a tenant-scoped strategy shared across workflows, default false */
|
|
192
|
+
isTenantScoped?: boolean | undefined;
|
|
193
|
+
/** (optional) CEL expression over task input returning the max runs for that task's concurrency group; the group's effective limit is the value from its most recently created task. Overrides max_runs per group; a non-integer or negative result fails the task, and 0 holds the group until a newer task raises the limit */
|
|
194
|
+
maxRunsExpression?: string | undefined;
|
|
180
195
|
}
|
|
181
196
|
export interface TaskBatchConfig {
|
|
182
197
|
/** (required) maximum items per batch */
|
package/protoc/v1/workflows.js
CHANGED
|
@@ -1794,7 +1794,14 @@ exports.DefaultFilter = {
|
|
|
1794
1794
|
},
|
|
1795
1795
|
};
|
|
1796
1796
|
function createBaseConcurrency() {
|
|
1797
|
-
return {
|
|
1797
|
+
return {
|
|
1798
|
+
expression: '',
|
|
1799
|
+
maxRuns: undefined,
|
|
1800
|
+
limitStrategy: undefined,
|
|
1801
|
+
name: undefined,
|
|
1802
|
+
isTenantScoped: undefined,
|
|
1803
|
+
maxRunsExpression: undefined,
|
|
1804
|
+
};
|
|
1798
1805
|
}
|
|
1799
1806
|
exports.Concurrency = {
|
|
1800
1807
|
encode(message, writer = new wire_1.BinaryWriter()) {
|
|
@@ -1807,6 +1814,15 @@ exports.Concurrency = {
|
|
|
1807
1814
|
if (message.limitStrategy !== undefined) {
|
|
1808
1815
|
writer.uint32(24).int32(message.limitStrategy);
|
|
1809
1816
|
}
|
|
1817
|
+
if (message.name !== undefined) {
|
|
1818
|
+
writer.uint32(34).string(message.name);
|
|
1819
|
+
}
|
|
1820
|
+
if (message.isTenantScoped !== undefined) {
|
|
1821
|
+
writer.uint32(40).bool(message.isTenantScoped);
|
|
1822
|
+
}
|
|
1823
|
+
if (message.maxRunsExpression !== undefined) {
|
|
1824
|
+
writer.uint32(50).string(message.maxRunsExpression);
|
|
1825
|
+
}
|
|
1810
1826
|
return writer;
|
|
1811
1827
|
},
|
|
1812
1828
|
decode(input, length) {
|
|
@@ -1837,6 +1853,27 @@ exports.Concurrency = {
|
|
|
1837
1853
|
message.limitStrategy = reader.int32();
|
|
1838
1854
|
continue;
|
|
1839
1855
|
}
|
|
1856
|
+
case 4: {
|
|
1857
|
+
if (tag !== 34) {
|
|
1858
|
+
break;
|
|
1859
|
+
}
|
|
1860
|
+
message.name = reader.string();
|
|
1861
|
+
continue;
|
|
1862
|
+
}
|
|
1863
|
+
case 5: {
|
|
1864
|
+
if (tag !== 40) {
|
|
1865
|
+
break;
|
|
1866
|
+
}
|
|
1867
|
+
message.isTenantScoped = reader.bool();
|
|
1868
|
+
continue;
|
|
1869
|
+
}
|
|
1870
|
+
case 6: {
|
|
1871
|
+
if (tag !== 50) {
|
|
1872
|
+
break;
|
|
1873
|
+
}
|
|
1874
|
+
message.maxRunsExpression = reader.string();
|
|
1875
|
+
continue;
|
|
1876
|
+
}
|
|
1840
1877
|
}
|
|
1841
1878
|
if ((tag & 7) === 4 || tag === 0) {
|
|
1842
1879
|
break;
|
|
@@ -1858,6 +1895,17 @@ exports.Concurrency = {
|
|
|
1858
1895
|
: isSet(object.limit_strategy)
|
|
1859
1896
|
? concurrencyLimitStrategyFromJSON(object.limit_strategy)
|
|
1860
1897
|
: undefined,
|
|
1898
|
+
name: isSet(object.name) ? globalThis.String(object.name) : undefined,
|
|
1899
|
+
isTenantScoped: isSet(object.isTenantScoped)
|
|
1900
|
+
? globalThis.Boolean(object.isTenantScoped)
|
|
1901
|
+
: isSet(object.is_tenant_scoped)
|
|
1902
|
+
? globalThis.Boolean(object.is_tenant_scoped)
|
|
1903
|
+
: undefined,
|
|
1904
|
+
maxRunsExpression: isSet(object.maxRunsExpression)
|
|
1905
|
+
? globalThis.String(object.maxRunsExpression)
|
|
1906
|
+
: isSet(object.max_runs_expression)
|
|
1907
|
+
? globalThis.String(object.max_runs_expression)
|
|
1908
|
+
: undefined,
|
|
1861
1909
|
};
|
|
1862
1910
|
},
|
|
1863
1911
|
toJSON(message) {
|
|
@@ -1871,17 +1919,29 @@ exports.Concurrency = {
|
|
|
1871
1919
|
if (message.limitStrategy !== undefined) {
|
|
1872
1920
|
obj.limitStrategy = concurrencyLimitStrategyToJSON(message.limitStrategy);
|
|
1873
1921
|
}
|
|
1922
|
+
if (message.name !== undefined) {
|
|
1923
|
+
obj.name = message.name;
|
|
1924
|
+
}
|
|
1925
|
+
if (message.isTenantScoped !== undefined) {
|
|
1926
|
+
obj.isTenantScoped = message.isTenantScoped;
|
|
1927
|
+
}
|
|
1928
|
+
if (message.maxRunsExpression !== undefined) {
|
|
1929
|
+
obj.maxRunsExpression = message.maxRunsExpression;
|
|
1930
|
+
}
|
|
1874
1931
|
return obj;
|
|
1875
1932
|
},
|
|
1876
1933
|
create(base) {
|
|
1877
1934
|
return exports.Concurrency.fromPartial(base !== null && base !== void 0 ? base : {});
|
|
1878
1935
|
},
|
|
1879
1936
|
fromPartial(object) {
|
|
1880
|
-
var _a, _b, _c;
|
|
1937
|
+
var _a, _b, _c, _d, _e, _f;
|
|
1881
1938
|
const message = createBaseConcurrency();
|
|
1882
1939
|
message.expression = (_a = object.expression) !== null && _a !== void 0 ? _a : '';
|
|
1883
1940
|
message.maxRuns = (_b = object.maxRuns) !== null && _b !== void 0 ? _b : undefined;
|
|
1884
1941
|
message.limitStrategy = (_c = object.limitStrategy) !== null && _c !== void 0 ? _c : undefined;
|
|
1942
|
+
message.name = (_d = object.name) !== null && _d !== void 0 ? _d : undefined;
|
|
1943
|
+
message.isTenantScoped = (_e = object.isTenantScoped) !== null && _e !== void 0 ? _e : undefined;
|
|
1944
|
+
message.maxRunsExpression = (_f = object.maxRunsExpression) !== null && _f !== void 0 ? _f : undefined;
|
|
1885
1945
|
return message;
|
|
1886
1946
|
},
|
|
1887
1947
|
};
|
|
@@ -381,6 +381,8 @@ export declare class DurableContext<T, K = {}> extends Context<T, K> {
|
|
|
381
381
|
private _evictionManager;
|
|
382
382
|
private _engineVersion;
|
|
383
383
|
private _waitKey;
|
|
384
|
+
private _sendEventLock;
|
|
385
|
+
private _serializeSendEvent;
|
|
384
386
|
constructor(action: Action, v1: HatchetClient, worker: InternalWorker, durableListener: DurableListenerClient, evictionManager?: DurableEvictionManager, engineVersion?: string);
|
|
385
387
|
get supportsEviction(): boolean;
|
|
386
388
|
get durableListener(): DurableListenerClient;
|
|
@@ -692,9 +692,27 @@ exports.Context = Context;
|
|
|
692
692
|
* It extends the Context class and includes additional methods for durable execution like sleepFor and waitFor.
|
|
693
693
|
*/
|
|
694
694
|
class DurableContext extends Context {
|
|
695
|
+
_serializeSendEvent(send) {
|
|
696
|
+
// Re-check cancellation when the queued send actually executes: once this
|
|
697
|
+
// invocation is aborted (evicted), a queued send must not reach the
|
|
698
|
+
// server, where it could race the eviction and append to the event log
|
|
699
|
+
// out of recorded order. Python gets this for free from task
|
|
700
|
+
// cancellation killing coroutines queued on the send lock.
|
|
701
|
+
const runSend = () => {
|
|
702
|
+
this.throwIfCancelled();
|
|
703
|
+
return send();
|
|
704
|
+
};
|
|
705
|
+
const result = this._sendEventLock.then(runSend, runSend);
|
|
706
|
+
this._sendEventLock = result.then(() => undefined, () => undefined);
|
|
707
|
+
return result;
|
|
708
|
+
}
|
|
695
709
|
constructor(action, v1, worker, durableListener, evictionManager, engineVersion) {
|
|
696
710
|
super(action, v1, worker);
|
|
697
711
|
this._waitKey = 0;
|
|
712
|
+
// Serializes sendEvent calls from concurrent coroutines in this invocation.
|
|
713
|
+
// The listener keys pending acks by (task, invocation), so overlapping sends
|
|
714
|
+
// would overwrite each other's ack and cross-wire branch/node assignments.
|
|
715
|
+
this._sendEventLock = Promise.resolve();
|
|
698
716
|
this._durableListener = durableListener;
|
|
699
717
|
this._evictionManager = evictionManager;
|
|
700
718
|
this._engineVersion = engineVersion;
|
|
@@ -767,14 +785,14 @@ class DurableContext extends Context {
|
|
|
767
785
|
}
|
|
768
786
|
const rendered = (0, conditions_1.Render)(condition_1.Action.CREATE, conditions);
|
|
769
787
|
const pbConditions = (0, transformer_1.conditionsToPb)(rendered, this.v1.config.namespace);
|
|
770
|
-
const ack = yield this._durableListener.sendEvent(this.action.taskRunExternalId, this.invocationCount, {
|
|
788
|
+
const ack = yield this._serializeSendEvent(() => this._durableListener.sendEvent(this.action.taskRunExternalId, this.invocationCount, {
|
|
771
789
|
kind: 'waitFor',
|
|
772
790
|
waitForConditions: {
|
|
773
791
|
sleepConditions: pbConditions.sleepConditions,
|
|
774
792
|
userEventConditions: pbConditions.userEventConditions,
|
|
775
793
|
},
|
|
776
794
|
label,
|
|
777
|
-
});
|
|
795
|
+
}));
|
|
778
796
|
const resourceId = rendered
|
|
779
797
|
.map((c) => c.base.readableDataKey)
|
|
780
798
|
.filter(Boolean)
|
|
@@ -787,9 +805,8 @@ class DurableContext extends Context {
|
|
|
787
805
|
}
|
|
788
806
|
waitForEvent(key, expression, payloadSchema, scope, lookbackWindow, label) {
|
|
789
807
|
return __awaiter(this, void 0, void 0, function* () {
|
|
790
|
-
const now = yield this.now();
|
|
791
808
|
const considerEventsSince = lookbackWindow
|
|
792
|
-
? new Date(now.getTime() - (0, duration_1.durationToMs)(lookbackWindow)).toISOString()
|
|
809
|
+
? new Date((yield this.now()).getTime() - (0, duration_1.durationToMs)(lookbackWindow)).toISOString()
|
|
793
810
|
: undefined;
|
|
794
811
|
const res = yield this.waitFor({
|
|
795
812
|
eventKey: key,
|
|
@@ -918,10 +935,10 @@ class DurableContext extends Context {
|
|
|
918
935
|
const { triggerOpts } = this._buildTriggerOpts(child.workflow, child.input, child.options);
|
|
919
936
|
return triggerOpts;
|
|
920
937
|
});
|
|
921
|
-
const ack = yield this._durableListener.sendEvent(this.action.taskRunExternalId, this.invocationCount, {
|
|
938
|
+
const ack = yield this._serializeSendEvent(() => this._durableListener.sendEvent(this.action.taskRunExternalId, this.invocationCount, {
|
|
922
939
|
kind: 'runChildren',
|
|
923
940
|
triggerOpts: triggerOptsList,
|
|
924
|
-
});
|
|
941
|
+
}));
|
|
925
942
|
const results = yield Promise.all(ack.runEntries.map((entry) => this.withEvictionWait('runChild', `workflow:bulk-child`, () => __awaiter(this, void 0, void 0, function* () {
|
|
926
943
|
const result = yield this._durableListener.waitForCallback(this.action.taskRunExternalId, this.invocationCount, entry.branchId, entry.nodeId, { signal: this.abortController.signal });
|
|
927
944
|
if (result.isFailure) {
|
|
@@ -946,10 +963,11 @@ class DurableContext extends Context {
|
|
|
946
963
|
return fn();
|
|
947
964
|
}
|
|
948
965
|
const memoKey = computeMemoKey(this.action.taskRunExternalId, deps);
|
|
949
|
-
const ack = yield this._durableListener.sendEvent(this.action.taskRunExternalId, this.invocationCount, {
|
|
966
|
+
const ack = yield this._serializeSendEvent(() => this._durableListener.sendEvent(this.action.taskRunExternalId, this.invocationCount, {
|
|
950
967
|
kind: 'memo',
|
|
951
968
|
memoKey,
|
|
952
|
-
});
|
|
969
|
+
}));
|
|
970
|
+
this._durableListener.consumeCallbackWithoutBlocking(this.action.taskRunExternalId, this.invocationCount, ack.branchId, ack.nodeId);
|
|
953
971
|
if (ack.memoAlreadyExisted && ack.memoResultPayload && ack.memoResultPayload.length > 0) {
|
|
954
972
|
const serialized = new TextDecoder().decode(ack.memoResultPayload);
|
|
955
973
|
return JSON.parse(serialized);
|
|
@@ -21,7 +21,7 @@ export declare class DurableEvictionManager {
|
|
|
21
21
|
private _ticking;
|
|
22
22
|
constructor(opts: {
|
|
23
23
|
durableSlots: number;
|
|
24
|
-
cancelLocal: (key: ActionKey) => void;
|
|
24
|
+
cancelLocal: (key: ActionKey, invocationCount: number) => void;
|
|
25
25
|
requestEvictionWithAck: (key: ActionKey, rec: DurableRunRecord) => Promise<void>;
|
|
26
26
|
config?: DurableEvictionConfig;
|
|
27
27
|
cache?: DurableEvictionCache;
|