Skip to content

Silverfin-Engineering/open3_safe

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

open3_safe

Open3.capture3 with timeout and RSS memory limit protection.

Runs a subprocess and captures its stdout/stderr, with optional hard limits on wall-clock time and resident memory. A single monitor thread handles both constraints so there is no signal-send race between them. Whole process groups are tracked by default, so grandchild processes are included in RSS accounting and are killed on OOM/timeout.

Installation

Add to your Gemfile:

gem "open3_safe"

Or install directly:

gem install open3_safe

Usage

The interface mirrors Open3.capture3 — environment hash and spawn options are passed through to Process.spawn — with extra keys consumed before spawning.

Basic

result = Open3Safe.capture3_safe("echo", "hello")
result[:stdout]  # => "hello\n"
result[:status].success?  # => true

With timeout

result = Open3Safe.capture3_safe("sleep", "10", timeout: 3)
result[:timeout]  # => true
result[:status].signaled?  # => true

With RSS memory limit

result = Open3Safe.capture3_safe(
  "ruby", "-e", "Array.new(200_000_000, 0); sleep 10",
  max_rss: 100 * 1024 * 1024,  # 100 MB
  rss_check_interval: 0.5
)
result[:oom_killed]  # => true
result[:peak_rss]    # => integer, bytes

With both limits

result = Open3Safe.capture3_safe(
  "my_command", "--flag",
  timeout: 30,
  max_rss: 256 * 1024 * 1024,
  kill_after: 5,
  command_name: "my_command"
)

With stdin

# String
result = Open3Safe.capture3_safe("cat", stdin_data: "hello")

# IO object
IO.pipe do |r, w|
  w.write("hello")
  w.close
  result = Open3Safe.capture3_safe("cat", stdin_data: r)
end

Options

All options are passed as keyword arguments in the trailing hash, alongside any standard Process.spawn options.

Option Default Description
stdin_data "" String or IO to write to the command's stdin
timeout nil Seconds after which the signal is sent
signal :TERM Signal to send on timeout or OOM kill
kill_after nil Seconds to wait after the initial signal before sending SIGKILL
max_rss nil RSS byte threshold; kills the process if exceeded
rss_check_interval 2 Seconds between RSS polls (only used when max_rss is set)
command_name inferred Label used in callbacks; defaults to the command's basename
pgroup true Spawn in a new process group; RSS is summed across all processes in the group and signals target the whole group

Any other keys (e.g. :chdir, :env, unsetenv_others) are forwarded to Process.spawn.

Return value

capture3_safe returns a Hash:

Key Type Description
pid Integer PID of the spawned process
status Process::Status Exit status
stdout String Captured standard output
stderr String Captured standard error
timeout Boolean true if killed due to timeout
oom_killed Boolean true if killed due to RSS exceeding max_rss
peak_rss Integer or nil Peak RSS in bytes observed during execution; nil when max_rss is not set
duration Float Wall-clock seconds the command ran for

Observability callbacks

Configure callbacks to hook into kills and completions, e.g. for metrics or logging:

Open3Safe.configure do |c|
  c.on_timeout_kill = ->(command_name, pid, timeout) {
    MyMetrics.increment("open3_safe.timeout", tags: ["command:#{command_name}"])
  }

  c.on_rss_kill = ->(command_name, pid, rss, max_rss) {
    MyLogger.warn("OOM kill", command: command_name, rss: rss, limit: max_rss)
  }

  c.on_finish = ->(command_name, result) {
    MyMetrics.timing("open3_safe.duration", result[:duration], tags: ["command:#{command_name}"])
  }
end
Callback Arguments
on_timeout_kill command_name, pid, timeout
on_rss_kill command_name, pid, rss, max_rss
on_finish command_name, result — the full result hash

Requirements

License

MIT — see LICENSE.

About

Open3Safe gem for safe external process execution with timeout and RSS monitoring

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages