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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.cargo/config.toml +3 -0
  3. data/.release-please-manifest.json +1 -1
  4. data/AGENTS.md +395 -0
  5. data/ARCHITECTURE.md +2 -2
  6. data/CHANGELOG.md +15 -0
  7. data/CLAUDE.md +1 -0
  8. data/CONFIGURATION.md +167 -0
  9. data/Cargo.lock +660 -326
  10. data/Cargo.toml +2 -1
  11. data/README.md +290 -191
  12. data/examples/keyed_state.rb +15 -3
  13. data/examples/keyed_state_windowing.rb +9 -1
  14. data/ext/prosody/Cargo.toml +2 -1
  15. data/ext/prosody/src/admin.rs +1 -5
  16. data/ext/prosody/src/bridge/mod.rs +17 -32
  17. data/ext/prosody/src/client/config.rs +194 -89
  18. data/ext/prosody/src/client/mod.rs +167 -74
  19. data/ext/prosody/src/client/request.rs +132 -0
  20. data/ext/prosody/src/client/support.rs +122 -0
  21. data/ext/prosody/src/handler/context.rs +24 -20
  22. data/ext/prosody/src/handler/message.rs +50 -0
  23. data/ext/prosody/src/handler/mod.rs +112 -84
  24. data/ext/prosody/src/handler/state/mod.rs +488 -0
  25. data/ext/prosody/src/handler/state/registration.rs +104 -0
  26. data/ext/prosody/src/handler/state/scan.rs +218 -0
  27. data/ext/prosody/src/lib.rs +15 -3
  28. data/ext/prosody/src/published.rs +273 -0
  29. data/ext/prosody/src/scheduler/mod.rs +2 -2
  30. data/ext/prosody/src/scheduler/processor.rs +2 -2
  31. data/ext/prosody/src/scheduler/result.rs +7 -4
  32. data/ext/prosody/src/util.rs +86 -5
  33. data/lib/prosody/configuration.rb +49 -15
  34. data/lib/prosody/handler.rb +63 -10
  35. data/lib/prosody/native_stubs.rb +197 -31
  36. data/lib/prosody/request.rb +45 -0
  37. data/lib/prosody/state.rb +164 -41
  38. data/lib/prosody/version.rb +1 -1
  39. data/lib/prosody.rb +1 -0
  40. data/sig/configuration.rbs +51 -15
  41. data/sig/handler.rbs +12 -4
  42. data/sig/prosody.rbs +43 -2
  43. data/sig/request.rbs +66 -0
  44. data/sig/state.rbs +165 -47
  45. data/steep_expectations.yml +10 -0
  46. data/typecheck/payload_types.rb +14 -3
  47. data/typecheck/payload_types.rbs +4 -2
  48. data/typecheck_negative/payload_types.rb +4 -0
  49. data/typecheck_negative/payload_types.rbs +1 -0
  50. metadata +12 -2
  51. data/ext/prosody/src/handler/state.rs +0 -1035
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6e796ed66be2610f629c69b181c1cac30155aa0c2196de69fc7a58538816caca
4
- data.tar.gz: db008d5d6e205204100eb405ebd01937772a83c1d502cac5a7ef3998bf866099
3
+ metadata.gz: 052a7e84a72066480eec4ac9ced4e9ab0df381899bda1cd9a643f6e24338863d
4
+ data.tar.gz: 2f853ea6a2b3e7341513b88256fec70f670c289f58ebc3e5256348816735ee49
5
5
  SHA512:
6
- metadata.gz: 1b33f8d570c33a9172073f9ab94b8e253d6dd965cec4d06ea0637c030b6013e1c20c95c03a9715abb0de4ab1e68ba292efc3ae85e1e350d5c36d36d29c5c66ee
7
- data.tar.gz: 6b8784b0f7dc8fec51a6c4f6b359df0335afb1f77abda27f8210c05a8991f994c17cca29e188a3cf733ee86f3501c24551177416922c32157dc53c081bd9d45d
6
+ metadata.gz: 5198b84350257b6b0a2b92e1a0a8833fcc4f8ff966b115146096352610a81110323ec78af5b1868f441aface34b6c561063c02303bb1ecb0f35e338c996ce948
7
+ data.tar.gz: ceebfd36b6ebd53b1462a563b423e168b028fea0fa5d1b3e36d7478bf8ca0a655ac1f8268c8152a0b4d93cb4ca438ef622cd89c7a4ca895e38bc769d529df1e7
data/.cargo/config.toml CHANGED
@@ -1,2 +1,5 @@
1
1
  [env]
2
2
  CMAKE_POLICY_VERSION_MINIMUM = { value = "3.5", force = true }
3
+
4
+ [target.x86_64-unknown-linux-gnu]
5
+ rustflags = ["-C", "target-cpu=x86-64-v3"]
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.4.0"
2
+ ".": "0.5.0"
3
3
  }
data/AGENTS.md ADDED
@@ -0,0 +1,395 @@
1
+ # CLAUDE.md
2
+
3
+ Development patterns and practices for prosody-rb: Ruby bindings for the
4
+ Prosody Kafka client library. A magnus/rb-sys extension (`ext/prosody`) wraps
5
+ the published `prosody` Rust crate; the Ruby library (`lib/prosody`) carries
6
+ the public API, with RBS signatures in `sig/`. `ARCHITECTURE.md` explains the
7
+ Ruby/Rust bridge in depth — read it before touching the extension.
8
+
9
+ ## Design Principles
10
+
11
+ These come before everything else. Every change is judged against them.
12
+
13
+ **Write code that is simple, clear, well-factored, elegant, easy to
14
+ understand, correct, and idiomatic.** A reader should grasp the intent without
15
+ effort. If a change makes the code harder to read, the change is wrong, even
16
+ if it is faster or shorter. If two designs are correct, pick the one that is
17
+ easier to delete.
18
+
19
+ **Make invalid states unrepresentable in the type system.** When a compiler
20
+ or type checker can prove a contract, no test, comment, or convention has to.
21
+ In Rust, prefer distinct types for distinct concepts, restricted constructors,
22
+ and `enum` sum types over flag fields. In the RBS signatures, give the public
23
+ surface precise types instead of loose ones. If a bug class can be made
24
+ uncompilable, do that instead of writing a runtime check.
25
+
26
+ **Delete more than you add.** Every change should leave the codebase smaller,
27
+ simpler, or both. If you must add code, look first for duplication you can
28
+ fold, abstractions that no longer pay rent, dead branches, and stale comments.
29
+ The end-state diff should net negative whenever the task allows. Line count is
30
+ not the only axis: plain duplicated arms often read better than generic
31
+ machinery.
32
+
33
+ **Identify, document, and enforce invariants.** For every load-bearing piece
34
+ of state: name the invariant, write it down near the type or function that
35
+ owns it, enforce it in the type system if you can, otherwise assert it at the
36
+ boundary, and cover it with a test. If you cannot name the invariant, you do
37
+ not yet understand the code well enough to change it.
38
+
39
+ **Leave the codebase better than you found it.** Drive-by simplifications are
40
+ encouraged when they are scoped to the area you are already touching. Do not
41
+ sprawl — but do not walk past obvious cleanup either.
42
+
43
+ ## Definition of Done
44
+
45
+ No change is complete until every line below holds. These are acts, not
46
+ aspirations — perform each one; do not merely agree with it:
47
+
48
+ 1. `cargo clippy --manifest-path ext/prosody/Cargo.toml` and the same with
49
+ `--tests` — zero warnings. `cargo doc` — zero warnings. `make format-check`
50
+ passes (rustfmt, taplo, standardrb).
51
+ 2. `bundle exec rake rbs` and `bundle exec rake steep` — zero errors. They
52
+ validate the signatures and type-check the library, the typed examples,
53
+ and the negative fixtures.
54
+ 3. `make test 2>&1 | tee /tmp/rspec-output.txt` — re-running slow suites is
55
+ expensive; grep the file, not the pipe.
56
+ 4. Every new or converted test was proved falsifiable once: inject the
57
+ failure, watch it go red, revert.
58
+ 5. Every deleted test names its surviving stronger test in the commit message.
59
+ 6. Everything the change replaces is gone — code, tests, signatures, doc
60
+ vocabulary (see Redesign hygiene). "The new thing works" is half done.
61
+ 7. Every claim written this session — doc cross-reference, "covered by" note,
62
+ exemplar path — was verified to resolve, not recalled from memory.
63
+ 8. The diff is net-negative, or each addition is individually justified.
64
+
65
+ ## Development Setup
66
+
67
+ Start required services with:
68
+
69
+ ```bash
70
+ docker-compose up -d
71
+ ```
72
+
73
+ Services:
74
+
75
+ - Kafka: localhost:9094
76
+ - Cassandra: localhost:9042
77
+
78
+ Common commands (`make help` lists them):
79
+
80
+ ```bash
81
+ make compile-dev # Compile the Rust extension (development mode)
82
+ make compile # Compile the Rust extension (release mode)
83
+ make test # Run the RSpec suite (needs Kafka and Cassandra)
84
+ make test-tracing # Run the tracing suite against a local OTel collector
85
+ make format # Format Rust, TOML, and Ruby code
86
+ make format-check # Check formatting without modifying files
87
+ ```
88
+
89
+ `rake` runs the full default gate: `compile spec standard rbs steep`.
90
+ Recompile with `make compile-dev` after any Rust change, or the specs run
91
+ against stale native code.
92
+
93
+ ## Critical Rules
94
+
95
+ **Error Handling (Rust):**
96
+
97
+ - Never use `expect`, `unwrap`, `panic`, or `ok()` - forbidden by lints
98
+ - Propagate errors with `?` unless explicitly authorized to swallow
99
+ - Use `thiserror` for structured errors; box only when Clippy warns
100
+
101
+ **Memory (Rust):**
102
+
103
+ - **Never leak memory.** `std::mem::forget`, `Box::leak`, and `ManuallyDrop`
104
+ without an explicit reclamation path are forbidden. If a test must simulate
105
+ "Drop never ran", seed the underlying state directly; forgetting is never
106
+ the shortcut.
107
+ - **No unbounded keyed RAM.** Any in-memory structure keyed by message key or
108
+ collection must have a fixed capacity bound. Every in-memory map names its
109
+ removal path; self-draining maps are fine, but the drain is still named.
110
+
111
+ **Allocation and layout (tiger style — https://tigerstyle.dev/):**
112
+
113
+ - No hot-path allocation that is not upfront and bounded. A steady-state path
114
+ (per message, per timer fire, per handler call) must not allocate a buffer
115
+ whose size is discovered at runtime and grown as needed.
116
+ - Pick the buffer by what is known about the size: compile-time constant →
117
+ stack array; runtime-varying but almost always small → `SmallVec` sized to
118
+ the common case; genuinely unbounded → `Vec::with_capacity` sized once.
119
+ - `with_capacity` excuses the sizing, never the allocation. A per-call heap
120
+ allocation on a steady-state path is the defect itself.
121
+ - Never add a gratuitous allocation to satisfy the borrow checker. Reach for
122
+ a function item, an index, or a borrow before a scratch `Vec`.
123
+ - No amortized resize buffers on the hot path. If a reusable scratch buffer
124
+ is unavoidable, allocate it once at construction with a fixed bound.
125
+ - **Lay data out for the access pattern.** A hot path that scans one or two
126
+ fields across many entries must find those fields contiguously. Reach the
127
+ full record only for the entry the scan selects. An array of `Option<Arc<T>>`
128
+ turns a two-word decision into one heap dereference per entry, and thrashes
129
+ the CPU cache. Memory bandwidth is the bottleneck today, so the scan decides
130
+ the layout, not the record. Don't thrash the cache. False sharing counts:
131
+ keep atomics that different threads write off one line.
132
+ - Simplicity is not sacrificed for this. When zero-alloc and simple genuinely
133
+ conflict, keep it simple and leave a comment naming the allocation.
134
+
135
+ **Code Quality:**
136
+
137
+ - Lint, doc, type, and format gates live in Definition of Done — zero
138
+ warnings tolerated.
139
+ - Never suppress warnings with `#[allow(...)]` without permission. All clippy
140
+ and rustc warnings are fixed properly, not suppressed.
141
+ - Never introduce `dyn` without permission — prefer generics and associated
142
+ types. The type-erased surface this binding consumes already lives in the
143
+ published `prosody` crate.
144
+
145
+ **Ruby/Rust boundary:**
146
+
147
+ - Rust cannot call Ruby methods from Rust-created threads. Only Ruby-created
148
+ threads run Ruby code safely. `ARCHITECTURE.md` documents the bridge that
149
+ upholds this; never bypass it.
150
+ - Release the GVL for blocking work in the extension so other Ruby threads
151
+ can run.
152
+
153
+ **JSON codec:**
154
+
155
+ - This binding never defines its own payload codec. Payload encoding and
156
+ decoding belong to the `prosody` crate's codec; the binding passes payload
157
+ bytes through it.
158
+ - `serde_json`, `simd_json`, and the `json!` macro are banned in Rust
159
+ production code here for payload handling. Tests may use `serde_json::Value`
160
+ as a concrete payload type.
161
+
162
+ **Redesign hygiene:**
163
+
164
+ When a design is replaced, remove *all* of it in the same change —
165
+ half-deleted designs are where bloat and bug re-introduction live:
166
+
167
+ - Sweep the old design's vocabulary from every doc comment, signature, and
168
+ example. A stale doc can instruct a reader to re-introduce a fixed bug.
169
+ - Code whose only caller is its own test is dead — delete both together.
170
+ - Struct fields threaded through configs but only read at construction are
171
+ residue from a superseded design — remove them end-to-end.
172
+ - Do not build surface ahead of a caller: delete zero-caller paths, or make
173
+ them owner-confirmed, tested features.
174
+
175
+ **Debugging Discipline:**
176
+
177
+ - Never claim "found the issue" without rigorous proof
178
+ - Evidence first (logs, tests, reproducible behavior) → hypothesis → test → verify
179
+
180
+ **Documentation:**
181
+
182
+ - **All written text for this project must conform to ASD-STE100 (Simplified
183
+ Technical English). No written text is exempt.** This rule applies to
184
+ documentation, comments, READMEs, plans, issues, reviews, chat responses,
185
+ commit messages, PR text, and user-facing text. Apply these primary STE rules:
186
+ - Use the active voice. Write instructions in the imperative.
187
+ - Write short sentences. Use 20 words or fewer for instructions. Use 25
188
+ words or fewer for descriptions.
189
+ - Write one instruction per sentence. Keep one topic per paragraph. Use a
190
+ maximum of six sentences in each paragraph.
191
+ - Use a word with only one meaning. Use the same word for the same thing.
192
+ - Use simple verb tenses. Do not use an "-ing" form as a verb when a simple
193
+ tense is correct.
194
+ - Do not use a noun cluster of more than three nouns.
195
+ - Use approved technical names and technical verbs consistently.
196
+ - Write doc comments for a reader unfamiliar with the codebase. Lead with
197
+ what the thing is, how to use it, and what guarantee it gives — not the
198
+ internal mechanism.
199
+ - Short declarative sentences, one idea each. At most one parenthetical aside
200
+ per comment, never nested.
201
+ - Never argue with an imagined reviewer. State what the code does and the
202
+ invariant it upholds. Mention a rejected alternative only when a maintainer
203
+ would plausibly reintroduce it, as its own plain sentence.
204
+ - No invented compound jargon. Spell the idea out in ordinary words;
205
+ established terms keep their standard form.
206
+ - State an invariant at the type or function that owns it, once. Reference
207
+ the owning type elsewhere instead of restating.
208
+ - Be concise. Bad or needless docs hurt readability — prefer fewer, sharper
209
+ words.
210
+ - Never cite a plan's or spec's section number, phase number, or ordinal in
211
+ durable docs — code comments, CLAUDE.md, PR and commit text. Name the
212
+ concept instead.
213
+ - Avoid vague metaphor filler in prose, comments, and commit/PR text ("north
214
+ star", "surface area", "lean into", "double-click", "first-class citizen").
215
+ Say the concrete thing instead.
216
+
217
+ **Style:**
218
+
219
+ - Prefer `use` statements over fully qualified prefixes
220
+ - Methods without `self` should be functions (except `new` and similar)
221
+ - Ask before large structural changes
222
+ - Default to `pub(crate)`/`pub(super)` in Rust; make something `pub` only as
223
+ a deliberate API decision.
224
+ - Keep trait constraints as local as possible: put a constraint on the
225
+ function that needs it, not the struct.
226
+ - When a proposed simplification is examined and rejected, record the ruling
227
+ in one sentence at the site so the next pass does not re-litigate it.
228
+ - Ruby code follows standardrb; the formatter is the arbiter.
229
+
230
+ **Git:**
231
+
232
+ - Never add self-attribution to branch names, commits, PR titles, PR
233
+ descriptions, or code comments.
234
+ - Use conventional commits for commit titles and PR titles (e.g., `fix:`,
235
+ `feat:`, `docs:`, `refactor:`).
236
+ - PR titles and descriptions are written for a reader who is not intimately
237
+ familiar with the project. Lead with what changed and why.
238
+ - Never hard-wrap paragraphs in GitHub PR descriptions, PR comments, or issue
239
+ text. Each prose paragraph is one single line; blank lines separate
240
+ paragraphs.
241
+ - PR descriptions never include a test plan or a list of verification steps.
242
+ - Do not reference internal phase numbers, task IDs, or spec sections in
243
+ commits or code comments.
244
+ - Never run `git reset` or `git checkout` that would destroy uncommitted or
245
+ committed changes without explicit human permission. Prefer `git stash`, an
246
+ explicit commit, or `git restore --staged <path>`.
247
+ - Use `gh` for GitHub operations (PRs, issues, API).
248
+
249
+ ## Error Classification
250
+
251
+ Distinguish permanent from transient errors for retry logic:
252
+
253
+ ```rust
254
+ #[derive(Debug, Clone, Copy)]
255
+ pub enum ErrorType {
256
+ Permanent, // Business logic - don't retry
257
+ Transient, // Network/timeout - retry with backoff
258
+ }
259
+ ```
260
+
261
+ A permanent error discards the in-flight message. An error the caller's code
262
+ causes (bad input, wrong argument shape) classifies as transient unless the
263
+ caller explicitly declares it permanent — a transient error retries and stays
264
+ visible, so no message is silently lost.
265
+
266
+ ## Concurrency Invariants (inherited from prosody)
267
+
268
+ - **One handler per key, system-wide.** The framework guarantees at most one
269
+ message or timer handler for a given key executes anywhere in the cluster
270
+ at any moment. Never design for concurrent writers on the same key — that
271
+ scenario cannot occur.
272
+ - **At most one partition owner.** Kafka partition assignment guarantees one
273
+ consumer group member owns each partition at a time.
274
+ - These invariants are why distributed locks and optimistic concurrency are
275
+ never needed for per-key state. The framework provides the exclusivity;
276
+ binding code and examples can assume it.
277
+
278
+ ## Code Organization
279
+
280
+ **Maximum file size: 500 lines.** A file that exceeds it is subdivided into
281
+ modules. Split along a seam the code already has, and give each module a doc
282
+ comment naming what it owns. Re-export from the parent so the split is
283
+ invisible to callers. A split that only balances line counts is worse than
284
+ the long file; find the real seam.
285
+
286
+ **Prefer one-word module names** (`bridge`, `client`, `handler`,
287
+ `scheduler`). A two-word name usually means the module owns more than one
288
+ concern, or the name restates its parent's path. A compound name is right
289
+ only when the compound is the domain term.
290
+
291
+ **Order within Rust files (topological by dependencies):**
292
+ Constants → Statics → Types → Implementations → Functions → Errors (bottom)
293
+
294
+ ## Types, Signatures, and Examples
295
+
296
+ - `sig/` — public RBS signatures; `sig-private/` — internal signatures.
297
+ Every API change updates the signatures in the same commit.
298
+ - `lib/prosody/native_stubs.rb` — documented Ruby stubs for the classes and
299
+ methods the Rust extension implements. Editors and documentation tools read
300
+ them; the runtime does not (the native extension provides the real
301
+ definitions), and Steep ignores the file. When the extension's public
302
+ surface changes, update the matching stub and its YARD doc in the same
303
+ commit.
304
+ - Steep targets (`Steepfile`): `lib` checks the implementation;
305
+ `consumer_types` (`typecheck/`) verifies a payload type flows through the
306
+ public API; `typed_examples` checks every runnable example;
307
+ `negative_types` (`typecheck_negative/`) holds deliberately-invalid calls
308
+ with committed expectations — the gate fails if an expected diagnostic
309
+ disappears.
310
+ - `examples/*.rb` each carry a sibling `.rbs` signature and are type-checked
311
+ by the `typed_examples` target.
312
+ - Run `bundle exec rake rbs` to validate signatures and
313
+ `bundle exec rake steep` to type-check.
314
+
315
+ ## Testing
316
+
317
+ Test suites live in `spec/` (RSpec). Integration specs need the Docker
318
+ Compose services. Run with `make test`; write output to a file:
319
+
320
+ ```bash
321
+ make test 2>&1 | tee /tmp/rspec-output.txt
322
+ grep -i fail /tmp/rspec-output.txt
323
+ ```
324
+
325
+ **Test principles:**
326
+
327
+ - Drive tests by invariants, not by paths. Name the invariant (round-trip,
328
+ parity, idempotence) and prefer few broad tests over many narrow example
329
+ tests. Use realistic inputs, not happy-path toys.
330
+ - A test must be able to fail. When you write or convert a test, prove it can
331
+ go red once: inject the failure, watch it fail, revert.
332
+ - Never delete a test without naming, in the commit, the surviving test that
333
+ covers the same invariant at least as strongly.
334
+ - Never use `sleep` except for backpressure simulation. Wait on events,
335
+ channels, or notifications with a deadline — the deadline is a hang-guard,
336
+ never the assertion.
337
+ - Root-cause every intermittent failure. A passing re-run proves nothing.
338
+ Extract the reproducer and land it as a deterministic regression test.
339
+ - In Rust tests, use `assert` or a `Result` with `?` — never
340
+ `expect`/`unwrap`, never swallow errors.
341
+
342
+ ## Common Patterns (Rust)
343
+
344
+ - Use `parking_lot` over `std::sync`
345
+ - For concurrent hash sets/maps, use `scc` (`scc::HashSet` / `scc::HashMap`),
346
+ never a `Mutex<HashSet>` / `Mutex<HashMap>`; pair it with
347
+ `ahash::RandomState`. In async code prefer its async interface.
348
+ - Use `tokio::sync` primitives (`Notify`, channels, `select!`) for async
349
+ - Independent I/O runs concurrently, never serially. Drive N independent
350
+ reads through a bounded `buffered(N)` (order-preserving) or
351
+ `buffer_unordered(N)` (unordered). Reserve serial `await` for genuinely
352
+ dependent reads, where each result determines the next.
353
+ - Drive futures over non-tokio primitives through the cooperative budget:
354
+ wrap each per-item future with `tokio::task::coop::cooperative` inside the
355
+ producing closure, so a drain of ready items cannot starve the worker.
356
+ - Mark builders with `#[must_use]`
357
+ - Use `LazyLock` for expensive static initialization
358
+
359
+ ## Tracing / OpenTelemetry
360
+
361
+ - Instrument with `#[instrument]`, never a hand-built `info_span!` +
362
+ `.instrument(...)`. Use `skip_all` plus explicit `fields`, and `err` to
363
+ record failures on the span.
364
+ - Span level is audience: spans the user's own code causes export at info;
365
+ framework-internal spans use `level = "debug"`.
366
+ - Record unsigned integers as `i64` — the OTel layer stringifies
367
+ `u64`/`usize`. Record attribute values with `%` (Display) where the type
368
+ allows.
369
+ - Import tracing macros from `tracing` directly — never `use tracing::log::…`;
370
+ no bridge is installed, so events logged through it silently vanish.
371
+ - Never cache a `Span` — cache an `opentelemetry::Context` and recreate spans
372
+ on read. Cloning a span creates another reference to the same underlying
373
+ span; finishing one finishes all.
374
+
375
+ ## Workflows
376
+
377
+ When launching multi-agent workflows:
378
+
379
+ - Select model and effort per task by complexity — do not let every agent
380
+ inherit the session model. Never downgrade a stage whose output gates a
381
+ commit or ship decision.
382
+ - Disable the advisor in every agent prompt.
383
+ - Keep structured-output schemata trivially simple: flat objects with a few
384
+ short bounded fields; put detail in report files.
385
+
386
+ ## Research
387
+
388
+ - Automatically use context7 for code generation and library documentation.
389
+
390
+ ## CI planning
391
+
392
+ - Check Cargo Rail after each CI path or repository layout change.
393
+ - Confirm that README-only changes select documentation jobs only.
394
+ - Confirm that source changes select all required build and test jobs.
395
+ - Add `rail.toml` only when the default rules classify a path incorrectly.
data/ARCHITECTURE.md CHANGED
@@ -558,7 +558,7 @@ Understanding this architecture helps you write better Prosody applications:
558
558
  AuthorizationError # Permission issues need manual fixing
559
559
  ```
560
560
 
561
- 3. **Ensure Clean Shutdown**: Always call `unsubscribe` before exiting
561
+ 3. **Ensure Clean Shutdown**: Always call `shutdown` before exiting
562
562
  ```ruby
563
563
  # Set up a shutdown queue
564
564
  shutdown = Queue.new
@@ -575,7 +575,7 @@ Understanding this architecture helps you write better Prosody applications:
575
575
 
576
576
  # Clean shutdown
577
577
  puts "Shutting down gracefully..."
578
- client.unsubscribe
578
+ client.shutdown
579
579
  ```
580
580
 
581
581
  4. **Monitor with Traces**: Use OpenTelemetry to understand message processing
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.0](https://github.com/prosody-events/prosody-rb/compare/prosody/v0.4.0...prosody/v0.5.0) (2026-08-19)
4
+
5
+
6
+ ### Features
7
+
8
+ * add subsystem requests ([#44](https://github.com/prosody-events/prosody-rb/issues/44)) ([672c1de](https://github.com/prosody-events/prosody-rb/commit/672c1de8cbdf883c7cb41e1c82b25851057cf733))
9
+ * add typed excise records and requests ([#45](https://github.com/prosody-events/prosody-rb/issues/45)) ([1581b37](https://github.com/prosody-events/prosody-rb/commit/1581b3768a8ce1ae72677135aabfae04496ce943))
10
+ * expose published keyed state ([#36](https://github.com/prosody-events/prosody-rb/issues/36)) ([bffefa2](https://github.com/prosody-events/prosody-rb/commit/bffefa2908b0d96cc43221babfed1d5ac6554ca2))
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **logging:** flush OTel telemetry on exit ([#35](https://github.com/prosody-events/prosody-rb/issues/35)) ([531d22a](https://github.com/prosody-events/prosody-rb/commit/531d22af7b0a8af0e3335b7a706884af6b1e1833))
16
+ * **release:** exclude source-only specs from gem validation ([#33](https://github.com/prosody-events/prosody-rb/issues/33)) ([6d8d745](https://github.com/prosody-events/prosody-rb/commit/6d8d745c0ca9950643566cb11fb21090a6fdb738))
17
+
3
18
  ## [0.4.0](https://github.com/prosody-events/prosody-rb/compare/prosody/v0.3.0...prosody/v0.4.0) (2026-07-21)
4
19
 
5
20
 
data/CLAUDE.md ADDED
@@ -0,0 +1 @@
1
+ AGENTS.md
data/CONFIGURATION.md ADDED
@@ -0,0 +1,167 @@
1
+ # Configuration
2
+
3
+ Configure via constructor options or environment variables. Options fall back to environment variables when unset.
4
+
5
+ The Ruby client reports values it cannot convert to Prosody types. Prosody validates configuration semantics when the client is built.
6
+
7
+ ## Core
8
+
9
+ | Option / Environment Variable | Description | Default |
10
+ |-----------------------------------------|---------------------------------------------------|--------------|
11
+ | `bootstrap_servers` / `PROSODY_BOOTSTRAP_SERVERS` | Kafka servers to connect to | - |
12
+ | `group_id` / `PROSODY_GROUP_ID` | Consumer group name | - |
13
+ | `subscribed_topics` / `PROSODY_SUBSCRIBED_TOPICS` | Topics to read from | - |
14
+ | `allowed_events` / `PROSODY_ALLOWED_EVENTS` | Only process events matching these prefixes | (all) |
15
+ | `source_system` / `PROSODY_SOURCE_SYSTEM` | Tag for outgoing messages (prevents reprocessing)| `<group_id>` |
16
+ | `mock` / `PROSODY_MOCK` | Use in-memory Kafka for testing | false |
17
+ | `mode` / - | Processing mode: `pipeline`, `low_latency`, or `best_effort` | `pipeline` |
18
+ | - / `PROSODY_LOG` | Rust log filter, such as `info` or `prosody=debug` | `info` |
19
+
20
+ ## Requests
21
+
22
+ Requests work with the defaults on one network. Without a network name, peers always use the direct listener address.
23
+ With a network name, peers with the same name use the direct address. Other peers use the advertised connect URI.
24
+ Use a different bind address for each client that shares a host.
25
+
26
+ | Option / Environment Variable | Description | Default |
27
+ |--------------------------------|-------------|---------|
28
+ | `peer_bind_address` / `PROSODY_PEER_BIND_ADDRESS` | Socket address for the peer gRPC listener | Default network interface address on port 9099 |
29
+ | `peer_advertised_connect` / `PROSODY_PEER_ADVERTISED_CONNECT` | gRPC connect URI that peers on another network use | (none) |
30
+ | `peer_network_name` / `PROSODY_PEER_NETWORK_NAME` | Nonempty network name for direct peer routes | (none) |
31
+ | `peer_cache_capacity` / `PROSODY_PEER_CACHE_CAPACITY` | Maximum channels and peer records in each peer cache | 256 |
32
+ | `peer_registration_ttl` / `PROSODY_PEER_REGISTRATION_TTL` | Directory lease duration; use 5 seconds through 20 years | 30s |
33
+
34
+ Set `subsystem` to make this client answer requests. Without it, the client consumes messages but does not answer requests.
35
+
36
+ ## Consumer
37
+
38
+ | Option / Environment Variable | Description | Default |
39
+ |-----------------------------------------|------------------------------------------------------|------------------------|
40
+ | `max_concurrency` / `PROSODY_MAX_CONCURRENCY` | Max messages being processed simultaneously | 32 |
41
+ | `max_uncommitted` / `PROSODY_MAX_UNCOMMITTED` | Max queued messages before pausing consumption | 64 |
42
+ | `timeout` / `PROSODY_TIMEOUT` | Cancel handler if it runs longer than this | 80% of stall threshold |
43
+ | `commit_interval` / `PROSODY_COMMIT_INTERVAL` | How often to save progress to Kafka | 1s |
44
+ | `poll_interval` / `PROSODY_POLL_INTERVAL` | How often to fetch new messages from Kafka | 100ms |
45
+ | `shutdown_timeout` / `PROSODY_SHUTDOWN_TIMEOUT` | Shutdown budget; handlers run freely until cancellation fires near the end of the timeout | 30s |
46
+ | `stall_threshold` / `PROSODY_STALL_THRESHOLD` | Report unhealthy if no progress for this long | 5m |
47
+ | `probe_port` / `PROSODY_PROBE_PORT` | HTTP port for health checks; use `false`, `:disabled`, or the environment value `none` to disable | 8000 |
48
+ | - / `PROSODY_STATISTICS_INTERVAL` | How often librdkafka reports client statistics; must be between 1ms and 24h | 5s |
49
+ | `failure_topic` / `PROSODY_FAILURE_TOPIC` | Send unprocessable messages here (dead letter queue) | - |
50
+ | `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 |
51
+ | `idempotence_version` / `PROSODY_IDEMPOTENCE_VERSION` | Version string for cache-busting dedup hashes | 1 |
52
+ | `idempotence_ttl` / `PROSODY_IDEMPOTENCE_TTL` | TTL for dedup records in Cassandra | 7d (604800 seconds) |
53
+ | `slab_size` / `PROSODY_SLAB_SIZE` | Timer storage granularity (rarely needs changing) | 1h |
54
+ | `message_spans` / `PROSODY_MESSAGE_SPANS` | Span linking for message execution: `child` (child-of) or `follows_from` | `child` |
55
+ | `timer_spans` / `PROSODY_TIMER_SPANS` | Span linking for timer execution: `child` (child-of) or `follows_from` | `follows_from` |
56
+
57
+ ## Producer
58
+
59
+ | Option / Environment Variable | Description | Default |
60
+ |-----------------------------------------|---------------------------------|---------|
61
+ | `send_timeout` / `PROSODY_SEND_TIMEOUT` | Give up sending after this long | 1s |
62
+
63
+ ## Retry
64
+
65
+ Retry backoff applies in pipeline and low-latency modes. `max_retries` controls how many retries low-latency mode performs before routing the failure to `failure_topic`. Pipeline mode uses deferral and does not use this limit.
66
+
67
+ | Option / Environment Variable | Description | Default |
68
+ |-----------------------------------------|-----------------------------------|---------|
69
+ | `max_retries` / `PROSODY_MAX_RETRIES` | Low-latency retries before routing to the failure topic | 3 |
70
+ | `retry_base` / `PROSODY_RETRY_BASE` | Wait this long before first retry | 20ms |
71
+ | `max_retry_delay` / `PROSODY_RETRY_MAX_DELAY` | Never wait longer than this | 5m |
72
+
73
+ ## Deferral (Pipeline Mode)
74
+
75
+ | Option / Environment Variable | Description | Default |
76
+ |-----------------------------------------|---------------------------------------------------|---------|
77
+ | `defer_enabled` / `PROSODY_DEFER_ENABLED` | Enable deferral for new messages | true |
78
+ | `defer_base` / `PROSODY_DEFER_BASE` | Wait this long before first deferred retry | 1s |
79
+ | `defer_max_delay` / `PROSODY_DEFER_MAX_DELAY` | Never wait longer than this | 24h |
80
+ | `defer_failure_threshold` / `PROSODY_DEFER_FAILURE_THRESHOLD` | Disable deferral when failure rate exceeds this | 0.9 |
81
+ | `defer_failure_window` / `PROSODY_DEFER_FAILURE_WINDOW` | Measure failure rate over this time window | 5m |
82
+ | `defer_store_cache_size` / `PROSODY_DEFER_STORE_CACHE_SIZE` | Maximum deferred store cache entries per Cassandra defer store | 8192 |
83
+
84
+ ## Kafka Message Loader (All Modes)
85
+
86
+ The shared loader resolves Kafka messages for deferral and keyed state:
87
+
88
+ | Option / Environment Variable | Description | Default |
89
+ |--------------------------------|-------------|---------|
90
+ | `loader_cache_size` / `PROSODY_LOADER_CACHE_SIZE` | Maximum messages retained by the shared Kafka loader | 1024 |
91
+ | `loader_seek_timeout` / `PROSODY_LOADER_SEEK_TIMEOUT` | Timeout for Kafka loader seek operations | 30s |
92
+ | `loader_discard_threshold` / `PROSODY_LOADER_DISCARD_THRESHOLD` | Sequential-read distance before the loader seeks | 100 |
93
+
94
+ ## Monopolization Detection (Pipeline Mode)
95
+
96
+ | Option / Environment Variable | Description | Default |
97
+ |-----------------------------------------|-----------------------------------------|---------|
98
+ | `monopolization_enabled` / `PROSODY_MONOPOLIZATION_ENABLED` | Enable hot key protection | true |
99
+ | `monopolization_threshold` / `PROSODY_MONOPOLIZATION_THRESHOLD` | Max handler time as fraction of window | 0.9 |
100
+ | `monopolization_window` / `PROSODY_MONOPOLIZATION_WINDOW` | Measurement window | 5m |
101
+ | `monopolization_cache_size` / `PROSODY_MONOPOLIZATION_CACHE_SIZE` | Max distinct keys to track | 8192 |
102
+
103
+ ## Fair Scheduling (All Modes)
104
+
105
+ | Option / Environment Variable | Description | Default |
106
+ |-----------------------------------------|------------------------------------------------------------------|---------|
107
+ | `scheduler_failure_weight` / `PROSODY_SCHEDULER_FAILURE_WEIGHT` | Fraction of processing time reserved for retries | 0.3 |
108
+ | `scheduler_max_wait` / `PROSODY_SCHEDULER_MAX_WAIT` | Messages waiting this long get maximum priority | 2m |
109
+ | `scheduler_wait_weight` / `PROSODY_SCHEDULER_WAIT_WEIGHT` | Priority boost for waiting messages (higher = more aggressive) | 200.0 |
110
+ | `scheduler_cache_size` / `PROSODY_SCHEDULER_CACHE_SIZE` | Max distinct keys to track | 8192 |
111
+
112
+ ## Telemetry
113
+
114
+ Prosody emits message, timer, and producer lifecycle events to a Kafka topic for observability:
115
+
116
+ | Option / Environment Variable | Description | Default |
117
+ |-----------------------------------------|------------------------------------------------|----------------------------|
118
+ | `telemetry_topic` / `PROSODY_TELEMETRY_TOPIC` | Kafka topic to produce telemetry events to | `prosody.telemetry-events` |
119
+ | `telemetry_enabled` / `PROSODY_TELEMETRY_ENABLED` | Enable or disable the telemetry emitter | true |
120
+
121
+ Mock mode disables telemetry automatically, regardless of `telemetry_enabled`.
122
+
123
+ ## Cassandra
124
+
125
+ Persistent storage for timers, deferral, deduplication, and keyed state. It is not needed when `mock: true`.
126
+
127
+ | Option / Environment Variable | Description | Default |
128
+ |-----------------------------------------|------------------------------------|---------|
129
+ | `cassandra_nodes` / `PROSODY_CASSANDRA_NODES` | Servers to connect to (host:port) | - |
130
+ | `cassandra_keyspace` / `PROSODY_CASSANDRA_KEYSPACE` | Keyspace name | prosody |
131
+ | `cassandra_user` / `PROSODY_CASSANDRA_USER` | Username | - |
132
+ | `cassandra_password` / `PROSODY_CASSANDRA_PASSWORD` | Password | - |
133
+ | `cassandra_datacenter` / `PROSODY_CASSANDRA_DATACENTER` | Prefer this datacenter for queries | - |
134
+ | `cassandra_rack` / `PROSODY_CASSANDRA_RACK` | Prefer this rack for queries | - |
135
+ | `cassandra_retention` / `PROSODY_CASSANDRA_RETENTION` | Delete data older than this | 1y |
136
+
137
+ ## Keyed State
138
+
139
+ Register keyed-state collections before you subscribe. Persistence is backed by Cassandra and is not needed when `mock: true`. See [Keyed State](README.md#keyed-state) for handler usage. Where an option and an environment variable are paired, an explicitly set option wins. Otherwise, the environment variable applies, then the default.
140
+
141
+ | Option / Environment Variable | Description | Default |
142
+ |-------------------------------|-------------|---------|
143
+ | `state_collections` / - | Keyed-state collections to register before subscribe (array of definitions or config hashes; duplicate names rejected) | (none) |
144
+ | `subsystem` / `PROSODY_SUBSYSTEM` | Subsystem name used to advertise JSON collections whose definitions set `published: true` | (none) |
145
+ | `state_cache_dir` / `PROSODY_STATE_CACHE_DIR` | Disk workspace for the local keyed-state cache; each live client needs its own directory. Set a mounted path in production | per-client temp dir |
146
+ | `state_owned_cache_size` / `PROSODY_STATE_OWNED_CACHE_SIZE` | Capacity of the owning keyed-state cache; accepts sizes such as `64 MiB` or `500 MB` | storage-engine default |
147
+ | `state_read_cache_size` / `PROSODY_STATE_READ_CACHE_SIZE` | Capacity of the published-state read cache; accepts sizes such as `1 MiB` | `state_owned_cache_size` or `PROSODY_STATE_OWNED_CACHE_SIZE` when set; otherwise 1 MiB |
148
+ | `state_read_cache` / `PROSODY_STATE_READ_CACHE_TTL` | Default published-read cache TTL. Use `false` or the environment value `none` to bypass the cache | 5s |
149
+ | `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 |
150
+
151
+ Prefer the definition constructors from the [API reference](README.md#api-reference). They serialize into `state_collections`, so you can reuse the same object with `context.state`. Each entry has these fields:
152
+
153
+ Published collections require `subsystem`. Keep it configured for one deployment after removing `published: true` so readers can observe the collection's retirement.
154
+
155
+ | Field | Description | Default |
156
+ |-------|-------------|---------|
157
+ | `name` | Collection name; non-empty and unique within the client | (required) |
158
+ | `kind` | `"value"`, `"map"`, or `"deque"` | (required) |
159
+ | `payload` | `"json"` (JSON values) or `"message"` (the full Kafka message the handler received) | (required) |
160
+ | `ttl_seconds` | Per-write TTL in whole seconds (at least 1; must exceed the recovery delay) | (none) |
161
+ | `read_uncommitted` | Opt out of transactional staging | false |
162
+ | `published` | Allow read-only access from other consumer groups; JSON collections only | false |
163
+ | `read_cache` | Published-read cache override: a positive duration, `false`, or inherit when omitted | inherit |
164
+ | `keyset_limit` | Map-only; ordered-scan bound in `0..=4096` (`0` disables ordered-scan tracking) | 128 |
165
+ | `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 |
166
+
167
+ Constructors set these via keyword arguments (`ttl:`, `keyset_limit:`, `capacity:`, `read_uncommitted:`, `published:`, `read_cache:`). `read_cache` is a positive duration in seconds, `false` to bypass the cache, or `nil` to inherit the client default.