Skip to content

Commit

Permalink
Allow workers to timeout.
Browse files Browse the repository at this point in the history
  • Loading branch information
tgxworld committed Apr 17, 2017
1 parent dc0d44e commit e2e87bc
Show file tree
Hide file tree
Showing 6 changed files with 127 additions and 1 deletion.
4 changes: 4 additions & 0 deletions lib/image_optim.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ class ImageOptim
# Cache worker digests
attr_reader :cache_worker_digests

# Timeout
attr_reader :timeout

# Initialize workers, specify options using worker underscored name:
#
# pass false to disable worker
Expand Down Expand Up @@ -76,6 +79,7 @@ def initialize(options = {})
allow_lossy
cache_dir
cache_worker_digests
timeout
].each do |name|
instance_variable_set(:"@#{name}", config.send(name))
$stderr << "#{name}: #{send(name)}\n" if verbose
Expand Down
64 changes: 64 additions & 0 deletions lib/image_optim/cmd.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require 'English'
require 'timeout'

class ImageOptim
# Helper for running commands
Expand All @@ -15,6 +16,32 @@ def run(*args)
success
end

# Run commands using `Process.spawn`
# Return success status
# Will raise Timeout::Error when command timeouts
def run_with_timeout(timeout, *args)
success = false

if timeout > 0
pid = spawn_process(*args)

begin
Timeout.timeout(timeout) do
Process.wait(pid, 0)
check_status!
success = $CHILD_STATUS.exitstatus.zero?
end
rescue Timeout::Error => e
cleanup_process(pid)
raise e
end
else
success = run(*args)
end

success
end

# Run using backtick
# Return captured output
# Will raise SignalException if process was interrupted
Expand Down Expand Up @@ -44,6 +71,43 @@ def check_status!

fail SignalException, status.termsig
end

def cleanup_process(pid)
Thread.new do
Process.kill('-TERM', pid)
Process.detach(pid)

begin
Timeout.timeout(10) do
begin
Process.getpgid(pid)
rescue Errno::ESRCH
sleep 0.001
retry
end
end
rescue Timeout::Error
Process.kill('-KILL', pid)
end
end
end

def spawn_process(*args)
pgroup_opt = Gem.win_platform? ? :new_pgroup : :pgroup

if args.last.is_a?(Hash)
args.last[pgroup_opt] = true
else
args.push(pgroup_opt => true)
end

if Process.respond_to?(:spawn)
Process.spawn(*args)
else
args.pop if RUBY_VERSION < '1.9'
Process.fork{ exec(*args) }
end
end
end
end
end
5 changes: 5 additions & 0 deletions lib/image_optim/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,11 @@ def cache_worker_digests
!!get!(:cache_worker_digests)
end

def timeout
timeout = get!(:timeout)
timeout ? timeout.to_i : 0
end

# Options for worker class by its `bin_sym`:
# * `Hash` passed as is
# * `{}` for `true` or `nil`
Expand Down
13 changes: 12 additions & 1 deletion lib/image_optim/worker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ class ImageOptim
class Worker
extend ClassMethods

class Timeout < Timeout::Error; end

class << self
# Default init for worker is new
# Check example of override in gifsicle worker
Expand Down Expand Up @@ -151,7 +153,16 @@ def run_command(cmd_args)
{:out => Path::NULL, :err => Path::NULL},
].flatten
end
Cmd.run(*args)

if @image_optim.timeout > 0
begin
Cmd.run_with_timeout(@image_optim.timeout, *args)
rescue ::Timeout::Error
raise ImageOptim::Worker::Timeout
end
else
Cmd.run(*args)
end
end
end
end
26 changes: 26 additions & 0 deletions spec/image_optim/cmd_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,32 @@ def expect_int_exception(&block)
end
end

describe '.run_with_timeout' do
it 'calls spawn and returns status' do
expect(Cmd.run_with_timeout(20, 'sh -c "exit 0"')).to eq(true)
expect($CHILD_STATUS.exitstatus).to eq(0)

[1, 66].each do |status|
expect(Cmd.run_with_timeout(20, "sh -c \"exit #{status}\"")).
to eq(false)

expect($CHILD_STATUS.exitstatus).to eq(status)
end
end

it 'raises Timeout::Error if process timeouts' do
expect{ Cmd.run_with_timeout(0.001, 'sleep 1') }.
to raise_error(Timeout::Error)
end

it 'calls system if timeout is <= zero' do
expect(Cmd.run_with_timeout(0, 'sh -c "sleep 0.001; exit 0"')).to eq(true)

expect(Cmd.run_with_timeout(-1, 'sh -c "sleep 0.001; exit 1"')).
to eq(false)
end
end

describe '.capture' do
it 'calls ` and returns result' do
output = double
Expand Down
16 changes: 16 additions & 0 deletions spec/image_optim/config_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,22 @@
end
end

describe '#timeout' do
before do
allow(IOConfig).to receive(:read_options).and_return({})
end

it 'is 0 by default' do
config = IOConfig.new({})
expect(config.timeout).to eq(0)
end

it 'converts value to number' do
config = IOConfig.new(:timeout => '15')
expect(config.timeout).to eq(15)
end
end

describe '#for_worker' do
before do
allow(IOConfig).to receive(:read_options).and_return({})
Expand Down

0 comments on commit e2e87bc

Please sign in to comment.