wreq-rb 0.6.1 → 0.6.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: dd3f8d369b26129c00889f9e357c368ad8db091e5feabb5273d5b92cb4e92c56
4
- data.tar.gz: 84e3bdf4fedc4340b8d88495acbb2a8b1020bb77837d15b4bdec6070d3d3bc72
3
+ metadata.gz: ce3d1d61f00a9a6585dcf3dedf9c32efe8bb25d99ab3ce332deee5de6c84ee4f
4
+ data.tar.gz: 97e22c094b0d0f28db1c0e65f94eb82eeca2f55bb1a96fa8e7f706d17a008df3
5
5
  SHA512:
6
- metadata.gz: 7f7e75b4ec8e64998c979157d640529244a0e102a4ebdfb67d3c6a3a2543b468e6688b05bce68f7ad2480f12cdfadcc727b5a041e3bd40f429149e63294fa397
7
- data.tar.gz: ddf1859a8f3cab61028fd74416e4f5c4633ffab12f9c9dda9e8828eaf9ac4c605c8da2547d10b8256fbbbb6ee3d30c40a1d2affd145eb17e478bbecd76c03c27
6
+ metadata.gz: 7ce21633e4394d4f4d6a3a71ad38708d98e9809f3d0910bf5e0f947adb2bdabbb59481bcd5a4c9096114fc74ffa035c2519e4460733a61c9a810220be3c6b7a3
7
+ data.tar.gz: 643d9f5411aaa343c165665a9a1de939102d2abe7396b5c07ff16e18456a0c5fce885ffe0118c57491ce850227117a5d5248ebda5d083c0b5c903020b0458978
data/Cargo.lock CHANGED
@@ -4211,7 +4211,7 @@ dependencies = [
4211
4211
 
4212
4212
  [[package]]
4213
4213
  name = "wreq_rb"
4214
- version = "0.6.1"
4214
+ version = "0.6.2"
4215
4215
  dependencies = [
4216
4216
  "bytes",
4217
4217
  "http",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "wreq_rb"
3
- version = "0.6.1"
3
+ version = "0.6.2"
4
4
  edition = "2021"
5
5
  publish = false
6
6
 
@@ -31,6 +31,9 @@ serde_json = "1.0"
31
31
  bytes = "1"
32
32
  http = "1"
33
33
 
34
+ [dev-dependencies]
35
+ magnus = { version = "0.8", features = ["embed"] }
36
+
34
37
  [target.'cfg(target_os = "linux")'.dependencies]
35
38
  wreq = { path = "../../vendor/wreq", version = "=0.16.1", features = [
36
39
  "prefix-symbols",
@@ -47,7 +47,7 @@ fn runtime() -> &'static Runtime {
47
47
  /// # Safety
48
48
  /// The closure must NOT access any Ruby objects or call any Ruby C API.
49
49
  /// Extract all data from Ruby before calling this, convert results after.
50
- unsafe fn without_gvl<F, R>(f: F) -> R
50
+ unsafe fn without_gvl<F, R>(f: F) -> Result<R, magnus::Error>
51
51
  where
52
52
  F: FnOnce(CancellationToken) -> R,
53
53
  {
@@ -89,19 +89,22 @@ where
89
89
  let data_ptr = &mut data as *mut CallData<F, R> as *mut c_void;
90
90
 
91
91
  unsafe {
92
- rb_sys::rb_thread_call_without_gvl(
93
- Some(call::<F, R>),
94
- data_ptr,
95
- Some(ubf::<F, R>),
96
- data_ptr,
97
- );
92
+ magnus::rb_sys::protect(|| {
93
+ rb_sys::rb_thread_call_without_gvl(
94
+ Some(call::<F, R>),
95
+ data_ptr,
96
+ Some(ubf::<F, R>),
97
+ data_ptr,
98
+ );
99
+ 0
100
+ })?;
98
101
  }
99
102
 
100
103
  if let Some(payload) = data.panic_payload {
101
104
  panic::resume_unwind(payload);
102
105
  }
103
106
 
104
- data.result.unwrap()
107
+ Ok(data.result.unwrap())
105
108
  }
106
109
 
107
110
  /// Collected response data as pure Rust types (no Ruby objects).
@@ -288,12 +291,13 @@ impl Client {
288
291
  builder = builder.default_headers(hmap);
289
292
  }
290
293
 
291
- if let Some(t) = hash_get_float(&opts, "timeout")? {
292
- builder = builder.timeout(Duration::from_secs_f64(t));
294
+ let timeout = hash_get_float(&opts, "timeout")?;
295
+ if let Some(timeout) = timeout {
296
+ builder = builder.timeout(Duration::from_secs_f64(timeout));
293
297
  }
294
298
 
295
- if let Some(t) = hash_get_float(&opts, "connect_timeout")? {
296
- builder = builder.connect_timeout(Duration::from_secs_f64(t));
299
+ if let Some(connect_timeout) = hash_get_float(&opts, "connect_timeout")?.or(timeout) {
300
+ builder = builder.connect_timeout(Duration::from_secs_f64(connect_timeout));
297
301
  }
298
302
 
299
303
  if let Some(t) = hash_get_float(&opts, "read_timeout")? {
@@ -489,7 +493,7 @@ impl Client {
489
493
  }
490
494
  })
491
495
  })
492
- };
496
+ }?;
493
497
 
494
498
  let data = match outcome {
495
499
  RequestOutcome::Ok(d) => d,
@@ -545,7 +549,7 @@ impl Client {
545
549
  }
546
550
  })
547
551
  })
548
- };
552
+ }?;
549
553
 
550
554
  let items = match outcome {
551
555
  BatchOutcome::Done(items) => items,
@@ -877,3 +881,89 @@ pub fn init(_ruby: &magnus::Ruby, module: &magnus::RModule) -> Result<(), magnus
877
881
 
878
882
  Ok(())
879
883
  }
884
+
885
+ #[cfg(test)]
886
+ mod tests {
887
+ use super::*;
888
+ use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
889
+
890
+ static READY: AtomicBool = AtomicBool::new(false);
891
+ static LIVE: AtomicUsize = AtomicUsize::new(0);
892
+
893
+ struct DropProbe;
894
+
895
+ impl DropProbe {
896
+ fn new() -> Self {
897
+ LIVE.fetch_add(1, Ordering::SeqCst);
898
+ Self
899
+ }
900
+ }
901
+
902
+ impl Drop for DropProbe {
903
+ fn drop(&mut self) {
904
+ LIVE.fetch_sub(1, Ordering::SeqCst);
905
+ }
906
+ }
907
+
908
+ fn probe_ready() -> bool {
909
+ READY.load(Ordering::SeqCst)
910
+ }
911
+
912
+ fn drop_probe() -> Result<(), magnus::Error> {
913
+ let _caller_resource = DropProbe::new();
914
+ let result = unsafe {
915
+ without_gvl(|token| {
916
+ let _callback_resource = DropProbe::new();
917
+ READY.store(true, Ordering::SeqCst);
918
+ runtime().block_on(token.cancelled());
919
+ DropProbe::new()
920
+ })
921
+ }?;
922
+ drop(result);
923
+ Ok(())
924
+ }
925
+
926
+ #[test]
927
+ fn ruby_interrupts_drop_native_resources() {
928
+ let ruby = unsafe { magnus::embed::init() };
929
+ let result = unsafe { without_gvl(|_| DropProbe::new()) }.unwrap();
930
+ assert_eq!(LIVE.load(Ordering::SeqCst), 1);
931
+ drop(result);
932
+ assert_eq!(LIVE.load(Ordering::SeqCst), 0);
933
+
934
+ let panicked = panic::catch_unwind(|| unsafe {
935
+ without_gvl::<_, ()>(|_| {
936
+ let _resource = DropProbe::new();
937
+ panic!("probe panic");
938
+ })
939
+ });
940
+ assert!(panicked.is_err());
941
+ assert_eq!(LIVE.load(Ordering::SeqCst), 0);
942
+
943
+ ruby.define_global_function("wreq_drop_probe", function!(drop_probe, 0));
944
+ ruby.define_global_function("wreq_probe_ready", function!(probe_ready, 0));
945
+
946
+ for (interrupt, expected) in [
947
+ ("worker.kill", None),
948
+ ("worker.raise(RuntimeError, 'stop request')", Some("stop request")),
949
+ ] {
950
+ READY.store(false, Ordering::SeqCst);
951
+ let result: Option<String> = ruby.eval(&format!(
952
+ "worker = Thread.new do
953
+ begin
954
+ wreq_drop_probe
955
+ 'returned normally'
956
+ rescue RuntimeError => error
957
+ error.message
958
+ end
959
+ end
960
+ Thread.pass until wreq_probe_ready
961
+ {interrupt}
962
+ worker.value"
963
+ )).unwrap();
964
+
965
+ assert_eq!(result.as_deref(), expected);
966
+ assert_eq!(LIVE.load(Ordering::SeqCst), 0, "resources leaked after {interrupt}");
967
+ }
968
+ }
969
+ }
@@ -5,7 +5,8 @@ use magnus::{
5
5
  use crate::error::generic_error;
6
6
 
7
7
  /// Wraps a wreq::Response in a Ruby-accessible type.
8
- #[magnus::wrap(class = "Wreq::Response", free_immediately)]
8
+ #[derive(magnus::TypedData)]
9
+ #[magnus(class = "Wreq::Response", free_immediately, size)]
9
10
  pub struct Response {
10
11
  status: u16,
11
12
  headers: Vec<(String, String)>,
@@ -26,7 +27,7 @@ impl Response {
26
27
  content_length: Option<u64>,
27
28
  transfer_size: Option<u64>,
28
29
  ) -> Self {
29
- Self {
30
+ let response = Self {
30
31
  status,
31
32
  headers,
32
33
  body,
@@ -34,7 +35,18 @@ impl Response {
34
35
  version,
35
36
  content_length,
36
37
  transfer_size,
37
- }
38
+ };
39
+ unsafe { Ruby::get_unchecked() }
40
+ .gc_adjust_memory_usage(response.heap_size() as isize);
41
+ response
42
+ }
43
+
44
+ fn heap_size(&self) -> usize {
45
+ self.body.capacity()
46
+ + self.url.capacity()
47
+ + self.version.capacity()
48
+ + self.headers.capacity() * std::mem::size_of::<(String, String)>()
49
+ + self.headers.iter().map(|(name, value)| name.capacity() + value.capacity()).sum::<usize>()
38
50
  }
39
51
 
40
52
  fn status(&self) -> u16 {
@@ -119,6 +131,19 @@ impl Response {
119
131
  }
120
132
  }
121
133
 
134
+ impl magnus::DataTypeFunctions for Response {
135
+ fn size(&self) -> usize {
136
+ std::mem::size_of::<Self>() + self.heap_size()
137
+ }
138
+ }
139
+
140
+ impl Drop for Response {
141
+ fn drop(&mut self) {
142
+ unsafe { Ruby::get_unchecked() }
143
+ .gc_adjust_memory_usage(-(self.heap_size() as isize));
144
+ }
145
+ }
146
+
122
147
  pub fn init(ruby: &magnus::Ruby, module: &magnus::RModule) -> Result<(), magnus::Error> {
123
148
  let class = module.define_class("Response", ruby.class_object())?;
124
149
  class.define_method("status", method!(Response::status, 0))?;
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Wreq
4
- VERSION = "0.6.1"
4
+ VERSION = "0.6.2"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wreq-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.1
4
+ version: 0.6.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yicheng Zhou
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2026-08-28 00:00:00.000000000 Z
12
+ date: 2026-09-13 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rb_sys