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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.cargo/config.toml +3 -0
  3. data/.release-please-manifest.json +1 -1
  4. data/AGENTS.md +395 -0
  5. data/ARCHITECTURE.md +2 -2
  6. data/CHANGELOG.md +15 -0
  7. data/CLAUDE.md +1 -0
  8. data/CONFIGURATION.md +167 -0
  9. data/Cargo.lock +660 -326
  10. data/Cargo.toml +2 -1
  11. data/README.md +290 -191
  12. data/examples/keyed_state.rb +15 -3
  13. data/examples/keyed_state_windowing.rb +9 -1
  14. data/ext/prosody/Cargo.toml +2 -1
  15. data/ext/prosody/src/admin.rs +1 -5
  16. data/ext/prosody/src/bridge/mod.rs +17 -32
  17. data/ext/prosody/src/client/config.rs +194 -89
  18. data/ext/prosody/src/client/mod.rs +167 -74
  19. data/ext/prosody/src/client/request.rs +132 -0
  20. data/ext/prosody/src/client/support.rs +122 -0
  21. data/ext/prosody/src/handler/context.rs +24 -20
  22. data/ext/prosody/src/handler/message.rs +50 -0
  23. data/ext/prosody/src/handler/mod.rs +112 -84
  24. data/ext/prosody/src/handler/state/mod.rs +488 -0
  25. data/ext/prosody/src/handler/state/registration.rs +104 -0
  26. data/ext/prosody/src/handler/state/scan.rs +218 -0
  27. data/ext/prosody/src/lib.rs +15 -3
  28. data/ext/prosody/src/published.rs +273 -0
  29. data/ext/prosody/src/scheduler/mod.rs +2 -2
  30. data/ext/prosody/src/scheduler/processor.rs +2 -2
  31. data/ext/prosody/src/scheduler/result.rs +7 -4
  32. data/ext/prosody/src/util.rs +86 -5
  33. data/lib/prosody/configuration.rb +49 -15
  34. data/lib/prosody/handler.rb +63 -10
  35. data/lib/prosody/native_stubs.rb +197 -31
  36. data/lib/prosody/request.rb +45 -0
  37. data/lib/prosody/state.rb +164 -41
  38. data/lib/prosody/version.rb +1 -1
  39. data/lib/prosody.rb +1 -0
  40. data/sig/configuration.rbs +51 -15
  41. data/sig/handler.rbs +12 -4
  42. data/sig/prosody.rbs +43 -2
  43. data/sig/request.rbs +66 -0
  44. data/sig/state.rbs +165 -47
  45. data/steep_expectations.yml +10 -0
  46. data/typecheck/payload_types.rb +14 -3
  47. data/typecheck/payload_types.rbs +4 -2
  48. data/typecheck_negative/payload_types.rb +4 -0
  49. data/typecheck_negative/payload_types.rbs +1 -0
  50. metadata +12 -2
  51. data/ext/prosody/src/handler/state.rs +0 -1035
@@ -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"] + [message.payload["order_id"]]))
34
+ cart.set(current.merge("items" => current["items"] + [payload["order_id"]]))
26
35
 
27
36
  totals = context.state(TOTALS)
28
- totals.set(message.key, message.payload["total"])
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.unsubscribe
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.unsubscribe
54
+ client.shutdown
47
55
  end
@@ -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", "v2"] }
18
+ mimalloc = { workspace = true, features = ["local_dynamic_tls"] }
18
19
  opentelemetry.workspace = true
19
20
  prosody = { workspace = true }
20
21
  rb-sys.workspace = true
@@ -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::value::ReprValue;
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: UnboundedSender<RubyFunction>,
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, mut rx) = unbounded_channel();
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(&mut rx, ruby) else {
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<(), SendError<RubyFunction>> {
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: &mut UnboundedReceiver<RubyFunction>, ruby: &Ruby) -> Result<(), BridgeError> {
237
- // Set up cancellation channel
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
- // Wait for either a command or cancellation
244
- let maybe_command = block_on(async {
245
- select! {
246
- _ = cancel_rx => Err(BridgeError::Cancelled),
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
- if let Ok(command) = rx.try_recv() {
259
- commands.push(command);
260
- } else {
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
- let Some(cancel_tx) = maybe_cancel_tx.take() else {
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
  };