@hotmeshio/hotmesh 0.14.6 → 0.14.7
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/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 +33 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,34 +29,17 @@ Install the package:
|
|
|
29
29
|
npm install @hotmeshio/hotmesh
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
The repo includes a `docker-compose.yml` that starts Postgres
|
|
32
|
+
The repo includes a `docker-compose.yml` that starts Postgres and a development container:
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
35
|
docker compose up -d
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
See the [Durable API reference](https://docs.hotmesh.io/classes/services_durable.Durable.html) for the full API surface — workflows, activities, signals, child workflows, and more.
|
|
39
39
|
|
|
40
|
-
##
|
|
40
|
+
## Writing workflows
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
```typescript
|
|
45
|
-
// activities.ts (shared between both approaches)
|
|
46
|
-
export async function checkInventory(itemId: string): Promise<number> {
|
|
47
|
-
return getInventoryCount(itemId);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export async function reserveItem(itemId: string, quantity: number): Promise<string> {
|
|
51
|
-
return createReservation(itemId, quantity);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export async function notifyBackorder(itemId: string): Promise<void> {
|
|
55
|
-
await sendBackorderEmail(itemId);
|
|
56
|
-
}
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
### Option 1: Code
|
|
42
|
+
**Define the workflow** — plain TypeScript with branching, loops, and error handling. Activities are proxied so their results are checkpointed and replayed on restart.
|
|
60
43
|
|
|
61
44
|
```typescript
|
|
62
45
|
// workflows.ts
|
|
@@ -76,124 +59,72 @@ export async function orderWorkflow(itemId: string, qty: number) {
|
|
|
76
59
|
return 'backordered';
|
|
77
60
|
}
|
|
78
61
|
}
|
|
62
|
+
```
|
|
79
63
|
|
|
80
|
-
|
|
81
|
-
|
|
64
|
+
**Start a worker** — connects to Postgres and begins processing workflows on the given task queue.
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
// worker.ts
|
|
68
|
+
import { Durable } from '@hotmeshio/hotmesh';
|
|
69
|
+
import { Client as Postgres } from 'pg';
|
|
70
|
+
import { orderWorkflow } from './workflows';
|
|
82
71
|
|
|
83
72
|
const connection = {
|
|
84
73
|
class: Postgres,
|
|
85
74
|
options: { connectionString: 'postgresql://localhost:5432/mydb' }
|
|
86
75
|
};
|
|
87
76
|
|
|
88
|
-
await Durable.Worker.create({
|
|
77
|
+
const worker = await Durable.Worker.create({
|
|
89
78
|
connection,
|
|
90
79
|
taskQueue: 'orders',
|
|
91
80
|
workflow: orderWorkflow,
|
|
92
|
-
activities,
|
|
93
81
|
});
|
|
94
82
|
|
|
83
|
+
await worker.run();
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Run a workflow** — start an execution and await its result. The client can run in a different process, container, or server.
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
// client.ts
|
|
90
|
+
import { Durable } from '@hotmeshio/hotmesh';
|
|
91
|
+
import { Client as Postgres } from 'pg';
|
|
92
|
+
|
|
93
|
+
const connection = {
|
|
94
|
+
class: Postgres,
|
|
95
|
+
options: { connectionString: 'postgresql://localhost:5432/mydb' }
|
|
96
|
+
};
|
|
97
|
+
|
|
95
98
|
const client = new Durable.Client({ connection });
|
|
96
99
|
const handle = await client.workflow.start({
|
|
97
100
|
args: ['item-123', 5],
|
|
98
101
|
taskQueue: 'orders',
|
|
99
102
|
workflowName: 'orderWorkflow',
|
|
100
|
-
workflowId: 'order-456'
|
|
103
|
+
workflowId: 'order-456',
|
|
101
104
|
});
|
|
102
105
|
|
|
103
106
|
const result = await handle.result();
|
|
104
107
|
```
|
|
105
108
|
|
|
106
|
-
###
|
|
107
|
-
|
|
108
|
-
```yaml
|
|
109
|
-
# order.yaml
|
|
110
|
-
activities:
|
|
111
|
-
trigger:
|
|
112
|
-
type: trigger
|
|
113
|
-
|
|
114
|
-
checkInventory:
|
|
115
|
-
type: worker
|
|
116
|
-
topic: inventory.check
|
|
117
|
-
|
|
118
|
-
reserveItem:
|
|
119
|
-
type: worker
|
|
120
|
-
topic: inventory.reserve
|
|
121
|
-
|
|
122
|
-
notifyBackorder:
|
|
123
|
-
type: worker
|
|
124
|
-
topic: inventory.backorder.notify
|
|
125
|
-
|
|
126
|
-
transitions:
|
|
127
|
-
trigger:
|
|
128
|
-
- to: checkInventory
|
|
129
|
-
|
|
130
|
-
checkInventory:
|
|
131
|
-
- to: reserveItem
|
|
132
|
-
conditions:
|
|
133
|
-
match:
|
|
134
|
-
- expected: true
|
|
135
|
-
actual:
|
|
136
|
-
'@pipe':
|
|
137
|
-
- ['{checkInventory.output.data.availableQty}', '{trigger.output.data.requestedQty}']
|
|
138
|
-
- ['{@conditional.gte}']
|
|
139
|
-
|
|
140
|
-
- to: notifyBackorder
|
|
141
|
-
conditions:
|
|
142
|
-
match:
|
|
143
|
-
- expected: false
|
|
144
|
-
actual:
|
|
145
|
-
'@pipe':
|
|
146
|
-
- ['{checkInventory.output.data.availableQty}', '{trigger.output.data.requestedQty}']
|
|
147
|
-
- ['{@conditional.gte}']
|
|
148
|
-
```
|
|
109
|
+
### Activities
|
|
149
110
|
|
|
150
|
-
|
|
151
|
-
```typescript
|
|
152
|
-
// main.ts (reuses same activities.ts)
|
|
153
|
-
import * as activities from './activities';
|
|
111
|
+
Activities are your side-effectful functions — database calls, API requests, anything non-deterministic. HotMesh checkpoints their results so they're never re-executed on replay.
|
|
154
112
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
topic: 'inventory.check',
|
|
161
|
-
connection,
|
|
162
|
-
callback: async (data) => {
|
|
163
|
-
const availableQty = await activities.checkInventory(data.data.itemId);
|
|
164
|
-
return { metadata: { ...data.metadata }, data: { availableQty } };
|
|
165
|
-
}
|
|
166
|
-
},
|
|
167
|
-
{
|
|
168
|
-
topic: 'inventory.reserve',
|
|
169
|
-
connection,
|
|
170
|
-
callback: async (data) => {
|
|
171
|
-
const reservationId = await activities.reserveItem(data.data.itemId, data.data.quantity);
|
|
172
|
-
return { metadata: { ...data.metadata }, data: { reservationId } };
|
|
173
|
-
}
|
|
174
|
-
},
|
|
175
|
-
{
|
|
176
|
-
topic: 'inventory.backorder.notify',
|
|
177
|
-
connection,
|
|
178
|
-
callback: async (data) => {
|
|
179
|
-
await activities.notifyBackorder(data.data.itemId);
|
|
180
|
-
return { metadata: { ...data.metadata } };
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
]
|
|
184
|
-
});
|
|
113
|
+
```typescript
|
|
114
|
+
// activities.ts
|
|
115
|
+
export async function checkInventory(itemId: string): Promise<number> {
|
|
116
|
+
return getInventoryCount(itemId);
|
|
117
|
+
}
|
|
185
118
|
|
|
186
|
-
|
|
187
|
-
|
|
119
|
+
export async function reserveItem(itemId: string, quantity: number): Promise<string> {
|
|
120
|
+
return createReservation(itemId, quantity);
|
|
121
|
+
}
|
|
188
122
|
|
|
189
|
-
|
|
190
|
-
itemId
|
|
191
|
-
|
|
192
|
-
});
|
|
123
|
+
export async function notifyBackorder(itemId: string): Promise<void> {
|
|
124
|
+
await sendBackorderEmail(itemId);
|
|
125
|
+
}
|
|
193
126
|
```
|
|
194
127
|
|
|
195
|
-
Both compile to the same distributed execution model.
|
|
196
|
-
|
|
197
128
|
## Common patterns
|
|
198
129
|
|
|
199
130
|
All snippets below run inside a workflow function (like `orderWorkflow` above). Durable methods are available as static imports:
|
|
@@ -321,6 +252,12 @@ There is no proprietary dashboard. Workflow state lives in Postgres, so use what
|
|
|
321
252
|
- **Logging** — set `HMSH_LOGLEVEL` (`debug`, `info`, `warn`, `error`, `silent`) to control log verbosity.
|
|
322
253
|
- **OpenTelemetry** — set `HMSH_TELEMETRY=true` to emit spans and metrics. Plug in any OTel-compatible collector.
|
|
323
254
|
|
|
255
|
+
## YAML workflows
|
|
256
|
+
|
|
257
|
+
HotMesh also supports a declarative YAML syntax. The same activities run in both modes — the difference is compilation speed. YAML workflows compile ~10x faster because the execution graph is declared upfront rather than discovered through replay. The tradeoff is expressiveness: YAML uses a functional pipe syntax for conditions and transformations instead of native TypeScript control flow.
|
|
258
|
+
|
|
259
|
+
See the [Quick Start guide](https://github.com/hotmeshio/sdk-typescript/blob/main/docs/quickstart.md) for YAML examples and the `tests/functional/` directory for working implementations.
|
|
260
|
+
|
|
324
261
|
## Architecture
|
|
325
262
|
|
|
326
263
|
For a deep dive into the transactional execution model — how every step is crash-safe, how the monotonic collation ledger guarantees exactly-once delivery, and how cycles and retries remain correct under arbitrary failure — see the [Collation Design Document](https://github.com/hotmeshio/sdk-typescript/blob/main/services/collator/README.md). The symbolic system (how to design workflows) and lifecycle details (how to deploy workflows) are covered in the [Architectural Overview](https://zenodo.org/records/12168558).
|
package/build/modules/enums.d.ts
CHANGED
|
@@ -162,6 +162,21 @@ export declare const HMSH_XCLAIM_DELAY_MS: number;
|
|
|
162
162
|
export declare const HMSH_XCLAIM_COUNT: number;
|
|
163
163
|
export declare const HMSH_XPENDING_COUNT: number;
|
|
164
164
|
export declare const HMSH_BATCH_SIZE: number;
|
|
165
|
+
/**
|
|
166
|
+
* Minimum batch size under adaptive scaling (default: 1).
|
|
167
|
+
*
|
|
168
|
+
* When stream depth is high, the adaptive logic reduces batch size
|
|
169
|
+
* to relieve back-pressure. This value is the floor — the smallest
|
|
170
|
+
* batch the system will fetch per consume cycle.
|
|
171
|
+
*
|
|
172
|
+
* - 1 (default): fully serial under max stress, safest
|
|
173
|
+
* - 2: retains some parallelism while limiting contention
|
|
174
|
+
*
|
|
175
|
+
* Both values produce equivalent throughput in practice (~233s for
|
|
176
|
+
* 1000 concurrent workflows). The reduction from the configured
|
|
177
|
+
* HMSH_BATCH_SIZE is what matters most — the floor is a safety net.
|
|
178
|
+
*/
|
|
179
|
+
export declare const HMSH_BATCH_SIZE_MIN: number;
|
|
165
180
|
/**
|
|
166
181
|
* Postgres stream reservation timeout in seconds (default: 30).
|
|
167
182
|
*
|
|
@@ -197,6 +212,32 @@ export declare const HMSH_BATCH_SIZE: number;
|
|
|
197
212
|
* HMSH_RESERVATION_TIMEOUT_S=30 (default)
|
|
198
213
|
*/
|
|
199
214
|
export declare const HMSH_RESERVATION_TIMEOUT_S: number;
|
|
215
|
+
/**
|
|
216
|
+
* Maximum reservation timeout in seconds for adaptive scaling (default: 1800).
|
|
217
|
+
*
|
|
218
|
+
* This is the ceiling for the adaptive reservation timeout — how far the
|
|
219
|
+
* system is allowed to stretch under sustained load. The adaptive logic
|
|
220
|
+
* only uses what it needs based on stream depth; this value defines the
|
|
221
|
+
* upper bound, not the steady state.
|
|
222
|
+
*
|
|
223
|
+
* The tradeoff is recovery time after a consumer crash: if a consumer
|
|
224
|
+
* reserves a message and dies, that message is unavailable until the
|
|
225
|
+
* timeout expires. A higher ceiling means longer recovery from crashes
|
|
226
|
+
* but prevents duplicate delivery under heavy sustained load.
|
|
227
|
+
*
|
|
228
|
+
* In practice, crashes are rare and the delay is bounded. The cost of
|
|
229
|
+
* a ceiling that is too low — duplicate delivery, collation errors,
|
|
230
|
+
* wasted CPU, workflow stalls — is far higher than a slightly longer
|
|
231
|
+
* recovery window after a crash.
|
|
232
|
+
*
|
|
233
|
+
* **Tuning guidance:**
|
|
234
|
+
* - Dedicated infrastructure with ample CPU: lower ceiling is fine (600s)
|
|
235
|
+
* - Shared/multi-tenant or CPU-constrained: use the default (1800s)
|
|
236
|
+
* - Long-running batch imports or large workflow graphs: increase (3600s+)
|
|
237
|
+
* - Cloud deployments without CPU contention: the adaptive logic will
|
|
238
|
+
* naturally stay near the starting timeout and rarely approach the ceiling
|
|
239
|
+
*/
|
|
240
|
+
export declare const HMSH_RESERVATION_TIMEOUT_MAX_S: number;
|
|
200
241
|
export declare const HMSH_EXPIRE_DURATION: number;
|
|
201
242
|
export declare const HMSH_FIDELITY_SECONDS: number;
|
|
202
243
|
export declare const HMSH_SCOUT_INTERVAL_SECONDS: number;
|
package/build/modules/enums.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
4
|
-
exports.HMSH_ROUTER_POLL_FALLBACK_INTERVAL = exports.HMSH_NOTIFY_PAYLOAD_LIMIT = exports.DEFAULT_TASK_QUEUE = exports.HMSH_GUID_SIZE = exports.HMSH_ROUTER_SCOUT_INTERVAL_MS = exports.HMSH_ROUTER_SCOUT_INTERVAL_SECONDS = void 0;
|
|
3
|
+
exports.HMSH_EXPIRE_DURATION = 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_COUNT = exports.HMSH_XCLAIM_DELAY_MS = exports.HMSH_BLOCK_TIME_MS = exports.HMSH_DURABLE_INITIAL_INTERVAL = exports.HMSH_DURABLE_EXP_BACKOFF = exports.HMSH_DURABLE_MAX_INTERVAL = exports.HMSH_DURABLE_MAX_ATTEMPTS = exports.HMSH_GRADUATED_INTERVAL_MS = exports.HMSH_MAX_TIMEOUT_MS = exports.HMSH_POISON_MESSAGE_THRESHOLD = exports.HMSH_MAX_RETRIES = exports.MAX_DELAY = exports.MAX_STREAM_RETRIES = exports.INITIAL_STREAM_BACKOFF = exports.MAX_STREAM_BACKOFF = exports.HMSH_EXPIRE_JOB_SECONDS = exports.HMSH_OTT_WAIT_TIME = exports.HMSH_DEPLOYMENT_PAUSE = exports.HMSH_DEPLOYMENT_DELAY = exports.HMSH_ACTIVATION_MAX_RETRY = exports.HMSH_QUORUM_DELAY_MS = exports.HMSH_QUORUM_ROLLCALL_CYCLES = exports.HMSH_STATUS_UNKNOWN = exports.HMSH_CODE_DURABLE_RETRYABLE = exports.HMSH_CODE_DURABLE_FATAL = exports.HMSH_CODE_DURABLE_MAXED = exports.HMSH_CODE_DURABLE_TIMEOUT = exports.HMSH_CODE_DURABLE_WAIT = exports.HMSH_CODE_DURABLE_CONTINUE = exports.HMSH_CODE_DURABLE_PROXY = exports.HMSH_CODE_DURABLE_CHILD = exports.HMSH_CODE_DURABLE_ALL = exports.HMSH_CODE_DURABLE_SLEEP = exports.HMSH_CODE_UNACKED = exports.HMSH_CODE_TIMEOUT = exports.HMSH_CODE_UNKNOWN = exports.HMSH_CODE_INTERRUPT = exports.HMSH_CODE_NOTFOUND = exports.HMSH_CODE_PENDING = exports.HMSH_CODE_SUCCESS = exports.HMSH_PENDING_SIGNAL_EXPIRE = exports.HMSH_SIGNAL_EXPIRE = exports.HMSH_TELEMETRY = exports.HMSH_LOGLEVEL = void 0;
|
|
4
|
+
exports.HMSH_ROUTER_POLL_FALLBACK_INTERVAL = exports.HMSH_NOTIFY_PAYLOAD_LIMIT = exports.DEFAULT_TASK_QUEUE = exports.HMSH_GUID_SIZE = exports.HMSH_ROUTER_SCOUT_INTERVAL_MS = exports.HMSH_ROUTER_SCOUT_INTERVAL_SECONDS = exports.HMSH_SCOUT_INTERVAL_SECONDS = exports.HMSH_FIDELITY_SECONDS = void 0;
|
|
5
5
|
/**
|
|
6
6
|
* Determines the log level for the application. The default is 'info'.
|
|
7
7
|
*/
|
|
@@ -179,6 +179,21 @@ exports.HMSH_XCLAIM_DELAY_MS = parseInt(process.env.HMSH_XCLAIM_DELAY_MS, 10) ||
|
|
|
179
179
|
exports.HMSH_XCLAIM_COUNT = parseInt(process.env.HMSH_XCLAIM_COUNT, 10) || 3;
|
|
180
180
|
exports.HMSH_XPENDING_COUNT = parseInt(process.env.HMSH_XPENDING_COUNT, 10) || 10;
|
|
181
181
|
exports.HMSH_BATCH_SIZE = parseInt(process.env.HMSH_BATCH_SIZE, 10) || 10;
|
|
182
|
+
/**
|
|
183
|
+
* Minimum batch size under adaptive scaling (default: 1).
|
|
184
|
+
*
|
|
185
|
+
* When stream depth is high, the adaptive logic reduces batch size
|
|
186
|
+
* to relieve back-pressure. This value is the floor — the smallest
|
|
187
|
+
* batch the system will fetch per consume cycle.
|
|
188
|
+
*
|
|
189
|
+
* - 1 (default): fully serial under max stress, safest
|
|
190
|
+
* - 2: retains some parallelism while limiting contention
|
|
191
|
+
*
|
|
192
|
+
* Both values produce equivalent throughput in practice (~233s for
|
|
193
|
+
* 1000 concurrent workflows). The reduction from the configured
|
|
194
|
+
* HMSH_BATCH_SIZE is what matters most — the floor is a safety net.
|
|
195
|
+
*/
|
|
196
|
+
exports.HMSH_BATCH_SIZE_MIN = parseInt(process.env.HMSH_BATCH_SIZE_MIN, 10) || 1;
|
|
182
197
|
/**
|
|
183
198
|
* Postgres stream reservation timeout in seconds (default: 30).
|
|
184
199
|
*
|
|
@@ -214,6 +229,32 @@ exports.HMSH_BATCH_SIZE = parseInt(process.env.HMSH_BATCH_SIZE, 10) || 10;
|
|
|
214
229
|
* HMSH_RESERVATION_TIMEOUT_S=30 (default)
|
|
215
230
|
*/
|
|
216
231
|
exports.HMSH_RESERVATION_TIMEOUT_S = parseInt(process.env.HMSH_RESERVATION_TIMEOUT_S, 10) || 30;
|
|
232
|
+
/**
|
|
233
|
+
* Maximum reservation timeout in seconds for adaptive scaling (default: 1800).
|
|
234
|
+
*
|
|
235
|
+
* This is the ceiling for the adaptive reservation timeout — how far the
|
|
236
|
+
* system is allowed to stretch under sustained load. The adaptive logic
|
|
237
|
+
* only uses what it needs based on stream depth; this value defines the
|
|
238
|
+
* upper bound, not the steady state.
|
|
239
|
+
*
|
|
240
|
+
* The tradeoff is recovery time after a consumer crash: if a consumer
|
|
241
|
+
* reserves a message and dies, that message is unavailable until the
|
|
242
|
+
* timeout expires. A higher ceiling means longer recovery from crashes
|
|
243
|
+
* but prevents duplicate delivery under heavy sustained load.
|
|
244
|
+
*
|
|
245
|
+
* In practice, crashes are rare and the delay is bounded. The cost of
|
|
246
|
+
* a ceiling that is too low — duplicate delivery, collation errors,
|
|
247
|
+
* wasted CPU, workflow stalls — is far higher than a slightly longer
|
|
248
|
+
* recovery window after a crash.
|
|
249
|
+
*
|
|
250
|
+
* **Tuning guidance:**
|
|
251
|
+
* - Dedicated infrastructure with ample CPU: lower ceiling is fine (600s)
|
|
252
|
+
* - Shared/multi-tenant or CPU-constrained: use the default (1800s)
|
|
253
|
+
* - Long-running batch imports or large workflow graphs: increase (3600s+)
|
|
254
|
+
* - Cloud deployments without CPU contention: the adaptive logic will
|
|
255
|
+
* naturally stay near the starting timeout and rarely approach the ceiling
|
|
256
|
+
*/
|
|
257
|
+
exports.HMSH_RESERVATION_TIMEOUT_MAX_S = parseInt(process.env.HMSH_RESERVATION_TIMEOUT_MAX_S, 10) || 1800;
|
|
217
258
|
// TASK WORKER
|
|
218
259
|
exports.HMSH_EXPIRE_DURATION = parseInt(process.env.HMSH_EXPIRE_DURATION, 10) || 1;
|
|
219
260
|
const BASE_FIDELITY_SECONDS = 5;
|
package/build/package.json
CHANGED
|
@@ -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,
|
|
@@ -597,5 +611,4 @@ class ConsumptionManager {
|
|
|
597
611
|
ConsumptionManager.DEPTH_CHECK_INTERVAL_MS = 10000;
|
|
598
612
|
ConsumptionManager.DEPTH_SCALE_UP_THRESHOLD = 100;
|
|
599
613
|
ConsumptionManager.DEPTH_SCALE_DOWN_THRESHOLD = 10;
|
|
600
|
-
ConsumptionManager.RESERVATION_TIMEOUT_MAX_S = 600;
|
|
601
614
|
exports.ConsumptionManager = ConsumptionManager;
|