serverengine 2.0.0pre1-x86-mingw32

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +5 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +20 -0
  5. data/Changelog +122 -0
  6. data/Gemfile +2 -0
  7. data/LICENSE +202 -0
  8. data/NOTICE +3 -0
  9. data/README.md +514 -0
  10. data/Rakefile +26 -0
  11. data/appveyor.yml +24 -0
  12. data/examples/server.rb +138 -0
  13. data/examples/spawn_worker_script.rb +38 -0
  14. data/lib/serverengine.rb +46 -0
  15. data/lib/serverengine/blocking_flag.rb +77 -0
  16. data/lib/serverengine/command_sender.rb +89 -0
  17. data/lib/serverengine/config_loader.rb +82 -0
  18. data/lib/serverengine/daemon.rb +233 -0
  19. data/lib/serverengine/daemon_logger.rb +135 -0
  20. data/lib/serverengine/embedded_server.rb +67 -0
  21. data/lib/serverengine/multi_process_server.rb +155 -0
  22. data/lib/serverengine/multi_spawn_server.rb +95 -0
  23. data/lib/serverengine/multi_thread_server.rb +80 -0
  24. data/lib/serverengine/multi_worker_server.rb +150 -0
  25. data/lib/serverengine/privilege.rb +57 -0
  26. data/lib/serverengine/process_manager.rb +508 -0
  27. data/lib/serverengine/server.rb +178 -0
  28. data/lib/serverengine/signal_thread.rb +116 -0
  29. data/lib/serverengine/signals.rb +31 -0
  30. data/lib/serverengine/socket_manager.rb +171 -0
  31. data/lib/serverengine/socket_manager_unix.rb +98 -0
  32. data/lib/serverengine/socket_manager_win.rb +154 -0
  33. data/lib/serverengine/supervisor.rb +313 -0
  34. data/lib/serverengine/utils.rb +62 -0
  35. data/lib/serverengine/version.rb +3 -0
  36. data/lib/serverengine/winsock.rb +128 -0
  37. data/lib/serverengine/worker.rb +81 -0
  38. data/serverengine.gemspec +37 -0
  39. data/spec/blocking_flag_spec.rb +59 -0
  40. data/spec/daemon_logger_spec.rb +175 -0
  41. data/spec/daemon_spec.rb +169 -0
  42. data/spec/multi_process_server_spec.rb +113 -0
  43. data/spec/server_worker_context.rb +232 -0
  44. data/spec/signal_thread_spec.rb +94 -0
  45. data/spec/socket_manager_spec.rb +119 -0
  46. data/spec/spec_helper.rb +19 -0
  47. data/spec/supervisor_spec.rb +215 -0
  48. metadata +184 -0
@@ -0,0 +1,57 @@
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
+ require 'etc'
19
+
20
+ module ServerEngine
21
+ module Privilege
22
+ def self.get_etc_passwd(user)
23
+ if user.to_i.to_s == user
24
+ Etc.getpwuid(user.to_i)
25
+ else
26
+ Etc.getpwnam(user)
27
+ end
28
+ end
29
+
30
+ def self.get_etc_group(group)
31
+ if group.to_i.to_s == group
32
+ Etc.getgrgid(group.to_i)
33
+ else
34
+ Etc.getgrnam(group)
35
+ end
36
+ end
37
+
38
+ def self.change(user, group)
39
+ if user
40
+ etc_pw = get_etc_passwd(user)
41
+ user_groups = [etc_pw.gid]
42
+ Etc.setgrent
43
+ Etc.group { |gr| user_groups << gr.gid if gr.mem.include?(etc_pw.name) } # emulate 'id -G'
44
+
45
+ Process.groups = Process.groups | user_groups
46
+ Process::UID.change_privilege(etc_pw.uid)
47
+ end
48
+
49
+ if group
50
+ etc_group = get_etc_group(group)
51
+ Process::GID.change_privilege(etc_group.gid)
52
+ end
53
+
54
+ nil
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,508 @@
1
+ #
2
+ # ServerEngine
3
+ #
4
+ # Copyright (C) 2012-2013 Sadayuki Furuhashi
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
+ require 'fcntl'
19
+
20
+ module ServerEngine
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
+ @auto_tick_interval = config[:auto_tick_interval] || 1
35
+
36
+ @enable_heartbeat = !!config[:enable_heartbeat]
37
+ if ServerEngine.windows?
38
+ # heartbeat is not supported on Windows platform. See also spawn method.
39
+ @enable_heartbeat = false
40
+ end
41
+ @auto_heartbeat = !!config.fetch(:auto_heartbeat, true)
42
+
43
+ case op = config[:on_heartbeat_error]
44
+ when nil
45
+ @heartbeat_error_proc = lambda {|t| }
46
+ when Proc
47
+ @heartbeat_error_proc = op
48
+ when :abort
49
+ @heartbeat_error_proc = lambda {|t| exit 1 }
50
+ else
51
+ raise ArgumentError, "unexpected :on_heartbeat_error option (expected Proc, true or false but got #{op.class})"
52
+ end
53
+
54
+ configure(config)
55
+
56
+ @closed = false
57
+ @read_buffer = ''
58
+
59
+ if @auto_tick
60
+ TickThread.new(@auto_tick_interval, &method(:tick))
61
+ end
62
+ end
63
+
64
+ attr_accessor :logger
65
+
66
+ attr_accessor :cloexec_mode
67
+
68
+ attr_reader :graceful_kill_signal, :immediate_kill_signal
69
+ attr_reader :auto_tick, :auto_tick_interval
70
+ attr_reader :enable_heartbeat, :auto_heartbeat
71
+
72
+ attr_accessor :command_sender
73
+ attr_reader :command_sender_pipe
74
+
75
+ CONFIG_PARAMS = {
76
+ heartbeat_interval: 1,
77
+ heartbeat_timeout: 180,
78
+ graceful_kill_interval: 15,
79
+ graceful_kill_interval_increment: 10,
80
+ graceful_kill_timeout: 600,
81
+ immediate_kill_interval: 10,
82
+ immediate_kill_interval_increment: 10,
83
+ immediate_kill_timeout: 600,
84
+ }
85
+
86
+ CONFIG_PARAMS.each_pair do |key,default_value|
87
+ attr_reader key
88
+
89
+ define_method("#{key}=") do |v|
90
+ v = default_value if v == nil
91
+ instance_variable_set("@#{key}", v)
92
+ end
93
+ end
94
+
95
+ def configure(config, opts={})
96
+ prefix = opts[:prefix] || ""
97
+ CONFIG_PARAMS.keys.each {|key|
98
+ send("#{key}=", config[:"#{prefix}#{key}"])
99
+ }
100
+ end
101
+
102
+ def monitor_options
103
+ {
104
+ enable_heartbeat: @enable_heartbeat,
105
+ heartbeat_timeout: @heartbeat_timeout,
106
+ graceful_kill_signal: @graceful_kill_signal,
107
+ graceful_kill_timeout: @graceful_kill_timeout,
108
+ graceful_kill_interval: @graceful_kill_interval,
109
+ graceful_kill_interval_increment: @graceful_kill_interval_increment,
110
+ immediate_kill_signal: @immediate_kill_signal,
111
+ immediate_kill_timeout: @immediate_kill_timeout,
112
+ immediate_kill_interval: @immediate_kill_interval,
113
+ immediate_kill_interval_increment: @immediate_kill_interval_increment,
114
+ }
115
+ end
116
+
117
+ def fork(&block)
118
+ if ServerEngine.windows?
119
+ raise NotImplementedError, "fork is not available on this platform. Please use spawn (worker_type: 'spawn')."
120
+ end
121
+
122
+ rpipe, wpipe = new_pipe_pair
123
+
124
+ begin
125
+ pid = Process.fork do
126
+ self.close
127
+ begin
128
+ t = Target.new(wpipe)
129
+ if @enable_heartbeat && @auto_heartbeat
130
+ HeartbeatThread.new(@heartbeat_interval, t, @heartbeat_error_proc)
131
+ end
132
+
133
+ block.call(t)
134
+ exit! 0
135
+
136
+ rescue
137
+ ServerEngine.dump_uncaught_error($!)
138
+ ensure
139
+ exit! 1
140
+ end
141
+ end
142
+
143
+ m = Monitor.new(pid, monitor_options)
144
+
145
+ @monitors << m
146
+ @rpipes[rpipe] = m
147
+ rpipe = nil
148
+
149
+ return m
150
+
151
+ ensure
152
+ wpipe.close
153
+ rpipe.close if rpipe
154
+ end
155
+ end
156
+
157
+ def spawn(*args)
158
+ if args.first.is_a?(Hash)
159
+ env = args.shift.dup
160
+ else
161
+ env = {}
162
+ end
163
+
164
+ if args.last.is_a?(Hash)
165
+ options = args.pop.dup
166
+ else
167
+ options = {}
168
+ end
169
+
170
+ # pipe is necessary even if @enable_heartbeat == false because
171
+ # parent process detects shutdown of a child process using it
172
+ begin
173
+ unless ServerEngine.windows?
174
+ # heartbeat is not supported on Windows platform
175
+ rpipe, wpipe = new_pipe_pair
176
+ options[[wpipe.fileno]] = wpipe
177
+ if @enable_heartbeat
178
+ env['SERVERENGINE_HEARTBEAT_PIPE'] = wpipe.fileno.to_s
179
+ end
180
+ end
181
+
182
+ if @command_sender == "pipe"
183
+ inpipe, @command_sender_pipe = IO.pipe
184
+ @command_sender_pipe.sync = true
185
+ @command_sender_pipe.binmode
186
+ options[:in] = inpipe
187
+ end
188
+ pid = Process.spawn(env, *args, options)
189
+ if @command_sender == "pipe"
190
+ inpipe.close
191
+ end
192
+
193
+ m = Monitor.new(pid, monitor_options)
194
+
195
+ @monitors << m
196
+
197
+ unless ServerEngine.windows?
198
+ @rpipes[rpipe] = m
199
+ rpipe = nil
200
+ end
201
+
202
+ return m
203
+
204
+ ensure
205
+ wpipe.close if wpipe
206
+ rpipe.close if rpipe
207
+ end
208
+ end
209
+
210
+ def new_pipe_pair
211
+ rpipe, wpipe = IO.pipe
212
+
213
+ if Fcntl.const_defined?(:F_SETFD) && Fcntl.const_defined?(:FD_CLOEXEC)
214
+ case @cloexec_mode
215
+ when :target_only
216
+ wpipe.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
217
+ when :monitor_only
218
+ rpipe.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
219
+ else
220
+ rpipe.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
221
+ wpipe.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
222
+ end
223
+ end
224
+
225
+ rpipe.sync = true
226
+ wpipe.sync = true
227
+
228
+ return rpipe, wpipe
229
+ end
230
+
231
+ def close
232
+ @closed = true
233
+ @rpipes.keys.each {|m| m.close }
234
+ nil
235
+ end
236
+
237
+ def tick(blocking_timeout=0)
238
+ if @closed
239
+ raise AlreadyClosedError.new
240
+ end
241
+
242
+ time ||= Time.now
243
+
244
+ unless ServerEngine.windows?
245
+ # heartbeat is not supported on Windows platform.
246
+
247
+ if @rpipes.empty?
248
+ sleep blocking_timeout if blocking_timeout > 0
249
+ return nil
250
+ end
251
+
252
+ ready_pipes, _, _ = IO.select(@rpipes.keys, nil, nil, blocking_timeout)
253
+
254
+ if ready_pipes
255
+ ready_pipes.each do |r|
256
+ begin
257
+ r.read_nonblock(1024, @read_buffer)
258
+ rescue Errno::EAGAIN, Errno::EINTR
259
+ next
260
+ rescue #EOFError
261
+ m = @rpipes.delete(r)
262
+ m.start_immediate_stop!
263
+ r.close rescue nil
264
+ next
265
+ end
266
+
267
+ if m = @rpipes[r]
268
+ m.last_heartbeat_time = time
269
+ end
270
+ end
271
+ end
272
+ end
273
+
274
+ @monitors.delete_if {|m|
275
+ !m.tick(time)
276
+ }
277
+
278
+ nil
279
+ end
280
+
281
+ class AlreadyClosedError < EOFError
282
+ end
283
+
284
+ HEARTBEAT_MESSAGE = [0].pack('C')
285
+
286
+ class Monitor
287
+ def initialize(pid, opts={})
288
+ @pid = pid
289
+
290
+ @enable_heartbeat = opts[:enable_heartbeat]
291
+ @heartbeat_timeout = opts[:heartbeat_timeout]
292
+
293
+ @graceful_kill_signal = opts[:graceful_kill_signal]
294
+ @graceful_kill_timeout = opts[:graceful_kill_timeout]
295
+ @graceful_kill_interval = opts[:graceful_kill_interval]
296
+ @graceful_kill_interval_increment = opts[:graceful_kill_interval_increment]
297
+
298
+ @immediate_kill_signal = opts[:immediate_kill_signal]
299
+ @immediate_kill_timeout = opts[:immediate_kill_timeout]
300
+ @immediate_kill_interval = opts[:immediate_kill_interval]
301
+ @immediate_kill_interval_increment = opts[:immediate_kill_interval_increment]
302
+
303
+ @error = false
304
+ @last_heartbeat_time = Time.now
305
+ @next_kill_time = nil
306
+ @graceful_kill_start_time = nil
307
+ @immediate_kill_start_time = nil
308
+ @kill_count = 0
309
+ end
310
+
311
+ attr_accessor :last_heartbeat_time
312
+ attr_reader :pid
313
+
314
+ def heartbeat_delay
315
+ now = Time.now
316
+ now - @last_heartbeat_time
317
+ end
318
+
319
+ def send_signal(sig)
320
+ pid = @pid
321
+ return nil unless pid
322
+
323
+ begin
324
+ Process.kill(sig, pid)
325
+ return true
326
+ rescue #Errno::ECHILD, Errno::ESRCH, Errno::EPERM
327
+ return false
328
+ end
329
+ end
330
+
331
+ def try_join
332
+ pid = @pid
333
+ return true unless pid
334
+
335
+ begin
336
+ pid, status = Process.waitpid2(pid, Process::WNOHANG)
337
+ code = status
338
+ rescue #Errno::ECHILD, Errno::ESRCH, Errno::EPERM
339
+ # assume that any errors mean the child process is dead
340
+ code = $!
341
+ end
342
+
343
+ if code
344
+ @pid = nil
345
+ return code
346
+ end
347
+
348
+ return false
349
+ end
350
+
351
+ def join
352
+ pid = @pid
353
+ return nil unless pid
354
+
355
+ begin
356
+ pid, status = Process.waitpid2(pid)
357
+ code = status
358
+ rescue #Errno::ECHILD, Errno::ESRCH, Errno::EPERM
359
+ # assume that any errors mean the child process is dead
360
+ code = $!
361
+ end
362
+ @pid = nil
363
+
364
+ return code
365
+ end
366
+
367
+ def start_graceful_stop!
368
+ now = Time.now
369
+ @next_kill_time ||= now
370
+ @graceful_kill_start_time ||= now
371
+ end
372
+
373
+ def start_immediate_stop!
374
+ now = Time.now
375
+ @next_kill_time ||= now
376
+ @immediate_kill_start_time ||= now
377
+ end
378
+
379
+ def tick(now=Time.now)
380
+ pid = @pid
381
+ return false unless pid
382
+
383
+ if !@immediate_kill_start_time
384
+ # check heartbeat timeout or escalation
385
+ if (
386
+ # heartbeat timeout
387
+ @enable_heartbeat &&
388
+ heartbeat_delay >= @heartbeat_timeout
389
+ ) || (
390
+ # escalation
391
+ @graceful_kill_start_time &&
392
+ @graceful_kill_timeout >= 0 &&
393
+ @graceful_kill_start_time < now - @graceful_kill_timeout
394
+ )
395
+ # escalate to immediate kill
396
+ @kill_count = 0
397
+ @immediate_kill_start_time = now
398
+ @next_kill_time = now
399
+ end
400
+ end
401
+
402
+ if !@next_kill_time || @next_kill_time > now
403
+ # expect next tick
404
+ return true
405
+ end
406
+
407
+ # send signal now
408
+
409
+ if @immediate_kill_start_time
410
+ interval = @immediate_kill_interval
411
+ interval_incr = @immediate_kill_interval_increment
412
+ if @immediate_kill_timeout >= 0 &&
413
+ @immediate_kill_start_time <= now - @immediate_kill_timeout
414
+ # escalate to SIGKILL
415
+ signal = :KILL
416
+ else
417
+ signal = @immediate_kill_signal
418
+ end
419
+
420
+ else
421
+ signal = @graceful_kill_signal
422
+ interval = @graceful_kill_interval
423
+ interval_incr = @graceful_kill_interval_increment
424
+ end
425
+
426
+ begin
427
+ if ServerEngine.windows? && (signal == :KILL || signal == :SIGKILL)
428
+ system("taskkill /f /pid #{pid}")
429
+ else
430
+ Process.kill(signal, pid)
431
+ end
432
+ rescue #Errno::ECHILD, Errno::ESRCH, Errno::EPERM
433
+ # assume that any errors mean the child process is dead
434
+ @pid = nil
435
+ return false
436
+ end
437
+
438
+ @next_kill_time = now + interval + interval_incr * @kill_count
439
+ @kill_count += 1
440
+
441
+ # expect next tick
442
+ return true
443
+ end
444
+ end
445
+
446
+ class TickThread < Thread
447
+ def initialize(auto_tick_interval, &tick)
448
+ @auto_tick_interval = auto_tick_interval
449
+ @tick = tick
450
+ super(&method(:main))
451
+ end
452
+
453
+ private
454
+
455
+ def main
456
+ while true
457
+ @tick.call(@auto_tick_interval)
458
+ end
459
+ nil
460
+ rescue AlreadyClosedError
461
+ nil
462
+ end
463
+ end
464
+
465
+ class Target
466
+ def initialize(pipe)
467
+ @pipe = pipe
468
+ end
469
+
470
+ attr_reader :pipe
471
+
472
+ def heartbeat!
473
+ @pipe.write HEARTBEAT_MESSAGE
474
+ end
475
+
476
+ def close
477
+ if @pipe
478
+ @pipe.close rescue nil
479
+ @pipe = nil
480
+ end
481
+ end
482
+ end
483
+
484
+ class HeartbeatThread < Thread
485
+ def initialize(heartbeat_interval, target, error_proc)
486
+ @heartbeat_interval = heartbeat_interval
487
+ @target = target
488
+ @error_proc = error_proc
489
+ super(&method(:main))
490
+ end
491
+
492
+ private
493
+
494
+ def main
495
+ while true
496
+ sleep @heartbeat_interval
497
+ @target.heartbeat!
498
+ end
499
+ nil
500
+ rescue
501
+ @error_proc.call(self)
502
+ nil
503
+ end
504
+ end
505
+
506
+ end
507
+
508
+ end