@prosody-events/prosody 0.1.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/LICENSE +21 -0
- package/README.md +978 -0
- package/bindings.d.ts +522 -0
- package/bindings.js +587 -0
- package/index.d.ts +307 -0
- package/index.js +588 -0
- package/package.json +82 -0
package/README.md
ADDED
|
@@ -0,0 +1,978 @@
|
|
|
1
|
+
# Prosody: JavaScript Bindings for Kafka
|
|
2
|
+
|
|
3
|
+
Prosody offers JavaScript bindings to the [Prosody Kafka client](https://github.com/prosody-events/prosody), providing
|
|
4
|
+
features for message production and consumption, including configurable retry mechanisms, failure handling
|
|
5
|
+
strategies, and integrated OpenTelemetry support for distributed tracing.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Kafka Consumer**: Per-key ordering with cross-key concurrency, offset management, consumer groups
|
|
10
|
+
- **Kafka Producer**: Idempotent delivery with configurable retries
|
|
11
|
+
- **Timer System**: Persistent scheduled execution backed by Cassandra or in-memory store
|
|
12
|
+
- **Quality of Service**: Fair scheduling limits concurrency and prevents failures from starving fresh traffic. Pipeline mode adds deferred retry and monopolization detection
|
|
13
|
+
- **Distributed Tracing**: OpenTelemetry integration for tracing message flow across services
|
|
14
|
+
- **Backpressure**: Pauses partitions when handlers fall behind
|
|
15
|
+
- **Mocking**: In-memory Kafka broker for tests (`mock: true`)
|
|
16
|
+
- **Failure Handling**: Pipeline (retry forever), Low-Latency (dead letter), Best-Effort (log and skip)
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @prosody-events/prosody
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Note**: For Linux deployments, use the `558529356944.dkr.ecr.us-east-1.amazonaws.com/jemalloc-images:node-22` base image, which provides optimized memory allocation and cross-compilation support.
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
```javascript
|
|
29
|
+
const {ProsodyClient} = require('@prosody-events/prosody');
|
|
30
|
+
|
|
31
|
+
// Initialize the client with Kafka bootstrap servers, consumer group, and topics
|
|
32
|
+
const client = new ProsodyClient({
|
|
33
|
+
// Bootstrap servers should normally be set using the PROSODY_BOOTSTRAP_SERVERS environment variable
|
|
34
|
+
bootstrapServers: "localhost:9092",
|
|
35
|
+
|
|
36
|
+
// To allow loopbacks, the source_system must be different from the group_id.
|
|
37
|
+
// Normally, the source_system would be left unspecified, which would default to the group_id.
|
|
38
|
+
sourceSystem: "my-application-source",
|
|
39
|
+
|
|
40
|
+
// The group_id should be set to the name of your application
|
|
41
|
+
groupId: "my-consumer-group",
|
|
42
|
+
|
|
43
|
+
// Topics the client should subscribe to
|
|
44
|
+
subscribedTopics: "my-topic"
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Define a message handler
|
|
48
|
+
const messageHandler = {
|
|
49
|
+
onMessage: async (context, message, signal) => {
|
|
50
|
+
// Process the received message
|
|
51
|
+
console.log(`Received message: ${JSON.stringify(message)}`);
|
|
52
|
+
|
|
53
|
+
// Schedule a timer for delayed processing
|
|
54
|
+
if (message.payload.scheduleFollowup) {
|
|
55
|
+
const followupTime = new Date(Date.now() + 30000); // 30 seconds from now
|
|
56
|
+
await context.schedule(followupTime);
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
onTimer: async (context, timer, signal) => {
|
|
61
|
+
// Handle timer firing
|
|
62
|
+
console.log(`Timer fired for key: ${timer.key} at ${timer.time}`);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// Subscribe to messages using the message handler
|
|
67
|
+
client.subscribe(messageHandler);
|
|
68
|
+
|
|
69
|
+
// Send a message to a topic
|
|
70
|
+
await client.send("my-topic", "message-key", {content: "Hello, Kafka!"});
|
|
71
|
+
|
|
72
|
+
// Ensure proper shutdown when done
|
|
73
|
+
await client.unsubscribe();
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Architecture
|
|
77
|
+
|
|
78
|
+
Prosody enables efficient, parallel processing of Kafka messages while maintaining order for messages with the same key:
|
|
79
|
+
|
|
80
|
+
- **Partition-Level Parallelism**: Separate management of each Kafka partition
|
|
81
|
+
- **Key-Based Queuing**: Ordered processing for each key within a partition
|
|
82
|
+
- **Concurrent Processing**: Simultaneous processing of different keys
|
|
83
|
+
- **Backpressure Management**: Pause consumption from backed-up partitions
|
|
84
|
+
|
|
85
|
+
## Quality of Service
|
|
86
|
+
|
|
87
|
+
All modes use **fair scheduling** to limit concurrency and distribute execution time. Pipeline mode adds **deferred
|
|
88
|
+
retry** and **monopolization detection**.
|
|
89
|
+
|
|
90
|
+
### Fair Scheduling (All Modes)
|
|
91
|
+
|
|
92
|
+
The scheduler controls which message runs next and how many run concurrently.
|
|
93
|
+
|
|
94
|
+
**Virtual Time (VT):** Each key accumulates VT equal to its handler execution time. The scheduler picks the key with the
|
|
95
|
+
lowest VT. A key that runs for 500ms accumulates 500ms of VT; a key that hasn't run recently has zero VT and gets
|
|
96
|
+
priority.
|
|
97
|
+
|
|
98
|
+
**Two-Class Split:** Normal messages and failure retries have separate VT pools. The scheduler allocates execution time
|
|
99
|
+
between them (default: 70% normal, 30% failure). During a failure spike, retries get at most 30% of execution time—fresh
|
|
100
|
+
messages continue processing.
|
|
101
|
+
|
|
102
|
+
**Starvation Prevention:** Tasks receive a quadratic priority boost based on wait time. A task waiting 2 minutes
|
|
103
|
+
(configurable) gets maximum boost, overriding VT disadvantage.
|
|
104
|
+
|
|
105
|
+
### Deferred Retry (Pipeline Mode)
|
|
106
|
+
|
|
107
|
+
Moves failing keys to timer-based retry so the partition can continue processing other keys.
|
|
108
|
+
|
|
109
|
+
On transient failure: store the message offset in Cassandra, schedule a timer, return success. The partition advances.
|
|
110
|
+
When the timer fires, reload the message from Kafka and retry.
|
|
111
|
+
|
|
112
|
+
```javascript
|
|
113
|
+
// Configure defer behavior
|
|
114
|
+
const client = new ProsodyClient({
|
|
115
|
+
groupId: "my-consumer-group",
|
|
116
|
+
subscribedTopics: "my-topic",
|
|
117
|
+
deferEnabled: true, // Enable deferral (default: true)
|
|
118
|
+
deferBaseMs: 1000, // Wait 1s before first retry
|
|
119
|
+
deferMaxDelayMs: 86400000, // Cap at 24 hours
|
|
120
|
+
deferFailureThreshold: 0.9 // Disable when >90% failing
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**Failure Rate Gating:** When >90% of recent messages fail, deferral disables. The retry middleware blocks the
|
|
125
|
+
partition, applying backpressure upstream.
|
|
126
|
+
|
|
127
|
+
### Monopolization Detection (Pipeline Mode)
|
|
128
|
+
|
|
129
|
+
Rejects keys that consume too much execution time.
|
|
130
|
+
|
|
131
|
+
The middleware tracks per-key execution time in 5-minute rolling windows. Keys exceeding 90% of window time are rejected
|
|
132
|
+
with a transient error, routing them through defer.
|
|
133
|
+
|
|
134
|
+
```javascript
|
|
135
|
+
// Configure monopolization detection
|
|
136
|
+
const client = new ProsodyClient({
|
|
137
|
+
groupId: "my-consumer-group",
|
|
138
|
+
subscribedTopics: "my-topic",
|
|
139
|
+
monopolizationEnabled: true, // Enable detection (default: true)
|
|
140
|
+
monopolizationThreshold: 0.9, // Reject keys using >90% of window
|
|
141
|
+
monopolizationWindowMs: 300000 // 5-minute window
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Handler Timeout
|
|
146
|
+
|
|
147
|
+
Handlers are automatically cancelled if they exceed a deadline:
|
|
148
|
+
|
|
149
|
+
```javascript
|
|
150
|
+
const client = new ProsodyClient({
|
|
151
|
+
groupId: "my-consumer-group",
|
|
152
|
+
subscribedTopics: "my-topic",
|
|
153
|
+
timeoutMs: 30000, // Cancel after 30 seconds
|
|
154
|
+
stallThresholdMs: 60000 // Report unhealthy after 60 seconds
|
|
155
|
+
});
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
When a handler times out, `context.shouldCancel` becomes `true` and `context.onCancel()` resolves. The handler should
|
|
159
|
+
exit promptly. If not specified, timeout defaults to 80% of `stallThresholdMs`.
|
|
160
|
+
|
|
161
|
+
## Configuration
|
|
162
|
+
|
|
163
|
+
Configure via constructor options or environment variables. Options fall back to environment variables when unset.
|
|
164
|
+
|
|
165
|
+
### Core
|
|
166
|
+
|
|
167
|
+
| Option / Environment Variable | Description | Default |
|
|
168
|
+
|-----------------------------------------|---------------------------------------------------|--------------|
|
|
169
|
+
| `bootstrapServers` / `PROSODY_BOOTSTRAP_SERVERS` | Kafka servers to connect to | - |
|
|
170
|
+
| `groupId` / `PROSODY_GROUP_ID` | Consumer group name | - |
|
|
171
|
+
| `subscribedTopics` / `PROSODY_SUBSCRIBED_TOPICS` | Topics to read from | - |
|
|
172
|
+
| `allowedEvents` / `PROSODY_ALLOWED_EVENTS` | Only process events matching these prefixes | (all) |
|
|
173
|
+
| `sourceSystem` / `PROSODY_SOURCE_SYSTEM` | Tag for outgoing messages (prevents reprocessing)| `<groupId>` |
|
|
174
|
+
| `mock` / `PROSODY_MOCK` | Use in-memory Kafka for testing | false |
|
|
175
|
+
|
|
176
|
+
### Consumer
|
|
177
|
+
|
|
178
|
+
| Option / Environment Variable | Description | Default |
|
|
179
|
+
|-----------------------------------------|------------------------------------------------------|------------------------|
|
|
180
|
+
| `maxConcurrency` / `PROSODY_MAX_CONCURRENCY` | Max messages being processed simultaneously | 32 |
|
|
181
|
+
| `maxUncommitted` / `PROSODY_MAX_UNCOMMITTED` | Max queued messages before pausing consumption | 64 |
|
|
182
|
+
| `timeoutMs` / `PROSODY_TIMEOUT` | Cancel handler if it runs longer than this | 80% of stall threshold |
|
|
183
|
+
| `commitIntervalMs` / `PROSODY_COMMIT_INTERVAL` | How often to save progress to Kafka | 1s |
|
|
184
|
+
| `pollIntervalMs` / `PROSODY_POLL_INTERVAL` | How often to fetch new messages from Kafka | 100ms |
|
|
185
|
+
| `shutdownTimeoutMs` / `PROSODY_SHUTDOWN_TIMEOUT` | Wait this long for in-flight work before force-quit | 30s |
|
|
186
|
+
| `stallThresholdMs` / `PROSODY_STALL_THRESHOLD` | Report unhealthy if no progress for this long | 5m |
|
|
187
|
+
| `probePort` / `PROSODY_PROBE_PORT` | HTTP port for health checks (null to disable) | 8000 |
|
|
188
|
+
| `failureTopic` / `PROSODY_FAILURE_TOPIC` | Send unprocessable messages here (dead letter queue) | - |
|
|
189
|
+
| `idempotenceCacheSize` / `PROSODY_IDEMPOTENCE_CACHE_SIZE` | Global shared cache capacity across all partitions for deduplicating messages. Set to 0 to disable the entire deduplication middleware (both in-memory cache and Cassandra persistent store) | 8192 |
|
|
190
|
+
| `idempotenceVersion` / `PROSODY_IDEMPOTENCE_VERSION` | Version string for cache-busting dedup hashes | `"1"` |
|
|
191
|
+
| `idempotenceTtlS` / `PROSODY_IDEMPOTENCE_TTL` | TTL for dedup records in Cassandra in seconds | 604800 |
|
|
192
|
+
| `slabSizeMs` / `PROSODY_SLAB_SIZE` | Timer storage granularity (rarely needs changing) | 1h |
|
|
193
|
+
| `messageSpans` / `PROSODY_MESSAGE_SPANS` | Span linking for message execution: `child` (child-of) or `follows_from` | `child` |
|
|
194
|
+
| `timerSpans` / `PROSODY_TIMER_SPANS` | Span linking for timer execution: `child` (child-of) or `follows_from` | `follows_from` |
|
|
195
|
+
|
|
196
|
+
### Producer
|
|
197
|
+
|
|
198
|
+
| Option / Environment Variable | Description | Default |
|
|
199
|
+
|-----------------------------------------|---------------------------------|---------|
|
|
200
|
+
| `sendTimeoutMs` / `PROSODY_SEND_TIMEOUT` | Give up sending after this long | 1s |
|
|
201
|
+
|
|
202
|
+
### Retry
|
|
203
|
+
|
|
204
|
+
When a handler fails, retry with exponential backoff:
|
|
205
|
+
|
|
206
|
+
| Option / Environment Variable | Description | Default |
|
|
207
|
+
|-----------------------------------------|-----------------------------------|---------|
|
|
208
|
+
| `maxRetries` / `PROSODY_MAX_RETRIES` | Give up after this many attempts | 3 |
|
|
209
|
+
| `retryBaseMs` / `PROSODY_RETRY_BASE` | Wait this long before first retry | 20ms |
|
|
210
|
+
| `maxRetryDelayMs` / `PROSODY_RETRY_MAX_DELAY` | Never wait longer than this | 5m |
|
|
211
|
+
|
|
212
|
+
### Deferral (Pipeline Mode)
|
|
213
|
+
|
|
214
|
+
| Option / Environment Variable | Description | Default |
|
|
215
|
+
|-----------------------------------------|---------------------------------------------------|---------|
|
|
216
|
+
| `deferEnabled` / `PROSODY_DEFER_ENABLED` | Enable deferral for new messages | true |
|
|
217
|
+
| `deferBaseMs` / `PROSODY_DEFER_BASE` | Wait this long before first deferred retry | 1s |
|
|
218
|
+
| `deferMaxDelayMs` / `PROSODY_DEFER_MAX_DELAY` | Never wait longer than this | 24h |
|
|
219
|
+
| `deferFailureThreshold` / `PROSODY_DEFER_FAILURE_THRESHOLD` | Disable deferral when failure rate exceeds this | 0.9 |
|
|
220
|
+
| `deferFailureWindowMs` / `PROSODY_DEFER_FAILURE_WINDOW` | Measure failure rate over this time window | 5m |
|
|
221
|
+
| `deferCacheSize` / `PROSODY_DEFER_CACHE_SIZE` | Track this many deferred keys in memory | 1024 |
|
|
222
|
+
| `deferSeekTimeoutMs` / `PROSODY_DEFER_SEEK_TIMEOUT` | Timeout when loading deferred messages | 30s |
|
|
223
|
+
| `deferDiscardThreshold` / `PROSODY_DEFER_DISCARD_THRESHOLD` | Read optimization (rarely needs changing) | 100 |
|
|
224
|
+
|
|
225
|
+
### Monopolization Detection (Pipeline Mode)
|
|
226
|
+
|
|
227
|
+
| Option / Environment Variable | Description | Default |
|
|
228
|
+
|-----------------------------------------|-----------------------------------------|---------|
|
|
229
|
+
| `monopolizationEnabled` / `PROSODY_MONOPOLIZATION_ENABLED` | Enable hot key protection | true |
|
|
230
|
+
| `monopolizationThreshold` / `PROSODY_MONOPOLIZATION_THRESHOLD` | Max handler time as fraction of window | 0.9 |
|
|
231
|
+
| `monopolizationWindowMs` / `PROSODY_MONOPOLIZATION_WINDOW` | Measurement window | 5m |
|
|
232
|
+
| `monopolizationCacheSize` / `PROSODY_MONOPOLIZATION_CACHE_SIZE` | Max distinct keys to track | 8192 |
|
|
233
|
+
|
|
234
|
+
### Fair Scheduling (All Modes)
|
|
235
|
+
|
|
236
|
+
| Option / Environment Variable | Description | Default |
|
|
237
|
+
|-----------------------------------------|------------------------------------------------------------------|---------|
|
|
238
|
+
| `schedulerFailureWeight` / `PROSODY_SCHEDULER_FAILURE_WEIGHT` | Fraction of processing time reserved for retries | 0.3 |
|
|
239
|
+
| `schedulerMaxWaitMs` / `PROSODY_SCHEDULER_MAX_WAIT` | Messages waiting this long get maximum priority | 2m |
|
|
240
|
+
| `schedulerWaitWeight` / `PROSODY_SCHEDULER_WAIT_WEIGHT` | Priority boost for waiting messages (higher = more aggressive) | 200.0 |
|
|
241
|
+
| `schedulerCacheSize` / `PROSODY_SCHEDULER_CACHE_SIZE` | Max distinct keys to track | 8192 |
|
|
242
|
+
|
|
243
|
+
### Telemetry Emitter
|
|
244
|
+
|
|
245
|
+
Produces message and timer lifecycle events to a Kafka topic for observability:
|
|
246
|
+
|
|
247
|
+
| Option / Environment Variable | Description | Default |
|
|
248
|
+
|-----------------------------------------|---------------------------------------------------|----------------------------|
|
|
249
|
+
| `telemetryEnabled` / `PROSODY_TELEMETRY_ENABLED` | Produce lifecycle events to Kafka | true |
|
|
250
|
+
| `telemetryTopic` / `PROSODY_TELEMETRY_TOPIC` | Kafka topic for telemetry events | prosody.telemetry-events |
|
|
251
|
+
|
|
252
|
+
### Cassandra
|
|
253
|
+
|
|
254
|
+
Persistent storage for timers and deferred retries (not needed if `mock: true`):
|
|
255
|
+
|
|
256
|
+
| Option / Environment Variable | Description | Default |
|
|
257
|
+
|-----------------------------------------|------------------------------------|---------|
|
|
258
|
+
| `cassandraNodes` / `PROSODY_CASSANDRA_NODES` | Servers to connect to (host:port) | - |
|
|
259
|
+
| `cassandraKeyspace` / `PROSODY_CASSANDRA_KEYSPACE` | Keyspace name | prosody |
|
|
260
|
+
| `cassandraUser` / `PROSODY_CASSANDRA_USER` | Username | - |
|
|
261
|
+
| `cassandraPassword` / `PROSODY_CASSANDRA_PASSWORD` | Password | - |
|
|
262
|
+
| `cassandraDatacenter` / `PROSODY_CASSANDRA_DATACENTER` | Prefer this datacenter for queries | - |
|
|
263
|
+
| `cassandraRack` / `PROSODY_CASSANDRA_RACK` | Prefer this rack for queries | - |
|
|
264
|
+
| `cassandraRetentionSeconds` / `PROSODY_CASSANDRA_RETENTION` | Delete data older than this | 1y |
|
|
265
|
+
|
|
266
|
+
## Liveness and Readiness Probes
|
|
267
|
+
|
|
268
|
+
Prosody includes a built-in probe server for consumer-based applications that provides health check endpoints. The probe
|
|
269
|
+
server is tied to the consumer's lifecycle and offers two main endpoints:
|
|
270
|
+
|
|
271
|
+
1. `/readyz`: A readiness probe that checks if any partitions are assigned to the consumer. Returns a success status
|
|
272
|
+
only when the consumer has at least one partition assigned, indicating it's ready to process messages.
|
|
273
|
+
|
|
274
|
+
2. `/livez`: A liveness probe that checks if any partitions have stalled (haven't processed a message within a
|
|
275
|
+
configured time threshold).
|
|
276
|
+
|
|
277
|
+
Configure the probe server using either the client constructor:
|
|
278
|
+
|
|
279
|
+
```javascript
|
|
280
|
+
const client = new ProsodyClient({
|
|
281
|
+
groupId: "my-consumer-group",
|
|
282
|
+
subscribedTopics: "my-topic",
|
|
283
|
+
probePort: 8000, // Set to null to disable
|
|
284
|
+
stallThresholdMs: 15000 // 15 seconds before considering a partition stalled
|
|
285
|
+
});
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Or via environment variables:
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
PROSODY_PROBE_PORT=8000 # Set to 'none' to disable
|
|
292
|
+
PROSODY_STALL_THRESHOLD=15s # Default stall detection threshold
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### Important Notes
|
|
296
|
+
|
|
297
|
+
1. The probe server starts automatically when the consumer is subscribed and stops when unsubscribed.
|
|
298
|
+
2. A partition is considered "stalled" if it hasn't processed a message within the `stallThreshold` duration.
|
|
299
|
+
3. The stall threshold should be set based on your application's message processing latency and expected message
|
|
300
|
+
frequency.
|
|
301
|
+
4. Setting the threshold too low might cause false positives, while setting it too high could delay detection of actual
|
|
302
|
+
issues.
|
|
303
|
+
5. The probe server is only active when consuming messages (not for producer-only usage).
|
|
304
|
+
|
|
305
|
+
You can monitor the stall state programmatically using the client's properties:
|
|
306
|
+
|
|
307
|
+
```javascript
|
|
308
|
+
// Get the number of partitions currently assigned to this consumer
|
|
309
|
+
const partitionCount = client.assignedPartitionCount;
|
|
310
|
+
|
|
311
|
+
// You can use these in your own health checks or monitoring
|
|
312
|
+
if (client.isStalled) {
|
|
313
|
+
console.warn('Consumer has stalled partitions');
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
## Advanced Usage
|
|
318
|
+
|
|
319
|
+
### Pipeline Mode
|
|
320
|
+
|
|
321
|
+
All messages must be processed. Retries indefinitely. Uses defer and monopolization detection.
|
|
322
|
+
|
|
323
|
+
**Middleware stack:**
|
|
324
|
+
```
|
|
325
|
+
Kafka → Deduplication → Retry → Defer → Monopolization → Shutdown → Scheduler → Timeout → Telemetry → Handler
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
| Layer | Purpose |
|
|
329
|
+
|----------------|----------------------------------------------------------|
|
|
330
|
+
| Deduplication | Skips messages whose ID was already processed |
|
|
331
|
+
| Retry | Retries transient errors indefinitely |
|
|
332
|
+
| Defer | Stores failing messages for timer-based retry |
|
|
333
|
+
| Monopolization | Rejects keys exceeding execution time threshold |
|
|
334
|
+
| Shutdown | Drains in-flight work on partition revocation |
|
|
335
|
+
| Scheduler | Enforces concurrency limits and VT-based priority |
|
|
336
|
+
| Timeout | Cancels handlers exceeding deadline |
|
|
337
|
+
| Telemetry | Emits handler lifecycle events |
|
|
338
|
+
|
|
339
|
+
```javascript
|
|
340
|
+
const client = new ProsodyClient({
|
|
341
|
+
mode: Mode.Pipeline, // Default mode
|
|
342
|
+
groupId: "my-consumer-group",
|
|
343
|
+
subscribedTopics: "my-topic"
|
|
344
|
+
});
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### Low-Latency Mode
|
|
348
|
+
|
|
349
|
+
Tries a few times, then routes failures to a dead letter topic.
|
|
350
|
+
|
|
351
|
+
- Retries up to `maxRetries` times, then writes to failure topic
|
|
352
|
+
- Fair scheduling limits how much time retries consume
|
|
353
|
+
- Use when you need to keep moving and can reprocess failures later
|
|
354
|
+
|
|
355
|
+
```javascript
|
|
356
|
+
const client = new ProsodyClient({
|
|
357
|
+
mode: Mode.LowLatency,
|
|
358
|
+
groupId: "my-consumer-group",
|
|
359
|
+
subscribedTopics: "my-topic",
|
|
360
|
+
failureTopic: "failed-messages", // Required for low-latency mode
|
|
361
|
+
maxRetries: 3 // Give up after 3 attempts
|
|
362
|
+
});
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### Best-Effort Mode
|
|
366
|
+
|
|
367
|
+
Logs failures and moves on.
|
|
368
|
+
|
|
369
|
+
- No retries; failed messages are logged and committed
|
|
370
|
+
- Fair scheduling still enforces concurrency limits
|
|
371
|
+
- Use for development or when message loss is acceptable
|
|
372
|
+
|
|
373
|
+
```javascript
|
|
374
|
+
const client = new ProsodyClient({
|
|
375
|
+
mode: Mode.BestEffort,
|
|
376
|
+
groupId: "my-consumer-group",
|
|
377
|
+
subscribedTopics: "my-topic"
|
|
378
|
+
});
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
## Event Type Filtering
|
|
382
|
+
|
|
383
|
+
Prosody supports filtering messages based on event type prefixes, allowing your consumer to process only specific types
|
|
384
|
+
of events:
|
|
385
|
+
|
|
386
|
+
```javascript
|
|
387
|
+
// Process only events with types starting with "user." or "account."
|
|
388
|
+
const client = new ProsodyClient({
|
|
389
|
+
groupId: "my-consumer-group",
|
|
390
|
+
subscribedTopics: "my-topic",
|
|
391
|
+
allowedEvents: ["user.", "account."]
|
|
392
|
+
});
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
Or via environment variables:
|
|
396
|
+
|
|
397
|
+
```bash
|
|
398
|
+
PROSODY_ALLOWED_EVENTS=user.,account.
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### Matching Behavior
|
|
402
|
+
|
|
403
|
+
Prefixes must match exactly from the start of the event type:
|
|
404
|
+
|
|
405
|
+
✓ Matches:
|
|
406
|
+
|
|
407
|
+
- `{"type": "user.created"}` matches prefix `user.`
|
|
408
|
+
- `{"type": "account.deleted"}` matches prefix `account.`
|
|
409
|
+
|
|
410
|
+
✗ No Match:
|
|
411
|
+
|
|
412
|
+
- `{"type": "admin.user.created"}` doesn't match `user.`
|
|
413
|
+
- `{"type": "my.account.deleted"}` doesn't match `account.`
|
|
414
|
+
- `{"type": "notification"}` doesn't match any prefix
|
|
415
|
+
|
|
416
|
+
If no prefixes are configured, all messages are processed. Messages without a `type` field are always processed.
|
|
417
|
+
|
|
418
|
+
## Source System Deduplication
|
|
419
|
+
|
|
420
|
+
Prosody prevents processing loops in distributed systems by tracking the source of each message:
|
|
421
|
+
|
|
422
|
+
```javascript
|
|
423
|
+
// Consumer and producer in one application
|
|
424
|
+
const client = new ProsodyClient({
|
|
425
|
+
groupId: "my-service",
|
|
426
|
+
sourceSystem: "my-service-producer", // Must differ from groupId to allow loopbacks; defaults to groupId
|
|
427
|
+
subscribedTopics: "my-topic"
|
|
428
|
+
});
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
Or via environment variable:
|
|
432
|
+
|
|
433
|
+
```bash
|
|
434
|
+
PROSODY_SOURCE_SYSTEM=my-service-producer
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
### How It Works
|
|
438
|
+
|
|
439
|
+
1. **Producers** add a `source-system` header to all outgoing messages.
|
|
440
|
+
2. **Consumers** check this header on incoming messages.
|
|
441
|
+
3. If a message's source system matches the consumer's group ID, the message is skipped.
|
|
442
|
+
|
|
443
|
+
This prevents endless loops where a service consumes its own produced messages.
|
|
444
|
+
|
|
445
|
+
### Message Deduplication
|
|
446
|
+
|
|
447
|
+
Prosody automatically deduplicates messages using the `id` field in their JSON payload. Consecutive messages with the
|
|
448
|
+
same ID and key are processed only once.
|
|
449
|
+
|
|
450
|
+
Deduplication uses a two-tier approach:
|
|
451
|
+
|
|
452
|
+
- **Global in-memory cache**: A single cache shared across all partitions within the same consumer instance. Survives
|
|
453
|
+
partition reassignments within the same process. Controlled by `idempotenceCacheSize` (default 8192).
|
|
454
|
+
- **Cassandra-backed persistent store**: Survives restarts and rebalances across instances. TTL controlled by
|
|
455
|
+
`idempotenceTtlS` (default 7 days, i.e. 604800s).
|
|
456
|
+
|
|
457
|
+
Setting `idempotenceCacheSize` to `0` disables the **entire** deduplication middleware, including both the in-memory
|
|
458
|
+
cache and the Cassandra persistent store. No dedup lookups or writes will occur.
|
|
459
|
+
|
|
460
|
+
```javascript
|
|
461
|
+
// Messages with IDs are deduplicated per key
|
|
462
|
+
await client.send("my-topic", "key1", {
|
|
463
|
+
id: "msg-123", // Message will be processed
|
|
464
|
+
content: "Hello!"
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
await client.send("my-topic", "key1", {
|
|
468
|
+
id: "msg-123", // Message will be skipped (duplicate)
|
|
469
|
+
content: "Hello again!"
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
await client.send("my-topic", "key2", {
|
|
473
|
+
id: "msg-123", // Message will be processed (different key)
|
|
474
|
+
content: "Hello!"
|
|
475
|
+
});
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
The entire deduplication middleware (both in-memory cache and Cassandra persistent store) can be disabled by setting `idempotenceCacheSize: 0`:
|
|
479
|
+
|
|
480
|
+
```javascript
|
|
481
|
+
const client = new ProsodyClient({
|
|
482
|
+
groupId: "my-consumer-group",
|
|
483
|
+
subscribedTopics: "my-topic",
|
|
484
|
+
idempotenceCacheSize: 0 // Disable deduplication entirely
|
|
485
|
+
});
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
Or via environment variable:
|
|
489
|
+
|
|
490
|
+
```bash
|
|
491
|
+
PROSODY_IDEMPOTENCE_CACHE_SIZE=0
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
To invalidate all previously recorded dedup entries (forcing reprocessing of messages), change the version:
|
|
495
|
+
|
|
496
|
+
```javascript
|
|
497
|
+
const client = new ProsodyClient({
|
|
498
|
+
groupId: "my-consumer-group",
|
|
499
|
+
subscribedTopics: "my-topic",
|
|
500
|
+
idempotenceVersion: "2" // Changing this invalidates all previously recorded entries
|
|
501
|
+
});
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
Or via environment variable:
|
|
505
|
+
|
|
506
|
+
```bash
|
|
507
|
+
PROSODY_IDEMPOTENCE_VERSION=2
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
## Timer Functionality
|
|
511
|
+
|
|
512
|
+
Prosody supports timer-based delayed execution within message handlers. When a timer fires, your handler's `onTimer` method will be called:
|
|
513
|
+
|
|
514
|
+
```javascript
|
|
515
|
+
const messageHandler = {
|
|
516
|
+
onMessage: async (context, message, signal) => {
|
|
517
|
+
// Schedule a timer to fire in 30 seconds
|
|
518
|
+
const futureTime = new Date(Date.now() + 30000);
|
|
519
|
+
await context.schedule(futureTime);
|
|
520
|
+
|
|
521
|
+
// Schedule multiple timers
|
|
522
|
+
const oneMinute = new Date(Date.now() + 60000);
|
|
523
|
+
const twoMinutes = new Date(Date.now() + 120000);
|
|
524
|
+
await context.schedule(oneMinute);
|
|
525
|
+
await context.schedule(twoMinutes);
|
|
526
|
+
|
|
527
|
+
// Check what's scheduled
|
|
528
|
+
const scheduled = await context.scheduled();
|
|
529
|
+
console.log(`Scheduled timers: ${scheduled.length}`);
|
|
530
|
+
},
|
|
531
|
+
|
|
532
|
+
onTimer: async (context, timer, signal) => {
|
|
533
|
+
console.log('Timer fired!');
|
|
534
|
+
console.log(`Key: ${timer.key}`);
|
|
535
|
+
console.log(`Scheduled time: ${timer.time}`);
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
### Timer Methods
|
|
541
|
+
|
|
542
|
+
The context provides timer scheduling methods that allow you to delay execution or implement timeout behavior:
|
|
543
|
+
|
|
544
|
+
- `schedule(time)`: Schedules a timer to fire at the specified time
|
|
545
|
+
- `clearAndSchedule(time)`: Clears all timers and schedules a new one
|
|
546
|
+
- `unschedule(time)`: Removes a timer scheduled for the specified time
|
|
547
|
+
- `clearScheduled()`: Removes all scheduled timers
|
|
548
|
+
- `scheduled()`: Returns an array of all scheduled timer times
|
|
549
|
+
|
|
550
|
+
### Timer Object
|
|
551
|
+
|
|
552
|
+
When a timer fires, the `onTimer` method receives a timer object with these properties:
|
|
553
|
+
|
|
554
|
+
- `key` (string): The entity key identifying what this timer belongs to
|
|
555
|
+
- `time` (Date): The time when this timer was scheduled to fire
|
|
556
|
+
|
|
557
|
+
**Note**: Timer precision is limited to seconds due to the underlying storage format. Sub-second precision in scheduled times will be rounded to the nearest second.
|
|
558
|
+
|
|
559
|
+
### Timer Configuration
|
|
560
|
+
|
|
561
|
+
Timer functionality requires Cassandra for persistence unless running in mock mode. Configure Cassandra connection via environment variable:
|
|
562
|
+
|
|
563
|
+
```bash
|
|
564
|
+
PROSODY_CASSANDRA_NODES=localhost:9042 # Required for timer persistence
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
Or programmatically when creating the client:
|
|
568
|
+
|
|
569
|
+
```javascript
|
|
570
|
+
const client = new ProsodyClient({
|
|
571
|
+
bootstrapServers: "localhost:9092",
|
|
572
|
+
groupId: "my-application",
|
|
573
|
+
subscribedTopics: "my-topic",
|
|
574
|
+
cassandraNodes: "localhost:9042" // Required unless mock: true
|
|
575
|
+
});
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
For testing, you can use mock mode to avoid Cassandra dependency:
|
|
579
|
+
|
|
580
|
+
```javascript
|
|
581
|
+
// Mock mode for testing (timers work but aren't persisted)
|
|
582
|
+
const client = new ProsodyClient({
|
|
583
|
+
bootstrapServers: "localhost:9092",
|
|
584
|
+
groupId: "my-application",
|
|
585
|
+
subscribedTopics: "my-topic",
|
|
586
|
+
mock: true // No Cassandra required in mock mode
|
|
587
|
+
});
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
### Error Handling
|
|
591
|
+
|
|
592
|
+
Prosody classifies errors as transient (temporary, can be retried) or permanent (won't be resolved by retrying). By
|
|
593
|
+
default, all errors are considered transient.
|
|
594
|
+
|
|
595
|
+
#### Using Decorators
|
|
596
|
+
|
|
597
|
+
If you're using TypeScript or a JavaScript environment that supports decorators, you can use the `@permanent` decorator
|
|
598
|
+
to classify exceptions that should not be retried:
|
|
599
|
+
|
|
600
|
+
```javascript
|
|
601
|
+
import {permanent, ProsodyClient} from '@prosody-events/prosody';
|
|
602
|
+
|
|
603
|
+
class MyHandler {
|
|
604
|
+
@permanent(TypeError, AttributeError)
|
|
605
|
+
async onMessage(context, message, signal) {
|
|
606
|
+
// Your message handling logic here
|
|
607
|
+
// TypeError and AttributeError will be treated as permanent
|
|
608
|
+
// All other exceptions will be treated as transient (default behavior)
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const client = new ProsodyClient(config);
|
|
613
|
+
client.subscribe(new MyHandler());
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
#### Without Decorators
|
|
617
|
+
|
|
618
|
+
If you're not using decorators, you can still classify errors as permanent by throwing a `PermanentError`:
|
|
619
|
+
|
|
620
|
+
```javascript
|
|
621
|
+
import {PermanentError, ProsodyClient} from '@prosody-events/prosody';
|
|
622
|
+
|
|
623
|
+
const messageHandler = {
|
|
624
|
+
onMessage: async (context, message, signal) => {
|
|
625
|
+
try {
|
|
626
|
+
// Your message handling logic here
|
|
627
|
+
} catch (error) {
|
|
628
|
+
if (error instanceof TypeError || error instanceof AttributeError) {
|
|
629
|
+
throw new PermanentError(error.message);
|
|
630
|
+
}
|
|
631
|
+
// All other exceptions will be treated as transient (default behavior)
|
|
632
|
+
throw error;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
const client = new ProsodyClient(config);
|
|
638
|
+
client.subscribe(messageHandler);
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
#### Best Practices for Error Handling
|
|
642
|
+
|
|
643
|
+
- Use permanent errors for issues like malformed data or business logic violations.
|
|
644
|
+
- Use transient errors for temporary issues like network problems.
|
|
645
|
+
- Be cautious with permanent errors as they prevent retries and can result in data loss.
|
|
646
|
+
- Consider system reliability and data consistency when classifying errors.
|
|
647
|
+
|
|
648
|
+
## OpenTelemetry Tracing
|
|
649
|
+
|
|
650
|
+
Prosody supports OpenTelemetry tracing, allowing you to monitor and analyze the performance of your Kafka-based
|
|
651
|
+
applications. The library will emit traces using the OTLP protocol if the `OTEL_EXPORTER_OTLP_ENDPOINT` environment
|
|
652
|
+
variable is defined.
|
|
653
|
+
|
|
654
|
+
Note: Prosody emits its own traces separately because it uses its own tracing runtime, as it would be expensive to send
|
|
655
|
+
all traces to JavaScript.
|
|
656
|
+
|
|
657
|
+
### Required Packages
|
|
658
|
+
|
|
659
|
+
To use OpenTelemetry tracing with Prosody, you need to install the following packages:
|
|
660
|
+
|
|
661
|
+
```
|
|
662
|
+
npm install @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/exporter-trace-otlp-http
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
### Initializing Tracing
|
|
666
|
+
|
|
667
|
+
To initialize tracing in your application:
|
|
668
|
+
|
|
669
|
+
```javascript
|
|
670
|
+
const opentelemetry = require('@opentelemetry/api');
|
|
671
|
+
const {NodeSDK} = require('@opentelemetry/sdk-node');
|
|
672
|
+
const {OTLPTraceExporter} = require('@opentelemetry/exporter-trace-otlp-http');
|
|
673
|
+
|
|
674
|
+
const sdk = new NodeSDK({
|
|
675
|
+
traceExporter: new OTLPTraceExporter(),
|
|
676
|
+
serviceName: 'my-service-name',
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
sdk.start();
|
|
680
|
+
|
|
681
|
+
// Creates a tracer from the global tracer provider
|
|
682
|
+
const tracer = opentelemetry.trace.getTracer('my-service-name');
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
### Setting OpenTelemetry Environment Variables
|
|
686
|
+
|
|
687
|
+
Set the following standard OpenTelemetry environment variables:
|
|
688
|
+
|
|
689
|
+
```
|
|
690
|
+
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
|
|
691
|
+
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
|
|
692
|
+
OTEL_SERVICE_NAME=my-service-name
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
For more information on these and other OpenTelemetry environment variables, refer to
|
|
696
|
+
the [OpenTelemetry specification](https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#general-sdk-configuration).
|
|
697
|
+
|
|
698
|
+
### Using Tracing in Your Application
|
|
699
|
+
|
|
700
|
+
After initializing tracing, you can define spans in your application, and they will be properly propagated through
|
|
701
|
+
Kafka:
|
|
702
|
+
|
|
703
|
+
```javascript
|
|
704
|
+
const {ProsodyClient} = require('@prosody-events/prosody');
|
|
705
|
+
const opentelemetry = require('@opentelemetry/api');
|
|
706
|
+
|
|
707
|
+
const tracer = opentelemetry.trace.getTracer('my-service-name');
|
|
708
|
+
|
|
709
|
+
const client = new ProsodyClient({
|
|
710
|
+
groupId: "my-consumer-group",
|
|
711
|
+
subscribedTopics: "my-topic"
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
const messageHandler = {
|
|
715
|
+
onMessage: async (context, message, signal) => {
|
|
716
|
+
const span = tracer.startSpan('process-message');
|
|
717
|
+
try {
|
|
718
|
+
// Process the received message
|
|
719
|
+
span.addEvent('message.received', {
|
|
720
|
+
'message.payload': JSON.stringify(message)
|
|
721
|
+
});
|
|
722
|
+
} finally {
|
|
723
|
+
span.end();
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
client.subscribe(messageHandler);
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
### Span Linking
|
|
732
|
+
|
|
733
|
+
By default, message execution spans use **`child`** (child-of relationship — the execution span is part of
|
|
734
|
+
the same trace as the producer). Timer execution spans use **`follows_from`** (the execution span starts a
|
|
735
|
+
new trace with a span link back to the scheduling span, since timer execution is causally related but not part of
|
|
736
|
+
the same operation).
|
|
737
|
+
|
|
738
|
+
Both strategies are configurable via the `messageSpans` / `PROSODY_MESSAGE_SPANS` and `timerSpans` /
|
|
739
|
+
`PROSODY_TIMER_SPANS` options. Accepted values: `'child'`, `'follows_from'`.
|
|
740
|
+
|
|
741
|
+
## Best Practices
|
|
742
|
+
|
|
743
|
+
### Ensuring Idempotent Message Handlers
|
|
744
|
+
|
|
745
|
+
Idempotent message handlers are crucial for maintaining data consistency, fault tolerance, and scalability when working
|
|
746
|
+
with distributed, event-based systems. They ensure that processing a message multiple times has the same effect as
|
|
747
|
+
processing it once, which is essential for recovering from failures.
|
|
748
|
+
|
|
749
|
+
Strategies for achieving idempotence:
|
|
750
|
+
|
|
751
|
+
1. **Natural Idempotence**: Use inherently idempotent operations (e.g., setting a value in a key-value store).
|
|
752
|
+
|
|
753
|
+
2. **Deduplication with Unique Identifiers**:
|
|
754
|
+
|
|
755
|
+
- Kafka messages can be uniquely identified by their partition and offset.
|
|
756
|
+
- Before processing, check if the message has been handled before.
|
|
757
|
+
- Store processed message identifiers with an appropriate TTL.
|
|
758
|
+
|
|
759
|
+
3. **Database Upserts**: Use upsert operations for database writes.
|
|
760
|
+
|
|
761
|
+
4. **Partition Offset Tracking**:
|
|
762
|
+
|
|
763
|
+
- Store the latest processed offset for each partition.
|
|
764
|
+
- Only process messages with higher offsets than the last processed one.
|
|
765
|
+
- Critically, store these offsets transactionally with other state updates to ensure consistency.
|
|
766
|
+
|
|
767
|
+
5. **Idempotency Keys for External APIs**: Utilize idempotency keys when supported by external APIs.
|
|
768
|
+
|
|
769
|
+
6. **Check-then-Act Pattern**:
|
|
770
|
+
|
|
771
|
+
- For non-idempotent external systems, verify if an operation was previously completed before execution.
|
|
772
|
+
- Maintain a record of completed operations, keyed by a unique message identifier.
|
|
773
|
+
|
|
774
|
+
7. **Saga Pattern**:
|
|
775
|
+
|
|
776
|
+
- Implement a state machine in your database for multi-step operations.
|
|
777
|
+
- Each message advances the state machine, allowing for idempotent processing and easy failure recovery.
|
|
778
|
+
- Particularly useful for complex, distributed transactions across multiple services.
|
|
779
|
+
|
|
780
|
+
### Proper Shutdown
|
|
781
|
+
|
|
782
|
+
Always unsubscribe from topics before exiting your application:
|
|
783
|
+
|
|
784
|
+
```javascript
|
|
785
|
+
// Ensure proper shutdown
|
|
786
|
+
await client.unsubscribe();
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
This ensures:
|
|
790
|
+
|
|
791
|
+
1. Completion and commitment of all in-flight work
|
|
792
|
+
2. Quick rebalancing, allowing other consumers to take over partitions
|
|
793
|
+
3. Proper release of resources
|
|
794
|
+
|
|
795
|
+
Implement shutdown handling in your application:
|
|
796
|
+
|
|
797
|
+
```javascript
|
|
798
|
+
const {ProsodyClient} = require('@prosody-events/prosody');
|
|
799
|
+
|
|
800
|
+
async function main() {
|
|
801
|
+
const client = new ProsodyClient({
|
|
802
|
+
groupId: "my-consumer-group",
|
|
803
|
+
subscribedTopics: "my-topic"
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
const messageHandler = {
|
|
807
|
+
onMessage: async (context, message, signal) => {
|
|
808
|
+
// Process the message
|
|
809
|
+
}
|
|
810
|
+
};
|
|
811
|
+
|
|
812
|
+
client.subscribe(messageHandler);
|
|
813
|
+
|
|
814
|
+
// Create a promise that resolves when shutdown is signaled
|
|
815
|
+
const shutdownPromise = new Promise((resolve) => {
|
|
816
|
+
const shutdown = async (signal) => {
|
|
817
|
+
console.log(`Received ${signal}. Initiating shutdown...`);
|
|
818
|
+
await client.unsubscribe();
|
|
819
|
+
resolve();
|
|
820
|
+
};
|
|
821
|
+
|
|
822
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
823
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
824
|
+
process.on('SIGHUP', () => shutdown('SIGHUP'));
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
// Wait for shutdown to be signaled
|
|
828
|
+
await shutdownPromise;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
main().catch(console.error);
|
|
832
|
+
```
|
|
833
|
+
|
|
834
|
+
### Handling Task Cancellation
|
|
835
|
+
|
|
836
|
+
Prosody cancels tasks during partition rebalancing or shutdown. How you handle cancellation is critical:
|
|
837
|
+
|
|
838
|
+
- Prosody interprets task success based on exception propagation.
|
|
839
|
+
- A task that exits without an exception is considered successful.
|
|
840
|
+
- Any exception signals task failure.
|
|
841
|
+
|
|
842
|
+
The library uses AbortSignals in both the `send` method and `onMessage` handler. It's crucial to pass this abort signal
|
|
843
|
+
to any I/O operations, fetch calls, or database queries to ensure prompt task cancellation.
|
|
844
|
+
|
|
845
|
+
Best practices:
|
|
846
|
+
|
|
847
|
+
1. Exit promptly when cancelled to avoid rebalancing delays.
|
|
848
|
+
2. Use try/catch blocks to handle cancellation gracefully.
|
|
849
|
+
3. Use try/finally or equivalent constructs for clean resource handling.
|
|
850
|
+
4. Pass the AbortSignal to all async operations that support it.
|
|
851
|
+
|
|
852
|
+
Example of using AbortSignal in message processing:
|
|
853
|
+
|
|
854
|
+
```javascript
|
|
855
|
+
const messageHandler = {
|
|
856
|
+
onMessage: async (context, message, signal) => {
|
|
857
|
+
// Pass the signal to fetch calls
|
|
858
|
+
const response = await fetch('https://api.example.com', {signal});
|
|
859
|
+
const data = await response.json();
|
|
860
|
+
|
|
861
|
+
// Pass the signal to database operations
|
|
862
|
+
await db.query('INSERT INTO messages (payload) VALUES ($1)', [message.payload], {signal});
|
|
863
|
+
|
|
864
|
+
// Process the data...
|
|
865
|
+
|
|
866
|
+
// Send a message, passing the abort signal
|
|
867
|
+
await client.send('topic', 'key', {data: 'value'}, signal);
|
|
868
|
+
}
|
|
869
|
+
};
|
|
870
|
+
```
|
|
871
|
+
|
|
872
|
+
For the `send` method, note that while an abort signal will cause the method to return early, it may not cancel the
|
|
873
|
+
message being sent, depending on when the abort is signaled. If the abort occurs after the message has been handed off
|
|
874
|
+
to the Kafka client, the message may still be sent.
|
|
875
|
+
|
|
876
|
+
Failing to follow these practices can lead to:
|
|
877
|
+
|
|
878
|
+
- Slower message processing due to delayed rebalancing.
|
|
879
|
+
- Data loss from missed messages when cancellation errors are suppressed.
|
|
880
|
+
- Resource leaks if long-running operations aren't properly cancelled.
|
|
881
|
+
|
|
882
|
+
## Release Process
|
|
883
|
+
|
|
884
|
+
Prosody uses an automated release process managed by GitHub Actions. Here's an overview of how releases are handled:
|
|
885
|
+
|
|
886
|
+
1. **Trigger**: The release process is triggered automatically on pushes to the `main` branch.
|
|
887
|
+
|
|
888
|
+
2. **Release Please**: The process starts with the "Release Please" action, which:
|
|
889
|
+
- Analyzes commit messages since the last release.
|
|
890
|
+
- Creates or updates a release pull request with changelog updates and version bumps.
|
|
891
|
+
- When the PR is merged, it creates a GitHub release and a git tag.
|
|
892
|
+
|
|
893
|
+
3. **Build Process**: If a new release is created, the following build jobs are triggered:
|
|
894
|
+
- Linux builds for x86_64 and aarch64 (glibc).
|
|
895
|
+
- Windows build for x64.
|
|
896
|
+
- macOS build for aarch64 (Apple Silicon).
|
|
897
|
+
|
|
898
|
+
4. **Testing**: The built binaries are tested on Linux (x86_64 and aarch64) with Node.js 20 and 22.
|
|
899
|
+
|
|
900
|
+
5. **Artifact Upload**: Each build job uploads its artifacts (Node.js native addons) to GitHub Actions.
|
|
901
|
+
|
|
902
|
+
6. **Publication**: If all builds and tests are successful, the final step publishes the package to the npm registry.
|
|
903
|
+
|
|
904
|
+
### Contributing to Releases
|
|
905
|
+
|
|
906
|
+
To contribute to a release:
|
|
907
|
+
|
|
908
|
+
1. Make your changes in a feature branch.
|
|
909
|
+
2. Use [Conventional Commits](https://www.conventionalcommits.org/) syntax for your commit messages. This helps Release
|
|
910
|
+
Please determine the next version number and generate the changelog.
|
|
911
|
+
3. Create a pull request to merge your changes into the `main` branch.
|
|
912
|
+
4. Once your PR is approved and merged, Release Please will include your changes in the next release PR.
|
|
913
|
+
|
|
914
|
+
### Manual Releases
|
|
915
|
+
|
|
916
|
+
While the process is automated, manual intervention may sometimes be necessary:
|
|
917
|
+
|
|
918
|
+
- You can manually trigger the release workflow from the GitHub Actions tab if needed.
|
|
919
|
+
- If you need to make changes to the release PR created by Release Please, you can do so before merging it.
|
|
920
|
+
|
|
921
|
+
Remember, all releases are automatically published to the npm registry. Ensure you have thoroughly tested
|
|
922
|
+
your changes before merging to `main`.
|
|
923
|
+
|
|
924
|
+
## API Reference
|
|
925
|
+
|
|
926
|
+
### ProsodyClient
|
|
927
|
+
|
|
928
|
+
- `constructor(config: Configuration)`: Initialize a new ProsodyClient with the given configuration.
|
|
929
|
+
- `send(topic: string, key: string, payload: any, signal?: AbortSignal): Promise<void>`: Send a message to a specified
|
|
930
|
+
topic.
|
|
931
|
+
- `consumerState: ConsumerState`: Get the current state of the consumer.
|
|
932
|
+
- `sourceSystem: string`: Get the source system identifier configured for the client.
|
|
933
|
+
- `subscribe(eventHandler: EventHandler): void`: Subscribe to messages using the provided handler.
|
|
934
|
+
- `unsubscribe(): Promise<void>`: Unsubscribe from messages and shut down the consumer.
|
|
935
|
+
|
|
936
|
+
### EventHandler
|
|
937
|
+
|
|
938
|
+
Interface for handling messages and timers:
|
|
939
|
+
|
|
940
|
+
- `onMessage?: (context: Context, message: Message, signal: AbortSignal) => Promise<void>`: Handles incoming messages
|
|
941
|
+
- `onTimer?: (context: Context, timer: Timer, signal: AbortSignal) => Promise<void>`: Handles timer events
|
|
942
|
+
|
|
943
|
+
### Message
|
|
944
|
+
|
|
945
|
+
Represents a Kafka message with the following properties:
|
|
946
|
+
|
|
947
|
+
- `topic: string`: The name of the topic.
|
|
948
|
+
- `partition: number`: The partition number.
|
|
949
|
+
- `offset: number`: The message offset within the partition.
|
|
950
|
+
- `timestamp: Date`: The timestamp when the message was created or sent.
|
|
951
|
+
- `key: string`: The message key.
|
|
952
|
+
- `payload: any`: The message payload as a JSON-serializable value.
|
|
953
|
+
|
|
954
|
+
### Context
|
|
955
|
+
|
|
956
|
+
Represents the context of message processing:
|
|
957
|
+
|
|
958
|
+
- `onCancel(): Promise<void>`: A method that resolves when the context is cancelled.
|
|
959
|
+
- `shouldCancel: boolean`: A property indicating whether the context has been cancelled.
|
|
960
|
+
|
|
961
|
+
Timer scheduling methods:
|
|
962
|
+
|
|
963
|
+
- `schedule(time: Date): Promise<void>`: Schedules a timer to fire at the specified time
|
|
964
|
+
- `clearAndSchedule(time: Date): Promise<void>`: Clears all timers and schedules a new one
|
|
965
|
+
- `unschedule(time: Date): Promise<void>`: Removes a timer scheduled for the specified time
|
|
966
|
+
- `clearScheduled(): Promise<void>`: Removes all scheduled timers
|
|
967
|
+
- `scheduled(): Promise<Date[]>`: Returns an array of all scheduled timer times
|
|
968
|
+
|
|
969
|
+
### Timer
|
|
970
|
+
|
|
971
|
+
Represents a timer that has fired, provided to the `onTimer` method:
|
|
972
|
+
|
|
973
|
+
- `key: string`: The entity key identifying what this timer belongs to
|
|
974
|
+
- `time: Date`: The time when this timer was scheduled to fire
|
|
975
|
+
|
|
976
|
+
## License
|
|
977
|
+
|
|
978
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|