prosody 0.5.1 → 0.6.1

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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/.config/rail.toml +30 -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 +14 -0
  8. data/CONFIGURATION.md +20 -17
  9. data/Cargo.lock +300 -274
  10. data/Cargo.toml +11 -8
  11. data/README.md +104 -25
  12. data/examples/keyed_state.rb +10 -2
  13. data/examples/keyed_state.rbs +1 -0
  14. data/ext/prosody/Cargo.toml +2 -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/tracing_util.rs +0 -8
  41. data/ext/prosody/src/util.rs +65 -17
  42. data/lib/prosody/client.rb +32 -0
  43. data/lib/prosody/configuration.rb +20 -12
  44. data/lib/prosody/demand.rb +27 -0
  45. data/lib/prosody/native_stubs/client.rb +157 -0
  46. data/lib/prosody/native_stubs/context.rb +178 -0
  47. data/lib/prosody/native_stubs/message.rb +133 -0
  48. data/lib/prosody/native_stubs.rb +11 -956
  49. data/lib/prosody/state/deque.rb +253 -0
  50. data/lib/prosody/state/map.rb +313 -0
  51. data/lib/prosody/state/set.rb +121 -0
  52. data/lib/prosody/state/value.rb +58 -0
  53. data/lib/prosody/state.rb +157 -677
  54. data/lib/prosody/version.rb +1 -1
  55. data/lib/prosody.rb +2 -1
  56. data/sig/configuration.rbs +19 -10
  57. data/sig/prosody.rbs +37 -4
  58. data/sig/published.rbs +102 -0
  59. data/sig/state.rbs +109 -122
  60. data/typecheck/payload_types.rb +12 -0
  61. data/typecheck/payload_types.rbs +1 -0
  62. metadata +30 -11
  63. 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
  }
@@ -32,14 +32,6 @@ use std::collections::HashMap;
32
32
  /// - OpenTelemetry module is not available in Ruby
33
33
  /// - Context extraction or propagation fails
34
34
  /// - Ruby-to-Rust type conversion fails
35
- ///
36
- /// # Example
37
- ///
38
- /// ```rust
39
- /// let context = extract_opentelemetry_context(ruby, &propagator)?;
40
- /// let span = info_span!("operation");
41
- /// span.set_parent(context);
42
- /// ```
43
35
  pub fn extract_opentelemetry_context(
44
36
  ruby: &Ruby,
45
37
  propagator: &TextMapCompositePropagator,
@@ -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
 
@@ -25,13 +29,6 @@ use tracing::{error, warn};
25
29
  ///
26
30
  /// This macro requires a `ruby: &Ruby` parameter to enforce that it can only
27
31
  /// be used within a Ruby thread context, ensuring thread safety.
28
- ///
29
- /// # Examples
30
- ///
31
- /// ```
32
- /// let method_name = id!(ruby, "to_s");
33
- /// // Use method_name with Ruby function calls
34
- /// ```
35
32
  #[macro_export]
36
33
  macro_rules! id {
37
34
  ($ruby:expr, $str:expr) => {{
@@ -179,6 +176,65 @@ impl Drop for RubyDrop {
179
176
  }
180
177
  }
181
178
 
179
+ /// Writes one line to standard error.
180
+ ///
181
+ /// Use it only where no logger exists: before tracing starts, or when the
182
+ /// Ruby logger itself fails.
183
+ pub(crate) fn report(message: fmt::Arguments<'_>) {
184
+ drop(writeln!(io::stderr().lock(), "{message}"));
185
+ }
186
+
187
+ /// Converts a Ruby number of seconds into a [`Duration`].
188
+ ///
189
+ /// # Errors
190
+ ///
191
+ /// Returns an error that names `option` if the value is negative, not
192
+ /// finite, or too large for a [`Duration`].
193
+ pub(crate) fn seconds(option: &str, value: f64) -> Result<Duration, String> {
194
+ Duration::try_from_secs_f64(value)
195
+ .map_err(|_| format!("{option}: must be a finite, non-negative number of seconds"))
196
+ }
197
+
198
+ /// Detects the use of a native object in a forked child process.
199
+ ///
200
+ /// The Tokio runtime and the bridge thread do not survive `fork`. A native
201
+ /// object that waits on them in a child waits forever, so it raises instead.
202
+ #[derive(Clone, Copy, Debug)]
203
+ pub struct ForkGuard {
204
+ /// The process that created the object.
205
+ pid: u32,
206
+ /// The Ruby class name that the error message shows.
207
+ class: &'static str,
208
+ }
209
+
210
+ impl ForkGuard {
211
+ /// Records the current process for an object of the Ruby `class`.
212
+ pub fn new(class: &'static str) -> Self {
213
+ Self {
214
+ pid: process::id(),
215
+ class,
216
+ }
217
+ }
218
+
219
+ /// Checks that the current process created the object.
220
+ ///
221
+ /// # Errors
222
+ ///
223
+ /// Returns a `RuntimeError` in a forked child process.
224
+ pub fn check(self, ruby: &Ruby) -> Result<(), Error> {
225
+ if process::id() == self.pid {
226
+ return Ok(());
227
+ }
228
+ Err(Error::new(
229
+ ruby.exception_runtime_error(),
230
+ format!(
231
+ "{} cannot be used after fork. Create a new client in the child process.",
232
+ self.class
233
+ ),
234
+ ))
235
+ }
236
+ }
237
+
182
238
  /// Ensures a Tokio runtime context exists, entering one if necessary.
183
239
  ///
184
240
  /// Only creates a runtime guard when not already in a runtime context, avoiding
@@ -193,13 +249,6 @@ impl Drop for RubyDrop {
193
249
  ///
194
250
  /// `Some(EnterGuard)` if we entered a new runtime (hold the guard), or `None`
195
251
  /// if already in a runtime context.
196
- ///
197
- /// # Examples
198
- ///
199
- /// ```rust
200
- /// let _guard = ensure_runtime_context(ruby);
201
- /// // Safe to perform async operations
202
- /// ```
203
252
  pub fn ensure_runtime_context(ruby: &Ruby) -> Option<EnterGuard<'static>> {
204
253
  let guard = Handle::try_current().is_err().then(|| RUNTIME.enter());
205
254
 
@@ -207,14 +256,13 @@ pub fn ensure_runtime_context(ruby: &Ruby) -> Option<EnterGuard<'static>> {
207
256
  let bridge = BRIDGE.get_or_init(|| Bridge::new(ruby));
208
257
 
209
258
  // Initialize tracing for observability
210
- #[allow(clippy::print_stderr, reason = "logger has not been initialized yet")]
211
259
  TRACING_INIT.get_or_init(|| {
212
260
  let maybe_logger = Logger::new(ruby, bridge.clone())
213
- .inspect_err(|error| eprintln!("failed to create logger: {error:#}"))
261
+ .inspect_err(|error| report(format_args!("failed to create logger: {error:#}")))
214
262
  .ok();
215
263
 
216
264
  if let Err(error) = initialize_tracing(maybe_logger) {
217
- eprintln!("failed to initialize tracing: {error:#}");
265
+ report(format_args!("failed to initialize tracing: {error:#}"));
218
266
  }
219
267
  });
220
268
 
@@ -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