outpunch-ruby 0.5.1 → 0.5.2

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: 3ec879b4be7ad61501c03feb26f617df557016c6c57190fc799fa596df8fb5d5
4
- data.tar.gz: 2f59a5f6fd106d67baa2df00ede7656cc6b5f1e7f0eb5ca44d9766723925e357
3
+ metadata.gz: a0fde3ed16a529184039dde0c3143e09b131961a13fb2adc3e165b2748260e5c
4
+ data.tar.gz: d65b4b5de98d43b4b53413c5e56b10a2c628032f4510057c104d1142a97e6869
5
5
  SHA512:
6
- metadata.gz: 3414f515a0c1b501d1e4934b1c780e87ef13b3cc6fbfe1493be63e7974b694c849a6fb4cb0cda79b2af444d499142c1cad0136545f06bd1823646366c26a2b95
7
- data.tar.gz: b7f562121df9ab1ccb1e36097474a4225dae2ce20e91e2fbc66c8e79fccfb578f7bfc83777939eaf4cf5119831bd32c7cd24c1f3c492c6cbb782fa51a3da7cc4
6
+ metadata.gz: cf33afb82f7144a1326cc1fcc908dc750514d9cf9f88e78561bc171d6ca9ecc00eab2ab9e9cef8281796285fd21c0db0a96cabde4dbddebf202f5a9fb30b94c6
7
+ data.tar.gz: b3a9a6e390b5feaa49a0de162975098cd43054a6ef0e7541e1a7aa13eaf26bb138ae309ed3e7852ee918ab9b4fa528b3a415958991d6195d558c0a0c8c3476f1
@@ -8,11 +8,11 @@ name = "outpunch"
8
8
  crate-type = ["cdylib"]
9
9
 
10
10
  [dependencies]
11
- outpunch-client = { version = "0.5.1" }
11
+ outpunch-client = { version = "0.5.2" }
12
12
  magnus = { version = "0.8" }
13
13
  rb-sys = { version = "0.9", features = ["stable-api-compiled-fallback"] }
14
14
  tokio = { version = "1.50.0", features = ["full"] }
15
- outpunch_core = { version = "0.5.1", package = "outpunch" }
15
+ outpunch_core = { version = "0.5.2", package = "outpunch" }
16
16
 
17
17
  [build-dependencies]
18
18
  rb-sys-build = "0.9"
@@ -30,38 +30,43 @@ unsafe extern "C" fn unblock(data: *mut c_void) {
30
30
  }
31
31
 
32
32
  /// Run `func` with the GVL released. `func` receives a `Notify` that fires when Ruby
33
- /// interrupts the thread (Thread#kill, Thread#raise, Timeout); it returns `None` when
34
- /// it stopped early because of that.
33
+ /// interrupts the thread (Thread#raise, Thread#kill, Timeout, Thread#wakeup, signals); it
34
+ /// returns `None` when it stopped early because of that.
35
35
  ///
36
- /// Returns `Ok(None)` when the call was cancelled and Ruby had no exception pending.
37
- pub fn without_gvl<F, R>(ruby: &Ruby, func: F) -> Result<Option<R>, Error>
36
+ /// `rb_thread_call_without_gvl2` does not call `func` at all while an interrupt is pending,
37
+ /// and the VM timer makes one pending whenever threads compete for the GVL. So when `func` did
38
+ /// not run, the interrupt is handled and `func` is called again.
39
+ ///
40
+ /// Returns `Err` when Ruby has an exception to raise, `Ok(None)` when `func` was cancelled by an
41
+ /// interrupt that raised nothing (the caller decides whether to try again), `Ok(Some)` otherwise.
42
+ pub fn without_gvl<F, R>(ruby: &Ruby, mut func: F) -> Result<Option<R>, Error>
38
43
  where
39
- F: FnOnce(Arc<Notify>) -> Option<R>,
44
+ F: FnMut(Arc<Notify>) -> Option<R>,
40
45
  {
41
- let cancel = Arc::new(Notify::new());
42
- let mut state = Call {
43
- func: Some({
44
- let cancel = cancel.clone();
45
- move || func(cancel)
46
- }),
47
- result: None,
48
- };
46
+ loop {
47
+ let cancel = Arc::new(Notify::new());
48
+ let mut state = Call {
49
+ func: Some(|| func(cancel.clone())),
50
+ result: None,
51
+ };
49
52
 
50
- unsafe {
51
- rb_sys::rb_thread_call_without_gvl2(
52
- Some(trampoline(&state)),
53
- &mut state as *mut _ as *mut c_void,
54
- Some(unblock),
55
- Arc::as_ptr(&cancel) as *mut c_void,
56
- );
57
- }
53
+ unsafe {
54
+ rb_sys::rb_thread_call_without_gvl2(
55
+ Some(trampoline(&state)),
56
+ &mut state as *mut _ as *mut c_void,
57
+ Some(unblock),
58
+ Arc::as_ptr(&cancel) as *mut c_void,
59
+ );
60
+ }
58
61
 
59
- match state.result {
60
- Some(Ok(Some(value))) => Ok(Some(value)),
61
- Some(Ok(None)) | None => {
62
- ruby.thread_check_ints()?;
63
- Ok(None)
62
+ match state.result {
63
+ Some(Ok(Some(value))) => return Ok(Some(value)),
64
+ Some(Ok(None)) => {
65
+ ruby.thread_check_ints()?;
66
+ return Ok(None);
67
+ }
68
+ Some(Err(payload)) => panic::resume_unwind(payload),
69
+ None => ruby.thread_check_ints()?,
64
70
  }
65
- Some(Err(payload)) => panic::resume_unwind(payload),
66
71
  }
67
72
  }
@@ -1,4 +1,5 @@
1
1
  use std::collections::HashMap;
2
+ use std::panic;
2
3
  use std::sync::OnceLock;
3
4
  use std::time::Duration;
4
5
 
@@ -9,6 +10,7 @@ use outpunch_core::server::{
9
10
  };
10
11
  use tokio::runtime::Runtime;
11
12
  use tokio::sync::{Mutex, mpsc};
13
+ use tokio::task::{JoinError, JoinHandle};
12
14
 
13
15
  use crate::gvl::without_gvl;
14
16
 
@@ -23,6 +25,35 @@ fn runtime() -> &'static Runtime {
23
25
  })
24
26
  }
25
27
 
28
+ /// Wait for a task on the runtime with the GVL released. An interrupt that raises nothing
29
+ /// (Thread#wakeup, the VM timer) keeps waiting; one that raises returns `Err` and leaves the
30
+ /// task running for the caller to stop.
31
+ fn wait_for<T>(ruby: &Ruby, task: &mut JoinHandle<T>) -> Result<T, Error> {
32
+ loop {
33
+ let joined = without_gvl(ruby, |cancel| {
34
+ runtime().block_on(async {
35
+ tokio::select! {
36
+ joined = &mut *task => Some(joined),
37
+ _ = cancel.notified() => None,
38
+ }
39
+ })
40
+ })?;
41
+ if let Some(joined) = joined {
42
+ return joined.map_err(|err| task_failed(ruby, err));
43
+ }
44
+ }
45
+ }
46
+
47
+ fn task_failed(ruby: &Ruby, err: JoinError) -> Error {
48
+ if err.is_panic() {
49
+ panic::resume_unwind(err.into_panic());
50
+ }
51
+ Error::new(
52
+ ruby.exception_runtime_error(),
53
+ format!("outpunch task failed: {err}"),
54
+ )
55
+ }
56
+
26
57
  fn outpunch_error(ruby: &Ruby, name: &str) -> Result<ExceptionClass, Error> {
27
58
  ruby.class_object()
28
59
  .const_get::<_, RModule>("Outpunch")?
@@ -61,12 +92,10 @@ impl Server {
61
92
 
62
93
  fn is_connected(ruby: &Ruby, rb_self: &Self, service: String) -> Result<bool, Error> {
63
94
  loop {
64
- let server = rb_self.inner.clone();
65
- let service = service.clone();
66
- let result = without_gvl(ruby, move |cancel| {
95
+ let result = without_gvl(ruby, |cancel| {
67
96
  runtime().block_on(async {
68
97
  tokio::select! {
69
- connected = server.is_connected(&service) => Some(connected),
98
+ connected = rb_self.inner.is_connected(&service) => Some(connected),
70
99
  _ = cancel.notified() => None,
71
100
  }
72
101
  })
@@ -102,30 +131,20 @@ impl Server {
102
131
  };
103
132
 
104
133
  let server = rb_self.inner.clone();
105
- let result = without_gvl(ruby, move |cancel| {
106
- runtime().block_on(async {
107
- tokio::select! {
108
- result = server.try_handle_request(incoming) => Some(result),
109
- _ = cancel.notified() => None,
110
- }
111
- })
112
- })?;
134
+ let mut task = runtime().spawn(async move { server.try_handle_request(incoming).await });
135
+ let result = wait_for(ruby, &mut task).inspect_err(|_| task.abort())?;
113
136
 
114
137
  match result {
115
- None => Err(Error::new(
116
- ruby.exception_runtime_error(),
117
- "tunnel request interrupted",
118
- )),
119
- Some(Ok(response)) => response_hash(ruby, response),
120
- Some(Err(TunnelError::NotConnected)) => Err(Error::new(
138
+ Ok(response) => response_hash(ruby, response),
139
+ Err(TunnelError::NotConnected) => Err(Error::new(
121
140
  outpunch_error(ruby, "NotConnectedError")?,
122
141
  format!("Service '{service}' not connected"),
123
142
  )),
124
- Some(Err(TunnelError::Timeout)) => Err(Error::new(
143
+ Err(TunnelError::Timeout) => Err(Error::new(
125
144
  outpunch_error(ruby, "TunnelTimeoutError")?,
126
145
  format!("Tunnel timeout for service '{service}'"),
127
146
  )),
128
- Some(Err(err)) => Err(Error::new(
147
+ Err(err) => Err(Error::new(
129
148
  outpunch_error(ruby, "TunnelError")?,
130
149
  err.to_string(),
131
150
  )),
@@ -150,18 +169,23 @@ pub struct Connection {
150
169
  }
151
170
 
152
171
  impl Connection {
153
- /// Feed one WebSocket text message from the tunnel client into the core.
172
+ /// Feed one WebSocket text message from the tunnel client into the core. Blocks while the
173
+ /// core's queue is full.
154
174
  fn push_message(ruby: &Ruby, rb_self: &Self, text: String) -> Result<(), Error> {
155
- let inner = rb_self.inner.clone();
156
- without_gvl(ruby, move |cancel| {
157
- runtime().block_on(async {
158
- tokio::select! {
159
- _ = inner.push_message(text) => Some(()),
160
- _ = cancel.notified() => None,
161
- }
162
- })
163
- })?;
164
- Ok(())
175
+ // A cancelled send delivered nothing, so send again until it lands or Ruby raises.
176
+ loop {
177
+ let sent = without_gvl(ruby, |cancel| {
178
+ runtime().block_on(async {
179
+ tokio::select! {
180
+ _ = rb_self.inner.push_message(text.clone()) => Some(()),
181
+ _ = cancel.notified() => None,
182
+ }
183
+ })
184
+ })?;
185
+ if sent.is_some() {
186
+ return Ok(());
187
+ }
188
+ }
165
189
  }
166
190
 
167
191
  /// Block until the core has a message for the tunnel client. Returns nil once the
@@ -187,25 +211,16 @@ impl Connection {
187
211
  self.inner.close();
188
212
  }
189
213
 
190
- /// Run the connection lifecycle (auth, relay, cleanup). Blocks until the
191
- /// connection ends; an interrupt closes the connection.
214
+ /// Run the connection lifecycle (auth, relay, cleanup). Blocks until the connection ends.
215
+ /// An exception raised into this thread (Thread#raise, Thread#kill, Timeout) closes the
216
+ /// connection and then propagates.
192
217
  fn run(ruby: &Ruby, rb_self: &Self) -> Result<(), Error> {
193
218
  let inner = rb_self.inner.clone();
194
- without_gvl(ruby, move |cancel| {
195
- runtime().block_on(async {
196
- let run = inner.run();
197
- tokio::pin!(run);
198
- tokio::select! {
199
- _ = &mut run => {}
200
- _ = cancel.notified() => {
201
- inner.close();
202
- run.await;
203
- }
204
- }
205
- Some(())
206
- })
207
- })?;
208
- Ok(())
219
+ let mut task = runtime().spawn(async move { inner.run().await });
220
+ wait_for(ruby, &mut task).inspect_err(|_| {
221
+ rb_self.inner.close();
222
+ let _ = runtime().block_on(task);
223
+ })
209
224
  }
210
225
  }
211
226
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: outpunch-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.5.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - TechyCorp