omq-rs 0.2.5 → 0.2.7

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '068794048e18d65087a2f7feef656c5a69fce269140007ba804ff5394f417b22'
4
- data.tar.gz: b80f54254186826e27d4c77ad024877150c083a253e2742cc73c3b1baf935797
3
+ metadata.gz: 39bae86e920966ad2f4ec940ffb54a150447afd6950b6692105b67414c93658f
4
+ data.tar.gz: 706fef1a060bf0bdd1b120d75e0094b883d30c91e21d91bcfa25829e9034213f
5
5
  SHA512:
6
- metadata.gz: 8a5c55beafa7d9db560069671d08a03080d746017c053b7f67608a795d73f41c9d98f515f32f4efdc55c9f5b133057be9ad6a7787cfe10d3bd967baec9f29563
7
- data.tar.gz: 5428836642e04134201da527aedf24fb24dcaddb69650aeb264c1aeecec7110d7accebaa8c8f7d84cd353dab5013d2ff8e5b9f6b6492a26120c6365461776f0d
6
+ metadata.gz: 1ac452e8ed4a8e39e6a35a494fb6c425794ec40ee443184c78a5c8392317880d41d60a74caa8d748ad19bc263fc943d18f04254b23cd8559a63fda8faf8fd0e9
7
+ data.tar.gz: 4dbfc59cbdd388c5f3e478b62d1ddcc83bd8f96e29eebf5c7d09cefbed1c7319de37bd1750b8bd59a189c3c0749886b8c1f57dbd243e00fc8492db10846b3fd2
data/CHANGELOG.md CHANGED
@@ -2,6 +2,25 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.2.7] - 2026-10-20
6
+
7
+ ### Changed
8
+
9
+ - Bundle `omq-tokio` 0.30.0 and `omq-proto` 0.34.0 with adaptive Zstd
10
+ levels, handshake flood limits, and wakeup and lifecycle fixes.
11
+
12
+ ### Fixed
13
+
14
+ - Socket construction no longer releases the GVL under the materialization
15
+ lock, and close serializes with wrapper setup.
16
+
17
+ ## [0.2.6] - 2026-10-16
18
+
19
+ ### Changed
20
+
21
+ - Require `omq-tokio` 0.29.0 and `omq-proto` 0.33.0 with the latest PEER
22
+ limits and fan-out send readiness fixes.
23
+
5
24
  ## [0.2.5] - 2026-10-15
6
25
 
7
26
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "omq_rs_native"
3
- version = "0.2.5"
3
+ version = "0.2.7"
4
4
  edition = "2024"
5
5
  rust-version = "1.93"
6
6
  license = "ISC"
@@ -20,8 +20,8 @@ zstd = ["omq-tokio/zstd"]
20
20
  ws = ["omq-tokio/ws"]
21
21
 
22
22
  [dependencies]
23
- omq-proto = { version = "0.32.1", default-features = false }
24
- omq-tokio = { version = "0.28.1", default-features = false }
23
+ omq-proto = { version = "0.34.0", default-features = false }
24
+ omq-tokio = { version = "0.30.0", default-features = false }
25
25
  yring = { version = "0.3.20", features = ["async"] }
26
26
 
27
27
  bytes = "1.12.0"
@@ -10,13 +10,18 @@ use crate::notify::PipeNotify;
10
10
  use crate::options::parse_curve_public_key;
11
11
  use crate::rb::{self, RbResult};
12
12
 
13
+ /// Handshake facts handed to the Ruby auth callback as a hash.
14
+ struct AuthPeer {
15
+ public_key: [u8; 32],
16
+ identity: Option<Bytes>,
17
+ peer_address: Option<String>,
18
+ username: Option<String>,
19
+ password: Option<String>,
20
+ }
21
+
13
22
  enum AuthRequest {
14
23
  Check {
15
- public_key: [u8; 32],
16
- identity: Option<Bytes>,
17
- peer_address: Option<String>,
18
- username: Option<String>,
19
- password: Option<String>,
24
+ peer: AuthPeer,
20
25
  reply: flume::Sender<bool>,
21
26
  },
22
27
  Stop,
@@ -85,78 +90,18 @@ fn wait_for_request(data: &WorkerData) -> Option<AuthRequest> {
85
90
  unsafe extern "C" fn auth_worker_main(data: *mut c_void) -> VALUE {
86
91
  let data = unsafe { Box::from_raw(data.cast::<WorkerData>()) };
87
92
  let _ = catch_unwind(AssertUnwindSafe(|| {
88
- while let Some(AuthRequest::Check {
89
- public_key,
90
- identity,
91
- peer_address,
92
- username,
93
- password,
94
- reply,
95
- }) = wait_for_request(&data)
96
- {
97
- let accepted = invoke_callback(
98
- data.callback,
99
- public_key,
100
- identity.as_ref(),
101
- peer_address.as_deref(),
102
- username.as_deref(),
103
- password.as_deref(),
104
- );
93
+ while let Some(AuthRequest::Check { peer, reply }) = wait_for_request(&data) {
94
+ let accepted = invoke_callback(data.callback, &peer);
105
95
  let _ = reply.send(accepted);
106
96
  }
107
97
  }));
108
98
  rb::qnil()
109
99
  }
110
100
 
111
- fn invoke_callback(
112
- callback: VALUE,
113
- public_key: [u8; 32],
114
- identity: Option<&Bytes>,
115
- peer_address: Option<&str>,
116
- username: Option<&str>,
117
- password: Option<&str>,
118
- ) -> bool {
119
- let result = (|| -> RbResult<VALUE> {
120
- let peer = rb::hash_new()?;
121
- let key = if username.is_some() {
122
- rb::qnil()
123
- } else {
124
- #[cfg(feature = "curve")]
125
- {
126
- let key = omq_proto::CurvePublicKey::from_bytes(public_key)
127
- .to_z85()
128
- .into_bytes();
129
- rb::new_binary_string(&key)?
130
- }
131
- #[cfg(not(feature = "curve"))]
132
- {
133
- let _ = public_key;
134
- rb::qnil()
135
- }
136
- };
137
- rb::hash_aset(peer, rb::symbol("public_key")?, key)?;
138
- let identity = match identity {
139
- Some(value) => rb::new_binary_string(value)?,
140
- None => rb::qnil(),
141
- };
142
- rb::hash_aset(peer, rb::symbol("identity")?, identity)?;
143
- let peer_address = match peer_address {
144
- Some(value) => rb::new_utf8_string(value)?,
145
- None => rb::qnil(),
146
- };
147
- rb::hash_aset(peer, rb::symbol("peer_address")?, peer_address)?;
148
- let username = match username {
149
- Some(value) => rb::new_utf8_string(value)?,
150
- None => rb::qnil(),
151
- };
152
- rb::hash_aset(peer, rb::symbol("username")?, username)?;
153
- let password = match password {
154
- Some(value) => rb::new_utf8_string(value)?,
155
- None => rb::qnil(),
156
- };
157
- rb::hash_aset(peer, rb::symbol("password")?, password)?;
158
- rb::call_method_1(callback, c"call", peer)
159
- })();
101
+ /// Call the Ruby callback with the peer hash; any truthy result accepts.
102
+ /// A raised exception rejects the peer and clears Ruby's error state.
103
+ fn invoke_callback(callback: VALUE, peer: &AuthPeer) -> bool {
104
+ let result = peer_hash(peer).and_then(|hash| rb::call_method_1(callback, c"call", hash));
160
105
 
161
106
  if let Ok(value) = result {
162
107
  value != rb::qfalse() && value != rb::qnil()
@@ -166,6 +111,54 @@ fn invoke_callback(
166
111
  }
167
112
  }
168
113
 
114
+ fn peer_hash(peer: &AuthPeer) -> RbResult<VALUE> {
115
+ let hash = rb::hash_new()?;
116
+
117
+ // PLAIN peers carry a username; only CURVE peers expose a public key.
118
+ let key = if peer.username.is_some() {
119
+ rb::qnil()
120
+ } else {
121
+ #[cfg(feature = "curve")]
122
+ {
123
+ let key = omq_proto::CurvePublicKey::from_bytes(peer.public_key)
124
+ .to_z85()
125
+ .into_bytes();
126
+ rb::new_binary_string(&key)?
127
+ }
128
+ #[cfg(not(feature = "curve"))]
129
+ {
130
+ let _ = peer.public_key;
131
+ rb::qnil()
132
+ }
133
+ };
134
+ rb::hash_aset(hash, rb::symbol("public_key")?, key)?;
135
+
136
+ let identity = match &peer.identity {
137
+ Some(value) => rb::new_binary_string(value)?,
138
+ None => rb::qnil(),
139
+ };
140
+ rb::hash_aset(hash, rb::symbol("identity")?, identity)?;
141
+
142
+ let peer_address = match &peer.peer_address {
143
+ Some(value) => rb::new_utf8_string(value)?,
144
+ None => rb::qnil(),
145
+ };
146
+ rb::hash_aset(hash, rb::symbol("peer_address")?, peer_address)?;
147
+
148
+ let username = match &peer.username {
149
+ Some(value) => rb::new_utf8_string(value)?,
150
+ None => rb::qnil(),
151
+ };
152
+ rb::hash_aset(hash, rb::symbol("username")?, username)?;
153
+
154
+ let password = match &peer.password {
155
+ Some(value) => rb::new_utf8_string(value)?,
156
+ None => rb::qnil(),
157
+ };
158
+ rb::hash_aset(hash, rb::symbol("password")?, password)?;
159
+ Ok(hash)
160
+ }
161
+
169
162
  #[cfg(feature = "curve")]
170
163
  pub fn allowed_keys(value: VALUE) -> RbResult<omq_proto::Authenticator> {
171
164
  let count = rb::array_len(value)?;
@@ -207,11 +200,13 @@ pub fn callback(callback: VALUE) -> RbResult<(omq_proto::Authenticator, AuthWork
207
200
  let authenticator = omq_proto::Authenticator::new(move |peer| {
208
201
  let (reply, result) = flume::bounded(1);
209
202
  let request = AuthRequest::Check {
210
- public_key: peer.public_key,
211
- identity: peer.identity.clone(),
212
- peer_address: peer.peer_address.clone(),
213
- username: peer.username.clone(),
214
- password: peer.password.clone(),
203
+ peer: AuthPeer {
204
+ public_key: peer.public_key,
205
+ identity: peer.identity.clone(),
206
+ peer_address: peer.peer_address.clone(),
207
+ username: peer.username.clone(),
208
+ password: peer.password.clone(),
209
+ },
215
210
  reply,
216
211
  };
217
212
  if auth_sender.send(request).is_err() {
@@ -6,15 +6,28 @@ use rb_sys::VALUE;
6
6
 
7
7
  use crate::rb::{self, RbResult, RubyErr};
8
8
 
9
- #[expect(
10
- clippy::too_many_lines,
11
- reason = "flat option mapping mirrors omq-proto Options fields"
12
- )]
13
9
  pub fn build_options(hash: VALUE) -> RbResult<omq_tokio::Options> {
14
10
  rb::check_hash(hash)?;
15
11
 
12
+ // Groups run in a fixed order, so the first invalid option reported
13
+ // stays the same. Each maps fields one to one onto omq-proto Options.
16
14
  let mut opts = omq_tokio::Options::default();
15
+ apply_queue_options(hash, &mut opts)?;
16
+ apply_timing_options(hash, &mut opts)?;
17
+ apply_transport_options(hash, &mut opts)?;
18
+ apply_compression_options(hash, &mut opts)?;
19
+ apply_policy_options(hash, &mut opts)?;
20
+ if let Some(mechanism_type) = mechanism_type(hash)? {
21
+ apply_mechanism(hash, &mechanism_type, &mut opts)?;
22
+ }
23
+
24
+ opts.validate()
25
+ .map_err(|error| RubyErr::arg(error.to_string()))?;
26
+ Ok(opts)
27
+ }
17
28
 
29
+ /// Queue limits, receive rate limits, workload, linger, and identity.
30
+ fn apply_queue_options(hash: VALUE, opts: &mut omq_tokio::Options) -> RbResult<()> {
18
31
  if let Some(v) = get_opt_u32(hash, "send_hwm")? {
19
32
  opts.send_hwm = v;
20
33
  }
@@ -56,6 +69,11 @@ pub fn build_options(hash: VALUE) -> RbResult<omq_tokio::Options> {
56
69
  if let Some(v) = get_opt_bool(hash, "conflate")? {
57
70
  opts.conflate = v;
58
71
  }
72
+ Ok(())
73
+ }
74
+
75
+ /// Heartbeat and handshake timing.
76
+ fn apply_timing_options(hash: VALUE, opts: &mut omq_tokio::Options) -> RbResult<()> {
59
77
  if let Some(v) = get_opt_duration(hash, "heartbeat_interval")? {
60
78
  opts.heartbeat_interval = Some(v);
61
79
  }
@@ -71,6 +89,11 @@ pub fn build_options(hash: VALUE) -> RbResult<omq_tokio::Options> {
71
89
  if let Some(v) = get_opt_usize(hash, "max_pending_handshakes")? {
72
90
  opts.max_pending_handshakes = v;
73
91
  }
92
+ Ok(())
93
+ }
94
+
95
+ /// Message size limits, buffers, framing thresholds, and delivery flags.
96
+ fn apply_transport_options(hash: VALUE, opts: &mut omq_tokio::Options) -> RbResult<()> {
74
97
  if let Some(v) = get_opt_usize(hash, "max_message_size")? {
75
98
  opts.max_message_size = Some(v);
76
99
  }
@@ -95,6 +118,11 @@ pub fn build_options(hash: VALUE) -> RbResult<omq_tokio::Options> {
95
118
  if let Some(v) = get_opt_bool(hash, "reconnect_stop_conn_refused")? {
96
119
  opts.reconnect_stop_conn_refused = v;
97
120
  }
121
+ Ok(())
122
+ }
123
+
124
+ /// Compression dictionary, training, level, and offload.
125
+ fn apply_compression_options(hash: VALUE, opts: &mut omq_tokio::Options) -> RbResult<()> {
98
126
  if let Some(v) = get_opt_bytes(hash, "compression_dict")? {
99
127
  opts.compression_dict = (!v.is_empty()).then(|| Bytes::from(v));
100
128
  }
@@ -115,8 +143,14 @@ pub fn build_options(hash: VALUE) -> RbResult<omq_tokio::Options> {
115
143
  opts.max_recv_dict_size = Some(v);
116
144
  }
117
145
  if let Some(v) = get_opt_i64(hash, "compression_offload_threshold")? {
146
+ // Negative disables offload.
118
147
  opts.compression_offload_threshold = if v < 0 { None } else { Some(v as usize) };
119
148
  }
149
+ Ok(())
150
+ }
151
+
152
+ /// Mute and reconnect policies.
153
+ fn apply_policy_options(hash: VALUE, opts: &mut omq_tokio::Options) -> RbResult<()> {
120
154
  if let Some(v) = get_opt_string(hash, "on_mute")? {
121
155
  opts.on_mute = match v.as_str() {
122
156
  "drop_newest" | "drop" => omq_tokio::OnMute::DropNewest,
@@ -143,37 +177,37 @@ pub fn build_options(hash: VALUE) -> RbResult<omq_tokio::Options> {
143
177
  max: duration_from_seconds("reconnect_interval max", max)?,
144
178
  };
145
179
  }
180
+ Ok(())
181
+ }
146
182
 
147
- let mut mechanism_type = get_opt_string(hash, "mechanism_type")?;
148
- if mechanism_type.is_none() {
149
- let curve = option_present(
150
- hash,
151
- &[
152
- "curve_server",
153
- "curve_publickey",
154
- "curve_public_key",
155
- "curve_secretkey",
156
- "curve_secret_key",
157
- "curve_serverkey",
158
- "curve_server_key",
159
- ],
160
- )?;
161
- let plain = option_present(hash, &["plain_server", "plain_username", "plain_password"])?;
162
- mechanism_type = if curve {
163
- Some("curve".to_owned())
164
- } else if plain {
165
- Some("plain".to_owned())
166
- } else {
167
- None
168
- };
169
- }
170
- if let Some(mechanism_type) = mechanism_type {
171
- apply_mechanism(hash, &mechanism_type, &mut opts)?;
172
- }
173
-
174
- opts.validate()
175
- .map_err(|error| RubyErr::arg(error.to_string()))?;
176
- Ok(opts)
183
+ /// Explicit `mechanism_type`, or one inferred from CURVE or PLAIN keys.
184
+ /// CURVE wins when both kinds of keys are present.
185
+ fn mechanism_type(hash: VALUE) -> RbResult<Option<String>> {
186
+ if let Some(mechanism_type) = get_opt_string(hash, "mechanism_type")? {
187
+ return Ok(Some(mechanism_type));
188
+ }
189
+
190
+ let curve = option_present(
191
+ hash,
192
+ &[
193
+ "curve_server",
194
+ "curve_publickey",
195
+ "curve_public_key",
196
+ "curve_secretkey",
197
+ "curve_secret_key",
198
+ "curve_serverkey",
199
+ "curve_server_key",
200
+ ],
201
+ )?;
202
+ let plain = option_present(hash, &["plain_server", "plain_username", "plain_password"])?;
203
+
204
+ Ok(if curve {
205
+ Some("curve".to_owned())
206
+ } else if plain {
207
+ Some("plain".to_owned())
208
+ } else {
209
+ None
210
+ })
177
211
  }
178
212
 
179
213
  fn apply_mechanism(hash: VALUE, mech_type: &str, opts: &mut omq_tokio::Options) -> RbResult<()> {
@@ -181,75 +215,12 @@ fn apply_mechanism(hash: VALUE, mech_type: &str, opts: &mut omq_tokio::Options)
181
215
  "null" => {}
182
216
 
183
217
  #[cfg(feature = "curve")]
184
- "curve" => {
185
- let is_server =
186
- get_opt_bool_alias(hash, &["curve_server", "mechanism_server"])?.unwrap_or(false);
187
- let pub_key = get_opt_bytes_alias(
188
- hash,
189
- &[
190
- "curve_publickey",
191
- "curve_public_key",
192
- "mechanism_public_key",
193
- ],
194
- )?;
195
- let sec_key = get_opt_bytes_alias(
196
- hash,
197
- &[
198
- "curve_secretkey",
199
- "curve_secret_key",
200
- "mechanism_secret_key",
201
- ],
202
- )?;
203
-
204
- if is_server {
205
- let public = parse_curve_public_key(
206
- &pub_key
207
- .ok_or_else(|| RubyErr::arg("CURVE server requires curve_publickey"))?,
208
- "curve_publickey",
209
- )?;
210
- let secret = curve_secret_key(
211
- &sec_key
212
- .ok_or_else(|| RubyErr::arg("CURVE server requires curve_secretkey"))?,
213
- "curve_secretkey",
214
- )?;
215
- opts.mechanism = omq_proto::MechanismSetup::CurveServer {
216
- our_keypair: omq_proto::CurveKeypair { public, secret },
217
- options: omq_proto::CurveServerOptions::default(),
218
- };
219
- } else {
220
- let server_key = get_opt_bytes_alias(
221
- hash,
222
- &[
223
- "curve_serverkey",
224
- "curve_server_key",
225
- "mechanism_server_key",
226
- ],
227
- )?;
228
- let public = parse_curve_public_key(
229
- &pub_key
230
- .ok_or_else(|| RubyErr::arg("CURVE client requires curve_publickey"))?,
231
- "curve_publickey",
232
- )?;
233
- let secret = curve_secret_key(
234
- &sec_key
235
- .ok_or_else(|| RubyErr::arg("CURVE client requires curve_secretkey"))?,
236
- "curve_secretkey",
237
- )?;
238
- let server_public = parse_curve_public_key(
239
- &server_key
240
- .ok_or_else(|| RubyErr::arg("CURVE client requires curve_serverkey"))?,
241
- "curve_serverkey",
242
- )?;
243
- opts.mechanism = omq_proto::MechanismSetup::CurveClient {
244
- our_keypair: omq_proto::CurveKeypair { public, secret },
245
- server_public,
246
- };
247
- }
248
- }
218
+ "curve" => apply_curve_mechanism(hash, opts)?,
249
219
 
250
220
  #[cfg(feature = "plain")]
251
221
  "plain" => {
252
222
  if get_opt_bool_alias(hash, &["plain_server", "mechanism_server"])?.unwrap_or(false) {
223
+ // Fail closed until an authenticator is installed on the socket.
253
224
  opts.mechanism = omq_proto::MechanismSetup::PlainServer {
254
225
  authenticator: omq_proto::Authenticator::new(|_| false),
255
226
  };
@@ -269,6 +240,70 @@ fn apply_mechanism(hash: VALUE, mech_type: &str, opts: &mut omq_tokio::Options)
269
240
  Ok(())
270
241
  }
271
242
 
243
+ #[cfg(feature = "curve")]
244
+ fn apply_curve_mechanism(hash: VALUE, opts: &mut omq_tokio::Options) -> RbResult<()> {
245
+ let is_server =
246
+ get_opt_bool_alias(hash, &["curve_server", "mechanism_server"])?.unwrap_or(false);
247
+ let pub_key = get_opt_bytes_alias(
248
+ hash,
249
+ &[
250
+ "curve_publickey",
251
+ "curve_public_key",
252
+ "mechanism_public_key",
253
+ ],
254
+ )?;
255
+ let sec_key = get_opt_bytes_alias(
256
+ hash,
257
+ &[
258
+ "curve_secretkey",
259
+ "curve_secret_key",
260
+ "mechanism_secret_key",
261
+ ],
262
+ )?;
263
+
264
+ if is_server {
265
+ let public = parse_curve_public_key(
266
+ &pub_key.ok_or_else(|| RubyErr::arg("CURVE server requires curve_publickey"))?,
267
+ "curve_publickey",
268
+ )?;
269
+ let secret = curve_secret_key(
270
+ &sec_key.ok_or_else(|| RubyErr::arg("CURVE server requires curve_secretkey"))?,
271
+ "curve_secretkey",
272
+ )?;
273
+ opts.mechanism = omq_proto::MechanismSetup::CurveServer {
274
+ our_keypair: omq_proto::CurveKeypair { public, secret },
275
+ options: omq_proto::CurveServerOptions::default(),
276
+ };
277
+ return Ok(());
278
+ }
279
+
280
+ let server_key = get_opt_bytes_alias(
281
+ hash,
282
+ &[
283
+ "curve_serverkey",
284
+ "curve_server_key",
285
+ "mechanism_server_key",
286
+ ],
287
+ )?;
288
+ let public = parse_curve_public_key(
289
+ &pub_key.ok_or_else(|| RubyErr::arg("CURVE client requires curve_publickey"))?,
290
+ "curve_publickey",
291
+ )?;
292
+ let secret = curve_secret_key(
293
+ &sec_key.ok_or_else(|| RubyErr::arg("CURVE client requires curve_secretkey"))?,
294
+ "curve_secretkey",
295
+ )?;
296
+ let server_public = parse_curve_public_key(
297
+ &server_key.ok_or_else(|| RubyErr::arg("CURVE client requires curve_serverkey"))?,
298
+ "curve_serverkey",
299
+ )?;
300
+ opts.mechanism = omq_proto::MechanismSetup::CurveClient {
301
+ our_keypair: omq_proto::CurveKeypair { public, secret },
302
+ server_public,
303
+ };
304
+ Ok(())
305
+ }
306
+
272
307
  #[cfg(feature = "curve")]
273
308
  pub(crate) fn parse_curve_public_key(
274
309
  bytes: &[u8],
@@ -11,12 +11,10 @@ use tokio::task::JoinHandle;
11
11
 
12
12
  use crate::notify::PipeNotify;
13
13
 
14
- type Job = Box<dyn FnOnce() + Send + 'static>;
15
-
16
14
  struct RuntimeState {
17
15
  pid: u32,
18
16
  handle: Handle,
19
- submit: flume::Sender<Job>,
17
+ _keepalive: flume::Sender<()>,
20
18
  }
21
19
 
22
20
  static RUNTIME: Mutex<Option<RuntimeState>> = Mutex::new(None);
@@ -34,7 +32,7 @@ pub fn ensure_runtime(io_threads: usize) -> Handle {
34
32
  {
35
33
  return rt.handle.clone();
36
34
  }
37
- let (tx, rx) = flume::unbounded::<Job>();
35
+ let (tx, rx) = flume::bounded::<()>(1);
38
36
  let (handle_tx, handle_rx) = flume::bounded::<Handle>(1);
39
37
  let n = io_threads.max(1);
40
38
  thread::Builder::new()
@@ -54,9 +52,7 @@ pub fn ensure_runtime(io_threads: usize) -> Handle {
54
52
  };
55
53
  let _ = handle_tx.send(rt.handle().clone());
56
54
  rt.block_on(async move {
57
- while let Ok(job) = rx.recv_async().await {
58
- job();
59
- }
55
+ let _ = rx.recv_async().await;
60
56
  });
61
57
  })
62
58
  .expect("omq-rs: spawn tokio thread");
@@ -64,23 +60,11 @@ pub fn ensure_runtime(io_threads: usize) -> Handle {
64
60
  *guard = Some(RuntimeState {
65
61
  pid,
66
62
  handle: handle.clone(),
67
- submit: tx,
63
+ _keepalive: tx,
68
64
  });
69
65
  handle
70
66
  }
71
67
 
72
- fn submit_job(io_threads: usize) -> flume::Sender<Job> {
73
- let guard = RUNTIME.lock().unwrap();
74
- if let Some(ref rt) = *guard
75
- && rt.pid == std::process::id()
76
- {
77
- return rt.submit.clone();
78
- }
79
- drop(guard);
80
- ensure_runtime(io_threads);
81
- RUNTIME.lock().unwrap().as_ref().unwrap().submit.clone()
82
- }
83
-
84
68
  fn recv_blocking<T>(rx: flume::Receiver<T>, missing: &'static str) -> T {
85
69
  struct RecvBox<U> {
86
70
  rx: flume::Receiver<U>,
@@ -128,16 +112,14 @@ pub struct Materialized {
128
112
  pub recv_notify: Arc<PipeNotify>,
129
113
  pub send_notify: Arc<PipeNotify>,
130
114
  pub recv_space: Arc<tokio::sync::Notify>,
131
- pub send_pump: JoinHandle<()>,
132
115
  pub send_waiting: tokio::sync::watch::Sender<bool>,
133
- pub recv_pump: JoinHandle<()>,
134
116
 
135
117
  pub monitor_rx: flume::Receiver<MonitorEventData>,
136
118
  pub monitor_notify: Arc<PipeNotify>,
137
119
  pub peer_connected_notify: Arc<PipeNotify>,
138
120
  pub all_peers_gone_notify: Arc<PipeNotify>,
139
121
  pub subscriber_joined_notify: Arc<PipeNotify>,
140
- pub monitor_pump: JoinHandle<()>,
122
+ pub tasks: SocketTasks,
141
123
  }
142
124
 
143
125
  #[derive(Clone)]
@@ -154,17 +136,24 @@ pub enum MonitorValue {
154
136
  Text(String),
155
137
  }
156
138
 
157
- #[expect(
158
- clippy::too_many_lines,
159
- reason = "one exhaustive match keeps monitor event conversion auditable"
160
- )]
139
+ /// Map a monitor event to the Ruby-facing name, endpoint, and detail pairs.
140
+ /// Event kinds this binding does not know map to "unknown".
161
141
  fn convert_monitor_event(event: &omq_tokio::MonitorEvent) -> MonitorEventData {
162
- use omq_tokio::MonitorEvent::{
163
- Accepted, Closed, ConnectDelayed, Connected, Disconnected, HandshakeFailed,
164
- HandshakeSucceeded, JoinReceived, LeaveReceived, Listening, SubscribeReceived,
165
- UnsubscribeReceived,
166
- };
167
- match event {
142
+ connection_event(event)
143
+ .or_else(|| attempt_event(event))
144
+ .or_else(|| subscription_event(event))
145
+ .unwrap_or_else(|| MonitorEventData {
146
+ event_type: "unknown",
147
+ endpoint: None,
148
+ detail: vec![],
149
+ })
150
+ }
151
+
152
+ /// Listener and connection lifecycle events.
153
+ fn connection_event(event: &omq_tokio::MonitorEvent) -> Option<MonitorEventData> {
154
+ use omq_tokio::MonitorEvent::{Accepted, Connected, Disconnected, Listening};
155
+
156
+ let data = match event {
168
157
  Listening { endpoint } => MonitorEventData {
169
158
  event_type: "listening",
170
159
  endpoint: Some(endpoint.to_string()),
@@ -188,6 +177,29 @@ fn convert_monitor_event(event: &omq_tokio::MonitorEvent) -> MonitorEventData {
188
177
  endpoint: Some(endpoint.to_string()),
189
178
  detail: vec![("connection_id", MonitorValue::Integer(*connection_id))],
190
179
  },
180
+ Disconnected {
181
+ endpoint,
182
+ peer,
183
+ reason,
184
+ } => MonitorEventData {
185
+ event_type: "disconnected",
186
+ endpoint: Some(endpoint.to_string()),
187
+ detail: vec![
188
+ ("reason", MonitorValue::Text(format!("{reason:?}"))),
189
+ ("connection_id", MonitorValue::Integer(peer.connection_id)),
190
+ ],
191
+ },
192
+ _ => return None,
193
+ };
194
+
195
+ Some(data)
196
+ }
197
+
198
+ /// Connect retries and handshake outcomes.
199
+ fn attempt_event(event: &omq_tokio::MonitorEvent) -> Option<MonitorEventData> {
200
+ use omq_tokio::MonitorEvent::{ConnectDelayed, HandshakeFailed, HandshakeSucceeded};
201
+
202
+ let data = match event {
191
203
  HandshakeSucceeded { endpoint, peer } => {
192
204
  let mut detail = vec![("connection_id", MonitorValue::Integer(peer.connection_id))];
193
205
  if let Some(ref ident) = peer.peer_identity
@@ -223,18 +235,19 @@ fn convert_monitor_event(event: &omq_tokio::MonitorEvent) -> MonitorEventData {
223
235
  ("attempt", MonitorValue::Integer(u64::from(*attempt))),
224
236
  ],
225
237
  },
226
- Disconnected {
227
- endpoint,
228
- peer,
229
- reason,
230
- } => MonitorEventData {
231
- event_type: "disconnected",
232
- endpoint: Some(endpoint.to_string()),
233
- detail: vec![
234
- ("reason", MonitorValue::Text(format!("{reason:?}"))),
235
- ("connection_id", MonitorValue::Integer(peer.connection_id)),
236
- ],
237
- },
238
+ _ => return None,
239
+ };
240
+
241
+ Some(data)
242
+ }
243
+
244
+ /// Subscription and group traffic, plus socket close.
245
+ fn subscription_event(event: &omq_tokio::MonitorEvent) -> Option<MonitorEventData> {
246
+ use omq_tokio::MonitorEvent::{
247
+ Closed, JoinReceived, LeaveReceived, SubscribeReceived, UnsubscribeReceived,
248
+ };
249
+
250
+ let data = match event {
238
251
  SubscribeReceived { prefix } => MonitorEventData {
239
252
  event_type: "subscribe_received",
240
253
  endpoint: None,
@@ -260,12 +273,10 @@ fn convert_monitor_event(event: &omq_tokio::MonitorEvent) -> MonitorEventData {
260
273
  endpoint: None,
261
274
  detail: vec![],
262
275
  },
263
- _ => MonitorEventData {
264
- event_type: "unknown",
265
- endpoint: None,
266
- detail: vec![],
267
- },
268
- }
276
+ _ => return None,
277
+ };
278
+
279
+ Some(data)
269
280
  }
270
281
 
271
282
  async fn push_to_ring(
@@ -295,142 +306,188 @@ async fn push_to_ring(
295
306
  }
296
307
  }
297
308
 
298
- #[expect(clippy::too_many_arguments, clippy::too_many_lines)]
309
+ /// Pump ends and wake signals between the Ruby-facing rings and the socket.
310
+ pub struct PumpChannels {
311
+ pub send_cons: yring::AsyncConsumer<omq_tokio::Message>,
312
+ pub send_notify: Arc<PipeNotify>,
313
+ pub send_waiting: tokio::sync::watch::Sender<bool>,
314
+ pub recv_prod: yring::Producer<omq_tokio::Message>,
315
+ pub recv_notify: Arc<PipeNotify>,
316
+ pub recv_space: Arc<tokio::sync::Notify>,
317
+ }
318
+
319
+ /// Where the monitor pump reports events and one-shot peer milestones.
320
+ pub struct MonitorSinks {
321
+ pub events: flume::Sender<MonitorEventData>,
322
+ pub notify: Arc<PipeNotify>,
323
+ pub peer_connected: Arc<PipeNotify>,
324
+ pub all_peers_gone: Arc<PipeNotify>,
325
+ pub subscriber_joined: Arc<PipeNotify>,
326
+ }
327
+
328
+ /// Background tasks owned by one materialized socket.
329
+ pub struct SocketTasks {
330
+ pub send: JoinHandle<()>,
331
+ pub recv: JoinHandle<()>,
332
+ pub monitor: JoinHandle<()>,
333
+ }
334
+
299
335
  pub fn materialize(
300
336
  io_threads: usize,
301
337
  socket_type: omq_tokio::SocketType,
302
338
  options: omq_tokio::Options,
303
- send_cons: yring::AsyncConsumer<omq_tokio::Message>,
304
- mut recv_prod: yring::Producer<omq_tokio::Message>,
305
- recv_notify: Arc<PipeNotify>,
306
- send_notify: Arc<PipeNotify>,
339
+ pumps: PumpChannels,
340
+ monitor: MonitorSinks,
341
+ ) -> (Arc<InnerSocket>, SocketTasks) {
342
+ // Construction only spawns tasks. Keep the GVL while the caller holds
343
+ // its materialization lock; waiting without it would let close take
344
+ // the GVL and block on that same lock.
345
+ let handle = ensure_runtime(io_threads);
346
+ let _guard = handle.enter();
347
+ let sock = Arc::new(InnerSocket::new(socket_type, options));
348
+
349
+ let send_pump = tokio::spawn(run_send_pump(
350
+ sock.clone(),
351
+ pumps.send_cons,
352
+ pumps.send_notify,
353
+ pumps.send_waiting,
354
+ ));
355
+ let recv_pump = tokio::spawn(run_recv_pump(
356
+ sock.clone(),
357
+ pumps.recv_prod,
358
+ pumps.recv_notify,
359
+ pumps.recv_space,
360
+ ));
361
+
362
+ // Subscribe here, not inside the task: the broadcast receiver keeps
363
+ // events emitted before the task first runs.
364
+ let stream = sock.monitor();
365
+ let monitor_pump = tokio::spawn(run_monitor_pump(sock.clone(), stream, monitor));
366
+
367
+ let tasks = SocketTasks {
368
+ send: send_pump,
369
+ recv: recv_pump,
370
+ monitor: monitor_pump,
371
+ };
372
+ (sock, tasks)
373
+ }
374
+
375
+ /// Feed queued Ruby sends to the socket with a drain budget. REP instead
376
+ /// only watches send capacity.
377
+ async fn run_send_pump(
378
+ s: Arc<InnerSocket>,
379
+ mut send_cons: yring::AsyncConsumer<omq_tokio::Message>,
380
+ sn: Arc<PipeNotify>,
307
381
  send_waiting: tokio::sync::watch::Sender<bool>,
308
- recv_space: Arc<tokio::sync::Notify>,
309
- monitor_tx: flume::Sender<MonitorEventData>,
310
- monitor_notify: Arc<PipeNotify>,
311
- peer_connected_notify: Arc<PipeNotify>,
312
- all_peers_gone_notify: Arc<PipeNotify>,
313
- subscriber_joined_notify: Arc<PipeNotify>,
314
- ) -> (
315
- Arc<InnerSocket>,
316
- JoinHandle<()>,
317
- JoinHandle<()>,
318
- JoinHandle<()>,
319
382
  ) {
320
- let (otx, orx) = flume::bounded(1);
321
- let tx = submit_job(io_threads);
322
- let job: Job = Box::new(move || {
323
- let sock = Arc::new(InnerSocket::new(socket_type, options));
324
-
325
- let s = sock.clone();
326
- let sn = send_notify.clone();
327
- let send_pump = tokio::spawn(async move {
328
- if socket_type == omq_tokio::SocketType::Rep {
329
- // REP submits at application send. Watch only failed sends;
330
- // a successful retry cancels an obsolete capacity wait.
331
- watch_rep_send_capacity(&s, &send_waiting, &sn).await;
332
- return;
333
- }
334
- let mut send_cons = send_cons;
335
- let mut budget = omq_proto::flow::DrainBudget::new(256, 1024 * 1024);
336
- while let Some(msg) = futures::StreamExt::next(&mut send_cons).await {
337
- let byte_len = msg.byte_len();
338
- send_cons.release();
339
- sn.notify();
340
- let _ = s.send(msg).await;
341
- if !budget.account(byte_len) {
342
- budget.reset();
383
+ if s.socket_type() == omq_tokio::SocketType::Rep {
384
+ // REP submits at application send. Watch only failed sends;
385
+ // a successful retry cancels an obsolete capacity wait.
386
+ watch_rep_send_capacity(&s, &send_waiting, &sn).await;
387
+ return;
388
+ }
389
+
390
+ let mut budget = omq_proto::flow::DrainBudget::new(256, 1024 * 1024);
391
+ while let Some(msg) = futures::StreamExt::next(&mut send_cons).await {
392
+ let byte_len = msg.byte_len();
393
+ // Release the ring slot before the send so Ruby can refill while
394
+ // this message is in flight.
395
+ send_cons.release();
396
+ sn.notify();
397
+ let _ = s.send(msg).await;
398
+ if !budget.account(byte_len) {
399
+ budget.reset();
400
+ tokio::task::yield_now().await;
401
+ }
402
+ }
403
+ sn.notify();
404
+ }
405
+
406
+ /// Move received messages into the Ruby ring in budgeted batches, waking
407
+ /// the reader once per batch.
408
+ async fn run_recv_pump(
409
+ s: Arc<InnerSocket>,
410
+ mut recv_prod: yring::Producer<omq_tokio::Message>,
411
+ rn: Arc<PipeNotify>,
412
+ rs: Arc<tokio::sync::Notify>,
413
+ ) {
414
+ loop {
415
+ match s.recv_for_external_recv().await {
416
+ Ok(msg) => {
417
+ let mut budget = omq_proto::flow::DrainBudget::WORKER;
418
+ let mut more = budget.account(msg.byte_len());
419
+ push_to_ring(&mut recv_prod, msg, &rs).await;
420
+
421
+ while more && !recv_prod.is_full() {
422
+ match s.try_recv_for_external_recv() {
423
+ Ok(msg) => {
424
+ more = budget.account(msg.byte_len());
425
+ push_to_ring(&mut recv_prod, msg, &rs).await;
426
+ }
427
+ Err(_) => break,
428
+ }
429
+ }
430
+
431
+ recv_prod.flush();
432
+ rn.force_wake();
433
+ if !more {
343
434
  tokio::task::yield_now().await;
344
435
  }
345
436
  }
346
- sn.notify();
347
- });
348
-
349
- let s = sock.clone();
350
- let rn = recv_notify.clone();
351
- let rs = recv_space.clone();
352
- let recv_pump = tokio::spawn(async move {
353
- loop {
354
- match s.recv_for_external_recv().await {
355
- Ok(msg) => {
356
- let mut budget = omq_proto::flow::DrainBudget::WORKER;
357
- let mut more = budget.account(msg.byte_len());
358
- push_to_ring(&mut recv_prod, msg, &rs).await;
359
-
360
- while more && !recv_prod.is_full() {
361
- match s.try_recv_for_external_recv() {
362
- Ok(msg) => {
363
- more = budget.account(msg.byte_len());
364
- push_to_ring(&mut recv_prod, msg, &rs).await;
365
- }
366
- Err(_) => break,
367
- }
368
- }
437
+ Err(omq_tokio::Error::Closed) => break,
438
+ Err(_) => {}
439
+ }
440
+ }
441
+ }
442
+
443
+ /// Forward monitor events to Ruby and fire the one-shot peer milestones.
444
+ async fn run_monitor_pump(
445
+ peer_ready_sock: Arc<InnerSocket>,
446
+ mut stream: omq_tokio::MonitorStream,
447
+ sinks: MonitorSinks,
448
+ ) {
449
+ let mut peer_count: u32 = 0;
450
+ let mut had_peers = false;
451
+ let mut peer_connected_fired = false;
452
+ let mut subscriber_joined_fired = false;
369
453
 
370
- recv_prod.flush();
371
- rn.force_wake();
372
- if !more {
373
- tokio::task::yield_now().await;
454
+ loop {
455
+ match stream.recv().await {
456
+ Ok(event) => {
457
+ match &event {
458
+ omq_tokio::MonitorEvent::HandshakeSucceeded { .. } => {
459
+ peer_count += 1;
460
+ had_peers = true;
461
+ if !peer_connected_fired {
462
+ peer_connected_fired = true;
463
+ let _ = peer_ready_sock.connections().await;
464
+ sinks.peer_connected.force_wake();
374
465
  }
375
466
  }
376
- Err(omq_tokio::Error::Closed) => break,
377
- Err(_) => {}
378
- }
379
- }
380
- });
381
-
382
- let mut stream = sock.monitor();
383
- let peer_ready_sock = sock.clone();
384
- let monitor_pump = tokio::spawn(async move {
385
- let mut peer_count: u32 = 0;
386
- let mut had_peers = false;
387
- let mut peer_connected_fired = false;
388
- let mut subscriber_joined_fired = false;
389
-
390
- loop {
391
- match stream.recv().await {
392
- Ok(event) => {
393
- match &event {
394
- omq_tokio::MonitorEvent::HandshakeSucceeded { .. } => {
395
- peer_count += 1;
396
- had_peers = true;
397
- if !peer_connected_fired {
398
- peer_connected_fired = true;
399
- let _ = peer_ready_sock.connections().await;
400
- peer_connected_notify.force_wake();
401
- }
402
- }
403
- omq_tokio::MonitorEvent::Disconnected { .. } => {
404
- peer_count = peer_count.saturating_sub(1);
405
- if had_peers && peer_count == 0 {
406
- all_peers_gone_notify.force_wake();
407
- }
408
- }
409
- omq_tokio::MonitorEvent::SubscribeReceived { .. }
410
- | omq_tokio::MonitorEvent::JoinReceived { .. }
411
- if !subscriber_joined_fired =>
412
- {
413
- subscriber_joined_fired = true;
414
- subscriber_joined_notify.force_wake();
415
- }
416
- _ => {}
467
+ omq_tokio::MonitorEvent::Disconnected { .. } => {
468
+ peer_count = peer_count.saturating_sub(1);
469
+ if had_peers && peer_count == 0 {
470
+ sinks.all_peers_gone.force_wake();
417
471
  }
418
-
419
- let data = convert_monitor_event(&event);
420
- let _ = monitor_tx.try_send(data);
421
- monitor_notify.notify();
422
472
  }
423
- Err(omq_tokio::MonitorRecvError::Lagged(_)) => {}
424
- Err(_) => break,
473
+ omq_tokio::MonitorEvent::SubscribeReceived { .. }
474
+ | omq_tokio::MonitorEvent::JoinReceived { .. }
475
+ if !subscriber_joined_fired =>
476
+ {
477
+ subscriber_joined_fired = true;
478
+ sinks.subscriber_joined.force_wake();
479
+ }
480
+ _ => {}
425
481
  }
426
- }
427
- });
428
482
 
429
- let _ = otx.send((sock, send_pump, recv_pump, monitor_pump));
430
- });
431
- tx.send(job).expect("omq-rs: tokio runtime gone");
432
-
433
- recv_blocking(orx, "omq-rs: materialize failed")
483
+ let data = convert_monitor_event(&event);
484
+ let _ = sinks.events.try_send(data);
485
+ sinks.notify.notify();
486
+ }
487
+ Err(omq_tokio::MonitorRecvError::Lagged(_)) => {}
488
+ Err(_) => break,
489
+ }
490
+ }
434
491
  }
435
492
 
436
493
  async fn watch_rep_send_capacity(
@@ -454,15 +511,21 @@ async fn watch_rep_send_capacity(
454
511
  }
455
512
  }
456
513
 
514
+ /// Stop the pumps and close the socket, giving queued sends up to the
515
+ /// linger time (30 s when unset, at least 10 ms) to flush.
457
516
  pub fn destroy_socket(
458
517
  io_threads: usize,
459
518
  sock: Arc<InnerSocket>,
460
519
  send_prod: Mutex<yring::AsyncProducer<omq_tokio::Message>>,
461
- mut send_pump: JoinHandle<()>,
462
- recv_pump: JoinHandle<()>,
463
- monitor_pump: JoinHandle<()>,
520
+ tasks: SocketTasks,
464
521
  linger: Option<Duration>,
465
522
  ) {
523
+ let SocketTasks {
524
+ send: mut send_pump,
525
+ recv: recv_pump,
526
+ monitor: monitor_pump,
527
+ } = tasks;
528
+
466
529
  recv_pump.abort();
467
530
  monitor_pump.abort();
468
531
  if sock.socket_type() == omq_tokio::SocketType::Rep {
@@ -344,7 +344,18 @@ fn rust_socket_materialize_impl(rb_self: &RustSocket) -> RbResult<()> {
344
344
  }
345
345
 
346
346
  #[cfg(feature = "plain")]
347
- if rb_self
347
+ ensure_plain_auth_configured(rb_self)?;
348
+
349
+ let opts = rb_self.options.lock().unwrap().take().unwrap_or_default();
350
+ *slot = Some(spawn_materialized(rb_self.socket_type, opts));
351
+ Ok(())
352
+ }
353
+
354
+ /// A PLAIN server without an installed authenticator would reject every
355
+ /// client; fail loudly at materialization instead.
356
+ #[cfg(feature = "plain")]
357
+ fn ensure_plain_auth_configured(rb_self: &RustSocket) -> RbResult<()> {
358
+ let plain_server = rb_self
348
359
  .options
349
360
  .lock()
350
361
  .unwrap()
@@ -354,15 +365,21 @@ fn rust_socket_materialize_impl(rb_self: &RustSocket) -> RbResult<()> {
354
365
  options.mechanism,
355
366
  omq_proto::MechanismSetup::PlainServer { .. }
356
367
  )
357
- })
358
- && !rb_self.plain_auth_configured.load(Ordering::Acquire)
359
- {
368
+ });
369
+ if plain_server && !rb_self.plain_auth_configured.load(Ordering::Acquire) {
360
370
  return Err(RubyErr::runtime(
361
371
  "PLAIN server requires explicit authentication via set_plain_auth",
362
372
  ));
363
373
  }
374
+ Ok(())
375
+ }
364
376
 
365
- let opts = rb_self.options.lock().unwrap().take().unwrap_or_default();
377
+ /// Build the Ruby-facing rings and signals, then create the socket and
378
+ /// its pumps on the runtime.
379
+ fn spawn_materialized(
380
+ socket_type: omq_tokio::SocketType,
381
+ opts: omq_tokio::Options,
382
+ ) -> Materialized {
366
383
  let send_cap = opts.send_hwm.max(1) as usize;
367
384
  let recv_cap = opts.recv_hwm.max(1) as usize;
368
385
  let (send_prod, send_cons) = yring::async_spsc(send_cap);
@@ -378,41 +395,38 @@ fn rust_socket_materialize_impl(rb_self: &RustSocket) -> RbResult<()> {
378
395
  let all_peers_gone_notify = Arc::new(PipeNotify::new());
379
396
  let subscriber_joined_notify = Arc::new(PipeNotify::new());
380
397
 
381
- let (socket, send_pump, recv_pump, monitor_pump) = runtime::materialize(
382
- io_threads(),
383
- rb_self.socket_type,
384
- opts,
398
+ let pumps = runtime::PumpChannels {
385
399
  send_cons,
400
+ send_notify: send_notify.clone(),
401
+ send_waiting: send_waiting.clone(),
386
402
  recv_prod,
387
- recv_notify.clone(),
388
- send_notify.clone(),
389
- send_waiting.clone(),
390
- recv_space.clone(),
391
- monitor_tx,
392
- monitor_notify.clone(),
393
- peer_connected_notify.clone(),
394
- all_peers_gone_notify.clone(),
395
- subscriber_joined_notify.clone(),
396
- );
403
+ recv_notify: recv_notify.clone(),
404
+ recv_space: recv_space.clone(),
405
+ };
406
+ let monitor = runtime::MonitorSinks {
407
+ events: monitor_tx,
408
+ notify: monitor_notify.clone(),
409
+ peer_connected: peer_connected_notify.clone(),
410
+ all_peers_gone: all_peers_gone_notify.clone(),
411
+ subscriber_joined: subscriber_joined_notify.clone(),
412
+ };
413
+ let (socket, tasks) = runtime::materialize(io_threads(), socket_type, opts, pumps, monitor);
397
414
 
398
- *slot = Some(Materialized {
415
+ Materialized {
399
416
  socket,
400
417
  send_prod: Mutex::new(send_prod),
401
418
  recv_cons: Mutex::new(recv_cons),
402
419
  recv_notify,
403
420
  send_notify,
404
421
  recv_space,
405
- send_pump,
406
422
  send_waiting,
407
- recv_pump,
408
423
  monitor_rx,
409
424
  monitor_notify,
410
425
  peer_connected_notify,
411
426
  all_peers_gone_notify,
412
427
  subscriber_joined_notify,
413
- monitor_pump,
414
- });
415
- Ok(())
428
+ tasks,
429
+ }
416
430
  }
417
431
 
418
432
  unsafe extern "C" fn rust_socket_materialize(rb_self: VALUE) -> VALUE {
@@ -931,15 +945,7 @@ fn rust_socket_close_impl(rb_self: &RustSocket, wait_for_auth_worker: bool) {
931
945
  m.subscriber_joined_notify.force_wake();
932
946
  m.monitor_notify.force_wake();
933
947
  let linger = *rb_self.linger.lock().unwrap();
934
- runtime::destroy_socket(
935
- io_threads(),
936
- m.socket,
937
- m.send_prod,
938
- m.send_pump,
939
- m.recv_pump,
940
- m.monitor_pump,
941
- linger,
942
- );
948
+ runtime::destroy_socket(io_threads(), m.socket, m.send_prod, m.tasks, linger);
943
949
  }
944
950
  }
945
951
 
@@ -1085,33 +1091,55 @@ pub fn register(native: VALUE) -> RbResult<()> {
1085
1091
  rb::undef_alloc_func(class)?;
1086
1092
  rb::define_singleton_method_1(class, c"new", rust_socket_new)?;
1087
1093
  rb::define_method_1(class, c"set_options", rust_socket_set_options)?;
1088
- #[cfg(feature = "curve")]
1089
- {
1090
- rb::define_method_1(
1091
- class,
1092
- c"set_curve_auth_keys",
1093
- rust_socket_set_curve_auth_keys,
1094
- )?;
1095
- rb::define_method_1(
1096
- class,
1097
- c"set_curve_auth_callback",
1098
- rust_socket_set_curve_auth_callback,
1099
- )?;
1100
- rb::define_method_0(class, c"clear_curve_auth", rust_socket_clear_curve_auth)?;
1101
- }
1102
- #[cfg(feature = "plain")]
1103
- {
1104
- rb::define_method_1(
1105
- class,
1106
- c"set_plain_auth_credentials",
1107
- rust_socket_set_plain_auth_credentials,
1108
- )?;
1109
- rb::define_method_1(
1110
- class,
1111
- c"set_plain_auth_callback",
1112
- rust_socket_set_plain_auth_callback,
1113
- )?;
1114
- }
1094
+ }
1095
+
1096
+ #[cfg(feature = "curve")]
1097
+ register_curve_auth_methods(class)?;
1098
+ #[cfg(feature = "plain")]
1099
+ register_plain_auth_methods(class)?;
1100
+ register_io_methods(class)?;
1101
+
1102
+ Ok(())
1103
+ }
1104
+
1105
+ #[cfg(feature = "curve")]
1106
+ fn register_curve_auth_methods(class: VALUE) -> RbResult<()> {
1107
+ unsafe {
1108
+ rb::define_method_1(
1109
+ class,
1110
+ c"set_curve_auth_keys",
1111
+ rust_socket_set_curve_auth_keys,
1112
+ )?;
1113
+ rb::define_method_1(
1114
+ class,
1115
+ c"set_curve_auth_callback",
1116
+ rust_socket_set_curve_auth_callback,
1117
+ )?;
1118
+ rb::define_method_0(class, c"clear_curve_auth", rust_socket_clear_curve_auth)?;
1119
+ }
1120
+ Ok(())
1121
+ }
1122
+
1123
+ #[cfg(feature = "plain")]
1124
+ fn register_plain_auth_methods(class: VALUE) -> RbResult<()> {
1125
+ unsafe {
1126
+ rb::define_method_1(
1127
+ class,
1128
+ c"set_plain_auth_credentials",
1129
+ rust_socket_set_plain_auth_credentials,
1130
+ )?;
1131
+ rb::define_method_1(
1132
+ class,
1133
+ c"set_plain_auth_callback",
1134
+ rust_socket_set_plain_auth_callback,
1135
+ )?;
1136
+ }
1137
+ Ok(())
1138
+ }
1139
+
1140
+ /// Lifecycle, endpoint, message, readiness fd, and subscription methods.
1141
+ fn register_io_methods(class: VALUE) -> RbResult<()> {
1142
+ unsafe {
1115
1143
  rb::define_method_0(class, c"materialize", rust_socket_materialize)?;
1116
1144
  rb::define_method_1(class, c"bind", rust_socket_bind)?;
1117
1145
  rb::define_method_1(class, c"connect", rust_socket_connect)?;
data/lib/omq/rs/socket.rb CHANGED
@@ -671,11 +671,13 @@ module OMQ
671
671
  #
672
672
  # @return [nil]
673
673
  def close
674
- return if closed?
674
+ @materialize_lock.synchronize do
675
+ return if closed?
675
676
 
676
- @native.close
677
- close_wrapper(@recv_io)
678
- close_wrapper(@send_io)
677
+ @native.close
678
+ close_wrapper(@recv_io)
679
+ close_wrapper(@send_io)
680
+ end
679
681
  nil
680
682
  end
681
683
 
@@ -5,6 +5,6 @@ module OMQ
5
5
  module Rust
6
6
  # Ruby binding version.
7
7
  # @return [String]
8
- VERSION = "0.2.5"
8
+ VERSION = "0.2.7"
9
9
  end
10
10
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omq-rs
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.5
4
+ version: 0.2.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrik Wenger