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 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