tengine_job_agent 0.3.17

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1 @@
1
+ /pid_for_*
@@ -0,0 +1,17 @@
1
+ ---
2
+ timeout: 1
3
+ # log_dir: "log"
4
+ logfile: "<%= %!log/#{File.basename($PROGRAM_NAME)}-#{`hostname`.strip}-#{Process.pid}.log! %>"
5
+ connection:
6
+ host: 'localhost'
7
+ port: 5672
8
+ # vhost:
9
+ # user:
10
+ # pass:
11
+ exchange:
12
+ name: 'tengine_event_exchange'
13
+ type: 'direct'
14
+ durable: true
15
+ heartbeat:
16
+ job:
17
+ interval: 1
@@ -0,0 +1,14 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+ require 'simplecov'
4
+ SimpleCov.start if ENV["COVERAGE"]
5
+ require 'rspec'
6
+ require 'tengine_job_agent'
7
+
8
+ # Requires supporting files with custom matchers and macros, etc,
9
+ # in ./support/ and its subdirectories.
10
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
11
+
12
+ RSpec.configure do |config|
13
+
14
+ end
@@ -0,0 +1,124 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'spec_helper'
3
+ require 'tmpdir'
4
+ require 'tempfile'
5
+
6
+ describe TengineJobAgent::CommandUtils do
7
+ describe ".included" do
8
+ it "ClassMethodsを追加" do
9
+ foo = Class.new do
10
+ include TengineJobAgent::CommandUtils
11
+ end
12
+ foo.singleton_class.ancestors.should include(TengineJobAgent::CommandUtils::ClassMethods)
13
+ end
14
+ end
15
+
16
+ context "::ClassMethods" do
17
+ describe "#load_config" do
18
+ subject { Class.new { include TengineJobAgent::CommandUtils::ClassMethods }.new }
19
+
20
+ it "Hashをかえす" do
21
+ Dir.chdir File.expand_path("../..", __FILE__) do
22
+ subject.load_config.should be_kind_of(Hash)
23
+ end
24
+ end
25
+
26
+ it "./tengine_job_agent.ymlを読む" do
27
+ Dir.mktmpdir do |nam|
28
+ Dir.chdir nam do
29
+ File.open("tengine_job_agent.yml", "wb") {|f| f.puts "foo: bar\n" }
30
+ subject.load_config.should == { "foo" => "bar" }
31
+ end
32
+ end
33
+ end
34
+
35
+ it "./config/tengine_job_agent.ymlを読む" do
36
+ Dir.mktmpdir do |nam|
37
+ Dir.chdir nam do
38
+ Dir.mkdir "config"
39
+ File.open("config/tengine_job_agent.yml", "wb") {|f| f.puts "foo: bar\n" }
40
+ subject.load_config.should == { "foo" => "bar" }
41
+ end
42
+ end
43
+ end
44
+
45
+ it "/etc/tengine_job_agent.ymlを読む" do
46
+ begin
47
+ if File.exist? "/etc/tengine_job_agent.yml"
48
+ obj = YAML.load_file "/etc/tengine_job_agent.yml"
49
+ subject.load_config.should == obj
50
+ else
51
+ File.open("/etc/tengine_job_agent.yml", "wb") {|f| f.puts "foo: bar\n" }
52
+ subject.load_config.should == { "foo" => "bar" }
53
+ end
54
+ rescue Errno::EACCES
55
+ pending $!.message
56
+ end
57
+ end
58
+ end
59
+
60
+ describe "#new_logger" do
61
+ subject { Class.new { include TengineJobAgent::CommandUtils::ClassMethods }.new }
62
+ before { subject.stub(:name).and_return("foobar") }
63
+
64
+ it "logfileを指定する場合" do
65
+ Dir.mktmpdir do |nam|
66
+ Logger.should_receive(:new).with("foo/bar/baz.log")
67
+ subject.new_logger('logfile' => "foo/bar/baz.log")
68
+ end
69
+ end
70
+
71
+ it "logfileもlog_dirも指定する場合" do
72
+ Dir.mktmpdir do |nam|
73
+ Logger.should_receive(:new).with(/\/foobar-\d+?.log$/)
74
+ subject.new_logger({})
75
+ end
76
+ end
77
+
78
+ it "Loggerを返す" do
79
+ Dir.mktmpdir do |nam|
80
+ subject.new_logger('log_dir' => nam).should be_kind_of(Logger)
81
+ end
82
+ end
83
+
84
+ it "引数はディレクトリである" do
85
+ Tempfile.new("") do |f|
86
+ subject.new_logger('log_dir' => "nonexistent").should raise_exception(Errno::ENOENT)
87
+ subject.new_logger('log_dir' => f.path).should raise_exception(Errno::ENOENT)
88
+ end
89
+ end
90
+
91
+ it "ログファイルは引数のディレクトリの中にできる" do
92
+ Dir.mktmpdir do |nam|
93
+ subject.new_logger('log_dir' => nam)
94
+ nam.should have_at_least(3).files
95
+ end
96
+ end
97
+
98
+ end
99
+
100
+ describe "#process" do
101
+ subject { Class.new { include TengineJobAgent::CommandUtils } }
102
+ let(:instance) { mock(subject.new) }
103
+ before do
104
+ instance
105
+ subject.stub(:new).with(anything, anything, anything).and_return(instance)
106
+ subject.stub(:name).and_return(File.basename(__FILE__, ".rb"))
107
+ end
108
+
109
+ it "インスタンスを生成してprocessを呼ぶ" do
110
+ instance.should_receive(:process)
111
+ Dir.chdir File.expand_path("../..", __FILE__) do
112
+ subject.process
113
+ end
114
+ end
115
+
116
+ it "失敗するとfalseを返す" do
117
+ instance.should_receive(:process).and_raise(RuntimeError)
118
+ Dir.chdir File.expand_path("../..", __FILE__) do
119
+ subject.process.should == false
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,107 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'spec_helper'
3
+
4
+ require 'yaml'
5
+ require 'tengine/support/yaml_with_erb'
6
+ require 'rbconfig'
7
+
8
+ describe TengineJobAgent::Run do
9
+
10
+ before do
11
+ @log_buffer = StringIO.new
12
+ @logger = Logger.new(@log_buffer)
13
+ config = YAML.load_file(File.expand_path("../config/tengine_job_agent.yml.erb",
14
+ File.dirname(__FILE__)))
15
+ @config = config.inject({}) {|r, (k, v)| r.update k.intern => v }
16
+ end
17
+
18
+ subject do
19
+ Dir.chdir File.expand_path("../..", __FILE__) do
20
+ TengineJobAgent::Run.new(@logger, %w"scripts/echo_foo.sh", @config)
21
+ end
22
+ end
23
+
24
+ it { should_not be_nil }
25
+
26
+ describe "#process" do
27
+
28
+ let(:f) { mock(File.open("/dev/null")) }
29
+
30
+ before do
31
+ subject.stub(:spawn_watchdog).and_return mock(Numeric.new)
32
+ f
33
+ File.stub(:open).with(an_instance_of(String), "r").and_yield(f)
34
+ File.stub(:open).with(an_instance_of(String), "w")
35
+ end
36
+
37
+ context "正常起動" do
38
+ it "EXIT_SUCCESS" do
39
+ STDOUT.stub(:puts).with(an_instance_of(String))
40
+ f.stub(:gets).and_return("0\n")
41
+ subject.process.should == true
42
+ end
43
+ end
44
+
45
+ context "spawnできない" do
46
+ it "EXIT_FAILURE" do
47
+ STDERR.stub(:puts) do |arg|
48
+ arg.should =~ /foo bar/
49
+ end
50
+ f.stub(:read).and_return("foo bar")
51
+ f.stub(:gets).and_return("foo bar")
52
+ f.stub(:rewind)
53
+ subject.process.should == false
54
+ end
55
+ end
56
+
57
+ context "timeout" do
58
+ it "EXIT_FAILURE" do
59
+ f.stub(:gets)
60
+ lambda { subject.process }.should raise_exception(Timeout::Error)
61
+ end
62
+ end
63
+ end
64
+
65
+ describe "#spawn_watchdog" do
66
+ it "tengine_job_agent_watchdogを起動する" do
67
+ watchdog = File.expand_path("../../bin/tengine_job_agent_watchdog", File.dirname(__FILE__))
68
+ Process.should_receive(:spawn).with(RbConfig.ruby, watchdog, an_instance_of(String), anything)
69
+ subject.spawn_watchdog
70
+ end
71
+
72
+ it "pidを返す" do
73
+ pid = mock(Numeric.new)
74
+ Process.stub(:spawn).with(RbConfig.ruby, anything, anything, anything).and_return(pid)
75
+ subject.spawn_watchdog.should == pid
76
+ end
77
+
78
+ it "終了を待たない" do
79
+ Process.stub(:spawn).with(RbConfig.ruby, anything, anything, anything)
80
+ Process.should_not_receive :wait
81
+ Process.should_not_receive :waitpid
82
+ Process.should_not_receive :waitpid2
83
+ subject.spawn_watchdog
84
+ end
85
+ end
86
+
87
+ describe "#initialize" do
88
+ it "第一引数にlogger" do
89
+ watchdog = File.expand_path("../../bin/tengine_job_agent_watchdog", File.dirname(__FILE__))
90
+ Process.should_receive(:spawn).with(RbConfig.ruby, watchdog, an_instance_of(String), anything)
91
+ subject.spawn_watchdog
92
+ @log_buffer.string.should_not be_empty
93
+ end
94
+
95
+ it "第二引数は起動するプロセスへの引数の配列" do
96
+ watchdog = File.expand_path("../../bin/tengine_job_agent_watchdog", File.dirname(__FILE__))
97
+ Process.should_receive(:spawn).with(RbConfig.ruby, watchdog, an_instance_of(String), "scripts/echo_foo.sh")
98
+ subject.spawn_watchdog
99
+ end
100
+
101
+ it "第三引数はconfig" do
102
+ subject.stub(:timeout) do |tim|
103
+ tim.should == @config[:timeout]
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+
3
+ echo 'foo'
4
+ exit 0
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+
3
+ typeset -i time=$1
4
+ echo "`date` sleep.sh start"
5
+ sleep `expr 1 + $time`
6
+ echo "`date` sleep.sh finish"
@@ -0,0 +1,307 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'spec_helper'
3
+ require 'amqp'
4
+
5
+ require 'timeout'
6
+
7
+ require 'yaml'
8
+ require 'tengine/support/yaml_with_erb'
9
+
10
+ describe TengineJobAgent::Watchdog do
11
+
12
+ before do
13
+ @log_buffer = StringIO.new
14
+ @logger = Logger.new(@log_buffer)
15
+ @config = YAML.load_file(File.expand_path("../config/tengine_job_agent.yml.erb",
16
+ File.dirname(__FILE__)))
17
+ end
18
+
19
+ subject do
20
+ @pid_path = "/dev/null"
21
+ echo_foo = File.expand_path "../scripts/echo_foo.sh", __FILE__
22
+ TengineJobAgent::Watchdog.new(@logger, [@pid_path, echo_foo], @config)
23
+ end
24
+
25
+ it { should_not be_nil }
26
+
27
+ describe "#process" do
28
+ let(:pid) { mock(Numeric.new) }
29
+ let(:stat) { mock($?) }
30
+ before do
31
+ bigzero = (1 << 1024).coerce(0)[0]
32
+ pid.stub(:to_int).and_return(bigzero)
33
+ stat.stub(:exitstatus).and_return(bigzero)
34
+ end
35
+
36
+ it "spawnする" do
37
+ subject.should_receive(:spawn_process).and_return(pid)
38
+ subject.stub(:detach_and_wait_process).with(pid).and_return(stat)
39
+ subject.stub(:fire_finished).with(pid, stat)
40
+ sender = mock(:sender)
41
+ sender.stub_chain(:mq_suite, :ensures).with(:connection).and_yield
42
+ subject.stub(:sender).and_return(sender)
43
+ sender.stub(:wait_for_connection).and_yield
44
+ EM.run do
45
+ EM.add_timer(0.1) { EM.stop }
46
+ subject.process
47
+ end
48
+ end
49
+
50
+ it "子プロセスを待つ" do
51
+ subject.stub(:spawn_process).and_return(pid)
52
+ subject.should_receive(:detach_and_wait_process).and_return(stat)
53
+ subject.stub(:fire_finished).with(pid, stat)
54
+ sender = mock(:sender)
55
+ sender.stub_chain(:mq_suite, :ensures).with(:connection).and_yield
56
+ subject.stub(:sender).and_return(sender)
57
+ sender.stub(:wait_for_connection).and_yield
58
+ EM.run do
59
+ EM.add_timer(0.1) { EM.stop }
60
+ subject.process
61
+ end
62
+ end
63
+
64
+ context "ファイルへの出力" do
65
+ before do
66
+ @echo_foo = File.expand_path "../scripts/echo_foo.sh", __FILE__
67
+ mock_stdout = mock(:stdout, :path => "/tmp/stdout")
68
+ mock_stderr = mock(:stderr, :path => "/tmp/stderr")
69
+ subject.should_receive(:with_tmp_outs).and_yield(mock_stdout, mock_stderr)
70
+ sender = mock(:sender)
71
+ sender.stub_chain(:mq_suite, :ensures).with(:connection).and_yield
72
+ subject.stub(:sender).and_return(sender)
73
+ sender.stub(:wait_for_connection).and_yield
74
+ end
75
+
76
+ it "実行に成功した場合はPIDが出力される" do
77
+ subject.should_receive(:spawn_process).and_return(pid)
78
+ mock_pid_file = mock(:pid_file)
79
+ File.should_receive(:open).with(@pid_path, "a").and_yield(mock_pid_file)
80
+ mock_pid_file.should_receive(:puts).with(pid)
81
+ subject.should_receive(:detach_and_wait_process).with(pid)
82
+ EM.run do
83
+ EM.add_timer(0.1) { EM.stop }
84
+ subject.process
85
+ end
86
+ end
87
+
88
+ it "ファイルが存在せずspawnに失敗した場合、エラーを出力する" do
89
+ subject.should_receive(:spawn_process).and_raise(Errno::ENOENT.new(@echo_foo))
90
+ mock_pid_file = mock(:pid_file)
91
+ File.should_receive(:open).with(@pid_path, "a").and_yield(mock_pid_file)
92
+ mock_pid_file.should_receive(:puts).with("[Errno::ENOENT] No such file or directory - #{@echo_foo}")
93
+ timeout(0.5) do
94
+ EM.run do
95
+ # EM.add_timer(0.1) { EM.stop } # 起動に失敗したらEMは自動でstopされる
96
+ subject.process
97
+ end
98
+ end
99
+ end
100
+ end
101
+
102
+ end
103
+
104
+ describe "#spawn_process" do
105
+ let(:pid) { mock(Numeric.new) }
106
+ let(:thr) { mock(Thread.start do Thread.stop end) }
107
+ let(:stat) { mock($?) }
108
+ before do
109
+ o = mock(STDOUT)
110
+ e = mock(STDERR)
111
+ o.stub(:path).and_return(String.new)
112
+ e.stub(:path).and_return(String.new)
113
+ subject.instance_eval do
114
+ @stdout = o
115
+ @stderr = e
116
+ end
117
+ end
118
+
119
+ it "spawnする" do
120
+ echo_foo = File.expand_path "../scripts/echo_foo.sh", __FILE__
121
+ Process.should_receive(:spawn).with(echo_foo, an_instance_of(Hash)).and_return(pid)
122
+ subject.spawn_process.should == pid
123
+ end
124
+
125
+ it "No such file or directoryで失敗する" do
126
+ echo_foo = File.expand_path "../scripts/echo_foo.sh", __FILE__
127
+ Process.should_receive(:spawn).with(echo_foo, an_instance_of(Hash)).
128
+ and_raise(Errno::ENOENT.new(echo_foo))
129
+ expect {
130
+ subject.spawn_process
131
+ }.to raise_error(Errno::ENOENT)
132
+ end
133
+ end
134
+
135
+ describe "#detach_and_wait_process" do
136
+ let(:pid) { mock(Numeric.new) }
137
+ let(:stat) { mock($?) }
138
+ before do
139
+ bigzero = (1 << 1024).coerce(0).first
140
+ stat.stub(:exitstatus).and_return(bigzero)
141
+ pid.stub(:to_int).and_return(bigzero)
142
+ Process.stub(:waitpid2).with(pid) do
143
+ sleep 3
144
+ [pid, stat]
145
+ end
146
+ subject.stub(:fire_finished) do EM.stop end
147
+ subject.stub(:fire_heartbeat).with(pid).and_yield
148
+ end
149
+
150
+ it "pidを待つ" do
151
+ EM.run do
152
+ subject.should_receive(:fire_finished) do EM.stop end
153
+ subject.detach_and_wait_process(pid)
154
+ end
155
+ end
156
+
157
+ it "heartbeatをfireしつづける" do
158
+ EM.run do
159
+ subject.should_receive(:fire_heartbeat).at_least(2).times.and_yield
160
+ subject.detach_and_wait_process(pid)
161
+ end
162
+ end
163
+
164
+ it "https://www.pivotaltracker.com/story/show/21515847" do
165
+ EM.run do
166
+ subject.instance_eval { @config["heartbeat"]["job"]["interval"] = 0 }
167
+ subject.unstub(:fire_heartbeat)
168
+ subject.should_receive(:fire_heartbeat).at_least(1).times.and_yield
169
+ subject.detach_and_wait_process(pid)
170
+ end
171
+ end
172
+
173
+ context "プロセスは正常に動き続けているがfireに失敗した場合" do
174
+ it "その回のfireはあきらめる。例外などで死なない" do
175
+ EM.run do
176
+ subject.unstub(:fire_heartbeat)
177
+ s = mock(Tengine::Event::Sender.new)
178
+ subject.stub(:sender).and_return(s)
179
+ def s.fire e, h, &b
180
+ h[:retry_count].should_not == nil
181
+ h[:retry_count].should == 0
182
+ b.yield if b
183
+ end
184
+ expect {
185
+ subject.detach_and_wait_process(pid)
186
+ }.to_not raise_exception(Tengine::Event::Sender::RetryError)
187
+ end
188
+ end
189
+ end
190
+
191
+ context "finishしたときのtimerの挙動" do
192
+ def live_timers_count
193
+ # これはひどい...
194
+ ObjectSpace.each_object(EM::PeriodicTimer).reject do |i|
195
+ i.instance_eval do
196
+ @cancelled
197
+ end
198
+ end
199
+ end
200
+ it "https://www.pivotaltracker.com/story/show/21466285" do
201
+ n = live_timers_count
202
+ EM.run do
203
+ subject.detach_and_wait_process(pid)
204
+ end
205
+ live_timers_count.should == n
206
+ end
207
+ end
208
+ end
209
+
210
+ describe "#fire_finished" do
211
+ let(:pid) { mock(Numeric.new) }
212
+ let(:stat) { mock($?) }
213
+ before do
214
+ pid.stub(:to_int).and_return(-0.0/1.0)
215
+ conn = mock(:connection)
216
+ ch = Object.new
217
+
218
+ AMQP.stub(:connect).with(an_instance_of(Hash)).and_return(conn)
219
+ AMQP::Channel.stub(:new).with(conn, :prefetch => 1, :auto_recovery => true).and_return(ch)
220
+ AMQP::Exchange.stub(:new).with(ch, "direct", "exchange1",
221
+ :passive=>false, :durable=>true, :auto_delete=>false, :internal=>false, :nowait=>true)
222
+ conn.stub(:on_tcp_connection_loss)
223
+ conn.stub(:after_recovery)
224
+ conn.stub(:on_closed)
225
+ sender = mock(:sender)
226
+ subject.stub(:sender).and_return(sender)
227
+ sender.stub(:wait_for_connection).and_yield
228
+
229
+ o = mock(STDOUT)
230
+ e = mock(STDERR)
231
+ o.stub(:path).and_return(String.new)
232
+ e.stub(:path).and_return(String.new)
233
+ subject.instance_eval do
234
+ @stdout = o
235
+ @stderr = e
236
+ end
237
+ end
238
+
239
+ it "finished.process.job.tengineをfire" do
240
+ EM.run do
241
+ FileUtils.stub(:cp).with(an_instance_of(String), an_instance_of(String))
242
+ stat.stub(:exitstatus).and_return(0)
243
+ s = mock(Tengine::Event::Sender.new)
244
+ subject.stub(:sender).and_return s
245
+ s.stub(:stop)
246
+ s.should_receive(:fire) do |k, v|
247
+ k.should == "finished.process.job.tengine"
248
+ v[:level_key].should == :info
249
+ v[:properties]["pid"].should == pid
250
+ v[:properties]["exit_status"].should == stat.exitstatus
251
+ end
252
+ subject.fire_finished(pid, stat)
253
+ EM.add_timer(0.1) { EM.stop }
254
+ end
255
+ end
256
+
257
+ it "プロセスが失敗していた場合" do
258
+ EM.run do
259
+ FileUtils.stub(:cp).with(an_instance_of(String), an_instance_of(String))
260
+ stat.stub(:exitstatus).and_return(256)
261
+ s = mock(Tengine::Event::Sender.new)
262
+ subject.stub(:sender).and_return s
263
+ s.stub(:stop)
264
+ s.should_receive(:fire) do |k, v|
265
+ k.should == "finished.process.job.tengine"
266
+ v[:level_key].should == :error
267
+ v[:properties][:message].should =~ /^Job process failed./
268
+ end
269
+ subject.fire_finished(pid, stat)
270
+ EM.add_timer(0.1) { EM.stop }
271
+ end
272
+ end
273
+
274
+ context "プロセスは正常に終了したがfireに失敗した場合" do
275
+ it "fireできるようになるまでリトライを続ける" do
276
+ EM.run do
277
+ stat.stub(:exitstatus).and_return(0)
278
+ s = mock(Tengine::Event::Sender.new)
279
+ n = 0
280
+ subject.stub(:sender).and_return(s)
281
+ s.stub(:stop)
282
+ s.stub(:fire).with("finished.process.job.tengine", an_instance_of(Hash)) do |e, h|
283
+ if h[:retry_count]
284
+ h[:retry_count].should > 0
285
+ end
286
+ end
287
+ expect {
288
+ subject.fire_finished(pid, stat)
289
+ }.to_not raise_exception(Tengine::Event::Sender::RetryError)
290
+ EM.add_timer(0.1) { EM.stop }
291
+ end
292
+ end
293
+ end
294
+ end
295
+
296
+ describe "#sender" do
297
+ before do
298
+ conn = mock(:connection)
299
+ conn.stub(:on_tcp_connection_loss)
300
+ conn.stub(:after_recovery)
301
+ conn.stub(:on_closed)
302
+ AMQP.stub(:connect).with(an_instance_of(Hash)).and_return(conn)
303
+ end
304
+ subject { TengineJobAgent::Watchdog.new(@logger, %w"", @config).sender }
305
+ it { should be_kind_of(Tengine::Event::Sender) }
306
+ end
307
+ end