Skip to content

Make the native runtime safe across Process.fork #112

Description

@ZilvinasKucinskas

Priority: P0

Related API contract: Provide a stable error hierarchy and safe, class-specific metadata

Describe the issue

After wreq-ruby has executed a request in a parent process, using it after Process.fork is unsafe. With wreq 1.2.4 on Ruby 3.3.11 (arm64-darwin25):

  • an inherited Wreq::Client failed in the child with Wreq::ConnectionError; and
  • constructing a fresh Wreq::Client in the child produced a native segmentation fault.

This matters to ordinary Ruby deployments. Prefork servers load application code in a master process and then fork workers. A native extension may decide that inherited clients cannot be reused, but it must detect that state and recover or raise a Ruby exception; it must never crash the process.

Reproduction

The following uses a local socket only. The first request initializes wreq-ruby's global Tokio runtime before the second fork:

require "socket"
require "wreq"

server = TCPServer.new("127.0.0.1", 0)
port = server.addr[1]

server_pid = fork do
  socket = server.accept
  loop { break if socket.gets == "\r\n" }
  socket.write("HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok")
  socket.close
  exit! 0
end
server.close

Wreq::Client.new.get("http://127.0.0.1:#{port}/").bytes
Process.wait(server_pid)

child_pid = fork do
  Wreq::Client.new
  exit! 0
end

_, status = Process.wait2(child_pid)
p status.signaled?
p status.termsig

Observed on the platform above:

[BUG] Segmentation fault
true
11

In a separate child, reusing the client that performed the warm-up request raised Wreq::ConnectionError instead of completing a local request.

Required behavior

  • Detect when the current PID differs from the PID that owns the native runtime or client pool.
  • Never reuse parent sockets, executors, tasks, or synchronization state in a child.
  • A freshly constructed client must work in the child after the parent has used wreq-ruby.
  • For an inherited client, either rebuild process-local resources lazily or raise a documented Ruby exception such as Wreq::ForkError < Wreq::Error.
  • If transparent reinitialization is not feasible, expose an idempotent Wreq.after_fork! hook and document it for prefork servers. Automatic PID detection is preferable because Process.fork, IO.popen("-"), and server-specific hooks can all create children.
  • Parent-process clients must continue to work after a child exits.

The exact implementation is intentionally not prescribed. The observable contract is that a PID change cannot lead to undefined behavior or a native abort.

Acceptance criteria

  • No fork-related path can panic, abort, hang indefinitely, or segfault Ruby.
  • A child can construct a new client and complete a local request after the parent has completed one.
  • Inherited clients have explicit, tested semantics: safe reinitialization or a typed Ruby error.
  • Runtime and connection-pool state is process-local after a fork.
  • Tests run the scenario in subprocesses on platforms that support fork and assert both exit status and request behavior.
  • Documentation includes a Puma/Unicorn-style prefork example if an explicit hook is required.

Ruby ecosystem precedent

  • Puma cluster mode forks worker processes and provides before_fork/before_worker_boot hooks specifically for process-local resource management.
  • Ruby documents Process._fork as the common internal path used by Kernel#fork, Process.fork, and IO.popen("-"), and as a hook point for libraries that must observe fork events.

Relevant wreq-ruby source

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions