serverengine 2.0.7 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -50,19 +50,30 @@ module ServerEngine
50
50
  private
51
51
 
52
52
  def listen_tcp_new(bind_ip, port)
53
- sock = TCPServer.new(bind_ip.to_s, port)
54
- sock.listen(Socket::SOMAXCONN) # TODO make backlog configurable if necessary
55
- return sock
53
+ if ENV['SERVERENGINE_USE_SOCKET_REUSEPORT'] == '1'
54
+ # Based on Addrinfo#listen
55
+ tsock = Socket.new(bind_ip.ipv6? ? ::Socket::AF_INET6 : ::Socket::AF_INET, ::Socket::SOCK_STREAM, 0)
56
+ tsock.ipv6only! if bind_ip.ipv6?
57
+ tsock.setsockopt(:SOCKET, :REUSEPORT, true)
58
+ tsock.setsockopt(:SOCKET, :REUSEADDR, true)
59
+ tsock.bind(Addrinfo.tcp(bind_ip.to_s, port))
60
+ tsock.listen(::Socket::SOMAXCONN)
61
+ tsock.autoclose = false
62
+ TCPServer.for_fd(tsock.fileno)
63
+ else
64
+ # TCPServer.new doesn't set IPV6_V6ONLY flag, so use Addrinfo class instead.
65
+ # TODO: make backlog configurable if necessary
66
+ tsock = Addrinfo.tcp(bind_ip.to_s, port).listen(::Socket::SOMAXCONN)
67
+ tsock.autoclose = false
68
+ TCPServer.for_fd(tsock.fileno)
69
+ end
56
70
  end
57
71
 
58
72
  def listen_udp_new(bind_ip, port)
59
- if bind_ip.ipv6?
60
- sock = UDPSocket.new(Socket::AF_INET6)
61
- else
62
- sock = UDPSocket.new(Socket::AF_INET)
63
- end
64
- sock.bind(bind_ip.to_s, port)
65
- return sock
73
+ # UDPSocket.new doesn't set IPV6_V6ONLY flag, so use Addrinfo class instead.
74
+ usock = Addrinfo.udp(bind_ip.to_s, port).bind
75
+ usock.autoclose = false
76
+ UDPSocket.for_fd(usock.fileno)
66
77
  end
67
78
 
68
79
  def start_server(path)
@@ -70,7 +81,12 @@ module ServerEngine
70
81
  # when client changed working directory
71
82
  path = File.expand_path(path)
72
83
 
73
- @server = UNIXServer.new(path)
84
+ begin
85
+ old_umask = File.umask(0077) # Protect unix socket from other users
86
+ @server = UNIXServer.new(path)
87
+ ensure
88
+ File.umask(old_umask)
89
+ end
74
90
 
75
91
  @thread = Thread.new do
76
92
  begin
@@ -96,7 +112,14 @@ module ServerEngine
96
112
  end
97
113
 
98
114
  def send_socket(peer, pid, method, bind, port)
99
- sock = send(method, bind, port) # calls listen_tcp or listen_udp
115
+ sock = case method
116
+ when :listen_tcp
117
+ listen_tcp(bind, port)
118
+ when :listen_udp
119
+ listen_udp(bind, port)
120
+ else
121
+ raise ArgumentError, "Unknown method: #{method.inspect}"
122
+ end
100
123
 
101
124
  SocketManager.send_peer(peer, nil)
102
125
 
@@ -1,3 +1,3 @@
1
1
  module ServerEngine
2
- VERSION = "2.0.7"
2
+ VERSION = "2.3.0"
3
3
  end
@@ -21,6 +21,7 @@ module ServerEngine
21
21
  require 'fiddle/import'
22
22
  require 'fiddle/types'
23
23
  require 'socket'
24
+ require 'rbconfig'
24
25
 
25
26
  extend Fiddle::Importer
26
27
 
@@ -78,6 +79,19 @@ module ServerEngine
78
79
  end
79
80
  end
80
81
 
82
+ def self.last_error
83
+ # On Ruby 3.0 calling WSAGetLastError here can't retrieve correct error
84
+ # code because Ruby's internal code resets it.
85
+ # See also:
86
+ # * https://github.com/ruby/fiddle/issues/72
87
+ # * https://bugs.ruby-lang.org/issues/17813
88
+ if Fiddle.respond_to?(:win32_last_socket_error)
89
+ Fiddle.win32_last_socket_error || 0
90
+ else
91
+ self.WSAGetLastError
92
+ end
93
+ end
94
+
81
95
  INVALID_SOCKET = -1
82
96
  end
83
97
 
@@ -85,21 +99,13 @@ module ServerEngine
85
99
  extend Fiddle::Importer
86
100
 
87
101
  dlload "kernel32"
88
- extern "int GetModuleFileNameA(int, char *, int)"
89
102
  extern "int CloseHandle(int)"
90
103
 
91
- ruby_bin_path_buf = Fiddle::Pointer.malloc(1000)
92
- GetModuleFileNameA(0, ruby_bin_path_buf, ruby_bin_path_buf.size)
93
-
94
- ruby_bin_path = ruby_bin_path_buf.to_s.gsub(/\\/, '/')
95
- ruby_dll_paths = File.dirname(ruby_bin_path) + '/*msvcr*ruby*.dll'
96
- ruby_dll_path = Dir.glob(ruby_dll_paths).first
97
- dlload ruby_dll_path
98
-
104
+ dlload RbConfig::CONFIG['LIBRUBY_SO']
99
105
  extern "int rb_w32_map_errno(int)"
100
106
 
101
107
  def self.raise_last_error(name)
102
- errno = rb_w32_map_errno(WinSock.WSAGetLastError)
108
+ errno = rb_w32_map_errno(WinSock.last_error)
103
109
  raise SystemCallError.new(name, errno)
104
110
  end
105
111
 
data/lib/serverengine.rb CHANGED
@@ -35,12 +35,27 @@ module ServerEngine
35
35
 
36
36
  def self.ruby_bin_path
37
37
  if ServerEngine.windows?
38
- require 'windows/library'
39
- ruby_path = "\0" * 256
40
- Windows::Library::GetModuleFileName.call(0, ruby_path, 256)
41
- return ruby_path.rstrip.gsub(/\\/, '/')
38
+ ServerEngine::Win32.ruby_bin_path
42
39
  else
43
- return File.join(RbConfig::CONFIG["bindir"], RbConfig::CONFIG["RUBY_INSTALL_NAME"]) + RbConfig::CONFIG["EXEEXT"]
40
+ File.join(RbConfig::CONFIG["bindir"], RbConfig::CONFIG["RUBY_INSTALL_NAME"]) + RbConfig::CONFIG["EXEEXT"]
41
+ end
42
+ end
43
+
44
+ if ServerEngine.windows?
45
+ module Win32
46
+ require 'fiddle/import'
47
+
48
+ extend Fiddle::Importer
49
+
50
+ dlload "kernel32"
51
+ extern "int GetModuleFileNameW(int, void *, int)"
52
+
53
+ def self.ruby_bin_path
54
+ ruby_bin_path_buf = Fiddle::Pointer.malloc(1024)
55
+ len = GetModuleFileNameW(0, ruby_bin_path_buf, ruby_bin_path_buf.size / 2)
56
+ path_bytes = ruby_bin_path_buf[0, len * 2]
57
+ path_bytes.encode('UTF-8', 'UTF-16LE').gsub(/\\/, '/')
58
+ end
44
59
  end
45
60
  end
46
61
  end
data/serverengine.gemspec CHANGED
@@ -27,11 +27,5 @@ Gem::Specification.new do |gem|
27
27
  gem.add_development_dependency 'rake-compiler-dock', ['~> 0.5.0']
28
28
  gem.add_development_dependency 'rake-compiler', ['~> 0.9.4']
29
29
 
30
- # build gem for a certain platform. see also Rakefile
31
- fake_platform = ENV['GEM_BUILD_FAKE_PLATFORM'].to_s
32
- gem.platform = fake_platform unless fake_platform.empty?
33
- if /mswin|mingw/ =~ fake_platform || (/mswin|mingw/ =~ RUBY_PLATFORM && fake_platform.empty?)
34
- # windows dependencies
35
- gem.add_runtime_dependency("windows-pr", ["~> 1.2.5"])
36
- end
30
+ gem.add_development_dependency "timecop", ["~> 0.9.5"]
37
31
  end
@@ -20,10 +20,16 @@ describe ServerEngine::BlockingFlag do
20
20
  it 'wait_for_set timeout' do
21
21
  start = Time.now
22
22
 
23
- subject.wait_for_set(0.01)
23
+ subject.wait_for_set(0.1)
24
24
  elapsed = Time.now - start
25
25
 
26
- elapsed.should >= 0.01
26
+ if ServerEngine.windows? && ENV['CI'] == 'True'
27
+ # timer seems low accuracy on Windows CI container, often a bit shorter
28
+ # than expected
29
+ elapsed.should >= 0.1 * 0.95
30
+ else
31
+ elapsed.should >= 0.1
32
+ end
27
33
  end
28
34
 
29
35
  it 'wait_for_reset timeout' do
@@ -31,10 +37,16 @@ describe ServerEngine::BlockingFlag do
31
37
 
32
38
  start = Time.now
33
39
 
34
- subject.wait_for_reset(0.01)
40
+ subject.wait_for_reset(0.1)
35
41
  elapsed = Time.now - start
36
42
 
37
- elapsed.should >= 0.01
43
+ if ServerEngine.windows? && ENV['CI'] == 'True'
44
+ # timer seems low accuracy on Windows CI container, often a bit shorter
45
+ # than expected
46
+ elapsed.should >= 0.1 * 0.95
47
+ else
48
+ elapsed.should >= 0.1
49
+ end
38
50
  end
39
51
 
40
52
  it 'wait' do
@@ -1,4 +1,5 @@
1
1
  require 'stringio'
2
+ require 'timecop'
2
3
 
3
4
  describe ServerEngine::DaemonLogger do
4
5
  before { FileUtils.rm_rf("tmp") }
@@ -172,4 +173,28 @@ describe ServerEngine::DaemonLogger do
172
173
  $stderr = STDERR
173
174
  stderr.should_not =~ /(log shifting failed|log writing failed|log rotation inter-process lock failed)/
174
175
  end
176
+
177
+ it 'reopen log when path is renamed' do
178
+ pending "rename isn't supported on windows" if ServerEngine.windows?
179
+
180
+ log = DaemonLogger.new("tmp/rotate.log", { level: 'info', log_rotate_age: 0 })
181
+
182
+ log.info '11111'
183
+ File.read("tmp/rotate.log").should include('11111')
184
+ File.rename("tmp/rotate.log", "tmp/rotate.log.1")
185
+
186
+ Timecop.travel(Time.now + 1)
187
+
188
+ log.info '22222'
189
+ contents = File.read("tmp/rotate.log.1")
190
+ contents.should include('11111')
191
+ contents.should include('22222')
192
+
193
+ FileUtils.touch("tmp/rotate.log")
194
+ Timecop.travel(Time.now + 1)
195
+
196
+ log.info '33333'
197
+ File.read("tmp/rotate.log").should include('33333')
198
+ File.read("tmp/rotate.log.1").should_not include('33333')
199
+ end
175
200
  end
@@ -1,25 +1,208 @@
1
1
  require 'timeout'
2
+ require 'timecop'
2
3
 
3
4
  describe ServerEngine::MultiSpawnServer do
4
5
  include_context 'test server and worker'
5
6
 
6
- context 'with command_sender=pipe' do
7
- it 'starts worker processes' do
8
- config = {workers: 2, command_sender: 'pipe', log_stdout: false, log_stderr: false}
7
+ describe 'starts worker processes' do
8
+ context 'with command_sender=pipe' do
9
+ it do
10
+ config = {workers: 2, command_sender: 'pipe', log_stdout: false, log_stderr: false}
9
11
 
10
- s = ServerEngine::MultiSpawnServer.new(TestWorker) { config.dup }
11
- t = Thread.new { s.main }
12
+ s = ServerEngine::MultiSpawnServer.new(TestWorker) { config.dup }
13
+ t = Thread.new { s.main }
12
14
 
13
- begin
14
- wait_for_fork
15
+ begin
16
+ wait_for_fork
15
17
 
16
- Timeout.timeout(5) do
17
- sleep(0.5) until test_state(:worker_run) == 2
18
+ Timeout.timeout(5) do
19
+ sleep(0.5) until test_state(:worker_run) == 2
20
+ end
21
+ test_state(:worker_run).should == 2
22
+ ensure
23
+ s.stop(true)
24
+ t.join
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ describe 'keepalive_workers' do
31
+ let(:config) {
32
+ {
33
+ workers: workers,
34
+ command_sender: 'pipe',
35
+ log_stdout: false,
36
+ log_stderr: false,
37
+ start_worker_delay: start_worker_delay,
38
+ start_worker_delay_rand: 0,
39
+ restart_worker_interval: restart_worker_interval,
40
+ }
41
+ }
42
+ let(:workers) { 3 }
43
+ let(:server) { ServerEngine::MultiSpawnServer.new(TestWorker) { config.dup } }
44
+ let(:monitors) { server.instance_variable_get(:@monitors) }
45
+
46
+ context 'default' do
47
+ let(:start_worker_delay) { 0 }
48
+ let(:restart_worker_interval) { 0 }
49
+
50
+ it do
51
+ t = Thread.new { server.main }
52
+
53
+ begin
54
+ wait_for_fork
55
+
56
+ Timeout.timeout(5) do
57
+ sleep(0.5) until monitors.count { |m| m && m.alive? } == workers
58
+ end
59
+
60
+ monitors.each do |m|
61
+ m.send_stop(true)
62
+ end
63
+
64
+ # To prevent the judge before stopping once
65
+ wait_for_stop
66
+
67
+ -> {
68
+ Timeout.timeout(5) do
69
+ sleep(0.5) until monitors.count { |m| m.alive? } == workers
70
+ end
71
+ }.should_not raise_error, "Not all workers restarted correctly."
72
+ ensure
73
+ server.stop(true)
74
+ t.join
75
+ end
76
+ end
77
+ end
78
+
79
+ context 'with only restart_worker_interval' do
80
+ let(:start_worker_delay) { 0 }
81
+ let(:restart_worker_interval) { 10 }
82
+
83
+ it do
84
+ t = Thread.new { server.main }
85
+
86
+ begin
87
+ wait_for_fork
88
+
89
+ # Wait for initial starting
90
+ Timeout.timeout(5) do
91
+ sleep(0.5) until monitors.count { |m| m && m.alive? } == workers
92
+ end
93
+
94
+ monitors.each do |m|
95
+ m.send_stop(true)
96
+ end
97
+
98
+ # Wait for all workers to stop and to be set restarting time
99
+ Timeout.timeout(5) do
100
+ sleep(0.5) until monitors.count { |m| m.alive? || m.restart_at.nil? } == 0
101
+ end
102
+
103
+ Timecop.freeze
104
+
105
+ mergin_time = 3
106
+
107
+ Timecop.freeze(Time.now + restart_worker_interval - mergin_time)
108
+ sleep(1.5)
109
+ monitors.count { |m| m.alive? }.should == 0
110
+
111
+ Timecop.freeze(Time.now + 2 * mergin_time)
112
+ -> {
113
+ Timeout.timeout(5) do
114
+ sleep(0.5) until monitors.count { |m| m.alive? } == workers
115
+ end
116
+ }.should_not raise_error, "Not all workers restarted correctly."
117
+ ensure
118
+ server.stop(true)
119
+ t.join
120
+ end
121
+ end
122
+ end
123
+
124
+ context 'with only start_worker_delay' do
125
+ let(:start_worker_delay) { 3 }
126
+ let(:restart_worker_interval) { 0 }
127
+
128
+ it do
129
+ t = Thread.new { server.main }
130
+
131
+ begin
132
+ wait_for_fork
133
+
134
+ # Initial starts are delayed too, so set longer timeout.
135
+ # (`start_worker_delay` uses `sleep` inside, so Timecop can't skip this wait.)
136
+ Timeout.timeout(start_worker_delay * workers) do
137
+ sleep(0.5) until monitors.count { |m| m && m.alive? } == workers
138
+ end
139
+
140
+ # Skip time to avoid getting a delay for the initial starts.
141
+ Timecop.travel(Time.now + start_worker_delay)
142
+
143
+ monitors.each do |m|
144
+ m.send_stop(true)
145
+ end
146
+
147
+ sleep(3)
148
+
149
+ # The first worker should restart immediately.
150
+ monitors.count { |m| m.alive? }.should satisfy { |c| 0 < c && c < workers }
151
+
152
+ # `start_worker_delay` uses `sleep` inside, so Timecop can't skip this wait.
153
+ sleep(start_worker_delay * workers)
154
+ monitors.count { |m| m.alive? }.should == workers
155
+ ensure
156
+ server.stop(true)
157
+ t.join
158
+ end
159
+ end
160
+ end
161
+
162
+ context 'with both options' do
163
+ let(:start_worker_delay) { 3 }
164
+ let(:restart_worker_interval) { 10 }
165
+
166
+ it do
167
+ t = Thread.new { server.main }
168
+
169
+ begin
170
+ wait_for_fork
171
+
172
+ # Initial starts are delayed too, so set longer timeout.
173
+ # (`start_worker_delay` uses `sleep` inside, so Timecop can't skip this wait.)
174
+ Timeout.timeout(start_worker_delay * workers) do
175
+ sleep(0.5) until monitors.count { |m| m && m.alive? } == workers
176
+ end
177
+
178
+ monitors.each do |m|
179
+ m.send_stop(true)
180
+ end
181
+
182
+ # Wait for all workers to stop and to be set restarting time
183
+ Timeout.timeout(5) do
184
+ sleep(0.5) until monitors.count { |m| m.alive? || m.restart_at.nil? } == 0
185
+ end
186
+
187
+ Timecop.freeze
188
+
189
+ mergin_time = 3
190
+
191
+ Timecop.freeze(Time.now + restart_worker_interval - mergin_time)
192
+ sleep(1.5)
193
+ monitors.count { |m| m.alive? }.should == 0
194
+
195
+ Timecop.travel(Time.now + 2 * mergin_time)
196
+ sleep(1.5)
197
+ monitors.count { |m| m.alive? }.should satisfy { |c| 0 < c && c < workers }
198
+
199
+ # `start_worker_delay` uses `sleep` inside, so Timecop can't skip this wait.
200
+ sleep(start_worker_delay * workers)
201
+ monitors.count { |m| m.alive? }.should == workers
202
+ ensure
203
+ server.stop(true)
204
+ t.join
18
205
  end
19
- test_state(:worker_run).should == 2
20
- ensure
21
- s.stop(true)
22
- t.join
23
206
  end
24
207
  end
25
208
  end
@@ -1,6 +1,7 @@
1
1
 
2
2
  require 'thread'
3
3
  require 'yaml'
4
+ require 'timecop'
4
5
 
5
6
  def reset_test_state
6
7
  FileUtils.mkdir_p 'tmp'
@@ -165,8 +166,9 @@ module TestWorker
165
166
 
166
167
  def run
167
168
  incr_test_state :worker_run
168
- 5.times do
169
- # repeats 5 times because signal handlers
169
+ # This means this worker will automatically finish after 50 seconds.
170
+ 10.times do
171
+ # repeats multiple times because signal handlers
170
172
  # interrupts wait
171
173
  @stop_flag.wait(5.0)
172
174
  end
@@ -252,16 +254,25 @@ end
252
254
 
253
255
  shared_context 'test server and worker' do
254
256
  before { reset_test_state }
257
+ after { Timecop.return }
258
+
259
+ unless self.const_defined?(:WAIT_RATIO)
260
+ if ServerEngine.windows?
261
+ WAIT_RATIO = 2
262
+ else
263
+ WAIT_RATIO = 1
264
+ end
265
+ end
255
266
 
256
267
  def wait_for_fork
257
- sleep 0.8
268
+ sleep 1.5 * WAIT_RATIO
258
269
  end
259
270
 
260
271
  def wait_for_stop
261
- sleep 0.8
272
+ sleep 0.8 * WAIT_RATIO
262
273
  end
263
274
 
264
275
  def wait_for_restart
265
- sleep 1.5
276
+ sleep 1.5 * WAIT_RATIO
266
277
  end
267
278
  end
@@ -19,7 +19,21 @@ describe ServerEngine::SocketManager do
19
19
  File.unlink(server_path) if server_path.is_a?(String) && File.exist?(server_path)
20
20
  end
21
21
 
22
- if !ServerEngine.windows?
22
+ if ServerEngine.windows?
23
+ context 'Server.generate_path' do
24
+ it 'returns socket path as port number' do
25
+ path = SocketManager::Server.generate_path
26
+ expect(path).to be_between(49152, 65535)
27
+ end
28
+
29
+ it 'can be changed via environment variable' do
30
+ ENV['SERVERENGINE_SOCKETMANAGER_PORT'] = '54321'
31
+ path = SocketManager::Server.generate_path
32
+ expect(path).to be 54321
33
+ ENV.delete('SERVERENGINE_SOCKETMANAGER_PORT')
34
+ end
35
+ end
36
+ else
23
37
  context 'Server.generate_path' do
24
38
  it 'returns socket path under /tmp' do
25
39
  path = SocketManager::Server.generate_path
@@ -155,7 +169,27 @@ describe ServerEngine::SocketManager do
155
169
  test_state(:is_udp_socket).should == 1
156
170
  test_state(:udp_data_sent).should == 1
157
171
  end
158
- end if (TCPServer.open("::1",0) rescue nil)
172
+ end if (TCPServer.open("::1", 0) rescue nil)
173
+
174
+ unless ServerEngine.windows?
175
+ context 'using ipv4/ipv6' do
176
+ it 'can bind ipv4/ipv6 together' do
177
+ server = SocketManager::Server.open(server_path)
178
+ client = ServerEngine::SocketManager::Client.new(server_path)
179
+
180
+ tcp_v4 = client.listen_tcp('0.0.0.0', test_port)
181
+ udp_v4 = client.listen_udp('0.0.0.0', test_port)
182
+ tcp_v6 = client.listen_tcp('::', test_port)
183
+ udp_v6 = client.listen_udp('::', test_port)
184
+
185
+ tcp_v4.close
186
+ udp_v4.close
187
+ tcp_v6.close
188
+ udp_v6.close
189
+ server.close
190
+ end
191
+ end if (TCPServer.open("::", 0) rescue nil)
192
+ end
159
193
  end
160
194
 
161
195
  if ServerEngine.windows?
data/spec/spec_helper.rb CHANGED
@@ -1,4 +1,9 @@
1
1
  require 'bundler'
2
+ require 'rspec'
3
+
4
+ RSpec.configure do |config|
5
+ config.color_enabled = true
6
+ end
2
7
 
3
8
  begin
4
9
  Bundler.setup(:default, :test)
@@ -191,13 +191,19 @@ describe ServerEngine::Supervisor do
191
191
  sv, t = start_supervisor(RunErrorWorker, server_restart_wait: 1, command_sender: sender)
192
192
 
193
193
  begin
194
- sleep 2.2
194
+ sleep 2.5
195
195
  ensure
196
196
  sv.stop(true)
197
197
  t.join
198
198
  end
199
199
 
200
- test_state(:worker_run).should == 3
200
+ if ServerEngine.windows?
201
+ # Because launching a process on Windows is high cost,
202
+ # it doesn't often reach to 3.
203
+ test_state(:worker_run).should <= 3
204
+ else
205
+ test_state(:worker_run).should == 3
206
+ end
201
207
  end
202
208
  end
203
209
  end
@@ -0,0 +1,17 @@
1
+ describe ServerEngine::WinSock do
2
+ # On Ruby 3.0, you need to use fiddle 1.0.8 or later to retrieve a correct
3
+ # error code. In addition, you need to specify the path of fiddle by RUBYLIB
4
+ # or `ruby -I` when you use RubyInstaller because it loads Ruby's bundled
5
+ # fiddle before initializing gem.
6
+ # See also:
7
+ # * https://github.com/ruby/fiddle/issues/72
8
+ # * https://bugs.ruby-lang.org/issues/17813
9
+ # * https://github.com/oneclick/rubyinstaller2/blob/8225034c22152d8195bc0aabc42a956c79d6c712/lib/ruby_installer/build/dll_directory.rb
10
+ context 'last_error' do
11
+ it 'bind error' do
12
+ expect(WinSock.bind(0, nil, 0)).to be -1
13
+ WSAENOTSOCK = 10038
14
+ expect(WinSock.last_error).to be WSAENOTSOCK
15
+ end
16
+ end
17
+ end if ServerEngine.windows?