wreq 1.2.11 → 1.2.13
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.lock +88 -146
- data/Cargo.toml +10 -7
- data/crates/wreq-util/src/emulate/profile/chrome/http2.rs +1 -1
- data/crates/wreq-util/src/emulate/profile/firefox/http2.rs +2 -2
- data/crates/wreq-util/src/emulate/profile/opera/http2.rs +1 -1
- data/docs/fork-safety.md +51 -0
- data/docs/interrupt-handling.md +134 -0
- data/examples/tls_info.rb +27 -0
- data/lib/wreq.rb +37 -0
- data/lib/wreq_ruby/body.rb +9 -0
- data/lib/wreq_ruby/client.rb +24 -1
- data/lib/wreq_ruby/cookie.rb +9 -0
- data/lib/wreq_ruby/error.rb +15 -0
- data/lib/wreq_ruby/response.rb +44 -0
- data/lib/wreq_ruby/tls.rb +73 -0
- data/src/arch.rs +174 -0
- data/src/client/body/stream.rs +28 -22
- data/src/client/req.rs +119 -122
- data/src/client/resp.rs +120 -47
- data/src/client.rs +103 -44
- data/src/cookie.rs +47 -11
- data/src/error.rs +28 -0
- data/src/lib.rs +3 -1
- data/src/macros.rs +2 -3
- data/src/rt.rs +46 -25
- data/src/tls.rs +51 -0
- data/test/fork_test.rb +106 -0
- data/test/scripts/fork_safety.rb +133 -0
- data/test/scripts/prefork_runtime.rb +95 -0
- data/test/support/tls_server.rb +95 -0
- data/test/tls_info_test.rb +59 -0
- metadata +11 -1
data/lib/wreq_ruby/response.rb
CHANGED
|
@@ -8,6 +8,13 @@ unless defined?(Wreq)
|
|
|
8
8
|
# access to HTTP response data including status codes, headers, body
|
|
9
9
|
# content, and streaming capabilities.
|
|
10
10
|
#
|
|
11
|
+
# A response belongs to the process that received it. Accessing its metadata
|
|
12
|
+
# or body after inheriting it from a parent raises Wreq::ForkError.
|
|
13
|
+
#
|
|
14
|
+
# @note Fork safety Keep each response in the process that received it.
|
|
15
|
+
# Issue a new request in the worker instead of carrying a response through
|
|
16
|
+
# `fork`.
|
|
17
|
+
#
|
|
11
18
|
# @example Basic response handling
|
|
12
19
|
# response = client.get("https://api.example.com")
|
|
13
20
|
# puts response.status.as_int # => 200
|
|
@@ -26,6 +33,7 @@ unless defined?(Wreq)
|
|
|
26
33
|
# Get the HTTP status code as an integer.
|
|
27
34
|
#
|
|
28
35
|
# @return [Integer] Status code (e.g., 200, 404, 500)
|
|
36
|
+
# @raise [Wreq::ForkError] if the response belongs to the parent process
|
|
29
37
|
# @example
|
|
30
38
|
# response.code # => 200
|
|
31
39
|
def code
|
|
@@ -34,6 +42,7 @@ unless defined?(Wreq)
|
|
|
34
42
|
# Get the HTTP status code object.
|
|
35
43
|
#
|
|
36
44
|
# @return [Wreq::StatusCode] Status code wrapper with helper methods
|
|
45
|
+
# @raise [Wreq::ForkError] if the response belongs to the parent process
|
|
37
46
|
# @example
|
|
38
47
|
# status = response.status
|
|
39
48
|
# status.success? # => true
|
|
@@ -43,6 +52,7 @@ unless defined?(Wreq)
|
|
|
43
52
|
# Get the HTTP protocol version used.
|
|
44
53
|
#
|
|
45
54
|
# @return [Wreq::Version] HTTP version (HTTP/1.1, HTTP/2, etc.)
|
|
55
|
+
# @raise [Wreq::ForkError] if the response belongs to the parent process
|
|
46
56
|
# @example
|
|
47
57
|
# response.version # => Wreq::Version::HTTP_11
|
|
48
58
|
def version
|
|
@@ -51,6 +61,7 @@ unless defined?(Wreq)
|
|
|
51
61
|
# Get the final URL after redirects.
|
|
52
62
|
#
|
|
53
63
|
# @return [String] The final URL
|
|
64
|
+
# @raise [Wreq::ForkError] if the response belongs to the parent process
|
|
54
65
|
# @example
|
|
55
66
|
# response.url # => "https://example.com/final-page"
|
|
56
67
|
def url
|
|
@@ -59,6 +70,7 @@ unless defined?(Wreq)
|
|
|
59
70
|
# Get the content length if known.
|
|
60
71
|
#
|
|
61
72
|
# @return [Integer, nil] Content length in bytes, or nil if unknown
|
|
73
|
+
# @raise [Wreq::ForkError] if the response belongs to the parent process
|
|
62
74
|
# @example
|
|
63
75
|
# response.content_length # => 1024
|
|
64
76
|
def content_length
|
|
@@ -72,6 +84,7 @@ unless defined?(Wreq)
|
|
|
72
84
|
# response or a later snapshot, and object identity is not guaranteed.
|
|
73
85
|
#
|
|
74
86
|
# @return [Wreq::Headers] Response headers
|
|
87
|
+
# @raise [Wreq::ForkError] if the response belongs to the parent process
|
|
75
88
|
# @example
|
|
76
89
|
# response.headers.get("content-type") # => "application/json"
|
|
77
90
|
def headers
|
|
@@ -80,6 +93,7 @@ unless defined?(Wreq)
|
|
|
80
93
|
# Get the local socket address.
|
|
81
94
|
#
|
|
82
95
|
# @return [String, nil] Local address (e.g., "127.0.0.1:54321"), or nil
|
|
96
|
+
# @raise [Wreq::ForkError] if the response belongs to the parent process
|
|
83
97
|
# @example
|
|
84
98
|
# response.local_addr # => "192.168.1.100:54321"
|
|
85
99
|
def local_addr
|
|
@@ -88,6 +102,7 @@ unless defined?(Wreq)
|
|
|
88
102
|
# Get the remote socket address.
|
|
89
103
|
#
|
|
90
104
|
# @return [String, nil] Remote address (e.g., "93.184.216.34:443"), or nil
|
|
105
|
+
# @raise [Wreq::ForkError] if the response belongs to the parent process
|
|
91
106
|
# @example
|
|
92
107
|
# response.remote_addr # => "93.184.216.34:443"
|
|
93
108
|
def remote_addr
|
|
@@ -98,6 +113,7 @@ unless defined?(Wreq)
|
|
|
98
113
|
# Invalid `Set-Cookie` values are skipped.
|
|
99
114
|
#
|
|
100
115
|
# @return [Array<Wreq::Cookie>] Parsed response cookies
|
|
116
|
+
# @raise [Wreq::ForkError] if the response belongs to the parent process
|
|
101
117
|
# @example
|
|
102
118
|
# response.cookies.each do |cookie|
|
|
103
119
|
# puts "#{cookie.name}=#{cookie.value}"
|
|
@@ -107,6 +123,7 @@ unless defined?(Wreq)
|
|
|
107
123
|
|
|
108
124
|
# Get the response bytes as a binary string.
|
|
109
125
|
# @return [String] Response body as binary data
|
|
126
|
+
# @raise [Wreq::ForkError] if the response belongs to the parent process
|
|
110
127
|
# @example
|
|
111
128
|
# binary_data = response.bytes
|
|
112
129
|
# puts binary_data.size # => 1024
|
|
@@ -122,6 +139,7 @@ unless defined?(Wreq)
|
|
|
122
139
|
# html = response.text("ISO-8859-1")
|
|
123
140
|
# puts html
|
|
124
141
|
# @raise [Wreq::DecodingError] if body cannot be decoded with the specified encoding
|
|
142
|
+
# @raise [Wreq::ForkError] if the response belongs to the parent process
|
|
125
143
|
def text(default_encoding = "UTF-8")
|
|
126
144
|
end
|
|
127
145
|
|
|
@@ -132,6 +150,7 @@ unless defined?(Wreq)
|
|
|
132
150
|
#
|
|
133
151
|
# @return [Object] Parsed JSON (Hash, Array, String, Integer, Float, Boolean, nil)
|
|
134
152
|
# @raise [Wreq::DecodingError] if body is not valid JSON
|
|
153
|
+
# @raise [Wreq::ForkError] if the response belongs to the parent process
|
|
135
154
|
# @example
|
|
136
155
|
# data = response.json
|
|
137
156
|
# puts data["key"]
|
|
@@ -149,6 +168,7 @@ unless defined?(Wreq)
|
|
|
149
168
|
# @raise [LocalJumpError] if called without a block
|
|
150
169
|
# @raise [Wreq::TimeoutError, Wreq::BodyError, Wreq::ConnectionResetError, Wreq::RequestError]
|
|
151
170
|
# if streaming fails while reading the response body
|
|
171
|
+
# @raise [Wreq::ForkError] if the response belongs to the parent process
|
|
152
172
|
# @example Save response to file
|
|
153
173
|
# File.open("output.bin", "wb") do |f|
|
|
154
174
|
# response.chunks { |chunk| f.write(chunk) }
|
|
@@ -165,10 +185,32 @@ unless defined?(Wreq)
|
|
|
165
185
|
# Close the response and free associated resources.
|
|
166
186
|
#
|
|
167
187
|
# @return [void]
|
|
188
|
+
# @raise [Wreq::ForkError] if the response belongs to the parent process
|
|
168
189
|
# @example
|
|
169
190
|
# response.close
|
|
170
191
|
def close
|
|
171
192
|
end
|
|
193
|
+
|
|
194
|
+
# Return TLS information captured for this response.
|
|
195
|
+
#
|
|
196
|
+
# Returns +nil+ when +tls_info: true+ was not enabled, the response used
|
|
197
|
+
# plain HTTP, or the transport supplied no TLS information. Reading or
|
|
198
|
+
# closing the response body does not discard captured TLS data.
|
|
199
|
+
#
|
|
200
|
+
# @return [Wreq::TlsInfo, nil] TLS information for this response, or +nil+
|
|
201
|
+
# when unavailable
|
|
202
|
+
# @raise [Wreq::ForkError] if the response belongs to the parent process
|
|
203
|
+
# @example
|
|
204
|
+
# client = Wreq::Client.new(tls_info: true)
|
|
205
|
+
# response = client.get("https://example.com")
|
|
206
|
+
# tls = response.tls_info
|
|
207
|
+
#
|
|
208
|
+
# if tls
|
|
209
|
+
# tls.peer_certificate # => DER-encoded binary String
|
|
210
|
+
# tls.peer_certificate_chain # => frozen Array of DER binary Strings
|
|
211
|
+
# end
|
|
212
|
+
def tls_info
|
|
213
|
+
end
|
|
172
214
|
end
|
|
173
215
|
end
|
|
174
216
|
end
|
|
@@ -180,6 +222,7 @@ module Wreq
|
|
|
180
222
|
# Returns the response body as a string.
|
|
181
223
|
#
|
|
182
224
|
# @return [String] Response body text
|
|
225
|
+
# @raise [Wreq::ForkError] if the response belongs to the parent process
|
|
183
226
|
# @example
|
|
184
227
|
# puts response.to_s
|
|
185
228
|
# puts response
|
|
@@ -193,6 +236,7 @@ module Wreq
|
|
|
193
236
|
# Format: #<Wreq::Response STATUS content-type="..." body=SIZE>
|
|
194
237
|
#
|
|
195
238
|
# @return [String] Compact formatted response information
|
|
239
|
+
# @raise [Wreq::ForkError] if the response belongs to the parent process
|
|
196
240
|
# @example
|
|
197
241
|
# p response
|
|
198
242
|
# # => #<Wreq::Response 200 content-type="application/json" body=456B>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
unless defined?(Wreq)
|
|
4
|
+
module Wreq
|
|
5
|
+
# Peer certificate data captured for one HTTPS response.
|
|
6
|
+
#
|
|
7
|
+
# Instances are returned by {Wreq::Response#tls_info}. Certificate bytes
|
|
8
|
+
# remain available after the response body is read or closed, even if the
|
|
9
|
+
# connection is later reused.
|
|
10
|
+
#
|
|
11
|
+
# The returned certificate Strings are Ruby-owned copies. Changing one does
|
|
12
|
+
# not alter the stored TLS data or values returned by later calls. The chain
|
|
13
|
+
# Array is frozen, but its String elements remain mutable.
|
|
14
|
+
#
|
|
15
|
+
# Certificates use the DER encoding described by the X.509 profile in
|
|
16
|
+
# RFC 5280.
|
|
17
|
+
#
|
|
18
|
+
# @example Parse the peer certificate with OpenSSL
|
|
19
|
+
# require "openssl"
|
|
20
|
+
#
|
|
21
|
+
# client = Wreq::Client.new(tls_info: true)
|
|
22
|
+
# response = client.get("https://example.com")
|
|
23
|
+
# der = response.tls_info&.peer_certificate
|
|
24
|
+
#
|
|
25
|
+
# if der
|
|
26
|
+
# certificate = OpenSSL::X509::Certificate.new(der)
|
|
27
|
+
# puts certificate.subject
|
|
28
|
+
# end
|
|
29
|
+
# @see https://www.rfc-editor.org/rfc/rfc5280#section-4.1 X.509 certificate format
|
|
30
|
+
class TlsInfo
|
|
31
|
+
# Return the peer's leaf certificate.
|
|
32
|
+
#
|
|
33
|
+
# @return [String, nil] a new DER-encoded String with
|
|
34
|
+
# +Encoding::BINARY+, or +nil+ when the transport did not provide one
|
|
35
|
+
def peer_certificate
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Return the peer certificate chain.
|
|
39
|
+
#
|
|
40
|
+
# The Array is frozen. Each element is a new DER-encoded binary String.
|
|
41
|
+
# The chain includes the leaf certificate when the transport supplies it.
|
|
42
|
+
#
|
|
43
|
+
# @return [Array<String>, nil] a frozen Array of certificate copies, or
|
|
44
|
+
# +nil+ when the transport did not provide a chain
|
|
45
|
+
def peer_certificate_chain
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# ======================== Ruby API Extensions ========================
|
|
52
|
+
|
|
53
|
+
module Wreq
|
|
54
|
+
class TlsInfo
|
|
55
|
+
# Return a compact summary for debugging.
|
|
56
|
+
#
|
|
57
|
+
# The summary reports the leaf certificate size and the number of
|
|
58
|
+
# certificates in the chain without printing the DER data.
|
|
59
|
+
#
|
|
60
|
+
# @return [String] TLS certificate metadata
|
|
61
|
+
# @example
|
|
62
|
+
# tls_info.inspect
|
|
63
|
+
# # => "#<Wreq::TlsInfo peer_certificate=781B peer_certificate_chain=1>"
|
|
64
|
+
def inspect
|
|
65
|
+
certificate = peer_certificate
|
|
66
|
+
chain = peer_certificate_chain
|
|
67
|
+
certificate_size = certificate ? "#{certificate.bytesize}B" : "nil"
|
|
68
|
+
chain_size = chain ? chain.length : "nil"
|
|
69
|
+
|
|
70
|
+
"#<#{self.class} peer_certificate=#{certificate_size} peer_certificate_chain=#{chain_size}>"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
data/src/arch.rs
CHANGED
|
@@ -6,6 +6,77 @@
|
|
|
6
6
|
//! do not leak into the rest of the binding.
|
|
7
7
|
#![allow(unsafe_code)]
|
|
8
8
|
|
|
9
|
+
use std::mem::ManuallyDrop;
|
|
10
|
+
|
|
11
|
+
use magnus::Ruby;
|
|
12
|
+
|
|
13
|
+
#[cfg(unix)]
|
|
14
|
+
use crate::error::fork_error;
|
|
15
|
+
|
|
16
|
+
/// Native state that belongs to the process where it was created.
|
|
17
|
+
///
|
|
18
|
+
/// A forked child must not destroy inherited clients, channels, or response
|
|
19
|
+
/// bodies because their synchronization state may belong to threads that no
|
|
20
|
+
/// longer exist. The child intentionally leaks the value and lets the operating
|
|
21
|
+
/// system reclaim it when the process exits.
|
|
22
|
+
///
|
|
23
|
+
/// `Send` and `Sync` only describe access between threads in one process. They
|
|
24
|
+
/// do not make a runtime, lock, channel, or connection pool safe after `fork`.
|
|
25
|
+
///
|
|
26
|
+
/// [`ProcessLocal::get`] is the only access path and checks the object's own
|
|
27
|
+
/// process generation before exposing its value.
|
|
28
|
+
pub(crate) struct ProcessLocal<T> {
|
|
29
|
+
value: ManuallyDrop<T>,
|
|
30
|
+
#[cfg(unix)]
|
|
31
|
+
owner: unix::ProcessToken,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
impl<T> ProcessLocal<T> {
|
|
35
|
+
/// Wrap native state created by the current process.
|
|
36
|
+
pub(crate) fn new(value: T) -> Self {
|
|
37
|
+
Self {
|
|
38
|
+
value: ManuallyDrop::new(value),
|
|
39
|
+
#[cfg(unix)]
|
|
40
|
+
owner: unix::ProcessToken::current(),
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// Borrow native state only from the process that created it.
|
|
45
|
+
///
|
|
46
|
+
/// # Errors
|
|
47
|
+
///
|
|
48
|
+
/// Returns `Wreq::ForkError` when the value was inherited from a parent
|
|
49
|
+
/// process.
|
|
50
|
+
#[inline]
|
|
51
|
+
pub(crate) fn get(&self, ruby: &Ruby) -> Result<&T, magnus::Error> {
|
|
52
|
+
#[cfg(unix)]
|
|
53
|
+
if let Some((owner_pid, current_pid)) = self.owner.forked_process_ids() {
|
|
54
|
+
return Err(fork_error(ruby, owner_pid, current_pid));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
#[cfg(not(unix))]
|
|
58
|
+
let _ = ruby;
|
|
59
|
+
|
|
60
|
+
Ok(&self.value)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
impl<T> Drop for ProcessLocal<T> {
|
|
65
|
+
fn drop(&mut self) {
|
|
66
|
+
#[cfg(unix)]
|
|
67
|
+
if self.owner.forked_process_ids().is_some() {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// SAFETY: `new` initializes the value exactly once, `ManuallyDrop`
|
|
72
|
+
// prevents an automatic second drop, and this wrapper's `Drop`
|
|
73
|
+
// implementation runs at most once.
|
|
74
|
+
unsafe {
|
|
75
|
+
ManuallyDrop::drop(&mut self.value);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
9
80
|
/// Whether the native client exposes TCP user-timeout configuration.
|
|
10
81
|
pub(crate) const SUPPORTS_TCP_USER_TIMEOUT: bool = cfg!(any(
|
|
11
82
|
target_os = "android",
|
|
@@ -27,6 +98,82 @@ pub(crate) const SUPPORTS_INTERFACE: bool = cfg!(any(
|
|
|
27
98
|
target_os = "watchos",
|
|
28
99
|
));
|
|
29
100
|
|
|
101
|
+
#[cfg(unix)]
|
|
102
|
+
mod unix {
|
|
103
|
+
use std::{io, process, sync::OnceLock};
|
|
104
|
+
|
|
105
|
+
/// Identity of the process generation that created native state.
|
|
106
|
+
///
|
|
107
|
+
/// Forkguard's child callback only advances an atomic generation counter.
|
|
108
|
+
/// The PID is retained for diagnostics and as a fallback if registering
|
|
109
|
+
/// the callback fails.
|
|
110
|
+
/// https://pubs.opengroup.org/onlinepubs/9799919799/functions/pthread_atfork.html
|
|
111
|
+
pub(super) struct ProcessToken {
|
|
112
|
+
detector: Option<forkguard::Guard>,
|
|
113
|
+
owner_pid: u32,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
impl ProcessToken {
|
|
117
|
+
/// Capture the current process and fork generation.
|
|
118
|
+
pub(super) fn current() -> Self {
|
|
119
|
+
Self::try_current().unwrap_or_else(|_| Self {
|
|
120
|
+
detector: None,
|
|
121
|
+
owner_pid: process::id(),
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// Capture the current process after registering fork detection.
|
|
126
|
+
fn try_current() -> io::Result<Self> {
|
|
127
|
+
forkguard::Guard::try_new()
|
|
128
|
+
.map(|detector| Self {
|
|
129
|
+
detector: Some(detector),
|
|
130
|
+
owner_pid: process::id(),
|
|
131
|
+
})
|
|
132
|
+
.map_err(|error| io::Error::from_raw_os_error(error.code().get()))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/// Return process IDs when this token was inherited through a fork.
|
|
136
|
+
pub(super) fn forked_process_ids(&self) -> Option<(u32, u32)> {
|
|
137
|
+
if let Some(detector) = &self.detector {
|
|
138
|
+
// Keep the stored generation unchanged so repeated accesses
|
|
139
|
+
// continue to reject the same inherited object. Cloning the
|
|
140
|
+
// detector copies one usize.
|
|
141
|
+
return detector
|
|
142
|
+
.clone()
|
|
143
|
+
.detected_fork()
|
|
144
|
+
.then(|| (self.owner_pid, process::id()));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let current_pid = process::id();
|
|
148
|
+
(self.owner_pid != current_pid).then_some((self.owner_pid, current_pid))
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/// Runtime owner captured when Tokio first initializes.
|
|
153
|
+
static RUNTIME_OWNER: OnceLock<ProcessToken> = OnceLock::new();
|
|
154
|
+
|
|
155
|
+
/// Register process fork tracking before the Tokio runtime is initialized.
|
|
156
|
+
pub(crate) fn initialize_fork_tracking() -> io::Result<()> {
|
|
157
|
+
if RUNTIME_OWNER.get().is_some() {
|
|
158
|
+
return Ok(());
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let owner = ProcessToken::try_current()?;
|
|
162
|
+
let _ = RUNTIME_OWNER.set(owner);
|
|
163
|
+
Ok(())
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/// Return process IDs when this process inherited an initialized runtime.
|
|
167
|
+
pub(crate) fn forked_process_ids() -> Option<(u32, u32)> {
|
|
168
|
+
RUNTIME_OWNER
|
|
169
|
+
.get()
|
|
170
|
+
.and_then(ProcessToken::forked_process_ids)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
#[cfg(unix)]
|
|
175
|
+
pub(crate) use unix::{forked_process_ids, initialize_fork_tracking};
|
|
176
|
+
|
|
30
177
|
#[cfg(all(target_os = "windows", target_env = "gnu"))]
|
|
31
178
|
mod windows_gnu {
|
|
32
179
|
//! Windows GNU support.
|
|
@@ -53,3 +200,30 @@ mod windows_gnu {
|
|
|
53
200
|
}
|
|
54
201
|
}
|
|
55
202
|
}
|
|
203
|
+
|
|
204
|
+
#[cfg(test)]
|
|
205
|
+
mod tests {
|
|
206
|
+
use std::cell::Cell;
|
|
207
|
+
|
|
208
|
+
use super::ProcessLocal;
|
|
209
|
+
|
|
210
|
+
struct DropCounter<'a>(&'a Cell<usize>);
|
|
211
|
+
|
|
212
|
+
impl Drop for DropCounter<'_> {
|
|
213
|
+
fn drop(&mut self) {
|
|
214
|
+
self.0.set(self.0.get() + 1);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
#[test]
|
|
219
|
+
fn process_local_drops_in_its_owner_process() {
|
|
220
|
+
let drops = Cell::new(0);
|
|
221
|
+
|
|
222
|
+
{
|
|
223
|
+
let value = ProcessLocal::new(DropCounter(&drops));
|
|
224
|
+
assert_eq!(value.value.0.get(), 0);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
assert_eq!(drops.get(), 1);
|
|
228
|
+
}
|
|
229
|
+
}
|
data/src/client/body/stream.rs
CHANGED
|
@@ -13,6 +13,7 @@ use magnus::{Error, Integer, RString, Ruby, Value, scan_args::scan_args};
|
|
|
13
13
|
use tokio::sync::{Mutex, Semaphore, mpsc};
|
|
14
14
|
|
|
15
15
|
use crate::{
|
|
16
|
+
arch::ProcessLocal,
|
|
16
17
|
error::{
|
|
17
18
|
argument_error, body_sender_borrow_error, body_sender_borrow_mut_error,
|
|
18
19
|
body_sender_send_error, closed_body_sender_error, memory_error, type_error, wreq_error,
|
|
@@ -33,7 +34,7 @@ pub struct BodyReceiver(Mutex<Pin<Box<dyn Stream<Item = wreq::Result<Bytes>> + S
|
|
|
33
34
|
/// receiver. Ruby's GVL protects state access; no [`RefCell`] borrow is kept
|
|
34
35
|
/// while request backpressure waits without the GVL.
|
|
35
36
|
#[magnus::wrap(class = "Wreq::BodySender", free_immediately, size)]
|
|
36
|
-
pub struct BodySender(RefCell<InnerBodySender
|
|
37
|
+
pub struct BodySender(ProcessLocal<RefCell<InnerBodySender>>);
|
|
37
38
|
|
|
38
39
|
/// Mutable ownership state for both halves of the body channel.
|
|
39
40
|
struct InnerBodySender {
|
|
@@ -64,17 +65,14 @@ impl BodyReceiver {
|
|
|
64
65
|
|
|
65
66
|
/// Read the next body chunk, converting stream errors into Ruby errors.
|
|
66
67
|
pub fn next(&self, ruby: &Ruby) -> Result<Option<Bytes>, Error> {
|
|
67
|
-
rt::
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
},
|
|
76
|
-
wreq_error,
|
|
77
|
-
)
|
|
68
|
+
rt::block_on(ruby, async {
|
|
69
|
+
match self.0.lock().await.as_mut().next().await {
|
|
70
|
+
Some(Ok(data)) => Ok(Some(data)),
|
|
71
|
+
Some(Err(err)) => Err(err),
|
|
72
|
+
None => Ok(None),
|
|
73
|
+
}
|
|
74
|
+
})?
|
|
75
|
+
.map_err(|err| wreq_error(ruby, err))
|
|
78
76
|
}
|
|
79
77
|
}
|
|
80
78
|
|
|
@@ -100,10 +98,12 @@ impl BodySender {
|
|
|
100
98
|
let (tx, rx) =
|
|
101
99
|
catch_unwind(|| mpsc::channel(capacity)).map_err(|_| invalid_capacity_error(ruby))?;
|
|
102
100
|
|
|
103
|
-
Ok(BodySender(RefCell::new(
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
101
|
+
Ok(BodySender(ProcessLocal::new(RefCell::new(
|
|
102
|
+
InnerBodySender {
|
|
103
|
+
tx: Some(tx),
|
|
104
|
+
rx: Some(rx),
|
|
105
|
+
},
|
|
106
|
+
))))
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
/// Push a binary chunk, waiting for capacity when the channel is full.
|
|
@@ -113,7 +113,8 @@ impl BodySender {
|
|
|
113
113
|
/// # Errors
|
|
114
114
|
///
|
|
115
115
|
/// Returns `IOError` after either channel side has closed. An interrupted
|
|
116
|
-
/// wait raises `Wreq::InterruptError`.
|
|
116
|
+
/// wait raises `Wreq::InterruptError`. Returns `Wreq::ForkError` before
|
|
117
|
+
/// reading an inherited channel.
|
|
117
118
|
pub fn push(ruby: &Ruby, rb_self: &Self, data: RString) -> Result<(), Error> {
|
|
118
119
|
// Clone during the shared borrow, then release it before waiting
|
|
119
120
|
// for capacity. Request attachment needs a mutable borrow.
|
|
@@ -122,7 +123,8 @@ impl BodySender {
|
|
|
122
123
|
_ => return Err(closed_body_sender_error(ruby)),
|
|
123
124
|
};
|
|
124
125
|
|
|
125
|
-
rt::
|
|
126
|
+
rt::block_on(ruby, tx.send(data.to_bytes()))?
|
|
127
|
+
.map_err(|err| body_sender_send_error(ruby, err))
|
|
126
128
|
}
|
|
127
129
|
|
|
128
130
|
/// Close the producing side while retaining the receiver and queued chunks.
|
|
@@ -131,7 +133,8 @@ impl BodySender {
|
|
|
131
133
|
///
|
|
132
134
|
/// # Errors
|
|
133
135
|
///
|
|
134
|
-
/// Returns `Wreq::
|
|
136
|
+
/// Returns `Wreq::ForkError` before reading an inherited channel, or
|
|
137
|
+
/// `Wreq::BodyError` if the internal state is already borrowed.
|
|
135
138
|
pub fn close(ruby: &Ruby, rb_self: &Self) -> Result<(), Error> {
|
|
136
139
|
let mut inner = rb_self.write_inner(ruby)?;
|
|
137
140
|
inner.tx.take();
|
|
@@ -142,21 +145,24 @@ impl BodySender {
|
|
|
142
145
|
///
|
|
143
146
|
/// # Errors
|
|
144
147
|
///
|
|
145
|
-
/// Returns `Wreq::
|
|
148
|
+
/// Returns `Wreq::ForkError` before reading an inherited channel, or
|
|
149
|
+
/// `Wreq::BodyError` if the internal state is already borrowed.
|
|
146
150
|
pub fn is_closed(ruby: &Ruby, rb_self: &Self) -> Result<bool, Error> {
|
|
147
151
|
rb_self.read_inner(ruby).map(|r| r.is_closed())
|
|
148
152
|
}
|
|
149
153
|
|
|
150
|
-
/// Borrow
|
|
154
|
+
/// Borrow channel state only in the process that created this sender.
|
|
151
155
|
fn read_inner(&self, ruby: &Ruby) -> Result<Ref<'_, InnerBodySender>, Error> {
|
|
152
156
|
self.0
|
|
157
|
+
.get(ruby)?
|
|
153
158
|
.try_borrow()
|
|
154
159
|
.map_err(|err| body_sender_borrow_error(ruby, err))
|
|
155
160
|
}
|
|
156
161
|
|
|
157
|
-
/// Mutably borrow
|
|
162
|
+
/// Mutably borrow channel state only in the process that created this sender.
|
|
158
163
|
fn write_inner(&self, ruby: &Ruby) -> Result<RefMut<'_, InnerBodySender>, Error> {
|
|
159
164
|
self.0
|
|
165
|
+
.get(ruby)?
|
|
160
166
|
.try_borrow_mut()
|
|
161
167
|
.map_err(|err| body_sender_borrow_mut_error(ruby, err))
|
|
162
168
|
}
|