@hotmeshio/hotmesh 0.14.6 → 0.14.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -111
- package/build/modules/enums.d.ts +41 -0
- package/build/modules/enums.js +43 -2
- package/build/package.json +1 -1
- package/build/services/activities/worker.js +14 -0
- package/build/services/durable/exporter.js +2 -2
- package/build/services/durable/schemas/factory.d.ts +1 -19
- package/build/services/durable/schemas/factory.js +122 -524
- package/build/services/durable/worker.js +25 -14
- package/build/services/router/config/index.d.ts +2 -2
- package/build/services/router/config/index.js +3 -1
- package/build/services/router/consumption/index.d.ts +6 -5
- package/build/services/router/consumption/index.js +47 -25
- package/build/services/router/error-handling/index.d.ts +1 -1
- package/build/services/router/error-handling/index.js +12 -11
- package/build/services/store/providers/postgres/exporter-sql.d.ts +4 -0
- package/build/services/store/providers/postgres/exporter-sql.js +11 -1
- package/build/services/store/providers/postgres/postgres.js +24 -0
- package/build/services/stream/providers/postgres/postgres.d.ts +6 -0
- package/build/services/stream/providers/postgres/postgres.js +25 -0
- package/package.json +1 -1
|
@@ -377,15 +377,13 @@ class WorkerService {
|
|
|
377
377
|
//use code 599 as a proxy for all retryable errors
|
|
378
378
|
// (basically anything not 596, 597, 598)
|
|
379
379
|
return {
|
|
380
|
-
status: stream_1.StreamStatus.
|
|
380
|
+
status: stream_1.StreamStatus.ERROR,
|
|
381
381
|
code: 599,
|
|
382
382
|
metadata: { ...data.metadata },
|
|
383
383
|
data: {
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
code: enums_1.HMSH_CODE_DURABLE_RETRYABLE,
|
|
388
|
-
},
|
|
384
|
+
message: err.message,
|
|
385
|
+
stack: err.stack,
|
|
386
|
+
code: enums_1.HMSH_CODE_DURABLE_RETRYABLE,
|
|
389
387
|
},
|
|
390
388
|
};
|
|
391
389
|
}
|
|
@@ -607,15 +605,13 @@ class WorkerService {
|
|
|
607
605
|
//use code 599 as a proxy for all retryable errors
|
|
608
606
|
// (basically anything not 596, 597, 598)
|
|
609
607
|
return {
|
|
610
|
-
status: stream_1.StreamStatus.
|
|
608
|
+
status: stream_1.StreamStatus.ERROR,
|
|
611
609
|
code: enums_1.HMSH_CODE_DURABLE_RETRYABLE,
|
|
612
610
|
metadata: { ...data.metadata },
|
|
613
611
|
data: {
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
timestamp: (0, utils_1.formatISODate)(new Date()),
|
|
618
|
-
},
|
|
612
|
+
message: err.message,
|
|
613
|
+
stack: err.stack,
|
|
614
|
+
code: enums_1.HMSH_CODE_DURABLE_RETRYABLE,
|
|
619
615
|
},
|
|
620
616
|
};
|
|
621
617
|
}
|
|
@@ -963,9 +959,24 @@ class WorkerService {
|
|
|
963
959
|
}, telemetry_3.SpanStatusCode.ERROR, err.message);
|
|
964
960
|
}
|
|
965
961
|
isProcessing = true;
|
|
962
|
+
const errorCode = err.code || new errors_1.DurableRetryError(err.message).code;
|
|
963
|
+
if (errorCode === enums_1.HMSH_CODE_DURABLE_RETRYABLE) {
|
|
964
|
+
// Retryable errors use status: ERROR so the engine-level
|
|
965
|
+
// retry mechanism (handleRetry + _streamRetryConfig) kicks in
|
|
966
|
+
return withPatchMarkers({
|
|
967
|
+
status: stream_1.StreamStatus.ERROR,
|
|
968
|
+
code: errorCode,
|
|
969
|
+
metadata: { ...data.metadata },
|
|
970
|
+
data: {
|
|
971
|
+
message: err.message,
|
|
972
|
+
stack: err.stack,
|
|
973
|
+
code: errorCode,
|
|
974
|
+
},
|
|
975
|
+
});
|
|
976
|
+
}
|
|
966
977
|
return withPatchMarkers({
|
|
967
978
|
status: stream_1.StreamStatus.SUCCESS,
|
|
968
|
-
code:
|
|
979
|
+
code: errorCode,
|
|
969
980
|
metadata: { ...data.metadata },
|
|
970
981
|
data: {
|
|
971
982
|
$error: {
|
|
@@ -973,7 +984,7 @@ class WorkerService {
|
|
|
973
984
|
type: err.name,
|
|
974
985
|
name: err.name,
|
|
975
986
|
stack: err.stack,
|
|
976
|
-
code:
|
|
987
|
+
code: errorCode,
|
|
977
988
|
},
|
|
978
989
|
},
|
|
979
990
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { HMSH_BLOCK_TIME_MS, HMSH_MAX_RETRIES, HMSH_MAX_TIMEOUT_MS, HMSH_GRADUATED_INTERVAL_MS, HMSH_CODE_UNACKED, HMSH_CODE_UNKNOWN, HMSH_STATUS_UNKNOWN, HMSH_XCLAIM_COUNT, HMSH_XCLAIM_DELAY_MS, HMSH_XPENDING_COUNT, HMSH_BATCH_SIZE, HMSH_RESERVATION_TIMEOUT_S, MAX_DELAY, MAX_STREAM_BACKOFF, INITIAL_STREAM_BACKOFF, MAX_STREAM_RETRIES, HMSH_POISON_MESSAGE_THRESHOLD } from '../../../modules/enums';
|
|
1
|
+
import { HMSH_BLOCK_TIME_MS, HMSH_MAX_RETRIES, HMSH_MAX_TIMEOUT_MS, HMSH_GRADUATED_INTERVAL_MS, HMSH_CODE_UNACKED, HMSH_CODE_UNKNOWN, HMSH_STATUS_UNKNOWN, HMSH_XCLAIM_COUNT, HMSH_XCLAIM_DELAY_MS, HMSH_XPENDING_COUNT, HMSH_BATCH_SIZE, HMSH_BATCH_SIZE_MIN, HMSH_RESERVATION_TIMEOUT_S, HMSH_RESERVATION_TIMEOUT_MAX_S, MAX_DELAY, MAX_STREAM_BACKOFF, INITIAL_STREAM_BACKOFF, MAX_STREAM_RETRIES, HMSH_POISON_MESSAGE_THRESHOLD } from '../../../modules/enums';
|
|
2
2
|
import { RouterConfig } from '../../../types/stream';
|
|
3
3
|
export declare class RouterConfigManager {
|
|
4
4
|
static validateThrottle(delayInMillis: number): void;
|
|
@@ -8,4 +8,4 @@ export declare class RouterConfigManager {
|
|
|
8
8
|
readonly: boolean;
|
|
9
9
|
};
|
|
10
10
|
}
|
|
11
|
-
export { HMSH_BLOCK_TIME_MS, HMSH_MAX_RETRIES, HMSH_MAX_TIMEOUT_MS, HMSH_GRADUATED_INTERVAL_MS, HMSH_CODE_UNACKED, HMSH_CODE_UNKNOWN, HMSH_STATUS_UNKNOWN, HMSH_XCLAIM_COUNT, HMSH_XCLAIM_DELAY_MS, HMSH_XPENDING_COUNT, HMSH_BATCH_SIZE, HMSH_RESERVATION_TIMEOUT_S, MAX_DELAY, MAX_STREAM_BACKOFF, INITIAL_STREAM_BACKOFF, MAX_STREAM_RETRIES, HMSH_POISON_MESSAGE_THRESHOLD, };
|
|
11
|
+
export { HMSH_BLOCK_TIME_MS, HMSH_MAX_RETRIES, HMSH_MAX_TIMEOUT_MS, HMSH_GRADUATED_INTERVAL_MS, HMSH_CODE_UNACKED, HMSH_CODE_UNKNOWN, HMSH_STATUS_UNKNOWN, HMSH_XCLAIM_COUNT, HMSH_XCLAIM_DELAY_MS, HMSH_XPENDING_COUNT, HMSH_BATCH_SIZE, HMSH_BATCH_SIZE_MIN, HMSH_RESERVATION_TIMEOUT_S, HMSH_RESERVATION_TIMEOUT_MAX_S, MAX_DELAY, MAX_STREAM_BACKOFF, INITIAL_STREAM_BACKOFF, MAX_STREAM_RETRIES, HMSH_POISON_MESSAGE_THRESHOLD, };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.HMSH_POISON_MESSAGE_THRESHOLD = exports.MAX_STREAM_RETRIES = exports.INITIAL_STREAM_BACKOFF = exports.MAX_STREAM_BACKOFF = exports.MAX_DELAY = exports.HMSH_RESERVATION_TIMEOUT_S = exports.HMSH_BATCH_SIZE = exports.HMSH_XPENDING_COUNT = exports.HMSH_XCLAIM_DELAY_MS = exports.HMSH_XCLAIM_COUNT = exports.HMSH_STATUS_UNKNOWN = exports.HMSH_CODE_UNKNOWN = exports.HMSH_CODE_UNACKED = exports.HMSH_GRADUATED_INTERVAL_MS = exports.HMSH_MAX_TIMEOUT_MS = exports.HMSH_MAX_RETRIES = exports.HMSH_BLOCK_TIME_MS = exports.RouterConfigManager = void 0;
|
|
3
|
+
exports.HMSH_POISON_MESSAGE_THRESHOLD = exports.MAX_STREAM_RETRIES = exports.INITIAL_STREAM_BACKOFF = exports.MAX_STREAM_BACKOFF = exports.MAX_DELAY = exports.HMSH_RESERVATION_TIMEOUT_MAX_S = exports.HMSH_RESERVATION_TIMEOUT_S = exports.HMSH_BATCH_SIZE_MIN = exports.HMSH_BATCH_SIZE = exports.HMSH_XPENDING_COUNT = exports.HMSH_XCLAIM_DELAY_MS = exports.HMSH_XCLAIM_COUNT = exports.HMSH_STATUS_UNKNOWN = exports.HMSH_CODE_UNKNOWN = exports.HMSH_CODE_UNACKED = exports.HMSH_GRADUATED_INTERVAL_MS = exports.HMSH_MAX_TIMEOUT_MS = exports.HMSH_MAX_RETRIES = exports.HMSH_BLOCK_TIME_MS = exports.RouterConfigManager = void 0;
|
|
4
4
|
const enums_1 = require("../../../modules/enums");
|
|
5
5
|
Object.defineProperty(exports, "HMSH_BLOCK_TIME_MS", { enumerable: true, get: function () { return enums_1.HMSH_BLOCK_TIME_MS; } });
|
|
6
6
|
Object.defineProperty(exports, "HMSH_MAX_RETRIES", { enumerable: true, get: function () { return enums_1.HMSH_MAX_RETRIES; } });
|
|
@@ -13,7 +13,9 @@ Object.defineProperty(exports, "HMSH_XCLAIM_COUNT", { enumerable: true, get: fun
|
|
|
13
13
|
Object.defineProperty(exports, "HMSH_XCLAIM_DELAY_MS", { enumerable: true, get: function () { return enums_1.HMSH_XCLAIM_DELAY_MS; } });
|
|
14
14
|
Object.defineProperty(exports, "HMSH_XPENDING_COUNT", { enumerable: true, get: function () { return enums_1.HMSH_XPENDING_COUNT; } });
|
|
15
15
|
Object.defineProperty(exports, "HMSH_BATCH_SIZE", { enumerable: true, get: function () { return enums_1.HMSH_BATCH_SIZE; } });
|
|
16
|
+
Object.defineProperty(exports, "HMSH_BATCH_SIZE_MIN", { enumerable: true, get: function () { return enums_1.HMSH_BATCH_SIZE_MIN; } });
|
|
16
17
|
Object.defineProperty(exports, "HMSH_RESERVATION_TIMEOUT_S", { enumerable: true, get: function () { return enums_1.HMSH_RESERVATION_TIMEOUT_S; } });
|
|
18
|
+
Object.defineProperty(exports, "HMSH_RESERVATION_TIMEOUT_MAX_S", { enumerable: true, get: function () { return enums_1.HMSH_RESERVATION_TIMEOUT_MAX_S; } });
|
|
17
19
|
Object.defineProperty(exports, "MAX_DELAY", { enumerable: true, get: function () { return enums_1.MAX_DELAY; } });
|
|
18
20
|
Object.defineProperty(exports, "MAX_STREAM_BACKOFF", { enumerable: true, get: function () { return enums_1.MAX_STREAM_BACKOFF; } });
|
|
19
21
|
Object.defineProperty(exports, "INITIAL_STREAM_BACKOFF", { enumerable: true, get: function () { return enums_1.INITIAL_STREAM_BACKOFF; } });
|
|
@@ -27,19 +27,20 @@ export declare class ConsumptionManager<S extends StreamService<ProviderClient,
|
|
|
27
27
|
private router;
|
|
28
28
|
private retry;
|
|
29
29
|
private adaptiveReservationTimeout;
|
|
30
|
+
private adaptiveBatchSize;
|
|
30
31
|
private lastDepthCheckAt;
|
|
31
32
|
private static readonly DEPTH_CHECK_INTERVAL_MS;
|
|
32
33
|
private static readonly DEPTH_SCALE_UP_THRESHOLD;
|
|
33
34
|
private static readonly DEPTH_SCALE_DOWN_THRESHOLD;
|
|
34
|
-
private static readonly RESERVATION_TIMEOUT_MAX_S;
|
|
35
35
|
constructor(stream: S, logger: ILogger, throttleManager: ThrottleManager, errorHandler: ErrorHandler, lifecycleManager: LifecycleManager<S>, reclaimDelay: number, reclaimCount: number, appId: string, role: any, router: any, retry?: import('../../../types/stream').RetryPolicy);
|
|
36
36
|
/**
|
|
37
37
|
* Adjusts reservation timeout based on stream depth. Called periodically
|
|
38
|
-
* from the consume loop. When depth is high
|
|
39
|
-
*
|
|
40
|
-
*
|
|
38
|
+
* from the consume loop. When depth is high:
|
|
39
|
+
* - reservation timeout grows (prevents duplicate re-reservation)
|
|
40
|
+
* - batch size shrinks (reduces in-memory blocking, shares the stream)
|
|
41
|
+
* When depth drops, both restore toward configured defaults.
|
|
41
42
|
*/
|
|
42
|
-
private
|
|
43
|
+
private adjustConsumptionPressure;
|
|
43
44
|
createGroup(stream: string, group: string): Promise<void>;
|
|
44
45
|
publishMessage(topic: string, streamData: StreamData | StreamDataResponse, transaction?: ProviderTransaction): Promise<string | ProviderTransaction>;
|
|
45
46
|
consumeMessages(stream: string, group: string, consumer: string, callback: (streamData: StreamData) => Promise<StreamDataResponse | void>): Promise<void>;
|
|
@@ -17,10 +17,13 @@ class ConsumptionManager {
|
|
|
17
17
|
get hasReachedMaxBackoff() { return this.router.hasReachedMaxBackoff; }
|
|
18
18
|
set hasReachedMaxBackoff(v) { this.router.hasReachedMaxBackoff = v; }
|
|
19
19
|
constructor(stream, logger, throttleManager, errorHandler, lifecycleManager, reclaimDelay, reclaimCount, appId, role, router, retry) {
|
|
20
|
-
// Adaptive
|
|
21
|
-
//
|
|
22
|
-
//
|
|
20
|
+
// Adaptive consumption pressure — scales reservation timeout AND batch
|
|
21
|
+
// size based on stream depth. Under load: timeout grows (prevents
|
|
22
|
+
// duplicate re-reservation) and batch size shrinks (reduces in-memory
|
|
23
|
+
// blocking, lets other consumers share the stream). When idle, both
|
|
24
|
+
// restore toward configured defaults.
|
|
23
25
|
this.adaptiveReservationTimeout = config_1.HMSH_RESERVATION_TIMEOUT_S;
|
|
26
|
+
this.adaptiveBatchSize = config_1.HMSH_BATCH_SIZE;
|
|
24
27
|
this.lastDepthCheckAt = 0;
|
|
25
28
|
this.stream = stream;
|
|
26
29
|
this.logger = logger;
|
|
@@ -36,11 +39,12 @@ class ConsumptionManager {
|
|
|
36
39
|
}
|
|
37
40
|
/**
|
|
38
41
|
* Adjusts reservation timeout based on stream depth. Called periodically
|
|
39
|
-
* from the consume loop. When depth is high
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
+
* from the consume loop. When depth is high:
|
|
43
|
+
* - reservation timeout grows (prevents duplicate re-reservation)
|
|
44
|
+
* - batch size shrinks (reduces in-memory blocking, shares the stream)
|
|
45
|
+
* When depth drops, both restore toward configured defaults.
|
|
42
46
|
*/
|
|
43
|
-
async
|
|
47
|
+
async adjustConsumptionPressure(stream) {
|
|
44
48
|
const now = Date.now();
|
|
45
49
|
if (now - this.lastDepthCheckAt < ConsumptionManager.DEPTH_CHECK_INTERVAL_MS) {
|
|
46
50
|
return;
|
|
@@ -48,27 +52,37 @@ class ConsumptionManager {
|
|
|
48
52
|
this.lastDepthCheckAt = now;
|
|
49
53
|
try {
|
|
50
54
|
const depth = await this.stream.getStreamDepth(stream);
|
|
51
|
-
const
|
|
55
|
+
const prevTimeout = this.adaptiveReservationTimeout;
|
|
56
|
+
const prevBatch = this.adaptiveBatchSize;
|
|
52
57
|
if (depth > ConsumptionManager.DEPTH_SCALE_UP_THRESHOLD) {
|
|
53
|
-
// Scale up
|
|
54
|
-
this.adaptiveReservationTimeout = Math.min(this.adaptiveReservationTimeout * 2,
|
|
58
|
+
// Scale up timeout, scale down batch size
|
|
59
|
+
this.adaptiveReservationTimeout = Math.min(this.adaptiveReservationTimeout * 2, config_1.HMSH_RESERVATION_TIMEOUT_MAX_S);
|
|
60
|
+
this.adaptiveBatchSize = Math.max(Math.floor(this.adaptiveBatchSize / 2), config_1.HMSH_BATCH_SIZE_MIN);
|
|
55
61
|
}
|
|
56
62
|
else if (depth < ConsumptionManager.DEPTH_SCALE_DOWN_THRESHOLD) {
|
|
57
|
-
// Scale down
|
|
63
|
+
// Scale down timeout, scale up batch size
|
|
58
64
|
this.adaptiveReservationTimeout = Math.max(Math.floor(this.adaptiveReservationTimeout / 2), config_1.HMSH_RESERVATION_TIMEOUT_S);
|
|
65
|
+
this.adaptiveBatchSize = Math.min(this.adaptiveBatchSize * 2, config_1.HMSH_BATCH_SIZE);
|
|
59
66
|
}
|
|
60
|
-
if (this.adaptiveReservationTimeout !==
|
|
61
|
-
// Update the stream provider so notification-path fetches
|
|
62
|
-
// also use the adaptive timeout
|
|
67
|
+
if (this.adaptiveReservationTimeout !== prevTimeout) {
|
|
63
68
|
this.stream.reservationTimeout = this.adaptiveReservationTimeout;
|
|
64
69
|
this.logger.info('stream-reservation-timeout-adjusted', {
|
|
65
70
|
stream,
|
|
66
71
|
depth,
|
|
67
|
-
previousTimeoutS:
|
|
72
|
+
previousTimeoutS: prevTimeout,
|
|
68
73
|
newTimeoutS: this.adaptiveReservationTimeout,
|
|
69
74
|
configuredDefaultS: config_1.HMSH_RESERVATION_TIMEOUT_S,
|
|
70
75
|
});
|
|
71
76
|
}
|
|
77
|
+
if (this.adaptiveBatchSize !== prevBatch) {
|
|
78
|
+
this.logger.info('stream-batch-size-adjusted', {
|
|
79
|
+
stream,
|
|
80
|
+
depth,
|
|
81
|
+
previousBatchSize: prevBatch,
|
|
82
|
+
newBatchSize: this.adaptiveBatchSize,
|
|
83
|
+
configuredDefaultBatchSize: config_1.HMSH_BATCH_SIZE,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
72
86
|
}
|
|
73
87
|
catch {
|
|
74
88
|
// Stream depth check is best-effort; don't fail the consume loop
|
|
@@ -153,7 +167,7 @@ class ConsumptionManager {
|
|
|
153
167
|
return;
|
|
154
168
|
}
|
|
155
169
|
// Adapt reservation timeout based on stream depth
|
|
156
|
-
await this.
|
|
170
|
+
await this.adjustConsumptionPressure(stream);
|
|
157
171
|
await this.throttleManager.customSleep(); // respect throttle
|
|
158
172
|
if (this.lifecycleManager.isStopped(group, consumer, stream) ||
|
|
159
173
|
this.throttleManager.isPaused()) {
|
|
@@ -274,12 +288,12 @@ class ConsumptionManager {
|
|
|
274
288
|
try {
|
|
275
289
|
let messages = [];
|
|
276
290
|
// Adapt reservation timeout based on stream depth
|
|
277
|
-
await this.
|
|
291
|
+
await this.adjustConsumptionPressure(stream);
|
|
278
292
|
if (!this.hasReachedMaxBackoff) {
|
|
279
293
|
// Normal mode: try with backoff and finite retries
|
|
280
294
|
const features = this.stream.getProviderSpecificFeatures();
|
|
281
295
|
const isPostgres = features.supportsParallelProcessing;
|
|
282
|
-
const batchSize = isPostgres ?
|
|
296
|
+
const batchSize = isPostgres ? this.adaptiveBatchSize : 1;
|
|
283
297
|
messages = await this.stream.consumeMessages(stream, group, consumer, {
|
|
284
298
|
blockTimeout: streamDuration,
|
|
285
299
|
batchSize,
|
|
@@ -294,7 +308,7 @@ class ConsumptionManager {
|
|
|
294
308
|
// Fallback mode: just try once, no backoff
|
|
295
309
|
const features = this.stream.getProviderSpecificFeatures();
|
|
296
310
|
const isPostgres = features.supportsParallelProcessing;
|
|
297
|
-
const batchSize = isPostgres ?
|
|
311
|
+
const batchSize = isPostgres ? this.adaptiveBatchSize : 1;
|
|
298
312
|
messages = await this.stream.consumeMessages(stream, group, consumer, {
|
|
299
313
|
blockTimeout: streamDuration,
|
|
300
314
|
batchSize,
|
|
@@ -571,14 +585,23 @@ class ConsumptionManager {
|
|
|
571
585
|
// Extract retry policy with priority:
|
|
572
586
|
// 1. Use message-level _streamRetryConfig (from database columns or previous retry)
|
|
573
587
|
// 2. Fall back to router-level retry (from worker config)
|
|
574
|
-
const
|
|
588
|
+
const streamRetryConfig = input._streamRetryConfig;
|
|
589
|
+
const retry = streamRetryConfig
|
|
575
590
|
? {
|
|
576
|
-
maximumAttempts:
|
|
577
|
-
backoffCoefficient:
|
|
578
|
-
maximumInterval:
|
|
591
|
+
maximumAttempts: streamRetryConfig.max_retry_attempts,
|
|
592
|
+
backoffCoefficient: streamRetryConfig.backoff_coefficient,
|
|
593
|
+
maximumInterval: streamRetryConfig.maximum_interval_seconds,
|
|
594
|
+
initialInterval: streamRetryConfig.initialInterval ?? input.data?.initialInterval ?? 1,
|
|
579
595
|
}
|
|
580
596
|
: this.retry;
|
|
581
|
-
return await this.errorHandler.handleRetry(input, output, this.publishMessage.bind(this), retry)
|
|
597
|
+
return await this.errorHandler.handleRetry(input, output, this.publishMessage.bind(this), retry, (topic, delayMs) => {
|
|
598
|
+
// Schedule a targeted NOTIFY so the consumer wakes up
|
|
599
|
+
// when the visibility-delayed retry message becomes visible
|
|
600
|
+
if (typeof this.stream.scheduleStreamNotify === 'function') {
|
|
601
|
+
const streamKey = this.stream.mintKey(key_1.KeyType.STREAMS, { topic });
|
|
602
|
+
this.stream.scheduleStreamNotify(streamKey, delayMs);
|
|
603
|
+
}
|
|
604
|
+
});
|
|
582
605
|
}
|
|
583
606
|
else if (typeof output.metadata !== 'object') {
|
|
584
607
|
output.metadata = { ...input.metadata, guid: (0, utils_1.guid)() };
|
|
@@ -597,5 +620,4 @@ class ConsumptionManager {
|
|
|
597
620
|
ConsumptionManager.DEPTH_CHECK_INTERVAL_MS = 10000;
|
|
598
621
|
ConsumptionManager.DEPTH_SCALE_UP_THRESHOLD = 100;
|
|
599
622
|
ConsumptionManager.DEPTH_SCALE_DOWN_THRESHOLD = 10;
|
|
600
|
-
ConsumptionManager.RESERVATION_TIMEOUT_MAX_S = 600;
|
|
601
623
|
exports.ConsumptionManager = ConsumptionManager;
|
|
@@ -4,5 +4,5 @@ export declare class ErrorHandler {
|
|
|
4
4
|
structureUnhandledError(input: StreamData, err: Error): StreamDataResponse;
|
|
5
5
|
structureUnacknowledgedError(input: StreamData): StreamDataResponse;
|
|
6
6
|
structureError(input: StreamData, output: StreamDataResponse): StreamDataResponse;
|
|
7
|
-
handleRetry(input: StreamData, output: StreamDataResponse, publishMessage: (topic: string, streamData: StreamData | StreamDataResponse) => Promise<string>, retry?: RetryPolicy): Promise<string>;
|
|
7
|
+
handleRetry(input: StreamData, output: StreamDataResponse, publishMessage: (topic: string, streamData: StreamData | StreamDataResponse) => Promise<string>, retry?: RetryPolicy, onRetryScheduled?: (topic: string, delayMs: number) => void): Promise<string>;
|
|
8
8
|
}
|
|
@@ -14,16 +14,11 @@ class ErrorHandler {
|
|
|
14
14
|
const maxInterval = typeof retry.maximumInterval === 'string'
|
|
15
15
|
? parseInt(retry.maximumInterval)
|
|
16
16
|
: (retry.maximumInterval || 120);
|
|
17
|
-
|
|
18
|
-
// tryCount=0 is 1st attempt, tryCount=1 is 2nd attempt, etc.
|
|
19
|
-
// So after tryCount, we've made (tryCount + 1) attempts
|
|
20
|
-
// We can retry if (tryCount + 1) < maxAttempts
|
|
17
|
+
const initialIntervalS = retry.initialInterval || 1;
|
|
21
18
|
if ((tryCount + 1) < maxAttempts) {
|
|
22
|
-
// Exponential backoff: min(coefficient^(try+1), maxInterval)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const backoffSeconds = Math.min(Math.pow(backoffCoeff, tryCount + 1), maxInterval);
|
|
26
|
-
return [true, backoffSeconds * 1000]; // Convert to milliseconds
|
|
19
|
+
// Exponential backoff: min(initialInterval * coefficient^(try+1), maxInterval)
|
|
20
|
+
const backoffSeconds = Math.min(initialIntervalS * Math.pow(backoffCoeff, tryCount + 1), maxInterval);
|
|
21
|
+
return [true, backoffSeconds * 1000];
|
|
27
22
|
}
|
|
28
23
|
return [false, 0];
|
|
29
24
|
}
|
|
@@ -101,7 +96,7 @@ class ErrorHandler {
|
|
|
101
96
|
data,
|
|
102
97
|
};
|
|
103
98
|
}
|
|
104
|
-
async handleRetry(input, output, publishMessage, retry) {
|
|
99
|
+
async handleRetry(input, output, publishMessage, retry, onRetryScheduled) {
|
|
105
100
|
const [shouldRetry, timeout] = this.shouldRetry(input, output, retry);
|
|
106
101
|
if (shouldRetry) {
|
|
107
102
|
// Only sleep if no retry (legacy behavior for backward compatibility)
|
|
@@ -126,7 +121,13 @@ class ErrorHandler {
|
|
|
126
121
|
// Track retry attempt count in database
|
|
127
122
|
const currentAttempt = input._retryAttempt || 0;
|
|
128
123
|
newMessage._retryAttempt = currentAttempt + 1;
|
|
129
|
-
|
|
124
|
+
const messageId = (await publishMessage(input.metadata.topic, newMessage));
|
|
125
|
+
// Schedule a targeted NOTIFY so the consumer wakes up when
|
|
126
|
+
// the visibility-delayed message becomes visible
|
|
127
|
+
if (retry && timeout > 0 && onRetryScheduled) {
|
|
128
|
+
onRetryScheduled(input.metadata.topic, timeout);
|
|
129
|
+
}
|
|
130
|
+
return messageId;
|
|
130
131
|
}
|
|
131
132
|
else {
|
|
132
133
|
const structuredError = this.structureError(input, output);
|
|
@@ -15,6 +15,10 @@ export declare const GET_JOB_ATTRIBUTES = "\n SELECT symbol || dimension AS fie
|
|
|
15
15
|
* Matches all activity jobs for the given workflow and extracts their input arguments.
|
|
16
16
|
*/
|
|
17
17
|
export declare const GET_ACTIVITY_INPUTS = "\n SELECT j.key, ja.value\n FROM {schema}.jobs j\n JOIN {schema}.jobs_attributes ja ON ja.job_id = j.id\n WHERE j.key LIKE $1\n AND ja.symbol = $2 AND ja.dimension = $3\n";
|
|
18
|
+
/**
|
|
19
|
+
* Fetch activity inputs from worker_streams for direct worker proxyer activities.
|
|
20
|
+
*/
|
|
21
|
+
export declare const GET_PROXYER_STREAM_INPUTS = "\n SELECT message\n FROM {schema}.worker_streams\n WHERE jid = $1\n AND aid IN ('proxyer', 'collator_proxyer', 'signaler_proxyer')\n ORDER BY created_at, id\n";
|
|
18
22
|
/**
|
|
19
23
|
* Fetch all worker stream messages for a job AND its child activities.
|
|
20
24
|
* Child activity jobs use the pattern: -{parentJobId}-$activityName-N
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* These queries support the exporter's input enrichment and direct query features.
|
|
5
5
|
*/
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
exports.buildChildWorkflowInputsQuery = exports.GET_STREAM_HISTORY_BY_JID_AND_AID = exports.GET_STREAM_HISTORY_BY_JID_AND_TYPE = exports.GET_STREAM_HISTORY_BY_JID = exports.GET_ACTIVITY_INPUTS = exports.GET_JOB_ATTRIBUTES = exports.GET_JOB_BY_KEY = void 0;
|
|
7
|
+
exports.buildChildWorkflowInputsQuery = exports.GET_STREAM_HISTORY_BY_JID_AND_AID = exports.GET_STREAM_HISTORY_BY_JID_AND_TYPE = exports.GET_STREAM_HISTORY_BY_JID = exports.GET_PROXYER_STREAM_INPUTS = exports.GET_ACTIVITY_INPUTS = exports.GET_JOB_ATTRIBUTES = exports.GET_JOB_BY_KEY = void 0;
|
|
8
8
|
/**
|
|
9
9
|
* Fetch job record by key.
|
|
10
10
|
*/
|
|
@@ -34,6 +34,16 @@ exports.GET_ACTIVITY_INPUTS = `
|
|
|
34
34
|
WHERE j.key LIKE $1
|
|
35
35
|
AND ja.symbol = $2 AND ja.dimension = $3
|
|
36
36
|
`;
|
|
37
|
+
/**
|
|
38
|
+
* Fetch activity inputs from worker_streams for direct worker proxyer activities.
|
|
39
|
+
*/
|
|
40
|
+
exports.GET_PROXYER_STREAM_INPUTS = `
|
|
41
|
+
SELECT message
|
|
42
|
+
FROM {schema}.worker_streams
|
|
43
|
+
WHERE jid = $1
|
|
44
|
+
AND aid IN ('proxyer', 'collator_proxyer', 'signaler_proxyer')
|
|
45
|
+
ORDER BY created_at, id
|
|
46
|
+
`;
|
|
37
47
|
/**
|
|
38
48
|
* Fetch all worker stream messages for a job AND its child activities.
|
|
39
49
|
* Child activity jobs use the pattern: -{parentJobId}-$activityName-N
|
|
@@ -1442,6 +1442,30 @@ class PostgresStoreService extends __1.StoreService {
|
|
|
1442
1442
|
// Skip unparseable values
|
|
1443
1443
|
}
|
|
1444
1444
|
}
|
|
1445
|
+
// If no results from legacy approach, try direct worker approach:
|
|
1446
|
+
// extract arguments from proxyer messages in worker_streams
|
|
1447
|
+
if (byNameIndex.size === 0) {
|
|
1448
|
+
const { GET_PROXYER_STREAM_INPUTS } = await Promise.resolve().then(() => __importStar(require('./exporter-sql')));
|
|
1449
|
+
const streamSql = GET_PROXYER_STREAM_INPUTS.replace(/{schema}/g, schemaName);
|
|
1450
|
+
const streamResult = await this.pgClient.query(streamSql, [workflowId]);
|
|
1451
|
+
for (const row of streamResult.rows) {
|
|
1452
|
+
try {
|
|
1453
|
+
const msg = typeof row.message === 'string' ? JSON.parse(row.message) : row.message;
|
|
1454
|
+
const data = msg?.data;
|
|
1455
|
+
if (data?.activityName && data?.arguments) {
|
|
1456
|
+
const activityName = data.activityName;
|
|
1457
|
+
const wfId = data.workflowId || '';
|
|
1458
|
+
const idxMatch = wfId.match(/-(\d+)$/);
|
|
1459
|
+
const execIndex = idxMatch ? idxMatch[1] : '0';
|
|
1460
|
+
byNameIndex.set(`${activityName}:${execIndex}`, data.arguments);
|
|
1461
|
+
byJobId.set(wfId, data.arguments);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
catch {
|
|
1465
|
+
// Skip unparseable messages
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1445
1469
|
return { byJobId, byNameIndex };
|
|
1446
1470
|
}
|
|
1447
1471
|
/**
|
|
@@ -58,6 +58,12 @@ declare class PostgresStreamService extends StreamService<PostgresClientType & P
|
|
|
58
58
|
* added to the transaction for atomic execution.
|
|
59
59
|
*/
|
|
60
60
|
publishMessages(streamName: string, messages: string[], options?: PublishMessageConfig): Promise<string[] | ProviderTransaction>;
|
|
61
|
+
/**
|
|
62
|
+
* Schedule a NOTIFY for a worker stream after a delay. Used to wake up
|
|
63
|
+
* consumers when a visibility-delayed retry message becomes visible,
|
|
64
|
+
* avoiding the need to wait for the scout's fallback poll.
|
|
65
|
+
*/
|
|
66
|
+
scheduleStreamNotify(streamName: string, delayMs: number): void;
|
|
61
67
|
_publishMessages(streamName: string, messages: string[], options?: PublishMessageConfig): {
|
|
62
68
|
sql: string;
|
|
63
69
|
params: any[];
|
|
@@ -174,6 +174,31 @@ class PostgresStreamService extends index_1.StreamService {
|
|
|
174
174
|
const target = this.resolveStreamTarget(streamName);
|
|
175
175
|
return Messages.publishMessages(this.streamClient, target.tableName, target.streamName, target.isEngine, messages, options, this.logger);
|
|
176
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* Schedule a NOTIFY for a worker stream after a delay. Used to wake up
|
|
179
|
+
* consumers when a visibility-delayed retry message becomes visible,
|
|
180
|
+
* avoiding the need to wait for the scout's fallback poll.
|
|
181
|
+
*/
|
|
182
|
+
scheduleStreamNotify(streamName, delayMs) {
|
|
183
|
+
const target = this.resolveStreamTarget(streamName);
|
|
184
|
+
const prefix = target.isEngine ? 'eng_' : 'wrk_';
|
|
185
|
+
let channelName = `${prefix}${target.streamName}`;
|
|
186
|
+
if (channelName.length > 63) {
|
|
187
|
+
channelName = channelName.substring(0, 63);
|
|
188
|
+
}
|
|
189
|
+
const payload = JSON.stringify({
|
|
190
|
+
stream_name: target.streamName,
|
|
191
|
+
table_type: target.isEngine ? 'engine' : 'worker',
|
|
192
|
+
});
|
|
193
|
+
setTimeout(async () => {
|
|
194
|
+
try {
|
|
195
|
+
await this.streamClient.query(`SELECT pg_notify($1, $2)`, [channelName, payload]);
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
// Best-effort; the scout fallback will pick it up
|
|
199
|
+
}
|
|
200
|
+
}, delayMs);
|
|
201
|
+
}
|
|
177
202
|
_publishMessages(streamName, messages, options) {
|
|
178
203
|
const target = this.resolveStreamTarget(streamName);
|
|
179
204
|
return Messages.buildPublishSQL(target.tableName, target.streamName, target.isEngine, messages, options);
|