Project

General

Profile

Actions

Feature #22093

open

Introduce `Process::ID` for process IDs returned by `Process.spawn` and `fork`

Feature #22093: Introduce `Process::ID` for process IDs returned by `Process.spawn` and `fork`
2

Added by nobu (Nobuyoshi Nakada) 3 days ago. Updated 3 days ago.

Status:
Open
Assignee:
-
Target version:
-
[ruby-core:125624]

Description

Currently, process IDs returned by Process.spawn and fork are plain Integer objects. This works, but it makes common process-handling code slightly verbose and loses the opportunity to attach process-specific behavior to the returned value.

For example:

pid = Process.spawn(...)
_, status = Process.wait2(pid)
if status.success?
  ...
end

I propose introducing Process::ID, an Integer-like object representing a system process ID. Process.spawn and parent-side Process.fork would return Process::ID. Process::ID would provide convenience methods such as #wait and #detach.

This allows process lifecycle code to be written more directly:

pid = Process.spawn(...)
status = pid.wait
if status.success?
  ...
end

and, when the parent does not intend to wait explicitly:

pid = Process.spawn(...)
pid.detach

Proposal

Add Process::ID.

Process::ID would:

  • represent a process ID
  • provide #to_i and #to_int
  • provide #to_s returning the decimal PID string
  • provide #inspect, like as Process::Status
  • provide #pid as a reader for the integer PID
  • provide #wait, returning Process::Status
  • provide #detach, returning Process::Waiter
  • support comparison with the underlying integer PID

Process.spawn would return a Process::ID instead of an Integer.

On platforms with fork, parent-side fork and Process.fork would return a Process::ID. The child side would keep the current behavior: nil for fork and 0 for Process._fork.

IO#pid for IO.popen should also return the associated Process::ID when applicable.

Rationale

A process ID is currently represented by an Integer, but conceptually it is not just a number. It is a handle-like value used with process APIs such as wait, waitpid, kill, and detach.

Providing a dedicated object makes common operations more discoverable and concise, while keeping compatibility through #to_i/#to_int.

This is similar in spirit to Process::Status: process-related values can still expose primitive information, but the object itself can provide process-specific operations.

#wait is useful when the parent wants to wait for the child and obtain its Process::Status:

pid = Process.spawn("ruby", "-e", "exit 0")
pid.wait.success? #=> true

#detach is useful when the parent intentionally does not want to wait for the child directly, but still wants to avoid leaving a zombie process:

pid = Process.spawn("ruby", "-e", "sleep 1")
waiter = pid.detach
waiter.value #=> #<Process::Status: pid ... exit 0>

This is preferable to tying process cleanup to object finalization. A dfree/GC-based wait would make process reaping depend on GC timing and could unexpectedly consume the child status before explicit Process.wait code gets to it. #detach keeps the ownership transfer explicit.

Compatibility

This is a visible compatibility change because Process.spawn(...).class would become Process::ID instead of Integer.

However, code that passes the PID to existing process APIs should continue to work because Process::ID provides #to_int.

Potentially affected code is code that checks the exact class of the returned value, such as pid.instance_of?(Integer), or uses type checks such as Integer === pid. Such code would need to treat the value as integer-like instead, for example by using pid.to_i when an actual Integer object is needed.

For Process._fork, if it is overridden, fork should preserve the object returned by the overridden _fork on the parent side, as long as it is Integer-like. This keeps custom hooks compatible with code that returns PID wrapper objects.

Examples

pid = Process.spawn("ruby", "-e", "exit 0")
pid.class      #=> Process::ID
pid.to_i       #=> 12345
pid.to_s       #=> "12345"
pid.wait       #=> #<Process::Status: pid 12345 exit 0>
pid = fork { exit! true }
pid.class           #=> Process::ID
pid.wait.success?   #=> true
pid = Process.spawn("ruby", "-e", "sleep 1")
thread = pid.detach #=> #<Process::Waiter:...>
thread.value        #=> #<Process::Status: pid ... exit 0>

Open questions

  • Should Process::ID include Comparable, or only define <=>?
    <=> is useful because PIDs have historically been Integer objects and are sometimes sorted simply to get a deterministic order, for example in Process.waitall results. The numeric ordering itself has no process-specific meaning, but preserving sortability is convenient.

  • Should Process::ID#wait accept the same integer flags as existing wait APIs, provide keyword arguments, or support both?
    Integer flags such as Process::WNOHANG are consistent with Process.waitpid, while keywords such as nohang: true, untraced: true, and continued: true may be more readable for a new convenience method.

  • Should Process::ID#detach simply be equivalent to Process.detach(self)?

  • Are there other process-related APIs that should return or preserve Process::ID?

Updated by byroot (Jean Boussier) 3 days ago Actions #1 [ruby-core:125626]

I'm very much in favor of such API as a much more OO and friendly API than passing numeric PIDs around.

Updated by alanwu (Alan Wu) 3 days ago Actions #2 [ruby-core:125629]

Nice, and theoretically this enables using pidfd_open(2) underneath the abstraction to deal with pid recycling race conditions. (Whether that's a good idea is off topic.)

Updated by kaiquekandykoga (Kaíque Kandy Koga) 3 days ago Actions #3 [ruby-core:125630]

I like that!

Just adding an idea:

process_id = Process.spawn("ruby", "-e", "exit 0")
pid = process_id.pid
process_id2 = Process::ID(pid)

process_id and process_id2 are both Process::ID instances pointing to the same process. This can be handy if I need to persist the PID temporarily and later re‑initialise a fresh Process::ID for it.

Actions

Also available in: PDF Atom