serverengine 2.0.7 → 2.3.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.
@@ -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?