Project

General

Profile

Actions

Feature #22093

closed

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) 10 days ago. Updated 3 days ago.

Status:
Rejected
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) 9 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) 9 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) 9 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.

Updated by matz (Yukihiro Matsumoto) 4 days ago Actions #4 [ruby-core:125689]

I see the appeal of pid.wait and pid.detach, but I'm not convinced.

A PID is just a number, not a handle in the OS sense, so Process::ID would look handle-like without the safety a real handle (e.g. pidfd) would give. Unlike Process::Status, it would carry only a single integer and exist just to attach methods.

PIDs are also returned by many other APIs (Process.pid, ppid, waitpid, PID files, etc.). Changing only spawn/fork/IO#pid is inconsistent, and changing all of them broadens the compatibility impact.

For the modest ergonomic gain, I'd rather keep PIDs as Integer for now.

Matz.

Updated by nobu (Nobuyoshi Nakada) 3 days ago Actions #5

  • Status changed from Open to Rejected
Actions

Also available in: PDF Atom