trident 0.1.0
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.
- checksums.yaml +15 -0
- data/.coveralls.yml +1 -0
- data/.gitignore +17 -0
- data/.travis.yml +8 -0
- data/Gemfile +9 -0
- data/LICENSE.txt +22 -0
- data/README.md +33 -0
- data/Rakefile +18 -0
- data/bin/trident +8 -0
- data/lib/trident.rb +8 -0
- data/lib/trident/cli.rb +130 -0
- data/lib/trident/pool.rb +108 -0
- data/lib/trident/pool_handler.rb +31 -0
- data/lib/trident/pool_manager.rb +79 -0
- data/lib/trident/signal_handler.rb +167 -0
- data/lib/trident/utils.rb +9 -0
- data/lib/trident/version.rb +3 -0
- data/test/fixtures/integration_project/config/trident.yml +51 -0
- data/test/integration/trident_test.rb +105 -0
- data/test/test_helper.rb +144 -0
- data/test/unit/trident/cli_test.rb +253 -0
- data/test/unit/trident/pool_handler_test.rb +70 -0
- data/test/unit/trident/pool_manager_test.rb +131 -0
- data/test/unit/trident/pool_test.rb +233 -0
- data/test/unit/trident/signal_handler_test.rb +262 -0
- data/test/unit/trident/utils_test.rb +20 -0
- data/trident.example.yml +49 -0
- data/trident.gemspec +29 -0
- metadata +180 -0
@@ -0,0 +1,233 @@
|
|
1
|
+
require_relative '../../test_helper'
|
2
|
+
|
3
|
+
class Trident::PoolTest < MiniTest::Should::TestCase
|
4
|
+
|
5
|
+
setup do
|
6
|
+
SignalHandler.stubs(:reset_for_fork)
|
7
|
+
|
8
|
+
PoolHandler.constants(false).each do |c|
|
9
|
+
PoolHandler.send(:remove_const, c) if c =~ /^Test/
|
10
|
+
end
|
11
|
+
env = <<-EOS
|
12
|
+
class TestPoolWorker
|
13
|
+
def initialize(o)
|
14
|
+
@o = o
|
15
|
+
end
|
16
|
+
def start
|
17
|
+
sleep(@o['sleep']) if @o['sleep']
|
18
|
+
end
|
19
|
+
end
|
20
|
+
EOS
|
21
|
+
signal_mappings = {'stop_forcefully' => 'KILL', 'stop_gracefully' => 'TERM'}
|
22
|
+
@handler = PoolHandler.new("foo", "TestPoolWorker", env, signal_mappings, {})
|
23
|
+
end
|
24
|
+
|
25
|
+
context "#spawn_worker" do
|
26
|
+
|
27
|
+
should "fork a worker" do
|
28
|
+
pool = Pool.new("foo", @handler, 1, 'sleep' => 0.1)
|
29
|
+
assert_empty pool.workers
|
30
|
+
pool.send(:spawn_worker)
|
31
|
+
assert_equal 1, pool.workers.size
|
32
|
+
Process.waitpid(pool.workers.first)
|
33
|
+
assert $?.success?
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
context "#kill_worker" do
|
39
|
+
|
40
|
+
should "kill a worker" do
|
41
|
+
pool = Pool.new("foo", @handler, 1, 'sleep' => 1)
|
42
|
+
pool.send(:spawn_worker)
|
43
|
+
pid = pool.workers.first
|
44
|
+
|
45
|
+
pool.send(:kill_worker, pid, 'stop_forcefully')
|
46
|
+
Process.waitpid(pid)
|
47
|
+
assert ! $?.success?
|
48
|
+
assert_empty pool.workers
|
49
|
+
end
|
50
|
+
|
51
|
+
should "kill a worker with specific signal" do
|
52
|
+
pool = Pool.new("foo", @handler, 1, 'sleep' => 1)
|
53
|
+
pool.send(:spawn_worker)
|
54
|
+
pid = pool.workers.first
|
55
|
+
|
56
|
+
Process.expects(:kill).with("TERM", pid)
|
57
|
+
pool.send(:kill_worker, pid, 'stop_gracefully')
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
context "#spawn_workers" do
|
63
|
+
|
64
|
+
should "start multiple workers" do
|
65
|
+
pool = Pool.new("foo", @handler, 4, 'sleep' => 1)
|
66
|
+
pool.send(:spawn_workers, 4)
|
67
|
+
assert_equal 4, pool.workers.size
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
context "#kill_workers" do
|
73
|
+
|
74
|
+
should "kill multiple workers, most recent first" do
|
75
|
+
pool = Pool.new("foo", @handler, 4, 'sleep' => 1)
|
76
|
+
pool.send(:spawn_workers, 4)
|
77
|
+
orig_workers = pool.workers.dup
|
78
|
+
assert_equal 4, orig_workers.size
|
79
|
+
|
80
|
+
pool.send(:kill_workers, 3, 'stop_forcefully')
|
81
|
+
assert_equal 1, pool.workers.size
|
82
|
+
assert_equal orig_workers.first, pool.workers.first
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
context "#cleanup_dead_workers" do
|
88
|
+
|
89
|
+
should "stop tracking workers that have died" do
|
90
|
+
pool = Pool.new("foo", @handler, 4, 'sleep' => 0)
|
91
|
+
pool.send(:spawn_workers, 4)
|
92
|
+
|
93
|
+
sleep 0.1
|
94
|
+
assert_equal 4, pool.workers.size
|
95
|
+
pool.send(:cleanup_dead_workers)
|
96
|
+
assert_equal 0, pool.workers.size
|
97
|
+
end
|
98
|
+
|
99
|
+
should "block waiting for workers that have died when blocking" do
|
100
|
+
pool = Pool.new("foo", @handler, 1, 'sleep' => 0.2)
|
101
|
+
pool.send(:spawn_workers, 1)
|
102
|
+
assert_equal 1, pool.workers.size
|
103
|
+
|
104
|
+
thread = Thread.new { pool.send(:cleanup_dead_workers, true) }
|
105
|
+
sleep(0.1)
|
106
|
+
assert_equal 1, pool.workers.size
|
107
|
+
thread.join
|
108
|
+
assert_equal 0, pool.workers.size
|
109
|
+
end
|
110
|
+
|
111
|
+
should "not block waiting for workers that have died when not-blocking" do
|
112
|
+
pool = Pool.new("foo", @handler, 1, 'sleep' => 0.1)
|
113
|
+
pool.send(:spawn_workers, 1)
|
114
|
+
assert_equal 1, pool.workers.size
|
115
|
+
|
116
|
+
pool.send(:cleanup_dead_workers, false)
|
117
|
+
assert_equal 1, pool.workers.size
|
118
|
+
end
|
119
|
+
|
120
|
+
should "cleanup workers that have died even if already waited on" do
|
121
|
+
pool = Pool.new("foo", @handler, 4, 'sleep' => 0)
|
122
|
+
pool.send(:spawn_workers, 4)
|
123
|
+
|
124
|
+
# Calling process.wait on a pid that was already waited on throws a ECHLD
|
125
|
+
Process.waitall
|
126
|
+
assert_equal 4, pool.workers.size
|
127
|
+
pool.send(:cleanup_dead_workers, false)
|
128
|
+
|
129
|
+
assert_equal 0, pool.workers.size
|
130
|
+
end
|
131
|
+
|
132
|
+
|
133
|
+
end
|
134
|
+
|
135
|
+
context "#maintain_worker_count" do
|
136
|
+
|
137
|
+
should "spawn workers when count is low" do
|
138
|
+
pool = Pool.new("foo", @handler, 2, 'sleep' => 0.1)
|
139
|
+
assert_empty pool.workers
|
140
|
+
|
141
|
+
pool.send(:maintain_worker_count, 'stop_gracefully')
|
142
|
+
assert_equal 2, pool.workers.size
|
143
|
+
end
|
144
|
+
|
145
|
+
should "kill workers when count is high" do
|
146
|
+
pool = Pool.new("foo", @handler, 2, 'sleep' => 0.1)
|
147
|
+
pool.send(:spawn_workers, 4)
|
148
|
+
assert_equal 4, pool.workers.size
|
149
|
+
|
150
|
+
pool.send(:maintain_worker_count, 'stop_gracefully')
|
151
|
+
assert_equal 2, pool.workers.size
|
152
|
+
end
|
153
|
+
|
154
|
+
should "kill workers with given action when count is high" do
|
155
|
+
pool = Pool.new("foo", @handler, 2, 'sleep' => 0.1)
|
156
|
+
pool.send(:spawn_workers, 4)
|
157
|
+
assert_equal 4, pool.workers.size
|
158
|
+
|
159
|
+
Process.expects(:kill).with("KILL", pool.workers.to_a[-1])
|
160
|
+
Process.expects(:kill).with("KILL", pool.workers.to_a[-2])
|
161
|
+
pool.send(:maintain_worker_count, 'stop_forcefully')
|
162
|
+
|
163
|
+
pool.send(:spawn_workers, 2)
|
164
|
+
Process.expects(:kill).with("TERM", pool.workers.to_a[-1])
|
165
|
+
Process.expects(:kill).with("TERM", pool.workers.to_a[-2])
|
166
|
+
pool.send(:maintain_worker_count, 'stop_gracefully')
|
167
|
+
end
|
168
|
+
|
169
|
+
should "do nothing when count is correct" do
|
170
|
+
Process.expects(:kill).never
|
171
|
+
pool = Pool.new("foo", @handler, 2, 'sleep' => 0.1)
|
172
|
+
pool.send(:spawn_workers, 2)
|
173
|
+
orig_workers = pool.workers.dup
|
174
|
+
assert_equal 2, orig_workers.size
|
175
|
+
|
176
|
+
pool.send(:maintain_worker_count, 'stop_gracefully')
|
177
|
+
assert_equal 2, pool.workers.size
|
178
|
+
assert_equal orig_workers, pool.workers
|
179
|
+
end
|
180
|
+
|
181
|
+
end
|
182
|
+
|
183
|
+
context "#start" do
|
184
|
+
|
185
|
+
should "start up the workers" do
|
186
|
+
pool = Pool.new("foo", @handler, 2, 'sleep' => 0.1)
|
187
|
+
pool.start
|
188
|
+
assert_equal 2, pool.workers.size
|
189
|
+
end
|
190
|
+
|
191
|
+
end
|
192
|
+
|
193
|
+
context "#stop" do
|
194
|
+
|
195
|
+
should "stop the workers" do
|
196
|
+
pool = Pool.new("foo", @handler, 2, 'sleep' => 0.1)
|
197
|
+
pool.start
|
198
|
+
assert_equal 2, pool.workers.size
|
199
|
+
pool.stop
|
200
|
+
assert_empty pool.workers
|
201
|
+
end
|
202
|
+
|
203
|
+
end
|
204
|
+
|
205
|
+
context "#wait" do
|
206
|
+
|
207
|
+
should "block till all workers complete" do
|
208
|
+
pool = Pool.new("foo", @handler, 2, 'sleep' => 0.1)
|
209
|
+
pool.start
|
210
|
+
assert_equal 2, pool.workers.size
|
211
|
+
pool.wait
|
212
|
+
assert_empty pool.workers
|
213
|
+
end
|
214
|
+
|
215
|
+
end
|
216
|
+
|
217
|
+
context "#update" do
|
218
|
+
|
219
|
+
should "update monitored workers" do
|
220
|
+
pool = Pool.new("foo", @handler, 2, 'sleep' => 0.2)
|
221
|
+
pool.start
|
222
|
+
orig_workers = pool.workers.dup
|
223
|
+
assert_equal 2, orig_workers.size
|
224
|
+
Process.kill("KILL", orig_workers.first)
|
225
|
+
sleep(0.1)
|
226
|
+
assert_equal orig_workers, pool.workers
|
227
|
+
pool.update
|
228
|
+
refute_equal orig_workers, pool.workers
|
229
|
+
end
|
230
|
+
|
231
|
+
end
|
232
|
+
|
233
|
+
end
|
@@ -0,0 +1,262 @@
|
|
1
|
+
require_relative '../../test_helper'
|
2
|
+
|
3
|
+
class Trident::SignalHandlerTest < MiniTest::Should::TestCase
|
4
|
+
|
5
|
+
class Target
|
6
|
+
attr_accessor :received
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@received = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def method_missing(method, *args, &block)
|
13
|
+
@received << [method, args, block]
|
14
|
+
method =~ /action_(.*)/ ? $1.to_sym : :noaction
|
15
|
+
end
|
16
|
+
|
17
|
+
def respond_to_missing?(name, include_private = false)
|
18
|
+
name !~ /nomethod/
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def do_in_child
|
23
|
+
read_from_child, write_from_child = IO.pipe
|
24
|
+
|
25
|
+
pid = fork do
|
26
|
+
read_from_child.close
|
27
|
+
result = yield
|
28
|
+
Marshal.dump(result, write_from_child)
|
29
|
+
exit!(0) # skips exit handlers.
|
30
|
+
end
|
31
|
+
|
32
|
+
[pid, read_from_child]
|
33
|
+
write_from_child.close
|
34
|
+
result = read_from_child.read
|
35
|
+
Process.wait(pid)
|
36
|
+
raise "child failed" if result.empty?
|
37
|
+
Marshal.load(result)
|
38
|
+
end
|
39
|
+
|
40
|
+
context "#initialize" do
|
41
|
+
|
42
|
+
should "add in CHLD handler" do
|
43
|
+
handler = SignalHandler.new({}, Target.new)
|
44
|
+
assert_equal({"SIGCHLD" => ["update"]}, handler.signal_mappings)
|
45
|
+
end
|
46
|
+
|
47
|
+
should "allow CHLD handler replacement" do
|
48
|
+
handler = SignalHandler.new({"CHLD" => "foo"}, Target.new)
|
49
|
+
assert_equal({"SIGCHLD" => ["foo"]}, handler.signal_mappings)
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
context "#signal_mappings==" do
|
55
|
+
|
56
|
+
should "normalize signal names" do
|
57
|
+
handler = SignalHandler.new({}, Target.new)
|
58
|
+
handler.send :signal_mappings=,
|
59
|
+
{"int" => "foo", "sigterm" => "bar",
|
60
|
+
"USR1" => "baz", "SIGUSR2" => ["bum", "hum"]}
|
61
|
+
|
62
|
+
assert_equal({"SIGCHLD" => ["update"], "SIGINT" => ["foo"],
|
63
|
+
"SIGTERM" => ["bar"], "SIGUSR1" => ["baz"],
|
64
|
+
"SIGUSR2" => ["bum", "hum"]}, handler.signal_mappings)
|
65
|
+
end
|
66
|
+
|
67
|
+
should "fail for duplicate signals" do
|
68
|
+
handler = SignalHandler.new({}, Target.new)
|
69
|
+
signals = {"int" => "foo", "sigint" => "bar"}
|
70
|
+
assert_raises(ArgumentError) { handler.send :signal_mappings=, signals }
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
context "#setup_self_pipe" do
|
76
|
+
|
77
|
+
should "create new pipes" do
|
78
|
+
handler = SignalHandler.new({}, Target.new)
|
79
|
+
assert_equal 0, handler.send(:self_pipe).size
|
80
|
+
handler.send :setup_self_pipe
|
81
|
+
assert_equal 2, handler.send(:self_pipe).size
|
82
|
+
end
|
83
|
+
|
84
|
+
should "replace pipes with new ones" do
|
85
|
+
handler = SignalHandler.new({}, Target.new)
|
86
|
+
handler.send :setup_self_pipe
|
87
|
+
old = handler.send(:self_pipe).dup
|
88
|
+
assert_equal 2, old.size
|
89
|
+
handler.send :setup_self_pipe
|
90
|
+
new = handler.send(:self_pipe).dup
|
91
|
+
assert_equal 2, new.size
|
92
|
+
refute_equal old, new
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
context "#setup_signal_handlers" do
|
98
|
+
|
99
|
+
should "trap given signals" do
|
100
|
+
signals = {"int" => "foo", "term" => "bar"}
|
101
|
+
handler = SignalHandler.new(signals, Target.new)
|
102
|
+
|
103
|
+
handler.expects(:trap_deferred).with("SIGINT")
|
104
|
+
handler.expects(:trap_deferred).with("SIGTERM")
|
105
|
+
handler.expects(:trap_deferred).with("SIGCHLD")
|
106
|
+
handler.send(:setup_signal_handlers)
|
107
|
+
end
|
108
|
+
|
109
|
+
should "save original signals" do
|
110
|
+
signals = {"int" => "foo", "term" => "bar"}
|
111
|
+
handler = SignalHandler.new(signals, Target.new)
|
112
|
+
|
113
|
+
handler.stubs(:trap_deferred)
|
114
|
+
handler.send(:setup_signal_handlers)
|
115
|
+
assert_equal({"SIGCHLD" => nil, "SIGINT" => nil, "SIGTERM" => nil},
|
116
|
+
handler.original_signal_handlers)
|
117
|
+
end
|
118
|
+
|
119
|
+
should "fail for unhandled methods" do
|
120
|
+
signals = {"term" => "nomethod"}
|
121
|
+
handler = SignalHandler.new(signals, Target.new)
|
122
|
+
|
123
|
+
handler.stubs(:trap_deferred)
|
124
|
+
assert_raises(ArgumentError) { handler.send(:setup_signal_handlers) }
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
|
129
|
+
context "#reset_signal_handlers" do
|
130
|
+
|
131
|
+
should "reset signals" do
|
132
|
+
signals = {"int" => "foo", "term" => "bar"}
|
133
|
+
handler = SignalHandler.new(signals, Target.new)
|
134
|
+
|
135
|
+
handler.stubs(:trap_deferred)
|
136
|
+
handler.send(:setup_signal_handlers)
|
137
|
+
|
138
|
+
handler.expects(:trap).with("SIGINT", nil)
|
139
|
+
handler.expects(:trap).with("SIGTERM", nil)
|
140
|
+
handler.expects(:trap).with("SIGCHLD", nil)
|
141
|
+
handler.send(:reset_signal_handlers)
|
142
|
+
assert_empty handler.original_signal_handlers
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
146
|
+
|
147
|
+
context "#handle_signal_queue" do
|
148
|
+
|
149
|
+
setup do
|
150
|
+
signals = {"int" => "foo", "term" => "bar"}
|
151
|
+
@target = Target.new
|
152
|
+
@handler = SignalHandler.new(signals, @target)
|
153
|
+
end
|
154
|
+
|
155
|
+
should "do nothing when queue empty" do
|
156
|
+
assert_empty @handler.signal_queue
|
157
|
+
assert_nil @handler.send(:handle_signal_queue)
|
158
|
+
assert_empty @target.received
|
159
|
+
end
|
160
|
+
|
161
|
+
should "do nothing if signal unknown" do
|
162
|
+
@handler.signal_queue << "SIGUSR1"
|
163
|
+
assert_nil @handler.send(:handle_signal_queue)
|
164
|
+
assert_empty @target.received
|
165
|
+
end
|
166
|
+
|
167
|
+
should "call target for known signal" do
|
168
|
+
@handler.signal_queue << "SIGINT"
|
169
|
+
assert_equal :noaction, @handler.send(:handle_signal_queue)
|
170
|
+
assert_equal [[:foo, [], nil]], @target.received
|
171
|
+
end
|
172
|
+
|
173
|
+
end
|
174
|
+
|
175
|
+
context "#snooze/wakeup" do
|
176
|
+
|
177
|
+
should "block until woken" do
|
178
|
+
handler = SignalHandler.new({}, Target.new)
|
179
|
+
handler.send(:setup_self_pipe)
|
180
|
+
thread = Thread.new { handler.snooze }
|
181
|
+
sleep 0.1
|
182
|
+
assert thread.alive?
|
183
|
+
handler.wakeup
|
184
|
+
sleep 0.1
|
185
|
+
refute thread.alive?
|
186
|
+
assert_equal ".", thread.value
|
187
|
+
end
|
188
|
+
|
189
|
+
end
|
190
|
+
|
191
|
+
context "#start/stop" do
|
192
|
+
|
193
|
+
should "block until woken" do
|
194
|
+
handler = SignalHandler.new({}, Target.new)
|
195
|
+
handler.stubs(:trap)
|
196
|
+
thread = Thread.new { handler.start }
|
197
|
+
sleep 0.1
|
198
|
+
assert thread.alive?
|
199
|
+
handler.stop
|
200
|
+
sleep 0.1
|
201
|
+
refute thread.alive?
|
202
|
+
end
|
203
|
+
|
204
|
+
end
|
205
|
+
|
206
|
+
context ".start" do
|
207
|
+
|
208
|
+
should "fail if already instantiated" do
|
209
|
+
SignalHandler.instance = SignalHandler.new({}, Target.new)
|
210
|
+
assert_raises(RuntimeError) { SignalHandler.start({}, Target.new) }
|
211
|
+
end
|
212
|
+
|
213
|
+
end
|
214
|
+
|
215
|
+
context ".stop" do
|
216
|
+
|
217
|
+
should "fail if not instantiated" do
|
218
|
+
SignalHandler.instance = nil
|
219
|
+
assert_raises(RuntimeError) { SignalHandler.stop }
|
220
|
+
end
|
221
|
+
|
222
|
+
end
|
223
|
+
|
224
|
+
context "api" do
|
225
|
+
|
226
|
+
should "react to signals" do
|
227
|
+
fc = ForkChild.new do
|
228
|
+
target = Target.new
|
229
|
+
SignalHandler.start({"int" => "foo", "term" => "bar", "usr1" => "action_break"}, target)
|
230
|
+
target.received
|
231
|
+
end
|
232
|
+
|
233
|
+
sleep 0.1
|
234
|
+
Process.kill("TERM", fc.pid)
|
235
|
+
sleep 0.1
|
236
|
+
Process.kill("INT", fc.pid)
|
237
|
+
sleep 0.1
|
238
|
+
Process.kill("USR1", fc.pid)
|
239
|
+
|
240
|
+
received = fc.wait
|
241
|
+
assert_equal [[:bar, [], nil], [:foo, [], nil], [:action_break, [], nil]], received
|
242
|
+
end
|
243
|
+
|
244
|
+
should "honor signal queue limit" do
|
245
|
+
fc = ForkChild.new do
|
246
|
+
$stderr.reopen("/dev/null")
|
247
|
+
target = Target.new
|
248
|
+
SignalHandler.start({"int" => "foo", "term" => "bar", "usr1" => "action_break"}, target)
|
249
|
+
target.received
|
250
|
+
end
|
251
|
+
sleep 0.1
|
252
|
+
|
253
|
+
50.times { Process.kill("TERM", fc.pid) }
|
254
|
+
sleep 0.2
|
255
|
+
Process.kill("USR1", fc.pid)
|
256
|
+
received = fc.wait
|
257
|
+
assert received.size > 0
|
258
|
+
assert received.size < 50
|
259
|
+
end
|
260
|
+
|
261
|
+
end
|
262
|
+
end
|