serverengine 2.0.0pre1-x64-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,98 @@
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 'socket'
19
+
20
+ module ServerEngine
21
+ module SocketManagerUnix
22
+
23
+ module ClientModule
24
+ private
25
+
26
+ def connect_peer(path)
27
+ return UNIXSocket.new(path)
28
+ end
29
+
30
+ def recv_tcp(peer, sent)
31
+ return peer.recv_io(TCPServer)
32
+ end
33
+
34
+ def recv_udp(peer, sent)
35
+ return peer.recv_io(UDPSocket)
36
+ end
37
+ end
38
+
39
+ module ServerModule
40
+ private
41
+
42
+ def listen_tcp_new(bind_ip, port)
43
+ sock = TCPServer.new(bind_ip.to_s, port)
44
+ sock.listen(Socket::SOMAXCONN) # TODO make backlog configurable if necessary
45
+ return sock
46
+ end
47
+
48
+ def listen_udp_new(bind_ip, port)
49
+ if bind_ip.ipv6?
50
+ sock = UDPSocket.new(Socket::AF_INET6)
51
+ else
52
+ sock = UDPSocket.new
53
+ end
54
+ sock.bind(bind_ip.to_s, port)
55
+ return sock
56
+ end
57
+
58
+ def start_server(path)
59
+ # return absolute path so that client can connect to this path
60
+ # when client changed working directory
61
+ path = File.expand_path(path)
62
+
63
+ @server = UNIXServer.new(path)
64
+
65
+ @thread = Thread.new do
66
+ begin
67
+ while peer = @server.accept
68
+ Thread.new(peer, &method(:process_peer)) # process_peer calls send_socket
69
+ end
70
+ rescue => e
71
+ unless @server.closed?
72
+ ServerEngine.dump_uncaught_error(e)
73
+ end
74
+ end
75
+ end
76
+
77
+ return path
78
+ end
79
+
80
+ def stop_server
81
+ @tcp_sockets.reject! {|key,lsock| lsock.close; true }
82
+ @udp_sockets.reject! {|key,usock| usock.close; true }
83
+ @server.close unless @server.closed?
84
+ # It cause dead lock and can't finish when joining thread using Ruby 2.1 on linux.
85
+ @thread.join if RUBY_VERSION >= "2.2"
86
+ end
87
+
88
+ def send_socket(peer, pid, method, bind, port)
89
+ sock = send(method, bind, port) # calls listen_tcp or listen_udp
90
+
91
+ SocketManager.send_peer(peer, nil)
92
+
93
+ peer.send_io sock
94
+ end
95
+ end
96
+
97
+ end
98
+ end
@@ -0,0 +1,154 @@
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 'socket'
19
+ require 'ipaddr'
20
+
21
+ require_relative 'winsock'
22
+
23
+ module ServerEngine
24
+ module SocketManagerWin
25
+
26
+ module ClientModule
27
+ private
28
+
29
+ def connect_peer(addr)
30
+ return TCPSocket.open("127.0.0.1", addr)
31
+ end
32
+
33
+ def recv_tcp(peer, sent)
34
+ proto = WinSock::WSAPROTOCOL_INFO.from_bin(sent)
35
+
36
+ handle = WinSock.WSASocketA(Socket::AF_INET, Socket::SOCK_STREAM, 0, proto, 0, 1)
37
+ if handle == WinSock::INVALID_SOCKET
38
+ RbWinSock.raise_last_error("WSASocketA(2)")
39
+ end
40
+
41
+ return RbWinSock.wrap_io_handle(TCPServer, handle, 0)
42
+ end
43
+
44
+ def recv_udp(peer, sent)
45
+ proto = WinSock::WSAPROTOCOL_INFO.from_bin(sent)
46
+
47
+ handle = WinSock.WSASocketA(Socket::AF_INET, Socket::SOCK_DGRAM, 0, proto, 0, 1)
48
+ if handle == WinSock::INVALID_SOCKET
49
+ RbWinSock.raise_last_error("WSASocketA(2)")
50
+ end
51
+
52
+ return RbWinSock.wrap_io_handle(UDPSocket, handle, 0)
53
+ end
54
+ end
55
+
56
+ module ServerModule
57
+ private
58
+
59
+ def listen_tcp_new(bind_ip, port)
60
+ sock_addr = Socket.pack_sockaddr_in(port, bind_ip.to_s)
61
+
62
+ handle = WinSock.WSASocketA(Socket::AF_INET, Socket::SOCK_STREAM, Socket::IPPROTO_TCP, nil, 0, 1)
63
+ if handle == WinSock::INVALID_SOCKET
64
+ RbWinSock.raise_last_error("WSASocketA(2)")
65
+ end
66
+
67
+ # wrap in TCPServer immediately so that its finalizer safely closes the handle
68
+ sock = RbWinSock.wrap_io_handle(TCPServer, handle, 0)
69
+
70
+ unless WinSock.bind(sock.handle, sock_addr, sock_addr.bytesize) == 0
71
+ RbWinSock.raise_last_error("bind(2)")
72
+ end
73
+ unless WinSock.listen(sock.handle, Socket::SOMAXCONN) == 0
74
+ RbWinSock.raise_last_error("listen(2)")
75
+ end
76
+
77
+ return sock
78
+ end
79
+
80
+ def listen_udp_new(bind_ip, port)
81
+ sock_addr = Socket.pack_sockaddr_in(port, bind_ip.to_s)
82
+
83
+ if IPAddr.new(IPSocket.getaddress(bind_ip.to_s)).ipv4?
84
+ handle = WinSock.WSASocketA(Socket::AF_INET, Socket::SOCK_DGRAM, Socket::IPPROTO_UDP, nil, 0, 1)
85
+ else
86
+ handle = WinSock.WSASocketA(Socket::AF_INET6, Socket::SOCK_DGRAM, Socket::IPPROTO_UDP, nil, 0, 1)
87
+ end
88
+
89
+ if handle == WinSock::INVALID_SOCKET
90
+ RbWinSock.raise_last_error("WSASocketA(2)")
91
+ end
92
+
93
+ # wrap in UDPSocket immediately so that its finalizer safely closes the handle
94
+ sock = RbWinSock.wrap_io_handle(UDPSocket, handle, 0)
95
+
96
+ unless WinSock.bind(sock.handle, sock_addr, sock_addr.bytesize) == 0
97
+ RbWinSock.raise_last_error("bind(2)")
98
+ end
99
+
100
+ return sock
101
+ end
102
+
103
+ def htons(h)
104
+ [h].pack("S").unpack("n")[0]
105
+ end
106
+
107
+ def start_server(addr)
108
+ # TODO: use TCPServer, but this is risky because using not conflict path is easy,
109
+ # but using not conflict port is difficult. Then We had better implement using NamedPipe.
110
+ @server = TCPServer.new("127.0.0.1", addr)
111
+ @thread = Thread.new do
112
+ begin
113
+ while peer = @server.accept
114
+ Thread.new(peer, &method(:process_peer)) # process_peer calls send_socket
115
+ end
116
+ rescue => e
117
+ unless @server.closed?
118
+ ServerEngine.dump_uncaught_error(e)
119
+ end
120
+ end
121
+ end
122
+
123
+ return path
124
+ end
125
+
126
+ def stop_server
127
+ @tcp_sockets.reject! {|key,lsock| lsock.close; true }
128
+ @udp_sockets.reject! {|key,usock| usock.close; true }
129
+ @server.close unless @server.closed?
130
+ @thread.join
131
+ end
132
+
133
+ def send_socket(peer, pid, method, bind, port)
134
+ case method
135
+ when :listen_tcp
136
+ sock = listen_tcp(bind, port)
137
+ type = Socket::SOCK_STREAM
138
+ when :listen_udp
139
+ sock = listen_udp(bind, port)
140
+ type = Socket::SOCK_DGRAM
141
+ else
142
+ raise ArgumentError, "Unknown method: #{method.inspect}"
143
+ end
144
+
145
+ proto = WinSock::WSAPROTOCOL_INFO.malloc
146
+ unless WinSock.WSADuplicateSocketA(sock.handle, pid, proto) == 0
147
+ RbWinSock.raise_last_error("WSADuplicateSocketA(3)")
148
+ end
149
+
150
+ SocketManager.send_peer(peer, proto.to_bin)
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,313 @@
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 'serverengine/config_loader'
19
+ require 'serverengine/blocking_flag'
20
+ require 'serverengine/process_manager'
21
+ require 'serverengine/command_sender'
22
+ require 'serverengine/signals'
23
+
24
+ require 'serverengine/embedded_server'
25
+ require 'serverengine/multi_process_server'
26
+ require 'serverengine/multi_thread_server'
27
+ require 'serverengine/multi_spawn_server'
28
+
29
+ module ServerEngine
30
+
31
+ class Supervisor
32
+ include ConfigLoader
33
+
34
+ def initialize(server_module, worker_module, load_config_proc={}, &block)
35
+ @server_module = server_module
36
+ @worker_module = worker_module
37
+
38
+ @detach_flag = BlockingFlag.new
39
+ @stop = false
40
+
41
+ @pm = ProcessManager.new(
42
+ auto_tick: false,
43
+ graceful_kill_signal: Signals::GRACEFUL_STOP,
44
+ immediate_kill_signal: Signals::IMMEDIATE_STOP,
45
+ enable_heartbeat: true,
46
+ auto_heartbeat: true,
47
+ )
48
+
49
+ super(load_config_proc, &block)
50
+
51
+ @create_server_proc = Supervisor.create_server_proc(server_module, worker_module, @config)
52
+ @server_process_name = @config[:server_process_name]
53
+
54
+ @restart_server_process = !!@config[:restart_server_process]
55
+ @unrecoverable_exit_codes = @config.fetch(:unrecoverable_exit_codes, [])
56
+ @enable_detach = !!@config[:enable_detach]
57
+ @exit_on_detach = !!@config[:exit_on_detach]
58
+ @disable_reload = !!@config[:disable_reload]
59
+
60
+ @command_pipe = @config.fetch(:command_pipe, nil)
61
+
62
+ @command_sender = @config.fetch(:command_sender, ServerEngine.windows? ? "pipe" : "signal")
63
+ if @command_sender == "pipe"
64
+ extend CommandSender::Pipe
65
+ else
66
+ extend CommandSender::Signal
67
+ end
68
+ end
69
+
70
+ # server is available after start_server() call.
71
+ attr_reader :server
72
+
73
+ def reload_config
74
+ super
75
+
76
+ @server_detach_wait = @config[:server_detach_wait] || 10.0
77
+ @server_restart_wait = @config[:server_restart_wait] || 1.0
78
+
79
+ @pm.configure(@config, prefix: 'server_')
80
+
81
+ nil
82
+ end
83
+
84
+ module ServerInitializer
85
+ def initialize
86
+ reload_config
87
+ end
88
+ end
89
+
90
+ def self.create_server_proc(server_module, worker_module, config)
91
+ wt = config[:worker_type] || 'embedded'
92
+ case wt
93
+ when 'embedded'
94
+ server_class = EmbeddedServer
95
+ when 'process'
96
+ server_class = MultiProcessServer
97
+ when 'thread'
98
+ server_class = MultiThreadServer
99
+ when 'spawn'
100
+ server_class = MultiSpawnServer
101
+ else
102
+ raise ArgumentError, "unexpected :worker_type option #{wt}"
103
+ end
104
+
105
+ lambda {|load_config_proc,logger|
106
+ s = server_class.new(worker_module, load_config_proc)
107
+ s.logger = logger
108
+ s.extend(ServerInitializer)
109
+ s.extend(server_module) if server_module
110
+ s.instance_eval { initialize }
111
+ s
112
+ }
113
+ end
114
+
115
+ def create_server(logger)
116
+ @server = @create_server_proc.call(@load_config_proc, logger)
117
+ end
118
+
119
+ def stop(stop_graceful)
120
+ @stop = true
121
+ _stop(stop_graceful)
122
+ end
123
+
124
+ def restart(stop_graceful)
125
+ reload_config
126
+ @logger.reopen! if @logger
127
+ if @restart_server_process
128
+ _stop(stop_graceful)
129
+ else
130
+ _restart(stop_graceful)
131
+ end
132
+ end
133
+
134
+ def reload
135
+ unless @disable_reload
136
+ reload_config
137
+ end
138
+ @logger.reopen! if @logger
139
+ _reload
140
+ end
141
+
142
+ def detach(stop_graceful)
143
+ if @enable_detach
144
+ @detach_flag.set!
145
+ _stop(stop_graceful)
146
+ else
147
+ stop(stop_graceful)
148
+ end
149
+ end
150
+
151
+ def install_signal_handlers
152
+ s = self
153
+ if @command_pipe
154
+ Thread.new do
155
+ until @command_pipe.closed?
156
+ case @command_pipe.gets.chomp
157
+ when "GRACEFUL_STOP"
158
+ s.stop(true)
159
+ when "IMMEDIATE_STOP"
160
+ s.stop(false)
161
+ when "GRACEFUL_RESTART"
162
+ s.restart(true)
163
+ when "IMMEDIATE_RESTART"
164
+ s.restart(false)
165
+ when "RELOAD"
166
+ s.reload
167
+ when "DETACH"
168
+ s.detach(true)
169
+ when "DUMP"
170
+ Sigdump.dump
171
+ end
172
+ end
173
+ end
174
+ else
175
+ SignalThread.new do |st|
176
+ st.trap(Signals::GRACEFUL_STOP) { s.stop(true) }
177
+ st.trap(Signals::IMMEDIATE_STOP) { s.stop(false) }
178
+ st.trap(Signals::GRACEFUL_RESTART) { s.restart(true) }
179
+ st.trap(Signals::IMMEDIATE_RESTART) { s.restart(false) }
180
+ st.trap(Signals::RELOAD) { s.reload }
181
+ st.trap(Signals::DETACH) { s.detach(true) }
182
+ st.trap(Signals::DUMP) { Sigdump.dump }
183
+ end
184
+ end
185
+ end
186
+
187
+ def main
188
+ # just in case Supervisor is not created by Daemon
189
+ create_logger unless @logger
190
+
191
+ @pmon = start_server
192
+
193
+ while true
194
+ # keep the child process alive in this loop
195
+ until @detach_flag.wait(0.5)
196
+ if try_join
197
+ return if @stop # supervisor stoppped explicitly
198
+ if @stop_status # set exit code told by server
199
+ raise SystemExit.new(@stop_status)
200
+ end
201
+
202
+ # child process died unexpectedly.
203
+ # sleep @server_detach_wait sec and reboot process
204
+ @pmon = reboot_server
205
+ end
206
+ end
207
+
208
+ wait_until = Time.now + @server_detach_wait
209
+ while (w = wait_until - Time.now) > 0
210
+ break if try_join
211
+ sleep [0.5, w].min
212
+ end
213
+
214
+ return if @exit_on_detach
215
+
216
+ @detach_flag.reset!
217
+ end
218
+ end
219
+
220
+ def logger=(logger)
221
+ super
222
+ @pm.logger = @logger
223
+ end
224
+
225
+ private
226
+
227
+ def send_signal(sig)
228
+ @pmon.send_signal(sig) if @pmon
229
+ nil
230
+ end
231
+
232
+ def try_join
233
+ if stat = @pmon.try_join
234
+ @logger.info "Server finished#{@stop ? '' : ' unexpectedly'} with #{ServerEngine.format_join_status(stat)}"
235
+ if !@stop && stat.is_a?(Process::Status) && stat.exited? && @unrecoverable_exit_codes.include?(stat.exitstatus)
236
+ @stop_status = stat.exitstatus
237
+ end
238
+ @pmon = nil
239
+ return stat
240
+ else
241
+ @pm.tick
242
+ return false
243
+ end
244
+ end
245
+
246
+ def start_server
247
+ if @command_sender == "pipe"
248
+ inpipe, @command_sender_pipe = IO.pipe
249
+ end
250
+
251
+ unless ServerEngine.windows?
252
+ s = create_server(logger)
253
+ @last_start_time = Time.now
254
+
255
+ begin
256
+ m = @pm.fork do
257
+ $0 = @server_process_name if @server_process_name
258
+ if @command_sender == "pipe"
259
+ @command_sender_pipe.close
260
+ s.instance_variable_set(:@command_pipe, inpipe)
261
+ end
262
+ s.install_signal_handlers
263
+
264
+ begin
265
+ s.main
266
+ rescue SystemExit => e
267
+ @logger.info "Server is exitting with code #{e.status}"
268
+ exit! e.status
269
+ end
270
+ end
271
+ if @command_sender == "pipe"
272
+ inpipe.close
273
+ end
274
+
275
+ return m
276
+ ensure
277
+ s.after_start
278
+ end
279
+ else # if ServerEngine.windows?
280
+ exconfig = {}
281
+ if @command_sender == "pipe"
282
+ exconfig[:in] = inpipe
283
+ end
284
+ @last_start_time = Time.now
285
+ m = @pm.spawn(*Array(config[:windows_daemon_cmdline]), exconfig)
286
+ if @command_sender == "pipe"
287
+ inpipe.close
288
+ end
289
+
290
+ return m
291
+ end
292
+ end
293
+
294
+ def reboot_server
295
+ # try reboot for ever until @detach_flag is set
296
+ while true
297
+ wait = @server_restart_wait - (Time.now - @last_start_time)
298
+ if @detach_flag.wait(wait > 0 ? wait : 0.1)
299
+ break
300
+ end
301
+
302
+ begin
303
+ return start_server
304
+ rescue
305
+ ServerEngine.dump_uncaught_error($!)
306
+ end
307
+ end
308
+
309
+ return nil
310
+ end
311
+ end
312
+
313
+ end