parallel_server 0.1.5.1 → 0.1.6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 955a693a92fa37abfe15c9c4c42ab28498a7786b
4
- data.tar.gz: 3c951d81ff0e66d70ceb1a65984b3c0b2b32fd8d
3
+ metadata.gz: 2e68bf1d6ac3906be2d5cc6fd0a4cf06fd2f6a77
4
+ data.tar.gz: d6b0f09154ec046e08353bdf7660941e7395a308
5
5
  SHA512:
6
- metadata.gz: c1409e37acbcc93c652d4845c67dfbe6b485545c7ce96ac4130b2439980ca38e9e57966b05ab1593b6775819043ff00891dac8b6ec3198be523ff15f03da26c1
7
- data.tar.gz: cadb59c70931be89fc63b82e093614125e1f962412b48278177f0d534557e856b8ed5a84d9f952faa9525add269117c546f653d51b7e6f3054891adc2bd54ae2
6
+ metadata.gz: 97eb5ecec4518b6c0f20e0c3d13f93610ae475f53731d63d5397aa5d36bacd973009980c69614036409b6a2efadbf455fbf2b88658fef75fb23670dd5a6fc509
7
+ data.tar.gz: a83190db61bfad2f2cac203851fb5022ec0c83dced8d50798eeb368dfb24c36fa37bb39c96fb808d68519cabb6d96c5a88f0b97ac182d7c9b41ccdf12f666429
data/README.md CHANGED
@@ -68,6 +68,13 @@ max_idle 経過後でもクライアントと接続中であればプロセス
68
68
  クライアントがこの回数の接続を受け付けるとプロセスを終了します(デフォルト: 1000)。
69
69
  max_use 経過後でもクライアントと接続中であればプロセスは終了しません。ただし `:min_processes`, `:max_processes` のカウント対象外です。
70
70
 
71
+ `:watchdog_timer` :
72
+ 子プロセスからのハートビートがこの秒数以上途切れると該当子プロセスに SIGTERM を送ります(デフォルト: 600)。
73
+ その後も子プロセスが死なない場合は60秒後に SIGKILL を送ります。
74
+
75
+ `:watchdog_signal` :
76
+ `:watchdog_timer` が切れた時に子プロセスに送るシグナルを文字列で指定します(デフォルト: `TERM`)。
77
+
71
78
  `:on_start` :
72
79
  子プロセス起動時に*子プロセス側*で実行される処理を Proc で指定します。Proc 実行時の引数はありません。
73
80
 
@@ -107,3 +114,9 @@ start を終了します。クライアントと接続中の子プロセスは
107
114
  * `stop!`
108
115
 
109
116
  start を終了します。子プロセスがクライアントと接続中でも SIGTERM で終了させます。
117
+
118
+ ### #detach_children
119
+
120
+ * `detach_children`
121
+
122
+ 子プロセスを切り離します。子プロセスは待ち受けポートを閉じ、接続中のクライアントがいなくなると終了します。
@@ -9,6 +9,8 @@ module ParallelServer
9
9
  DEFAULT_STANDBY_THREADS = 5
10
10
  DEFAULT_MAX_IDLE = 10
11
11
  DEFAULT_MAX_USE = 1000
12
+ DEFAULT_WATCHDOG_TIMER = 600
13
+ DEFAULT_WATCHDOG_SIGNAL = 'TERM'
12
14
 
13
15
  attr_reader :child_status
14
16
 
@@ -21,6 +23,8 @@ module ParallelServer
21
23
  # @option opts [Integer] :max_threads (1) maximum threads per process
22
24
  # @option opts [Integer] :standby_threads (5) keep free processes or threads
23
25
  # @option opts [Integer] :listen_backlog (nil) listen backlog
26
+ # @option opts [Integer] :watchdog_timer (600) watchdog timer
27
+ # @option opts [Integer] :watchdog_signal ('TERM') this signal is sent when watchdog timer expired.
24
28
  # @option opts [#call] :on_start (nil) object#call() is invoked when child process start. This is called in child process.
25
29
  # @option opts [#call] :on_reload (nil) object#call(hash) is invoked when reload. This is called in child process.
26
30
  # @option opts [#call] :on_child_start (nil) object#call(pid) is invoked when child process exit. This is call in parent process.
@@ -56,8 +60,7 @@ module ParallelServer
56
60
  raise 'block required' unless block
57
61
  @block = block
58
62
  unless @sockets
59
- @sockets = Socket.tcp_server_sockets(@host, @port)
60
- @sockets.each{|s| s.listen(@listen_backlog)} if @listen_backlog
63
+ @sockets = create_server_socket(@host, @port, @listen_backlog)
61
64
  @sockets_created = true
62
65
  end
63
66
  @reload_args = nil
@@ -99,6 +102,15 @@ module ParallelServer
99
102
  @loop = false
100
103
  end
101
104
 
105
+ # @return [void]
106
+ def detach_children
107
+ t = Time.now + 5
108
+ talk_to_children(detach: true)
109
+ while Time.now < t && @child_status.values.any?{|s| s[:status] == :run}
110
+ watch_children
111
+ end
112
+ end
113
+
102
114
  private
103
115
 
104
116
  # @return [void]
@@ -108,30 +120,43 @@ module ParallelServer
108
120
  old_listen_backlog = @listen_backlog
109
121
  set_variables_from_opts
110
122
 
111
- address_changed = false
112
123
  if @sockets_created ? (@host != host || @port != port) : @sockets != sockets
113
124
  @sockets.each{|s| s.close rescue nil} if @sockets_created
125
+ detach_children
114
126
  @sockets, @host, @port = sockets, host, port
115
127
  if @sockets
116
128
  @sockets_created = false
117
129
  else
118
- @sockets = Socket.tcp_server_sockets(@host, @port)
119
- @sockets.each{|s| s.listen(@listen_backlog)} if @listen_backlog
130
+ @sockets = create_server_socket(@host, @port, @listen_backlog)
120
131
  @sockets_created = true
121
132
  end
122
- address_changed = true
123
133
  elsif @listen_backlog != old_listen_backlog
124
134
  @sockets.each{|s| s.listen(@listen_backlog)} if @listen_backlog && @sockets_created
125
135
  end
126
136
 
127
- reload_children(address_changed)
137
+ reload_children
138
+ end
139
+
140
+ # @param host [String] hostname or IP address
141
+ # @param port [Integer / String] port number / service name
142
+ # @param backlog [Integer / nil] listen backlog
143
+ # @return [Array<Socket>] listening sockets
144
+ def create_server_socket(host, port, backlog)
145
+ t = Time.now + 5
146
+ begin
147
+ sockets = Socket.tcp_server_sockets(host, port)
148
+ rescue Errno::EADDRINUSE
149
+ raise if Time.now > t
150
+ sleep 0.1
151
+ retry
152
+ end
153
+ sockets.each{|s| s.listen(backlog)} if backlog
154
+ sockets
128
155
  end
129
156
 
130
- # @param address_changed [true/false]
131
157
  # @return [void]
132
- def reload_children(address_changed=false)
158
+ def reload_children
133
159
  data = {}
134
- data[:address_changed] = address_changed
135
160
  data[:options] = @opts.select{|_, value| Marshal.dump(value) rescue nil}
136
161
  talk_to_children data
137
162
  end
@@ -204,6 +229,7 @@ module ParallelServer
204
229
  if readable
205
230
  readable.each do |from_child|
206
231
  if st = Conversation.recv(from_child)
232
+ st[:time] = Time.now
207
233
  @child_status[from_child].update st
208
234
  if st[:status] == :stop
209
235
  @to_child[from_child].close rescue nil
@@ -218,6 +244,7 @@ module ParallelServer
218
244
  end
219
245
  end
220
246
  end
247
+ kill_frozen_children
221
248
  if @children.size != @child_status.size
222
249
  wait_children
223
250
  end
@@ -250,6 +277,19 @@ module ParallelServer
250
277
  @child_status.values.count{|st| st[:status] == :run}
251
278
  end
252
279
 
280
+ # @return [void]
281
+ def kill_frozen_children
282
+ now = Time.now
283
+ @child_status.each do |r, st|
284
+ if now > st[:time] + @watchdog_timer + 60
285
+ Process.kill 'KILL', @from_child[r]
286
+ elsif now > st[:time] + @watchdog_timer && ! st[:signal_sent]
287
+ Process.kill @watchdog_signal, @from_child[r]
288
+ st[:signal_sent] = true
289
+ end
290
+ end
291
+ end
292
+
253
293
  # @return [void]
254
294
  def wait_children
255
295
  @children.delete_if do |pid|
@@ -284,7 +324,7 @@ module ParallelServer
284
324
  r, w = from_child[0], to_child[1]
285
325
  @from_child[r] = pid
286
326
  @to_child[r] = w
287
- @child_status[r] = {status: :run, connections: {}}
327
+ @child_status[r] = {status: :run, connections: {}, time: Time.now}
288
328
  @children.push pid
289
329
  @on_child_start.call(pid) if @on_child_start
290
330
  end
@@ -295,6 +335,8 @@ module ParallelServer
295
335
  @max_threads = @opts[:max_threads] || DEFAULT_MAX_THREADS
296
336
  @standby_threads = @opts[:standby_threads] || DEFAULT_STANDBY_THREADS
297
337
  @listen_backlog = @opts[:listen_backlog]
338
+ @watchdog_timer = @opts[:watchdog_timer] || DEFAULT_WATCHDOG_TIMER
339
+ @watchdog_signal = @opts[:watchdog_signal] || DEFAULT_WATCHDOG_SIGNAL
298
340
  @on_start = @opts[:on_start]
299
341
  @on_child_start = @opts[:on_child_start]
300
342
  @on_child_exit = @opts[:on_child_exit]
@@ -373,6 +415,9 @@ module ParallelServer
373
415
  count += 1
374
416
  break if max_use > 0 && count >= max_use
375
417
  end
418
+ rescue => e
419
+ STDERR.puts e.inspect, e.backtrace.inspect
420
+ raise e
376
421
  ensure
377
422
  @status = :stop
378
423
  queue.push true
@@ -389,16 +434,29 @@ module ParallelServer
389
434
  # @param queue [Queue]
390
435
  # @return [void]
391
436
  def reload_loop(queue)
437
+ heartbeat_interval = 5
392
438
  while true
393
- data = Conversation.recv(@from_parent)
394
- break unless data
395
- break if data[:address_changed]
396
- @options.update data[:options]
397
- @options[:on_reload].call @options if @options[:on_reload]
398
- @threads_cv.signal
439
+ time = Time.now
440
+ if IO.select([@from_parent], nil, nil, heartbeat_interval)
441
+ heartbeat_interval -= Time.now - time
442
+ heartbeat_interval = 0 if heartbeat_interval < 0
443
+ data = Conversation.recv(@from_parent)
444
+ break if data.nil? or data[:detach]
445
+ @options.update data[:options] if data[:options]
446
+ @options[:on_reload].call @options if @options[:on_reload]
447
+ @threads_cv.signal
448
+ else
449
+ heartbeat_interval = 5
450
+ @threads_mutex.synchronize do
451
+ Conversation.send(@to_parent, {})
452
+ end
453
+ end
399
454
  end
400
455
  @from_parent.close
401
456
  @from_parent = nil
457
+ rescue => e
458
+ STDERR.puts e.inspect, e.backtrace.inspect
459
+ raise e
402
460
  ensure
403
461
  @status = :stop
404
462
  queue.push true
@@ -0,0 +1,392 @@
1
+ require 'test/unit'
2
+ require 'test/unit/notify'
3
+ require 'socket'
4
+ require 'timeout'
5
+ require 'tempfile'
6
+
7
+ require 'parallel_server/prefork'
8
+
9
+ class TestParallelServerPrefork < Test::Unit::TestCase
10
+ def setup
11
+ @tmpf = Tempfile.new('test_prefork')
12
+ @prefork = ParallelServer::Prefork.new(*args, opts)
13
+ @thr = Thread.new do
14
+ begin
15
+ @prefork.start do |sock|
16
+ begin
17
+ sock.puts $$.to_s
18
+ sock.gets
19
+ rescue Errno::EPIPE
20
+ end
21
+ end
22
+ rescue
23
+ p $!, $@
24
+ raise
25
+ end
26
+ end
27
+ sleep 1
28
+ @clients = []
29
+ end
30
+
31
+ def teardown
32
+ @clients.each do |c|
33
+ c.close rescue nil
34
+ end
35
+ @prefork.stop!
36
+ @thr.join rescue nil
37
+ @tmpf.close true
38
+ end
39
+
40
+ def args
41
+ [port]
42
+ end
43
+
44
+ def port
45
+ ENV['TEST_PORT'] || 12345
46
+ end
47
+
48
+ def connect
49
+ s = TCPSocket.new('localhost', port)
50
+ @clients.push s
51
+ s.gets
52
+ s
53
+ end
54
+
55
+ def opts
56
+ {}
57
+ end
58
+
59
+ sub_test_case 'max_process: 1, max_threads: 1' do
60
+ def opts
61
+ {
62
+ min_processes: 1,
63
+ max_processes: 1,
64
+ max_threads: 1,
65
+ }
66
+ end
67
+
68
+ test '2nd wait for connection' do
69
+ connect
70
+ assert_raise Timeout::Error do
71
+ Timeout.timeout(0.5){ connect }
72
+ end
73
+ end
74
+
75
+ test '2nd connect after 1st connection is closed' do
76
+ connect
77
+ Thread.new{ sleep 0.3; @clients.first.puts }
78
+ Timeout.timeout(1){ connect }
79
+ end
80
+ end
81
+
82
+ sub_test_case 'max_process: 1, max_threads: 3' do
83
+ def opts
84
+ {
85
+ min_processes: 1,
86
+ max_processes: 1,
87
+ max_threads: 3,
88
+ }
89
+ end
90
+
91
+ test '4th request wait for connection' do
92
+ 3.times{ connect }
93
+ assert_raise Timeout::Error do
94
+ Timeout.timeout(0.5){ connect }
95
+ end
96
+ end
97
+
98
+ test '4th connect after 1st connection is closed' do
99
+ 3.times{ connect }
100
+ Thread.new{ sleep 0.3; @clients.first.puts }
101
+ Timeout.timeout(1){ connect }
102
+ end
103
+ end
104
+
105
+ sub_test_case 'max_process: 3, max_threads: 1' do
106
+ def opts
107
+ {
108
+ min_processes: 1,
109
+ max_processes: 3,
110
+ max_threads: 1,
111
+ }
112
+ end
113
+
114
+ test '4th request wait for connection' do
115
+ 3.times{ connect }
116
+ assert_raise Timeout::Error do
117
+ Timeout.timeout(0.5){ connect }
118
+ end
119
+ end
120
+
121
+ test '4th connect after 1st connection is closed' do
122
+ 3.times{ connect }
123
+ Thread.new{ sleep 0.3; @clients.first.puts }
124
+ Timeout.timeout(1){ connect }
125
+ end
126
+ end
127
+
128
+ sub_test_case 'min_processes: 3' do
129
+ def opts
130
+ {
131
+ min_processes: 3,
132
+ standby_threads: 1,
133
+ }
134
+ end
135
+
136
+ test '3 child processes exist' do
137
+ c = Dir.glob('/proc/*/status').count do |stat|
138
+ File.read(stat) =~ /^PPid:\t#{$$}$/
139
+ end
140
+ assert_equal 3, c
141
+ end
142
+ end
143
+
144
+ sub_test_case 'max_idle' do
145
+ def opts
146
+ @children = []
147
+ {
148
+ min_processes: 1,
149
+ max_processes: 1,
150
+ max_idle: 0.1,
151
+ on_child_start: ->(pid){ @children.push pid },
152
+ }
153
+ end
154
+
155
+ test 'unconnected process exists even if max_idle expired' do
156
+ sleep 0.5
157
+ assert File.exist?("/proc/#{@children[0]}")
158
+ assert_equal 1, @children.size
159
+ end
160
+
161
+ test 'first child process exited if it is connected' do
162
+ Process.waitpid fork{ connect.close }
163
+ sleep 0.5
164
+ assert ! File.exist?("/proc/#{@children[0]}")
165
+ assert_equal 2, @children.size
166
+ end
167
+ end
168
+
169
+ sub_test_case 'max_use' do
170
+ def opts
171
+ @children = []
172
+ {
173
+ min_processes: 1,
174
+ max_processes: 1,
175
+ max_use: 2,
176
+ on_child_start: ->(pid){ @children.push pid },
177
+ }
178
+ end
179
+
180
+ test 'child process exited if it is connected max_use times' do
181
+ Process.waitpid fork{ connect.close }
182
+ sleep 0.5
183
+ assert File.exist?("/proc/#{@children[0]}")
184
+ Process.waitpid fork{ connect.close }
185
+ sleep 0.5
186
+ assert ! File.exist?("/proc/#{@children[0]}")
187
+ end
188
+ end
189
+
190
+ sub_test_case 'standby_threads' do
191
+ def opts
192
+ @children = []
193
+ {
194
+ min_processes: 1,
195
+ max_processes: 20,
196
+ max_threads: 2,
197
+ on_child_start: ->(pid){ @children.push pid }
198
+ }
199
+ end
200
+
201
+ sub_test_case 'standby_threads=1' do
202
+ def opts
203
+ {
204
+ standby_threads: 1
205
+ }.merge super
206
+ end
207
+
208
+ test '1 child start' do
209
+ assert_equal 1, @children.size
210
+ end
211
+ end
212
+
213
+ sub_test_case '1 < standby_threads <= max_processes/max_threads' do
214
+ def opts
215
+ {
216
+ standby_threads: 10
217
+ }.merge super
218
+ end
219
+
220
+ test 'n children start' do
221
+ assert_equal 5, @children.size
222
+ end
223
+ end
224
+
225
+ sub_test_case 'standby_threads > max_processes/max_threads' do
226
+ def opts
227
+ {
228
+ standby_threads: 100
229
+ }.merge super
230
+ end
231
+
232
+ test 'max_processes children start' do
233
+ assert_equal 20, @children.size
234
+ end
235
+ end
236
+ end
237
+
238
+ sub_test_case 'on_start' do
239
+ def opts
240
+ {
241
+ min_processes: 1,
242
+ max_processes: 2,
243
+ max_threads: 1,
244
+ standby_threads: 1,
245
+ on_start: ->(){ File.open(@tmpf.path, 'a'){|f| f.puts $$} },
246
+ }
247
+ end
248
+
249
+ test 'execute block in child process when child start' do
250
+ assert_equal 1, File.read(@tmpf.path).lines.count
251
+ connect.close
252
+ sleep 0.5
253
+ assert_equal 2, File.read(@tmpf.path).lines.count
254
+ end
255
+ end
256
+
257
+ sub_test_case 'on_reload' do
258
+ def opts
259
+ {
260
+ min_processes: 5,
261
+ max_processes: 5,
262
+ on_reload: ->(_opts){ File.open(@tmpf.path, 'a'){|f| f.puts $$} },
263
+ }
264
+ end
265
+
266
+ test 'execute block when reload' do
267
+ assert_equal 0, File.read(@tmpf.path).lines.count
268
+ @prefork.reload(12345, opts)
269
+ sleep 0.5
270
+ assert_equal 5, File.read(@tmpf.path).lines.count
271
+ end
272
+ end
273
+
274
+ sub_test_case 'on_child_start' do
275
+ def opts
276
+ @childs = []
277
+ {
278
+ min_processes: 1,
279
+ max_processes: 2,
280
+ max_threads: 1,
281
+ standby_threads: 1,
282
+ on_child_start: ->(pid){ @childs.push pid },
283
+ }
284
+ end
285
+
286
+ test 'execute block when child start' do
287
+ assert_equal 1, @childs.count
288
+ connect.close
289
+ sleep 0.5
290
+ assert_equal 2, @childs.count
291
+ end
292
+ end
293
+
294
+ sub_test_case 'on_child_exit' do
295
+ def opts
296
+ @childs = []
297
+ {
298
+ min_processes: 1,
299
+ max_processes: 1,
300
+ max_idle: 0.001,
301
+ on_child_exit: ->(pid, st){ @childs.push pid; @st = st },
302
+ }
303
+ end
304
+
305
+ test 'execute block when child exit' do
306
+ assert_equal [], @childs
307
+ s = TCPSocket.new('localhost', port)
308
+ pid = s.gets.chomp.to_i
309
+ s.close
310
+ sleep 0.5
311
+ assert_equal [pid], @childs
312
+ assert_equal 0, @st.exitstatus
313
+ end
314
+ end
315
+
316
+ sub_test_case 'each_nonblock' do
317
+ test 'run in single thread unless timeout occur' do
318
+ @prefork.singleton_class.class_eval{public :each_nonblock}
319
+ values = Array.new(100, true)
320
+ result = []
321
+ @prefork.each_nonblock(values, 1) do |x|
322
+ result.push Thread.current
323
+ end
324
+ assert_equal result.size, values.size
325
+ assert_equal result.uniq.size, 1
326
+ end
327
+
328
+ test 'run in multiple thread if timeout occur' do
329
+ @prefork.singleton_class.class_eval{public :each_nonblock}
330
+ values = Array.new(10, true)
331
+ result = []
332
+ @prefork.each_nonblock(values, 0.1) do |x|
333
+ result.push Thread.current
334
+ sleep 0.3
335
+ end
336
+ assert_equal result.size, values.size
337
+ assert result.uniq.size == 10
338
+ end
339
+ end
340
+
341
+ sub_test_case 'new(port)' do
342
+ def args
343
+ [port]
344
+ end
345
+
346
+ test 'connection' do
347
+ Timeout.timeout(1){ connect }
348
+ end
349
+ end
350
+
351
+ sub_test_case 'new(host, port)' do
352
+ def args
353
+ ['localhost', port]
354
+ end
355
+
356
+ test 'connection' do
357
+ Timeout.timeout(1){ connect }
358
+ end
359
+ end
360
+
361
+ sub_test_case 'new(socket)' do
362
+ def args
363
+ @sock = TCPServer.new(port)
364
+ [@sock]
365
+ end
366
+
367
+ def teardown
368
+ @sock.close
369
+ super
370
+ end
371
+
372
+ test 'connection' do
373
+ Timeout.timeout(1){ connect }
374
+ end
375
+ end
376
+
377
+ sub_test_case 'new(sockets)' do
378
+ def args
379
+ @socks = [TCPServer.new(port), TCPServer.new(port+1)]
380
+ [@socks]
381
+ end
382
+
383
+ def teardown
384
+ @socks.each(&:close)
385
+ super
386
+ end
387
+
388
+ test 'connection' do
389
+ Timeout.timeout(1){ connect }
390
+ end
391
+ end
392
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: parallel_server
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5.1
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tomita Masahiro
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-05-19 00:00:00.000000000 Z
11
+ date: 2015-05-31 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Parallel TCP Server library. This is easy to make Multi-Process / Multi-Thread
14
14
  server
@@ -20,6 +20,7 @@ extra_rdoc_files:
20
20
  files:
21
21
  - README.md
22
22
  - lib/parallel_server/prefork.rb
23
+ - test/parallel_server/test_prefork.rb
23
24
  homepage: http://github.com/tmtm/parallel_server
24
25
  licenses:
25
26
  - Ruby's
@@ -40,8 +41,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
40
41
  version: '0'
41
42
  requirements: []
42
43
  rubyforge_project:
43
- rubygems_version: 2.2.2
44
+ rubygems_version: 2.4.5
44
45
  signing_key:
45
46
  specification_version: 4
46
47
  summary: Parallel TCP Server library
47
- test_files: []
48
+ test_files:
49
+ - test/parallel_server/test_prefork.rb
50
+ has_rdoc: true