serverengine 2.0.0pre1-x86-mingw32

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.
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