parallel_server 0.1
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 +7 -0
- data/README.md +93 -0
- data/lib/parallel_server/prefork.rb +379 -0
- metadata +48 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 3d780167579eef4a691c12ab19a0ace61393a41e
|
4
|
+
data.tar.gz: f6a61cfbe9f26dfb093eef85347a5d64ba706449
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 98a37012be40e65058989a402d01058c4d6af1505d095187881dcddc94d1cceed50d5ea66b594e7131837e75dd182d8fa333c51dde2ff3edab240e10c7439768
|
7
|
+
data.tar.gz: c41df8dbfdac04e5923eb110e8b10231d878c1b97a80b97925480abe95558d23e90a9c63e03b6627967432563fc976838792d46544e6f2dd7367d7de096dcda5
|
data/README.md
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
ParallelServer
|
2
|
+
==============
|
3
|
+
|
4
|
+
ParallelServer は Ruby の並列 TCP/IP サーバーを簡単に作ることが出来るライブラリです。
|
5
|
+
|
6
|
+
ParallelServer::Prefork
|
7
|
+
-----------------------
|
8
|
+
|
9
|
+
あらかじめ処理用のプロセスを生成しておき、接続毎にスレッドを生成して処理を実行します。
|
10
|
+
|
11
|
+
### 例
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
require 'parallel_server/prefork'
|
15
|
+
|
16
|
+
pl = ParallelServer::Prefork.new(12345, max_processes: 100, max_idle: 100)
|
17
|
+
pl.start do |sock, addr|
|
18
|
+
sock.puts 'Who are you?'
|
19
|
+
name = sock.gets
|
20
|
+
sock.puts "Hello, #{name}"
|
21
|
+
end
|
22
|
+
```
|
23
|
+
|
24
|
+
### ParallelServer::Prefork.new
|
25
|
+
|
26
|
+
* `ParallelServer::Prefork.new(port, opts={})`
|
27
|
+
* `ParallelServer::Prefork.new(host, port, opts={})`
|
28
|
+
|
29
|
+
#### host
|
30
|
+
|
31
|
+
待ち受けるIPアドレス。省略時はすべてのIPアドレスで待ち受けます。
|
32
|
+
|
33
|
+
#### port
|
34
|
+
待ち受けるTCPポート番号。
|
35
|
+
|
36
|
+
#### opts
|
37
|
+
|
38
|
+
ParallelServer::Prefork の動作を設定するパラメータ。
|
39
|
+
|
40
|
+
`:min_processes` :
|
41
|
+
最小プロセス数を指定します(デフォルト: 5)。
|
42
|
+
|
43
|
+
`:max_processes` :
|
44
|
+
最大プロセス数を指定します(デフォルト: 20)。
|
45
|
+
|
46
|
+
`:max_threads` :
|
47
|
+
1プロセスあたりの最大スレッド数を指定します(デフォルト: 1)。
|
48
|
+
|
49
|
+
`:standby_threads` :
|
50
|
+
空きスレッド数を指定します(デフォルト: 5)。
|
51
|
+
少なくともこの数だけの接続を受け付けられるようにプロセス数を調整します。
|
52
|
+
|
53
|
+
`:back_log` :
|
54
|
+
待ち受けポートの listen back log を指定します(デフォルト: standby_threads と同じ)。
|
55
|
+
|
56
|
+
`:max_idle` :
|
57
|
+
指定秒数の間、クライアントからの新たな接続がないとプロセスを終了します(デフォルト: 10)。
|
58
|
+
生成後一度も接続されていないプロセスはこのパラメータの影響を受けません。
|
59
|
+
max_idle 経過後でもクライアントと接続中であればプロセスは終了しません。ただし `:min_processes`, `:max_processes` のカウント対象外です。
|
60
|
+
|
61
|
+
`:on_child_start` :
|
62
|
+
子プロセス起動時に*子プロセス側*で実行される処理を Proc で指定します。Proc 実行時の引数はありません。
|
63
|
+
|
64
|
+
`:on_child_exit` :
|
65
|
+
子プロセス終了時に*親プロセス側*で実行される処理を Proc で指定します。Proc 実行時の引数はプロセスID(Integer)と終了ステータス(Process::Status)です。
|
66
|
+
|
67
|
+
### #start
|
68
|
+
|
69
|
+
* `start{|sock, addr| ...}`
|
70
|
+
|
71
|
+
待ち受けを開始します。クライアントから接続する毎にスレッドを生成して、ブロックを実行します。
|
72
|
+
ブロックパラメータは Socket と Addrinfo です。
|
73
|
+
|
74
|
+
### #reload
|
75
|
+
|
76
|
+
* `reload(port, opts={})`
|
77
|
+
* `reload(host, port, opts={})`
|
78
|
+
|
79
|
+
引数の形式は new と同じです。
|
80
|
+
|
81
|
+
start 後にパラメータを変更したい場合に使用します。
|
82
|
+
|
83
|
+
### #stop
|
84
|
+
|
85
|
+
* `stop`
|
86
|
+
|
87
|
+
start を終了します。クライアントと接続中の子プロセスは接続が切断されるまで終了しません。
|
88
|
+
|
89
|
+
### #stop!
|
90
|
+
|
91
|
+
* `stop!`
|
92
|
+
|
93
|
+
start を終了します。子プロセスがクライアントと接続中でも SIGTERM で終了させます。
|
@@ -0,0 +1,379 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'thread'
|
3
|
+
|
4
|
+
module ParallelServer
|
5
|
+
class Prefork
|
6
|
+
DEFAULT_MIN_PROCESSES = 5
|
7
|
+
DEFAULT_MAX_PROCESSES = 20
|
8
|
+
DEFAULT_MAX_THREADS = 1
|
9
|
+
DEFAULT_STANDBY_THREADS = 5
|
10
|
+
DEFAULT_MAX_IDLE = 10
|
11
|
+
|
12
|
+
# @!macro [new] args
|
13
|
+
# @param host [String] hostname or IP address
|
14
|
+
# @param port [Integer / String] port number / service name
|
15
|
+
# @param opts [Hash] options
|
16
|
+
# @option opts [Integer] :min_processes (5) minimum processes
|
17
|
+
# @option opts [Integer] :max_processes (20) maximum processes
|
18
|
+
# @option opts [Integer] :max_idle (10) cihld process exits if max_idle seconds is expired
|
19
|
+
# @option opts [Integer] :max_threads (1) maximum threads per process
|
20
|
+
# @option opts [#call] :on_child_start (nil) object#call() is invoked when child process start. This is called in child process.
|
21
|
+
# @option opts [#call] :on_child_exit (nil) object#call(pid, status) is invoked when child process stop. This is call in parent process.
|
22
|
+
# @option opts [Integer] :standby_threads (5) keep free processes or threads
|
23
|
+
# @option opts [Integer] :back_log (same as standby_threads) listen back log
|
24
|
+
|
25
|
+
# @overload initialize(host=nil, port, opts={})
|
26
|
+
# @!macro args
|
27
|
+
def initialize(*args)
|
28
|
+
host, port, opts = parse_args(*args)
|
29
|
+
@host, @port, @opts = host, port, opts
|
30
|
+
@min_processes = opts[:min_processes] || DEFAULT_MIN_PROCESSES
|
31
|
+
@max_processes = opts[:max_processes] || DEFAULT_MAX_PROCESSES
|
32
|
+
@max_threads = opts[:max_threads] || DEFAULT_MAX_THREADS
|
33
|
+
@on_child_start = opts[:on_child_start]
|
34
|
+
@on_child_exit = opts[:on_child_exit]
|
35
|
+
@standby_threads = opts[:standby_threads] || DEFAULT_STANDBY_THREADS
|
36
|
+
@back_log = opts[:back_log] || @standby_threads
|
37
|
+
@from_child = {} # IO => pid
|
38
|
+
@to_child = {} # pid => IO
|
39
|
+
@child_status = {} # pid => Hash
|
40
|
+
@children = [] # pid
|
41
|
+
end
|
42
|
+
|
43
|
+
# @return [void]
|
44
|
+
# @yield [sock, addr]
|
45
|
+
# @yieldparam sock [Socket]
|
46
|
+
# @yieldparam addr [Addrinfo]
|
47
|
+
def start(&block)
|
48
|
+
raise 'block required' unless block
|
49
|
+
@block = block
|
50
|
+
@loop = true
|
51
|
+
@sockets = Socket.tcp_server_sockets(@host, @port)
|
52
|
+
@sockets.each{|s| s.listen(@back_log)}
|
53
|
+
@reload_args = nil
|
54
|
+
while @loop
|
55
|
+
do_reload if @reload_args
|
56
|
+
watch_children
|
57
|
+
adjust_children
|
58
|
+
end
|
59
|
+
@sockets.each(&:close)
|
60
|
+
@to_child.values.each(&:close)
|
61
|
+
@to_child.clear
|
62
|
+
Thread.new{wait_all_children}
|
63
|
+
end
|
64
|
+
|
65
|
+
# @overload reload(host=nil, port, opts={})
|
66
|
+
# @macro args
|
67
|
+
# @return [void]
|
68
|
+
def reload(*args)
|
69
|
+
@reload_args = parse_args(*args)
|
70
|
+
end
|
71
|
+
|
72
|
+
# @return [void]
|
73
|
+
def do_reload
|
74
|
+
host, port, @opts = @reload_args
|
75
|
+
@reload_args = nil
|
76
|
+
|
77
|
+
@min_processes = @opts[:min_processes] || DEFAULT_MIN_PROCESSES
|
78
|
+
@max_processes = @opts[:max_processes] || DEFAULT_MAX_PROCESSES
|
79
|
+
@max_threads = @opts[:max_threads] || DEFAULT_MAX_THREADS
|
80
|
+
@on_child_start = @opts[:on_child_start]
|
81
|
+
@on_child_exit = @opts[:on_child_exit]
|
82
|
+
@standby_threads = @opts[:standby_threads] || DEFAULT_STANDBY_THREADS
|
83
|
+
@back_log = @opts[:back_log] || @standby_threads
|
84
|
+
|
85
|
+
data = {}
|
86
|
+
if @host != host || @port != port
|
87
|
+
@host, @port = host, port
|
88
|
+
@sockets.each(&:close)
|
89
|
+
@sockets = Socket.tcp_server_sockets(@host, @port)
|
90
|
+
@sockets.each{|s| s.listen(@back_log)}
|
91
|
+
data[:address_changed] = true
|
92
|
+
end
|
93
|
+
data[:opts] = @opts.select{|_, value| Marshal.dump(value) rescue nil}
|
94
|
+
data = Marshal.dump(data)
|
95
|
+
@to_child.values.each do |pipe|
|
96
|
+
talk_to_child pipe, data
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# @param io [IO]
|
101
|
+
# @param data [String]
|
102
|
+
# @return [void]
|
103
|
+
def talk_to_child(io, data)
|
104
|
+
io.puts data.length
|
105
|
+
io.write data
|
106
|
+
rescue Errno::EPIPE
|
107
|
+
# ignore
|
108
|
+
end
|
109
|
+
|
110
|
+
# @return [void]
|
111
|
+
def stop
|
112
|
+
@loop = false
|
113
|
+
end
|
114
|
+
|
115
|
+
# @return [void]
|
116
|
+
def stop!
|
117
|
+
Process.kill 'TERM', *@children rescue nil
|
118
|
+
@loop = false
|
119
|
+
end
|
120
|
+
|
121
|
+
# @overload parse_args(host=nil, port, opts={})
|
122
|
+
# @macro args
|
123
|
+
# @return [Array<String, String, Hash>] hostname, port, option
|
124
|
+
def parse_args(*args)
|
125
|
+
opts = {}
|
126
|
+
arg_count = args.size
|
127
|
+
if args.last.is_a? Hash
|
128
|
+
opts = args.pop
|
129
|
+
end
|
130
|
+
if args.size == 1
|
131
|
+
host, port = nil, args.first
|
132
|
+
elsif args.size == 2
|
133
|
+
host, port = args
|
134
|
+
else
|
135
|
+
raise ArgumentError, "wrong number of arguments (#{arg_count} for 1..3)"
|
136
|
+
end
|
137
|
+
return host, port, opts
|
138
|
+
end
|
139
|
+
|
140
|
+
# @return [Integer]
|
141
|
+
def watch_children
|
142
|
+
rset = @from_child.empty? ? nil : @from_child.keys
|
143
|
+
readable, = IO.select(rset, nil, nil, 0.1)
|
144
|
+
if readable
|
145
|
+
readable.each do |from_child|
|
146
|
+
pid = @from_child[from_child]
|
147
|
+
if st = read_child_status(from_child)
|
148
|
+
@child_status[pid] = st
|
149
|
+
else
|
150
|
+
@from_child.delete from_child
|
151
|
+
@to_child.delete pid
|
152
|
+
@child_status.delete pid
|
153
|
+
from_child.close
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
if @children.size != @child_status.size
|
158
|
+
wait_children
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# @param io [IO]
|
163
|
+
# @return [Hash]
|
164
|
+
def read_child_status(io)
|
165
|
+
len = io.gets
|
166
|
+
return unless len && len =~ /\A\d+\n/
|
167
|
+
len = len.to_i
|
168
|
+
data = io.read(len)
|
169
|
+
return unless data.size == len
|
170
|
+
Marshal.load(data)
|
171
|
+
end
|
172
|
+
|
173
|
+
# @return [void]
|
174
|
+
def adjust_children
|
175
|
+
(@min_processes - available_children).times do
|
176
|
+
start_child
|
177
|
+
end
|
178
|
+
capa, conn = current_capacity_and_connections
|
179
|
+
required_connections = conn + @standby_threads
|
180
|
+
required_processes = (required_connections - capa + @max_threads - 1) / @max_threads
|
181
|
+
[required_processes, @max_processes - available_children].min.times do
|
182
|
+
start_child
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
# current capacity and current connections
|
187
|
+
# @return [Array<Integer, Integer>]
|
188
|
+
def current_capacity_and_connections
|
189
|
+
values = @child_status.values
|
190
|
+
capa = values.map{|st| st[:capacity]}.reduce(&:+).to_i
|
191
|
+
conn = values.map{|st| st[:running]}.reduce(&:+).to_i
|
192
|
+
return [capa, conn]
|
193
|
+
end
|
194
|
+
|
195
|
+
# @return [Integer]
|
196
|
+
def available_children
|
197
|
+
@child_status.values.select{|st| st[:capacity] > 0}.size
|
198
|
+
end
|
199
|
+
|
200
|
+
# @return [void]
|
201
|
+
def wait_children
|
202
|
+
@children.delete_if do |pid|
|
203
|
+
_pid, status = Process.waitpid2(pid, Process::WNOHANG)
|
204
|
+
@on_child_exit.call(pid, status) if _pid && @on_child_exit
|
205
|
+
_pid
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
# @return [void]
|
210
|
+
def wait_all_children
|
211
|
+
until @children.empty?
|
212
|
+
watch_children
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
# @return [void]
|
217
|
+
def start_child
|
218
|
+
from_child = IO.pipe
|
219
|
+
to_child = IO.pipe
|
220
|
+
pid = fork do
|
221
|
+
@from_child.keys.each(&:close)
|
222
|
+
@to_child.values.each(&:close)
|
223
|
+
from_child[0].close
|
224
|
+
to_child[1].close
|
225
|
+
@on_child_start.call if @on_child_start
|
226
|
+
Child.new(@sockets, @opts, from_child[1], to_child[0]).start(@block)
|
227
|
+
end
|
228
|
+
from_child[1].close
|
229
|
+
to_child[0].close
|
230
|
+
@from_child[from_child[0]] = pid
|
231
|
+
@to_child[pid] = to_child[1]
|
232
|
+
@children.push pid
|
233
|
+
@child_status[pid] = {capacity: @max_threads, running: 0}
|
234
|
+
end
|
235
|
+
|
236
|
+
class Child
|
237
|
+
# @param sockets [Array<Socket>]
|
238
|
+
# @param opts [Hash]
|
239
|
+
# @param to_parent [IO]
|
240
|
+
# @param from_parent [IO]
|
241
|
+
def initialize(sockets, opts, to_parent, from_parent)
|
242
|
+
@sockets = sockets
|
243
|
+
@opts = opts
|
244
|
+
@to_parent = to_parent
|
245
|
+
@from_parent = from_parent
|
246
|
+
@threads = {}
|
247
|
+
@threads_mutex = Mutex.new
|
248
|
+
@threads_cv = ConditionVariable.new
|
249
|
+
@status = :run
|
250
|
+
end
|
251
|
+
|
252
|
+
# @return [Integer]
|
253
|
+
def max_threads
|
254
|
+
@opts[:max_threads] || DEFAULT_MAX_THREADS
|
255
|
+
end
|
256
|
+
|
257
|
+
# @return [Integer]
|
258
|
+
def max_idle
|
259
|
+
@opts[:max_idle] || DEFAULT_MAX_IDLE
|
260
|
+
end
|
261
|
+
|
262
|
+
# @param block [#call]
|
263
|
+
# @return [void]
|
264
|
+
def start(block)
|
265
|
+
first = true
|
266
|
+
while @status == :run
|
267
|
+
wait_thread
|
268
|
+
sock, addr = accept(first)
|
269
|
+
break unless sock
|
270
|
+
first = false
|
271
|
+
thr = Thread.new(sock, addr){|s, a| run(s, a, block)}
|
272
|
+
connected(thr)
|
273
|
+
end
|
274
|
+
@sockets.each(&:close)
|
275
|
+
@threads_mutex.synchronize do
|
276
|
+
notice_status
|
277
|
+
end
|
278
|
+
wait_all_connections
|
279
|
+
end
|
280
|
+
|
281
|
+
# @return [void]
|
282
|
+
def wait_all_connections
|
283
|
+
@threads.keys.each do |thr|
|
284
|
+
thr.join rescue nil
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
# @return [void]
|
289
|
+
def reload
|
290
|
+
len = @from_parent.gets
|
291
|
+
raise unless len && len =~ /\A\d+\n/
|
292
|
+
len = len.to_i
|
293
|
+
data = @from_parent.read(len)
|
294
|
+
raise unless data.size == len
|
295
|
+
data = Marshal.load(data)
|
296
|
+
raise if data[:address_changed]
|
297
|
+
@opts.update data[:opts]
|
298
|
+
rescue
|
299
|
+
@status = :stop
|
300
|
+
end
|
301
|
+
|
302
|
+
# @return [void]
|
303
|
+
def wait_thread
|
304
|
+
@threads_mutex.synchronize do
|
305
|
+
while @threads.size >= max_threads
|
306
|
+
@threads_cv.wait(@threads_mutex)
|
307
|
+
end
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
# @param thread [Thread]
|
312
|
+
# @return [void]
|
313
|
+
def connected(thread)
|
314
|
+
@threads_mutex.synchronize do
|
315
|
+
@threads[thread] = true
|
316
|
+
notice_status
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
# @return [void]
|
321
|
+
def disconnect
|
322
|
+
@threads_mutex.synchronize do
|
323
|
+
@threads.delete Thread.current
|
324
|
+
notice_status
|
325
|
+
@threads_cv.signal
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
# @return [void]
|
330
|
+
def notice_status
|
331
|
+
status = {
|
332
|
+
running: @threads.size,
|
333
|
+
capacity: @status == :run ? max_threads : 0,
|
334
|
+
}
|
335
|
+
data = Marshal.dump(status)
|
336
|
+
@to_parent.puts data.length
|
337
|
+
@to_parent.write data
|
338
|
+
rescue Errno::EPIPE
|
339
|
+
# ignore
|
340
|
+
end
|
341
|
+
|
342
|
+
# @param sock [Socket]
|
343
|
+
# @param addr [AddrInfo]
|
344
|
+
# @param block [#call]
|
345
|
+
# @return [void]
|
346
|
+
def run(sock, addr, block)
|
347
|
+
block.call(sock, addr)
|
348
|
+
rescue Exception => e
|
349
|
+
STDERR.puts e.inspect, e.backtrace.inspect
|
350
|
+
ensure
|
351
|
+
sock.close rescue nil
|
352
|
+
disconnect
|
353
|
+
end
|
354
|
+
|
355
|
+
# @param first [Boolean]
|
356
|
+
# @return [Array<Socket, AddrInfo>]
|
357
|
+
# @return [nil]
|
358
|
+
def accept(first=nil)
|
359
|
+
while true
|
360
|
+
timer = first ? nil : max_idle
|
361
|
+
readable, = IO.select(@sockets+[@from_parent], nil, nil, timer)
|
362
|
+
return nil unless readable
|
363
|
+
r, = readable
|
364
|
+
if r == @from_parent
|
365
|
+
reload
|
366
|
+
next if @status == :run
|
367
|
+
return nil
|
368
|
+
end
|
369
|
+
begin
|
370
|
+
sock, addr = r.accept_nonblock
|
371
|
+
return [sock, addr]
|
372
|
+
rescue IO::WaitReadable
|
373
|
+
next
|
374
|
+
end
|
375
|
+
end
|
376
|
+
end
|
377
|
+
end
|
378
|
+
end
|
379
|
+
end
|
metadata
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: parallel_server
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.1'
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Tomita Masahiro
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-07-30 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Parallel TCP Server library. This is easy to make Multi-Process / Multi-Thread
|
14
|
+
server
|
15
|
+
email: tommy@tmtm.org
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files:
|
19
|
+
- README.md
|
20
|
+
files:
|
21
|
+
- README.md
|
22
|
+
- lib/parallel_server/prefork.rb
|
23
|
+
homepage: http://github.com/tmtm/parallel_server
|
24
|
+
licenses:
|
25
|
+
- Ruby's
|
26
|
+
metadata: {}
|
27
|
+
post_install_message:
|
28
|
+
rdoc_options: []
|
29
|
+
require_paths:
|
30
|
+
- lib
|
31
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
32
|
+
requirements:
|
33
|
+
- - ">="
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: '0'
|
36
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
requirements: []
|
42
|
+
rubyforge_project:
|
43
|
+
rubygems_version: 2.2.2
|
44
|
+
signing_key:
|
45
|
+
specification_version: 4
|
46
|
+
summary: Parallel TCP Server library
|
47
|
+
test_files: []
|
48
|
+
has_rdoc: true
|