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
@@ -1,76 +1,97 @@
1
1
  //! Read-only published-state handles for Ruby.
2
+ //!
3
+ //! Every reader method runs through [`Reads`], which refuses a forked child
4
+ //! and joins the caller's OpenTelemetry context. A failed read raises the
5
+ //! same typed state errors as an owned handle.
2
6
 
3
7
  use crate::bridge::Bridge;
4
8
  use crate::handler::{
5
- NativeJsonDequeScan, NativeJsonMapScan, NativeMapKeyScan, parse_direction,
6
- published_deque_scan, published_map_key_scan, published_map_scan,
9
+ NativeJsonDequeScan, NativeJsonMapScan, NativeMapKeyScan, key_query, position_query, run_state,
7
10
  };
11
+ use crate::util::ForkGuard;
8
12
  use crate::{ROOT_MOD, id};
9
- use magnus::value::ReprValue;
10
- use magnus::{Error, Module, Ruby, StaticSymbol, Value, method};
13
+ use magnus::{Error, Module, RHash, Ruby, StaticSymbol, Value, method};
11
14
  use opentelemetry::propagation::TextMapCompositePropagator;
12
- use prosody::JsonCodec;
15
+ use prosody::consumer::event_context::ErasedStateError;
13
16
  use prosody::high_level::erased::{
14
- ErasedDirection, SharedDequeReader, SharedMapReader, SharedValueReader,
17
+ SharedDequeReader, SharedMapReader, SharedSetReader, SharedValueReader,
15
18
  };
16
- use prosody::state::Direction;
19
+ use serde_json::Value as JsonValue;
17
20
  use serde_magnus::serialize;
18
21
  use std::sync::Arc;
19
- use tracing::Span;
20
22
 
21
- fn read_error(ruby: &Ruby, error: &impl ToString) -> Error {
22
- Error::new(ruby.exception_runtime_error(), error.to_string())
23
+ /// The bridge, trace propagator, and fork guard that every reader shares.
24
+ #[derive(Clone)]
25
+ pub(crate) struct Reads {
26
+ pub(crate) bridge: Bridge,
27
+ pub(crate) propagator: Arc<TextMapCompositePropagator>,
28
+ pub(crate) fork: ForkGuard,
23
29
  }
24
30
 
25
- fn erased_direction(direction: Direction) -> ErasedDirection {
26
- match direction {
27
- Direction::Forward => ErasedDirection::Forward,
28
- Direction::Backward => ErasedDirection::Backward,
31
+ impl Reads {
32
+ /// Waits for one read in the caller's trace.
33
+ ///
34
+ /// # Errors
35
+ ///
36
+ /// Raises `RuntimeError` after fork, and a typed state error when the
37
+ /// read fails.
38
+ fn read<F, T>(&self, ruby: &Ruby, read: F) -> Result<T, Error>
39
+ where
40
+ F: Future<Output = Result<T, ErasedStateError>> + Send + 'static,
41
+ T: Send + 'static,
42
+ {
43
+ self.fork.check(ruby)?;
44
+ run_state(ruby, &self.bridge, &self.propagator, read)
45
+ }
46
+
47
+ /// Waits for one optional JSON read and returns the value or `nil`.
48
+ ///
49
+ /// # Errors
50
+ ///
51
+ /// See [`Reads::read`].
52
+ fn read_json<F>(&self, ruby: &Ruby, read: F) -> Result<Value, Error>
53
+ where
54
+ F: Future<Output = Result<Option<JsonValue>, ErasedStateError>> + Send + 'static,
55
+ {
56
+ serialize(ruby, &self.read(ruby, read)?)
57
+ }
58
+
59
+ /// Returns the bridge and propagator for a new scan.
60
+ ///
61
+ /// # Errors
62
+ ///
63
+ /// Raises `RuntimeError` after fork.
64
+ fn scan_parts(&self, ruby: &Ruby) -> Result<(Bridge, Arc<TextMapCompositePropagator>), Error> {
65
+ self.fork.check(ruby)?;
66
+ Ok((self.bridge.clone(), Arc::clone(&self.propagator)))
29
67
  }
30
68
  }
31
69
 
32
70
  #[magnus::wrap(class = "Prosody::NativePublishedValue")]
33
71
  pub(crate) struct NativePublishedValue {
34
- pub(crate) inner: SharedValueReader<JsonCodec>,
35
- pub(crate) bridge: Bridge,
72
+ pub(crate) inner: SharedValueReader<JsonValue>,
73
+ pub(crate) reads: Reads,
36
74
  }
37
75
 
38
76
  impl NativePublishedValue {
39
77
  fn get(ruby: &Ruby, this: &Self, key: String) -> Result<Value, Error> {
40
78
  let inner = Arc::clone(&this.inner);
41
- let value = this
42
- .bridge
43
- .wait_for(ruby, async move { inner.get(key).await }, Span::current())?
44
- .map_err(|error| read_error(ruby, &error))?;
45
- match value {
46
- Some(value) => serialize(ruby, &value),
47
- None => Ok(ruby.qnil().as_value()),
48
- }
79
+ this.reads
80
+ .read_json(ruby, async move { inner.get(key).await })
49
81
  }
50
82
  }
51
83
 
52
84
  #[magnus::wrap(class = "Prosody::NativePublishedMap")]
53
85
  pub(crate) struct NativePublishedMap {
54
- pub(crate) inner: SharedMapReader<JsonCodec>,
55
- pub(crate) bridge: Bridge,
56
- pub(crate) propagator: Arc<TextMapCompositePropagator>,
86
+ pub(crate) inner: SharedMapReader<JsonValue>,
87
+ pub(crate) reads: Reads,
57
88
  }
58
89
 
59
90
  impl NativePublishedMap {
60
91
  fn get(ruby: &Ruby, this: &Self, key: String, map_key: String) -> Result<Value, Error> {
61
92
  let inner = Arc::clone(&this.inner);
62
- let value = this
63
- .bridge
64
- .wait_for(
65
- ruby,
66
- async move { inner.get(key, map_key).await },
67
- Span::current(),
68
- )?
69
- .map_err(|error| read_error(ruby, &error))?;
70
- match value {
71
- Some(value) => serialize(ruby, &value),
72
- None => Ok(ruby.qnil().as_value()),
73
- }
93
+ this.reads
94
+ .read_json(ruby, async move { inner.get(key, map_key).await })
74
95
  }
75
96
 
76
97
  fn get_many(
@@ -81,25 +102,34 @@ impl NativePublishedMap {
81
102
  ) -> Result<Value, Error> {
82
103
  let inner = Arc::clone(&this.inner);
83
104
  let values = this
84
- .bridge
85
- .wait_for(
86
- ruby,
87
- async move { inner.get_many(key, map_keys).await },
88
- Span::current(),
89
- )?
90
- .map_err(|error| read_error(ruby, &error))?;
105
+ .reads
106
+ .read(ruby, async move { inner.get_many(key, map_keys).await })?;
91
107
  serialize(ruby, &values)
92
108
  }
93
109
 
110
+ fn contains_many(
111
+ ruby: &Ruby,
112
+ this: &Self,
113
+ key: String,
114
+ map_keys: Vec<String>,
115
+ ) -> Result<Vec<bool>, Error> {
116
+ let inner = Arc::clone(&this.inner);
117
+ this.reads.read(
118
+ ruby,
119
+ async move { inner.contains_many(key, map_keys).await },
120
+ )
121
+ }
122
+
123
+ fn is_empty(ruby: &Ruby, this: &Self, key: String) -> Result<bool, Error> {
124
+ let inner = Arc::clone(&this.inner);
125
+ this.reads
126
+ .read(ruby, async move { inner.is_empty(key).await })
127
+ }
128
+
94
129
  fn contains_key(ruby: &Ruby, this: &Self, key: String, map_key: String) -> Result<bool, Error> {
95
130
  let inner = Arc::clone(&this.inner);
96
- this.bridge
97
- .wait_for(
98
- ruby,
99
- async move { inner.contains_key(key, map_key).await },
100
- Span::current(),
101
- )?
102
- .map_err(|error| read_error(ruby, &error))
131
+ this.reads
132
+ .read(ruby, async move { inner.contains_key(key, map_key).await })
103
133
  }
104
134
 
105
135
  fn scan(
@@ -107,23 +137,12 @@ impl NativePublishedMap {
107
137
  this: &Self,
108
138
  key: String,
109
139
  direction: StaticSymbol,
140
+ options: RHash,
110
141
  ) -> Result<NativeJsonMapScan, Error> {
111
- let direction = erased_direction(parse_direction(ruby, direction)?);
112
- let inner = Arc::clone(&this.inner);
113
- let cursor = this
114
- .bridge
115
- .wait_for(
116
- ruby,
117
- async move { inner.stream(key, direction).await },
118
- Span::current(),
119
- )?
120
- .map_err(|error| read_error(ruby, &error))?;
121
- published_map_scan(
122
- ruby,
123
- cursor,
124
- this.bridge.clone(),
125
- Arc::clone(&this.propagator),
126
- )
142
+ let query = key_query(ruby, direction, options)?;
143
+ let (bridge, propagator) = this.reads.scan_parts(ruby)?;
144
+ let entries = this.inner.entries(key).with_query(query).stream();
145
+ Ok(NativeJsonMapScan::new(entries, bridge, propagator))
127
146
  }
128
147
 
129
148
  fn keys(
@@ -131,98 +150,95 @@ impl NativePublishedMap {
131
150
  this: &Self,
132
151
  key: String,
133
152
  direction: StaticSymbol,
153
+ options: RHash,
134
154
  ) -> Result<NativeMapKeyScan, Error> {
135
- let direction = erased_direction(parse_direction(ruby, direction)?);
155
+ let query = key_query(ruby, direction, options)?;
156
+ let (bridge, propagator) = this.reads.scan_parts(ruby)?;
157
+ let keys = this.inner.keys(key).with_query(query).stream();
158
+ Ok(NativeMapKeyScan::new(keys, bridge, propagator))
159
+ }
160
+ }
161
+
162
+ #[magnus::wrap(class = "Prosody::NativePublishedSet")]
163
+ pub(crate) struct NativePublishedSet {
164
+ pub(crate) inner: SharedSetReader,
165
+ pub(crate) reads: Reads,
166
+ }
167
+
168
+ impl NativePublishedSet {
169
+ fn contains(ruby: &Ruby, this: &Self, key: String, member: String) -> Result<bool, Error> {
136
170
  let inner = Arc::clone(&this.inner);
137
- let cursor = this
138
- .bridge
139
- .wait_for(
140
- ruby,
141
- async move { inner.keys(key, direction).await },
142
- Span::current(),
143
- )?
144
- .map_err(|error| read_error(ruby, &error))?;
145
- published_map_key_scan(
146
- ruby,
147
- cursor,
148
- this.bridge.clone(),
149
- Arc::clone(&this.propagator),
150
- )
171
+ this.reads
172
+ .read(ruby, async move { inner.contains(key, member).await })
173
+ }
174
+
175
+ fn contains_many(
176
+ ruby: &Ruby,
177
+ this: &Self,
178
+ key: String,
179
+ members: Vec<String>,
180
+ ) -> Result<Vec<bool>, Error> {
181
+ let inner = Arc::clone(&this.inner);
182
+ this.reads
183
+ .read(ruby, async move { inner.contains_many(key, members).await })
184
+ }
185
+
186
+ fn is_empty(ruby: &Ruby, this: &Self, key: String) -> Result<bool, Error> {
187
+ let inner = Arc::clone(&this.inner);
188
+ this.reads
189
+ .read(ruby, async move { inner.is_empty(key).await })
190
+ }
191
+
192
+ /// Opens a member cursor. Members are bare `String` keys, so the map key
193
+ /// cursor carries them.
194
+ fn keys(
195
+ ruby: &Ruby,
196
+ this: &Self,
197
+ key: String,
198
+ direction: StaticSymbol,
199
+ options: RHash,
200
+ ) -> Result<NativeMapKeyScan, Error> {
201
+ let query = key_query(ruby, direction, options)?;
202
+ let (bridge, propagator) = this.reads.scan_parts(ruby)?;
203
+ let members = this.inner.keys(key).with_query(query).stream();
204
+ Ok(NativeMapKeyScan::new(members, bridge, propagator))
151
205
  }
152
206
  }
153
207
 
154
208
  #[magnus::wrap(class = "Prosody::NativePublishedDeque")]
155
209
  pub(crate) struct NativePublishedDeque {
156
- pub(crate) inner: SharedDequeReader<JsonCodec>,
157
- pub(crate) bridge: Bridge,
158
- pub(crate) propagator: Arc<TextMapCompositePropagator>,
210
+ pub(crate) inner: SharedDequeReader<JsonValue>,
211
+ pub(crate) reads: Reads,
159
212
  }
160
213
 
161
214
  impl NativePublishedDeque {
162
215
  fn get(ruby: &Ruby, this: &Self, key: String, index: usize) -> Result<Value, Error> {
163
216
  let inner = Arc::clone(&this.inner);
164
- let value = this
165
- .bridge
166
- .wait_for(
167
- ruby,
168
- async move { inner.get(key, index).await },
169
- Span::current(),
170
- )?
171
- .map_err(|error| read_error(ruby, &error))?;
172
- match value {
173
- Some(value) => serialize(ruby, &value),
174
- None => Ok(ruby.qnil().as_value()),
175
- }
217
+ this.reads
218
+ .read_json(ruby, async move { inner.get(key, index).await })
176
219
  }
177
220
 
178
221
  fn length(ruby: &Ruby, this: &Self, key: String) -> Result<usize, Error> {
179
222
  let inner = Arc::clone(&this.inner);
180
- this.bridge
181
- .wait_for(ruby, async move { inner.len(key).await }, Span::current())?
182
- .map_err(|error| read_error(ruby, &error))
223
+ this.reads.read(ruby, async move { inner.len(key).await })
183
224
  }
184
225
 
185
226
  fn is_empty(ruby: &Ruby, this: &Self, key: String) -> Result<bool, Error> {
186
227
  let inner = Arc::clone(&this.inner);
187
- this.bridge
188
- .wait_for(
189
- ruby,
190
- async move { inner.is_empty(key).await },
191
- Span::current(),
192
- )?
193
- .map_err(|error| read_error(ruby, &error))
228
+ this.reads
229
+ .read(ruby, async move { inner.is_empty(key).await })
194
230
  }
195
231
 
196
232
  fn peek_front(ruby: &Ruby, this: &Self, key: String) -> Result<Value, Error> {
197
233
  let inner = Arc::clone(&this.inner);
198
- let value = this
199
- .bridge
200
- .wait_for(
201
- ruby,
202
- async move { inner.peek_front(key).await },
203
- Span::current(),
204
- )?
205
- .map_err(|error| read_error(ruby, &error))?;
206
- match value {
207
- Some(value) => serialize(ruby, &value),
208
- None => Ok(ruby.qnil().as_value()),
209
- }
234
+ this.reads
235
+ .read_json(ruby, async move { inner.peek_front(key).await })
210
236
  }
211
237
 
212
238
  fn peek_back(ruby: &Ruby, this: &Self, key: String) -> Result<Value, Error> {
213
239
  let inner = Arc::clone(&this.inner);
214
- let value = this
215
- .bridge
216
- .wait_for(
217
- ruby,
218
- async move { inner.peek_back(key).await },
219
- Span::current(),
220
- )?
221
- .map_err(|error| read_error(ruby, &error))?;
222
- match value {
223
- Some(value) => serialize(ruby, &value),
224
- None => Ok(ruby.qnil().as_value()),
225
- }
240
+ this.reads
241
+ .read_json(ruby, async move { inner.peek_back(key).await })
226
242
  }
227
243
 
228
244
  fn scan(
@@ -230,23 +246,12 @@ impl NativePublishedDeque {
230
246
  this: &Self,
231
247
  key: String,
232
248
  direction: StaticSymbol,
249
+ options: RHash,
233
250
  ) -> Result<NativeJsonDequeScan, Error> {
234
- let direction = erased_direction(parse_direction(ruby, direction)?);
235
- let inner = Arc::clone(&this.inner);
236
- let cursor = this
237
- .bridge
238
- .wait_for(
239
- ruby,
240
- async move { inner.stream(key, direction).await },
241
- Span::current(),
242
- )?
243
- .map_err(|error| read_error(ruby, &error))?;
244
- published_deque_scan(
245
- ruby,
246
- cursor,
247
- this.bridge.clone(),
248
- Arc::clone(&this.propagator),
249
- )
251
+ let query = position_query(ruby, direction, options)?;
252
+ let (bridge, propagator) = this.reads.scan_parts(ruby)?;
253
+ let values = this.inner.values(key).with_query(query).stream();
254
+ Ok(NativeJsonDequeScan::new(values, bridge, propagator))
250
255
  }
251
256
  }
252
257
 
@@ -259,8 +264,22 @@ pub(crate) fn init(ruby: &Ruby) -> Result<(), Error> {
259
264
  map.define_method("get", method!(NativePublishedMap::get, 2))?;
260
265
  map.define_method("get_many", method!(NativePublishedMap::get_many, 2))?;
261
266
  map.define_method("contains_key", method!(NativePublishedMap::contains_key, 2))?;
262
- map.define_method("scan", method!(NativePublishedMap::scan, 2))?;
263
- map.define_method("keys", method!(NativePublishedMap::keys, 2))?;
267
+ map.define_method(
268
+ "contains_many",
269
+ method!(NativePublishedMap::contains_many, 2),
270
+ )?;
271
+ map.define_method("is_empty", method!(NativePublishedMap::is_empty, 1))?;
272
+ map.define_method("scan", method!(NativePublishedMap::scan, 3))?;
273
+ map.define_method("keys", method!(NativePublishedMap::keys, 3))?;
274
+
275
+ let set = module.define_class(id!(ruby, "NativePublishedSet"), ruby.class_object())?;
276
+ set.define_method("contains", method!(NativePublishedSet::contains, 2))?;
277
+ set.define_method(
278
+ "contains_many",
279
+ method!(NativePublishedSet::contains_many, 2),
280
+ )?;
281
+ set.define_method("is_empty", method!(NativePublishedSet::is_empty, 1))?;
282
+ set.define_method("keys", method!(NativePublishedSet::keys, 3))?;
264
283
 
265
284
  let deque = module.define_class(id!(ruby, "NativePublishedDeque"), ruby.class_object())?;
266
285
  deque.define_method("get", method!(NativePublishedDeque::get, 2))?;
@@ -268,6 +287,6 @@ pub(crate) fn init(ruby: &Ruby) -> Result<(), Error> {
268
287
  deque.define_method("is_empty", method!(NativePublishedDeque::is_empty, 1))?;
269
288
  deque.define_method("peek_front", method!(NativePublishedDeque::peek_front, 1))?;
270
289
  deque.define_method("peek_back", method!(NativePublishedDeque::peek_back, 1))?;
271
- deque.define_method("scan", method!(NativePublishedDeque::scan, 2))?;
290
+ deque.define_method("scan", method!(NativePublishedDeque::scan, 3))?;
272
291
  Ok(())
273
292
  }
@@ -80,9 +80,10 @@ impl ResultSender {
80
80
  return false;
81
81
  };
82
82
 
83
+ // A result with no JSON form is a handler mistake, so it retries.
83
84
  if is_success {
84
85
  let result = deserialize(ruby, result)
85
- .map_err(|error| ProcessingError::Permanent(error.to_string()));
86
+ .map_err(|error| ProcessingError::Transient(error.to_string()));
86
87
  if result_tx.send(result).is_err() {
87
88
  debug!("discarding result; receiver went away");
88
89
  }
@@ -13,7 +13,11 @@ use prosody::tracing::{
13
13
  TracingError, flush_telemetry as core_flush_telemetry, initialize_tracing,
14
14
  shutdown_telemetry as core_shutdown_telemetry,
15
15
  };
16
+ use std::fmt;
17
+ use std::io::{self, Write};
16
18
  use std::mem::{ManuallyDrop, forget};
19
+ use std::process;
20
+ use std::time::Duration;
17
21
  use tokio::runtime::{EnterGuard, Handle};
18
22
  use tracing::{error, warn};
19
23
 
@@ -179,6 +183,65 @@ impl Drop for RubyDrop {
179
183
  }
180
184
  }
181
185
 
186
+ /// Writes one line to standard error.
187
+ ///
188
+ /// Use it only where no logger exists: before tracing starts, or when the
189
+ /// Ruby logger itself fails.
190
+ pub(crate) fn report(message: fmt::Arguments<'_>) {
191
+ drop(writeln!(io::stderr().lock(), "{message}"));
192
+ }
193
+
194
+ /// Converts a Ruby number of seconds into a [`Duration`].
195
+ ///
196
+ /// # Errors
197
+ ///
198
+ /// Returns an error that names `option` if the value is negative, not
199
+ /// finite, or too large for a [`Duration`].
200
+ pub(crate) fn seconds(option: &str, value: f64) -> Result<Duration, String> {
201
+ Duration::try_from_secs_f64(value)
202
+ .map_err(|_| format!("{option}: must be a finite, non-negative number of seconds"))
203
+ }
204
+
205
+ /// Detects the use of a native object in a forked child process.
206
+ ///
207
+ /// The Tokio runtime and the bridge thread do not survive `fork`. A native
208
+ /// object that waits on them in a child waits forever, so it raises instead.
209
+ #[derive(Clone, Copy, Debug)]
210
+ pub struct ForkGuard {
211
+ /// The process that created the object.
212
+ pid: u32,
213
+ /// The Ruby class name that the error message shows.
214
+ class: &'static str,
215
+ }
216
+
217
+ impl ForkGuard {
218
+ /// Records the current process for an object of the Ruby `class`.
219
+ pub fn new(class: &'static str) -> Self {
220
+ Self {
221
+ pid: process::id(),
222
+ class,
223
+ }
224
+ }
225
+
226
+ /// Checks that the current process created the object.
227
+ ///
228
+ /// # Errors
229
+ ///
230
+ /// Returns a `RuntimeError` in a forked child process.
231
+ pub fn check(self, ruby: &Ruby) -> Result<(), Error> {
232
+ if process::id() == self.pid {
233
+ return Ok(());
234
+ }
235
+ Err(Error::new(
236
+ ruby.exception_runtime_error(),
237
+ format!(
238
+ "{} cannot be used after fork. Create a new client in the child process.",
239
+ self.class
240
+ ),
241
+ ))
242
+ }
243
+ }
244
+
182
245
  /// Ensures a Tokio runtime context exists, entering one if necessary.
183
246
  ///
184
247
  /// Only creates a runtime guard when not already in a runtime context, avoiding
@@ -207,14 +270,13 @@ pub fn ensure_runtime_context(ruby: &Ruby) -> Option<EnterGuard<'static>> {
207
270
  let bridge = BRIDGE.get_or_init(|| Bridge::new(ruby));
208
271
 
209
272
  // Initialize tracing for observability
210
- #[allow(clippy::print_stderr, reason = "logger has not been initialized yet")]
211
273
  TRACING_INIT.get_or_init(|| {
212
274
  let maybe_logger = Logger::new(ruby, bridge.clone())
213
- .inspect_err(|error| eprintln!("failed to create logger: {error:#}"))
275
+ .inspect_err(|error| report(format_args!("failed to create logger: {error:#}")))
214
276
  .ok();
215
277
 
216
278
  if let Err(error) = initialize_tracing(maybe_logger) {
217
- eprintln!("failed to initialize tracing: {error:#}");
279
+ report(format_args!("failed to initialize tracing: {error:#}"));
218
280
  }
219
281
  });
220
282
 
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosody
4
+ # Ruby-side lifecycle helpers for the native {Prosody::Client}.
5
+ class Client
6
+ # Creates a client, yields it, and shuts it down when the block exits.
7
+ # The client shuts down also when the block raises. Repeated shutdown
8
+ # calls wait for the same operation, so the block can also call
9
+ # {#shutdown}.
10
+ #
11
+ # @param config [Hash, Configuration] Client configuration
12
+ # @yieldparam client [Client] the new client
13
+ # @return [Object] the value of the block
14
+ # @raise [ArgumentError] if no block is given or the configuration is invalid
15
+ # @raise [RuntimeError] if client initialization or shutdown fails
16
+ #
17
+ # @example
18
+ # Prosody::Client.open(bootstrap_servers: "localhost:9092") do |client|
19
+ # client.send_message("my-topic", "key", {"hello" => "world"})
20
+ # end
21
+ def self.open(config)
22
+ raise ArgumentError, "Prosody::Client.open requires a block" unless block_given?
23
+
24
+ client = new(config)
25
+ begin
26
+ yield client
27
+ ensure
28
+ client.shutdown
29
+ end
30
+ end
31
+ end
32
+ end
@@ -124,14 +124,18 @@ module Prosody
124
124
  # Interval between offset commit operations (in seconds).
125
125
  config_param :commit_interval, converter: ->(v) { duration_converter(v) }
126
126
 
127
+ # Interval between librdkafka statistics reports (in seconds).
128
+ # Env: PROSODY_STATISTICS_INTERVAL. Default: 5 seconds.
129
+ config_param :statistics_interval, converter: ->(v) { duration_converter(v) }
130
+
127
131
  # Base delay for retry operations (in seconds).
128
132
  config_param :retry_base, converter: ->(v) { duration_converter(v) }
129
133
 
130
134
  # Maximum delay between retries (in seconds).
131
135
  config_param :max_retry_delay, converter: ->(v) { duration_converter(v) }
132
136
 
133
- # Global shared cache capacity across all partitions for message deduplication.
134
- # Must be at least 1. Default: 8192.
137
+ # Capacity of the producer idempotence cache and of the consumer
138
+ # deduplication cache. Must be at least 1. Default: 8192.
135
139
  config_param :idempotence_cache_size, converter: ->(v) { Integer(v) }
136
140
 
137
141
  # Version string for cache-busting deduplication hashes. Changing this
@@ -303,7 +307,7 @@ module Prosody
303
307
 
304
308
  # Keyed-state collections to register before subscribe.
305
309
  #
306
- # Accepts an array of StateDefinition objects (from Prosody.value/map/deque
310
+ # Accepts an array of StateDefinition objects (from Prosody.value/map/set/deque
307
311
  # and their message_* siblings) or already-serialized registration hashes.
308
312
  # Duplicate names within the set are rejected by the native layer.
309
313
  config_param :state_collections,
@@ -312,13 +316,15 @@ module Prosody
312
316
  list.map { |d| d.respond_to?(:to_state_config) ? d.to_state_config : d }
313
317
  }
314
318
 
315
- # Subsystem under which published JSON collections are advertised.
319
+ # Subsystem under which published JSON and set collections are advertised.
316
320
  # Uses PROSODY_SUBSYSTEM when omitted. Published collections require it.
317
321
  config_param :subsystem, converter: lambda(&:to_s)
318
322
 
319
- # Disk workspace for the local keyed-state cache. Each live client
320
- # needs its own directory. Falls back to the
321
- # PROSODY_STATE_CACHE_DIR environment variable. Must not be an empty string.
323
+ # Directory for the local keyed-state caches. Each consumer opens its
324
+ # cache in a new subdirectory and removes it when the consumer stops, so
325
+ # clients can share the directory. Falls back to the
326
+ # PROSODY_STATE_CACHE_DIR environment variable, then to
327
+ # <temp>/prosody/keyed-state. Must not be an empty string.
322
328
  config_param :state_cache_dir, converter: lambda(&:to_s)
323
329
 
324
330
  # Capacity of the owning keyed-state cache, such as "64 MiB". Uses
@@ -326,6 +332,13 @@ module Prosody
326
332
  # selects its default.
327
333
  config_param :state_owned_cache_size, converter: lambda(&:to_s)
328
334
 
335
+ # Bytes of in-memory writes the local keyed-state cache holds for each
336
+ # assigned partition before it flushes them to disk, such as "16 MiB".
337
+ # Memory use scales with the number of assigned partitions. Uses
338
+ # PROSODY_STATE_MEMTABLE_SIZE when omitted. Otherwise, the engine uses
339
+ # its default of 64 MiB.
340
+ config_param :state_memtable_size, converter: lambda(&:to_s)
341
+
329
342
  # Capacity of the published-state read-through cache, such as "1 MiB".
330
343
  # Uses PROSODY_STATE_READ_CACHE_SIZE when omitted. It then uses the owned
331
344
  # cache size when set, or 1 MiB when both sizes are unset.
@@ -336,11 +349,6 @@ module Prosody
336
349
  config_param :state_read_cache,
337
350
  converter: ->(v) { (v == true || v == false) ? v : Float(v) }
338
351
 
339
- # Delay in whole seconds between staging a provisional cell and the
340
- # keyed-state recovery sweep. Every registered TTL must strictly exceed this.
341
- # Must be a whole number of seconds >= 1 (validated natively).
342
- config_param :state_recovery_delay, converter: ->(v) { duration_converter(v) }
343
-
344
352
  # Operation mode of the client.
345
353
  #
346
354
  # Valid values:
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosody
4
+ # The demand a handler call serves, from +context.demand+.
5
+ #
6
+ # +kind+ is +:normal+ for a normal delivery and +:failure+ for a retry after
7
+ # a failure. +retries+ is the number of retries: 0 for a normal delivery and
8
+ # 1 on the first retry. After Prosody defers an event, the count starts
9
+ # again at 1. The count is an estimate. Keep an exact attempt count in
10
+ # keyed state if the handler needs one.
11
+ #
12
+ # @example Alert only on a retry
13
+ # def on_message(context, message)
14
+ # alert(message.key) if context.demand.failure?
15
+ # end
16
+ Demand = Data.define(:kind, :retries) do
17
+ # Whether this is a normal delivery.
18
+ #
19
+ # @return [Boolean]
20
+ def normal? = kind == :normal
21
+
22
+ # Whether this is a retry after a failure.
23
+ #
24
+ # @return [Boolean]
25
+ def failure? = kind == :failure
26
+ end
27
+ end