prosody 0.4.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 +2 -2
- data/CHANGELOG.md +15 -0
- data/CLAUDE.md +1 -0
- data/CONFIGURATION.md +167 -0
- data/Cargo.lock +660 -326
- data/Cargo.toml +2 -1
- data/README.md +290 -191
- data/examples/keyed_state.rb +15 -3
- data/examples/keyed_state_windowing.rb +9 -1
- data/ext/prosody/Cargo.toml +2 -1
- data/ext/prosody/src/admin.rs +1 -5
- data/ext/prosody/src/bridge/mod.rs +17 -32
- data/ext/prosody/src/client/config.rs +194 -89
- 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 +24 -20
- data/ext/prosody/src/handler/message.rs +50 -0
- data/ext/prosody/src/handler/mod.rs +112 -84
- 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 +49 -15
- data/lib/prosody/handler.rb +63 -10
- data/lib/prosody/native_stubs.rb +197 -31
- data/lib/prosody/request.rb +45 -0
- data/lib/prosody/state.rb +164 -41
- data/lib/prosody/version.rb +1 -1
- data/lib/prosody.rb +1 -0
- data/sig/configuration.rbs +51 -15
- data/sig/handler.rbs +12 -4
- data/sig/prosody.rbs +43 -2
- data/sig/request.rbs +66 -0
- data/sig/state.rbs +165 -47
- data/steep_expectations.yml +10 -0
- data/typecheck/payload_types.rb +14 -3
- data/typecheck/payload_types.rbs +4 -2
- data/typecheck_negative/payload_types.rb +4 -0
- data/typecheck_negative/payload_types.rbs +1 -0
- metadata +12 -2
- data/ext/prosody/src/handler/state.rs +0 -1035
data/README.md
CHANGED
|
@@ -30,10 +30,10 @@ Or install directly:
|
|
|
30
30
|
gem install prosody
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
The gem ships RBS signatures for the public API. `Prosody::EventHandler[Payload]`
|
|
34
|
-
carries
|
|
35
|
-
|
|
36
|
-
message, definition, or state handle
|
|
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
37
|
[typed examples](examples/) for Ruby and companion RBS files checked by Steep.
|
|
38
38
|
|
|
39
39
|
## Quick Start
|
|
@@ -59,6 +59,12 @@ client = Prosody::Client.new(
|
|
|
59
59
|
|
|
60
60
|
# Define a custom message handler
|
|
61
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
|
+
|
|
62
68
|
def on_message(context, message)
|
|
63
69
|
# Process the received message
|
|
64
70
|
puts "Received message: #{message.payload.inspect}"
|
|
@@ -81,11 +87,22 @@ client.subscribe(MyHandler.new)
|
|
|
81
87
|
|
|
82
88
|
# Send a message to a topic
|
|
83
89
|
client.send_message("my-topic", "message-key", {"content" => "Hello, Kafka!"})
|
|
90
|
+
client.excise("my-topic", "obsolete-key")
|
|
84
91
|
|
|
85
92
|
# Ensure proper shutdown when done
|
|
86
|
-
client.
|
|
93
|
+
client.shutdown
|
|
87
94
|
```
|
|
88
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
|
+
|
|
89
106
|
## Architecture
|
|
90
107
|
|
|
91
108
|
Prosody enables efficient, parallel processing of Kafka messages while maintaining order for messages with the same key:
|
|
@@ -173,134 +190,9 @@ timeout defaults to 80% of `stall_threshold`.
|
|
|
173
190
|
|
|
174
191
|
## Configuration
|
|
175
192
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
### Core
|
|
179
|
-
|
|
180
|
-
| Option / Environment Variable | Description | Default |
|
|
181
|
-
|-----------------------------------------|---------------------------------------------------|--------------|
|
|
182
|
-
| `bootstrap_servers` / `PROSODY_BOOTSTRAP_SERVERS` | Kafka servers to connect to | - |
|
|
183
|
-
| `group_id` / `PROSODY_GROUP_ID` | Consumer group name | - |
|
|
184
|
-
| `subscribed_topics` / `PROSODY_SUBSCRIBED_TOPICS` | Topics to read from | - |
|
|
185
|
-
| `allowed_events` / `PROSODY_ALLOWED_EVENTS` | Only process events matching these prefixes | (all) |
|
|
186
|
-
| `source_system` / `PROSODY_SOURCE_SYSTEM` | Tag for outgoing messages (prevents reprocessing)| `<group_id>` |
|
|
187
|
-
| `mock` / `PROSODY_MOCK` | Use in-memory Kafka for testing | false |
|
|
188
|
-
|
|
189
|
-
### Consumer
|
|
190
|
-
|
|
191
|
-
| Option / Environment Variable | Description | Default |
|
|
192
|
-
|-----------------------------------------|------------------------------------------------------|------------------------|
|
|
193
|
-
| `max_concurrency` / `PROSODY_MAX_CONCURRENCY` | Max messages being processed simultaneously | 32 |
|
|
194
|
-
| `max_uncommitted` / `PROSODY_MAX_UNCOMMITTED` | Max queued messages before pausing consumption | 64 |
|
|
195
|
-
| `timeout` / `PROSODY_TIMEOUT` | Cancel handler if it runs longer than this | 80% of stall threshold |
|
|
196
|
-
| `commit_interval` / `PROSODY_COMMIT_INTERVAL` | How often to save progress to Kafka | 1s |
|
|
197
|
-
| `poll_interval` / `PROSODY_POLL_INTERVAL` | How often to fetch new messages from Kafka | 100ms |
|
|
198
|
-
| `shutdown_timeout` / `PROSODY_SHUTDOWN_TIMEOUT` | Shutdown budget; handlers run freely until cancellation fires near the end of the timeout | 30s |
|
|
199
|
-
| `stall_threshold` / `PROSODY_STALL_THRESHOLD` | Report unhealthy if no progress for this long | 5m |
|
|
200
|
-
| `probe_port` / `PROSODY_PROBE_PORT` | HTTP port for health checks (nil to disable) | 8000 |
|
|
201
|
-
| `failure_topic` / `PROSODY_FAILURE_TOPIC` | Send unprocessable messages here (dead letter queue) | - |
|
|
202
|
-
| `idempotence_cache_size` / `PROSODY_IDEMPOTENCE_CACHE_SIZE` | Global shared cache capacity across all partitions for message deduplication. Consumer deduplication is mandatory and cannot be disabled, so this must be at least 1; setting it to 0 in the client configuration is rejected | 8192 |
|
|
203
|
-
| `idempotence_version` / `PROSODY_IDEMPOTENCE_VERSION` | Version string for cache-busting dedup hashes | 1 |
|
|
204
|
-
| `idempotence_ttl` / `PROSODY_IDEMPOTENCE_TTL` | TTL for dedup records in Cassandra | 7d (604800 seconds) |
|
|
205
|
-
| `slab_size` / `PROSODY_SLAB_SIZE` | Timer storage granularity (rarely needs changing) | 1h |
|
|
206
|
-
| `message_spans` / `PROSODY_MESSAGE_SPANS` | Span linking for message execution: `child` (child-of) or `follows_from` | `child` |
|
|
207
|
-
| `timer_spans` / `PROSODY_TIMER_SPANS` | Span linking for timer execution: `child` (child-of) or `follows_from` | `follows_from` |
|
|
208
|
-
|
|
209
|
-
### Producer
|
|
210
|
-
|
|
211
|
-
| Option / Environment Variable | Description | Default |
|
|
212
|
-
|-----------------------------------------|---------------------------------|---------|
|
|
213
|
-
| `send_timeout` / `PROSODY_SEND_TIMEOUT` | Give up sending after this long | 1s |
|
|
214
|
-
|
|
215
|
-
### Retry
|
|
216
|
-
|
|
217
|
-
When a handler fails, retry with exponential backoff:
|
|
218
|
-
|
|
219
|
-
| Option / Environment Variable | Description | Default |
|
|
220
|
-
|-----------------------------------------|-----------------------------------|---------|
|
|
221
|
-
| `max_retries` / `PROSODY_MAX_RETRIES` | Give up after this many attempts | 3 |
|
|
222
|
-
| `retry_base` / `PROSODY_RETRY_BASE` | Wait this long before first retry | 20ms |
|
|
223
|
-
| `max_retry_delay` / `PROSODY_RETRY_MAX_DELAY` | Never wait longer than this | 5m |
|
|
224
|
-
|
|
225
|
-
### Deferral (Pipeline Mode)
|
|
226
|
-
|
|
227
|
-
| Option / Environment Variable | Description | Default |
|
|
228
|
-
|-----------------------------------------|---------------------------------------------------|---------|
|
|
229
|
-
| `defer_enabled` / `PROSODY_DEFER_ENABLED` | Enable deferral for new messages | true |
|
|
230
|
-
| `defer_base` / `PROSODY_DEFER_BASE` | Wait this long before first deferred retry | 1s |
|
|
231
|
-
| `defer_max_delay` / `PROSODY_DEFER_MAX_DELAY` | Never wait longer than this | 24h |
|
|
232
|
-
| `defer_failure_threshold` / `PROSODY_DEFER_FAILURE_THRESHOLD` | Disable deferral when failure rate exceeds this | 0.9 |
|
|
233
|
-
| `defer_failure_window` / `PROSODY_DEFER_FAILURE_WINDOW` | Measure failure rate over this time window | 5m |
|
|
234
|
-
| `defer_cache_size` / `PROSODY_DEFER_CACHE_SIZE` | Track this many deferred keys in memory | 1024 |
|
|
235
|
-
| `defer_store_cache_size` / `PROSODY_DEFER_STORE_CACHE_SIZE` | Maximum deferred store cache entries per Cassandra defer store | 8192 |
|
|
236
|
-
| `defer_seek_timeout` / `PROSODY_DEFER_SEEK_TIMEOUT` | Timeout when loading deferred messages | 30s |
|
|
237
|
-
| `defer_discard_threshold` / `PROSODY_DEFER_DISCARD_THRESHOLD` | Read optimization (rarely needs changing) | 100 |
|
|
238
|
-
|
|
239
|
-
### Monopolization Detection (Pipeline Mode)
|
|
240
|
-
|
|
241
|
-
| Option / Environment Variable | Description | Default |
|
|
242
|
-
|-----------------------------------------|-----------------------------------------|---------|
|
|
243
|
-
| `monopolization_enabled` / `PROSODY_MONOPOLIZATION_ENABLED` | Enable hot key protection | true |
|
|
244
|
-
| `monopolization_threshold` / `PROSODY_MONOPOLIZATION_THRESHOLD` | Max handler time as fraction of window | 0.9 |
|
|
245
|
-
| `monopolization_window` / `PROSODY_MONOPOLIZATION_WINDOW` | Measurement window | 5m |
|
|
246
|
-
| `monopolization_cache_size` / `PROSODY_MONOPOLIZATION_CACHE_SIZE` | Max distinct keys to track | 8192 |
|
|
247
|
-
|
|
248
|
-
### Fair Scheduling (All Modes)
|
|
249
|
-
|
|
250
|
-
| Option / Environment Variable | Description | Default |
|
|
251
|
-
|-----------------------------------------|------------------------------------------------------------------|---------|
|
|
252
|
-
| `scheduler_failure_weight` / `PROSODY_SCHEDULER_FAILURE_WEIGHT` | Fraction of processing time reserved for retries | 0.3 |
|
|
253
|
-
| `scheduler_max_wait` / `PROSODY_SCHEDULER_MAX_WAIT` | Messages waiting this long get maximum priority | 2m |
|
|
254
|
-
| `scheduler_wait_weight` / `PROSODY_SCHEDULER_WAIT_WEIGHT` | Priority boost for waiting messages (higher = more aggressive) | 200.0 |
|
|
255
|
-
| `scheduler_cache_size` / `PROSODY_SCHEDULER_CACHE_SIZE` | Max distinct keys to track | 8192 |
|
|
256
|
-
|
|
257
|
-
### Cassandra
|
|
258
|
-
|
|
259
|
-
Persistent storage for timers and deferred retries (not needed if `mock: true`):
|
|
193
|
+
For the complete configuration reference, see [CONFIGURATION.md](CONFIGURATION.md).
|
|
260
194
|
|
|
261
|
-
|
|
262
|
-
|-----------------------------------------|------------------------------------|---------|
|
|
263
|
-
| `cassandra_nodes` / `PROSODY_CASSANDRA_NODES` | Servers to connect to (host:port) | - |
|
|
264
|
-
| `cassandra_keyspace` / `PROSODY_CASSANDRA_KEYSPACE` | Keyspace name | prosody |
|
|
265
|
-
| `cassandra_user` / `PROSODY_CASSANDRA_USER` | Username | - |
|
|
266
|
-
| `cassandra_password` / `PROSODY_CASSANDRA_PASSWORD` | Password | - |
|
|
267
|
-
| `cassandra_datacenter` / `PROSODY_CASSANDRA_DATACENTER` | Prefer this datacenter for queries | - |
|
|
268
|
-
| `cassandra_rack` / `PROSODY_CASSANDRA_RACK` | Prefer this rack for queries | - |
|
|
269
|
-
| `cassandra_retention` / `PROSODY_CASSANDRA_RETENTION` | Delete data older than this | 1y |
|
|
270
|
-
|
|
271
|
-
### Keyed State
|
|
272
|
-
|
|
273
|
-
Register keyed-state collections before you subscribe. Persistence is backed by Cassandra and is not needed when `mock: true`. See the [Keyed State](#keyed-state-1) feature section for handler usage; the client-level knobs and per-collection fields are below. Where an option and an environment variable are paired, an explicitly set option wins; otherwise the environment variable applies, then the default.
|
|
274
|
-
|
|
275
|
-
| Option / Environment Variable | Description | Default |
|
|
276
|
-
|-------------------------------|-------------|---------|
|
|
277
|
-
| `state_collections` / - | Keyed-state collections to register before subscribe (array of definitions or config hashes; duplicate names rejected) | (none) |
|
|
278
|
-
| `state_cache_dir` / `PROSODY_STATE_CACHE_DIR` | Disk workspace for the local keyed-state cache; each live client needs its own directory (it is locked exclusively) | per-client temp dir |
|
|
279
|
-
| `state_cache_size_bytes` / `PROSODY_STATE_CACHE_SIZE_BYTES` | Capacity of the in-memory keyed-state cache, in bytes; must be greater than 0. One cache is shared by all partition keyspaces | engine default |
|
|
280
|
-
| `state_recovery_delay` / `PROSODY_STATE_RECOVERY_DELAY` | Whole-second delay between staging a provisional cell and the recovery sweep; every collection TTL must strictly exceed it | 30s |
|
|
281
|
-
|
|
282
|
-
Prefer the definition constructors (`Prosody.value` / `.map` / `.deque` and their `message_*` variants, documented below): they serialize into `state_collections` so you declare each collection once and reuse the same object with `context.state`. Each entry has these fields:
|
|
283
|
-
|
|
284
|
-
| Field | Description | Default |
|
|
285
|
-
|-------|-------------|---------|
|
|
286
|
-
| `name` | Collection name; non-empty and unique within the client | (required) |
|
|
287
|
-
| `kind` | `"value"`, `"map"`, or `"deque"` | (required) |
|
|
288
|
-
| `payload` | `"json"` (JSON values) or `"message"` (the full Kafka message the handler received) | (required) |
|
|
289
|
-
| `ttl_seconds` | Per-write TTL in whole seconds (at least 1; must exceed the recovery delay) | (none) |
|
|
290
|
-
| `read_uncommitted` | Opt out of transactional staging | false |
|
|
291
|
-
| `keyset_limit` | Map-only; ordered-scan bound in `0..=4096` (`0` disables ordered-scan tracking) | 128 |
|
|
292
|
-
| `capacity` | Deque-only window bound (at least 1); keeps at most N slots, enforced lazily on push. Runtime-only and mutable across deploys — not persisted | unbounded |
|
|
293
|
-
|
|
294
|
-
Constructors set these via keyword arguments (`ttl:`, `keyset_limit:`, `capacity:`, `read_uncommitted:`).
|
|
295
|
-
|
|
296
|
-
### Telemetry Emitter
|
|
297
|
-
|
|
298
|
-
Prosody can emit internal processing events (message lifecycle, timer events) to a Kafka topic for observability:
|
|
299
|
-
|
|
300
|
-
| Option / Environment Variable | Description | Default |
|
|
301
|
-
|-----------------------------------------|------------------------------------------------|----------------------------|
|
|
302
|
-
| `telemetry_topic` / `PROSODY_TELEMETRY_TOPIC` | Kafka topic to produce telemetry events to | `prosody.telemetry-events` |
|
|
303
|
-
| `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.
|
|
304
196
|
|
|
305
197
|
## Logging
|
|
306
198
|
|
|
@@ -393,6 +285,73 @@ if client.is_stalled?
|
|
|
393
285
|
end
|
|
394
286
|
```
|
|
395
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
|
+
|
|
396
355
|
## Advanced Usage
|
|
397
356
|
|
|
398
357
|
### Pipeline Mode
|
|
@@ -564,15 +523,17 @@ Note that the in-memory cache is best-effort. Duplicates can still occur across
|
|
|
564
523
|
|
|
565
524
|
## Keyed State
|
|
566
525
|
|
|
567
|
-
|
|
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.
|
|
568
529
|
|
|
569
|
-
|
|
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.
|
|
570
531
|
|
|
571
|
-
|
|
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.
|
|
572
533
|
|
|
573
534
|
### A counter for each key
|
|
574
535
|
|
|
575
|
-
Declare each collection once
|
|
536
|
+
Declare each collection once. Register it on the client. In a handler, get the current key's state from the event context:
|
|
576
537
|
|
|
577
538
|
```ruby
|
|
578
539
|
COUNTER = Prosody.value("counter", ttl: 30 * 24 * 60 * 60)
|
|
@@ -582,6 +543,12 @@ class CountHandler < Prosody::EventHandler
|
|
|
582
543
|
count = context.state(COUNTER)
|
|
583
544
|
count.set((count.get || 0) + 1)
|
|
584
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
|
|
585
552
|
end
|
|
586
553
|
|
|
587
554
|
client = Prosody::Client.new(
|
|
@@ -591,11 +558,13 @@ client = Prosody::Client.new(
|
|
|
591
558
|
)
|
|
592
559
|
```
|
|
593
560
|
|
|
594
|
-
|
|
561
|
+
Each Kafka key now has an independent counter. A counter expires when that key has no update for 30 days.
|
|
595
562
|
|
|
596
563
|
### Window activity into one notification
|
|
597
564
|
|
|
598
|
-
This example
|
|
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.
|
|
599
568
|
|
|
600
569
|
```ruby
|
|
601
570
|
WINDOW = Prosody.value("window", ttl: 24 * 60 * 60)
|
|
@@ -625,6 +594,13 @@ class ActivityHandler < Prosody::EventHandler
|
|
|
625
594
|
pending.clear
|
|
626
595
|
context.state(WINDOW).clear
|
|
627
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
|
|
628
604
|
end
|
|
629
605
|
```
|
|
630
606
|
|
|
@@ -632,18 +608,20 @@ See the complete, Steep-checked example for signatures, client setup, and `notif
|
|
|
632
608
|
|
|
633
609
|
Why this works:
|
|
634
610
|
|
|
635
|
-
- Register both definitions in `state_collections` before
|
|
611
|
+
- Register both definitions in `state_collections` before you subscribe. Keyed state uses Cassandra unless `mock: true`.
|
|
636
612
|
- Use `clear_and_schedule`, not `schedule`, so a retried event does not add another timer for the same key.
|
|
637
|
-
- `capacity: 100` and the one-day TTL
|
|
638
|
-
- A `message_deque` requires the original Kafka messages
|
|
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.
|
|
639
615
|
- Prosody runs one handler at a time for each key, so a user's message and timer handlers cannot overlap.
|
|
640
|
-
-
|
|
616
|
+
- A notification is outside the state transaction. A retry can send it again. Use a stable operation ID to reject duplicate notifications.
|
|
641
617
|
|
|
642
618
|
### Collections and handles
|
|
643
619
|
|
|
644
|
-
A definition
|
|
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.
|
|
645
623
|
|
|
646
|
-
|
|
624
|
+
State operations look synchronous. They yield the current fiber while Prosody performs the work.
|
|
647
625
|
|
|
648
626
|
| Collection | JSON payload | Kafka message | Main operations |
|
|
649
627
|
| --- | --- | --- | --- |
|
|
@@ -651,17 +629,60 @@ Create handles inside the handler and do not retain them or their iterators afte
|
|
|
651
629
|
| Ordered string map | `Prosody.map` | `Prosody.message_map` | `get`, `get_many`, `key?`, `set`, `delete`, `each_pair`, `each_key`, `clear` |
|
|
652
630
|
| Deque | `Prosody.deque` | `Prosody.message_deque` | `push`, `unshift`, `pop`, `shift`, `get`, `length`, `each`, `clear` |
|
|
653
631
|
|
|
654
|
-
Map and deque scans return enumerators when called without a block. Map keys are strings.
|
|
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
|
|
655
637
|
|
|
656
|
-
|
|
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.
|
|
657
639
|
|
|
658
|
-
|
|
640
|
+
This transaction applies only to keyed state. Some workflows need state changes before the handler ends, so each collection also provides explicit controls:
|
|
659
641
|
|
|
660
|
-
|
|
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.
|
|
661
645
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
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.
|
|
665
686
|
|
|
666
687
|
## Timer Functionality
|
|
667
688
|
|
|
@@ -690,6 +711,11 @@ class MyHandler < Prosody::EventHandler
|
|
|
690
711
|
puts "Key: #{timer.key}"
|
|
691
712
|
puts "Scheduled time: #{timer.time}"
|
|
692
713
|
end
|
|
714
|
+
|
|
715
|
+
def on_excise(context, _message)
|
|
716
|
+
context.clear_scheduled
|
|
717
|
+
nil
|
|
718
|
+
end
|
|
693
719
|
end
|
|
694
720
|
```
|
|
695
721
|
|
|
@@ -814,6 +840,9 @@ class MyHandler < Prosody::EventHandler
|
|
|
814
840
|
})
|
|
815
841
|
end
|
|
816
842
|
end
|
|
843
|
+
|
|
844
|
+
def on_excise(_context, _message); end
|
|
845
|
+
def on_timer(_context, _timer); end
|
|
817
846
|
end
|
|
818
847
|
```
|
|
819
848
|
|
|
@@ -868,22 +897,17 @@ Strategies for achieving idempotence:
|
|
|
868
897
|
- Each message advances the state machine, allowing for idempotent processing and easy failure recovery.
|
|
869
898
|
- Particularly useful for complex, distributed transactions across multiple services.
|
|
870
899
|
|
|
871
|
-
###
|
|
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.
|
|
872
903
|
|
|
873
|
-
|
|
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`.
|
|
874
905
|
|
|
875
906
|
```ruby
|
|
876
|
-
|
|
877
|
-
client.unsubscribe
|
|
907
|
+
client.shutdown
|
|
878
908
|
```
|
|
879
909
|
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
1. Completion and commitment of all in-flight work
|
|
883
|
-
2. Quick rebalancing, allowing other consumers to take over partitions
|
|
884
|
-
3. Proper release of resources
|
|
885
|
-
|
|
886
|
-
Implement shutdown handling in your application using signal handlers:
|
|
910
|
+
Handle application shutdown with signal handlers:
|
|
887
911
|
|
|
888
912
|
```ruby
|
|
889
913
|
require "prosody"
|
|
@@ -894,22 +918,22 @@ client = Prosody::Client.new(
|
|
|
894
918
|
subscribed_topics: "my-topic"
|
|
895
919
|
)
|
|
896
920
|
|
|
897
|
-
#
|
|
921
|
+
# Create the shutdown queue.
|
|
898
922
|
shutdown = Queue.new
|
|
899
923
|
|
|
900
|
-
#
|
|
924
|
+
# Register the signal handlers.
|
|
901
925
|
Signal.trap("INT") { shutdown.push(nil) }
|
|
902
926
|
Signal.trap("TERM") { shutdown.push(nil) }
|
|
903
927
|
|
|
904
|
-
# Subscribe
|
|
928
|
+
# Subscribe with the application handler.
|
|
905
929
|
client.subscribe(MyHandler.new)
|
|
906
930
|
|
|
907
|
-
#
|
|
908
|
-
shutdown.pop
|
|
931
|
+
# Wait for a shutdown signal.
|
|
932
|
+
shutdown.pop
|
|
909
933
|
|
|
910
|
-
#
|
|
911
|
-
puts "
|
|
912
|
-
client.
|
|
934
|
+
# Shut down the client.
|
|
935
|
+
puts "Client shutdown starts."
|
|
936
|
+
client.shutdown
|
|
913
937
|
```
|
|
914
938
|
|
|
915
939
|
### Error Handling
|
|
@@ -917,6 +941,8 @@ client.unsubscribe
|
|
|
917
941
|
Prosody classifies errors as transient (temporary, can be retried) or permanent (won't be resolved by retrying). By
|
|
918
942
|
default, all errors are considered transient.
|
|
919
943
|
|
|
944
|
+
The error classes and classification methods apply to `on_message`, `on_excise`, and `on_timer`.
|
|
945
|
+
|
|
920
946
|
Use the `Prosody::EventHandler` error classification methods:
|
|
921
947
|
|
|
922
948
|
```ruby
|
|
@@ -933,6 +959,9 @@ class MyHandler < Prosody::EventHandler
|
|
|
933
959
|
# JSON::ParserError will be treated as transient
|
|
934
960
|
# All other exceptions will be treated as transient (default behavior)
|
|
935
961
|
end
|
|
962
|
+
|
|
963
|
+
def on_excise(_context, _message); end
|
|
964
|
+
def on_timer(_context, _timer); end
|
|
936
965
|
end
|
|
937
966
|
```
|
|
938
967
|
|
|
@@ -970,6 +999,9 @@ class MyHandler < Prosody::EventHandler
|
|
|
970
999
|
release_resource(resource)
|
|
971
1000
|
end
|
|
972
1001
|
end
|
|
1002
|
+
|
|
1003
|
+
def on_excise(_context, _message); end
|
|
1004
|
+
def on_timer(_context, _timer); end
|
|
973
1005
|
end
|
|
974
1006
|
```
|
|
975
1007
|
|
|
@@ -1031,15 +1063,26 @@ Ensure you have thoroughly tested your changes before merging to `main`.
|
|
|
1031
1063
|
|
|
1032
1064
|
### Prosody::Client
|
|
1033
1065
|
|
|
1034
|
-
- `new(
|
|
1066
|
+
- `new(config)` or `new(**options)`: Create a client from a `Configuration`, hash, or keyword options.
|
|
1035
1067
|
- `send_message(String topic, String key, Prosody::json_value payload)`: Send a JSON-serializable message.
|
|
1036
|
-
- `
|
|
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`).
|
|
1037
1072
|
- `source_system`: Get the source system identifier configured for the client.
|
|
1038
|
-
- `
|
|
1039
|
-
- `
|
|
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.
|
|
1040
1077
|
- `assigned_partitions`: Get the number of partitions currently assigned to this consumer.
|
|
1041
1078
|
- `is_stalled?`: Check if the consumer has stalled partitions.
|
|
1042
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
|
+
|
|
1043
1086
|
### Prosody::EventHandler
|
|
1044
1087
|
|
|
1045
1088
|
A base class for user-defined handlers. Its RBS payload parameter flows into
|
|
@@ -1058,9 +1101,15 @@ class MyHandler < Prosody::EventHandler
|
|
|
1058
1101
|
def on_timer(context, timer)
|
|
1059
1102
|
# Implement your timer handling logic here
|
|
1060
1103
|
end
|
|
1104
|
+
|
|
1105
|
+
def on_excise(_context, _message)
|
|
1106
|
+
# Implement your excise handling logic here
|
|
1107
|
+
end
|
|
1061
1108
|
end
|
|
1062
1109
|
```
|
|
1063
1110
|
|
|
1111
|
+
`on_message`, `on_excise`, and `on_timer` are the handler callbacks. The `permanent` and `transient` methods classify selected exceptions.
|
|
1112
|
+
|
|
1064
1113
|
### Prosody::Message
|
|
1065
1114
|
|
|
1066
1115
|
`Prosody::Message[Payload]` represents a Kafka message. `Payload` defaults to
|
|
@@ -1073,9 +1122,11 @@ your application's RBS:
|
|
|
1073
1122
|
|
|
1074
1123
|
```rbs
|
|
1075
1124
|
type order_event = { "order_id" => String, "total" => Integer }
|
|
1125
|
+
type response = { "accepted" => bool }
|
|
1076
1126
|
|
|
1077
|
-
class OrderHandler < Prosody::EventHandler[order_event]
|
|
1078
|
-
def on_message: (Prosody::Context, Prosody::Message[order_event]) ->
|
|
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
|
|
1079
1130
|
end
|
|
1080
1131
|
```
|
|
1081
1132
|
|
|
@@ -1094,13 +1145,17 @@ Messages have the following attributes:
|
|
|
1094
1145
|
- `key` (String): The message key.
|
|
1095
1146
|
- `payload` (`Payload`): The JSON-deserialized message payload.
|
|
1096
1147
|
|
|
1148
|
+
### Prosody::ExciseMessage
|
|
1149
|
+
|
|
1150
|
+
An `ExciseMessage` has `topic`, `partition`, `offset`, `timestamp`, and `key` attributes. It has no `payload` attribute.
|
|
1151
|
+
|
|
1097
1152
|
### Prosody::Context
|
|
1098
1153
|
|
|
1099
|
-
Represents the
|
|
1154
|
+
Represents the current event context:
|
|
1100
1155
|
|
|
1101
1156
|
- `should_cancel?`: Check if cancellation has been requested (includes timeout and shutdown).
|
|
1102
|
-
- `on_cancel`:
|
|
1103
|
-
- `state(definition)`:
|
|
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).
|
|
1104
1159
|
|
|
1105
1160
|
Timer scheduling methods:
|
|
1106
1161
|
|
|
@@ -1117,35 +1172,79 @@ Represents a timer that has fired, provided to the `on_timer` method:
|
|
|
1117
1172
|
- `key` (String): The entity key identifying what this timer belongs to
|
|
1118
1173
|
- `time` (Time): The time when this timer was scheduled to fire
|
|
1119
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
|
+
|
|
1120
1183
|
### Keyed State
|
|
1121
1184
|
|
|
1122
1185
|
Definition constructors (each returns a frozen definition object used both in `Configuration#state_collections` and with `context.state`):
|
|
1123
1186
|
|
|
1124
|
-
- `Prosody.value(name, ttl: nil, read_uncommitted: nil)`
|
|
1125
|
-
- `Prosody.map(name, ttl: nil, keyset_limit: nil, read_uncommitted: nil)`
|
|
1126
|
-
- `Prosody.deque(name, ttl: nil, read_uncommitted: nil)`
|
|
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)`
|
|
1127
1190
|
- `Prosody.message_value(name, ttl: nil, read_uncommitted: nil)`
|
|
1128
1191
|
- `Prosody.message_map(name, ttl: nil, keyset_limit: nil, read_uncommitted: nil)`
|
|
1129
|
-
- `Prosody.message_deque(name, ttl: 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.
|
|
1130
1205
|
|
|
1131
1206
|
`Prosody::ValueState`:
|
|
1132
1207
|
|
|
1133
|
-
- `get`, `set(value)
|
|
1208
|
+
- `get` / `value`, `set(value)` / `value=`, `clear`, `commit`, and `rollback`
|
|
1134
1209
|
|
|
1135
1210
|
`Prosody::MapState` (keys are `String`):
|
|
1136
1211
|
|
|
1137
|
-
- `get
|
|
1138
|
-
- `
|
|
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`
|
|
1139
1216
|
|
|
1140
1217
|
`Prosody::DequeState`:
|
|
1141
1218
|
|
|
1142
|
-
- `push
|
|
1143
|
-
- `
|
|
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`
|
|
1144
1222
|
|
|
1145
1223
|
Errors:
|
|
1146
1224
|
|
|
1147
|
-
- `Prosody::TransientStateError < Prosody::TransientError`:
|
|
1148
|
-
- `Prosody::PermanentStateError < Prosody::PermanentError`:
|
|
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.
|
|
1149
1227
|
- `Prosody::NullValueError < Prosody::TransientStateError`: raised when a `nil` is written; use `clear`/`delete` instead.
|
|
1150
1228
|
|
|
1151
|
-
|
|
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.
|