angael 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
data/angael.gemspec CHANGED
@@ -20,5 +20,5 @@ Gem::Specification.new do |s|
20
20
  s.require_paths = ["lib"]
21
21
 
22
22
  s.add_development_dependency('rspec', '2.6.0')
23
- s.add_development_dependency('rspec-process-mocks')
23
+ s.add_development_dependency('rspec-process-mocks', '0.0.2')
24
24
  end
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Angael
2
- VERSION = "0.0.2"
2
+ VERSION = "0.0.3"
3
3
  end
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
- trap("CHLD") do
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
- # Some internal state so that other parts of our code know that we
75
- # intentionally stopped the child process.
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, 2) }
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 "is stopped" do
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 "is started" do
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
- context "child process does die within the worker's timeout" do
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.2
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: "0"
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