wreq 1.2.12 → 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.
data/src/rt.rs CHANGED
@@ -17,29 +17,13 @@ use crate::{
17
17
  /// Initialize the global runtime lazily and preserve failures for Ruby.
18
18
  static RUNTIME: OnceLock<Result<TokioRuntime, io::Error>> = OnceLock::new();
19
19
 
20
- enum BlockOnError<E> {
21
- Interrupted,
22
- Future(E),
23
- }
24
-
25
- /// Register fork tracking while the native extension is being loaded.
20
+ /// Reject a child process that inherited an initialized native runtime.
26
21
  ///
27
22
  /// # Errors
28
23
  ///
29
- /// Returns `Wreq::ForkError` if the platform cannot install its child-process
30
- /// callback.
31
- #[cfg(unix)]
32
- pub fn initialize(ruby: &Ruby) -> Result<(), magnus::Error> {
33
- arch::initialize_fork_tracking().map_err(|err| fork_handler_error(ruby, &err))
34
- }
35
-
36
- /// Reject a child process that inherited the loaded native extension.
37
- ///
38
- /// # Errors
39
- ///
40
- /// Returns `Wreq::ForkError` when the extension was loaded before the current
41
- /// process was forked.
42
- pub fn ensure_current(ruby: &Ruby) -> Result<(), magnus::Error> {
24
+ /// Returns `Wreq::ForkError` when the global runtime belongs to the parent
25
+ /// process.
26
+ fn ensure_runtime_owner(ruby: &Ruby) -> Result<(), magnus::Error> {
43
27
  #[cfg(unix)]
44
28
  if let Some((owner_pid, current_pid)) = arch::forked_process_ids() {
45
29
  return Err(fork_error(ruby, owner_pid, current_pid));
@@ -54,21 +38,25 @@ pub fn ensure_current(ruby: &Ruby) -> Result<(), magnus::Error> {
54
38
  /// Block on a future to completion on the current process's global Tokio runtime.
55
39
  ///
56
40
  /// The future runs without Ruby's GVL, so it must not construct Ruby objects or
57
- /// Ruby exceptions. Convert Rust errors back into Ruby errors after the GVL has
58
- /// been reacquired.
41
+ /// Ruby exceptions. Its output is returned unchanged. If that output is a
42
+ /// `Result`, convert its error after this function returns and reacquires the
43
+ /// GVL.
59
44
  ///
60
45
  /// # Errors
61
46
  ///
62
- /// Returns `Wreq::ForkError` if the extension belongs to a parent process,
47
+ /// Returns `Wreq::ForkError` if the runtime belongs to a parent process,
63
48
  /// `Wreq::BuilderError` if the Tokio runtime cannot be initialized,
64
- /// `Wreq::InterruptError` if Ruby interrupts the request, or the error produced
65
- /// by `map_err` if the future fails.
66
- pub fn try_block_on<F, T, E, M>(ruby: &Ruby, future: F, map_err: M) -> Result<T, magnus::Error>
49
+ /// or `Wreq::InterruptError` if Ruby interrupts the operation.
50
+ pub(crate) fn block_on<F>(ruby: &Ruby, future: F) -> Result<F::Output, magnus::Error>
67
51
  where
68
- F: Future<Output = Result<T, E>>,
69
- M: FnOnce(&Ruby, E) -> magnus::Error,
52
+ F: Future,
70
53
  {
71
- ensure_current(ruby)?;
54
+ // Install fork tracking at the same point as the lazy runtime. Loading the
55
+ // extension alone must not claim the runtime for the parent process.
56
+ #[cfg(unix)]
57
+ arch::initialize_fork_tracking().map_err(|err| fork_handler_error(ruby, &err))?;
58
+
59
+ ensure_runtime_owner(ruby)?;
72
60
  let runtime = RUNTIME
73
61
  .get_or_init(|| {
74
62
  let mut builder = Builder::new_multi_thread();
@@ -80,15 +68,11 @@ where
80
68
  runtime.block_on(async move {
81
69
  tokio::select! {
82
70
  biased;
83
- _ = flag.cancelled() => Err(BlockOnError::Interrupted),
84
- result = future => result.map_err(BlockOnError::Future),
71
+ _ = flag.cancelled() => None,
72
+ result = future => Some(result),
85
73
  }
86
74
  })
87
75
  });
88
76
 
89
- match result {
90
- Ok(value) => Ok(value),
91
- Err(BlockOnError::Interrupted) => Err(interrupt_error(ruby)),
92
- Err(BlockOnError::Future(err)) => Err(map_err(ruby, err)),
93
- }
77
+ result.ok_or_else(|| interrupt_error(ruby))
94
78
  }
data/test/fork_test.rb CHANGED
@@ -7,15 +7,16 @@ require "timeout"
7
7
 
8
8
  class ForkTest < Minitest::Test
9
9
  FORK_ERROR_LABELS = %w[
10
- before_runtime
11
- invalid_client
12
- invalid_request
13
- fresh_body_sender
10
+ module_request
11
+ fresh_client_request
12
+ fresh_body_sender_push
14
13
  inherited_body_sender_push
15
14
  inherited_body_sender_close
16
15
  inherited_body_sender_closed
17
- fresh_client
18
16
  inherited_client
17
+ inherited_jar
18
+ inherited_cookie_provider
19
+ inherited_response_metadata
19
20
  inherited_response
20
21
  inherited_response_text
21
22
  inherited_response_chunks
@@ -26,13 +27,36 @@ class ForkTest < Minitest::Test
26
27
  assert_operator Wreq::ForkError, :<, RuntimeError
27
28
  end
28
29
 
29
- def test_loaded_extension_is_rejected_after_fork
30
+ def test_loaded_extension_can_initialize_runtime_after_fork
31
+ skip "fork is not supported on this platform" unless Process.respond_to?(:fork)
32
+
33
+ stdout, stderr, status = run_fork_script("prefork_runtime.rb")
34
+
35
+ assert status.success?, "subprocess failed with #{status.inspect}: #{stderr}"
36
+ assert_equal "ok\n", stdout
37
+ assert_match(/loaded_only=ok/, stderr)
38
+ assert_match(/before_runtime=ok/, stderr)
39
+ assert_match(/parent_after_children=ok/, stderr)
40
+ %w[
41
+ inherited_client_before_runtime
42
+ inherited_sender_before_runtime
43
+ inherited_jar_before_runtime
44
+ inherited_cookie_provider_before_runtime
45
+ ].each do |label|
46
+ assert_match(/#{label}=Wreq::ForkError:.*cannot be used after fork/, stderr)
47
+ end
48
+ refute_match(/\[BUG\]|segmentation fault|panicked/i, stderr)
49
+ end
50
+
51
+ def test_initialized_runtime_is_rejected_after_fork
30
52
  skip "fork is not supported on this platform" unless Process.respond_to?(:fork)
31
53
 
32
54
  stdout, stderr, status = run_fork_script("fork_safety.rb")
33
55
 
34
56
  assert status.success?, "subprocess failed with #{status.inspect}: #{stderr}"
35
57
  assert_equal "ok\n", stdout
58
+ assert_match(/non_runtime_construction=ok/, stderr)
59
+ assert_match(/inherited_snapshots=ok/, stderr)
36
60
  FORK_ERROR_LABELS.each do |label|
37
61
  assert_match(/#{label}=Wreq::ForkError:.*cannot be used after fork/, stderr)
38
62
  assert_match(/#{label}_retry=Wreq::ForkError:.*cannot be used after fork/, stderr)
@@ -9,36 +9,22 @@ $stdout.sync = true
9
9
  $stderr.sync = true
10
10
 
11
11
  def expect_fork_error(label)
12
- child_pid = fork do
13
- 2.times do |attempt|
14
- attempt_label = attempt.zero? ? label : "#{label}_retry"
15
-
16
- begin
17
- Timeout.timeout(5) { yield }
18
- rescue Wreq::ForkError => error
19
- warn "#{attempt_label}=#{error.class}: #{error.message}"
20
- next
21
- rescue => error
22
- warn "#{attempt_label}=unexpected #{error.class}: #{error.message}"
23
- exit! 2
24
- end
12
+ 2.times do |attempt|
13
+ attempt_label = attempt.zero? ? label : "#{label}_retry"
25
14
 
26
- warn "#{attempt_label}=missing Wreq::ForkError"
27
- exit! 3
15
+ begin
16
+ yield
17
+ rescue Wreq::ForkError => error
18
+ warn "#{attempt_label}=#{error.class}: #{error.message}"
19
+ next
20
+ rescue => error
21
+ abort "#{attempt_label}=unexpected #{error.class}: #{error.message}"
28
22
  end
29
23
 
30
- exit! 0
24
+ abort "#{attempt_label}=missing Wreq::ForkError"
31
25
  end
32
-
33
- _, status = Process.wait2(child_pid)
34
- abort "#{label} child failed with #{status.inspect}" unless status.success?
35
26
  end
36
27
 
37
- expect_fork_error("before_runtime") { Wreq::Client.new }
38
- expect_fork_error("invalid_client") { Wreq::Client.new(unknown: true) }
39
- expect_fork_error("invalid_request") { Wreq.get(1) }
40
- expect_fork_error("fresh_body_sender") { Wreq::BodySender.new(0) }
41
-
42
28
  server = TCPServer.new("127.0.0.1", 0)
43
29
  port = server.addr[1]
44
30
  server_pid = fork do
@@ -51,7 +37,7 @@ server_pid = fork do
51
37
  while (line = socket.gets)
52
38
  break if line == "\r\n"
53
39
  end
54
- socket.write("HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok")
40
+ socket.write("HTTP/1.1 200 OK\r\nX-Fork-Test: ok\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok")
55
41
  ensure
56
42
  socket.close
57
43
  end
@@ -63,29 +49,66 @@ end
63
49
  server.close
64
50
 
65
51
  url = "http://127.0.0.1:#{port}/"
52
+
53
+ # Start the server process before Tokio creates worker threads in the parent.
54
+ runtime_probe = Wreq::BodySender.new(1)
55
+ runtime_probe.push("warmup")
56
+
66
57
  client = Wreq::Client.new
67
58
  abort "parent warm-up failed" unless client.get(url).bytes == "ok"
68
59
 
69
- def build_inherited_objects(client, url)
70
- [Wreq::Client.new, Wreq::BodySender.new, client.get(url)]
60
+ inherited_objects = {
61
+ client: Wreq::Client.new,
62
+ sender: Wreq::BodySender.new,
63
+ response: client.get(url),
64
+ jar: Wreq::Jar.new
65
+ }
66
+ inherited_weak_refs = inherited_objects.values.map { |object| WeakRef.new(object) }
67
+ status_snapshot = inherited_objects[:response].status
68
+ headers_snapshot = inherited_objects[:response].headers
69
+
70
+ guard_pid = fork do
71
+ Timeout.timeout(10) do
72
+ jar = Wreq::Jar.new
73
+ jar.add("child=1; Path=/", url)
74
+ abort "fresh child jar failed" unless jar.get_all.one?
75
+
76
+ Wreq::Client.new(cookie_provider: jar)
77
+ Wreq::BodySender.new
78
+ warn "non_runtime_construction=ok"
79
+
80
+ abort "status snapshot changed" unless status_snapshot.to_i == 200
81
+ abort "headers snapshot changed" unless headers_snapshot["X-Fork-Test"] == "ok"
82
+ warn "inherited_snapshots=ok"
83
+
84
+ expect_fork_error("module_request") { Wreq.get(url) }
85
+ expect_fork_error("fresh_client_request") { Wreq::Client.new.get(url) }
86
+ expect_fork_error("fresh_body_sender_push") { Wreq::BodySender.new.push("chunk") }
87
+ expect_fork_error("inherited_body_sender_push") do
88
+ inherited_objects[:sender].push("chunk")
89
+ end
90
+ expect_fork_error("inherited_body_sender_close") { inherited_objects[:sender].close }
91
+ expect_fork_error("inherited_body_sender_closed") { inherited_objects[:sender].closed? }
92
+ expect_fork_error("inherited_client") { inherited_objects[:client].get(url) }
93
+ expect_fork_error("inherited_jar") { inherited_objects[:jar].get_all }
94
+ expect_fork_error("inherited_cookie_provider") do
95
+ Wreq::Client.new(cookie_provider: inherited_objects[:jar])
96
+ end
97
+ expect_fork_error("inherited_response_metadata") { inherited_objects[:response].status }
98
+ expect_fork_error("inherited_response") { inherited_objects[:response].bytes }
99
+ expect_fork_error("inherited_response_text") { inherited_objects[:response].text }
100
+ expect_fork_error("inherited_response_chunks") { inherited_objects[:response].chunks { nil } }
101
+ expect_fork_error("inherited_response_close") { inherited_objects[:response].close }
102
+ end
103
+ exit! 0
104
+ rescue => error
105
+ warn "guard_checks=unexpected #{error.class}: #{error.message}"
106
+ exit! 2
71
107
  end
108
+ _, guard_status = Process.wait2(guard_pid)
109
+ abort "guard checks child failed with #{guard_status.inspect}" unless guard_status.success?
72
110
 
73
- inherited_objects = build_inherited_objects(client, url)
74
- inherited_weak_refs = inherited_objects.map { |object| WeakRef.new(object) }
75
-
76
- expect_fork_error("inherited_body_sender_push") do
77
- inherited_objects[1].push("chunk")
78
- end
79
- expect_fork_error("inherited_body_sender_close") { inherited_objects[1].close }
80
- expect_fork_error("inherited_body_sender_closed") { inherited_objects[1].closed? }
81
- expect_fork_error("fresh_client") { Wreq::Client.new }
82
- expect_fork_error("inherited_client") { client.get(url) }
83
- expect_fork_error("inherited_response") { inherited_objects[2].bytes }
84
- expect_fork_error("inherited_response_text") { inherited_objects[2].text(1) }
85
- expect_fork_error("inherited_response_chunks") { inherited_objects[2].chunks }
86
- expect_fork_error("inherited_response_close") { inherited_objects[2].close }
87
-
88
- # Release the earlier test blocks so this array is the only strong reference.
111
+ # The hash is now the only strong reference to the inherited native objects.
89
112
  GC.start(full_mark: true, immediate_sweep: true)
90
113
  gc_pid = fork do
91
114
  inherited_objects = nil
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+ require "timeout"
5
+ require "wreq"
6
+
7
+ $stdout.sync = true
8
+ $stderr.sync = true
9
+
10
+ def expect_fork_error(label)
11
+ yield
12
+ abort "#{label}=missing Wreq::ForkError"
13
+ rescue Wreq::ForkError => error
14
+ warn "#{label}=#{error.class}: #{error.message}"
15
+ end
16
+
17
+ def run_child(label)
18
+ child_pid = fork do
19
+ Timeout.timeout(10) { yield }
20
+ warn "#{label}=ok"
21
+ exit! 0
22
+ rescue => error
23
+ warn "#{label}=unexpected #{error.class}: #{error.message}"
24
+ exit! 2
25
+ end
26
+
27
+ _, status = Process.wait2(child_pid)
28
+ abort "#{label} child failed with #{status.inspect}" unless status.success?
29
+ end
30
+
31
+ server = TCPServer.new("127.0.0.1", 0)
32
+ url = "http://127.0.0.1:#{server.addr[1]}/"
33
+ server_pid = fork do
34
+ 3.times do
35
+ ready = IO.select([server], nil, nil, 10)
36
+ exit! 4 unless ready
37
+
38
+ socket = server.accept
39
+ begin
40
+ while (line = socket.gets)
41
+ break if line == "\r\n"
42
+ end
43
+ socket.write("HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok")
44
+ ensure
45
+ socket.close
46
+ end
47
+ end
48
+ exit! 0
49
+ ensure
50
+ server.close
51
+ end
52
+ server.close
53
+
54
+ # Requiring the extension is the only parent-side Wreq operation before this fork.
55
+ run_child("loaded_only") do
56
+ abort "module request failed" unless Wreq.get(url).bytes == "ok"
57
+ end
58
+
59
+ inherited_client = Wreq::Client.new
60
+ inherited_sender = Wreq::BodySender.new
61
+ inherited_jar = Wreq::Jar.new
62
+
63
+ run_child("before_runtime") do
64
+ expect_fork_error("inherited_client_before_runtime") do
65
+ inherited_client.get(url)
66
+ end
67
+ expect_fork_error("inherited_sender_before_runtime") { inherited_sender.closed? }
68
+ expect_fork_error("inherited_jar_before_runtime") { inherited_jar.get_all }
69
+ expect_fork_error("inherited_cookie_provider_before_runtime") do
70
+ Wreq::Client.new(cookie_provider: inherited_jar)
71
+ end
72
+
73
+ sender = Wreq::BodySender.new
74
+ sender.push("child")
75
+
76
+ jar = Wreq::Jar.new
77
+ jar.add("child=1; Path=/", url)
78
+ abort "child jar failed" unless jar.get_all.one?
79
+
80
+ client = Wreq::Client.new(cookie_provider: jar)
81
+ abort "client request failed" unless client.get(url).bytes == "ok"
82
+ end
83
+
84
+ Timeout.timeout(10) do
85
+ abort "parent client failed" unless inherited_client.get(url).bytes == "ok"
86
+ inherited_sender.push("parent")
87
+ inherited_jar.add("parent=1; Path=/", url)
88
+ abort "parent jar failed" unless inherited_jar.get_all.one?
89
+ end
90
+ warn "parent_after_children=ok"
91
+
92
+ _, server_status = Process.wait2(server_pid)
93
+ abort "server failed with #{server_status.inspect}" unless server_status.success?
94
+
95
+ puts "ok"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wreq
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.12
4
+ version: 1.2.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - SearchApi
@@ -164,6 +164,7 @@ files:
164
164
  - test/request_test.rb
165
165
  - test/response_test.rb
166
166
  - test/scripts/fork_safety.rb
167
+ - test/scripts/prefork_runtime.rb
167
168
  - test/stream_test.rb
168
169
  - test/support/tls_server.rb
169
170
  - test/test_helper.rb