prosody 0.5.1 → 0.6.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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/.config/rail.toml +26 -0
  3. data/.release-please-manifest.json +1 -1
  4. data/.ruby-version +1 -1
  5. data/.taplo.toml +1 -1
  6. data/AGENTS.md +29 -15
  7. data/CHANGELOG.md +7 -0
  8. data/CONFIGURATION.md +19 -16
  9. data/Cargo.lock +254 -235
  10. data/Cargo.toml +11 -8
  11. data/README.md +100 -23
  12. data/examples/keyed_state.rb +10 -2
  13. data/examples/keyed_state.rbs +1 -0
  14. data/ext/prosody/Cargo.toml +1 -1
  15. data/ext/prosody/src/admin.rs +36 -36
  16. data/ext/prosody/src/bridge/mod.rs +5 -10
  17. data/ext/prosody/src/client/config/connections.rs +170 -0
  18. data/ext/prosody/src/client/config/middleware.rs +184 -0
  19. data/ext/prosody/src/client/config/mod.rs +396 -0
  20. data/ext/prosody/src/client/config/state.rs +323 -0
  21. data/ext/prosody/src/client/mod.rs +31 -117
  22. data/ext/prosody/src/client/readers.rs +106 -0
  23. data/ext/prosody/src/client/request.rs +2 -2
  24. data/ext/prosody/src/client/support.rs +13 -32
  25. data/ext/prosody/src/gvl.rs +8 -6
  26. data/ext/prosody/src/handler/{context.rs → context/mod.rs} +36 -150
  27. data/ext/prosody/src/handler/context/vending.rs +138 -0
  28. data/ext/prosody/src/handler/message.rs +36 -0
  29. data/ext/prosody/src/handler/mod.rs +18 -8
  30. data/ext/prosody/src/handler/state/deque.rs +144 -0
  31. data/ext/prosody/src/handler/state/mod.rs +163 -268
  32. data/ext/prosody/src/handler/state/query.rs +264 -0
  33. data/ext/prosody/src/handler/state/registration.rs +15 -86
  34. data/ext/prosody/src/handler/state/scan.rs +33 -141
  35. data/ext/prosody/src/handler/state/set.rs +98 -0
  36. data/ext/prosody/src/lib.rs +54 -36
  37. data/ext/prosody/src/logging.rs +6 -6
  38. data/ext/prosody/src/published.rs +171 -152
  39. data/ext/prosody/src/scheduler/result.rs +2 -1
  40. data/ext/prosody/src/util.rs +65 -3
  41. data/lib/prosody/client.rb +32 -0
  42. data/lib/prosody/configuration.rb +20 -12
  43. data/lib/prosody/demand.rb +27 -0
  44. data/lib/prosody/native_stubs/client.rb +157 -0
  45. data/lib/prosody/native_stubs/context.rb +178 -0
  46. data/lib/prosody/native_stubs/message.rb +133 -0
  47. data/lib/prosody/native_stubs.rb +11 -956
  48. data/lib/prosody/state/deque.rb +253 -0
  49. data/lib/prosody/state/map.rb +313 -0
  50. data/lib/prosody/state/set.rb +121 -0
  51. data/lib/prosody/state/value.rb +58 -0
  52. data/lib/prosody/state.rb +157 -677
  53. data/lib/prosody/version.rb +1 -1
  54. data/lib/prosody.rb +2 -1
  55. data/sig/configuration.rbs +19 -10
  56. data/sig/prosody.rbs +37 -4
  57. data/sig/published.rbs +102 -0
  58. data/sig/state.rbs +109 -122
  59. data/typecheck/payload_types.rb +12 -0
  60. data/typecheck/payload_types.rbs +1 -0
  61. metadata +30 -11
  62. data/ext/prosody/src/client/config.rs +0 -1300
@@ -0,0 +1,396 @@
1
+ //! Conversion of the Ruby client configuration into Prosody configuration.
2
+ //!
3
+ //! [`NativeConfiguration`] holds every option the Ruby side can set. This
4
+ //! module converts it into [`ConsumerBuilders`] and the processing [`Mode`].
5
+ //! The submodules own the other conversions: `connections` for the Kafka,
6
+ //! Cassandra, and telemetry builders, `middleware` for the middleware
7
+ //! builders, and `state` for keyed state.
8
+
9
+ use crate::util::seconds;
10
+ use prosody::PeerConfiguration;
11
+ use prosody::PeerEndpoint;
12
+ use prosody::consumer::ConsumerConfigurationBuilder;
13
+ use prosody::consumer::SpanRelation;
14
+ use prosody::high_level::ConsumerBuilders;
15
+ use prosody::high_level::mode::Mode;
16
+ use prosody::loader::KafkaLoaderConfiguration;
17
+ use serde::{Deserialize, Deserializer};
18
+ use serde_untagged::UntaggedEnumVisitor;
19
+ pub(crate) use state::{ReadCacheConfig, read_cache_policy};
20
+ use state::{StateCollectionConfig, build_keyed_state_config};
21
+ use std::net::SocketAddr;
22
+
23
+ mod connections;
24
+ mod middleware;
25
+ mod state;
26
+
27
+ /// Configuration structure for the Prosody client that maps Ruby configuration
28
+ /// values to their native Rust equivalents.
29
+ ///
30
+ /// This structure contains all possible configuration options that can be
31
+ /// provided by the Ruby side, which are then converted to the appropriate
32
+ /// Prosody configuration builder types.
33
+ #[derive(Clone, Default, Deserialize)]
34
+ pub struct NativeConfiguration {
35
+ /// List of Kafka bootstrap server addresses
36
+ bootstrap_servers: Option<Vec<String>>,
37
+
38
+ /// Whether to use mock mode (for testing)
39
+ mock: Option<bool>,
40
+
41
+ /// Maximum time to wait for a send operation to complete (in seconds)
42
+ send_timeout: Option<f64>,
43
+
44
+ /// Kafka consumer group ID
45
+ group_id: Option<String>,
46
+
47
+ /// Global shared cache capacity across all partitions for message
48
+ /// deduplication
49
+ idempotence_cache_size: Option<u32>,
50
+
51
+ /// Version string for cache-busting deduplication hashes
52
+ idempotence_version: Option<String>,
53
+
54
+ /// TTL for deduplication records in Cassandra (in seconds)
55
+ idempotence_ttl: Option<f64>,
56
+
57
+ /// List of Kafka topics to subscribe to
58
+ subscribed_topics: Option<Vec<String>>,
59
+
60
+ /// List of event types that the consumer is allowed to process
61
+ allowed_events: Option<Vec<String>>,
62
+
63
+ /// Identifier for the system producing messages
64
+ source_system: Option<String>,
65
+
66
+ /// Maximum number of concurrent message processing tasks
67
+ max_concurrency: Option<u32>,
68
+
69
+ /// Maximum number of messages to process before committing offsets
70
+ max_uncommitted: Option<u32>,
71
+
72
+ /// Threshold in seconds after which a stalled consumer is detected
73
+ stall_threshold: Option<f64>,
74
+
75
+ /// Maximum time to wait for a clean shutdown (in seconds)
76
+ shutdown_timeout: Option<f64>,
77
+
78
+ /// Interval between Kafka poll operations (in seconds)
79
+ poll_interval: Option<f64>,
80
+
81
+ /// Interval between offset commit operations (in seconds)
82
+ commit_interval: Option<f64>,
83
+
84
+ /// Interval between librdkafka statistics reports (in seconds)
85
+ statistics_interval: Option<f64>,
86
+
87
+ /// Operation mode of the client (`pipeline`, `low_latency`, `best_effort`)
88
+ mode: Option<String>,
89
+
90
+ /// Base delay for retry operations (in seconds)
91
+ retry_base: Option<f64>,
92
+
93
+ /// Maximum number of retry attempts
94
+ max_retries: Option<u32>,
95
+
96
+ /// Maximum delay between retries (in seconds)
97
+ max_retry_delay: Option<f64>,
98
+
99
+ /// Topic to send failed messages to
100
+ failure_topic: Option<String>,
101
+
102
+ /// Configuration for the health probe port
103
+ probe_port: Option<ProbePort>,
104
+
105
+ /// List of Cassandra contact nodes (hostnames or IPs)
106
+ cassandra_nodes: Option<Vec<String>>,
107
+
108
+ /// Keyspace used for persistent Prosody data in Cassandra.
109
+ cassandra_keyspace: Option<String>,
110
+
111
+ /// Preferred datacenter for Cassandra query routing
112
+ cassandra_datacenter: Option<String>,
113
+
114
+ /// Preferred rack identifier for Cassandra topology-aware routing
115
+ cassandra_rack: Option<String>,
116
+
117
+ /// Username for authenticating with Cassandra
118
+ cassandra_user: Option<String>,
119
+
120
+ /// Password for authenticating with Cassandra
121
+ cassandra_password: Option<String>,
122
+
123
+ /// Retention period for persistent timer and deferral data in Cassandra,
124
+ /// in seconds.
125
+ cassandra_retention: Option<f64>,
126
+
127
+ /// Timer slab partitioning duration in seconds.
128
+ /// Controls how timers are grouped for storage and retrieval.
129
+ slab_size: Option<f64>,
130
+
131
+ // Scheduler configuration
132
+ /// Target proportion of execution time for failure/retry task processing
133
+ /// (0.0 to 1.0). Controls bandwidth allocation between Normal and
134
+ /// Failure task classes.
135
+ scheduler_failure_weight: Option<f64>,
136
+
137
+ /// Wait duration (in seconds) at which urgency boost reaches maximum
138
+ /// intensity.
139
+ scheduler_max_wait: Option<f64>,
140
+
141
+ /// Maximum urgency boost (in seconds of virtual time) for waiting tasks.
142
+ scheduler_wait_weight: Option<f64>,
143
+
144
+ /// Cache capacity for tracking per-key virtual time in the scheduler.
145
+ scheduler_cache_size: Option<u32>,
146
+
147
+ // Monopolization configuration
148
+ /// Whether monopolization detection is enabled.
149
+ monopolization_enabled: Option<bool>,
150
+
151
+ /// Threshold for monopolization detection (0.0 to 1.0).
152
+ monopolization_threshold: Option<f64>,
153
+
154
+ /// Rolling window duration (in seconds) for monopolization detection.
155
+ monopolization_window: Option<f64>,
156
+
157
+ /// Cache size for tracking key execution intervals.
158
+ monopolization_cache_size: Option<u32>,
159
+
160
+ // Defer configuration
161
+ /// Whether deferral is enabled for new messages.
162
+ defer_enabled: Option<bool>,
163
+
164
+ /// Base exponential backoff delay for deferred retries (in seconds).
165
+ defer_base: Option<f64>,
166
+
167
+ /// Maximum delay between deferred retries (in seconds).
168
+ defer_max_delay: Option<f64>,
169
+
170
+ /// Failure rate threshold for disabling deferral (0.0 to 1.0).
171
+ defer_failure_threshold: Option<f64>,
172
+
173
+ /// Sliding window duration (in seconds) for failure rate tracking.
174
+ defer_failure_window: Option<f64>,
175
+
176
+ /// Maximum messages retained by the shared Kafka loader.
177
+ loader_cache_size: Option<u32>,
178
+
179
+ /// Maximum number of deferred store entries kept in the write-through cache
180
+ /// per Cassandra defer store.
181
+ defer_store_cache_size: Option<u32>,
182
+
183
+ /// Timeout for Kafka loader seek operations (in seconds).
184
+ loader_seek_timeout: Option<f64>,
185
+
186
+ /// Messages to read sequentially before seeking.
187
+ loader_discard_threshold: Option<i64>,
188
+
189
+ // Timeout configuration
190
+ /// Fixed timeout duration for handler execution (in seconds).
191
+ timeout: Option<f64>,
192
+
193
+ // Telemetry emitter configuration
194
+ /// Kafka topic to produce telemetry events to.
195
+ telemetry_topic: Option<String>,
196
+
197
+ /// Whether the telemetry emitter is enabled.
198
+ telemetry_enabled: Option<bool>,
199
+
200
+ // OTel span linking
201
+ /// Span linking for message execution spans (`child` or `follows_from`).
202
+ message_spans: Option<String>,
203
+
204
+ /// Span linking for timer execution spans (`child` or `follows_from`).
205
+ timer_spans: Option<String>,
206
+
207
+ /// Address for the peer listener.
208
+ peer_bind_address: Option<String>,
209
+
210
+ /// gRPC connect URI that peers use for this client.
211
+ peer_advertised_connect: Option<String>,
212
+
213
+ /// Network name used to identify direct routes.
214
+ peer_network_name: Option<String>,
215
+
216
+ /// Maximum number of peer channels and registrations held in each cache.
217
+ peer_cache_capacity: Option<usize>,
218
+
219
+ /// Duration of each peer registration lease, in seconds.
220
+ peer_registration_ttl: Option<f64>,
221
+
222
+ // Keyed-state configuration
223
+ /// Keyed-state collections to register before subscribe.
224
+ state_collections: Option<Vec<StateCollectionConfig>>,
225
+
226
+ /// Subsystem under which published collections are advertised.
227
+ subsystem: Option<String>,
228
+
229
+ /// Directory for the local keyed-state caches. Each consumer uses a new
230
+ /// subdirectory, so clients can share it. Must not be empty.
231
+ state_cache_dir: Option<String>,
232
+
233
+ /// Capacity of the owning keyed-state cache.
234
+ state_owned_cache_size: Option<String>,
235
+
236
+ /// Size at which the local keyed-state cache flushes a partition's
237
+ /// in-memory writes to disk. `None` uses `PROSODY_STATE_MEMTABLE_SIZE`,
238
+ /// or the engine default of 64 MiB when that is unset.
239
+ state_memtable_size: Option<String>,
240
+
241
+ /// Capacity of the published-state read-through cache.
242
+ state_read_cache_size: Option<String>,
243
+
244
+ /// Default cache policy for published-state reads.
245
+ state_read_cache: Option<ReadCacheConfig>,
246
+ }
247
+
248
+ /// Configuration for the health probe port.
249
+ ///
250
+ /// This enum represents the three possible states for the probe port
251
+ /// configuration:
252
+ /// - Unconfigured: The default state, where the standard configuration is used
253
+ /// - Disabled: Explicitly disables the probe port
254
+ /// - Configured: Sets the probe port to a specific port number
255
+ #[derive(Copy, Clone, Debug, Default)]
256
+ pub enum ProbePort {
257
+ /// Use default configuration
258
+ #[default]
259
+ Unconfigured,
260
+
261
+ /// Explicitly disable the probe port
262
+ Disabled,
263
+
264
+ /// Use a specific port number
265
+ Configured(u16),
266
+ }
267
+
268
+ impl<'de> Deserialize<'de> for ProbePort {
269
+ /// Reads a port number as `Configured`, `false` as `Disabled`, and `true`
270
+ /// or `nil` as `Unconfigured`.
271
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
272
+ where
273
+ D: Deserializer<'de>,
274
+ {
275
+ UntaggedEnumVisitor::new()
276
+ .u16(|port| Ok(Self::Configured(port)))
277
+ .bool(|enabled| {
278
+ if enabled {
279
+ Ok(Self::Unconfigured)
280
+ } else {
281
+ Ok(Self::Disabled)
282
+ }
283
+ })
284
+ .unit(|| Ok(Self::Unconfigured))
285
+ .deserialize(deserializer)
286
+ }
287
+ }
288
+
289
+ impl<'a> TryFrom<&'a NativeConfiguration> for Mode {
290
+ type Error = String;
291
+
292
+ /// Reads the processing mode. An unrecognized mode fails.
293
+ fn try_from(value: &'a NativeConfiguration) -> Result<Self, Self::Error> {
294
+ let Some(mode_str) = value.mode.as_deref() else {
295
+ return Ok(Mode::default());
296
+ };
297
+
298
+ match mode_str {
299
+ "pipeline" => Ok(Mode::Pipeline),
300
+ "low_latency" => Ok(Mode::LowLatency),
301
+ "best_effort" => Ok(Mode::BestEffort),
302
+ string => Err(format!("unrecognized mode: {string}")),
303
+ }
304
+ }
305
+ }
306
+
307
+ impl<'a> TryFrom<&'a NativeConfiguration> for ConsumerBuilders {
308
+ type Error = String;
309
+
310
+ /// Builds every consumer builder from the configuration.
311
+ ///
312
+ /// # Errors
313
+ ///
314
+ /// Fails on a bad setting, for example an unrecognized `message_spans` or
315
+ /// `timer_spans` value, a bad loader value, or a bad peer address.
316
+ fn try_from(config: &'a NativeConfiguration) -> Result<Self, Self::Error> {
317
+ let mut consumer: ConsumerConfigurationBuilder = config.try_into()?;
318
+
319
+ if let Some(s) = &config.message_spans {
320
+ let relation = s
321
+ .parse::<SpanRelation>()
322
+ .map_err(|e| format!("message_spans: {e}"))?;
323
+ consumer.message_spans(relation);
324
+ }
325
+
326
+ if let Some(s) = &config.timer_spans {
327
+ let relation = s
328
+ .parse::<SpanRelation>()
329
+ .map_err(|e| format!("timer_spans: {e}"))?;
330
+ consumer.timer_spans(relation);
331
+ }
332
+
333
+ // Route the shared Kafka message loader settings onto the consumer.
334
+ if config.loader_cache_size.is_some()
335
+ || config.loader_seek_timeout.is_some()
336
+ || config.loader_discard_threshold.is_some()
337
+ {
338
+ let mut loader = KafkaLoaderConfiguration::builder();
339
+
340
+ if let Some(cache_size) = &config.loader_cache_size {
341
+ loader.cache_size(*cache_size as usize);
342
+ }
343
+
344
+ if let Some(seek_timeout) = &config.loader_seek_timeout {
345
+ loader.seek_timeout(seconds("loader_seek_timeout", *seek_timeout)?);
346
+ }
347
+
348
+ if let Some(discard_threshold) = &config.loader_discard_threshold {
349
+ loader.discard_threshold(*discard_threshold);
350
+ }
351
+
352
+ consumer.loader(loader.build().map_err(|e| e.to_string())?);
353
+ }
354
+
355
+ Ok(Self {
356
+ consumer,
357
+ retry: config.try_into()?,
358
+ failure_topic: config.into(),
359
+ scheduler: config.try_into()?,
360
+ monopolization: config.try_into()?,
361
+ defer: config.try_into()?,
362
+ timeout: config.try_into()?,
363
+ dedup: config.try_into()?,
364
+ emitter: config.try_into()?,
365
+ keyed_state: build_keyed_state_config(config)?,
366
+ peer: build_peer_config(config)?,
367
+ })
368
+ }
369
+ }
370
+
371
+ fn build_peer_config(config: &NativeConfiguration) -> Result<PeerConfiguration, String> {
372
+ let mut builder = PeerConfiguration::builder();
373
+ if let Some(value) = &config.peer_bind_address {
374
+ builder.bind_address(
375
+ value
376
+ .parse::<SocketAddr>()
377
+ .map_err(|error| format!("peer_bind_address: {error}"))?,
378
+ );
379
+ }
380
+ if let Some(value) = &config.peer_advertised_connect {
381
+ builder.advertised_connect(
382
+ PeerEndpoint::try_from(value.clone())
383
+ .map_err(|error| format!("peer_advertised_connect: {error}"))?,
384
+ );
385
+ }
386
+ if let Some(value) = &config.peer_network_name {
387
+ builder.network_name(value.clone());
388
+ }
389
+ if let Some(value) = config.peer_cache_capacity {
390
+ builder.peer_cache_capacity(value);
391
+ }
392
+ if let Some(value) = config.peer_registration_ttl {
393
+ builder.registration_ttl(seconds("peer_registration_ttl", value)?);
394
+ }
395
+ builder.build().map_err(|error| error.to_string())
396
+ }