serverengine 1.5.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.
@@ -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