@hatchet-dev/typescript-sdk 1.35.1 → 1.37.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/rest/generated/Api.d.ts +85 -1
- package/clients/rest/generated/Api.js +30 -0
- package/clients/rest/generated/data-contracts.d.ts +11 -0
- package/package.json +1 -1
- package/protoc/dispatcher/dispatcher.d.ts +4 -0
- package/protoc/dispatcher/dispatcher.js +42 -2
- package/protoc/v1/streams.d.ts +34 -0
- package/protoc/v1/streams.js +279 -5
- package/protoc-es/dispatcher/dispatcher_pb.d.ts +12 -0
- package/protoc-es/dispatcher/dispatcher_pb.js +1 -1
- package/protoc-es/v1/streams_pb.d.ts +79 -0
- package/protoc-es/v1/streams_pb.js +12 -2
- package/v1/client/duration.js +3 -3
- package/v1/client/features/streams.d.ts +22 -0
- package/v1/client/features/streams.js +93 -13
- package/v1/client/worker/engine-version.d.ts +2 -0
- package/v1/client/worker/engine-version.js +7 -0
- package/v1/client/worker/worker-internal.d.ts +2 -0
- package/v1/client/worker/worker-internal.js +20 -1
- package/v1/examples/durable-streams/benchmark.d.ts +34 -0
- package/v1/examples/durable-streams/benchmark.js +120 -0
- package/v1/examples/e2e-worker.js +52 -49
- package/v1/examples/retry_after/run.d.ts +1 -0
- package/v1/examples/retry_after/run.js +21 -0
- package/v1/examples/retry_after/worker.d.ts +1 -0
- package/v1/examples/retry_after/worker.js +24 -0
- package/v1/examples/retry_after/workflow.d.ts +9 -0
- package/v1/examples/retry_after/workflow.js +40 -0
- package/v1/task.d.ts +13 -0
- package/v1/task.js +14 -1
- package/version.d.ts +1 -1
- package/version.js +1 -1
- package/v1/examples/durable-streams/publish-latency.d.ts +0 -15
- package/v1/examples/durable-streams/publish-latency.js +0 -50
|
@@ -34,6 +34,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
34
34
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
35
35
|
exports.StreamsClient = void 0;
|
|
36
36
|
const crypto_1 = require("crypto");
|
|
37
|
+
const axios_1 = require("axios");
|
|
37
38
|
const nice_grpc_1 = require("nice-grpc");
|
|
38
39
|
const grpc_helpers_1 = require("../../../util/grpc-helpers");
|
|
39
40
|
const grpc_error_1 = require("../../../util/grpc-error");
|
|
@@ -58,7 +59,27 @@ const permanentSubscribeErrors = new Set([
|
|
|
58
59
|
]);
|
|
59
60
|
const resubscribeMinDelayMs = 250;
|
|
60
61
|
const resubscribeMaxDelayMs = 10000;
|
|
62
|
+
// the server's limit for a payload sent over gRPC (MaxStreamMessagePayloadBytes)
|
|
63
|
+
const maxInlinePayloadBytes = 4 * 1024 * 1024 - 5 * 1024;
|
|
64
|
+
// the server's limit for an uploaded payload (MaxStreamUploadedPayloadBytes)
|
|
65
|
+
const maxUploadedPayloadBytes = 64 * 1024 * 1024;
|
|
66
|
+
const payloadRequestAttempts = 3;
|
|
67
|
+
function httpStatus(err) {
|
|
68
|
+
var _a;
|
|
69
|
+
return (0, axios_1.isAxiosError)(err) ? (_a = err.response) === null || _a === void 0 ? void 0 : _a.status : undefined;
|
|
70
|
+
}
|
|
71
|
+
// a payload fetch that got no response, a 5xx or a 429 may succeed later
|
|
72
|
+
function isTemporaryHttpError(err) {
|
|
73
|
+
if (!(0, axios_1.isAxiosError)(err)) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
const status = httpStatus(err);
|
|
77
|
+
return status === undefined || status >= 500 || status === 429;
|
|
78
|
+
}
|
|
61
79
|
function shouldResubscribe(err, attempt) {
|
|
80
|
+
if (isTemporaryHttpError(err)) {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
62
83
|
const code = (0, grpc_error_1.getGrpcErrorCode)(err);
|
|
63
84
|
if (code === undefined || permanentSubscribeErrors.has(code)) {
|
|
64
85
|
return false;
|
|
@@ -82,6 +103,7 @@ class StreamsClient {
|
|
|
82
103
|
// stored promise always resolves, so one failure doesn't block later calls
|
|
83
104
|
this.publishChains = new Map();
|
|
84
105
|
this._config = client.config;
|
|
106
|
+
this._client = client;
|
|
85
107
|
}
|
|
86
108
|
get grpc() {
|
|
87
109
|
if (!this._grpc) {
|
|
@@ -90,21 +112,46 @@ class StreamsClient {
|
|
|
90
112
|
}
|
|
91
113
|
return this._grpc;
|
|
92
114
|
}
|
|
93
|
-
|
|
94
|
-
|
|
115
|
+
// retries a 5xx or network failure; a 4xx won't succeed on retry
|
|
116
|
+
static withRetries(request) {
|
|
117
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
118
|
+
for (let attempt = 1;; attempt += 1) {
|
|
119
|
+
try {
|
|
120
|
+
return yield request();
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
const status = httpStatus(err);
|
|
124
|
+
if ((status !== undefined && status < 500) || attempt >= payloadRequestAttempts) {
|
|
125
|
+
throw err;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
uploadPayload(payload) {
|
|
132
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
133
|
+
const res = yield StreamsClient.withRetries(() => this._client.api.v1StreamPayloadUpload(this._config.tenant_id, {
|
|
134
|
+
payload: new Blob([payload]),
|
|
135
|
+
}));
|
|
136
|
+
return res.data.ref;
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
fetchPayload(payloadRef, signal) {
|
|
140
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
141
|
+
const res = yield StreamsClient.withRetries(() => this._client.api.v1StreamPayloadGet(this._config.tenant_id, { ref: payloadRef }, { format: 'arraybuffer', signal }));
|
|
142
|
+
return new Uint8Array(res.data);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
publishOrdered(key_1, namespace_1, topic_1, body_1) {
|
|
146
|
+
return __awaiter(this, arguments, void 0, function* (key, namespace, topic, body, retryGap = true) {
|
|
95
147
|
let producer = this.producers.get(key);
|
|
96
148
|
if (!producer) {
|
|
97
149
|
producer = { producerId: (0, crypto_1.randomUUID)(), seq: 0 };
|
|
98
150
|
this.producers.set(key, producer);
|
|
99
151
|
}
|
|
100
152
|
try {
|
|
101
|
-
yield this.grpc.publish({
|
|
102
|
-
|
|
103
|
-
topic,
|
|
104
|
-
payload,
|
|
105
|
-
producerId: producer.producerId,
|
|
106
|
-
producerSeq: producer.seq,
|
|
107
|
-
});
|
|
153
|
+
yield this.grpc.publish(Object.assign(Object.assign({ namespace,
|
|
154
|
+
topic }, body), { producerId: producer.producerId, producerSeq: producer.seq }));
|
|
108
155
|
}
|
|
109
156
|
catch (err) {
|
|
110
157
|
// it may still land, and a different payload under its seq would be dropped as a duplicate
|
|
@@ -113,7 +160,7 @@ class StreamsClient {
|
|
|
113
160
|
}
|
|
114
161
|
// a gap stored nothing (e.g. the watermark passed cursor retention), so resend as the new producer
|
|
115
162
|
if (retryGap && (0, grpc_error_1.getGrpcErrorCode)(err) === nice_grpc_1.Status.FAILED_PRECONDITION) {
|
|
116
|
-
return this.publishOrdered(key, namespace, topic,
|
|
163
|
+
return this.publishOrdered(key, namespace, topic, body, false);
|
|
117
164
|
}
|
|
118
165
|
throw err;
|
|
119
166
|
}
|
|
@@ -133,12 +180,41 @@ class StreamsClient {
|
|
|
133
180
|
const payload = typeof message === 'string' ? new TextEncoder().encode(message) : message;
|
|
134
181
|
const namespace = (_a = options === null || options === void 0 ? void 0 : options.namespace) !== null && _a !== void 0 ? _a : '';
|
|
135
182
|
const key = `${namespace}\u0000${topic}`;
|
|
183
|
+
if (payload.byteLength > maxUploadedPayloadBytes) {
|
|
184
|
+
throw new Error(`payload exceeds maximum size of ${maxUploadedPayloadBytes} bytes`);
|
|
185
|
+
}
|
|
186
|
+
// uploaded before taking its place in the topic's order, so uploads overlap
|
|
187
|
+
const uploaded = payload.byteLength > maxInlinePayloadBytes ? this.uploadPayload(payload) : undefined;
|
|
188
|
+
// awaited in order below; this only keeps an early failure from reporting as unhandled
|
|
189
|
+
uploaded === null || uploaded === void 0 ? void 0 : uploaded.catch(() => { });
|
|
136
190
|
const previous = (_b = this.publishChains.get(key)) !== null && _b !== void 0 ? _b : Promise.resolve();
|
|
137
|
-
const publishPromise = previous.then(() => this
|
|
191
|
+
const publishPromise = previous.then(() => __awaiter(this, void 0, void 0, function* () {
|
|
192
|
+
return this.publishOrdered(key, namespace, topic, uploaded ? { payloadRef: yield uploaded } : { payload });
|
|
193
|
+
}));
|
|
138
194
|
this.publishChains.set(key, publishPromise.catch(() => { }));
|
|
139
195
|
return publishPromise;
|
|
140
196
|
});
|
|
141
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* Describes topic's retained messages. Rejects with a NOT_FOUND gRPC error
|
|
200
|
+
* for a topic nothing was published to.
|
|
201
|
+
* @param topic - the topic to describe
|
|
202
|
+
* @param options - optional namespace override
|
|
203
|
+
*/
|
|
204
|
+
topicMetadata(topic, options) {
|
|
205
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
206
|
+
var _a;
|
|
207
|
+
const res = yield this.grpc.getTopicMetadata({ namespace: (_a = options === null || options === void 0 ? void 0 : options.namespace) !== null && _a !== void 0 ? _a : '', topic });
|
|
208
|
+
return {
|
|
209
|
+
namespace: res.namespace,
|
|
210
|
+
topic: res.topic,
|
|
211
|
+
tenantId: res.tenantId,
|
|
212
|
+
messageCount: res.messageCount,
|
|
213
|
+
latestCursor: res.latestCursor,
|
|
214
|
+
lastPublishedAt: res.lastPublishedAt,
|
|
215
|
+
};
|
|
216
|
+
});
|
|
217
|
+
}
|
|
142
218
|
/**
|
|
143
219
|
* Yields topic's messages after options.cursor, or from the oldest retained.
|
|
144
220
|
* If the connection drops, it resubscribes from the last message yielded, so
|
|
@@ -167,11 +243,15 @@ class StreamsClient {
|
|
|
167
243
|
if (msg.hangup) {
|
|
168
244
|
return yield __await(void 0);
|
|
169
245
|
}
|
|
170
|
-
failures = 0;
|
|
171
246
|
for (const entry of msg.entries) {
|
|
247
|
+
const payload = entry.payloadRef
|
|
248
|
+
? yield __await(this.fetchPayload(entry.payloadRef, signal))
|
|
249
|
+
: entry.payload;
|
|
172
250
|
({ cursor } = entry);
|
|
251
|
+
// reset only on delivery, so a payload fetch failing on every reconnect still backs off
|
|
252
|
+
failures = 0;
|
|
173
253
|
yield yield __await({
|
|
174
|
-
payload
|
|
254
|
+
payload,
|
|
175
255
|
cursor: entry.cursor,
|
|
176
256
|
createdAt: entry.createdAt,
|
|
177
257
|
});
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export declare const MinEngineVersion: {
|
|
2
2
|
readonly SLOT_CONFIG: "v0.78.23";
|
|
3
3
|
readonly DURABLE_EVICTION: "v0.80.0";
|
|
4
|
+
readonly RETRY_AFTER: "v0.111.0";
|
|
4
5
|
};
|
|
6
|
+
export declare function supportsRetryAfter(engineVersion: string | undefined): boolean;
|
|
5
7
|
export declare function supportsEviction(engineVersion: string | undefined): boolean;
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.MinEngineVersion = void 0;
|
|
4
|
+
exports.supportsRetryAfter = supportsRetryAfter;
|
|
4
5
|
exports.supportsEviction = supportsEviction;
|
|
5
6
|
const deprecation_1 = require("./deprecated/deprecation");
|
|
6
7
|
exports.MinEngineVersion = {
|
|
7
8
|
SLOT_CONFIG: 'v0.78.23',
|
|
8
9
|
DURABLE_EVICTION: 'v0.80.0',
|
|
10
|
+
RETRY_AFTER: 'v0.111.0',
|
|
9
11
|
};
|
|
12
|
+
function supportsRetryAfter(engineVersion) {
|
|
13
|
+
if (!engineVersion)
|
|
14
|
+
return false;
|
|
15
|
+
return !(0, deprecation_1.semverLessThan)(engineVersion, exports.MinEngineVersion.RETRY_AFTER);
|
|
16
|
+
}
|
|
10
17
|
function supportsEviction(engineVersion) {
|
|
11
18
|
if (!engineVersion)
|
|
12
19
|
return false;
|
|
@@ -69,6 +69,7 @@ export declare class InternalWorker {
|
|
|
69
69
|
* Keep the signature in sync with the instrumentor wrapper.
|
|
70
70
|
*/
|
|
71
71
|
handleStartStepRun(action: Action): Promise<Error | undefined>;
|
|
72
|
+
private retryAfterMs;
|
|
72
73
|
getStepActionEvent(action: Action, eventType: StepActionEventType, shouldNotRetry: boolean, payload?: any, retryCount?: number): StepActionEvent;
|
|
73
74
|
/**
|
|
74
75
|
* Handles a START_BATCH action: invokes the registered batch task handler once with
|
|
@@ -88,6 +89,7 @@ export declare class InternalWorker {
|
|
|
88
89
|
getBatchActionEvent(action: Action, eventType: StepActionEventType, items: {
|
|
89
90
|
taskRunExternalId: string;
|
|
90
91
|
eventPayload: string;
|
|
92
|
+
retryAfterMs?: number;
|
|
91
93
|
}[]): BatchActionEvent;
|
|
92
94
|
getGroupKeyActionEvent(action: Action, eventType: GroupKeyActionEventType, payload?: any): GroupKeyActionEvent;
|
|
93
95
|
/**
|
|
@@ -349,6 +349,7 @@ class InternalWorker {
|
|
|
349
349
|
});
|
|
350
350
|
const failure = (error) => __awaiter(this, void 0, void 0, function* () {
|
|
351
351
|
const shouldNotRetry = error instanceof task_1.NonRetryableError;
|
|
352
|
+
const retryAfterMs = this.retryAfterMs(error, actionId);
|
|
352
353
|
try {
|
|
353
354
|
if (context.cancelled) {
|
|
354
355
|
return;
|
|
@@ -366,6 +367,7 @@ class InternalWorker {
|
|
|
366
367
|
message: error === null || error === void 0 ? void 0 : error.message,
|
|
367
368
|
stack: error === null || error === void 0 ? void 0 : error.stack,
|
|
368
369
|
}, action.retryCount);
|
|
370
|
+
event.retryAfterMs = retryAfterMs;
|
|
369
371
|
yield this.client.dispatcher.sendStepActionEvent(event);
|
|
370
372
|
}
|
|
371
373
|
catch (e) {
|
|
@@ -423,6 +425,18 @@ class InternalWorker {
|
|
|
423
425
|
}
|
|
424
426
|
});
|
|
425
427
|
}
|
|
428
|
+
retryAfterMs(error, actionId) {
|
|
429
|
+
var _a;
|
|
430
|
+
if (!(error instanceof task_1.RetryAfterError)) {
|
|
431
|
+
return undefined;
|
|
432
|
+
}
|
|
433
|
+
if (!(0, engine_version_1.supportsRetryAfter)(this.engineVersion)) {
|
|
434
|
+
this.logger.warn(`RetryAfterError requires engine >= ${engine_version_1.MinEngineVersion.RETRY_AFTER} ` +
|
|
435
|
+
`(connected: ${(_a = this.engineVersion) !== null && _a !== void 0 ? _a : 'unknown'}). Task ${actionId} threw it, so the engine ` +
|
|
436
|
+
`will handle the failure with the task's retry policy instead of the requested delay.`);
|
|
437
|
+
}
|
|
438
|
+
return error.afterMs;
|
|
439
|
+
}
|
|
426
440
|
getStepActionEvent(action, eventType, shouldNotRetry, payload = '', retryCount = 0) {
|
|
427
441
|
return {
|
|
428
442
|
workerId: this.name,
|
|
@@ -518,7 +532,12 @@ class InternalWorker {
|
|
|
518
532
|
sendBatchFailureForAll(action, memberIds, error) {
|
|
519
533
|
return __awaiter(this, void 0, void 0, function* () {
|
|
520
534
|
const payload = JSON.stringify({ message: error === null || error === void 0 ? void 0 : error.message, stack: error === null || error === void 0 ? void 0 : error.stack });
|
|
521
|
-
const
|
|
535
|
+
const retryAfterMs = this.retryAfterMs(error, action.actionId);
|
|
536
|
+
const items = memberIds.map((id) => ({
|
|
537
|
+
taskRunExternalId: id,
|
|
538
|
+
eventPayload: payload,
|
|
539
|
+
retryAfterMs,
|
|
540
|
+
}));
|
|
522
541
|
try {
|
|
523
542
|
yield this.client.dispatcher.sendBatchActionEvent(this.getBatchActionEvent(action, dispatcher_1.StepActionEventType.STEP_EVENT_TYPE_FAILED, items));
|
|
524
543
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { HatchetClient } from '../..';
|
|
2
|
+
import { StreamEvent } from '../../client/features/streams';
|
|
3
|
+
export declare const MiB: number;
|
|
4
|
+
export declare const maxInlinePayloadBytes: number;
|
|
5
|
+
export declare const describeBenchmark: jest.Describe;
|
|
6
|
+
export type LatencyStats = {
|
|
7
|
+
count: number;
|
|
8
|
+
avgMs: number;
|
|
9
|
+
p50Ms: number;
|
|
10
|
+
p99Ms: number;
|
|
11
|
+
maxMs: number;
|
|
12
|
+
};
|
|
13
|
+
export declare function latencyStats(durationsMs: number[]): LatencyStats;
|
|
14
|
+
export declare function formatLatency(stats: LatencyStats, digits?: number): string;
|
|
15
|
+
export declare function formatSize(bytes: number): string;
|
|
16
|
+
export type PublishLatencyStats = LatencyStats & {
|
|
17
|
+
throughputPerSec: number;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Times `count` sequential publishes, after `warmupCount` unmeasured ones
|
|
21
|
+
* absorb connection setup and topic creation.
|
|
22
|
+
*/
|
|
23
|
+
export declare function measurePublishLatency(hatchet: HatchetClient, topic: string, payload: Uint8Array | string, count: number, warmupCount?: number): Promise<PublishLatencyStats>;
|
|
24
|
+
export declare function formatPublishLatency(stats: PublishLatencyStats): string;
|
|
25
|
+
export type LiveSubscription = {
|
|
26
|
+
/** resolves with the next message delivered after the call */
|
|
27
|
+
nextEvent: () => Promise<StreamEvent>;
|
|
28
|
+
close: () => Promise<void>;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Subscribes to topic and resolves once the subscription is live, so a
|
|
32
|
+
* benchmark's timings don't include subscribe setup.
|
|
33
|
+
*/
|
|
34
|
+
export declare function openLiveSubscription(hatchet: HatchetClient, topic: string): Promise<LiveSubscription>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __asyncValues = (this && this.__asyncValues) || function (o) {
|
|
12
|
+
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
|
|
13
|
+
var m = o[Symbol.asyncIterator], i;
|
|
14
|
+
return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
|
|
15
|
+
function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
|
|
16
|
+
function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
|
|
17
|
+
};
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.describeBenchmark = exports.maxInlinePayloadBytes = exports.MiB = void 0;
|
|
20
|
+
exports.latencyStats = latencyStats;
|
|
21
|
+
exports.formatLatency = formatLatency;
|
|
22
|
+
exports.formatSize = formatSize;
|
|
23
|
+
exports.measurePublishLatency = measurePublishLatency;
|
|
24
|
+
exports.formatPublishLatency = formatPublishLatency;
|
|
25
|
+
exports.openLiveSubscription = openLiveSubscription;
|
|
26
|
+
const perf_hooks_1 = require("perf_hooks");
|
|
27
|
+
exports.MiB = 1024 * 1024;
|
|
28
|
+
// the server's per-message gRPC limit (MaxStreamMessagePayloadBytes); larger payloads are uploaded
|
|
29
|
+
exports.maxInlinePayloadBytes = 4 * exports.MiB - 5 * 1024;
|
|
30
|
+
// benchmarks run only when asked for (test_durable_streams.sh --bench)
|
|
31
|
+
exports.describeBenchmark = process.env.HATCHET_E2E_BENCHMARKS ? describe : describe.skip;
|
|
32
|
+
function percentile(sorted, p) {
|
|
33
|
+
return sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * p))];
|
|
34
|
+
}
|
|
35
|
+
function latencyStats(durationsMs) {
|
|
36
|
+
const sorted = [...durationsMs].sort((a, b) => a - b);
|
|
37
|
+
return {
|
|
38
|
+
count: durationsMs.length,
|
|
39
|
+
avgMs: durationsMs.reduce((sum, d) => sum + d, 0) / durationsMs.length,
|
|
40
|
+
p50Ms: percentile(sorted, 0.5),
|
|
41
|
+
p99Ms: percentile(sorted, 0.99),
|
|
42
|
+
maxMs: sorted[sorted.length - 1],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function formatLatency(stats, digits = 2) {
|
|
46
|
+
const fmt = (ms) => ms.toFixed(digits);
|
|
47
|
+
return `avg=${fmt(stats.avgMs)}ms p50=${fmt(stats.p50Ms)}ms p99=${fmt(stats.p99Ms)}ms max=${fmt(stats.maxMs)}ms`;
|
|
48
|
+
}
|
|
49
|
+
function formatSize(bytes) {
|
|
50
|
+
if (bytes % exports.MiB === 0)
|
|
51
|
+
return `${bytes / exports.MiB}MiB`;
|
|
52
|
+
if (bytes % 1024 === 0)
|
|
53
|
+
return `${bytes / 1024}KiB`;
|
|
54
|
+
return `${bytes}B`;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Times `count` sequential publishes, after `warmupCount` unmeasured ones
|
|
58
|
+
* absorb connection setup and topic creation.
|
|
59
|
+
*/
|
|
60
|
+
function measurePublishLatency(hatchet_1, topic_1, payload_1, count_1) {
|
|
61
|
+
return __awaiter(this, arguments, void 0, function* (hatchet, topic, payload, count, warmupCount = 5) {
|
|
62
|
+
for (let i = 0; i < warmupCount; i += 1) {
|
|
63
|
+
yield hatchet.streams.publish(topic, payload);
|
|
64
|
+
}
|
|
65
|
+
const durationsMs = [];
|
|
66
|
+
const start = perf_hooks_1.performance.now();
|
|
67
|
+
for (let i = 0; i < count; i += 1) {
|
|
68
|
+
const before = perf_hooks_1.performance.now();
|
|
69
|
+
yield hatchet.streams.publish(topic, payload);
|
|
70
|
+
durationsMs.push(perf_hooks_1.performance.now() - before);
|
|
71
|
+
}
|
|
72
|
+
const totalMs = perf_hooks_1.performance.now() - start;
|
|
73
|
+
return Object.assign(Object.assign({}, latencyStats(durationsMs)), { throughputPerSec: (count / totalMs) * 1000 });
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
function formatPublishLatency(stats) {
|
|
77
|
+
return `${formatLatency(stats)} throughput=${stats.throughputPerSec.toFixed(2)}/s`;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Subscribes to topic and resolves once the subscription is live, so a
|
|
81
|
+
* benchmark's timings don't include subscribe setup.
|
|
82
|
+
*/
|
|
83
|
+
function openLiveSubscription(hatchet, topic) {
|
|
84
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
85
|
+
const abort = new AbortController();
|
|
86
|
+
let onEvent;
|
|
87
|
+
const consumer = (() => __awaiter(this, void 0, void 0, function* () {
|
|
88
|
+
var _a, e_1, _b, _c;
|
|
89
|
+
try {
|
|
90
|
+
for (var _d = true, _e = __asyncValues(hatchet.streams.events(topic, { signal: abort.signal })), _f; _f = yield _e.next(), _a = _f.done, !_a; _d = true) {
|
|
91
|
+
_c = _f.value;
|
|
92
|
+
_d = false;
|
|
93
|
+
const ev = _c;
|
|
94
|
+
onEvent === null || onEvent === void 0 ? void 0 : onEvent(ev);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
98
|
+
finally {
|
|
99
|
+
try {
|
|
100
|
+
if (!_d && !_a && (_b = _e.return)) yield _b.call(_e);
|
|
101
|
+
}
|
|
102
|
+
finally { if (e_1) throw e_1.error; }
|
|
103
|
+
}
|
|
104
|
+
}))();
|
|
105
|
+
const nextEvent = () => new Promise((resolve) => {
|
|
106
|
+
onEvent = resolve;
|
|
107
|
+
});
|
|
108
|
+
// the subscription is live once a first message comes through
|
|
109
|
+
const ready = nextEvent();
|
|
110
|
+
yield hatchet.streams.publish(topic, 'ready');
|
|
111
|
+
yield ready;
|
|
112
|
+
return {
|
|
113
|
+
nextEvent,
|
|
114
|
+
close: () => __awaiter(this, void 0, void 0, function* () {
|
|
115
|
+
abort.abort();
|
|
116
|
+
yield consumer;
|
|
117
|
+
}),
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
}
|
|
@@ -35,21 +35,22 @@ const workflow_14 = require("./durable_callback_ordering/workflow");
|
|
|
35
35
|
const workflow_15 = require("./durable_sleep/workflow");
|
|
36
36
|
const workflow_16 = require("./logger/workflow");
|
|
37
37
|
const workflow_17 = require("./non_retryable/workflow");
|
|
38
|
-
const workflow_18 = require("./
|
|
39
|
-
const workflow_19 = require("./
|
|
40
|
-
const workflow_20 = require("./
|
|
41
|
-
const workflow_21 = require("./
|
|
42
|
-
const workflow_22 = require("./
|
|
38
|
+
const workflow_18 = require("./retry_after/workflow");
|
|
39
|
+
const workflow_19 = require("./on_failure/workflow");
|
|
40
|
+
const workflow_20 = require("./idempotency/workflow");
|
|
41
|
+
const workflow_21 = require("./on_event/workflow");
|
|
42
|
+
const workflow_22 = require("./return_exceptions/workflow");
|
|
43
|
+
const workflow_23 = require("./run_details/workflow");
|
|
43
44
|
const e2e_workflows_1 = require("./simple/e2e-workflows");
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
const
|
|
45
|
+
const workflow_24 = require("./batch_assign/workflow");
|
|
46
|
+
const workflow_25 = require("./streaming/workflow");
|
|
47
|
+
const workflow_26 = require("./subscribe_to_stream/workflow");
|
|
48
|
+
const workflow_27 = require("./timeout/workflow");
|
|
49
|
+
const workflow_28 = require("./webhooks/workflow");
|
|
50
|
+
const workflow_29 = require("./child_index/workflow");
|
|
51
|
+
const workflow_30 = require("./support_agent/workflow");
|
|
52
|
+
const workflow_31 = require("./welcome_email/workflow");
|
|
53
|
+
const workflow_32 = require("./pdf_pipeline/workflow");
|
|
53
54
|
const workflows = [
|
|
54
55
|
workflow_1.bulkChild,
|
|
55
56
|
workflow_1.bulkParentWorkflow,
|
|
@@ -101,43 +102,45 @@ const workflows = [
|
|
|
101
102
|
workflow_13.evictableChildBulkSpawn,
|
|
102
103
|
(0, workflow_16.createLoggingWorkflow)(hatchet_client_1.hatchet),
|
|
103
104
|
workflow_17.nonRetryableWorkflow,
|
|
104
|
-
workflow_18.
|
|
105
|
-
|
|
106
|
-
workflow_19.
|
|
107
|
-
workflow_20.
|
|
108
|
-
|
|
109
|
-
|
|
105
|
+
workflow_18.retryAfterUpstreamDelay,
|
|
106
|
+
workflow_18.retryAfterExponentialBackoff,
|
|
107
|
+
workflow_19.failureWorkflow,
|
|
108
|
+
workflow_20.idempotentTask,
|
|
109
|
+
workflow_20.idempotentTaskShortWindow,
|
|
110
|
+
workflow_21.lower,
|
|
111
|
+
workflow_22.returnExceptionsTask,
|
|
112
|
+
workflow_23.runDetailTestWorkflow,
|
|
110
113
|
e2e_workflows_1.helloWorld,
|
|
111
114
|
e2e_workflows_1.helloWorldDurable,
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
115
|
+
workflow_25.streamingTask,
|
|
116
|
+
workflow_26.dagStream,
|
|
117
|
+
workflow_26.longStream,
|
|
118
|
+
workflow_27.timeoutTask,
|
|
119
|
+
workflow_27.refreshTimeoutTask,
|
|
120
|
+
workflow_28.webhookWorkflow,
|
|
121
|
+
workflow_29.childIndexChild,
|
|
122
|
+
workflow_29.childIndexParent,
|
|
123
|
+
workflow_29.scenarioTask,
|
|
124
|
+
workflow_29.orchestratorTask,
|
|
125
|
+
workflow_30.supportAgent,
|
|
126
|
+
workflow_30.triageTicket,
|
|
127
|
+
workflow_30.generateReply,
|
|
128
|
+
workflow_30.escalateTicket,
|
|
129
|
+
workflow_31.welcomeEmail,
|
|
130
|
+
workflow_32.pdfPipeline,
|
|
131
|
+
workflow_24.batchSimple,
|
|
132
|
+
workflow_24.batchKeyed,
|
|
133
|
+
workflow_24.batchKeyedFailable,
|
|
134
|
+
workflow_24.batchKeyedInterval,
|
|
135
|
+
workflow_24.batchLarge,
|
|
136
|
+
workflow_24.batchSingle,
|
|
137
|
+
workflow_24.batchOrdered,
|
|
138
|
+
workflow_24.batchBroadcast,
|
|
139
|
+
workflow_24.batchCancel,
|
|
140
|
+
workflow_24.child,
|
|
141
|
+
workflow_24.childBatch,
|
|
142
|
+
workflow_24.batchChildSpawn,
|
|
143
|
+
workflow_24.batchChildBatchSpawn,
|
|
141
144
|
];
|
|
142
145
|
function main() {
|
|
143
146
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
const workflow_1 = require("./workflow");
|
|
13
|
+
function main() {
|
|
14
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
15
|
+
const res = yield workflow_1.retryAfterUpstreamDelay.run({ failingAttempts: 2 });
|
|
16
|
+
console.log(res);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
if (require.main === module) {
|
|
20
|
+
main();
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
const hatchet_client_1 = require("../hatchet-client");
|
|
13
|
+
const workflow_1 = require("./workflow");
|
|
14
|
+
function main() {
|
|
15
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
16
|
+
const worker = yield hatchet_client_1.hatchet.worker('retry-after-worker', {
|
|
17
|
+
workflows: [workflow_1.retryAfterUpstreamDelay, workflow_1.retryAfterExponentialBackoff],
|
|
18
|
+
});
|
|
19
|
+
yield worker.start();
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
if (require.main === module) {
|
|
23
|
+
main();
|
|
24
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type UpstreamInput = {
|
|
2
|
+
failingAttempts: number;
|
|
3
|
+
};
|
|
4
|
+
export declare const retryAfterUpstreamDelay: import("../..").TaskWorkflowDeclaration<UpstreamInput, {
|
|
5
|
+
attempt: number;
|
|
6
|
+
}, {}, {}, {}, {}>;
|
|
7
|
+
export declare const retryAfterExponentialBackoff: import("../..").TaskWorkflowDeclaration<UpstreamInput, {
|
|
8
|
+
attempt: number;
|
|
9
|
+
}, {}, {}, {}, {}>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.retryAfterExponentialBackoff = exports.retryAfterUpstreamDelay = void 0;
|
|
13
|
+
const task_1 = require("../../task");
|
|
14
|
+
const hatchet_client_1 = require("../hatchet-client");
|
|
15
|
+
// > Retry after an upstream-provided delay
|
|
16
|
+
exports.retryAfterUpstreamDelay = hatchet_client_1.hatchet.task({
|
|
17
|
+
name: 'retry-after-upstream-delay',
|
|
18
|
+
retries: 5,
|
|
19
|
+
fn: (input, ctx) => __awaiter(void 0, void 0, void 0, function* () {
|
|
20
|
+
if (ctx.retryCount() < input.failingAttempts) {
|
|
21
|
+
throw new task_1.RetryAfterError('upstream rate limited', { after: '2s' });
|
|
22
|
+
}
|
|
23
|
+
return { attempt: ctx.retryCount() };
|
|
24
|
+
}),
|
|
25
|
+
});
|
|
26
|
+
// !!
|
|
27
|
+
// > Exponential backoff with full jitter
|
|
28
|
+
exports.retryAfterExponentialBackoff = hatchet_client_1.hatchet.task({
|
|
29
|
+
name: 'retry-after-exponential-backoff',
|
|
30
|
+
retries: 3,
|
|
31
|
+
fn: (input, ctx) => __awaiter(void 0, void 0, void 0, function* () {
|
|
32
|
+
if (ctx.retryCount() < input.failingAttempts) {
|
|
33
|
+
throw new task_1.RetryAfterError('upstream unavailable', {
|
|
34
|
+
after: Math.random() * Math.min(2 ** ctx.retryCount(), 60) * 1000,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return { attempt: ctx.retryCount() };
|
|
38
|
+
}),
|
|
39
|
+
});
|
|
40
|
+
// !!
|