prosody 0.3.0 → 0.5.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.
- checksums.yaml +4 -4
- data/.cargo/config.toml +3 -0
- data/.release-please-manifest.json +1 -1
- data/AGENTS.md +395 -0
- data/ARCHITECTURE.md +14 -4
- data/CHANGELOG.md +28 -0
- data/CLAUDE.md +1 -0
- data/CONFIGURATION.md +167 -0
- data/Cargo.lock +1115 -645
- data/Cargo.toml +7 -6
- data/README.md +436 -146
- data/Rakefile +11 -1
- data/examples/keyed_state.rb +70 -0
- data/examples/keyed_state.rbs +18 -0
- data/examples/keyed_state_windowing.rb +55 -0
- data/examples/keyed_state_windowing.rbs +16 -0
- data/ext/prosody/Cargo.toml +1 -0
- data/ext/prosody/src/admin.rs +1 -5
- data/ext/prosody/src/bridge/mod.rs +17 -32
- data/ext/prosody/src/client/config.rs +501 -28
- data/ext/prosody/src/client/mod.rs +167 -74
- data/ext/prosody/src/client/request.rs +132 -0
- data/ext/prosody/src/client/support.rs +122 -0
- data/ext/prosody/src/handler/context.rs +150 -5
- data/ext/prosody/src/handler/message.rs +67 -0
- data/ext/prosody/src/handler/mod.rs +115 -85
- data/ext/prosody/src/handler/state/mod.rs +488 -0
- data/ext/prosody/src/handler/state/registration.rs +104 -0
- data/ext/prosody/src/handler/state/scan.rs +218 -0
- data/ext/prosody/src/lib.rs +15 -3
- data/ext/prosody/src/published.rs +273 -0
- data/ext/prosody/src/scheduler/mod.rs +2 -2
- data/ext/prosody/src/scheduler/processor.rs +2 -2
- data/ext/prosody/src/scheduler/result.rs +7 -4
- data/ext/prosody/src/util.rs +86 -5
- data/lib/prosody/configuration.rb +71 -11
- data/lib/prosody/handler.rb +65 -8
- data/lib/prosody/native_stubs.rb +550 -9
- data/lib/prosody/request.rb +45 -0
- data/lib/prosody/state.rb +816 -0
- data/lib/prosody/version.rb +1 -1
- data/lib/prosody.rb +6 -0
- data/release-please-config.json +4 -0
- data/sig/configuration.rbs +70 -11
- data/sig/handler.rbs +17 -5
- data/sig/processor.rbs +28 -12
- data/sig/prosody.rbs +53 -7
- data/sig/request.rbs +66 -0
- data/sig/sentry.rbs +6 -0
- data/sig/state.rbs +390 -0
- data/steep_expectations.yml +57 -0
- data/typecheck/payload_types.rb +54 -0
- data/typecheck/payload_types.rbs +22 -0
- data/typecheck_negative/payload_types.rb +20 -0
- data/typecheck_negative/payload_types.rbs +9 -0
- metadata +32 -9
data/README.md
CHANGED
|
@@ -9,6 +9,7 @@ strategies, and integrated OpenTelemetry support for distributed tracing.
|
|
|
9
9
|
- **Kafka Consumer**: Per-key ordering with cross-key concurrency, offset management, consumer groups
|
|
10
10
|
- **Kafka Producer**: Idempotent delivery with configurable retries
|
|
11
11
|
- **Timer System**: Persistent scheduled execution backed by Cassandra or in-memory store
|
|
12
|
+
- **Keyed State**: Per-key value/map/deque collections that survive across events, transactional by default
|
|
12
13
|
- **Quality of Service**: Fair scheduling limits concurrency and prevents failures from starving fresh traffic. Pipeline mode adds deferred retry and monopolization detection
|
|
13
14
|
- **Distributed Tracing**: OpenTelemetry integration for tracing message flow across services
|
|
14
15
|
- **Backpressure**: Pauses partitions when handlers fall behind
|
|
@@ -29,6 +30,12 @@ Or install directly:
|
|
|
29
30
|
gem install prosody
|
|
30
31
|
```
|
|
31
32
|
|
|
33
|
+
The gem ships RBS signatures for the public API. `Prosody::EventHandler[Payload, Response]`
|
|
34
|
+
carries the payload type into `Prosody::Message[Payload]`. It also checks each handler response.
|
|
35
|
+
State definitions carry their item types through `context.state`. A bare handler,
|
|
36
|
+
message, definition, or state handle uses `Prosody::json_value`. See the
|
|
37
|
+
[typed examples](examples/) for Ruby and companion RBS files checked by Steep.
|
|
38
|
+
|
|
32
39
|
## Quick Start
|
|
33
40
|
|
|
34
41
|
```ruby
|
|
@@ -52,6 +59,12 @@ client = Prosody::Client.new(
|
|
|
52
59
|
|
|
53
60
|
# Define a custom message handler
|
|
54
61
|
class MyHandler < Prosody::EventHandler
|
|
62
|
+
def on_excise(context, message)
|
|
63
|
+
puts "Excise key: #{message.key}"
|
|
64
|
+
context.clear_scheduled
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
|
|
55
68
|
def on_message(context, message)
|
|
56
69
|
# Process the received message
|
|
57
70
|
puts "Received message: #{message.payload.inspect}"
|
|
@@ -73,12 +86,23 @@ end
|
|
|
73
86
|
client.subscribe(MyHandler.new)
|
|
74
87
|
|
|
75
88
|
# Send a message to a topic
|
|
76
|
-
client.send_message("my-topic", "message-key", {
|
|
89
|
+
client.send_message("my-topic", "message-key", {"content" => "Hello, Kafka!"})
|
|
90
|
+
client.excise("my-topic", "obsolete-key")
|
|
77
91
|
|
|
78
92
|
# Ensure proper shutdown when done
|
|
79
|
-
client.
|
|
93
|
+
client.shutdown
|
|
80
94
|
```
|
|
81
95
|
|
|
96
|
+
## Excise records
|
|
97
|
+
|
|
98
|
+
Applications can copy event data into keyed state and external stores. A regulatory or contractual deletion must remove every copy for one key.
|
|
99
|
+
|
|
100
|
+
An excise record carries this deletion command. Kafka encodes the command as a key with no payload. During topic compaction, Kafka deletes earlier values for the key. Call `excise(topic, key)` to send the record. Prosody routes the record to `on_excise`. The handler must delete all consumer-owned data for the key.
|
|
101
|
+
|
|
102
|
+
Each handler must implement `on_message`, `on_excise`, and `on_timer`. Subscription fails before consumption if a method is missing.
|
|
103
|
+
|
|
104
|
+
If an excise record is a request, return a response from `on_excise`. Prosody uses this response as the subsystem result.
|
|
105
|
+
|
|
82
106
|
## Architecture
|
|
83
107
|
|
|
84
108
|
Prosody enables efficient, parallel processing of Kafka messages while maintaining order for messages with the same key:
|
|
@@ -166,109 +190,9 @@ timeout defaults to 80% of `stall_threshold`.
|
|
|
166
190
|
|
|
167
191
|
## Configuration
|
|
168
192
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
### Core
|
|
172
|
-
|
|
173
|
-
| Option / Environment Variable | Description | Default |
|
|
174
|
-
|-----------------------------------------|---------------------------------------------------|--------------|
|
|
175
|
-
| `bootstrap_servers` / `PROSODY_BOOTSTRAP_SERVERS` | Kafka servers to connect to | - |
|
|
176
|
-
| `group_id` / `PROSODY_GROUP_ID` | Consumer group name | - |
|
|
177
|
-
| `subscribed_topics` / `PROSODY_SUBSCRIBED_TOPICS` | Topics to read from | - |
|
|
178
|
-
| `allowed_events` / `PROSODY_ALLOWED_EVENTS` | Only process events matching these prefixes | (all) |
|
|
179
|
-
| `source_system` / `PROSODY_SOURCE_SYSTEM` | Tag for outgoing messages (prevents reprocessing)| `<group_id>` |
|
|
180
|
-
| `mock` / `PROSODY_MOCK` | Use in-memory Kafka for testing | false |
|
|
181
|
-
|
|
182
|
-
### Consumer
|
|
183
|
-
|
|
184
|
-
| Option / Environment Variable | Description | Default |
|
|
185
|
-
|-----------------------------------------|------------------------------------------------------|------------------------|
|
|
186
|
-
| `max_concurrency` / `PROSODY_MAX_CONCURRENCY` | Max messages being processed simultaneously | 32 |
|
|
187
|
-
| `max_uncommitted` / `PROSODY_MAX_UNCOMMITTED` | Max queued messages before pausing consumption | 64 |
|
|
188
|
-
| `timeout` / `PROSODY_TIMEOUT` | Cancel handler if it runs longer than this | 80% of stall threshold |
|
|
189
|
-
| `commit_interval` / `PROSODY_COMMIT_INTERVAL` | How often to save progress to Kafka | 1s |
|
|
190
|
-
| `poll_interval` / `PROSODY_POLL_INTERVAL` | How often to fetch new messages from Kafka | 100ms |
|
|
191
|
-
| `shutdown_timeout` / `PROSODY_SHUTDOWN_TIMEOUT` | Shutdown budget; handlers run freely until cancellation fires near the end of the timeout | 30s |
|
|
192
|
-
| `stall_threshold` / `PROSODY_STALL_THRESHOLD` | Report unhealthy if no progress for this long | 5m |
|
|
193
|
-
| `probe_port` / `PROSODY_PROBE_PORT` | HTTP port for health checks (nil to disable) | 8000 |
|
|
194
|
-
| `failure_topic` / `PROSODY_FAILURE_TOPIC` | Send unprocessable messages here (dead letter queue) | - |
|
|
195
|
-
| `idempotence_cache_size` / `PROSODY_IDEMPOTENCE_CACHE_SIZE` | Global shared cache capacity across all partitions for message deduplication (0 disables the entire deduplication middleware, both in-memory and persistent) | 8192 |
|
|
196
|
-
| `idempotence_version` / `PROSODY_IDEMPOTENCE_VERSION` | Version string for cache-busting dedup hashes | 1 |
|
|
197
|
-
| `idempotence_ttl` / `PROSODY_IDEMPOTENCE_TTL` | TTL for dedup records in Cassandra | 7d (604800 seconds) |
|
|
198
|
-
| `slab_size` / `PROSODY_SLAB_SIZE` | Timer storage granularity (rarely needs changing) | 1h |
|
|
199
|
-
| `message_spans` / `PROSODY_MESSAGE_SPANS` | Span linking for message execution: `child` (child-of) or `follows_from` | `child` |
|
|
200
|
-
| `timer_spans` / `PROSODY_TIMER_SPANS` | Span linking for timer execution: `child` (child-of) or `follows_from` | `follows_from` |
|
|
201
|
-
|
|
202
|
-
### Producer
|
|
203
|
-
|
|
204
|
-
| Option / Environment Variable | Description | Default |
|
|
205
|
-
|-----------------------------------------|---------------------------------|---------|
|
|
206
|
-
| `send_timeout` / `PROSODY_SEND_TIMEOUT` | Give up sending after this long | 1s |
|
|
207
|
-
|
|
208
|
-
### Retry
|
|
209
|
-
|
|
210
|
-
When a handler fails, retry with exponential backoff:
|
|
211
|
-
|
|
212
|
-
| Option / Environment Variable | Description | Default |
|
|
213
|
-
|-----------------------------------------|-----------------------------------|---------|
|
|
214
|
-
| `max_retries` / `PROSODY_MAX_RETRIES` | Give up after this many attempts | 3 |
|
|
215
|
-
| `retry_base` / `PROSODY_RETRY_BASE` | Wait this long before first retry | 20ms |
|
|
216
|
-
| `max_retry_delay` / `PROSODY_RETRY_MAX_DELAY` | Never wait longer than this | 5m |
|
|
217
|
-
|
|
218
|
-
### Deferral (Pipeline Mode)
|
|
219
|
-
|
|
220
|
-
| Option / Environment Variable | Description | Default |
|
|
221
|
-
|-----------------------------------------|---------------------------------------------------|---------|
|
|
222
|
-
| `defer_enabled` / `PROSODY_DEFER_ENABLED` | Enable deferral for new messages | true |
|
|
223
|
-
| `defer_base` / `PROSODY_DEFER_BASE` | Wait this long before first deferred retry | 1s |
|
|
224
|
-
| `defer_max_delay` / `PROSODY_DEFER_MAX_DELAY` | Never wait longer than this | 24h |
|
|
225
|
-
| `defer_failure_threshold` / `PROSODY_DEFER_FAILURE_THRESHOLD` | Disable deferral when failure rate exceeds this | 0.9 |
|
|
226
|
-
| `defer_failure_window` / `PROSODY_DEFER_FAILURE_WINDOW` | Measure failure rate over this time window | 5m |
|
|
227
|
-
| `defer_cache_size` / `PROSODY_DEFER_CACHE_SIZE` | Track this many deferred keys in memory | 1024 |
|
|
228
|
-
| `defer_store_cache_size` / `PROSODY_DEFER_STORE_CACHE_SIZE` | Maximum deferred store cache entries per Cassandra defer store | 8192 |
|
|
229
|
-
| `defer_seek_timeout` / `PROSODY_DEFER_SEEK_TIMEOUT` | Timeout when loading deferred messages | 30s |
|
|
230
|
-
| `defer_discard_threshold` / `PROSODY_DEFER_DISCARD_THRESHOLD` | Read optimization (rarely needs changing) | 100 |
|
|
231
|
-
|
|
232
|
-
### Monopolization Detection (Pipeline Mode)
|
|
233
|
-
|
|
234
|
-
| Option / Environment Variable | Description | Default |
|
|
235
|
-
|-----------------------------------------|-----------------------------------------|---------|
|
|
236
|
-
| `monopolization_enabled` / `PROSODY_MONOPOLIZATION_ENABLED` | Enable hot key protection | true |
|
|
237
|
-
| `monopolization_threshold` / `PROSODY_MONOPOLIZATION_THRESHOLD` | Max handler time as fraction of window | 0.9 |
|
|
238
|
-
| `monopolization_window` / `PROSODY_MONOPOLIZATION_WINDOW` | Measurement window | 5m |
|
|
239
|
-
| `monopolization_cache_size` / `PROSODY_MONOPOLIZATION_CACHE_SIZE` | Max distinct keys to track | 8192 |
|
|
193
|
+
For the complete configuration reference, see [CONFIGURATION.md](CONFIGURATION.md).
|
|
240
194
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
| Option / Environment Variable | Description | Default |
|
|
244
|
-
|-----------------------------------------|------------------------------------------------------------------|---------|
|
|
245
|
-
| `scheduler_failure_weight` / `PROSODY_SCHEDULER_FAILURE_WEIGHT` | Fraction of processing time reserved for retries | 0.3 |
|
|
246
|
-
| `scheduler_max_wait` / `PROSODY_SCHEDULER_MAX_WAIT` | Messages waiting this long get maximum priority | 2m |
|
|
247
|
-
| `scheduler_wait_weight` / `PROSODY_SCHEDULER_WAIT_WEIGHT` | Priority boost for waiting messages (higher = more aggressive) | 200.0 |
|
|
248
|
-
| `scheduler_cache_size` / `PROSODY_SCHEDULER_CACHE_SIZE` | Max distinct keys to track | 8192 |
|
|
249
|
-
|
|
250
|
-
### Cassandra
|
|
251
|
-
|
|
252
|
-
Persistent storage for timers and deferred retries (not needed if `mock: true`):
|
|
253
|
-
|
|
254
|
-
| Option / Environment Variable | Description | Default |
|
|
255
|
-
|-----------------------------------------|------------------------------------|---------|
|
|
256
|
-
| `cassandra_nodes` / `PROSODY_CASSANDRA_NODES` | Servers to connect to (host:port) | - |
|
|
257
|
-
| `cassandra_keyspace` / `PROSODY_CASSANDRA_KEYSPACE` | Keyspace name | prosody |
|
|
258
|
-
| `cassandra_user` / `PROSODY_CASSANDRA_USER` | Username | - |
|
|
259
|
-
| `cassandra_password` / `PROSODY_CASSANDRA_PASSWORD` | Password | - |
|
|
260
|
-
| `cassandra_datacenter` / `PROSODY_CASSANDRA_DATACENTER` | Prefer this datacenter for queries | - |
|
|
261
|
-
| `cassandra_rack` / `PROSODY_CASSANDRA_RACK` | Prefer this rack for queries | - |
|
|
262
|
-
| `cassandra_retention` / `PROSODY_CASSANDRA_RETENTION` | Delete data older than this | 1y |
|
|
263
|
-
|
|
264
|
-
### Telemetry Emitter
|
|
265
|
-
|
|
266
|
-
Prosody can emit internal processing events (message lifecycle, timer events) to a Kafka topic for observability:
|
|
267
|
-
|
|
268
|
-
| Option / Environment Variable | Description | Default |
|
|
269
|
-
|-----------------------------------------|------------------------------------------------|----------------------------|
|
|
270
|
-
| `telemetry_topic` / `PROSODY_TELEMETRY_TOPIC` | Kafka topic to produce telemetry events to | `prosody.telemetry-events` |
|
|
271
|
-
| `telemetry_enabled` / `PROSODY_TELEMETRY_ENABLED` | Enable or disable the telemetry emitter | true |
|
|
195
|
+
Constructor options take precedence. Unset options use environment variables, then library defaults.
|
|
272
196
|
|
|
273
197
|
## Logging
|
|
274
198
|
|
|
@@ -361,6 +285,73 @@ if client.is_stalled?
|
|
|
361
285
|
end
|
|
362
286
|
```
|
|
363
287
|
|
|
288
|
+
## Subsystems
|
|
289
|
+
|
|
290
|
+
A consumer group ID identifies a set of processes that share records and the keyed state that the group owns. A subsystem can include one or more services and consumer groups. If callers use these IDs, a refactor can require changes to each caller.
|
|
291
|
+
|
|
292
|
+
A subsystem gives requests and published state one stable public name. Callers use this name instead of consumer group IDs. You can change its services and consumer groups without changing callers. Prosody uses the first response to a subsystem request. For each published-state read, it uses one consumer group that publishes the collection.
|
|
293
|
+
|
|
294
|
+
## Requests
|
|
295
|
+
|
|
296
|
+
Kafka decouples producers from consumers, so a send does not return consumer results. This asynchronous model lets each service process records independently. Some operations must wait for consumer results before they continue. A request recovers synchrony for the caller while consumers continue asynchronous processing.
|
|
297
|
+
|
|
298
|
+
Send a request from a handler or other application code. The Prosody client does not need an active subscription. The result hash uses canonical subsystem names as keys. Each value is a `Success` or `Failure` outcome. Use `request_excise` to send an excise record and collect the same outcome type.
|
|
299
|
+
|
|
300
|
+
Do not rely on hash order. The hash contains one entry for each selected subsystem. A missing response becomes a timeout `Failure`; Prosody does not omit the subsystem. The request raises an error for request-level failures, such as invalid input, a Kafka send failure, or shutdown. Do not wait for a request if the current consumer group must process it for the same key. That group cannot process it until the handler returns.
|
|
301
|
+
|
|
302
|
+
Message and excise handler return values become successful outcomes. Each return value must have a JSON representation.
|
|
303
|
+
|
|
304
|
+
Set `subsystem` to `inventory` on the client that subscribes this handler.
|
|
305
|
+
|
|
306
|
+
```ruby
|
|
307
|
+
class InventoryHandler < Prosody::EventHandler
|
|
308
|
+
def on_message(_context, message)
|
|
309
|
+
{"accepted" => message.key}
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def on_excise(_context, message)
|
|
313
|
+
{"excised" => message.key}
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def on_timer(_context, _timer)
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
Send the request:
|
|
322
|
+
|
|
323
|
+
Set `timeout` in seconds.
|
|
324
|
+
|
|
325
|
+
```ruby
|
|
326
|
+
subsystems = ["inventory", "billing"]
|
|
327
|
+
results = client.request(
|
|
328
|
+
topic: "orders",
|
|
329
|
+
key: "order-1",
|
|
330
|
+
payload: {"type" => "order.created"},
|
|
331
|
+
subsystems: subsystems,
|
|
332
|
+
timeout: 2.0
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
results.each do |subsystem, outcome|
|
|
336
|
+
if outcome.is_a?(Prosody::Failure)
|
|
337
|
+
warn "#{subsystem}: #{outcome.error.message}"
|
|
338
|
+
else
|
|
339
|
+
puts "#{subsystem}: #{outcome.value}"
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
The example can print these results:
|
|
345
|
+
|
|
346
|
+
```text
|
|
347
|
+
inventory: {"accepted"=>"order-1"}
|
|
348
|
+
billing: no response arrived before the deadline
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Each failure contains one typed response error.
|
|
352
|
+
|
|
353
|
+
Each response error has one message.
|
|
354
|
+
|
|
364
355
|
## Advanced Usage
|
|
365
356
|
|
|
366
357
|
### Pipeline Mode
|
|
@@ -476,36 +467,37 @@ The deduplication system uses:
|
|
|
476
467
|
```ruby
|
|
477
468
|
# Messages with IDs are deduplicated per key
|
|
478
469
|
client.send_message("my-topic", "key1", {
|
|
479
|
-
id
|
|
480
|
-
content
|
|
470
|
+
"id" => "msg-123", # Message will be processed
|
|
471
|
+
"content" => "Hello!"
|
|
481
472
|
})
|
|
482
473
|
|
|
483
474
|
client.send_message("my-topic", "key1", {
|
|
484
|
-
id
|
|
485
|
-
content
|
|
475
|
+
"id" => "msg-123", # Message will be skipped (duplicate)
|
|
476
|
+
"content" => "Hello again!"
|
|
486
477
|
})
|
|
487
478
|
|
|
488
479
|
client.send_message("my-topic", "key2", {
|
|
489
|
-
id
|
|
490
|
-
content
|
|
480
|
+
"id" => "msg-123", # Message will be processed (different key)
|
|
481
|
+
"content" => "Hello!"
|
|
491
482
|
})
|
|
492
483
|
```
|
|
493
484
|
|
|
494
|
-
|
|
485
|
+
Consumer deduplication is **mandatory** — it is the commit oracle that makes
|
|
486
|
+
keyed state correct — so it cannot be disabled. `idempotence_cache_size` must be
|
|
487
|
+
at least 1; setting it to `0` in the client configuration raises an
|
|
488
|
+
`ArgumentError`:
|
|
495
489
|
|
|
496
490
|
```ruby
|
|
497
491
|
client = Prosody::Client.new(
|
|
498
492
|
group_id: "my-consumer-group",
|
|
499
493
|
subscribed_topics: "my-topic",
|
|
500
|
-
idempotence_cache_size: 0 #
|
|
494
|
+
idempotence_cache_size: 0 # Rejected: consumer deduplication cannot be disabled
|
|
501
495
|
)
|
|
502
496
|
```
|
|
503
497
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
PROSODY_IDEMPOTENCE_CACHE_SIZE=0
|
|
508
|
-
```
|
|
498
|
+
This applies to every client — `Prosody::Client.new` always builds a consumer, so
|
|
499
|
+
`0` is rejected regardless of whether any topics are subscribed, and whether it is
|
|
500
|
+
supplied in the client configuration or via `PROSODY_IDEMPOTENCE_CACHE_SIZE`.
|
|
509
501
|
|
|
510
502
|
To invalidate all previously recorded dedup entries (e.g. after a data migration), change the version string:
|
|
511
503
|
|
|
@@ -529,6 +521,169 @@ client = Prosody::Client.new(
|
|
|
529
521
|
|
|
530
522
|
Note that the in-memory cache is best-effort. Duplicates can still occur across different process instances.
|
|
531
523
|
|
|
524
|
+
## Keyed State
|
|
525
|
+
|
|
526
|
+
Many stream transformations must reason across multiple events or timer firings. Windows, state machines, aggregates, and complex event processing all require state.
|
|
527
|
+
|
|
528
|
+
A Kafka key identifies an entity, such as a customer or order. Keyed state gives each key independent working state for these transformations. With Cassandra, the state survives restarts and partition reassignment.
|
|
529
|
+
|
|
530
|
+
Prosody selects the current message or timer key. It processes one event at a time for that key but can process other keys concurrently. By default, Prosody commits pending keyed-state changes only when the handler succeeds. If the handler returns an error, Prosody discards those changes.
|
|
531
|
+
|
|
532
|
+
Give most collections a time to live (TTL). Set the TTL beyond the longest timer or workflow that uses the collection. Omit it when state must remain for inactive keys.
|
|
533
|
+
|
|
534
|
+
### A counter for each key
|
|
535
|
+
|
|
536
|
+
Declare each collection once. Register it on the client. In a handler, get the current key's state from the event context:
|
|
537
|
+
|
|
538
|
+
```ruby
|
|
539
|
+
COUNTER = Prosody.value("counter", ttl: 30 * 24 * 60 * 60)
|
|
540
|
+
|
|
541
|
+
class CountHandler < Prosody::EventHandler
|
|
542
|
+
def on_message(context, _message)
|
|
543
|
+
count = context.state(COUNTER)
|
|
544
|
+
count.set((count.get || 0) + 1)
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
def on_excise(context, _message)
|
|
548
|
+
context.state(COUNTER).clear
|
|
549
|
+
nil
|
|
550
|
+
end
|
|
551
|
+
def on_timer(_context, _timer); end
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
client = Prosody::Client.new(
|
|
555
|
+
group_id: "counters",
|
|
556
|
+
subscribed_topics: "events",
|
|
557
|
+
state_collections: [COUNTER]
|
|
558
|
+
)
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
Each Kafka key now has an independent counter. A counter expires when that key has no update for 30 days.
|
|
562
|
+
|
|
563
|
+
### Window activity into one notification
|
|
564
|
+
|
|
565
|
+
This example sends the first event for a user immediately. It collects later events for five minutes and then sends one summary.
|
|
566
|
+
|
|
567
|
+
The user ID is the Kafka key. Each user has an independent window.
|
|
568
|
+
|
|
569
|
+
```ruby
|
|
570
|
+
WINDOW = Prosody.value("window", ttl: 24 * 60 * 60)
|
|
571
|
+
PENDING = Prosody.message_deque("pending", capacity: 100, ttl: 24 * 60 * 60)
|
|
572
|
+
|
|
573
|
+
class ActivityHandler < Prosody::EventHandler
|
|
574
|
+
def on_message(context, message)
|
|
575
|
+
window = context.state(WINDOW)
|
|
576
|
+
pending = context.state(PENDING)
|
|
577
|
+
|
|
578
|
+
if window.get
|
|
579
|
+
pending.push(message)
|
|
580
|
+
return
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
notify(message.key, [message])
|
|
584
|
+
window.set(true)
|
|
585
|
+
context.clear_and_schedule(Time.now + 5 * 60)
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
def on_timer(context, timer)
|
|
589
|
+
pending = context.state(PENDING)
|
|
590
|
+
batch = []
|
|
591
|
+
pending.each { |message| batch << message }
|
|
592
|
+
|
|
593
|
+
notify(timer.key, batch) unless batch.empty?
|
|
594
|
+
pending.clear
|
|
595
|
+
context.state(WINDOW).clear
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
def on_excise(context, _message)
|
|
599
|
+
context.state(PENDING).clear
|
|
600
|
+
context.state(WINDOW).clear
|
|
601
|
+
context.clear_scheduled
|
|
602
|
+
nil
|
|
603
|
+
end
|
|
604
|
+
end
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
See the complete, Steep-checked example for signatures, client setup, and `notify`: [`examples/keyed_state_windowing.rb`](examples/keyed_state_windowing.rb) and [`examples/keyed_state_windowing.rbs`](examples/keyed_state_windowing.rbs).
|
|
608
|
+
|
|
609
|
+
Why this works:
|
|
610
|
+
|
|
611
|
+
- Register both definitions in `state_collections` before you subscribe. Keyed state uses Cassandra unless `mock: true`.
|
|
612
|
+
- Use `clear_and_schedule`, not `schedule`, so a retried event does not add another timer for the same key.
|
|
613
|
+
- `capacity: 100` and the one-day TTL bound the saved backlog. Overflow drops the oldest message because this example only appends.
|
|
614
|
+
- A `message_deque` requires the original Kafka messages during the window. Use `deque` when topic retention or compaction cannot provide them.
|
|
615
|
+
- Prosody runs one handler at a time for each key, so a user's message and timer handlers cannot overlap.
|
|
616
|
+
- A notification is outside the state transaction. A retry can send it again. Use a stable operation ID to reject duplicate notifications.
|
|
617
|
+
|
|
618
|
+
### Collections and handles
|
|
619
|
+
|
|
620
|
+
A definition sets a collection's durable name, kind, and options. Register it once. Pass it to `context.state` in a handler.
|
|
621
|
+
|
|
622
|
+
Do not reuse a durable name for a different collection kind or payload type. Create handles inside the handler. Do not retain handles or iterators.
|
|
623
|
+
|
|
624
|
+
State operations look synchronous. They yield the current fiber while Prosody performs the work.
|
|
625
|
+
|
|
626
|
+
| Collection | JSON payload | Kafka message | Main operations |
|
|
627
|
+
| --- | --- | --- | --- |
|
|
628
|
+
| Value | `Prosody.value` | `Prosody.message_value` | `get`, `set`, `clear` |
|
|
629
|
+
| Ordered string map | `Prosody.map` | `Prosody.message_map` | `get`, `get_many`, `key?`, `set`, `delete`, `each_pair`, `each_key`, `clear` |
|
|
630
|
+
| Deque | `Prosody.deque` | `Prosody.message_deque` | `push`, `unshift`, `pop`, `shift`, `get`, `length`, `each`, `clear` |
|
|
631
|
+
|
|
632
|
+
Map and deque scans return enumerators when called without a block. Map keys are strings.
|
|
633
|
+
|
|
634
|
+
`nil` means absence. Do not store this value. Use `clear` or `delete`.
|
|
635
|
+
|
|
636
|
+
### When keyed-state changes become visible
|
|
637
|
+
|
|
638
|
+
By default, retries do not see pending state from a failed attempt. Reads in a handler see its earlier keyed-state writes. Prosody commits pending changes when the event succeeds and discards them when the handler raises.
|
|
639
|
+
|
|
640
|
+
This transaction applies only to keyed state. Some workflows need state changes before the handler ends, so each collection also provides explicit controls:
|
|
641
|
+
|
|
642
|
+
- `read_uncommitted: true` persists keyed-state changes before Prosody records the event as complete. If the process stops between these steps, Prosody can process the same event again. The retry sees state changes from the earlier attempt. You must make these keyed-state changes idempotent. Each retry must produce the same state.
|
|
643
|
+
- `commit` commits the collection's pending changes before the handler ends. A later handler failure does not remove them.
|
|
644
|
+
- `rollback` discards pending changes since the last `commit`. It cannot undo committed changes.
|
|
645
|
+
|
|
646
|
+
### Published state
|
|
647
|
+
|
|
648
|
+
Some callers need only the current value for a key. They can accept a stale value or a race with a concurrent update.
|
|
649
|
+
|
|
650
|
+
Use topics and event sourcing when a consumer must process each state change in order. Use published state for direct, read-only lookup of persisted keyed state. The caller does not need to consume the owner's topics or maintain a separate lookup store.
|
|
651
|
+
|
|
652
|
+
Configure the subsystem name on each publisher. Enable publication on the collection definition. Register the definition on the Prosody client:
|
|
653
|
+
|
|
654
|
+
```ruby
|
|
655
|
+
CURRENT_ORDER = Prosody.value("current-order", published: true)
|
|
656
|
+
|
|
657
|
+
owner = Prosody::Client.new(
|
|
658
|
+
group_id: "order-writer",
|
|
659
|
+
subsystem: "checkout",
|
|
660
|
+
state_collections: [CURRENT_ORDER]
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
# The handler uses the key from its current event.
|
|
664
|
+
current_order = context.state(CURRENT_ORDER)
|
|
665
|
+
current_order.set({"sku" => "book"})
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
Read published state from a handler or other application code. The Prosody client does not need an active subscription.
|
|
669
|
+
|
|
670
|
+
Use the subsystem and the same definition to open a reader:
|
|
671
|
+
|
|
672
|
+
```ruby
|
|
673
|
+
order_reader = client.state("checkout", CURRENT_ORDER)
|
|
674
|
+
current_order = order_reader.get("customer-123")
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
The reader cannot see pending changes that exist only in a handler. It cannot change the collection. Each read takes an explicit key because no handler supplies one.
|
|
678
|
+
|
|
679
|
+
Map and deque readers fetch data in chunks. They do not load the complete collection before iteration starts. Readers return an `Enumerator` without a block.
|
|
680
|
+
|
|
681
|
+
Use `reverse_each_pair`, `reverse_each_key`, `reverse_each_value`, or `reverse_each` for reverse traversal.
|
|
682
|
+
|
|
683
|
+
The default cache window is five seconds. Set `read_cache:` to select a different window. Set `read_cache: false` to bypass the cache.
|
|
684
|
+
|
|
685
|
+
To stop publication, deploy the definition with `published: false`. Keep the definition registered during that deployment. Keep the subsystem configured during that deployment.
|
|
686
|
+
|
|
532
687
|
## Timer Functionality
|
|
533
688
|
|
|
534
689
|
Prosody supports timer-based delayed execution within message handlers. When a timer fires, your handler's `on_timer` method will be called:
|
|
@@ -556,6 +711,11 @@ class MyHandler < Prosody::EventHandler
|
|
|
556
711
|
puts "Key: #{timer.key}"
|
|
557
712
|
puts "Scheduled time: #{timer.time}"
|
|
558
713
|
end
|
|
714
|
+
|
|
715
|
+
def on_excise(context, _message)
|
|
716
|
+
context.clear_scheduled
|
|
717
|
+
nil
|
|
718
|
+
end
|
|
559
719
|
end
|
|
560
720
|
```
|
|
561
721
|
|
|
@@ -680,6 +840,9 @@ class MyHandler < Prosody::EventHandler
|
|
|
680
840
|
})
|
|
681
841
|
end
|
|
682
842
|
end
|
|
843
|
+
|
|
844
|
+
def on_excise(_context, _message); end
|
|
845
|
+
def on_timer(_context, _timer); end
|
|
683
846
|
end
|
|
684
847
|
```
|
|
685
848
|
|
|
@@ -734,22 +897,17 @@ Strategies for achieving idempotence:
|
|
|
734
897
|
- Each message advances the state machine, allowing for idempotent processing and easy failure recovery.
|
|
735
898
|
- Particularly useful for complex, distributed transactions across multiple services.
|
|
736
899
|
|
|
737
|
-
###
|
|
900
|
+
### Application shutdown
|
|
901
|
+
|
|
902
|
+
A Prosody client runs a subscription, timers, and other services in the background. Before an application terminates, it must stop all client services. `unsubscribe` stops only the active subscription.
|
|
738
903
|
|
|
739
|
-
|
|
904
|
+
Call `shutdown` when the application terminates. It stops all client services and rejects new operations. Call `unsubscribe` only when the application will use the client again. You do not need to call `unsubscribe` before `shutdown`.
|
|
740
905
|
|
|
741
906
|
```ruby
|
|
742
|
-
|
|
743
|
-
client.unsubscribe
|
|
907
|
+
client.shutdown
|
|
744
908
|
```
|
|
745
909
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
1. Completion and commitment of all in-flight work
|
|
749
|
-
2. Quick rebalancing, allowing other consumers to take over partitions
|
|
750
|
-
3. Proper release of resources
|
|
751
|
-
|
|
752
|
-
Implement shutdown handling in your application using signal handlers:
|
|
910
|
+
Handle application shutdown with signal handlers:
|
|
753
911
|
|
|
754
912
|
```ruby
|
|
755
913
|
require "prosody"
|
|
@@ -760,22 +918,22 @@ client = Prosody::Client.new(
|
|
|
760
918
|
subscribed_topics: "my-topic"
|
|
761
919
|
)
|
|
762
920
|
|
|
763
|
-
#
|
|
921
|
+
# Create the shutdown queue.
|
|
764
922
|
shutdown = Queue.new
|
|
765
923
|
|
|
766
|
-
#
|
|
924
|
+
# Register the signal handlers.
|
|
767
925
|
Signal.trap("INT") { shutdown.push(nil) }
|
|
768
926
|
Signal.trap("TERM") { shutdown.push(nil) }
|
|
769
927
|
|
|
770
|
-
# Subscribe
|
|
928
|
+
# Subscribe with the application handler.
|
|
771
929
|
client.subscribe(MyHandler.new)
|
|
772
930
|
|
|
773
|
-
#
|
|
774
|
-
shutdown.pop
|
|
931
|
+
# Wait for a shutdown signal.
|
|
932
|
+
shutdown.pop
|
|
775
933
|
|
|
776
|
-
#
|
|
777
|
-
puts "
|
|
778
|
-
client.
|
|
934
|
+
# Shut down the client.
|
|
935
|
+
puts "Client shutdown starts."
|
|
936
|
+
client.shutdown
|
|
779
937
|
```
|
|
780
938
|
|
|
781
939
|
### Error Handling
|
|
@@ -783,6 +941,8 @@ client.unsubscribe
|
|
|
783
941
|
Prosody classifies errors as transient (temporary, can be retried) or permanent (won't be resolved by retrying). By
|
|
784
942
|
default, all errors are considered transient.
|
|
785
943
|
|
|
944
|
+
The error classes and classification methods apply to `on_message`, `on_excise`, and `on_timer`.
|
|
945
|
+
|
|
786
946
|
Use the `Prosody::EventHandler` error classification methods:
|
|
787
947
|
|
|
788
948
|
```ruby
|
|
@@ -799,6 +959,9 @@ class MyHandler < Prosody::EventHandler
|
|
|
799
959
|
# JSON::ParserError will be treated as transient
|
|
800
960
|
# All other exceptions will be treated as transient (default behavior)
|
|
801
961
|
end
|
|
962
|
+
|
|
963
|
+
def on_excise(_context, _message); end
|
|
964
|
+
def on_timer(_context, _timer); end
|
|
802
965
|
end
|
|
803
966
|
```
|
|
804
967
|
|
|
@@ -836,6 +999,9 @@ class MyHandler < Prosody::EventHandler
|
|
|
836
999
|
release_resource(resource)
|
|
837
1000
|
end
|
|
838
1001
|
end
|
|
1002
|
+
|
|
1003
|
+
def on_excise(_context, _message); end
|
|
1004
|
+
def on_timer(_context, _timer); end
|
|
839
1005
|
end
|
|
840
1006
|
```
|
|
841
1007
|
|
|
@@ -897,18 +1063,30 @@ Ensure you have thoroughly tested your changes before merging to `main`.
|
|
|
897
1063
|
|
|
898
1064
|
### Prosody::Client
|
|
899
1065
|
|
|
900
|
-
- `new(
|
|
901
|
-
- `send_message(topic, key, payload)`: Send a message
|
|
902
|
-
- `
|
|
1066
|
+
- `new(config)` or `new(**options)`: Create a client from a `Configuration`, hash, or keyword options.
|
|
1067
|
+
- `send_message(String topic, String key, Prosody::json_value payload)`: Send a JSON-serializable message.
|
|
1068
|
+
- `excise(String topic, String key)`: Send an excise record for a key.
|
|
1069
|
+
- `request(topic:, key:, payload:, subsystems:, timeout:)`: Return one outcome for each subsystem.
|
|
1070
|
+
- `request_excise(topic:, key:, subsystems:, timeout:)`: Return one excise outcome for each subsystem.
|
|
1071
|
+
- `consumer_state`: Get the client state (`:shut_down`, `:unconfigured`, `:configured`, or `:running`).
|
|
903
1072
|
- `source_system`: Get the source system identifier configured for the client.
|
|
904
|
-
- `
|
|
905
|
-
- `
|
|
1073
|
+
- `state(subsystem, definition)`: Open a typed, read-only published value, map, or deque.
|
|
1074
|
+
- `subscribe(handler)`: Start event processing with the specified handler.
|
|
1075
|
+
- `unsubscribe`: Stop the consumer. You can subscribe again later.
|
|
1076
|
+
- `shutdown`: Stop all client services. Concurrent and repeated calls wait for the same operation.
|
|
906
1077
|
- `assigned_partitions`: Get the number of partitions currently assigned to this consumer.
|
|
907
1078
|
- `is_stalled?`: Check if the consumer has stalled partitions.
|
|
908
1079
|
|
|
1080
|
+
### Prosody::AdminClient
|
|
1081
|
+
|
|
1082
|
+
- `new(bootstrap_servers)`: Create an admin client for the specified Kafka servers.
|
|
1083
|
+
- `create_topic(name, partitions, replication_factor)`: Create a Kafka topic.
|
|
1084
|
+
- `delete_topic(name)`: Delete a Kafka topic.
|
|
1085
|
+
|
|
909
1086
|
### Prosody::EventHandler
|
|
910
1087
|
|
|
911
|
-
A base class for user-defined handlers
|
|
1088
|
+
A base class for user-defined handlers. Its RBS payload parameter flows into
|
|
1089
|
+
`Message#payload`; a bare handler defaults to `Prosody::json_value`.
|
|
912
1090
|
|
|
913
1091
|
```ruby
|
|
914
1092
|
class MyHandler < Prosody::EventHandler
|
|
@@ -923,26 +1101,61 @@ class MyHandler < Prosody::EventHandler
|
|
|
923
1101
|
def on_timer(context, timer)
|
|
924
1102
|
# Implement your timer handling logic here
|
|
925
1103
|
end
|
|
1104
|
+
|
|
1105
|
+
def on_excise(_context, _message)
|
|
1106
|
+
# Implement your excise handling logic here
|
|
1107
|
+
end
|
|
926
1108
|
end
|
|
927
1109
|
```
|
|
928
1110
|
|
|
1111
|
+
`on_message`, `on_excise`, and `on_timer` are the handler callbacks. The `permanent` and `transient` methods classify selected exceptions.
|
|
1112
|
+
|
|
929
1113
|
### Prosody::Message
|
|
930
1114
|
|
|
931
|
-
|
|
1115
|
+
`Prosody::Message[Payload]` represents a Kafka message. `Payload` defaults to
|
|
1116
|
+
`Prosody::json_value` (`nil`, booleans, numbers, strings, arrays, and
|
|
1117
|
+
string-keyed hashes, recursively). The parameter is static documentation and
|
|
1118
|
+
does not perform runtime validation.
|
|
1119
|
+
|
|
1120
|
+
For a type-safe handler, describe the JSON record and specialize the handler in
|
|
1121
|
+
your application's RBS:
|
|
1122
|
+
|
|
1123
|
+
```rbs
|
|
1124
|
+
type order_event = { "order_id" => String, "total" => Integer }
|
|
1125
|
+
type response = { "accepted" => bool }
|
|
1126
|
+
|
|
1127
|
+
class OrderHandler < Prosody::EventHandler[order_event, response]
|
|
1128
|
+
def on_message: (Prosody::Context, Prosody::Message[order_event]) -> response
|
|
1129
|
+
def on_excise: (Prosody::Context, Prosody::ExciseMessage) -> response
|
|
1130
|
+
end
|
|
1131
|
+
```
|
|
1132
|
+
|
|
1133
|
+
Ruby can then use `message.payload["order_id"]` as a `String` and
|
|
1134
|
+
`message.payload["total"]` as an `Integer`. See
|
|
1135
|
+
[`examples/keyed_state.rb`](examples/keyed_state.rb) and its companion
|
|
1136
|
+
[`examples/keyed_state.rbs`](examples/keyed_state.rbs) for payload typing that
|
|
1137
|
+
also flows through message-backed state.
|
|
1138
|
+
|
|
1139
|
+
Messages have the following attributes:
|
|
932
1140
|
|
|
933
1141
|
- `topic` (String): The name of the topic.
|
|
934
1142
|
- `partition` (Integer): The partition number.
|
|
935
1143
|
- `offset` (Integer): The message offset within the partition.
|
|
936
1144
|
- `timestamp` (Time): The timestamp when the message was created or sent.
|
|
937
1145
|
- `key` (String): The message key.
|
|
938
|
-
- `payload` (
|
|
1146
|
+
- `payload` (`Payload`): The JSON-deserialized message payload.
|
|
1147
|
+
|
|
1148
|
+
### Prosody::ExciseMessage
|
|
1149
|
+
|
|
1150
|
+
An `ExciseMessage` has `topic`, `partition`, `offset`, `timestamp`, and `key` attributes. It has no `payload` attribute.
|
|
939
1151
|
|
|
940
1152
|
### Prosody::Context
|
|
941
1153
|
|
|
942
|
-
Represents the
|
|
1154
|
+
Represents the current event context:
|
|
943
1155
|
|
|
944
1156
|
- `should_cancel?`: Check if cancellation has been requested (includes timeout and shutdown).
|
|
945
|
-
- `on_cancel`:
|
|
1157
|
+
- `on_cancel`: Wait until cancellation occurs.
|
|
1158
|
+
- `state(definition)`: Bind a registered collection for the current attempt. An unregistered or mismatched definition raises `PermanentStateError`. See [Keyed State](#keyed-state-2).
|
|
946
1159
|
|
|
947
1160
|
Timer scheduling methods:
|
|
948
1161
|
|
|
@@ -958,3 +1171,80 @@ Represents a timer that has fired, provided to the `on_timer` method:
|
|
|
958
1171
|
|
|
959
1172
|
- `key` (String): The entity key identifying what this timer belongs to
|
|
960
1173
|
- `time` (Time): The time when this timer was scheduled to fire
|
|
1174
|
+
|
|
1175
|
+
### Requests
|
|
1176
|
+
|
|
1177
|
+
- `Prosody::Success[Value]`: Contains the response in `value`.
|
|
1178
|
+
- `Prosody::Failure`: Contains a response error in `error`.
|
|
1179
|
+
- `Prosody::HandlerError`, `Timeout`, `FormatMismatch`, and `MalformedResponse`: The possible response errors.
|
|
1180
|
+
- `Prosody::response_error`: The union of all response error types.
|
|
1181
|
+
- `Prosody::outcome[Value]`: A `Success[Value]` or `Failure`.
|
|
1182
|
+
|
|
1183
|
+
### Keyed State
|
|
1184
|
+
|
|
1185
|
+
Definition constructors (each returns a frozen definition object used both in `Configuration#state_collections` and with `context.state`):
|
|
1186
|
+
|
|
1187
|
+
- `Prosody.value(name, ttl: nil, read_uncommitted: nil, published: nil, read_cache: nil)`
|
|
1188
|
+
- `Prosody.map(name, ttl: nil, keyset_limit: nil, read_uncommitted: nil, published: nil, read_cache: nil)`
|
|
1189
|
+
- `Prosody.deque(name, ttl: nil, capacity: nil, read_uncommitted: nil, published: nil, read_cache: nil)`
|
|
1190
|
+
- `Prosody.message_value(name, ttl: nil, read_uncommitted: nil)`
|
|
1191
|
+
- `Prosody.message_map(name, ttl: nil, keyset_limit: nil, read_uncommitted: nil)`
|
|
1192
|
+
- `Prosody.message_deque(name, ttl: nil, capacity: nil, read_uncommitted: nil)`
|
|
1193
|
+
|
|
1194
|
+
Each constructor returns a `StateDefinition`. It exposes `name`, `kind`, `payload`, all supplied options, and `to_state_config`.
|
|
1195
|
+
|
|
1196
|
+
Published readers take the user key as their first argument. `Prosody::PublishedValue` provides `get`.
|
|
1197
|
+
|
|
1198
|
+
`Prosody::PublishedMap` provides `get`, `get_many`, `key?`, `has_key?`, `include?`, and `member?`.
|
|
1199
|
+
|
|
1200
|
+
It provides `each` or `each_pair`, `each_key`, and `each_value`. The reverse methods are `reverse_each_pair`, `reverse_each_key`, and `reverse_each_value`.
|
|
1201
|
+
|
|
1202
|
+
`Prosody::PublishedDeque` provides `get`, `length` or `size`, `empty?`, `first`, `last`, `each`, and `reverse_each`.
|
|
1203
|
+
|
|
1204
|
+
Traversal methods return an `Enumerator` without a block.
|
|
1205
|
+
|
|
1206
|
+
`Prosody::ValueState`:
|
|
1207
|
+
|
|
1208
|
+
- `get` / `value`, `set(value)` / `value=`, `clear`, `commit`, and `rollback`
|
|
1209
|
+
|
|
1210
|
+
`Prosody::MapState` (keys are `String`):
|
|
1211
|
+
|
|
1212
|
+
- `get` / `[]`, `get_many`, `set` / `[]=`, `store`, `delete`, and `clear`
|
|
1213
|
+
- `key?`, `has_key?`, `include?`, `member?`, `dig`, `slice`, `values_at`, `fetch`, and `fetch_values`
|
|
1214
|
+
- `each` / `each_pair`, `each_key`, and `each_value`, including each reverse form
|
|
1215
|
+
- `commit` and `rollback`
|
|
1216
|
+
|
|
1217
|
+
`Prosody::DequeState`:
|
|
1218
|
+
|
|
1219
|
+
- `push`, `append`, `<<`, `unshift`, `prepend`, `pop`, and `shift`
|
|
1220
|
+
- `length` / `size`, `empty?`, `get`, `fetch`, `first`, `last`, and `clear`
|
|
1221
|
+
- `each` / `reverse_each`, `commit`, and `rollback`
|
|
1222
|
+
|
|
1223
|
+
Errors:
|
|
1224
|
+
|
|
1225
|
+
- `Prosody::TransientStateError < Prosody::TransientError`: Reports a keyed-state error that Prosody can retry.
|
|
1226
|
+
- `Prosody::PermanentStateError < Prosody::PermanentError`: Reports a keyed-state error that another attempt cannot resolve.
|
|
1227
|
+
- `Prosody::NullValueError < Prosody::TransientStateError`: raised when a `nil` is written; use `clear`/`delete` instead.
|
|
1228
|
+
|
|
1229
|
+
Handler error types:
|
|
1230
|
+
|
|
1231
|
+
- `Prosody::Error`: Base Prosody error.
|
|
1232
|
+
- `Prosody::EventHandlerError`: Base class for classified handler errors.
|
|
1233
|
+
- `Prosody::TransientError`: Marks an error as retriable.
|
|
1234
|
+
- `Prosody::PermanentError`: Marks an error as final.
|
|
1235
|
+
|
|
1236
|
+
### Configuration
|
|
1237
|
+
|
|
1238
|
+
`Prosody::Configuration.new` accepts a hash or block. Its public properties match the settings in [Configuration](CONFIGURATION.md). `to_hash` returns a configuration hash.
|
|
1239
|
+
|
|
1240
|
+
### Logging and telemetry
|
|
1241
|
+
|
|
1242
|
+
- `Prosody.logger`: Get the current logger or create the default logger.
|
|
1243
|
+
- `Prosody.logger=`: Replace the current logger. Assign `nil` to restore the default.
|
|
1244
|
+
- `Prosody.flush_telemetry`: Export pending telemetry.
|
|
1245
|
+
- `Prosody.shutdown_telemetry`: Export pending telemetry and stop its providers.
|
|
1246
|
+
|
|
1247
|
+
### Sentry
|
|
1248
|
+
|
|
1249
|
+
- `Prosody::SentryIntegration.enabled?`: Test whether Sentry integration is active.
|
|
1250
|
+
- `Prosody::SentryIntegration.capture_exception(exception, context = {})`: Report an exception with optional context.
|