prosody 0.4.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 +2 -2
- data/CHANGELOG.md +15 -0
- data/CLAUDE.md +1 -0
- data/CONFIGURATION.md +167 -0
- data/Cargo.lock +660 -326
- data/Cargo.toml +2 -1
- data/README.md +290 -191
- data/examples/keyed_state.rb +15 -3
- data/examples/keyed_state_windowing.rb +9 -1
- data/ext/prosody/Cargo.toml +2 -1
- data/ext/prosody/src/admin.rs +1 -5
- data/ext/prosody/src/bridge/mod.rs +17 -32
- data/ext/prosody/src/client/config.rs +194 -89
- 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 +24 -20
- data/ext/prosody/src/handler/message.rs +50 -0
- data/ext/prosody/src/handler/mod.rs +112 -84
- 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 +49 -15
- data/lib/prosody/handler.rb +63 -10
- data/lib/prosody/native_stubs.rb +197 -31
- data/lib/prosody/request.rb +45 -0
- data/lib/prosody/state.rb +164 -41
- data/lib/prosody/version.rb +1 -1
- data/lib/prosody.rb +1 -0
- data/sig/configuration.rbs +51 -15
- data/sig/handler.rbs +12 -4
- data/sig/prosody.rbs +43 -2
- data/sig/request.rbs +66 -0
- data/sig/state.rbs +165 -47
- data/steep_expectations.yml +10 -0
- data/typecheck/payload_types.rb +14 -3
- data/typecheck/payload_types.rbs +4 -2
- data/typecheck_negative/payload_types.rb +4 -0
- data/typecheck_negative/payload_types.rbs +1 -0
- metadata +12 -2
- data/ext/prosody/src/handler/state.rs +0 -1035
data/examples/keyed_state.rb
CHANGED
|
@@ -15,17 +15,26 @@ TOTALS = Prosody.map("totals") # keys are always String
|
|
|
15
15
|
BACKLOG = Prosody.message_deque("backlog", capacity: 100) # bounded window of messages
|
|
16
16
|
|
|
17
17
|
class KeyedStateHandler < Prosody::EventHandler
|
|
18
|
+
def on_excise(context, message)
|
|
19
|
+
puts "Excise #{message.key}"
|
|
20
|
+
context.state(CART).clear
|
|
21
|
+
context.state(TOTALS).clear
|
|
22
|
+
context.state(BACKLOG).clear
|
|
23
|
+
nil
|
|
24
|
+
end
|
|
25
|
+
|
|
18
26
|
def initialize(logger:)
|
|
19
27
|
@logger = logger
|
|
20
28
|
end
|
|
21
29
|
|
|
22
30
|
def on_message(context, message)
|
|
31
|
+
payload = message.payload
|
|
23
32
|
cart = context.state(CART) # bound for this attempt only
|
|
24
33
|
current = cart.get || {"items" => []} # Hash, or nil when absent
|
|
25
|
-
cart.set(current.merge("items" => current["items"] + [
|
|
34
|
+
cart.set(current.merge("items" => current["items"] + [payload["order_id"]]))
|
|
26
35
|
|
|
27
36
|
totals = context.state(TOTALS)
|
|
28
|
-
totals.set(message.key,
|
|
37
|
+
totals.set(message.key, payload["total"])
|
|
29
38
|
# Steep infers key as String and total as Integer from TOTALS's RBS type.
|
|
30
39
|
totals.each_pair { |key, total| @logger.info(format_total(key, total)) }
|
|
31
40
|
|
|
@@ -35,6 +44,9 @@ class KeyedStateHandler < Prosody::EventHandler
|
|
|
35
44
|
@logger.info("oldest order: #{format_order_id(oldest.payload["order_id"])}") if oldest
|
|
36
45
|
end
|
|
37
46
|
|
|
47
|
+
def on_timer(_context, _timer)
|
|
48
|
+
end
|
|
49
|
+
|
|
38
50
|
private
|
|
39
51
|
|
|
40
52
|
def format_total(key, total)
|
|
@@ -54,5 +66,5 @@ if __FILE__ == $PROGRAM_NAME
|
|
|
54
66
|
state_collections: [CART, TOTALS, BACKLOG]
|
|
55
67
|
)
|
|
56
68
|
client.subscribe(KeyedStateHandler.new(logger: Logger.new($stdout)))
|
|
57
|
-
client.
|
|
69
|
+
client.shutdown
|
|
58
70
|
end
|
|
@@ -7,6 +7,14 @@ ACTIVITY_WINDOW = Prosody.value("activity-window")
|
|
|
7
7
|
PENDING_ACTIVITIES = Prosody.message_deque("pending-activities", capacity: 100)
|
|
8
8
|
|
|
9
9
|
class ActivityWindowHandler < Prosody::EventHandler
|
|
10
|
+
def on_excise(context, message)
|
|
11
|
+
puts "Excise #{message.key}"
|
|
12
|
+
context.state(PENDING_ACTIVITIES).clear
|
|
13
|
+
context.state(ACTIVITY_WINDOW).clear
|
|
14
|
+
context.clear_scheduled
|
|
15
|
+
nil
|
|
16
|
+
end
|
|
17
|
+
|
|
10
18
|
def on_message(context, message)
|
|
11
19
|
window = context.state(ACTIVITY_WINDOW)
|
|
12
20
|
pending = context.state(PENDING_ACTIVITIES)
|
|
@@ -43,5 +51,5 @@ if __FILE__ == $PROGRAM_NAME
|
|
|
43
51
|
state_collections: [ACTIVITY_WINDOW, PENDING_ACTIVITIES]
|
|
44
52
|
)
|
|
45
53
|
client.subscribe(ActivityWindowHandler.new)
|
|
46
|
-
client.
|
|
54
|
+
client.shutdown
|
|
47
55
|
end
|
data/ext/prosody/Cargo.toml
CHANGED
|
@@ -11,10 +11,11 @@ crate-type = ["cdylib"]
|
|
|
11
11
|
[dependencies]
|
|
12
12
|
atomic-take.workspace = true
|
|
13
13
|
bumpalo = { workspace = true, features = ["collections"] }
|
|
14
|
+
crossbeam-channel.workspace = true
|
|
14
15
|
educe.workspace = true
|
|
15
16
|
futures.workspace = true
|
|
16
17
|
magnus = { workspace = true, default-features = false }
|
|
17
|
-
mimalloc = { workspace = true, features = ["local_dynamic_tls"
|
|
18
|
+
mimalloc = { workspace = true, features = ["local_dynamic_tls"] }
|
|
18
19
|
opentelemetry.workspace = true
|
|
19
20
|
prosody = { workspace = true }
|
|
20
21
|
rb-sys.workspace = true
|
data/ext/prosody/src/admin.rs
CHANGED
|
@@ -6,8 +6,7 @@
|
|
|
6
6
|
use crate::bridge::Bridge;
|
|
7
7
|
use crate::util::ensure_runtime_context;
|
|
8
8
|
use crate::{ROOT_MOD, id};
|
|
9
|
-
use magnus::
|
|
10
|
-
use magnus::{Error, Module, Object, Ruby, Value, function, method};
|
|
9
|
+
use magnus::{Error, Module, Object, Ruby, function, method};
|
|
11
10
|
use prosody::admin::{AdminConfiguration, ProsodyAdminClient, TopicConfiguration};
|
|
12
11
|
use std::sync::Arc;
|
|
13
12
|
use tracing::Span;
|
|
@@ -164,8 +163,5 @@ pub fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
164
163
|
method!(AdminClient::delete_topic, 1),
|
|
165
164
|
)?;
|
|
166
165
|
|
|
167
|
-
// Make the admin client class private
|
|
168
|
-
let _: Value = module.funcall(id!(ruby, "private_constant"), (class_id,))?;
|
|
169
|
-
|
|
170
166
|
Ok(())
|
|
171
167
|
}
|
|
@@ -9,15 +9,12 @@ use crate::bridge::callback::AsyncCallback;
|
|
|
9
9
|
use crate::gvl::{GvlError, without_gvl};
|
|
10
10
|
use crate::{ROOT_MOD, RUNTIME, id};
|
|
11
11
|
use atomic_take::AtomicTake;
|
|
12
|
+
use crossbeam_channel::{Receiver, Sender, TryRecvError, TrySendError, bounded, select, unbounded};
|
|
12
13
|
use educe::Educe;
|
|
13
|
-
use futures::executor::block_on;
|
|
14
14
|
use magnus::value::{Lazy, ReprValue};
|
|
15
15
|
use magnus::{Error, Module, RClass, Ruby, Value};
|
|
16
16
|
use std::any::Any;
|
|
17
17
|
use thiserror::Error;
|
|
18
|
-
use tokio::select;
|
|
19
|
-
use tokio::sync::mpsc::error::SendError;
|
|
20
|
-
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel};
|
|
21
18
|
use tokio::sync::oneshot;
|
|
22
19
|
use tracing::{Instrument, Span, debug, error, warn};
|
|
23
20
|
|
|
@@ -63,7 +60,7 @@ pub struct Bridge {
|
|
|
63
60
|
/// Channel sender for submitting functions to be executed in the Ruby
|
|
64
61
|
/// context.
|
|
65
62
|
#[educe(Debug(ignore))]
|
|
66
|
-
tx:
|
|
63
|
+
tx: Sender<RubyFunction>,
|
|
67
64
|
}
|
|
68
65
|
|
|
69
66
|
impl Bridge {
|
|
@@ -83,12 +80,12 @@ impl Bridge {
|
|
|
83
80
|
///
|
|
84
81
|
/// A new `Bridge` instance.
|
|
85
82
|
pub fn new(ruby: &Ruby) -> Self {
|
|
86
|
-
let (tx,
|
|
83
|
+
let (tx, rx) = unbounded();
|
|
87
84
|
|
|
88
85
|
// Create a dedicated Ruby thread to process functions
|
|
89
86
|
ruby.thread_create_from_fn(move |ruby| {
|
|
90
87
|
loop {
|
|
91
|
-
let Err(error) = poll(&
|
|
88
|
+
let Err(error) = poll(&rx, ruby) else {
|
|
92
89
|
continue;
|
|
93
90
|
};
|
|
94
91
|
|
|
@@ -208,8 +205,8 @@ impl Bridge {
|
|
|
208
205
|
/// Unlike [`run`](Self::run), this method does not await a result and can
|
|
209
206
|
/// be called from synchronous contexts such as `Drop` implementations.
|
|
210
207
|
/// Returns `Err` only if the bridge receiver has been dropped (shutdown).
|
|
211
|
-
pub(crate) fn send(&self, function: RubyFunction) -> Result<(),
|
|
212
|
-
self.tx.send(function)
|
|
208
|
+
pub(crate) fn send(&self, function: RubyFunction) -> Result<(), BridgeError> {
|
|
209
|
+
self.tx.send(function).map_err(|_| BridgeError::Shutdown)
|
|
213
210
|
}
|
|
214
211
|
}
|
|
215
212
|
|
|
@@ -233,32 +230,24 @@ impl Bridge {
|
|
|
233
230
|
///
|
|
234
231
|
/// Returns a `BridgeError` if there was an issue with polling or executing
|
|
235
232
|
/// commands.
|
|
236
|
-
fn poll(rx: &
|
|
237
|
-
|
|
238
|
-
let (cancel_tx, cancel_rx) = oneshot::channel();
|
|
239
|
-
let mut maybe_cancel_tx = Some(cancel_tx);
|
|
233
|
+
fn poll(rx: &Receiver<RubyFunction>, ruby: &Ruby) -> Result<(), BridgeError> {
|
|
234
|
+
let (cancel_tx, cancel_rx) = bounded(1);
|
|
240
235
|
|
|
241
236
|
// Function to be executed without the GVL (Global VM Lock)
|
|
242
237
|
let poll_fn = || {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
result = rx.recv() => Ok(result),
|
|
248
|
-
}
|
|
249
|
-
})?;
|
|
250
|
-
|
|
251
|
-
let first_command = maybe_command.ok_or(BridgeError::Shutdown)?;
|
|
238
|
+
let first_command = select! {
|
|
239
|
+
recv(cancel_rx) -> _ => return Err(BridgeError::Cancelled),
|
|
240
|
+
recv(rx) -> result => result.map_err(|_| BridgeError::Shutdown)?,
|
|
241
|
+
};
|
|
252
242
|
|
|
253
243
|
// Batch up to POLL_BATCH_SIZE commands
|
|
254
244
|
let mut commands = Vec::with_capacity(POLL_BATCH_SIZE);
|
|
255
245
|
commands.push(first_command);
|
|
256
246
|
|
|
257
247
|
while commands.len() < POLL_BATCH_SIZE {
|
|
258
|
-
|
|
259
|
-
commands.push(command)
|
|
260
|
-
|
|
261
|
-
break;
|
|
248
|
+
match rx.try_recv() {
|
|
249
|
+
Ok(command) => commands.push(command),
|
|
250
|
+
Err(TryRecvError::Empty | TryRecvError::Disconnected) => break,
|
|
262
251
|
}
|
|
263
252
|
}
|
|
264
253
|
|
|
@@ -266,12 +255,8 @@ fn poll(rx: &mut UnboundedReceiver<RubyFunction>, ruby: &Ruby) -> Result<(), Bri
|
|
|
266
255
|
};
|
|
267
256
|
|
|
268
257
|
// Function to cancel polling if needed
|
|
269
|
-
let cancel_fn = || {
|
|
270
|
-
|
|
271
|
-
return;
|
|
272
|
-
};
|
|
273
|
-
|
|
274
|
-
if cancel_tx.send(()).is_err() {
|
|
258
|
+
let cancel_fn = move || {
|
|
259
|
+
if matches!(cancel_tx.try_send(()), Err(TrySendError::Disconnected(()))) {
|
|
275
260
|
warn!("Failed to cancel poll operation");
|
|
276
261
|
}
|
|
277
262
|
};
|