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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions