serverengine 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/COPYING +14 -0
- data/Changelog +5 -0
- data/Gemfile +3 -0
- data/README.md +332 -0
- data/Rakefile +14 -0
- data/lib/serverengine.rb +52 -0
- data/lib/serverengine/blocking_flag.rb +78 -0
- data/lib/serverengine/config_loader.rb +73 -0
- data/lib/serverengine/daemon.rb +165 -0
- data/lib/serverengine/daemon_logger.rb +110 -0
- data/lib/serverengine/embedded_server.rb +47 -0
- data/lib/serverengine/multi_process_server.rb +132 -0
- data/lib/serverengine/multi_thread_server.rb +61 -0
- data/lib/serverengine/multi_worker_server.rb +130 -0
- data/lib/serverengine/process_manager.rb +415 -0
- data/lib/serverengine/server.rb +103 -0
- data/lib/serverengine/signal_thread.rb +143 -0
- data/lib/serverengine/supervisor.rb +230 -0
- data/lib/serverengine/utils.rb +32 -0
- data/lib/serverengine/version.rb +3 -0
- data/lib/serverengine/worker.rb +73 -0
- data/serverengine.gemspec +25 -0
- data/spec/blocking_flag_spec.rb +58 -0
- data/spec/daemon_logger_spec.rb +88 -0
- data/spec/daemon_spec.rb +46 -0
- data/spec/multi_process_server_spec.rb +61 -0
- data/spec/server_worker_context.rb +118 -0
- data/spec/signal_thread_spec.rb +85 -0
- data/spec/spec_helper.rb +16 -0
- data/spec/supervisor_spec.rb +142 -0
- metadata +134 -0
@@ -0,0 +1,73 @@
|
|
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
|
+
module ConfigLoader
|
21
|
+
def initialize(load_config_proc={}, &block)
|
22
|
+
if block
|
23
|
+
@load_config_proc = block
|
24
|
+
else
|
25
|
+
if load_config_proc.is_a?(Hash)
|
26
|
+
@load_config_proc = lambda { load_config_proc }
|
27
|
+
else
|
28
|
+
@load_config_proc = load_config_proc
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
attr_reader :config
|
34
|
+
attr_accessor :logger
|
35
|
+
|
36
|
+
def reload_config
|
37
|
+
@config = @load_config_proc.call
|
38
|
+
|
39
|
+
@logger_class = @config[:logger_class] || DaemonLogger
|
40
|
+
|
41
|
+
case c = @config[:log]
|
42
|
+
when nil # default
|
43
|
+
@log_dev = STDERR
|
44
|
+
when "-"
|
45
|
+
@log_dev = STDOUT
|
46
|
+
else
|
47
|
+
@log_dev = c
|
48
|
+
end
|
49
|
+
|
50
|
+
if @logger
|
51
|
+
if @log_dev.is_a?(String)
|
52
|
+
@logger.path = @log_dev
|
53
|
+
end
|
54
|
+
|
55
|
+
@logger.level = @config[:log_level] || 'debug'
|
56
|
+
end
|
57
|
+
|
58
|
+
nil
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def create_logger
|
64
|
+
if logger = @config[:logger]
|
65
|
+
@logger = logger
|
66
|
+
return
|
67
|
+
end
|
68
|
+
|
69
|
+
@logger = @logger_class.new(@log_dev, @config)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
@@ -0,0 +1,165 @@
|
|
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 'shellwords'
|
21
|
+
|
22
|
+
class Daemon
|
23
|
+
include ConfigLoader
|
24
|
+
|
25
|
+
def initialize(server_module, worker_module, load_config_proc={}, &block)
|
26
|
+
@server_module = server_module
|
27
|
+
@worker_module = worker_module
|
28
|
+
|
29
|
+
super(load_config_proc, &block)
|
30
|
+
|
31
|
+
reload_config
|
32
|
+
|
33
|
+
@daemonize = @config.fetch(:daemonize, false)
|
34
|
+
|
35
|
+
if @config.fetch(:supervisor, false)
|
36
|
+
@create_server_proc = lambda do |load_config_proc,logger|
|
37
|
+
s = Supervisor.new(server_module, worker_module, load_config_proc)
|
38
|
+
s.logger = logger
|
39
|
+
s
|
40
|
+
end
|
41
|
+
else
|
42
|
+
@create_server_proc = Supervisor.create_server_proc(server_module, worker_module, @config)
|
43
|
+
end
|
44
|
+
|
45
|
+
@daemon_process_name = @config[:daemon_process_name]
|
46
|
+
|
47
|
+
@pid_path = @config[:pid_path]
|
48
|
+
@chuser = @config[:chuser]
|
49
|
+
@chgroup = @config[:chgroup]
|
50
|
+
@chumask = @config[:chumask]
|
51
|
+
end
|
52
|
+
|
53
|
+
module Signals
|
54
|
+
GRACEFUL_STOP = :TERM
|
55
|
+
IMMEDIATE_STOP = :QUIT
|
56
|
+
GRACEFUL_RESTART = :USR1
|
57
|
+
IMMEDIATE_RESTART = :HUP
|
58
|
+
RELOAD = :USR2
|
59
|
+
DETACH = :INT
|
60
|
+
DUMP = :CONT
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.change_privilege(user, group)
|
64
|
+
if group
|
65
|
+
chgid = group.to_i
|
66
|
+
if chgid.to_s != group
|
67
|
+
chgid = `id -g #{Shellwords.escape group}`.to_i
|
68
|
+
exit! 1 unless $?.success?
|
69
|
+
end
|
70
|
+
Process::GID.change_privilege(chgid)
|
71
|
+
end
|
72
|
+
|
73
|
+
if user
|
74
|
+
chuid = user.to_i
|
75
|
+
if chuid.to_s != user
|
76
|
+
chuid = `id -u #{Shellwords.escape user}`.to_i
|
77
|
+
exit! 1 unless $?.success?
|
78
|
+
end
|
79
|
+
|
80
|
+
user_groups = `id -G #{Shellwords.escape user}`.split.map(&:to_i)
|
81
|
+
exit! 1 unless $?.success?
|
82
|
+
|
83
|
+
Process.groups = Process.groups | user_groups
|
84
|
+
Process::UID.change_privilege(chuid)
|
85
|
+
end
|
86
|
+
|
87
|
+
nil
|
88
|
+
end
|
89
|
+
|
90
|
+
def run
|
91
|
+
begin
|
92
|
+
exit main
|
93
|
+
rescue
|
94
|
+
ServerEngine.dump_uncaught_error($!)
|
95
|
+
exit 1 # TODO exit code
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def main
|
100
|
+
unless @daemonize
|
101
|
+
s = create_server(create_logger)
|
102
|
+
s.install_signal_handlers
|
103
|
+
s.main
|
104
|
+
return 0
|
105
|
+
end
|
106
|
+
|
107
|
+
rpipe, wpipe = IO.pipe
|
108
|
+
wpipe.sync = true
|
109
|
+
|
110
|
+
Process.fork do
|
111
|
+
begin
|
112
|
+
rpipe.close
|
113
|
+
|
114
|
+
Process.setsid
|
115
|
+
Process.fork do
|
116
|
+
$0 = @daemon_process_name if @daemon_process_name
|
117
|
+
wpipe.write "#{Process.pid}\n"
|
118
|
+
|
119
|
+
Daemon.change_privilege(@chumask, @chgroup)
|
120
|
+
File.umask(@chumask) if @chumask
|
121
|
+
|
122
|
+
s = create_server(create_logger)
|
123
|
+
|
124
|
+
STDIN.reopen("/dev/null")
|
125
|
+
STDOUT.reopen("/dev/null", "wb")
|
126
|
+
STDERR.reopen("/dev/null", "wb")
|
127
|
+
|
128
|
+
s.install_signal_handlers
|
129
|
+
|
130
|
+
wpipe.write "\n"
|
131
|
+
wpipe.close
|
132
|
+
|
133
|
+
s.main
|
134
|
+
end
|
135
|
+
|
136
|
+
exit 0
|
137
|
+
ensure
|
138
|
+
exit! 1 # TODO exit code
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
wpipe.close
|
143
|
+
|
144
|
+
pid = rpipe.gets.to_i
|
145
|
+
if @pid_path
|
146
|
+
File.open(@pid_path, "w") {|f|
|
147
|
+
f.write "#{pid}\n"
|
148
|
+
}
|
149
|
+
end
|
150
|
+
|
151
|
+
data = rpipe.read
|
152
|
+
if data != "\n"
|
153
|
+
return 1 # TODO exit code
|
154
|
+
end
|
155
|
+
|
156
|
+
return 0
|
157
|
+
end
|
158
|
+
|
159
|
+
private
|
160
|
+
|
161
|
+
def create_server(logger)
|
162
|
+
@create_server_proc.call(@load_config_proc, logger)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
#
|
2
|
+
# ServerEngine
|
3
|
+
#
|
4
|
+
# Copyright (C) 2012 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 'logger'
|
21
|
+
|
22
|
+
class DaemonLogger < Logger
|
23
|
+
def initialize(dev, config={})
|
24
|
+
@hook_stdout = config.fetch(:log_stdout, true)
|
25
|
+
@hook_stderr = config.fetch(:log_stderr, true)
|
26
|
+
rotate_age = config[:log_rotate_age] || 5
|
27
|
+
rotate_size = config[:log_rotate_size] || 1048576
|
28
|
+
|
29
|
+
if dev.is_a?(String)
|
30
|
+
@path = dev
|
31
|
+
@io = File.open(@path, "a")
|
32
|
+
@io.sync = true
|
33
|
+
else
|
34
|
+
@io = dev
|
35
|
+
end
|
36
|
+
|
37
|
+
hook_stdout! if @hook_stdout
|
38
|
+
hook_stderr! if @hook_stderr
|
39
|
+
|
40
|
+
super(@io, rotate_age, rotate_size)
|
41
|
+
|
42
|
+
self.level = config[:level] || 'debug'
|
43
|
+
end
|
44
|
+
|
45
|
+
attr_accessor :path
|
46
|
+
|
47
|
+
def level=(expr)
|
48
|
+
case expr.to_s
|
49
|
+
when 'fatal', Logger::FATAL.to_s
|
50
|
+
e = Logger::FATAL
|
51
|
+
when 'error', Logger::ERROR.to_s
|
52
|
+
e = Logger::ERROR
|
53
|
+
when 'warn', Logger::WARN.to_s
|
54
|
+
e = Logger::WARN
|
55
|
+
when 'info', Logger::INFO.to_s
|
56
|
+
e = Logger::INFO
|
57
|
+
when 'debug', Logger::DEBUG.to_s
|
58
|
+
e = Logger::DEBUG
|
59
|
+
else
|
60
|
+
raise ArgumentError, "invalid log level: #{expr}"
|
61
|
+
end
|
62
|
+
|
63
|
+
super(e)
|
64
|
+
end
|
65
|
+
|
66
|
+
def hook_stdout!
|
67
|
+
STDOUT.sync = true
|
68
|
+
@hook_stdout = true
|
69
|
+
|
70
|
+
STDOUT.reopen(@io) if @io != STDOUT
|
71
|
+
self
|
72
|
+
end
|
73
|
+
|
74
|
+
def hook_stderr!
|
75
|
+
STDERR.sync = true
|
76
|
+
@hook_stderr = true
|
77
|
+
|
78
|
+
STDERR.reopen(@io) if @io != STDERR
|
79
|
+
self
|
80
|
+
end
|
81
|
+
|
82
|
+
def reopen!
|
83
|
+
if @path
|
84
|
+
@io.reopen(@path, "a")
|
85
|
+
@io.sync = true
|
86
|
+
hook_stdout! if @hook_stdout
|
87
|
+
hook_stderr! if @hook_stderr
|
88
|
+
end
|
89
|
+
nil
|
90
|
+
end
|
91
|
+
|
92
|
+
def reopen
|
93
|
+
begin
|
94
|
+
reopen!
|
95
|
+
return true
|
96
|
+
rescue
|
97
|
+
# TODO log?
|
98
|
+
return false
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def close
|
103
|
+
if @path
|
104
|
+
@io.close unless @io.closed?
|
105
|
+
end
|
106
|
+
nil
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
@@ -0,0 +1,47 @@
|
|
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 EmbeddedServer < Server
|
21
|
+
def run
|
22
|
+
@worker = create_worker(0)
|
23
|
+
until @stop
|
24
|
+
@worker.main
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def stop(stop_graceful)
|
29
|
+
super
|
30
|
+
Thread.new { @worker.stop }
|
31
|
+
nil
|
32
|
+
end
|
33
|
+
|
34
|
+
def restart(stop_graceful)
|
35
|
+
super
|
36
|
+
Thread.new { @worker.stop }
|
37
|
+
nil
|
38
|
+
end
|
39
|
+
|
40
|
+
def reload
|
41
|
+
super
|
42
|
+
Thread.new { @worker.reload }
|
43
|
+
nil
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
@@ -0,0 +1,132 @@
|
|
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 MultiProcessServer < MultiWorkerServer
|
21
|
+
def initialize(worker_module, load_config_proc={}, &block)
|
22
|
+
@pm = ProcessManager.new(
|
23
|
+
auto_tick: false,
|
24
|
+
graceful_kill_signal: Daemon::Signals::GRACEFUL_STOP,
|
25
|
+
auto_heartbeat: true,
|
26
|
+
abort_on_heartbeat_error: Proc.new do
|
27
|
+
@logger.fatal "parent process unexpectedly terminated"
|
28
|
+
exit! 1
|
29
|
+
end
|
30
|
+
)
|
31
|
+
|
32
|
+
super(worker_module, load_config_proc, &block)
|
33
|
+
|
34
|
+
@worker_process_name = @config[:worker_process_name]
|
35
|
+
end
|
36
|
+
|
37
|
+
def run
|
38
|
+
super
|
39
|
+
ensure
|
40
|
+
@pm.close
|
41
|
+
end
|
42
|
+
|
43
|
+
def logger=(logger)
|
44
|
+
super
|
45
|
+
@pm.logger = logger
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def reload_config
|
51
|
+
super
|
52
|
+
|
53
|
+
@chuser = @config[:worker_chuser]
|
54
|
+
@chgroup = @config[:worker_chgroup]
|
55
|
+
@chumask = @config[:worker_chumask]
|
56
|
+
|
57
|
+
@pm.configure(@config, prefix: 'worker_')
|
58
|
+
|
59
|
+
nil
|
60
|
+
end
|
61
|
+
|
62
|
+
def start_worker(wid)
|
63
|
+
w = create_worker(wid)
|
64
|
+
|
65
|
+
w.before_fork
|
66
|
+
begin
|
67
|
+
pmon = @pm.fork do |t|
|
68
|
+
$0 = @worker_process_name % [wid] if @worker_process_name
|
69
|
+
w.install_signal_handlers
|
70
|
+
|
71
|
+
Daemon.change_privilege(@chumask, @chgroup)
|
72
|
+
File.umask(@chumask) if @chumask
|
73
|
+
|
74
|
+
## recreate the logger created at Server#main
|
75
|
+
#create_logger
|
76
|
+
|
77
|
+
w.main
|
78
|
+
end
|
79
|
+
|
80
|
+
ensure
|
81
|
+
w.close
|
82
|
+
end
|
83
|
+
|
84
|
+
return WorkerMonitor.new(w, wid, pmon)
|
85
|
+
end
|
86
|
+
|
87
|
+
def wait_tick
|
88
|
+
@pm.tick(0.5)
|
89
|
+
end
|
90
|
+
|
91
|
+
class WorkerMonitor
|
92
|
+
def initialize(worker, wid, pmon)
|
93
|
+
@worker = worker
|
94
|
+
@wid = wid
|
95
|
+
@pmon = pmon
|
96
|
+
end
|
97
|
+
|
98
|
+
def send_stop(stop_graceful)
|
99
|
+
@stop = true
|
100
|
+
if stop_graceful
|
101
|
+
@pmon.start_graceful_stop! if @pmon
|
102
|
+
else
|
103
|
+
@pmon.start_immediate_stop! if @pmon
|
104
|
+
end
|
105
|
+
nil
|
106
|
+
end
|
107
|
+
|
108
|
+
def send_reload
|
109
|
+
@pmon.send_signal(Daemon::Signals::RELOAD) if @pmon
|
110
|
+
nil
|
111
|
+
end
|
112
|
+
|
113
|
+
#def join
|
114
|
+
# @pmon.join if @pmon
|
115
|
+
# nil
|
116
|
+
#end
|
117
|
+
|
118
|
+
def alive?
|
119
|
+
return false unless @pmon
|
120
|
+
|
121
|
+
if stat = @pmon.try_join
|
122
|
+
@worker.logger.info "Worker #{@wid} finished#{@stop ? '' : ' unexpectedly'} with #{ProcessManager.format_join_status(stat)}"
|
123
|
+
@pmon = nil
|
124
|
+
return false
|
125
|
+
else
|
126
|
+
return true
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|