prosody 0.2.1 → 0.4.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/.release-please-manifest.json +1 -1
- data/ARCHITECTURE.md +12 -2
- data/CHANGELOG.md +20 -0
- data/Cargo.lock +586 -450
- data/Cargo.toml +6 -6
- data/README.md +211 -20
- data/Rakefile +11 -1
- data/examples/keyed_state.rb +58 -0
- data/examples/keyed_state.rbs +18 -0
- data/examples/keyed_state_windowing.rb +47 -0
- data/examples/keyed_state_windowing.rbs +16 -0
- data/ext/prosody/Cargo.toml +1 -1
- data/ext/prosody/src/client/config.rs +387 -19
- data/ext/prosody/src/handler/context.rs +146 -5
- data/ext/prosody/src/handler/message.rs +17 -0
- data/ext/prosody/src/handler/mod.rs +4 -2
- data/ext/prosody/src/handler/state.rs +1035 -0
- data/ext/prosody/src/lib.rs +1 -0
- data/lib/prosody/configuration.rb +26 -0
- data/lib/prosody/handler.rb +6 -2
- data/lib/prosody/native_stubs.rb +381 -6
- data/lib/prosody/state.rb +693 -0
- data/lib/prosody/version.rb +1 -1
- data/lib/prosody.rb +5 -0
- data/release-please-config.json +4 -0
- data/sig/configuration.rbs +24 -1
- data/sig/handler.rbs +7 -3
- data/sig/processor.rbs +28 -12
- data/sig/prosody.rbs +11 -6
- data/sig/sentry.rbs +6 -0
- data/sig/state.rbs +272 -0
- data/steep_expectations.yml +47 -0
- data/typecheck/payload_types.rb +43 -0
- data/typecheck/payload_types.rbs +20 -0
- data/typecheck_negative/payload_types.rb +16 -0
- data/typecheck_negative/payload_types.rbs +8 -0
- metadata +24 -11
|
@@ -0,0 +1,1035 @@
|
|
|
1
|
+
//! Native layer for keyed state.
|
|
2
|
+
//!
|
|
3
|
+
//! Wraps the boxed erased handles from [`prosody::consumer::event_context`] as
|
|
4
|
+
//! Magnus classes. Collections are addressed by name; JSON payloads cross as
|
|
5
|
+
//! `serde_json::Value` via `serde_magnus` (exactly like [`Message`] payloads),
|
|
6
|
+
//! and Kafka-message items cross as the same [`Message`] object handlers
|
|
7
|
+
//! already receive.
|
|
8
|
+
//!
|
|
9
|
+
//! Every operation flows through [`Bridge::wait_for`]: the calling fiber yields
|
|
10
|
+
//! (via the fiber-scheduler-integrated `Queue#pop`) while tokio drives the
|
|
11
|
+
//! erased future, so the call looks blocking but never blocks the thread. The
|
|
12
|
+
//! extracted Ruby OpenTelemetry carrier is activated
|
|
13
|
+
//! (`FutureExt::with_context`) while core polls, so core's one semantic
|
|
14
|
+
//! collection span joins the event trace with no binding span.
|
|
15
|
+
//! Ruby↔`serde_json::Value` conversion runs on the Ruby thread *after*
|
|
16
|
+
//! `wait_for` returns, mirroring [`Message::payload`].
|
|
17
|
+
//!
|
|
18
|
+
//! Errors are STRUCTURAL: [`ErasedStateError::category`] selects the Ruby class
|
|
19
|
+
//! directly, and because [`crate::PermanentStateError`]/`TransientStateError`
|
|
20
|
+
//! subclass the existing `PermanentError`/`TransientError`, a rethrown state
|
|
21
|
+
//! error classifies correctly with no result-bridge change. Every caller
|
|
22
|
+
//! mistake the glue detects (a null write, a wrong item shape, an invalid
|
|
23
|
+
//! direction token, an unrepresentable value) rejects TRANSIENT so the message
|
|
24
|
+
//! retries and stays visible rather than being discarded.
|
|
25
|
+
//!
|
|
26
|
+
//! # Cancellation honesty
|
|
27
|
+
//!
|
|
28
|
+
//! `Async::Stop` may unwind the waiting fiber while the dispatched tokio op
|
|
29
|
+
//! completes detached — its effect landed before the boundary or is
|
|
30
|
+
//! epoch-fenced by core, and the result channel is simply dropped. For a
|
|
31
|
+
//! [`StateScan`] pull, the orphaned chunk is lost, but on cancellation the
|
|
32
|
+
//! whole attempt aborts and the scan is closed via the Ruby `ensure`, so the
|
|
33
|
+
//! dropped chunk is moot. Adding an in-flight replay slot would be new
|
|
34
|
+
//! architecture and would reimplement cancellation safety the contract assigns
|
|
35
|
+
//! to core.
|
|
36
|
+
|
|
37
|
+
use crate::bridge::{Bridge, QUEUE_CLASS};
|
|
38
|
+
use crate::handler::message::Message;
|
|
39
|
+
use crate::tracing_util::extract_opentelemetry_context;
|
|
40
|
+
use crate::util::ThreadSafeValue;
|
|
41
|
+
use crate::{ROOT_MOD, id};
|
|
42
|
+
use magnus::value::{Lazy, ReprValue};
|
|
43
|
+
use magnus::{Error, ExceptionClass, IntoValue, Module, Ruby, TryConvert, Value, method};
|
|
44
|
+
use opentelemetry::propagation::TextMapCompositePropagator;
|
|
45
|
+
use opentelemetry::trace::FutureExt;
|
|
46
|
+
use prosody::consumer::event_context::{
|
|
47
|
+
DynDequeState, DynMapState, DynValueState, ErasedCategory, ErasedStateError, StateCursor,
|
|
48
|
+
};
|
|
49
|
+
use prosody::consumer::message::ConsumerMessage;
|
|
50
|
+
use prosody::state::Direction;
|
|
51
|
+
use serde_json::Value as JsonValue;
|
|
52
|
+
use serde_magnus::{deserialize, serialize};
|
|
53
|
+
use std::cell::RefCell;
|
|
54
|
+
use std::collections::VecDeque;
|
|
55
|
+
use std::num::NonZeroUsize;
|
|
56
|
+
use std::sync::Arc;
|
|
57
|
+
use tracing::Span;
|
|
58
|
+
|
|
59
|
+
/// Lazily resolved `Prosody::PermanentStateError` class (defined in Ruby).
|
|
60
|
+
#[allow(
|
|
61
|
+
clippy::expect_used,
|
|
62
|
+
reason = "mirrors bridge.rs QUEUE_CLASS Lazy pattern"
|
|
63
|
+
)]
|
|
64
|
+
static PERMANENT_STATE_ERROR: Lazy<ExceptionClass> = Lazy::new(|ruby| {
|
|
65
|
+
ruby.get_inner(&ROOT_MOD)
|
|
66
|
+
.const_get("PermanentStateError")
|
|
67
|
+
.expect("Prosody::PermanentStateError")
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
/// Lazily resolved `Prosody::TransientStateError` class (defined in Ruby).
|
|
71
|
+
#[allow(
|
|
72
|
+
clippy::expect_used,
|
|
73
|
+
reason = "mirrors bridge.rs QUEUE_CLASS Lazy pattern"
|
|
74
|
+
)]
|
|
75
|
+
static TRANSIENT_STATE_ERROR: Lazy<ExceptionClass> = Lazy::new(|ruby| {
|
|
76
|
+
ruby.get_inner(&ROOT_MOD)
|
|
77
|
+
.const_get("TransientStateError")
|
|
78
|
+
.expect("Prosody::TransientStateError")
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
/// Lazily resolved `Prosody::NullValueError` class (defined in Ruby).
|
|
82
|
+
#[allow(
|
|
83
|
+
clippy::expect_used,
|
|
84
|
+
reason = "mirrors bridge.rs QUEUE_CLASS Lazy pattern"
|
|
85
|
+
)]
|
|
86
|
+
static NULL_VALUE_ERROR: Lazy<ExceptionClass> = Lazy::new(|ruby| {
|
|
87
|
+
ruby.get_inner(&ROOT_MOD)
|
|
88
|
+
.const_get("NullValueError")
|
|
89
|
+
.expect("Prosody::NullValueError")
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
/// Maximum number of immediately-ready scan items transported in one chunk.
|
|
93
|
+
/// Core owns ready-draining, error ordering, and pull serialization; this
|
|
94
|
+
/// binding owns only the transport cap and conversion.
|
|
95
|
+
#[allow(clippy::unwrap_used, reason = "256 is a nonzero literal; mirrors core")]
|
|
96
|
+
const SCAN_READY_CHUNK_SIZE: NonZeroUsize = NonZeroUsize::new(256).unwrap();
|
|
97
|
+
|
|
98
|
+
/// Maps an erased state error to the matching Ruby state-error class.
|
|
99
|
+
///
|
|
100
|
+
/// The category is data, so this never parses the human message. Because the
|
|
101
|
+
/// Ruby classes subclass the existing error hierarchy, the result bridge's
|
|
102
|
+
/// `#permanent?` path reclassifies a rethrown error with no change.
|
|
103
|
+
pub(crate) fn state_error(ruby: &Ruby, error: &ErasedStateError) -> Error {
|
|
104
|
+
let class = match error.category() {
|
|
105
|
+
ErasedCategory::Permanent => ruby.get_inner(&PERMANENT_STATE_ERROR),
|
|
106
|
+
ErasedCategory::Transient => ruby.get_inner(&TRANSIENT_STATE_ERROR),
|
|
107
|
+
};
|
|
108
|
+
Error::new(class, error.message().to_owned())
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/// Builds a transient state error for a caller-caused condition the glue
|
|
112
|
+
/// detects (a wrong item shape, an invalid direction token, an unrepresentable
|
|
113
|
+
/// value).
|
|
114
|
+
fn transient_state_error(ruby: &Ruby, message: String) -> Error {
|
|
115
|
+
Error::new(ruby.get_inner(&TRANSIENT_STATE_ERROR), message)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/// Builds a null-value error for a rejected JSON-null write.
|
|
119
|
+
fn null_value_error(ruby: &Ruby, message: String) -> Error {
|
|
120
|
+
Error::new(ruby.get_inner(&NULL_VALUE_ERROR), message)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/// Converts an optional JSON item into a Ruby value or `nil`.
|
|
124
|
+
fn json_or_nil(ruby: &Ruby, item: Option<JsonValue>) -> Result<Value, Error> {
|
|
125
|
+
match item {
|
|
126
|
+
Some(value) => serialize(ruby, &value),
|
|
127
|
+
None => Ok(ruby.qnil().as_value()),
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/// Converts an optional message item into a `Prosody::Message` or `nil`.
|
|
132
|
+
#[allow(
|
|
133
|
+
clippy::unnecessary_wraps,
|
|
134
|
+
reason = "parity with json_or_nil for uniform call sites"
|
|
135
|
+
)]
|
|
136
|
+
fn message_or_nil(ruby: &Ruby, item: Option<ConsumerMessage<JsonValue>>) -> Result<Value, Error> {
|
|
137
|
+
match item {
|
|
138
|
+
Some(message) => Ok(Message::from(message).into_value_with(ruby)),
|
|
139
|
+
None => Ok(ruby.qnil().as_value()),
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/// Converts a Ruby argument into a storable JSON item.
|
|
144
|
+
///
|
|
145
|
+
/// A value with no JSON representation is a caller mistake and rejects
|
|
146
|
+
/// transient. JSON `null` is rejected with a [`crate::NullValueError`] naming
|
|
147
|
+
/// the deletion verb via `null_advice`.
|
|
148
|
+
fn json_write_item(ruby: &Ruby, value: Value, null_advice: &str) -> Result<JsonValue, Error> {
|
|
149
|
+
let value: JsonValue = deserialize(ruby, value).map_err(|error| {
|
|
150
|
+
transient_state_error(ruby, format!("value is not representable as JSON: {error}"))
|
|
151
|
+
})?;
|
|
152
|
+
if value.is_null() {
|
|
153
|
+
return Err(null_value_error(
|
|
154
|
+
ruby,
|
|
155
|
+
format!("JSON null is not a storable value{null_advice}"),
|
|
156
|
+
));
|
|
157
|
+
}
|
|
158
|
+
Ok(value)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/// Converts a Ruby argument into a storable message item.
|
|
162
|
+
///
|
|
163
|
+
/// A non-message argument is a caller mistake (a JSON payload cannot be stored
|
|
164
|
+
/// in a message collection) and rejects transient. The wrapped
|
|
165
|
+
/// [`ConsumerMessage`] is cloned; see [`Message::consumer_message`].
|
|
166
|
+
fn message_write_item(
|
|
167
|
+
ruby: &Ruby,
|
|
168
|
+
value: Value,
|
|
169
|
+
shape_advice: &str,
|
|
170
|
+
) -> Result<ConsumerMessage<JsonValue>, Error> {
|
|
171
|
+
let message = <&Message>::try_convert(value).map_err(|_| {
|
|
172
|
+
transient_state_error(ruby, format!("expected a Prosody::Message{shape_advice}"))
|
|
173
|
+
})?;
|
|
174
|
+
Ok(message.consumer_message())
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/// Parses a scan-direction token into the core [`Direction`].
|
|
178
|
+
///
|
|
179
|
+
/// An invalid token is a caller mistake and rejects transient.
|
|
180
|
+
fn parse_direction(ruby: &Ruby, direction: &str) -> Result<Direction, Error> {
|
|
181
|
+
match direction {
|
|
182
|
+
"forward" => Ok(Direction::Forward),
|
|
183
|
+
"backward" => Ok(Direction::Backward),
|
|
184
|
+
other => Err(transient_state_error(
|
|
185
|
+
ruby,
|
|
186
|
+
format!("direction: expected \"forward\" or \"backward\", got {other:?}"),
|
|
187
|
+
)),
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/// Drives an erased async op that returns `Result<_, ErasedStateError>` through
|
|
192
|
+
/// [`Bridge::wait_for`] with the extracted carrier active, yielding the op's
|
|
193
|
+
/// `Ok` value (state error mapped to the matching Ruby class).
|
|
194
|
+
macro_rules! run_op {
|
|
195
|
+
($ruby:expr, $this:expr, $handle:expr, $call:ident ( $($arg:expr),* )) => {{
|
|
196
|
+
let handle = Arc::clone($handle);
|
|
197
|
+
let context = extract_opentelemetry_context($ruby, &$this.propagator)?;
|
|
198
|
+
$this
|
|
199
|
+
.bridge
|
|
200
|
+
.wait_for(
|
|
201
|
+
$ruby,
|
|
202
|
+
async move { handle.$call($($arg),*).with_context(context).await },
|
|
203
|
+
Span::current(),
|
|
204
|
+
)?
|
|
205
|
+
.map_err(|error| state_error($ruby, &error))
|
|
206
|
+
}};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/// Drives an infallible erased async op (returning `()`) through
|
|
210
|
+
/// [`Bridge::wait_for`] with the extracted carrier active.
|
|
211
|
+
macro_rules! run_infallible {
|
|
212
|
+
($ruby:expr, $this:expr, $handle:expr, $call:ident ()) => {{
|
|
213
|
+
let handle = Arc::clone($handle);
|
|
214
|
+
let context = extract_opentelemetry_context($ruby, &$this.propagator)?;
|
|
215
|
+
$this.bridge.wait_for(
|
|
216
|
+
$ruby,
|
|
217
|
+
async move { handle.$call().with_context(context).await },
|
|
218
|
+
Span::current(),
|
|
219
|
+
)?
|
|
220
|
+
}};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/// The two payload flavours a value handle wraps.
|
|
224
|
+
pub(crate) enum ValueStateVariant {
|
|
225
|
+
/// A JSON value collection.
|
|
226
|
+
Json(Arc<dyn DynValueState<JsonValue>>),
|
|
227
|
+
/// A Kafka-message collection.
|
|
228
|
+
Message(Arc<dyn DynValueState<ConsumerMessage<JsonValue>>>),
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/// The two payload flavours a map handle wraps.
|
|
232
|
+
pub(crate) enum MapStateVariant {
|
|
233
|
+
/// A JSON value collection.
|
|
234
|
+
Json(Arc<dyn DynMapState<JsonValue>>),
|
|
235
|
+
/// A Kafka-message collection.
|
|
236
|
+
Message(Arc<dyn DynMapState<ConsumerMessage<JsonValue>>>),
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/// The two payload flavours a deque handle wraps.
|
|
240
|
+
pub(crate) enum DequeStateVariant {
|
|
241
|
+
/// A JSON value collection.
|
|
242
|
+
Json(Arc<dyn DynDequeState<JsonValue>>),
|
|
243
|
+
/// A Kafka-message collection.
|
|
244
|
+
Message(Arc<dyn DynDequeState<ConsumerMessage<JsonValue>>>),
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/// Native single-value state handle, vended per event.
|
|
248
|
+
#[magnus::wrap(class = "Prosody::NativeValueState")]
|
|
249
|
+
pub struct NativeValueState {
|
|
250
|
+
state: ValueStateVariant,
|
|
251
|
+
bridge: Bridge,
|
|
252
|
+
propagator: Arc<TextMapCompositePropagator>,
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
impl NativeValueState {
|
|
256
|
+
/// Wraps a vended value handle with the bridge and propagator.
|
|
257
|
+
pub(crate) fn new(
|
|
258
|
+
state: ValueStateVariant,
|
|
259
|
+
bridge: Bridge,
|
|
260
|
+
propagator: Arc<TextMapCompositePropagator>,
|
|
261
|
+
) -> Self {
|
|
262
|
+
Self {
|
|
263
|
+
state,
|
|
264
|
+
bridge,
|
|
265
|
+
propagator,
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/// Reads the current value (`nil` when absent).
|
|
270
|
+
fn get(ruby: &Ruby, this: &Self) -> Result<Value, Error> {
|
|
271
|
+
match &this.state {
|
|
272
|
+
ValueStateVariant::Json(handle) => {
|
|
273
|
+
json_or_nil(ruby, run_op!(ruby, this, handle, get())?)
|
|
274
|
+
}
|
|
275
|
+
ValueStateVariant::Message(handle) => {
|
|
276
|
+
message_or_nil(ruby, run_op!(ruby, this, handle, get())?)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/// Buffers a write of the value. Rejects JSON null and item-shape mismatch.
|
|
282
|
+
fn set(ruby: &Ruby, this: &Self, value: Value) -> Result<Value, Error> {
|
|
283
|
+
match &this.state {
|
|
284
|
+
ValueStateVariant::Json(handle) => {
|
|
285
|
+
let item = json_write_item(ruby, value, "; use clear to remove the value")?;
|
|
286
|
+
run_op!(ruby, this, handle, set(item))?;
|
|
287
|
+
}
|
|
288
|
+
ValueStateVariant::Message(handle) => {
|
|
289
|
+
let item = message_write_item(
|
|
290
|
+
ruby,
|
|
291
|
+
value,
|
|
292
|
+
"; use clear to delete a message value collection",
|
|
293
|
+
)?;
|
|
294
|
+
run_op!(ruby, this, handle, set(item))?;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
Ok(ruby.qnil().as_value())
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/// Buffers a clear of the value.
|
|
301
|
+
fn clear(ruby: &Ruby, this: &Self) -> Result<Value, Error> {
|
|
302
|
+
match &this.state {
|
|
303
|
+
ValueStateVariant::Json(handle) => run_op!(ruby, this, handle, clear())?,
|
|
304
|
+
ValueStateVariant::Message(handle) => run_op!(ruby, this, handle, clear())?,
|
|
305
|
+
}
|
|
306
|
+
Ok(ruby.qnil().as_value())
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/// Durably commits the buffered operations. Returns `nil`: the erased FFI
|
|
310
|
+
/// seam drops the applied/no-op outcome (owner-ratified divergence from the
|
|
311
|
+
/// `:applied|:noop` naming; surfacing it requires a core change).
|
|
312
|
+
fn commit(ruby: &Ruby, this: &Self) -> Result<Value, Error> {
|
|
313
|
+
match &this.state {
|
|
314
|
+
ValueStateVariant::Json(handle) => run_op!(ruby, this, handle, commit())?,
|
|
315
|
+
ValueStateVariant::Message(handle) => run_op!(ruby, this, handle, commit())?,
|
|
316
|
+
}
|
|
317
|
+
Ok(ruby.qnil().as_value())
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/// Discards the buffered uncommitted operations. Returns `nil` (see
|
|
321
|
+
/// [`commit`](Self::commit)).
|
|
322
|
+
fn rollback(ruby: &Ruby, this: &Self) -> Result<Value, Error> {
|
|
323
|
+
match &this.state {
|
|
324
|
+
ValueStateVariant::Json(handle) => run_infallible!(ruby, this, handle, rollback()),
|
|
325
|
+
ValueStateVariant::Message(handle) => run_infallible!(ruby, this, handle, rollback()),
|
|
326
|
+
}
|
|
327
|
+
Ok(ruby.qnil().as_value())
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/// Native ordered-map state handle, keyed by `String`, vended per event.
|
|
332
|
+
#[magnus::wrap(class = "Prosody::NativeMapState")]
|
|
333
|
+
pub struct NativeMapState {
|
|
334
|
+
state: MapStateVariant,
|
|
335
|
+
bridge: Bridge,
|
|
336
|
+
propagator: Arc<TextMapCompositePropagator>,
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
impl NativeMapState {
|
|
340
|
+
/// Wraps a vended map handle with the bridge and propagator.
|
|
341
|
+
pub(crate) fn new(
|
|
342
|
+
state: MapStateVariant,
|
|
343
|
+
bridge: Bridge,
|
|
344
|
+
propagator: Arc<TextMapCompositePropagator>,
|
|
345
|
+
) -> Self {
|
|
346
|
+
Self {
|
|
347
|
+
state,
|
|
348
|
+
bridge,
|
|
349
|
+
propagator,
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/// Reads the value for `key` (`nil` when absent).
|
|
354
|
+
fn get(ruby: &Ruby, this: &Self, key: String) -> Result<Value, Error> {
|
|
355
|
+
match &this.state {
|
|
356
|
+
MapStateVariant::Json(handle) => {
|
|
357
|
+
json_or_nil(ruby, run_op!(ruby, this, handle, get(key))?)
|
|
358
|
+
}
|
|
359
|
+
MapStateVariant::Message(handle) => {
|
|
360
|
+
message_or_nil(ruby, run_op!(ruby, this, handle, get(key))?)
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/// Answers whether a stored cell exists for `key`, read through the event's
|
|
366
|
+
/// dirty overlay. No value decode and no resolver run — a message-backed
|
|
367
|
+
/// map answers presence with zero Kafka fetches — but not no-I/O: a
|
|
368
|
+
/// cache miss still reads the store.
|
|
369
|
+
fn contains_key(ruby: &Ruby, this: &Self, key: String) -> Result<Value, Error> {
|
|
370
|
+
let present = match &this.state {
|
|
371
|
+
MapStateVariant::Json(handle) => run_op!(ruby, this, handle, contains_key(key))?,
|
|
372
|
+
MapStateVariant::Message(handle) => run_op!(ruby, this, handle, contains_key(key))?,
|
|
373
|
+
};
|
|
374
|
+
Ok(present.into_value_with(ruby))
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/// Reads several keys as one isolated batch, one result per input key.
|
|
378
|
+
fn get_many(ruby: &Ruby, this: &Self, keys: Vec<String>) -> Result<Value, Error> {
|
|
379
|
+
match &this.state {
|
|
380
|
+
MapStateVariant::Json(handle) => {
|
|
381
|
+
let items = run_op!(ruby, this, handle, get_many(keys))?;
|
|
382
|
+
let array =
|
|
383
|
+
ruby.ary_try_from_iter(items.into_iter().map(|item| json_or_nil(ruby, item)))?;
|
|
384
|
+
Ok(array.as_value())
|
|
385
|
+
}
|
|
386
|
+
MapStateVariant::Message(handle) => {
|
|
387
|
+
let items = run_op!(ruby, this, handle, get_many(keys))?;
|
|
388
|
+
let array = ruby
|
|
389
|
+
.ary_try_from_iter(items.into_iter().map(|item| message_or_nil(ruby, item)))?;
|
|
390
|
+
Ok(array.as_value())
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/// Inserts or overwrites `key`. Rejects JSON null and item-shape mismatch.
|
|
396
|
+
fn set(ruby: &Ruby, this: &Self, key: String, value: Value) -> Result<Value, Error> {
|
|
397
|
+
match &this.state {
|
|
398
|
+
MapStateVariant::Json(handle) => {
|
|
399
|
+
let item = json_write_item(ruby, value, "; use delete(key) to remove the entry")?;
|
|
400
|
+
run_op!(ruby, this, handle, set(key, item))?;
|
|
401
|
+
}
|
|
402
|
+
MapStateVariant::Message(handle) => {
|
|
403
|
+
let item = message_write_item(
|
|
404
|
+
ruby,
|
|
405
|
+
value,
|
|
406
|
+
"; use delete(key) to remove a message map entry",
|
|
407
|
+
)?;
|
|
408
|
+
run_op!(ruby, this, handle, set(key, item))?;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
Ok(ruby.qnil().as_value())
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/// Removes `key`.
|
|
415
|
+
fn remove(ruby: &Ruby, this: &Self, key: String) -> Result<Value, Error> {
|
|
416
|
+
match &this.state {
|
|
417
|
+
MapStateVariant::Json(handle) => run_op!(ruby, this, handle, remove(key))?,
|
|
418
|
+
MapStateVariant::Message(handle) => run_op!(ruby, this, handle, remove(key))?,
|
|
419
|
+
}
|
|
420
|
+
Ok(ruby.qnil().as_value())
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/// Removes every entry.
|
|
424
|
+
fn clear(ruby: &Ruby, this: &Self) -> Result<Value, Error> {
|
|
425
|
+
match &this.state {
|
|
426
|
+
MapStateVariant::Json(handle) => run_op!(ruby, this, handle, clear())?,
|
|
427
|
+
MapStateVariant::Message(handle) => run_op!(ruby, this, handle, clear())?,
|
|
428
|
+
}
|
|
429
|
+
Ok(ruby.qnil().as_value())
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/// Opens a cursor over the live entries in key order, yielding `(key,
|
|
433
|
+
/// value)` pairs. Synchronous; the carrier is active while core constructs
|
|
434
|
+
/// the stream span.
|
|
435
|
+
#[allow(clippy::needless_pass_by_value, reason = "Magnus method argument type")]
|
|
436
|
+
fn scan(ruby: &Ruby, this: &Self, direction: String) -> Result<StateScan, Error> {
|
|
437
|
+
let dir = parse_direction(ruby, &direction)?;
|
|
438
|
+
let _guard = extract_opentelemetry_context(ruby, &this.propagator)?.attach();
|
|
439
|
+
let inner = match &this.state {
|
|
440
|
+
MapStateVariant::Json(handle) => ScanInner::MapJson {
|
|
441
|
+
cursor: Arc::from(handle.scan(dir)),
|
|
442
|
+
buffer: VecDeque::new(),
|
|
443
|
+
done: false,
|
|
444
|
+
},
|
|
445
|
+
MapStateVariant::Message(handle) => ScanInner::MapMessage {
|
|
446
|
+
cursor: Arc::from(handle.scan(dir)),
|
|
447
|
+
buffer: VecDeque::new(),
|
|
448
|
+
done: false,
|
|
449
|
+
},
|
|
450
|
+
};
|
|
451
|
+
StateScan::new(
|
|
452
|
+
ruby,
|
|
453
|
+
inner,
|
|
454
|
+
this.bridge.clone(),
|
|
455
|
+
Arc::clone(&this.propagator),
|
|
456
|
+
)
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/// Opens a cursor over the live keys in key order, yielding bare `String`
|
|
460
|
+
/// keys. Like [`scan`](Self::scan) but skips value decode and the resolver
|
|
461
|
+
/// — a message-backed map enumerates keys with zero Kafka fetches, though
|
|
462
|
+
/// not no-I/O. Synchronous; the carrier is active while core constructs the
|
|
463
|
+
/// stream span.
|
|
464
|
+
#[allow(clippy::needless_pass_by_value, reason = "Magnus method argument type")]
|
|
465
|
+
fn keys(ruby: &Ruby, this: &Self, direction: String) -> Result<StateScan, Error> {
|
|
466
|
+
let dir = parse_direction(ruby, &direction)?;
|
|
467
|
+
let _guard = extract_opentelemetry_context(ruby, &this.propagator)?.attach();
|
|
468
|
+
let inner = match &this.state {
|
|
469
|
+
MapStateVariant::Json(handle) => ScanInner::MapKeys {
|
|
470
|
+
cursor: Arc::from(handle.keys(dir)),
|
|
471
|
+
buffer: VecDeque::new(),
|
|
472
|
+
done: false,
|
|
473
|
+
},
|
|
474
|
+
MapStateVariant::Message(handle) => ScanInner::MapKeys {
|
|
475
|
+
cursor: Arc::from(handle.keys(dir)),
|
|
476
|
+
buffer: VecDeque::new(),
|
|
477
|
+
done: false,
|
|
478
|
+
},
|
|
479
|
+
};
|
|
480
|
+
StateScan::new(
|
|
481
|
+
ruby,
|
|
482
|
+
inner,
|
|
483
|
+
this.bridge.clone(),
|
|
484
|
+
Arc::clone(&this.propagator),
|
|
485
|
+
)
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/// Durably commits the buffered operations. Returns `nil` (see
|
|
489
|
+
/// [`NativeValueState::commit`]).
|
|
490
|
+
fn commit(ruby: &Ruby, this: &Self) -> Result<Value, Error> {
|
|
491
|
+
match &this.state {
|
|
492
|
+
MapStateVariant::Json(handle) => run_op!(ruby, this, handle, commit())?,
|
|
493
|
+
MapStateVariant::Message(handle) => run_op!(ruby, this, handle, commit())?,
|
|
494
|
+
}
|
|
495
|
+
Ok(ruby.qnil().as_value())
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/// Discards the buffered uncommitted operations. Returns `nil`.
|
|
499
|
+
fn rollback(ruby: &Ruby, this: &Self) -> Result<Value, Error> {
|
|
500
|
+
match &this.state {
|
|
501
|
+
MapStateVariant::Json(handle) => run_infallible!(ruby, this, handle, rollback()),
|
|
502
|
+
MapStateVariant::Message(handle) => run_infallible!(ruby, this, handle, rollback()),
|
|
503
|
+
}
|
|
504
|
+
Ok(ruby.qnil().as_value())
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/// Native deque state handle, vended per event.
|
|
509
|
+
#[magnus::wrap(class = "Prosody::NativeDequeState")]
|
|
510
|
+
pub struct NativeDequeState {
|
|
511
|
+
state: DequeStateVariant,
|
|
512
|
+
bridge: Bridge,
|
|
513
|
+
propagator: Arc<TextMapCompositePropagator>,
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
impl NativeDequeState {
|
|
517
|
+
/// Wraps a vended deque handle with the bridge and propagator.
|
|
518
|
+
pub(crate) fn new(
|
|
519
|
+
state: DequeStateVariant,
|
|
520
|
+
bridge: Bridge,
|
|
521
|
+
propagator: Arc<TextMapCompositePropagator>,
|
|
522
|
+
) -> Self {
|
|
523
|
+
Self {
|
|
524
|
+
state,
|
|
525
|
+
bridge,
|
|
526
|
+
propagator,
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/// The number of live elements. Ruby Integers are unbounded, so the full
|
|
531
|
+
/// `usize` crosses uncapped.
|
|
532
|
+
fn len(ruby: &Ruby, this: &Self) -> Result<Value, Error> {
|
|
533
|
+
let len = match &this.state {
|
|
534
|
+
DequeStateVariant::Json(handle) => run_op!(ruby, this, handle, len())?,
|
|
535
|
+
DequeStateVariant::Message(handle) => run_op!(ruby, this, handle, len())?,
|
|
536
|
+
};
|
|
537
|
+
Ok(len.into_value_with(ruby))
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/// Whether the deque holds no live elements.
|
|
541
|
+
fn is_empty(ruby: &Ruby, this: &Self) -> Result<Value, Error> {
|
|
542
|
+
let empty = match &this.state {
|
|
543
|
+
DequeStateVariant::Json(handle) => run_op!(ruby, this, handle, is_empty())?,
|
|
544
|
+
DequeStateVariant::Message(handle) => run_op!(ruby, this, handle, is_empty())?,
|
|
545
|
+
};
|
|
546
|
+
Ok(empty.into_value_with(ruby))
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/// Reads the element at front-relative `index` (`nil` past the end).
|
|
550
|
+
fn get(ruby: &Ruby, this: &Self, index: usize) -> Result<Value, Error> {
|
|
551
|
+
match &this.state {
|
|
552
|
+
DequeStateVariant::Json(handle) => {
|
|
553
|
+
json_or_nil(ruby, run_op!(ruby, this, handle, get(index))?)
|
|
554
|
+
}
|
|
555
|
+
DequeStateVariant::Message(handle) => {
|
|
556
|
+
message_or_nil(ruby, run_op!(ruby, this, handle, get(index))?)
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/// Reads the front endpoint slot (`get(0)` without the length round trip),
|
|
562
|
+
/// `nil` when empty. Under a TTL an expired endpoint slot yields `nil` even
|
|
563
|
+
/// when live interior elements remain — a peek never searches inward.
|
|
564
|
+
fn peek_front(ruby: &Ruby, this: &Self) -> Result<Value, Error> {
|
|
565
|
+
match &this.state {
|
|
566
|
+
DequeStateVariant::Json(handle) => {
|
|
567
|
+
json_or_nil(ruby, run_op!(ruby, this, handle, peek_front())?)
|
|
568
|
+
}
|
|
569
|
+
DequeStateVariant::Message(handle) => {
|
|
570
|
+
message_or_nil(ruby, run_op!(ruby, this, handle, peek_front())?)
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/// Reads the back endpoint slot (`get(len - 1)` without the length round
|
|
576
|
+
/// trip), `nil` when empty. Same endpoint-slot TTL semantics as
|
|
577
|
+
/// [`peek_front`](Self::peek_front).
|
|
578
|
+
fn peek_back(ruby: &Ruby, this: &Self) -> Result<Value, Error> {
|
|
579
|
+
match &this.state {
|
|
580
|
+
DequeStateVariant::Json(handle) => {
|
|
581
|
+
json_or_nil(ruby, run_op!(ruby, this, handle, peek_back())?)
|
|
582
|
+
}
|
|
583
|
+
DequeStateVariant::Message(handle) => {
|
|
584
|
+
message_or_nil(ruby, run_op!(ruby, this, handle, peek_back())?)
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/// Appends an element at the back. Rejects JSON null and item-shape
|
|
590
|
+
/// mismatch.
|
|
591
|
+
fn push_back(ruby: &Ruby, this: &Self, value: Value) -> Result<Value, Error> {
|
|
592
|
+
match &this.state {
|
|
593
|
+
DequeStateVariant::Json(handle) => {
|
|
594
|
+
let item = json_write_item(ruby, value, " in a deque")?;
|
|
595
|
+
run_op!(ruby, this, handle, push_back(item))?;
|
|
596
|
+
}
|
|
597
|
+
DequeStateVariant::Message(handle) => {
|
|
598
|
+
let item = message_write_item(ruby, value, " to push into a message deque")?;
|
|
599
|
+
run_op!(ruby, this, handle, push_back(item))?;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
Ok(ruby.qnil().as_value())
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/// Prepends an element at the front. Rejects JSON null and item-shape
|
|
606
|
+
/// mismatch.
|
|
607
|
+
fn push_front(ruby: &Ruby, this: &Self, value: Value) -> Result<Value, Error> {
|
|
608
|
+
match &this.state {
|
|
609
|
+
DequeStateVariant::Json(handle) => {
|
|
610
|
+
let item = json_write_item(ruby, value, " in a deque")?;
|
|
611
|
+
run_op!(ruby, this, handle, push_front(item))?;
|
|
612
|
+
}
|
|
613
|
+
DequeStateVariant::Message(handle) => {
|
|
614
|
+
let item = message_write_item(ruby, value, " to push into a message deque")?;
|
|
615
|
+
run_op!(ruby, this, handle, push_front(item))?;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
Ok(ruby.qnil().as_value())
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/// Removes and returns the front element (`nil` when empty).
|
|
622
|
+
fn pop_front(ruby: &Ruby, this: &Self) -> Result<Value, Error> {
|
|
623
|
+
match &this.state {
|
|
624
|
+
DequeStateVariant::Json(handle) => {
|
|
625
|
+
json_or_nil(ruby, run_op!(ruby, this, handle, pop_front())?)
|
|
626
|
+
}
|
|
627
|
+
DequeStateVariant::Message(handle) => {
|
|
628
|
+
message_or_nil(ruby, run_op!(ruby, this, handle, pop_front())?)
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/// Removes and returns the back element (`nil` when empty).
|
|
634
|
+
fn pop_back(ruby: &Ruby, this: &Self) -> Result<Value, Error> {
|
|
635
|
+
match &this.state {
|
|
636
|
+
DequeStateVariant::Json(handle) => {
|
|
637
|
+
json_or_nil(ruby, run_op!(ruby, this, handle, pop_back())?)
|
|
638
|
+
}
|
|
639
|
+
DequeStateVariant::Message(handle) => {
|
|
640
|
+
message_or_nil(ruby, run_op!(ruby, this, handle, pop_back())?)
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/// Removes every element.
|
|
646
|
+
fn clear(ruby: &Ruby, this: &Self) -> Result<Value, Error> {
|
|
647
|
+
match &this.state {
|
|
648
|
+
DequeStateVariant::Json(handle) => run_op!(ruby, this, handle, clear())?,
|
|
649
|
+
DequeStateVariant::Message(handle) => run_op!(ruby, this, handle, clear())?,
|
|
650
|
+
}
|
|
651
|
+
Ok(ruby.qnil().as_value())
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/// Opens a cursor over the live elements in index order. Synchronous; the
|
|
655
|
+
/// carrier is active while core constructs the stream span.
|
|
656
|
+
#[allow(clippy::needless_pass_by_value, reason = "Magnus method argument type")]
|
|
657
|
+
fn scan(ruby: &Ruby, this: &Self, direction: String) -> Result<StateScan, Error> {
|
|
658
|
+
let dir = parse_direction(ruby, &direction)?;
|
|
659
|
+
let _guard = extract_opentelemetry_context(ruby, &this.propagator)?.attach();
|
|
660
|
+
let inner = match &this.state {
|
|
661
|
+
DequeStateVariant::Json(handle) => ScanInner::DequeJson {
|
|
662
|
+
cursor: Arc::from(handle.scan(dir)),
|
|
663
|
+
buffer: VecDeque::new(),
|
|
664
|
+
done: false,
|
|
665
|
+
},
|
|
666
|
+
DequeStateVariant::Message(handle) => ScanInner::DequeMessage {
|
|
667
|
+
cursor: Arc::from(handle.scan(dir)),
|
|
668
|
+
buffer: VecDeque::new(),
|
|
669
|
+
done: false,
|
|
670
|
+
},
|
|
671
|
+
};
|
|
672
|
+
StateScan::new(
|
|
673
|
+
ruby,
|
|
674
|
+
inner,
|
|
675
|
+
this.bridge.clone(),
|
|
676
|
+
Arc::clone(&this.propagator),
|
|
677
|
+
)
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/// Durably commits the buffered operations. Returns `nil` (see
|
|
681
|
+
/// [`NativeValueState::commit`]).
|
|
682
|
+
fn commit(ruby: &Ruby, this: &Self) -> Result<Value, Error> {
|
|
683
|
+
match &this.state {
|
|
684
|
+
DequeStateVariant::Json(handle) => run_op!(ruby, this, handle, commit())?,
|
|
685
|
+
DequeStateVariant::Message(handle) => run_op!(ruby, this, handle, commit())?,
|
|
686
|
+
}
|
|
687
|
+
Ok(ruby.qnil().as_value())
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/// Discards the buffered uncommitted operations. Returns `nil`.
|
|
691
|
+
fn rollback(ruby: &Ruby, this: &Self) -> Result<Value, Error> {
|
|
692
|
+
match &this.state {
|
|
693
|
+
DequeStateVariant::Json(handle) => run_infallible!(ruby, this, handle, rollback()),
|
|
694
|
+
DequeStateVariant::Message(handle) => run_infallible!(ruby, this, handle, rollback()),
|
|
695
|
+
}
|
|
696
|
+
Ok(ruby.qnil().as_value())
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/// The four cursor flavours a scan yields, one per (collection, payload) pair.
|
|
701
|
+
///
|
|
702
|
+
/// Each retains a `buffer` of the items pulled in the current ready-chunk plus
|
|
703
|
+
/// a `done` flag; the erased [`StateCursor`] behind the [`Arc`] owns
|
|
704
|
+
/// exhaustion, error ordering, and close-idempotence.
|
|
705
|
+
enum ScanInner {
|
|
706
|
+
/// A deque JSON scan yielding values.
|
|
707
|
+
DequeJson {
|
|
708
|
+
/// The erased cursor.
|
|
709
|
+
cursor: Arc<StateCursor<JsonValue>>,
|
|
710
|
+
/// Items pulled but not yet yielded.
|
|
711
|
+
buffer: VecDeque<JsonValue>,
|
|
712
|
+
/// Whether the cursor is exhausted.
|
|
713
|
+
done: bool,
|
|
714
|
+
},
|
|
715
|
+
/// A map JSON scan yielding `(key, value)` entries.
|
|
716
|
+
MapJson {
|
|
717
|
+
/// The erased cursor.
|
|
718
|
+
cursor: Arc<StateCursor<(String, JsonValue)>>,
|
|
719
|
+
/// Items pulled but not yet yielded.
|
|
720
|
+
buffer: VecDeque<(String, JsonValue)>,
|
|
721
|
+
/// Whether the cursor is exhausted.
|
|
722
|
+
done: bool,
|
|
723
|
+
},
|
|
724
|
+
/// A deque message scan yielding messages.
|
|
725
|
+
DequeMessage {
|
|
726
|
+
/// The erased cursor.
|
|
727
|
+
cursor: Arc<StateCursor<ConsumerMessage<JsonValue>>>,
|
|
728
|
+
/// Items pulled but not yet yielded.
|
|
729
|
+
buffer: VecDeque<ConsumerMessage<JsonValue>>,
|
|
730
|
+
/// Whether the cursor is exhausted.
|
|
731
|
+
done: bool,
|
|
732
|
+
},
|
|
733
|
+
/// A map message scan yielding `(key, message)` entries.
|
|
734
|
+
MapMessage {
|
|
735
|
+
/// The erased cursor.
|
|
736
|
+
cursor: Arc<StateCursor<(String, ConsumerMessage<JsonValue>)>>,
|
|
737
|
+
/// Items pulled but not yet yielded.
|
|
738
|
+
buffer: VecDeque<(String, ConsumerMessage<JsonValue>)>,
|
|
739
|
+
/// Whether the cursor is exhausted.
|
|
740
|
+
done: bool,
|
|
741
|
+
},
|
|
742
|
+
/// A map key-only scan yielding bare keys (payload-agnostic).
|
|
743
|
+
MapKeys {
|
|
744
|
+
/// The erased cursor.
|
|
745
|
+
cursor: Arc<StateCursor<String>>,
|
|
746
|
+
/// Keys pulled but not yet yielded.
|
|
747
|
+
buffer: VecDeque<String>,
|
|
748
|
+
/// Whether the cursor is exhausted.
|
|
749
|
+
done: bool,
|
|
750
|
+
},
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/// Yields the next buffered item, or pulls a fresh ready-chunk through
|
|
754
|
+
/// [`Bridge::wait_for`], for one [`ScanInner`] arm. On exhaustion it closes the
|
|
755
|
+
/// cursor (idempotent) and returns `nil`.
|
|
756
|
+
macro_rules! drive_scan {
|
|
757
|
+
($ruby:expr, $this:expr, $cursor:expr, $buffer:expr, $done:expr, |$item:ident| $conv:block) => {{
|
|
758
|
+
loop {
|
|
759
|
+
if let Some($item) = $buffer.pop_front() {
|
|
760
|
+
return $conv;
|
|
761
|
+
}
|
|
762
|
+
if *$done {
|
|
763
|
+
return Ok($ruby.qnil().as_value());
|
|
764
|
+
}
|
|
765
|
+
let cursor = Arc::clone($cursor);
|
|
766
|
+
let context = extract_opentelemetry_context($ruby, &$this.propagator)?;
|
|
767
|
+
let chunk = $this
|
|
768
|
+
.bridge
|
|
769
|
+
.wait_for(
|
|
770
|
+
$ruby,
|
|
771
|
+
async move {
|
|
772
|
+
cursor
|
|
773
|
+
.next_ready_chunk(SCAN_READY_CHUNK_SIZE)
|
|
774
|
+
.with_context(context)
|
|
775
|
+
.await
|
|
776
|
+
},
|
|
777
|
+
Span::current(),
|
|
778
|
+
)?
|
|
779
|
+
.map_err(|error| state_error($ruby, &error))?;
|
|
780
|
+
match chunk {
|
|
781
|
+
Some(items) => $buffer.extend(items),
|
|
782
|
+
None => {
|
|
783
|
+
*$done = true;
|
|
784
|
+
let cursor = Arc::clone($cursor);
|
|
785
|
+
$this.bridge.wait_for(
|
|
786
|
+
$ruby,
|
|
787
|
+
async move { cursor.close().await },
|
|
788
|
+
Span::current(),
|
|
789
|
+
)?;
|
|
790
|
+
return Ok($ruby.qnil().as_value());
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}};
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/// Closes the erased cursor for one [`ScanInner`] arm (idempotent).
|
|
798
|
+
///
|
|
799
|
+
/// Marks the arm terminal and drops any buffered-but-unyielded items *before*
|
|
800
|
+
/// yielding, so a `#next` after `#close` returns `nil` immediately rather than
|
|
801
|
+
/// draining stale items.
|
|
802
|
+
macro_rules! close_scan {
|
|
803
|
+
($ruby:expr, $this:expr, $cursor:expr, $buffer:expr, $done:expr) => {{
|
|
804
|
+
*$done = true;
|
|
805
|
+
$buffer.clear();
|
|
806
|
+
let cursor = Arc::clone($cursor);
|
|
807
|
+
$this
|
|
808
|
+
.bridge
|
|
809
|
+
.wait_for($ruby, async move { cursor.close().await }, Span::current())?;
|
|
810
|
+
}};
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/// Item-oriented scan cursor over a map or deque collection.
|
|
814
|
+
///
|
|
815
|
+
/// `StateScan#next` yields individual items, pulling a fresh ready-chunk from
|
|
816
|
+
/// core when the buffer drains and returning `nil` at exhaustion (unambiguous
|
|
817
|
+
/// under the null ban). A single fiber-aware permit (a one-token
|
|
818
|
+
/// `Thread::Queue` held through [`ThreadSafeValue`]) serializes `#next` and
|
|
819
|
+
/// `#close` in invocation order across chunks: concurrent fibers block on the
|
|
820
|
+
/// permit rather than racing the buffer, an exception does not poison cleanup
|
|
821
|
+
/// (the permit is released on every path), and `#close` cannot run under an
|
|
822
|
+
/// active `#next`. `#close` is idempotent (core-owned) and wired into every
|
|
823
|
+
/// traversal path via the Ruby `ensure`.
|
|
824
|
+
#[magnus::wrap(class = "Prosody::StateScan")]
|
|
825
|
+
pub struct StateScan {
|
|
826
|
+
inner: RefCell<ScanInner>,
|
|
827
|
+
lock: ThreadSafeValue,
|
|
828
|
+
bridge: Bridge,
|
|
829
|
+
propagator: Arc<TextMapCompositePropagator>,
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
impl StateScan {
|
|
833
|
+
/// Builds a scan around an erased cursor, seeding the one-token permit.
|
|
834
|
+
fn new(
|
|
835
|
+
ruby: &Ruby,
|
|
836
|
+
inner: ScanInner,
|
|
837
|
+
bridge: Bridge,
|
|
838
|
+
propagator: Arc<TextMapCompositePropagator>,
|
|
839
|
+
) -> Result<Self, Error> {
|
|
840
|
+
let queue: Value = ruby.get_inner(&QUEUE_CLASS).funcall(id!(ruby, "new"), ())?;
|
|
841
|
+
let _: Value = queue.funcall(id!(ruby, "push"), (ruby.qnil(),))?;
|
|
842
|
+
Ok(Self {
|
|
843
|
+
inner: RefCell::new(inner),
|
|
844
|
+
lock: ThreadSafeValue::new(queue, bridge.clone()),
|
|
845
|
+
bridge,
|
|
846
|
+
propagator,
|
|
847
|
+
})
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/// Acquires the permit, fiber-yielding until it is free.
|
|
851
|
+
fn acquire(&self, ruby: &Ruby) -> Result<(), Error> {
|
|
852
|
+
let _: Value = self.lock.get(ruby).funcall(id!(ruby, "pop"), ())?;
|
|
853
|
+
Ok(())
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/// Releases the permit (best-effort; runs on both success and failure).
|
|
857
|
+
fn release(&self, ruby: &Ruby) {
|
|
858
|
+
let _: Result<Value, Error> = self
|
|
859
|
+
.lock
|
|
860
|
+
.get(ruby)
|
|
861
|
+
.funcall(id!(ruby, "push"), (ruby.qnil(),));
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/// Yields the next scanned item, or `nil` at exhaustion.
|
|
865
|
+
fn next(ruby: &Ruby, this: &Self) -> Result<Value, Error> {
|
|
866
|
+
this.acquire(ruby)?;
|
|
867
|
+
let out = Self::next_locked(ruby, this);
|
|
868
|
+
this.release(ruby);
|
|
869
|
+
out
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/// The permit-protected body of [`next`](Self::next).
|
|
873
|
+
fn next_locked(ruby: &Ruby, this: &Self) -> Result<Value, Error> {
|
|
874
|
+
match &mut *this.inner.borrow_mut() {
|
|
875
|
+
ScanInner::DequeJson {
|
|
876
|
+
cursor,
|
|
877
|
+
buffer,
|
|
878
|
+
done,
|
|
879
|
+
} => drive_scan!(ruby, this, cursor, buffer, done, |item| {
|
|
880
|
+
serialize(ruby, &item)
|
|
881
|
+
}),
|
|
882
|
+
ScanInner::MapJson {
|
|
883
|
+
cursor,
|
|
884
|
+
buffer,
|
|
885
|
+
done,
|
|
886
|
+
} => drive_scan!(ruby, this, cursor, buffer, done, |item| {
|
|
887
|
+
let (key, value) = item;
|
|
888
|
+
let value: Value = serialize(ruby, &value)?;
|
|
889
|
+
Ok((key, value).into_value_with(ruby))
|
|
890
|
+
}),
|
|
891
|
+
ScanInner::DequeMessage {
|
|
892
|
+
cursor,
|
|
893
|
+
buffer,
|
|
894
|
+
done,
|
|
895
|
+
} => drive_scan!(ruby, this, cursor, buffer, done, |item| {
|
|
896
|
+
Ok(Message::from(item).into_value_with(ruby))
|
|
897
|
+
}),
|
|
898
|
+
ScanInner::MapMessage {
|
|
899
|
+
cursor,
|
|
900
|
+
buffer,
|
|
901
|
+
done,
|
|
902
|
+
} => drive_scan!(ruby, this, cursor, buffer, done, |item| {
|
|
903
|
+
let (key, message) = item;
|
|
904
|
+
Ok((key, Message::from(message)).into_value_with(ruby))
|
|
905
|
+
}),
|
|
906
|
+
ScanInner::MapKeys {
|
|
907
|
+
cursor,
|
|
908
|
+
buffer,
|
|
909
|
+
done,
|
|
910
|
+
} => drive_scan!(ruby, this, cursor, buffer, done, |item| {
|
|
911
|
+
Ok(item.into_value_with(ruby))
|
|
912
|
+
}),
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/// Closes the cursor, releasing the underlying stream. Idempotent, and
|
|
917
|
+
/// cannot run under an active `#next` (both take the permit).
|
|
918
|
+
fn close(ruby: &Ruby, this: &Self) -> Result<Value, Error> {
|
|
919
|
+
this.acquire(ruby)?;
|
|
920
|
+
let out = Self::close_locked(ruby, this);
|
|
921
|
+
this.release(ruby);
|
|
922
|
+
out
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/// The permit-protected body of [`close`](Self::close).
|
|
926
|
+
fn close_locked(ruby: &Ruby, this: &Self) -> Result<Value, Error> {
|
|
927
|
+
match &mut *this.inner.borrow_mut() {
|
|
928
|
+
ScanInner::DequeJson {
|
|
929
|
+
cursor,
|
|
930
|
+
buffer,
|
|
931
|
+
done,
|
|
932
|
+
} => close_scan!(ruby, this, cursor, buffer, done),
|
|
933
|
+
ScanInner::MapJson {
|
|
934
|
+
cursor,
|
|
935
|
+
buffer,
|
|
936
|
+
done,
|
|
937
|
+
} => close_scan!(ruby, this, cursor, buffer, done),
|
|
938
|
+
ScanInner::DequeMessage {
|
|
939
|
+
cursor,
|
|
940
|
+
buffer,
|
|
941
|
+
done,
|
|
942
|
+
} => close_scan!(ruby, this, cursor, buffer, done),
|
|
943
|
+
ScanInner::MapMessage {
|
|
944
|
+
cursor,
|
|
945
|
+
buffer,
|
|
946
|
+
done,
|
|
947
|
+
} => close_scan!(ruby, this, cursor, buffer, done),
|
|
948
|
+
ScanInner::MapKeys {
|
|
949
|
+
cursor,
|
|
950
|
+
buffer,
|
|
951
|
+
done,
|
|
952
|
+
} => close_scan!(ruby, this, cursor, buffer, done),
|
|
953
|
+
}
|
|
954
|
+
Ok(ruby.qnil().as_value())
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
/// Registers the native keyed-state classes and their methods.
|
|
959
|
+
///
|
|
960
|
+
/// # Errors
|
|
961
|
+
///
|
|
962
|
+
/// Returns a Magnus error if class or method definition fails.
|
|
963
|
+
pub fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
964
|
+
let module = ruby.get_inner(&ROOT_MOD);
|
|
965
|
+
|
|
966
|
+
let value = module.define_class(id!(ruby, "NativeValueState"), ruby.class_object())?;
|
|
967
|
+
value.define_method(id!(ruby, "get"), method!(NativeValueState::get, 0))?;
|
|
968
|
+
value.define_method(id!(ruby, "set"), method!(NativeValueState::set, 1))?;
|
|
969
|
+
value.define_method(id!(ruby, "clear"), method!(NativeValueState::clear, 0))?;
|
|
970
|
+
value.define_method(id!(ruby, "commit"), method!(NativeValueState::commit, 0))?;
|
|
971
|
+
value.define_method(
|
|
972
|
+
id!(ruby, "rollback"),
|
|
973
|
+
method!(NativeValueState::rollback, 0),
|
|
974
|
+
)?;
|
|
975
|
+
|
|
976
|
+
let map = module.define_class(id!(ruby, "NativeMapState"), ruby.class_object())?;
|
|
977
|
+
map.define_method(id!(ruby, "get"), method!(NativeMapState::get, 1))?;
|
|
978
|
+
map.define_method(
|
|
979
|
+
id!(ruby, "contains_key"),
|
|
980
|
+
method!(NativeMapState::contains_key, 1),
|
|
981
|
+
)?;
|
|
982
|
+
map.define_method(id!(ruby, "get_many"), method!(NativeMapState::get_many, 1))?;
|
|
983
|
+
map.define_method(id!(ruby, "set"), method!(NativeMapState::set, 2))?;
|
|
984
|
+
map.define_method(id!(ruby, "remove"), method!(NativeMapState::remove, 1))?;
|
|
985
|
+
map.define_method(id!(ruby, "clear"), method!(NativeMapState::clear, 0))?;
|
|
986
|
+
map.define_method(id!(ruby, "scan"), method!(NativeMapState::scan, 1))?;
|
|
987
|
+
map.define_method(id!(ruby, "keys"), method!(NativeMapState::keys, 1))?;
|
|
988
|
+
map.define_method(id!(ruby, "commit"), method!(NativeMapState::commit, 0))?;
|
|
989
|
+
map.define_method(id!(ruby, "rollback"), method!(NativeMapState::rollback, 0))?;
|
|
990
|
+
|
|
991
|
+
let deque = module.define_class(id!(ruby, "NativeDequeState"), ruby.class_object())?;
|
|
992
|
+
deque.define_method(id!(ruby, "len"), method!(NativeDequeState::len, 0))?;
|
|
993
|
+
deque.define_method(
|
|
994
|
+
id!(ruby, "is_empty"),
|
|
995
|
+
method!(NativeDequeState::is_empty, 0),
|
|
996
|
+
)?;
|
|
997
|
+
deque.define_method(id!(ruby, "get"), method!(NativeDequeState::get, 1))?;
|
|
998
|
+
deque.define_method(
|
|
999
|
+
id!(ruby, "peek_front"),
|
|
1000
|
+
method!(NativeDequeState::peek_front, 0),
|
|
1001
|
+
)?;
|
|
1002
|
+
deque.define_method(
|
|
1003
|
+
id!(ruby, "peek_back"),
|
|
1004
|
+
method!(NativeDequeState::peek_back, 0),
|
|
1005
|
+
)?;
|
|
1006
|
+
deque.define_method(
|
|
1007
|
+
id!(ruby, "push_back"),
|
|
1008
|
+
method!(NativeDequeState::push_back, 1),
|
|
1009
|
+
)?;
|
|
1010
|
+
deque.define_method(
|
|
1011
|
+
id!(ruby, "push_front"),
|
|
1012
|
+
method!(NativeDequeState::push_front, 1),
|
|
1013
|
+
)?;
|
|
1014
|
+
deque.define_method(
|
|
1015
|
+
id!(ruby, "pop_front"),
|
|
1016
|
+
method!(NativeDequeState::pop_front, 0),
|
|
1017
|
+
)?;
|
|
1018
|
+
deque.define_method(
|
|
1019
|
+
id!(ruby, "pop_back"),
|
|
1020
|
+
method!(NativeDequeState::pop_back, 0),
|
|
1021
|
+
)?;
|
|
1022
|
+
deque.define_method(id!(ruby, "clear"), method!(NativeDequeState::clear, 0))?;
|
|
1023
|
+
deque.define_method(id!(ruby, "scan"), method!(NativeDequeState::scan, 1))?;
|
|
1024
|
+
deque.define_method(id!(ruby, "commit"), method!(NativeDequeState::commit, 0))?;
|
|
1025
|
+
deque.define_method(
|
|
1026
|
+
id!(ruby, "rollback"),
|
|
1027
|
+
method!(NativeDequeState::rollback, 0),
|
|
1028
|
+
)?;
|
|
1029
|
+
|
|
1030
|
+
let scan = module.define_class(id!(ruby, "StateScan"), ruby.class_object())?;
|
|
1031
|
+
scan.define_method(id!(ruby, "next"), method!(StateScan::next, 0))?;
|
|
1032
|
+
scan.define_method(id!(ruby, "close"), method!(StateScan::close, 0))?;
|
|
1033
|
+
|
|
1034
|
+
Ok(())
|
|
1035
|
+
}
|