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.
- 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,103 @@
|
|
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 Server
|
21
|
+
include ConfigLoader
|
22
|
+
|
23
|
+
def initialize(worker_module, load_config_proc={}, &block)
|
24
|
+
@worker_module = worker_module
|
25
|
+
|
26
|
+
@stop = false
|
27
|
+
|
28
|
+
super(load_config_proc, &block)
|
29
|
+
|
30
|
+
reload_config
|
31
|
+
end
|
32
|
+
|
33
|
+
def before_run
|
34
|
+
end
|
35
|
+
|
36
|
+
def after_run
|
37
|
+
end
|
38
|
+
|
39
|
+
def stop(stop_graceful)
|
40
|
+
@stop = true
|
41
|
+
nil
|
42
|
+
end
|
43
|
+
|
44
|
+
def close
|
45
|
+
end
|
46
|
+
|
47
|
+
def restart(stop_graceful)
|
48
|
+
reload_config
|
49
|
+
@logger.reopen! if @logger
|
50
|
+
nil
|
51
|
+
end
|
52
|
+
|
53
|
+
def reload
|
54
|
+
reload_config
|
55
|
+
@logger.reopen! if @logger
|
56
|
+
nil
|
57
|
+
end
|
58
|
+
|
59
|
+
def install_signal_handlers
|
60
|
+
s = self
|
61
|
+
SignalThread.new do |st|
|
62
|
+
st.trap(Daemon::Signals::GRACEFUL_STOP) { s.stop(true) }
|
63
|
+
st.trap(Daemon::Signals::IMMEDIATE_STOP) { s.stop(false) }
|
64
|
+
st.trap(Daemon::Signals::GRACEFUL_RESTART) { s.restart(true) }
|
65
|
+
st.trap(Daemon::Signals::IMMEDIATE_RESTART) { s.restart(false) }
|
66
|
+
st.trap(Daemon::Signals::RELOAD) { s.reload }
|
67
|
+
st.trap(Daemon::Signals::DETACH) { s.stop(true) }
|
68
|
+
st.trap(Daemon::Signals::DUMP) { Sigdump.dump }
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def main
|
73
|
+
create_logger unless @logger
|
74
|
+
|
75
|
+
before_run
|
76
|
+
|
77
|
+
begin
|
78
|
+
run
|
79
|
+
ensure
|
80
|
+
after_run
|
81
|
+
end
|
82
|
+
|
83
|
+
ensure
|
84
|
+
close
|
85
|
+
end
|
86
|
+
|
87
|
+
module WorkerInitializer
|
88
|
+
def initialize
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def create_worker(wid)
|
95
|
+
w = Worker.new(self, wid)
|
96
|
+
w.extend(WorkerInitializer)
|
97
|
+
w.extend(@worker_module)
|
98
|
+
w.instance_eval { initialize }
|
99
|
+
w
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
@@ -0,0 +1,143 @@
|
|
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 SignalThread < Thread
|
21
|
+
def initialize(&block)
|
22
|
+
require 'thread'
|
23
|
+
|
24
|
+
@handlers = {}
|
25
|
+
|
26
|
+
@mutex = Mutex.new
|
27
|
+
@cond = ConditionVariable.new
|
28
|
+
@queue = []
|
29
|
+
@finished = false
|
30
|
+
|
31
|
+
block.call(self) if block
|
32
|
+
|
33
|
+
super(&method(:main))
|
34
|
+
end
|
35
|
+
|
36
|
+
def trap(sig, command=nil, &block)
|
37
|
+
# normalize signal names
|
38
|
+
sig = sig.to_s.upcase
|
39
|
+
if sig[0,3] == "SIG"
|
40
|
+
sig = sig[3..-1]
|
41
|
+
end
|
42
|
+
sig = sig.to_sym
|
43
|
+
|
44
|
+
old = @handlers[sig]
|
45
|
+
if block
|
46
|
+
Kernel.trap(sig) { enqueue(sig) }
|
47
|
+
@handlers[sig] = block
|
48
|
+
else
|
49
|
+
Kernel.trap(sig, command)
|
50
|
+
@handlers.delete(sig)
|
51
|
+
end
|
52
|
+
|
53
|
+
old
|
54
|
+
end
|
55
|
+
|
56
|
+
def handlers
|
57
|
+
handlers.dup
|
58
|
+
end
|
59
|
+
|
60
|
+
def stop
|
61
|
+
@mutex.synchronize do
|
62
|
+
## synchronized state 1
|
63
|
+
@finished = true
|
64
|
+
@cond.broadcast
|
65
|
+
## synchronized state 2
|
66
|
+
end
|
67
|
+
self
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def main
|
73
|
+
@mutex.lock
|
74
|
+
|
75
|
+
until @finished
|
76
|
+
## synchronized state 3
|
77
|
+
|
78
|
+
sig = @queue.shift
|
79
|
+
unless sig
|
80
|
+
## synchronized state 4
|
81
|
+
@cond.wait(@mutex, 1)
|
82
|
+
next
|
83
|
+
end
|
84
|
+
|
85
|
+
## synchronized state 5
|
86
|
+
|
87
|
+
@mutex.unlock
|
88
|
+
begin
|
89
|
+
@handlers[sig].call(sig)
|
90
|
+
rescue
|
91
|
+
ServerEngine.dump_uncaught_error($!)
|
92
|
+
ensure
|
93
|
+
@mutex.lock
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
## synchronized state 6
|
98
|
+
nil
|
99
|
+
|
100
|
+
ensure
|
101
|
+
@mutex.unlock
|
102
|
+
@finished = false
|
103
|
+
end
|
104
|
+
|
105
|
+
def enqueue(sig)
|
106
|
+
@queue << sig
|
107
|
+
|
108
|
+
unless @mutex.try_lock
|
109
|
+
#
|
110
|
+
# here couldn't acquire @mutex.
|
111
|
+
#
|
112
|
+
# A) a thread is in synchronized state 1 or 2.
|
113
|
+
# In this case, here doesn't have to broadcast because the thread will/did broadcast.
|
114
|
+
#
|
115
|
+
# B) `self` thread is in synchronized state 3
|
116
|
+
# In this case, here doesn't have to broadcast because the `self` thread will
|
117
|
+
# take a task from the queue soon.
|
118
|
+
#
|
119
|
+
# C) `self` thread is in synchronized state 4
|
120
|
+
# In this case, here needs to broadcast but doesn't broadcast. Thus it causes
|
121
|
+
# blocking upto 1 second :(
|
122
|
+
#
|
123
|
+
# D) `self` thread is in synchronized state 5
|
124
|
+
# In this case, here doesn't have to broadcast because the `self` thread will
|
125
|
+
# change to synchronized state 3 or 6 soon.
|
126
|
+
#
|
127
|
+
# E) the main thread (the only thread which calls this method) is in synchronized
|
128
|
+
# state 7. In this case, here doesn't have to broadcast.
|
129
|
+
#
|
130
|
+
return
|
131
|
+
end
|
132
|
+
|
133
|
+
## synchronized state 7
|
134
|
+
|
135
|
+
begin
|
136
|
+
@cond.broadcast
|
137
|
+
ensure
|
138
|
+
@mutex.unlock
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,230 @@
|
|
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 Supervisor
|
21
|
+
include ConfigLoader
|
22
|
+
|
23
|
+
def initialize(server_module, worker_module, load_config_proc={}, &block)
|
24
|
+
@server_module = server_module
|
25
|
+
@worker_module = worker_module
|
26
|
+
|
27
|
+
@detach_flag = BlockingFlag.new
|
28
|
+
@stop = false
|
29
|
+
|
30
|
+
@pm = ProcessManager.new(
|
31
|
+
auto_tick: false,
|
32
|
+
graceful_kill_signal: Daemon::Signals::GRACEFUL_STOP,
|
33
|
+
immediate_kill_signal: Daemon::Signals::IMMEDIATE_STOP,
|
34
|
+
auto_heartbeat: true,
|
35
|
+
abort_on_heartbeat_error: false,
|
36
|
+
)
|
37
|
+
|
38
|
+
super(load_config_proc, &block)
|
39
|
+
|
40
|
+
reload_config
|
41
|
+
|
42
|
+
@create_server_proc = Supervisor.create_server_proc(server_module, worker_module, @config)
|
43
|
+
@server_process_name = @config[:server_process_name]
|
44
|
+
|
45
|
+
@restart_server_process = !!@config[:restart_server_process]
|
46
|
+
@enable_detach = !!@config[:enable_detach]
|
47
|
+
@exit_on_detach = !!@config[:exit_on_detach]
|
48
|
+
@disable_reload = !!@config[:disable_reload]
|
49
|
+
end
|
50
|
+
|
51
|
+
def reload_config
|
52
|
+
super
|
53
|
+
|
54
|
+
@server_detach_wait = @config[:server_detach_wait] || 10.0
|
55
|
+
@server_restart_wait = @config[:server_restart_wait] || 1.0
|
56
|
+
|
57
|
+
@pm.configure(@config, prefix: 'server_')
|
58
|
+
|
59
|
+
nil
|
60
|
+
end
|
61
|
+
|
62
|
+
module ServerInitializer
|
63
|
+
def initialize
|
64
|
+
reload_config
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.create_server_proc(server_module, worker_module, config)
|
69
|
+
wt = config[:worker_type] || 'embedded'
|
70
|
+
case wt
|
71
|
+
when 'embedded'
|
72
|
+
server_class = EmbeddedServer
|
73
|
+
when 'process'
|
74
|
+
server_class = MultiProcessServer
|
75
|
+
when 'thread'
|
76
|
+
server_class = MultiThreadServer
|
77
|
+
else
|
78
|
+
raise ArgumentError, "unexpected :worker_type option #{wt}"
|
79
|
+
end
|
80
|
+
|
81
|
+
lambda {|load_config_proc,logger|
|
82
|
+
s = server_class.new(worker_module, load_config_proc)
|
83
|
+
s.logger = logger
|
84
|
+
s.extend(ServerInitializer)
|
85
|
+
s.extend(server_module) if server_module
|
86
|
+
s.instance_eval { initialize }
|
87
|
+
s
|
88
|
+
}
|
89
|
+
end
|
90
|
+
|
91
|
+
def create_server(logger)
|
92
|
+
@create_server_proc.call(@load_config_proc, logger)
|
93
|
+
end
|
94
|
+
|
95
|
+
def stop(stop_graceful)
|
96
|
+
@stop = true
|
97
|
+
send_signal(stop_graceful ? Daemon::Signals::GRACEFUL_STOP : Daemon::Signals::IMMEDIATE_STOP)
|
98
|
+
end
|
99
|
+
|
100
|
+
def restart(stop_graceful)
|
101
|
+
reload_config
|
102
|
+
@logger.reopen! if @logger
|
103
|
+
if @restart_server_process
|
104
|
+
send_signal(stop_graceful ? Daemon::Signals::GRACEFUL_STOP : Daemon::Signals::IMMEDIATE_STOP)
|
105
|
+
else
|
106
|
+
send_signal(stop_graceful ? Daemon::Signals::GRACEFUL_RESTART : Daemon::Signals::IMMEDIATE_RESTART)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def reload
|
111
|
+
unless @disable_reload
|
112
|
+
reload_config
|
113
|
+
end
|
114
|
+
@logger.reopen! if @logger
|
115
|
+
send_signal(Daemon::Signals::RELOAD)
|
116
|
+
end
|
117
|
+
|
118
|
+
def detach(stop_graceful)
|
119
|
+
if @enable_detach
|
120
|
+
@detach_flag.set!
|
121
|
+
send_signal(stop_graceful ? Daemon::Signals::GRACEFUL_STOP : Daemon::Signals::IMMEDIATE_STOP)
|
122
|
+
else
|
123
|
+
stop(stop_graceful)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def install_signal_handlers
|
128
|
+
s = self
|
129
|
+
SignalThread.new do |st|
|
130
|
+
st.trap(Daemon::Signals::GRACEFUL_STOP) { s.stop(true) }
|
131
|
+
st.trap(Daemon::Signals::IMMEDIATE_STOP) { s.stop(false) }
|
132
|
+
st.trap(Daemon::Signals::GRACEFUL_RESTART) { s.restart(true) }
|
133
|
+
st.trap(Daemon::Signals::IMMEDIATE_RESTART) { s.restart(false) }
|
134
|
+
st.trap(Daemon::Signals::RELOAD) { s.reload }
|
135
|
+
st.trap(Daemon::Signals::DETACH) { s.detach(true) }
|
136
|
+
st.trap(Daemon::Signals::DUMP) { Sigdump.dump }
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def main
|
141
|
+
# just in case Supervisor is not created by Daemon
|
142
|
+
create_logger unless @logger
|
143
|
+
|
144
|
+
@pmon = start_server
|
145
|
+
|
146
|
+
while true
|
147
|
+
# keep the child process alive in this loop
|
148
|
+
until @detach_flag.wait(0.5)
|
149
|
+
if stat = try_join
|
150
|
+
return if @stop # supervisor stoppped explicitly
|
151
|
+
|
152
|
+
# child process died unexpectedly.
|
153
|
+
# sleep @server_detach_wait sec and reboot process
|
154
|
+
@pmon = reboot_server
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
wait_until = Time.now + @server_detach_wait
|
159
|
+
while (w = wait_until - Time.now) > 0
|
160
|
+
break if try_join
|
161
|
+
sleep [0.5, w].min
|
162
|
+
end
|
163
|
+
|
164
|
+
return if @exit_on_detach
|
165
|
+
|
166
|
+
@detach_flag.reset!
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def logger=(logger)
|
171
|
+
super
|
172
|
+
@pm.logger = @logger
|
173
|
+
end
|
174
|
+
|
175
|
+
private
|
176
|
+
|
177
|
+
def send_signal(sig)
|
178
|
+
@pmon.send_signal(sig) if @pmon
|
179
|
+
nil
|
180
|
+
end
|
181
|
+
|
182
|
+
def try_join
|
183
|
+
if stat = @pmon.try_join
|
184
|
+
@logger.info "Server finished#{@stop ? '' : ' unexpectedly'} with #{ProcessManager.format_join_status(stat)}"
|
185
|
+
@pmon = nil
|
186
|
+
return stat
|
187
|
+
else
|
188
|
+
@pm.tick
|
189
|
+
return false
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def start_server
|
194
|
+
s = create_server(logger)
|
195
|
+
@last_start_time = Time.now
|
196
|
+
|
197
|
+
begin
|
198
|
+
m = @pm.fork do
|
199
|
+
$0 = @server_process_name if @server_process_name
|
200
|
+
s.install_signal_handlers
|
201
|
+
|
202
|
+
s.main
|
203
|
+
end
|
204
|
+
|
205
|
+
return m
|
206
|
+
ensure
|
207
|
+
s.close
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def reboot_server
|
212
|
+
# try reboot for ever until @detach_flag is set
|
213
|
+
while true
|
214
|
+
wait = @server_restart_wait - (Time.now - @last_start_time)
|
215
|
+
if @detach_flag.wait(wait > 0 ? wait : 0.1)
|
216
|
+
break
|
217
|
+
end
|
218
|
+
|
219
|
+
begin
|
220
|
+
return start_server
|
221
|
+
rescue
|
222
|
+
ServerEngine.dump_uncaught_error($!)
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
return nil
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
end
|