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.
Files changed (4) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +93 -0
  3. data/lib/parallel_server/prefork.rb +379 -0
  4. 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