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,323 @@
1
+ //! Conversion of the keyed-state options into a [`KeyedStateConfiguration`].
2
+ //!
3
+ //! This module parses each declared state collection and registers its
4
+ //! descriptor. It also maps the cache, read cache, and subsystem options.
5
+
6
+ use super::NativeConfiguration;
7
+ use crate::util::seconds;
8
+ use prosody::consumer::KeyedStateConfiguration;
9
+ use prosody::consumer::kafka_state::{message_deque_state, message_map_state, message_state};
10
+ use prosody::loader::KafkaLoader;
11
+ use prosody::state::ReadCachePolicy;
12
+ use prosody::state::descriptor::{StateDescriptor, deque_state, map_state, set_state, value_state};
13
+ use prosody::state::order_codec::Utf8KeyCodec;
14
+ use prosody::subsystem::SubsystemName;
15
+ use prosody::timers::duration::CompactDuration;
16
+ use prosody::{ByteSize, JsonCodec};
17
+ use serde::Deserialize;
18
+ use std::num::NonZeroUsize;
19
+ use std::path::PathBuf;
20
+
21
+ /// Declares one keyed-state collection to register before subscribe.
22
+ #[derive(Clone, Debug, Deserialize)]
23
+ pub(super) struct StateCollectionConfig {
24
+ /// The collection name. Prosody requires it to be non-empty and unique.
25
+ name: String,
26
+
27
+ /// The collection kind: `"value"`, `"map"`, `"set"`, or `"deque"`.
28
+ kind: String,
29
+
30
+ /// The item payload: `"json"` or `"message"`. A set stores membership
31
+ /// only, so it has no payload.
32
+ payload: Option<String>,
33
+
34
+ /// Optional per-write TTL in whole seconds.
35
+ ttl_seconds: Option<u32>,
36
+
37
+ /// Optional opt-out of transactional staging.
38
+ read_uncommitted: Option<bool>,
39
+
40
+ /// Whether other consumer groups may read this JSON or set collection.
41
+ published: Option<bool>,
42
+
43
+ /// Optional map or set keyset bound (`0..=4096`). Prosody enforces the
44
+ /// ceiling.
45
+ keyset_limit: Option<usize>,
46
+
47
+ /// Optional deque-only window capacity. Runtime-only and not persisted.
48
+ capacity: Option<NonZeroUsize>,
49
+ }
50
+
51
+ /// A read cache option: `false` disables the cache, and a number sets the
52
+ /// TTL in seconds.
53
+ #[derive(Clone, Copy, Debug, Deserialize)]
54
+ #[serde(untagged)]
55
+ pub(crate) enum ReadCacheConfig {
56
+ Disabled(bool),
57
+ Ttl(f64),
58
+ }
59
+
60
+ /// The kind of a keyed-state collection.
61
+ enum CollectionKind {
62
+ /// A single-value collection.
63
+ Value,
64
+ /// A `String`-keyed ordered map.
65
+ Map,
66
+ /// A presence-only ordered set of `String` members.
67
+ Set,
68
+ /// A deque.
69
+ Deque,
70
+ }
71
+
72
+ /// The item payload of a value, map, or deque collection.
73
+ enum CollectionPayload {
74
+ /// JSON values.
75
+ Json,
76
+ /// The full Kafka message the handler received.
77
+ Message,
78
+ }
79
+
80
+ /// Parses a collection-kind token.
81
+ ///
82
+ /// # Errors
83
+ ///
84
+ /// Returns a permanent-category error naming the field if the token is not
85
+ /// `"value"`, `"map"`, `"set"`, or `"deque"`.
86
+ fn parse_kind(index: usize, kind: &str) -> Result<CollectionKind, String> {
87
+ match kind {
88
+ "value" => Ok(CollectionKind::Value),
89
+ "map" => Ok(CollectionKind::Map),
90
+ "set" => Ok(CollectionKind::Set),
91
+ "deque" => Ok(CollectionKind::Deque),
92
+ other => Err(format!(
93
+ "state_collections[{index}].kind: expected \"value\", \"map\", \"set\", or \"deque\", \
94
+ got {other:?}"
95
+ )),
96
+ }
97
+ }
98
+
99
+ /// Parses a collection-payload token.
100
+ ///
101
+ /// # Errors
102
+ ///
103
+ /// Returns a permanent-category error naming the field if the token is not
104
+ /// `"json"` or `"message"`.
105
+ fn parse_payload(index: usize, payload: &str) -> Result<CollectionPayload, String> {
106
+ match payload {
107
+ "json" => Ok(CollectionPayload::Json),
108
+ "message" => Ok(CollectionPayload::Message),
109
+ other => Err(format!(
110
+ "state_collections[{index}].payload: expected \"json\" or \"message\", got {other:?}"
111
+ )),
112
+ }
113
+ }
114
+
115
+ /// Applies the options that every collection kind takes: TTL, commit mode,
116
+ /// and publication.
117
+ fn with_def<D: StateDescriptor>(descriptor: D, collection: &StateCollectionConfig) -> D {
118
+ let mut descriptor = descriptor;
119
+ if let Some(ttl) = collection.ttl_seconds {
120
+ descriptor = descriptor.ttl(CompactDuration::new(ttl));
121
+ }
122
+ if collection.read_uncommitted == Some(true) {
123
+ descriptor = descriptor.read_uncommitted();
124
+ }
125
+ if let Some(published) = collection.published {
126
+ descriptor = descriptor.published(published);
127
+ }
128
+ descriptor
129
+ }
130
+
131
+ /// Maps one collection into its descriptor. Values, maps, and deques hold
132
+ /// JSON or messages. Sets have no payload.
133
+ ///
134
+ /// # Errors
135
+ ///
136
+ /// Returns a permanent-category error when a host value cannot be mapped into
137
+ /// a Prosody type.
138
+ fn register_state_collection(
139
+ keyed: &mut KeyedStateConfiguration,
140
+ index: usize,
141
+ collection: &StateCollectionConfig,
142
+ ) -> Result<(), String> {
143
+ let kind = parse_kind(index, &collection.kind)?;
144
+ let payload = collection
145
+ .payload
146
+ .as_deref()
147
+ .map(|payload| parse_payload(index, payload))
148
+ .transpose()?;
149
+ let keyset_limit = keyset_limit(collection.keyset_limit, &kind, index)?;
150
+ let capacity = capacity(collection.capacity, &kind, index)?;
151
+
152
+ let name = collection.name.as_str();
153
+ match (kind, payload) {
154
+ (CollectionKind::Value, Some(CollectionPayload::Json)) => {
155
+ let _ = keyed.register(with_def(value_state::<JsonCodec>(name), collection));
156
+ }
157
+ (CollectionKind::Map, Some(CollectionPayload::Json)) => {
158
+ let mut descriptor = with_def(map_state::<Utf8KeyCodec, JsonCodec>(name), collection);
159
+ if let Some(limit) = keyset_limit {
160
+ descriptor = descriptor.keyset_limit(limit);
161
+ }
162
+ let _ = keyed.register(descriptor);
163
+ }
164
+ (CollectionKind::Deque, Some(CollectionPayload::Json)) => {
165
+ let mut descriptor = with_def(deque_state::<JsonCodec>(name), collection);
166
+ if let Some(capacity) = capacity {
167
+ descriptor = descriptor.capacity(capacity);
168
+ }
169
+ let _ = keyed.register(descriptor);
170
+ }
171
+ (CollectionKind::Value, Some(CollectionPayload::Message)) => {
172
+ let descriptor = message_state::<KafkaLoader<JsonCodec>>(name);
173
+ let _ = keyed.register(with_def(descriptor, collection));
174
+ }
175
+ (CollectionKind::Map, Some(CollectionPayload::Message)) => {
176
+ let descriptor = message_map_state::<Utf8KeyCodec, KafkaLoader<JsonCodec>>(name);
177
+ let mut descriptor = with_def(descriptor, collection);
178
+ if let Some(limit) = keyset_limit {
179
+ descriptor = descriptor.keyset_limit(limit);
180
+ }
181
+ let _ = keyed.register(descriptor);
182
+ }
183
+ (CollectionKind::Deque, Some(CollectionPayload::Message)) => {
184
+ let descriptor = message_deque_state::<KafkaLoader<JsonCodec>>(name);
185
+ let mut descriptor = with_def(descriptor, collection);
186
+ if let Some(capacity) = capacity {
187
+ descriptor = descriptor.capacity(capacity);
188
+ }
189
+ let _ = keyed.register(descriptor);
190
+ }
191
+ (CollectionKind::Set, None) => {
192
+ let mut descriptor = with_def(set_state::<Utf8KeyCodec>(name), collection);
193
+ if let Some(limit) = keyset_limit {
194
+ descriptor = descriptor.keyset_limit(limit);
195
+ }
196
+ let _ = keyed.register(descriptor);
197
+ }
198
+ (CollectionKind::Set, Some(_)) => {
199
+ return Err(format!(
200
+ "state_collections[{index}].payload: omit it for a set collection"
201
+ ));
202
+ }
203
+ (CollectionKind::Value | CollectionKind::Map | CollectionKind::Deque, None) => {
204
+ return Err(format!(
205
+ "state_collections[{index}].payload: required for value, map, and deque \
206
+ collections"
207
+ ));
208
+ }
209
+ }
210
+
211
+ Ok(())
212
+ }
213
+
214
+ /// Rejects a keyset limit on a collection that is not a map or a set.
215
+ fn keyset_limit(
216
+ value: Option<usize>,
217
+ kind: &CollectionKind,
218
+ index: usize,
219
+ ) -> Result<Option<usize>, String> {
220
+ if value.is_some() && !matches!(kind, CollectionKind::Map | CollectionKind::Set) {
221
+ return Err(format!(
222
+ "state_collections[{index}].keyset_limit: only valid for map and set collections"
223
+ ));
224
+ }
225
+ Ok(value)
226
+ }
227
+
228
+ /// Rejects a capacity on a collection that is not a deque.
229
+ fn capacity(
230
+ value: Option<NonZeroUsize>,
231
+ kind: &CollectionKind,
232
+ index: usize,
233
+ ) -> Result<Option<NonZeroUsize>, String> {
234
+ if value.is_some() && !matches!(kind, CollectionKind::Deque) {
235
+ return Err(format!(
236
+ "state_collections[{index}].capacity: only valid for deque collections"
237
+ ));
238
+ }
239
+ Ok(value)
240
+ }
241
+
242
+ /// Converts a read cache option into a core policy. An absent option inherits
243
+ /// the default.
244
+ ///
245
+ /// # Errors
246
+ ///
247
+ /// Returns an error that names `option` for `true` or an invalid duration.
248
+ pub(crate) fn read_cache_policy(
249
+ option: &str,
250
+ config: Option<ReadCacheConfig>,
251
+ ) -> Result<ReadCachePolicy, String> {
252
+ match config {
253
+ None => Ok(ReadCachePolicy::Inherit),
254
+ Some(ReadCacheConfig::Disabled(false)) => Ok(ReadCachePolicy::Disabled),
255
+ Some(ReadCacheConfig::Disabled(true)) => Err(format!(
256
+ "{option}: true is ambiguous; use a duration or false"
257
+ )),
258
+ Some(ReadCacheConfig::Ttl(value)) => seconds(option, value).map(ReadCachePolicy::Ttl),
259
+ }
260
+ }
261
+
262
+ /// Builds the `KeyedStateConfiguration` by mapping each declared collection.
263
+ /// The normal Prosody construction path validates the result.
264
+ ///
265
+ /// # Errors
266
+ ///
267
+ /// Returns an error if a host value cannot be mapped.
268
+ pub(super) fn build_keyed_state_config(
269
+ config: &NativeConfiguration,
270
+ ) -> Result<KeyedStateConfiguration, String> {
271
+ let mut builder = KeyedStateConfiguration::builder();
272
+
273
+ if let Some(dir) = &config.state_cache_dir {
274
+ builder.cache_dir(PathBuf::from(dir));
275
+ }
276
+
277
+ if let Some(size) = &config.state_owned_cache_size {
278
+ let size = size
279
+ .parse::<ByteSize>()
280
+ .map_err(|error| format!("state_owned_cache_size: {error}"))?;
281
+ builder.owned_cache_size(Some(size));
282
+ }
283
+
284
+ if let Some(size) = &config.state_memtable_size {
285
+ let size = size
286
+ .parse::<ByteSize>()
287
+ .map_err(|error| format!("state_memtable_size: {error}"))?;
288
+ builder.memtable_size(Some(size));
289
+ }
290
+
291
+ if let Some(size) = &config.state_read_cache_size {
292
+ let size = size
293
+ .parse::<ByteSize>()
294
+ .map_err(|error| format!("state_read_cache_size: {error}"))?;
295
+ builder.read_cache_size(Some(size));
296
+ }
297
+
298
+ match read_cache_policy("state_read_cache", config.state_read_cache)? {
299
+ ReadCachePolicy::Inherit => {}
300
+ ReadCachePolicy::Disabled => {
301
+ builder.read_cache_ttl(None);
302
+ }
303
+ ReadCachePolicy::Ttl(ttl) => {
304
+ builder.read_cache_ttl(Some(ttl));
305
+ }
306
+ }
307
+
308
+ if let Some(subsystem) = &config.subsystem {
309
+ builder.subsystem(Some(
310
+ SubsystemName::try_new(subsystem).map_err(|error| error.to_string())?,
311
+ ));
312
+ }
313
+
314
+ let mut keyed = builder.build().map_err(|error| error.to_string())?;
315
+
316
+ if let Some(collections) = &config.state_collections {
317
+ for (index, collection) in collections.iter().enumerate() {
318
+ register_state_collection(&mut keyed, index, collection)?;
319
+ }
320
+ }
321
+
322
+ Ok(keyed)
323
+ }
@@ -13,9 +13,8 @@
13
13
  use crate::bridge::Bridge;
14
14
  use crate::client::config::NativeConfiguration;
15
15
  use crate::handler::RubyHandler;
16
- use crate::published::{NativePublishedDeque, NativePublishedMap, NativePublishedValue};
17
16
  use crate::tracing_util::extract_opentelemetry_context;
18
- use crate::util::ensure_runtime_context;
17
+ use crate::util::{ForkGuard, ensure_runtime_context};
19
18
  use crate::{BRIDGE, ROOT_MOD, id};
20
19
  use educe::Educe;
21
20
  use futures::FutureExt;
@@ -28,10 +27,9 @@ use magnus::{
28
27
  use opentelemetry::propagation::TextMapCompositePropagator;
29
28
  use prosody::cassandra::config::CassandraConfigurationBuilder;
30
29
  use prosody::high_level::ConsumerBuilders;
31
- use prosody::high_level::erased::{
32
- ErasedConsumerState, ErasedReadCache, SharedHighLevelClient, new_erased,
33
- };
30
+ use prosody::high_level::erased::{ErasedConsumerState, SharedHighLevelClient, new_erased};
34
31
  use prosody::high_level::mode::Mode;
32
+ use prosody::producer::ProducerConfigurationBuilder;
35
33
  use prosody::propagator::new_propagator;
36
34
  use prosody::requester::ResponseError;
37
35
  use prosody::subsystem::SubsystemName;
@@ -45,11 +43,12 @@ use tracing_opentelemetry::OpenTelemetrySpanExt;
45
43
 
46
44
  /// Configuration types and conversion between Ruby and Rust representations
47
45
  mod config;
46
+ mod readers;
48
47
  mod request;
49
48
  mod support;
50
49
 
51
50
  pub use support::init;
52
- use support::{read_cache, response_error, shutdown, validate_handler};
51
+ use support::{response_error, shutdown, validate_handler};
53
52
 
54
53
  type Shutdown = Shared<BoxFuture<'static, Result<(), Arc<str>>>>;
55
54
 
@@ -72,8 +71,8 @@ pub struct Client {
72
71
  bridge: Bridge,
73
72
  /// OpenTelemetry propagator for distributed tracing
74
73
  propagator: Arc<TextMapCompositePropagator>,
75
- /// PID at construction time, used to detect post-fork usage
76
- pid: u32,
74
+ /// Refuses use in a forked child process
75
+ fork: ForkGuard,
77
76
  }
78
77
 
79
78
  impl Client {
@@ -108,16 +107,12 @@ impl Client {
108
107
  };
109
108
 
110
109
  let config_hash: Value = config_obj.funcall(id!(ruby, "to_hash"), ())?;
111
- let native_config = NativeConfiguration::from_value(ruby, config_hash)?;
110
+ let native_config: NativeConfiguration = deserialize(ruby, config_hash)?;
112
111
  let config_ref = &native_config;
112
+ let arg_error = |error: String| Error::new(ruby.exception_arg_error(), error);
113
113
 
114
- let mode: Mode = config_ref
115
- .try_into()
116
- .map_err(|error: String| Error::new(ruby.exception_arg_error(), error))?;
117
-
118
- let consumer_builders: ConsumerBuilders = config_ref
119
- .try_into()
120
- .map_err(|error: String| Error::new(ruby.exception_arg_error(), error))?;
114
+ let mode: Mode = config_ref.try_into().map_err(arg_error)?;
115
+ let consumer_builders: ConsumerBuilders = config_ref.try_into().map_err(arg_error)?;
121
116
 
122
117
  let bridge = BRIDGE
123
118
  .get()
@@ -126,13 +121,20 @@ impl Client {
126
121
  "Bridge not initialized",
127
122
  ))?
128
123
  .clone();
129
- let cassandra = Into::<CassandraConfigurationBuilder>::into(config_ref);
130
- let mut producer = config_ref.into();
124
+ let cassandra: CassandraConfigurationBuilder = config_ref.try_into().map_err(arg_error)?;
125
+ let mut producer: ProducerConfigurationBuilder =
126
+ config_ref.try_into().map_err(arg_error)?;
131
127
  let client = bridge
132
128
  .wait_for(
133
129
  ruby,
134
130
  async move {
135
- new_erased(mode, &mut producer, &consumer_builders, &cassandra).await
131
+ Box::pin(new_erased(
132
+ mode,
133
+ &mut producer,
134
+ &consumer_builders,
135
+ &cassandra,
136
+ ))
137
+ .await
136
138
  },
137
139
  Span::current(),
138
140
  )?
@@ -143,21 +145,10 @@ impl Client {
143
145
  inner: client,
144
146
  bridge,
145
147
  propagator: Arc::new(new_propagator()),
146
- pid: std::process::id(),
148
+ fork: ForkGuard::new("Prosody::Client"),
147
149
  })
148
150
  }
149
151
 
150
- fn check_fork(ruby: &Ruby, this: &Self) -> Result<(), Error> {
151
- if std::process::id() != this.pid {
152
- return Err(Error::new(
153
- ruby.exception_runtime_error(),
154
- "Prosody::Client cannot be used after fork. Create a new client in the child \
155
- process.",
156
- ));
157
- }
158
- Ok(())
159
- }
160
-
161
152
  /// Returns the current state of the consumer.
162
153
  ///
163
154
  /// The consumer can be in one of four states:
@@ -181,7 +172,7 @@ impl Client {
181
172
  /// build, with the full error message from the underlying
182
173
  /// `ModeConfigurationError`.
183
174
  pub fn consumer_state(ruby: &Ruby, this: &Self) -> Result<StaticSymbol, Error> {
184
- Self::check_fork(ruby, this)?;
175
+ this.fork.check(ruby)?;
185
176
  let inner = this.inner.clone();
186
177
  let state: Result<&'static str, String> = this.bridge.wait_for(
187
178
  ruby,
@@ -227,7 +218,7 @@ impl Client {
227
218
  key: String,
228
219
  payload: Value,
229
220
  ) -> Result<(), Error> {
230
- Self::check_fork(ruby, this)?;
221
+ this.fork.check(ruby)?;
231
222
  let _guard = ensure_runtime_context(ruby);
232
223
  let client = this.inner.clone();
233
224
  let value = deserialize(ruby, payload)?;
@@ -251,7 +242,7 @@ impl Client {
251
242
 
252
243
  /// Sends an excise record for a key.
253
244
  fn excise(ruby: &Ruby, this: &Self, topic: String, key: String) -> Result<(), Error> {
254
- Self::check_fork(ruby, this)?;
245
+ this.fork.check(ruby)?;
255
246
  let _guard = ensure_runtime_context(ruby);
256
247
  let client = this.inner.clone();
257
248
  let context = extract_opentelemetry_context(ruby, &this.propagator)?;
@@ -274,7 +265,7 @@ impl Client {
274
265
  ///
275
266
  /// Returns an error if the handler is incomplete or subscription fails.
276
267
  fn subscribe(ruby: &Ruby, this: &Self, handler: Value) -> Result<(), Error> {
277
- Self::check_fork(ruby, this)?;
268
+ this.fork.check(ruby)?;
278
269
  validate_handler(ruby, handler)?;
279
270
  let _guard = ensure_runtime_context(ruby);
280
271
  let wrapper = RubyHandler::new(this.bridge.clone(), ruby, handler)?;
@@ -304,8 +295,8 @@ impl Client {
304
295
  /// # Returns
305
296
  ///
306
297
  /// The number of assigned partitions as a u32.
307
- pub fn assigned_partitions(ruby: &Ruby, this: &Self) -> Result<u32, Error> {
308
- Self::check_fork(ruby, this)?;
298
+ pub fn assigned_partition_count(ruby: &Ruby, this: &Self) -> Result<u32, Error> {
299
+ this.fork.check(ruby)?;
309
300
  let inner = this.inner.clone();
310
301
  this.bridge.wait_for(
311
302
  ruby,
@@ -328,7 +319,7 @@ impl Client {
328
319
  ///
329
320
  /// `true` if the consumer is stalled, `false` otherwise.
330
321
  pub fn is_stalled(ruby: &Ruby, this: &Self) -> Result<bool, Error> {
331
- Self::check_fork(ruby, this)?;
322
+ this.fork.check(ruby)?;
332
323
  let inner = this.inner.clone();
333
324
  this.bridge.wait_for(
334
325
  ruby,
@@ -351,7 +342,7 @@ impl Client {
351
342
  ///
352
343
  /// Returns an error if the unsubscribe operation fails.
353
344
  fn unsubscribe(ruby: &Ruby, this: &Self) -> Result<(), Error> {
354
- Self::check_fork(ruby, this)?;
345
+ this.fork.check(ruby)?;
355
346
  let _guard = ensure_runtime_context(ruby);
356
347
  let client = this.inner.clone();
357
348
 
@@ -371,7 +362,7 @@ impl Client {
371
362
  ///
372
363
  /// Returns an error if shutdown fails.
373
364
  fn shutdown(ruby: &Ruby, this: &Self) -> Result<(), Error> {
374
- Self::check_fork(ruby, this)?;
365
+ this.fork.check(ruby)?;
375
366
  let _guard = ensure_runtime_context(ruby);
376
367
  let shutdown = this.shutdown.clone();
377
368
 
@@ -395,81 +386,4 @@ impl Client {
395
386
  fn source_system(this: &Self) -> &str {
396
387
  this.inner.source_system()
397
388
  }
398
-
399
- fn published_value(
400
- ruby: &Ruby,
401
- this: &Self,
402
- subsystem: String,
403
- name: String,
404
- cache_seconds: Option<f64>,
405
- cache_disabled: bool,
406
- ) -> Result<NativePublishedValue, Error> {
407
- Self::check_fork(ruby, this)?;
408
- let cache = read_cache(ruby, cache_seconds, cache_disabled)?;
409
- let inner = this.inner.clone();
410
- let reader = this
411
- .bridge
412
- .wait_for(
413
- ruby,
414
- async move { inner.value_state(subsystem, name, cache).await },
415
- Span::current(),
416
- )?
417
- .map_err(|error| Error::new(ruby.exception_runtime_error(), error.to_string()))?;
418
- Ok(NativePublishedValue {
419
- inner: reader,
420
- bridge: this.bridge.clone(),
421
- })
422
- }
423
-
424
- fn published_map(
425
- ruby: &Ruby,
426
- this: &Self,
427
- subsystem: String,
428
- name: String,
429
- cache_seconds: Option<f64>,
430
- cache_disabled: bool,
431
- ) -> Result<NativePublishedMap, Error> {
432
- Self::check_fork(ruby, this)?;
433
- let cache = read_cache(ruby, cache_seconds, cache_disabled)?;
434
- let inner = this.inner.clone();
435
- let reader = this
436
- .bridge
437
- .wait_for(
438
- ruby,
439
- async move { inner.map_state(subsystem, name, cache).await },
440
- Span::current(),
441
- )?
442
- .map_err(|error| Error::new(ruby.exception_runtime_error(), error.to_string()))?;
443
- Ok(NativePublishedMap {
444
- inner: reader,
445
- bridge: this.bridge.clone(),
446
- propagator: Arc::clone(&this.propagator),
447
- })
448
- }
449
-
450
- fn published_deque(
451
- ruby: &Ruby,
452
- this: &Self,
453
- subsystem: String,
454
- name: String,
455
- cache_seconds: Option<f64>,
456
- cache_disabled: bool,
457
- ) -> Result<NativePublishedDeque, Error> {
458
- Self::check_fork(ruby, this)?;
459
- let cache = read_cache(ruby, cache_seconds, cache_disabled)?;
460
- let inner = this.inner.clone();
461
- let reader = this
462
- .bridge
463
- .wait_for(
464
- ruby,
465
- async move { inner.deque_state(subsystem, name, cache).await },
466
- Span::current(),
467
- )?
468
- .map_err(|error| Error::new(ruby.exception_runtime_error(), error.to_string()))?;
469
- Ok(NativePublishedDeque {
470
- inner: reader,
471
- bridge: this.bridge.clone(),
472
- propagator: Arc::clone(&this.propagator),
473
- })
474
- }
475
389
  }
@@ -0,0 +1,106 @@
1
+ //! Published-state reader constructors for `Prosody::Client`.
2
+ //!
3
+ //! Each constructor opens a core reader through the bridge and wraps it in the
4
+ //! matching native reader class.
5
+
6
+ use super::config::read_cache_policy;
7
+ use super::{Client, RubyHandler};
8
+ use crate::handler::state_error;
9
+ use crate::published::{
10
+ NativePublishedDeque, NativePublishedMap, NativePublishedSet, NativePublishedValue, Reads,
11
+ };
12
+ use magnus::{Error, Ruby, Value};
13
+ use prosody::high_level::HighLevelClientError;
14
+ use prosody::high_level::erased::{ErasedReadCache, ErasedReaderBuildError, SharedHighLevelClient};
15
+ use serde_magnus::deserialize;
16
+ use std::fmt::Display;
17
+ use std::sync::Arc;
18
+ use tracing::Span;
19
+
20
+ /// Opens one published-state reader, and returns it with the client's
21
+ /// bridge, propagator, and fork guard. `open` receives the shared client and
22
+ /// the resolved cache policy. A state reader error raises the matching typed
23
+ /// state error. Any other open failure raises `RuntimeError`.
24
+ fn open_reader<F, Fut, R, E>(
25
+ ruby: &Ruby,
26
+ this: &Client,
27
+ read_cache: Value,
28
+ open: F,
29
+ ) -> Result<(R, Reads), Error>
30
+ where
31
+ F: FnOnce(SharedHighLevelClient<RubyHandler>, ErasedReadCache) -> Fut,
32
+ Fut: Future<Output = Result<R, ErasedReaderBuildError<E>>> + Send + 'static,
33
+ R: Send,
34
+ E: Display + Send,
35
+ {
36
+ this.fork.check(ruby)?;
37
+ let cache = read_cache_policy("read_cache", deserialize(ruby, read_cache)?)
38
+ .map_err(|error| Error::new(ruby.exception_arg_error(), error))?;
39
+ let reader = this
40
+ .bridge
41
+ .wait_for(ruby, open(this.inner.clone(), cache), Span::current())?
42
+ .map_err(|error| match error {
43
+ ErasedReaderBuildError::Client(HighLevelClientError::StateReader(error)) => {
44
+ state_error(ruby, &error.into())
45
+ }
46
+ error => Error::new(ruby.exception_runtime_error(), error.to_string()),
47
+ })?;
48
+ let reads = Reads {
49
+ bridge: this.bridge.clone(),
50
+ propagator: Arc::clone(&this.propagator),
51
+ fork: this.fork,
52
+ };
53
+ Ok((reader, reads))
54
+ }
55
+
56
+ pub(super) fn published_value(
57
+ ruby: &Ruby,
58
+ this: &Client,
59
+ subsystem: String,
60
+ name: String,
61
+ read_cache: Value,
62
+ ) -> Result<NativePublishedValue, Error> {
63
+ let (inner, reads) = open_reader(ruby, this, read_cache, |client, cache| async move {
64
+ client.value_state(subsystem, name, cache).await
65
+ })?;
66
+ Ok(NativePublishedValue { inner, reads })
67
+ }
68
+
69
+ pub(super) fn published_map(
70
+ ruby: &Ruby,
71
+ this: &Client,
72
+ subsystem: String,
73
+ name: String,
74
+ read_cache: Value,
75
+ ) -> Result<NativePublishedMap, Error> {
76
+ let (inner, reads) = open_reader(ruby, this, read_cache, |client, cache| async move {
77
+ client.map_state(subsystem, name, cache).await
78
+ })?;
79
+ Ok(NativePublishedMap { inner, reads })
80
+ }
81
+
82
+ pub(super) fn published_set(
83
+ ruby: &Ruby,
84
+ this: &Client,
85
+ subsystem: String,
86
+ name: String,
87
+ read_cache: Value,
88
+ ) -> Result<NativePublishedSet, Error> {
89
+ let (inner, reads) = open_reader(ruby, this, read_cache, |client, cache| async move {
90
+ client.set_state(subsystem, name, cache).await
91
+ })?;
92
+ Ok(NativePublishedSet { inner, reads })
93
+ }
94
+
95
+ pub(super) fn published_deque(
96
+ ruby: &Ruby,
97
+ this: &Client,
98
+ subsystem: String,
99
+ name: String,
100
+ read_cache: Value,
101
+ ) -> Result<NativePublishedDeque, Error> {
102
+ let (inner, reads) = open_reader(ruby, this, read_cache, |client, cache| async move {
103
+ client.deque_state(subsystem, name, cache).await
104
+ })?;
105
+ Ok(NativePublishedDeque { inner, reads })
106
+ }