prosody 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.cargo/config.toml +3 -0
- data/.release-please-manifest.json +1 -1
- data/AGENTS.md +395 -0
- data/ARCHITECTURE.md +14 -4
- data/CHANGELOG.md +28 -0
- data/CLAUDE.md +1 -0
- data/CONFIGURATION.md +167 -0
- data/Cargo.lock +1115 -645
- data/Cargo.toml +7 -6
- data/README.md +436 -146
- data/Rakefile +11 -1
- data/examples/keyed_state.rb +70 -0
- data/examples/keyed_state.rbs +18 -0
- data/examples/keyed_state_windowing.rb +55 -0
- data/examples/keyed_state_windowing.rbs +16 -0
- data/ext/prosody/Cargo.toml +1 -0
- data/ext/prosody/src/admin.rs +1 -5
- data/ext/prosody/src/bridge/mod.rs +17 -32
- data/ext/prosody/src/client/config.rs +501 -28
- data/ext/prosody/src/client/mod.rs +167 -74
- data/ext/prosody/src/client/request.rs +132 -0
- data/ext/prosody/src/client/support.rs +122 -0
- data/ext/prosody/src/handler/context.rs +150 -5
- data/ext/prosody/src/handler/message.rs +67 -0
- data/ext/prosody/src/handler/mod.rs +115 -85
- data/ext/prosody/src/handler/state/mod.rs +488 -0
- data/ext/prosody/src/handler/state/registration.rs +104 -0
- data/ext/prosody/src/handler/state/scan.rs +218 -0
- data/ext/prosody/src/lib.rs +15 -3
- data/ext/prosody/src/published.rs +273 -0
- data/ext/prosody/src/scheduler/mod.rs +2 -2
- data/ext/prosody/src/scheduler/processor.rs +2 -2
- data/ext/prosody/src/scheduler/result.rs +7 -4
- data/ext/prosody/src/util.rs +86 -5
- data/lib/prosody/configuration.rb +71 -11
- data/lib/prosody/handler.rb +65 -8
- data/lib/prosody/native_stubs.rb +550 -9
- data/lib/prosody/request.rb +45 -0
- data/lib/prosody/state.rb +816 -0
- data/lib/prosody/version.rb +1 -1
- data/lib/prosody.rb +6 -0
- data/release-please-config.json +4 -0
- data/sig/configuration.rbs +70 -11
- data/sig/handler.rbs +17 -5
- data/sig/processor.rbs +28 -12
- data/sig/prosody.rbs +53 -7
- data/sig/request.rbs +66 -0
- data/sig/sentry.rbs +6 -0
- data/sig/state.rbs +390 -0
- data/steep_expectations.yml +57 -0
- data/typecheck/payload_types.rb +54 -0
- data/typecheck/payload_types.rbs +22 -0
- data/typecheck_negative/payload_types.rb +20 -0
- data/typecheck_negative/payload_types.rbs +9 -0
- metadata +32 -9
|
@@ -13,25 +13,45 @@
|
|
|
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};
|
|
16
17
|
use crate::tracing_util::extract_opentelemetry_context;
|
|
17
18
|
use crate::util::ensure_runtime_context;
|
|
18
19
|
use crate::{BRIDGE, ROOT_MOD, id};
|
|
19
20
|
use educe::Educe;
|
|
21
|
+
use futures::FutureExt;
|
|
22
|
+
use futures::future::{BoxFuture, Shared};
|
|
20
23
|
use magnus::value::ReprValue;
|
|
21
|
-
use magnus::{
|
|
24
|
+
use magnus::{
|
|
25
|
+
Class, Error, Module, Object, RClass, RModule, Ruby, StaticSymbol, Value, function, kwargs,
|
|
26
|
+
method,
|
|
27
|
+
};
|
|
22
28
|
use opentelemetry::propagation::TextMapCompositePropagator;
|
|
29
|
+
use prosody::cassandra::config::CassandraConfigurationBuilder;
|
|
23
30
|
use prosody::high_level::ConsumerBuilders;
|
|
24
|
-
use prosody::high_level::
|
|
31
|
+
use prosody::high_level::erased::{
|
|
32
|
+
ErasedConsumerState, ErasedReadCache, SharedHighLevelClient, new_erased,
|
|
33
|
+
};
|
|
25
34
|
use prosody::high_level::mode::Mode;
|
|
26
|
-
use prosody::high_level::state::ConsumerState;
|
|
27
35
|
use prosody::propagator::new_propagator;
|
|
36
|
+
use prosody::requester::ResponseError;
|
|
37
|
+
use prosody::subsystem::SubsystemName;
|
|
38
|
+
use serde::Deserialize;
|
|
28
39
|
use serde_magnus::deserialize;
|
|
40
|
+
use serde_magnus::serialize;
|
|
29
41
|
use std::sync::Arc;
|
|
42
|
+
use std::time::Duration;
|
|
30
43
|
use tracing::{Span, debug, info_span};
|
|
31
44
|
use tracing_opentelemetry::OpenTelemetrySpanExt;
|
|
32
45
|
|
|
33
46
|
/// Configuration types and conversion between Ruby and Rust representations
|
|
34
47
|
mod config;
|
|
48
|
+
mod request;
|
|
49
|
+
mod support;
|
|
50
|
+
|
|
51
|
+
pub use support::init;
|
|
52
|
+
use support::{read_cache, response_error, shutdown, validate_handler};
|
|
53
|
+
|
|
54
|
+
type Shutdown = Shared<BoxFuture<'static, Result<(), Arc<str>>>>;
|
|
35
55
|
|
|
36
56
|
/// A Ruby-compatible wrapper around the Prosody high-level client.
|
|
37
57
|
///
|
|
@@ -44,11 +64,14 @@ mod config;
|
|
|
44
64
|
pub struct Client {
|
|
45
65
|
/// The underlying Prosody client
|
|
46
66
|
#[educe(Debug(ignore))]
|
|
47
|
-
inner:
|
|
67
|
+
inner: SharedHighLevelClient<RubyHandler>,
|
|
68
|
+
/// One shutdown operation shared by all callers
|
|
69
|
+
#[educe(Debug(ignore))]
|
|
70
|
+
shutdown: Shutdown,
|
|
48
71
|
/// Bridge for communicating between Rust and Ruby
|
|
49
72
|
bridge: Bridge,
|
|
50
73
|
/// OpenTelemetry propagator for distributed tracing
|
|
51
|
-
propagator: TextMapCompositePropagator
|
|
74
|
+
propagator: Arc<TextMapCompositePropagator>,
|
|
52
75
|
/// PID at construction time, used to detect post-fork usage
|
|
53
76
|
pid: u32,
|
|
54
77
|
}
|
|
@@ -96,14 +119,6 @@ impl Client {
|
|
|
96
119
|
.try_into()
|
|
97
120
|
.map_err(|error: String| Error::new(ruby.exception_arg_error(), error))?;
|
|
98
121
|
|
|
99
|
-
let client = HighLevelClient::new(
|
|
100
|
-
mode,
|
|
101
|
-
&mut config_ref.into(),
|
|
102
|
-
&consumer_builders,
|
|
103
|
-
&config_ref.into(),
|
|
104
|
-
)
|
|
105
|
-
.map_err(|error| Error::new(ruby.exception_runtime_error(), error.to_string()))?;
|
|
106
|
-
|
|
107
122
|
let bridge = BRIDGE
|
|
108
123
|
.get()
|
|
109
124
|
.ok_or(Error::new(
|
|
@@ -111,11 +126,23 @@ impl Client {
|
|
|
111
126
|
"Bridge not initialized",
|
|
112
127
|
))?
|
|
113
128
|
.clone();
|
|
129
|
+
let cassandra = Into::<CassandraConfigurationBuilder>::into(config_ref);
|
|
130
|
+
let mut producer = config_ref.into();
|
|
131
|
+
let client = bridge
|
|
132
|
+
.wait_for(
|
|
133
|
+
ruby,
|
|
134
|
+
async move {
|
|
135
|
+
new_erased(mode, &mut producer, &consumer_builders, &cassandra).await
|
|
136
|
+
},
|
|
137
|
+
Span::current(),
|
|
138
|
+
)?
|
|
139
|
+
.map_err(|error| Error::new(ruby.exception_runtime_error(), error.to_string()))?;
|
|
114
140
|
|
|
115
141
|
Ok(Self {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
142
|
+
shutdown: shutdown(&client),
|
|
143
|
+
inner: client,
|
|
144
|
+
bridge,
|
|
145
|
+
propagator: Arc::new(new_propagator()),
|
|
119
146
|
pid: std::process::id(),
|
|
120
147
|
})
|
|
121
148
|
}
|
|
@@ -133,7 +160,8 @@ impl Client {
|
|
|
133
160
|
|
|
134
161
|
/// Returns the current state of the consumer.
|
|
135
162
|
///
|
|
136
|
-
/// The consumer can be in one of
|
|
163
|
+
/// The consumer can be in one of four states:
|
|
164
|
+
/// - `:shut_down` - The client is shut down
|
|
137
165
|
/// - `:unconfigured` - The consumer has not been configured yet
|
|
138
166
|
/// - `:configured` - The consumer is configured but not running
|
|
139
167
|
/// - `:running` - The consumer is actively consuming messages
|
|
@@ -158,14 +186,14 @@ impl Client {
|
|
|
158
186
|
let state: Result<&'static str, String> = this.bridge.wait_for(
|
|
159
187
|
ruby,
|
|
160
188
|
async move {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
Err(format!("consumer configuration failed: {
|
|
189
|
+
match inner.consumer_state().await {
|
|
190
|
+
ErasedConsumerState::Shutdown => Ok("shut_down"),
|
|
191
|
+
ErasedConsumerState::Unconfigured => Ok("unconfigured"),
|
|
192
|
+
ErasedConsumerState::ConfigurationFailed(error) => {
|
|
193
|
+
Err(format!("consumer configuration failed: {error}"))
|
|
166
194
|
}
|
|
167
|
-
|
|
168
|
-
|
|
195
|
+
ErasedConsumerState::Configured(_) => Ok("configured"),
|
|
196
|
+
ErasedConsumerState::Running { .. } => Ok("running"),
|
|
169
197
|
}
|
|
170
198
|
},
|
|
171
199
|
Span::current(),
|
|
@@ -215,30 +243,39 @@ impl Client {
|
|
|
215
243
|
this.bridge
|
|
216
244
|
.wait_for(
|
|
217
245
|
ruby,
|
|
218
|
-
async move { client.send(topic.as_str().into(),
|
|
246
|
+
async move { client.send(topic.as_str().into(), key, value).await },
|
|
219
247
|
span,
|
|
220
248
|
)?
|
|
221
249
|
.map_err(|error| Error::new(ruby.exception_runtime_error(), format!("{error:#}")))
|
|
222
250
|
}
|
|
223
251
|
|
|
224
|
-
///
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
252
|
+
/// Sends an excise record for a key.
|
|
253
|
+
fn excise(ruby: &Ruby, this: &Self, topic: String, key: String) -> Result<(), Error> {
|
|
254
|
+
Self::check_fork(ruby, this)?;
|
|
255
|
+
let _guard = ensure_runtime_context(ruby);
|
|
256
|
+
let client = this.inner.clone();
|
|
257
|
+
let context = extract_opentelemetry_context(ruby, &this.propagator)?;
|
|
258
|
+
let span = info_span!("ruby-excise", %topic, %key);
|
|
259
|
+
if let Err(err) = span.set_parent(context) {
|
|
260
|
+
debug!("failed to set parent span: {err:#}");
|
|
261
|
+
}
|
|
262
|
+
this.bridge
|
|
263
|
+
.wait_for(
|
|
264
|
+
ruby,
|
|
265
|
+
async move { client.excise(topic.as_str().into(), key).await },
|
|
266
|
+
span,
|
|
267
|
+
)?
|
|
268
|
+
.map_err(|error| Error::new(ruby.exception_runtime_error(), format!("{error:#}")))
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/// Subscribes with a complete Ruby event handler.
|
|
234
272
|
///
|
|
235
273
|
/// # Errors
|
|
236
274
|
///
|
|
237
|
-
/// Returns an error if
|
|
238
|
-
/// - The handler cannot be wrapped
|
|
239
|
-
/// - The client cannot subscribe with the handler
|
|
275
|
+
/// Returns an error if the handler is incomplete or subscription fails.
|
|
240
276
|
fn subscribe(ruby: &Ruby, this: &Self, handler: Value) -> Result<(), Error> {
|
|
241
277
|
Self::check_fork(ruby, this)?;
|
|
278
|
+
validate_handler(ruby, handler)?;
|
|
242
279
|
let _guard = ensure_runtime_context(ruby);
|
|
243
280
|
let wrapper = RubyHandler::new(this.bridge.clone(), ruby, handler)?;
|
|
244
281
|
let inner = this.inner.clone();
|
|
@@ -327,6 +364,22 @@ impl Client {
|
|
|
327
364
|
.map_err(|error| Error::new(ruby.exception_runtime_error(), format!("{error:#}")))
|
|
328
365
|
}
|
|
329
366
|
|
|
367
|
+
/// Shuts down the client and all its services.
|
|
368
|
+
/// Concurrent and repeated calls wait for the same operation.
|
|
369
|
+
///
|
|
370
|
+
/// # Errors
|
|
371
|
+
///
|
|
372
|
+
/// Returns an error if shutdown fails.
|
|
373
|
+
fn shutdown(ruby: &Ruby, this: &Self) -> Result<(), Error> {
|
|
374
|
+
Self::check_fork(ruby, this)?;
|
|
375
|
+
let _guard = ensure_runtime_context(ruby);
|
|
376
|
+
let shutdown = this.shutdown.clone();
|
|
377
|
+
|
|
378
|
+
this.bridge
|
|
379
|
+
.wait_for(ruby, shutdown, Span::current())?
|
|
380
|
+
.map_err(|error| Error::new(ruby.exception_runtime_error(), format!("{error:#}")))
|
|
381
|
+
}
|
|
382
|
+
|
|
330
383
|
/// Returns the configured source system identifier.
|
|
331
384
|
///
|
|
332
385
|
/// The source system is used to identify the originating service or
|
|
@@ -342,41 +395,81 @@ impl Client {
|
|
|
342
395
|
fn source_system(this: &Self) -> &str {
|
|
343
396
|
this.inner.source_system()
|
|
344
397
|
}
|
|
345
|
-
}
|
|
346
398
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
)
|
|
380
|
-
|
|
381
|
-
|
|
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
|
+
}
|
|
382
475
|
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
use super::{
|
|
2
|
+
Class, Client, Deserialize, Duration, Error, Module, OpenTelemetrySpanExt, RClass, ROOT_MOD,
|
|
3
|
+
ReprValue, Ruby, SubsystemName, Value, debug, deserialize, ensure_runtime_context,
|
|
4
|
+
extract_opentelemetry_context, id, info_span, kwargs, response_error, serialize,
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
#[derive(Deserialize)]
|
|
8
|
+
struct NativeRequest {
|
|
9
|
+
topic: String,
|
|
10
|
+
key: String,
|
|
11
|
+
payload: serde_json::Value,
|
|
12
|
+
subsystems: Vec<String>,
|
|
13
|
+
timeout: f64,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
#[derive(Deserialize)]
|
|
17
|
+
struct NativeExciseRequest {
|
|
18
|
+
topic: String,
|
|
19
|
+
key: String,
|
|
20
|
+
subsystems: Vec<String>,
|
|
21
|
+
timeout: f64,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
pub(super) fn request(ruby: &Ruby, this: &Client, request: Value) -> Result<Value, Error> {
|
|
25
|
+
Client::check_fork(ruby, this)?;
|
|
26
|
+
let _guard = ensure_runtime_context(ruby);
|
|
27
|
+
let request: NativeRequest = deserialize(ruby, request)?;
|
|
28
|
+
let (subsystems, timeout) = request_parameters(ruby, request.subsystems, request.timeout)?;
|
|
29
|
+
let topic = prosody::Topic::from(request.topic.as_str());
|
|
30
|
+
let context = extract_opentelemetry_context(ruby, &this.propagator)?;
|
|
31
|
+
let span = info_span!("ruby-request", topic = %request.topic, key = %request.key);
|
|
32
|
+
if let Err(error) = span.set_parent(context) {
|
|
33
|
+
debug!("failed to set parent span: {error:#}");
|
|
34
|
+
}
|
|
35
|
+
let inner = this.inner.clone();
|
|
36
|
+
let results = this
|
|
37
|
+
.bridge
|
|
38
|
+
.wait_for(
|
|
39
|
+
ruby,
|
|
40
|
+
async move {
|
|
41
|
+
inner
|
|
42
|
+
.request(
|
|
43
|
+
Vec::new(),
|
|
44
|
+
topic,
|
|
45
|
+
request.key,
|
|
46
|
+
request.payload,
|
|
47
|
+
subsystems,
|
|
48
|
+
timeout,
|
|
49
|
+
)
|
|
50
|
+
.await
|
|
51
|
+
},
|
|
52
|
+
span,
|
|
53
|
+
)?
|
|
54
|
+
.map_err(|error| Error::new(ruby.exception_runtime_error(), error.to_string()))?;
|
|
55
|
+
|
|
56
|
+
request_outcomes(ruby, results)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
pub(super) fn request_excise(ruby: &Ruby, this: &Client, request: Value) -> Result<Value, Error> {
|
|
60
|
+
Client::check_fork(ruby, this)?;
|
|
61
|
+
let _guard = ensure_runtime_context(ruby);
|
|
62
|
+
let request: NativeExciseRequest = deserialize(ruby, request)?;
|
|
63
|
+
let (subsystems, timeout) = request_parameters(ruby, request.subsystems, request.timeout)?;
|
|
64
|
+
let topic = prosody::Topic::from(request.topic.as_str());
|
|
65
|
+
let context = extract_opentelemetry_context(ruby, &this.propagator)?;
|
|
66
|
+
let span = info_span!("ruby-request-excise", topic = %request.topic, key = %request.key);
|
|
67
|
+
if let Err(error) = span.set_parent(context) {
|
|
68
|
+
debug!("failed to set parent span: {error:#}");
|
|
69
|
+
}
|
|
70
|
+
let inner = this.inner.clone();
|
|
71
|
+
let results = this
|
|
72
|
+
.bridge
|
|
73
|
+
.wait_for(
|
|
74
|
+
ruby,
|
|
75
|
+
async move {
|
|
76
|
+
inner
|
|
77
|
+
.request_excise(Vec::new(), topic, request.key, subsystems, timeout)
|
|
78
|
+
.await
|
|
79
|
+
},
|
|
80
|
+
span,
|
|
81
|
+
)?
|
|
82
|
+
.map_err(|error| Error::new(ruby.exception_runtime_error(), error.to_string()))?;
|
|
83
|
+
|
|
84
|
+
request_outcomes(ruby, results)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
fn request_parameters(
|
|
88
|
+
ruby: &Ruby,
|
|
89
|
+
subsystems: Vec<String>,
|
|
90
|
+
timeout: f64,
|
|
91
|
+
) -> Result<(Vec<SubsystemName>, Duration), Error> {
|
|
92
|
+
let subsystems = subsystems
|
|
93
|
+
.into_iter()
|
|
94
|
+
.map(SubsystemName::try_new)
|
|
95
|
+
.collect::<Result<Vec<_>, _>>()
|
|
96
|
+
.map_err(|error| Error::new(ruby.exception_arg_error(), error.to_string()))?;
|
|
97
|
+
let timeout = Duration::try_from_secs_f64(timeout).map_err(|_| {
|
|
98
|
+
Error::new(
|
|
99
|
+
ruby.exception_arg_error(),
|
|
100
|
+
"timeout must be a finite, non-negative duration",
|
|
101
|
+
)
|
|
102
|
+
})?;
|
|
103
|
+
Ok((subsystems, timeout))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
fn request_outcomes<I>(ruby: &Ruby, results: I) -> Result<Value, Error>
|
|
107
|
+
where
|
|
108
|
+
I: IntoIterator<
|
|
109
|
+
Item = (
|
|
110
|
+
SubsystemName,
|
|
111
|
+
Result<serde_json::Value, prosody::requester::ResponseError>,
|
|
112
|
+
),
|
|
113
|
+
>,
|
|
114
|
+
{
|
|
115
|
+
let module = ruby.get_inner(&ROOT_MOD);
|
|
116
|
+
let outcomes = ruby.hash_new();
|
|
117
|
+
let success: RClass = module.const_get(id!(ruby, "Success"))?;
|
|
118
|
+
let failure: RClass = module.const_get(id!(ruby, "Failure"))?;
|
|
119
|
+
for (subsystem, result) in results {
|
|
120
|
+
let outcome = match result {
|
|
121
|
+
Ok(value) => {
|
|
122
|
+
let value: Value = serialize(ruby, &value)?;
|
|
123
|
+
success.new_instance((kwargs!(ruby, "value" => value),))?
|
|
124
|
+
}
|
|
125
|
+
Err(error) => failure.new_instance((kwargs!(
|
|
126
|
+
ruby, "error" => response_error(ruby, module, error)?
|
|
127
|
+
),))?,
|
|
128
|
+
};
|
|
129
|
+
outcomes.aset(subsystem.as_str(), outcome)?;
|
|
130
|
+
}
|
|
131
|
+
Ok(outcomes.as_value())
|
|
132
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
use super::{
|
|
2
|
+
Arc, Class, Client, Duration, ErasedReadCache, Error, FutureExt, Module, Object, RClass,
|
|
3
|
+
RModule, ROOT_MOD, ReprValue, ResponseError, Ruby, RubyHandler, SharedHighLevelClient,
|
|
4
|
+
Shutdown, Value, function, id, kwargs, method, request,
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
pub(super) fn validate_handler(ruby: &Ruby, handler: Value) -> Result<(), Error> {
|
|
8
|
+
let event_handler: RClass = ruby
|
|
9
|
+
.get_inner(&ROOT_MOD)
|
|
10
|
+
.const_get(id!(ruby, "EventHandler"))?;
|
|
11
|
+
let _: Value = event_handler.funcall(id!(ruby, "validate_handler!"), (handler,))?;
|
|
12
|
+
Ok(())
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
pub(super) fn shutdown(client: &SharedHighLevelClient<RubyHandler>) -> Shutdown {
|
|
16
|
+
let client = client.clone();
|
|
17
|
+
async move {
|
|
18
|
+
client
|
|
19
|
+
.shutdown()
|
|
20
|
+
.await
|
|
21
|
+
.map_err(|error| Arc::from(error.to_string()))
|
|
22
|
+
}
|
|
23
|
+
.boxed()
|
|
24
|
+
.shared()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
pub(super) fn read_cache(
|
|
28
|
+
ruby: &Ruby,
|
|
29
|
+
seconds: Option<f64>,
|
|
30
|
+
disabled: bool,
|
|
31
|
+
) -> Result<ErasedReadCache, Error> {
|
|
32
|
+
match (seconds, disabled) {
|
|
33
|
+
(None, false) => Ok(ErasedReadCache::Inherit),
|
|
34
|
+
(None, true) => Ok(ErasedReadCache::Disabled),
|
|
35
|
+
(Some(seconds), false) => Duration::try_from_secs_f64(seconds)
|
|
36
|
+
.map(ErasedReadCache::Ttl)
|
|
37
|
+
.map_err(|_| {
|
|
38
|
+
Error::new(
|
|
39
|
+
ruby.exception_arg_error(),
|
|
40
|
+
"read_cache must be finite and non-negative",
|
|
41
|
+
)
|
|
42
|
+
}),
|
|
43
|
+
(Some(_), true) => Err(Error::new(
|
|
44
|
+
ruby.exception_arg_error(),
|
|
45
|
+
"read_cache cannot specify a TTL and be disabled",
|
|
46
|
+
)),
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// Initializes the client module in Ruby.
|
|
51
|
+
///
|
|
52
|
+
/// Defines the `Prosody::Client` class and its methods, making the client
|
|
53
|
+
/// functionality available to Ruby code.
|
|
54
|
+
///
|
|
55
|
+
/// # Arguments
|
|
56
|
+
///
|
|
57
|
+
/// * `ruby` - The Ruby VM context
|
|
58
|
+
///
|
|
59
|
+
/// # Errors
|
|
60
|
+
///
|
|
61
|
+
/// Returns an error if Ruby class or method definition fails.
|
|
62
|
+
pub fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
63
|
+
let module = ruby.get_inner(&ROOT_MOD);
|
|
64
|
+
let class = module.define_class(id!(ruby, "Client"), ruby.class_object())?;
|
|
65
|
+
|
|
66
|
+
class.define_singleton_method("new", function!(Client::new, 1))?;
|
|
67
|
+
class.define_method(
|
|
68
|
+
id!(ruby, "consumer_state"),
|
|
69
|
+
method!(Client::consumer_state, 0),
|
|
70
|
+
)?;
|
|
71
|
+
class.define_method(id!(ruby, "send_message"), method!(Client::send, 3))?;
|
|
72
|
+
class.define_method(id!(ruby, "excise"), method!(Client::excise, 2))?;
|
|
73
|
+
class.define_method(id!(ruby, "native_request"), method!(request::request, 1))?;
|
|
74
|
+
class.define_method(
|
|
75
|
+
id!(ruby, "native_request_excise"),
|
|
76
|
+
method!(request::request_excise, 1),
|
|
77
|
+
)?;
|
|
78
|
+
class.define_method(id!(ruby, "subscribe"), method!(Client::subscribe, 1))?;
|
|
79
|
+
class.define_method(
|
|
80
|
+
id!(ruby, "assigned_partitions"),
|
|
81
|
+
method!(Client::assigned_partitions, 0),
|
|
82
|
+
)?;
|
|
83
|
+
class.define_method(id!(ruby, "is_stalled?"), method!(Client::is_stalled, 0))?;
|
|
84
|
+
class.define_method(id!(ruby, "unsubscribe"), method!(Client::unsubscribe, 0))?;
|
|
85
|
+
class.define_method(id!(ruby, "shutdown"), method!(Client::shutdown, 0))?;
|
|
86
|
+
class.define_method(
|
|
87
|
+
id!(ruby, "source_system"),
|
|
88
|
+
method!(Client::source_system, 0),
|
|
89
|
+
)?;
|
|
90
|
+
class.define_method(
|
|
91
|
+
id!(ruby, "published_value"),
|
|
92
|
+
method!(Client::published_value, 4),
|
|
93
|
+
)?;
|
|
94
|
+
class.define_method(
|
|
95
|
+
id!(ruby, "published_map"),
|
|
96
|
+
method!(Client::published_map, 4),
|
|
97
|
+
)?;
|
|
98
|
+
class.define_method(
|
|
99
|
+
id!(ruby, "published_deque"),
|
|
100
|
+
method!(Client::published_deque, 4),
|
|
101
|
+
)?;
|
|
102
|
+
|
|
103
|
+
Ok(())
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
pub(super) fn response_error(
|
|
107
|
+
ruby: &Ruby,
|
|
108
|
+
module: RModule,
|
|
109
|
+
error: ResponseError,
|
|
110
|
+
) -> Result<Value, Error> {
|
|
111
|
+
let (name, message) = match error {
|
|
112
|
+
ResponseError::Handler { message } => ("HandlerError", Some(message)),
|
|
113
|
+
ResponseError::Timeout => ("Timeout", None),
|
|
114
|
+
ResponseError::FormatMismatch => ("FormatMismatch", None),
|
|
115
|
+
ResponseError::Malformed => ("MalformedResponse", None),
|
|
116
|
+
};
|
|
117
|
+
let class: RClass = module.const_get(name)?;
|
|
118
|
+
match message {
|
|
119
|
+
Some(message) => class.new_instance((kwargs!(ruby, "message" => message),)),
|
|
120
|
+
None => class.new_instance(()),
|
|
121
|
+
}
|
|
122
|
+
}
|