serverengine 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,61 @@
1
+ #
2
+ # ServerEngine
3
+ #
4
+ # Copyright (C) 2012-2013 FURUHASHI Sadayuki
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+ module ServerEngine
19
+
20
+ class MultiThreadServer < MultiWorkerServer
21
+ private
22
+
23
+ def start_worker(wid)
24
+ w = create_worker(wid)
25
+
26
+ w.before_fork
27
+ begin
28
+ thread = Thread.new(&w.method(:main))
29
+ ensure
30
+ w.close
31
+ end
32
+
33
+ return WorkerMonitor.new(w, thread)
34
+ end
35
+
36
+ class WorkerMonitor
37
+ def initialize(worker, thread)
38
+ @worker = worker
39
+ @thread = thread
40
+ end
41
+
42
+ def send_stop(stop_graceful)
43
+ Thread.new { @worker.stop }
44
+ nil
45
+ end
46
+
47
+ def send_reload
48
+ Thread.new { @worker.reload }
49
+ end
50
+
51
+ #def join
52
+ # @thread.join
53
+ #end
54
+
55
+ def alive?
56
+ @thread.alive?
57
+ end
58
+ end
59
+ end
60
+
61
+ end
@@ -0,0 +1,130 @@
1
+ #
2
+ # ServerEngine
3
+ #
4
+ # Copyright (C) 2012-2013 FURUHASHI Sadayuki
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+ module ServerEngine
19
+
20
+ class MultiWorkerServer < Server
21
+ def initialize(worker_module, load_config_proc={}, &block)
22
+ @monitors = []
23
+ @last_start_worker_time = 0
24
+
25
+ super(worker_module, load_config_proc, &block)
26
+ end
27
+
28
+ def stop(stop_graceful)
29
+ super
30
+ @monitors.each do |m|
31
+ m.send_stop(stop_graceful) if m
32
+ end
33
+ nil
34
+ end
35
+
36
+ def restart(stop_graceful)
37
+ super
38
+ @monitors.each do |m|
39
+ m.send_stop(stop_graceful) if m
40
+ end
41
+ nil
42
+ end
43
+
44
+ def reload
45
+ super
46
+ @monitors.each_with_index do |m|
47
+ m.send_reload if m
48
+ end
49
+ nil
50
+ end
51
+
52
+ def run
53
+ while true
54
+ num_alive = keepalive_workers
55
+ break if num_alive == 0
56
+ wait_tick
57
+ end
58
+ end
59
+
60
+ def scale_workers(n)
61
+ @num_workers = n
62
+
63
+ plus = n - @monitors.size
64
+ if plus > 0
65
+ @monitors.concat Array.new(plus, nil)
66
+ end
67
+
68
+ nil
69
+ end
70
+
71
+ private
72
+
73
+ def reload_config
74
+ super
75
+
76
+ @start_worker_delay = @config[:start_worker_delay] || 0
77
+ @start_worker_delay_rand = @config[:start_worker_delay_rand] || 0.2
78
+
79
+ scale_workers(@config[:workers] || 1)
80
+
81
+ nil
82
+ end
83
+
84
+ def wait_tick
85
+ sleep 0.5
86
+ end
87
+
88
+ def keepalive_workers
89
+ num_alive = 0
90
+
91
+ @monitors.each_with_index do |m,wid|
92
+ if m && m.alive?
93
+ # alive
94
+ num_alive += 1
95
+
96
+ elsif wid < @num_workers
97
+ # scale up or reboot
98
+ unless @stop
99
+ @monitors[wid] = delayed_start_worker(wid)
100
+ num_alive += 1
101
+ end
102
+
103
+ elsif m
104
+ # scale down
105
+ @monitors[wid] = nil
106
+ end
107
+ end
108
+
109
+ return num_alive
110
+ end
111
+
112
+ def delayed_start_worker(wid)
113
+ if @start_worker_delay > 0
114
+ delay = @start_worker_delay +
115
+ Kernel.rand * @start_worker_delay * @start_worker_delay_rand -
116
+ @start_worker_delay * @start_worker_delay_rand / 2
117
+
118
+ now = Time.now.to_f
119
+
120
+ wait = delay - (now - @last_start_worker_time)
121
+ sleep wait if wait > 0
122
+
123
+ @last_start_worker_time = now
124
+ end
125
+
126
+ start_worker(wid)
127
+ end
128
+ end
129
+
130
+ end
@@ -0,0 +1,415 @@
1
+ #
2
+ # ServerEngine
3
+ #
4
+ # Copyright (C) 2012-2013 FURUHASHI Sadayuki
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+ module ServerEngine
19
+
20
+ require 'fcntl'
21
+
22
+ class ProcessManager
23
+ def initialize(config={})
24
+ @monitors = []
25
+ @rpipes = {}
26
+ @heartbeat_time = {}
27
+
28
+ @cloexec_mode = config[:cloexec_mode]
29
+
30
+ @graceful_kill_signal = config[:graceful_kill_signal] || :TERM
31
+ @immediate_kill_signal = config[:immediate_kill_signal] || :QUIT
32
+
33
+ @auto_tick = config.fetch(:auto_tick, true)
34
+ @tick_interval = config[:tick_interval] || 1
35
+
36
+ @auto_heartbeat = config.fetch(:auto_heartbeat, true)
37
+
38
+ case op = config.fetch(:abort_on_heartbeat_error, true)
39
+ when Proc
40
+ @heartbeat_error_proc = op
41
+ when true
42
+ @heartbeat_error_proc = lambda {|t| exit 1 }
43
+ when false
44
+ @heartbeat_error_proc = lambda {|t| }
45
+ else
46
+ raise ArgumentError, "unexpected :abort_on_heartbeat_error option (expected Proc, true or false but got #{op.class})"
47
+ end
48
+
49
+ configure(config)
50
+
51
+ @closed = false
52
+ @read_buffer = ''
53
+
54
+ if @auto_tick
55
+ TickThread.new(self)
56
+ end
57
+ end
58
+
59
+ attr_accessor :logger
60
+
61
+ attr_accessor :cloexec_mode
62
+
63
+ CONFIG_PARAMS = {
64
+ heartbeat_interval: 1,
65
+ heartbeat_timeout: 60,
66
+ graceful_kill_interval: 2,
67
+ graceful_kill_interval_increment: 2,
68
+ graceful_kill_timeout: -1,
69
+ immediate_kill_interval: 2,
70
+ immediate_kill_interval_increment: 2,
71
+ immediate_kill_timeout: 60,
72
+ }
73
+
74
+ attr_reader :graceful_kill_signal, :immediate_kill_signal
75
+
76
+ CONFIG_PARAMS.each_pair do |key,default_value|
77
+ attr_reader key
78
+
79
+ define_method("#{key}=") do |v|
80
+ v = default_value if v == nil
81
+ instance_variable_set("@#{key}", v)
82
+ end
83
+ end
84
+
85
+ def configure(config, opts={})
86
+ prefix = opts[:prefix] || ""
87
+ CONFIG_PARAMS.keys.each {|key|
88
+ send("#{key}=", config[:"#{prefix}#{key}"])
89
+ }
90
+ end
91
+
92
+ def fork(&block)
93
+ rpipe, wpipe = new_pair
94
+
95
+ begin
96
+ pid = Process.fork do
97
+ self.close
98
+ begin
99
+ t = Target.new(wpipe)
100
+ if @auto_heartbeat
101
+ HeartbeatThread.new(self, t, @heartbeat_error_proc)
102
+ end
103
+
104
+ block.call(t)
105
+ exit! 0
106
+
107
+ rescue
108
+ ServerEngine.dump_uncaught_error($!)
109
+ ensure
110
+ exit! 1
111
+ end
112
+ end
113
+
114
+ m = Monitor.new(self, pid)
115
+
116
+ @monitors << m
117
+ @rpipes[rpipe] = m
118
+
119
+ return m
120
+
121
+ ensure
122
+ wpipe.close
123
+ end
124
+ end
125
+
126
+ def new_pair
127
+ rpipe, wpipe = IO.pipe
128
+
129
+ case @cloexec_mode
130
+ when :target_only
131
+ wpipe.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
132
+ when :monitor_only
133
+ rpipe.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
134
+ else
135
+ rpipe.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
136
+ wpipe.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
137
+ end
138
+
139
+ rpipe.sync = true
140
+ wpipe.sync = true
141
+
142
+ return rpipe, wpipe
143
+ end
144
+
145
+ def close
146
+ @closed = true
147
+ @rpipes.keys.each {|m| m.close }
148
+ nil
149
+ end
150
+
151
+ def tick(blocking_timeout=0)
152
+ if @closed
153
+ raise AlreadyClosedError.new
154
+ end
155
+
156
+ if @rpipes.empty?
157
+ sleep blocking_timeout if blocking_timeout > 0
158
+ return nil
159
+ end
160
+
161
+ ready_pipes, _, _ = IO.select(@rpipes.keys, nil, nil, blocking_timeout)
162
+ unless ready_pipes
163
+ return nil
164
+ end
165
+
166
+ time ||= Time.now
167
+
168
+ ready_pipes.each do |r|
169
+ begin
170
+ r.read_nonblock(1024, @read_buffer)
171
+ rescue Errno::EAGAIN, Errno::EINTR
172
+ next
173
+ rescue #EOFError
174
+ m = @rpipes.delete(r)
175
+ m.start_immediate_stop!
176
+ r.close rescue nil
177
+ next
178
+ end
179
+
180
+ if m = @rpipes[r]
181
+ m.last_heartbeat_time = time
182
+ end
183
+ end
184
+
185
+ @monitors.delete_if {|m|
186
+ !m.tick(time)
187
+ }
188
+
189
+ nil
190
+ end
191
+
192
+ def self.signal_name(n)
193
+ Signal.list.each_pair {|k,v|
194
+ return "SIG#{k}" if n == v
195
+ }
196
+ return n
197
+ end
198
+
199
+ def self.format_join_status(code)
200
+ case code
201
+ when Process::Status
202
+ if code.signaled?
203
+ "signal #{signal_name(code.termsig)}"
204
+ else
205
+ "status #{code.exitstatus}"
206
+ end
207
+ when Exception
208
+ "exception #{code}"
209
+ when nil
210
+ "unknown reason"
211
+ end
212
+ end
213
+
214
+ class AlreadyClosedError < EOFError
215
+ end
216
+
217
+ HEARTBEAT_MESSAGE = [0].pack('C')
218
+
219
+ class Monitor
220
+ def initialize(pm, pid)
221
+ @pm = pm
222
+ @pid = pid
223
+
224
+ @error = false
225
+ @last_heartbeat_time = Time.now
226
+ @next_kill_time = nil
227
+ @graceful_kill_start_time = nil
228
+ @immediate_kill_start_time = nil
229
+ @kill_count = 0
230
+ end
231
+
232
+ attr_accessor :last_heartbeat_time
233
+
234
+ def heartbeat_delay
235
+ now = Time.now
236
+ now - @last_heartbeat_time
237
+ end
238
+
239
+ def send_signal(sig)
240
+ pid = @pid
241
+ return nil unless pid
242
+
243
+ begin
244
+ Process.kill(sig, pid)
245
+ return true
246
+ rescue #Errno::ECHILD, Errno::ESRCH, Errno::EPERM
247
+ return false
248
+ end
249
+ end
250
+
251
+ def try_join
252
+ pid = @pid
253
+ return true unless pid
254
+
255
+ begin
256
+ pid, status = Process.waitpid2(pid, Process::WNOHANG)
257
+ code = status
258
+ rescue #Errno::ECHILD, Errno::ESRCH, Errno::EPERM
259
+ # assume that any errors mean the child process is dead
260
+ code = $!
261
+ end
262
+
263
+ if code
264
+ @pid = nil
265
+ return code
266
+ end
267
+
268
+ return false
269
+ end
270
+
271
+ def join
272
+ pid = @pid
273
+ return nil unless pid
274
+
275
+ begin
276
+ pid, status = Process.waitpid2(pid)
277
+ code = status
278
+ rescue #Errno::ECHILD, Errno::ESRCH, Errno::EPERM
279
+ # assume that any errors mean the child process is dead
280
+ code = $!
281
+ end
282
+ @pid = nil
283
+
284
+ return code
285
+ end
286
+
287
+ def start_graceful_stop!
288
+ now = Time.now
289
+ @next_kill_time ||= now
290
+ @graceful_kill_start_time ||= now
291
+ end
292
+
293
+ def start_immediate_stop!
294
+ now = Time.now
295
+ @next_kill_time ||= now
296
+ @immediate_kill_start_time ||= now
297
+ end
298
+
299
+ def tick(now=Time.now)
300
+ pid = @pid
301
+ return false unless pid
302
+
303
+ if !@immediate_kill_start_time
304
+ # check escalation
305
+ if heartbeat_delay >= @pm.heartbeat_timeout ||
306
+ (@graceful_kill_start_time && @pm.graceful_kill_timeout > 0 &&
307
+ @graceful_kill_start_time < now - @pm.graceful_kill_timeout)
308
+ # escalate to immediate kill
309
+ @kill_count = 0
310
+ @immediate_kill_start_time = now
311
+ @next_kill_time = now
312
+ end
313
+ end
314
+
315
+ if !@next_kill_time || @next_kill_time > now
316
+ # expect next tick
317
+ return true
318
+ end
319
+
320
+ # send signal now
321
+
322
+ if @immediate_kill_start_time
323
+ interval = @pm.immediate_kill_interval
324
+ interval_incr = @pm.immediate_kill_interval_increment
325
+ if @immediate_kill_start_time <= now - @pm.immediate_kill_timeout
326
+ # escalate to SIGKILL
327
+ signal = :KILL
328
+ else
329
+ signal = @pm.immediate_kill_signal
330
+ end
331
+
332
+ else
333
+ signal = @pm.graceful_kill_signal
334
+ interval = @pm.graceful_kill_interval
335
+ interval_incr = @pm.graceful_kill_interval_increment
336
+ end
337
+
338
+ begin
339
+ Process.kill(signal, pid)
340
+ rescue #Errno::ECHILD, Errno::ESRCH, Errno::EPERM
341
+ # assume that any errors mean the child process is dead
342
+ @pid = nil
343
+ return false
344
+ end
345
+
346
+ @next_kill_time = now + interval + interval_incr * @kill_count
347
+ @kill_count += 1
348
+
349
+ # expect next tick
350
+ return true
351
+ end
352
+ end
353
+
354
+ class TickThread < Thread
355
+ def initialize(pm)
356
+ @pm = pm
357
+ super(&method(:main))
358
+ end
359
+
360
+ private
361
+
362
+ def main
363
+ while true
364
+ @pm.tick(@pm.tick_interval)
365
+ end
366
+ nil
367
+ rescue AlreadyClosedError
368
+ nil
369
+ end
370
+ end
371
+
372
+ class Target
373
+ def initialize(pipe)
374
+ @pipe = pipe
375
+ end
376
+
377
+ attr_reader :pipe
378
+
379
+ def heartbeat!
380
+ @pipe.write HEARTBEAT_MESSAGE
381
+ end
382
+
383
+ def close
384
+ if @pipe
385
+ @pipe.close rescue nil
386
+ @pipe = nil
387
+ end
388
+ end
389
+ end
390
+
391
+ class HeartbeatThread < Thread
392
+ def initialize(pm, target, error_proc)
393
+ @pm = pm
394
+ @target = target
395
+ @error_proc = error_proc
396
+ super(&method(:main))
397
+ end
398
+
399
+ private
400
+
401
+ def main
402
+ while true
403
+ sleep @pm.heartbeat_interval
404
+ @target.heartbeat!
405
+ end
406
+ nil
407
+ rescue
408
+ @error_proc.call(self)
409
+ nil
410
+ end
411
+ end
412
+
413
+ end
414
+
415
+ end