Class: Heatwave::BoundedSubprocess

Inherits:
Object
  • Object
show all
Defined in:
app/lib/heatwave/bounded_subprocess.rb

Overview

Captures a subprocess under wall-clock, CPU, and address-space limits.

The stdout and stderr pipes drain concurrently so a noisy child cannot
deadlock while the parent waits. A timed-out child receives TERM, then
KILL if necessary, and is always reaped before the timeout propagates.

Defined Under Namespace

Classes: Error, MissingExecutable, TimeoutError

Constant Summary collapse

DEFAULT_TIMEOUT =

Default wall-clock deadline in seconds.

10
DEFAULT_RLIMIT_CPU =

Default child CPU limit in seconds.

10
DEFAULT_RLIMIT_AS =

Default child address-space limit (1 GiB).

1 << 30

Class Method Summary collapse

Class Method Details

.capture(*command, timeout: DEFAULT_TIMEOUT, rlimit_cpu: DEFAULT_RLIMIT_CPU, rlimit_as: DEFAULT_RLIMIT_AS) ⇒ Array(String, String, Process::Status)

Capture stdout, stderr, and exit status for an argv command.

Parameters:

  • command (Array<String>)

    executable followed by argv

  • timeout (Numeric) (defaults to: DEFAULT_TIMEOUT)

    wall-clock deadline in seconds

  • rlimit_cpu (Integer, nil) (defaults to: DEFAULT_RLIMIT_CPU)

    child CPU limit in seconds

  • rlimit_as (Integer, nil) (defaults to: DEFAULT_RLIMIT_AS)

    child address-space limit in bytes

Returns:

  • (Array(String, String, Process::Status))

    stdout, stderr, status

Raises:

  • (ArgumentError)

    when no command is provided

  • (TimeoutError)

    after terminating and reaping a timed-out child

  • (MissingExecutable)

    when the executable cannot be found



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'app/lib/heatwave/bounded_subprocess.rb', line 39

def capture(*command, timeout: DEFAULT_TIMEOUT, rlimit_cpu: DEFAULT_RLIMIT_CPU, rlimit_as: DEFAULT_RLIMIT_AS)
  raise ArgumentError, 'command is required' if command.empty?

  limits = { rlimit_cpu:, rlimit_as: }.compact
  begin
    capture_with_limits(command, timeout:, limits:)
  rescue Errno::EINVAL
    # macOS rejects finite RLIMIT_AS at spawn time. Retry with the CPU
    # cap alone; a second EINVAL is unrelated and must propagate.
    raise unless limits.delete(:rlimit_as)

    retry
  end
rescue Errno::ENOENT
  raise MissingExecutable, "#{command.first} binary not found"
end