angael 0.0.2 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/angael.gemspec +1 -1
- data/lib/angael/manager.rb +28 -0
- data/lib/angael/version.rb +1 -1
- data/lib/angael/worker.rb +7 -14
- data/spec/lib/angael/manager_spec.rb +83 -1
- data/spec/lib/angael/worker_spec.rb +26 -29
- data/spec/support/io_helpers.rb +14 -0
- metadata +4 -3
data/angael.gemspec
CHANGED
data/lib/angael/manager.rb
CHANGED
@@ -43,6 +43,20 @@ module Angael
|
|
43
43
|
def start!
|
44
44
|
workers.each { |w| w.start! }
|
45
45
|
|
46
|
+
trap("CHLD") do
|
47
|
+
workers.each do |w|
|
48
|
+
result = wait(w.pid)
|
49
|
+
#print w.pid.to_s
|
50
|
+
#print "\t"
|
51
|
+
#p result
|
52
|
+
if result
|
53
|
+
# worker terminated
|
54
|
+
# Restart it unless we asked it to stop.
|
55
|
+
w.restart! unless w.stopping?
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
46
60
|
loop do
|
47
61
|
trap("INT") do
|
48
62
|
stop!
|
@@ -74,5 +88,19 @@ module Angael
|
|
74
88
|
@logger.add(@log_level, "#{Time.now.utc} - #{self.class} (pid #{$$}): #{msg}") if @logger
|
75
89
|
end
|
76
90
|
|
91
|
+
# Returns immediately. If the process is still running, it returns nil.
|
92
|
+
# If the process is a zombie, it returns an array with the pid as the
|
93
|
+
# first element and a Process::Status object as the 2nd element, i.e.
|
94
|
+
# it returns the same thing as Process.wait2. If the process does not
|
95
|
+
# exist (i.e. it is completely gone) then it returns an array with the
|
96
|
+
# pid as the first element and nil as the 2nd element (because there
|
97
|
+
# is no Process::Status object to return).
|
98
|
+
def wait(pid)
|
99
|
+
begin
|
100
|
+
Process.wait2(pid, Process::WNOHANG)
|
101
|
+
rescue Errno::ECHILD
|
102
|
+
[pid, nil] # It did exit, but we don't know the exit status.
|
103
|
+
end
|
104
|
+
end
|
77
105
|
end
|
78
106
|
end
|
data/lib/angael/version.rb
CHANGED
data/lib/angael/worker.rb
CHANGED
@@ -24,17 +24,7 @@ module Angael
|
|
24
24
|
# Loops forever, taking jobs off the queue. SIGINT will stop it after
|
25
25
|
# allowing any jobs already taken from the queue to be processed.
|
26
26
|
def start!
|
27
|
-
|
28
|
-
__log("trapped SIGCHLD. Child PID #{pid}.")
|
29
|
-
|
30
|
-
# @stopping is set by #stop!. If it is true, then the child process was
|
31
|
-
# expected to die. If it is false/nil, then this is unexpected.
|
32
|
-
__log("Child process died unexpectedly") unless @stopping
|
33
|
-
# Reap the child process so that #started? will return false. But we can't
|
34
|
-
# block because this may be called for a Worker when a different Worker's
|
35
|
-
# child is the process that died.
|
36
|
-
wait_for_child(:dont_block => true) if pid
|
37
|
-
end
|
27
|
+
@stopping = false
|
38
28
|
|
39
29
|
@pid = fork_child do
|
40
30
|
__log("Started")
|
@@ -71,8 +61,8 @@ module Angael
|
|
71
61
|
return false
|
72
62
|
end
|
73
63
|
|
74
|
-
#
|
75
|
-
#
|
64
|
+
# This informs the Manager (through #stopping?) that we intentionally
|
65
|
+
# stopped the child process.
|
76
66
|
@stopping = true
|
77
67
|
|
78
68
|
begin
|
@@ -98,7 +88,6 @@ module Angael
|
|
98
88
|
end
|
99
89
|
end
|
100
90
|
end
|
101
|
-
@stopping = false
|
102
91
|
end
|
103
92
|
|
104
93
|
def started?
|
@@ -107,6 +96,10 @@ module Angael
|
|
107
96
|
def stopped?
|
108
97
|
!started?
|
109
98
|
end
|
99
|
+
# TODO: test this
|
100
|
+
def stopping?
|
101
|
+
@stopping
|
102
|
+
end
|
110
103
|
|
111
104
|
#########
|
112
105
|
private #
|
@@ -20,7 +20,8 @@ describe Angael::Manager do
|
|
20
20
|
end
|
21
21
|
|
22
22
|
describe "#start!" do
|
23
|
-
subject { Angael::Manager.new(Angael::TestSupport::SampleWorker,
|
23
|
+
subject { Angael::Manager.new(Angael::TestSupport::SampleWorker, 3) }
|
24
|
+
|
24
25
|
it "should start all its workers" do
|
25
26
|
subject.workers.each do |w|
|
26
27
|
w.should_receive_in_child_process(:start!)
|
@@ -38,6 +39,87 @@ describe Angael::Manager do
|
|
38
39
|
Process.wait(pid)
|
39
40
|
end
|
40
41
|
|
42
|
+
context "when it receives a SIGCHLD" do
|
43
|
+
after(:each) do
|
44
|
+
# Clean up
|
45
|
+
unless Process.wait2(@pid, Process::WNOHANG)
|
46
|
+
Process.kill('KILL', @pid) unless
|
47
|
+
Process.wait(@pid) rescue nil
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
context "when worker was asked to stop" do
|
52
|
+
it "should not restart the child process" do
|
53
|
+
subject.workers.each do |w|
|
54
|
+
w.stub(:work).and_return { sleep 0.1 }
|
55
|
+
w.should_receive_in_child_process(:restart!).exactly(0).times
|
56
|
+
end
|
57
|
+
|
58
|
+
@pid = Process.fork do
|
59
|
+
subject.start!
|
60
|
+
end
|
61
|
+
|
62
|
+
sleep 0.1 # Give the process a chance to start.
|
63
|
+
# This sends stop! to all the workers.
|
64
|
+
Process.kill('INT', @pid)
|
65
|
+
sleep 0.1 # Give the TempFile a chance to flush
|
66
|
+
end
|
67
|
+
|
68
|
+
it "should reap the child processes" do
|
69
|
+
subject.workers.each do |w|
|
70
|
+
w.stub(:work).and_return { sleep 0.05 }
|
71
|
+
end
|
72
|
+
|
73
|
+
# We need access to the worker objects to get their PIDs, so we
|
74
|
+
# fork a process which will send SIGINT to this current process.
|
75
|
+
# Then we start the Manager in this process and wait for it to
|
76
|
+
# get the SIGINT. Finally we rescue SystemExit so that this
|
77
|
+
# process doesn't exit with the Manager stops.
|
78
|
+
current_pid = $$
|
79
|
+
@pid = Process.fork do
|
80
|
+
sleep 0.1 # Give the process a chance to start.
|
81
|
+
Process.kill('INT', current_pid)
|
82
|
+
exit 0
|
83
|
+
end
|
84
|
+
begin
|
85
|
+
subject.start!
|
86
|
+
rescue SystemExit
|
87
|
+
nil
|
88
|
+
end
|
89
|
+
|
90
|
+
subject.workers.each do |w|
|
91
|
+
lambda do
|
92
|
+
Process.kill(0, w.pid)
|
93
|
+
end.should raise_error(Errno::ESRCH, "No such process")
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
context "when worker was not asked to stop" do
|
99
|
+
after(:each) do
|
100
|
+
# Clean up
|
101
|
+
Process.kill('INT', @pid)
|
102
|
+
sleep 0.1
|
103
|
+
end
|
104
|
+
it "should restart the child process" do
|
105
|
+
subject.workers.each do |w|
|
106
|
+
w.stub(:work).and_return do
|
107
|
+
sleep 0.05
|
108
|
+
# This is like exiting with an exception, but it prevents the ugly
|
109
|
+
# stacktrace.
|
110
|
+
exit 1
|
111
|
+
end
|
112
|
+
w.should_receive_in_child_process(:restart!).at_least(1).times
|
113
|
+
end
|
114
|
+
|
115
|
+
@pid = Process.fork do
|
116
|
+
subject.start!
|
117
|
+
end
|
118
|
+
|
119
|
+
sleep 0.1 # Give the process a chance to start.
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
41
123
|
|
42
124
|
%w(INT TERM).each do |sig|
|
43
125
|
context "when it receives a SIG#{sig}" do
|
@@ -26,6 +26,11 @@ describe Angael::Worker do
|
|
26
26
|
subject.should be_started
|
27
27
|
end
|
28
28
|
|
29
|
+
it "should not be stopping" do
|
30
|
+
subject.start!
|
31
|
+
subject.should_not be_stopping
|
32
|
+
end
|
33
|
+
|
29
34
|
it "should be doing work" do
|
30
35
|
# TODO: Update #should_receive_in_child_process to allow for a
|
31
36
|
# #more_than_once option, then use that instead of setting this all
|
@@ -63,6 +68,9 @@ describe Angael::Worker do
|
|
63
68
|
it "should not be started" do
|
64
69
|
Process.kill('KILL', subject.pid)
|
65
70
|
sleep 0.1 # Wait for SIGKILL to take effect.
|
71
|
+
# We must clean up the zombies here because the Worker class noramlly
|
72
|
+
# relies on the worker manager to do that.
|
73
|
+
Process.wait2(subject.pid, Process::WNOHANG)
|
66
74
|
subject.should_not be_started
|
67
75
|
end
|
68
76
|
|
@@ -81,26 +89,37 @@ describe Angael::Worker do
|
|
81
89
|
before { subject.stub(:work => nil) }
|
82
90
|
after { subject.stop! }
|
83
91
|
|
84
|
-
context "
|
92
|
+
context "when stopped" do
|
85
93
|
it "should return false" do
|
86
94
|
subject.should_not be_started
|
87
95
|
subject.stop!.should be_false
|
88
96
|
end
|
89
97
|
|
98
|
+
it "should not be stopping" do
|
99
|
+
subject.stop!
|
100
|
+
subject.should_not be_stopping
|
101
|
+
end
|
102
|
+
|
90
103
|
it "should not send a SIGINT to the child process" do
|
91
104
|
should_not_receive_and_run(Process, :kill, 'INT', subject.pid)
|
92
105
|
subject.stop!
|
93
106
|
end
|
94
107
|
end
|
95
108
|
|
96
|
-
context "
|
109
|
+
context "when started" do
|
97
110
|
it "should send a SIGINT to the child process" do
|
98
111
|
subject.start!
|
99
112
|
should_receive_and_run(Process, :kill, 'INT', subject.pid)
|
100
113
|
subject.stop!
|
101
114
|
end
|
102
115
|
|
103
|
-
|
116
|
+
it "should be stopping" do
|
117
|
+
subject.start!
|
118
|
+
subject.stop!
|
119
|
+
subject.should be_stopping
|
120
|
+
end
|
121
|
+
|
122
|
+
context "when child process does die within the worker's timeout" do
|
104
123
|
subject do
|
105
124
|
worker = Angael::TestSupport::SampleWorker.new
|
106
125
|
worker.stub(:timeout => 2)
|
@@ -132,7 +151,7 @@ describe Angael::Worker do
|
|
132
151
|
end
|
133
152
|
end
|
134
153
|
|
135
|
-
context "child process does not die within the worker's timeout" do
|
154
|
+
context "when child process does not die within the worker's timeout" do
|
136
155
|
subject do
|
137
156
|
worker = Angael::TestSupport::SampleWorker.new
|
138
157
|
worker.stub(:timeout => 1)
|
@@ -176,6 +195,9 @@ describe Angael::Worker do
|
|
176
195
|
# Clean up
|
177
196
|
Process._original_kill('KILL', subject.pid)
|
178
197
|
sleep 0.1 # Wait for SIGKILL to take effect.
|
198
|
+
# We must clean up the zombies here because the Worker class noramlly
|
199
|
+
# relies on the worker manager to do that.
|
200
|
+
Process.wait2(subject.pid, Process::WNOHANG)
|
179
201
|
pid_running?(subject.pid).should be_false
|
180
202
|
end
|
181
203
|
|
@@ -216,31 +238,6 @@ describe Angael::Worker do
|
|
216
238
|
pid, status = Process.wait2(subject.pid)
|
217
239
|
status.should == 0
|
218
240
|
end
|
219
|
-
|
220
|
-
it "should reap the child process" do
|
221
|
-
# As a general rule, I would only expect this method to be called
|
222
|
-
# once. However, signal behavior is difficult to predict. In some
|
223
|
-
# of our test runs, it is called more than once. If we do receive
|
224
|
-
# multiple SIGCHLD signals (which would be what causes this method
|
225
|
-
# to be called multiple times) there are no ill side effects. Thus
|
226
|
-
# I think it is quite safe to allow this method to be called more
|
227
|
-
# than once.
|
228
|
-
# -- Paul Cortens (2011-05-18)
|
229
|
-
subject.should_receive(:wait_for_child).at_least(:once)
|
230
|
-
|
231
|
-
subject.start!
|
232
|
-
sleep 0.1 # Make sure there was enough time for the child process to start.
|
233
|
-
Process.kill(sig, subject.pid)
|
234
|
-
|
235
|
-
# We need to wait this long to make sure the child process had time
|
236
|
-
# to respond to the signal. If this is decreased to 'sleep 0.1' then
|
237
|
-
# the tests will sometimes fail because there wasn't enough time for
|
238
|
-
# the trap block to run #wait_for_child.
|
239
|
-
sleep 0.5
|
240
|
-
|
241
|
-
# Clean up zombie child processes.
|
242
|
-
Process.wait(subject.pid)
|
243
|
-
end
|
244
241
|
end
|
245
242
|
end
|
246
243
|
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module IOHelpers
|
2
|
+
def suppress_stderr
|
3
|
+
# The output stream must be an IO-like object. In this case we capture it in
|
4
|
+
# an in-memory IO object so we can return the string value. You can assign any
|
5
|
+
# IO object here.
|
6
|
+
previous_stderr, $stderr = $stderr, StringIO.new
|
7
|
+
yield
|
8
|
+
ensure
|
9
|
+
# Restore the previous value of stderr (typically equal to STDERR).
|
10
|
+
$stderr = previous_stderr
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
RSpec.configuration.include(IOHelpers)
|
metadata
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
name: angael
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease:
|
5
|
-
version: 0.0.
|
5
|
+
version: 0.0.3
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Paul Cortens
|
@@ -30,9 +30,9 @@ dependencies:
|
|
30
30
|
requirement: &id002 !ruby/object:Gem::Requirement
|
31
31
|
none: false
|
32
32
|
requirements:
|
33
|
-
- - "
|
33
|
+
- - "="
|
34
34
|
- !ruby/object:Gem::Version
|
35
|
-
version:
|
35
|
+
version: 0.0.2
|
36
36
|
type: :development
|
37
37
|
version_requirements: *id002
|
38
38
|
description: Angael is a lightweight library for running repetitive background processes. It handles the forking and signal catching, allow you to just define what the background workers should do.
|
@@ -56,6 +56,7 @@ files:
|
|
56
56
|
- spec/lib/angael/manager_spec.rb
|
57
57
|
- spec/lib/angael/worker_spec.rb
|
58
58
|
- spec/spec_helper.rb
|
59
|
+
- spec/support/io_helpers.rb
|
59
60
|
- spec/support/test_worker.rb
|
60
61
|
has_rdoc: true
|
61
62
|
homepage: http://github.com/thoughtless/angael
|