serverengine 2.3.1 → 2.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8b802588a6e93b69c2dc9810ab33b9e3195e14d93c5f977961a04cc6d88cacf1
4
- data.tar.gz: 9921383ec5ae39cfe982da9ce8c0b432bbc553c2ea5d2090a2d849ec2500c230
3
+ metadata.gz: 516efcdedbeba3c7d7988c91cc121a37b52efca99726d833447fe374680cac43
4
+ data.tar.gz: b4265b4b425eb2d165881d579f6b2115c8e7bf2298c56f1cf75d6ed1cb804993
5
5
  SHA512:
6
- metadata.gz: ea4c0e4b34490665e4287c0f43e4ea1f32c10fe022464a69326505ec21aad2650e53b842c2aaeec2a969d914f73715ee059b2bdbb3d72d14b0bce51192bf217c
7
- data.tar.gz: 3e30810b08b8667f2295b41fca60c4683126d8936e32169d0789b8ec02ba3965fc631dbe069c9a8cf057e109ccaa9ad3ad14f4507b2388dcd9ae27463ce39651
6
+ metadata.gz: cc1d3263b08bd87e645563000b46a79a58437617006eb993b959d50105a875d1592bdce2e45c9bd1a4fbfd7740ab672d1e61a13e39654cdae798dbe0fe18f606
7
+ data.tar.gz: 62fca1438f15432ce570216dce14eabef6235835c2f43f875ba220b3ba6d6a86e8aaedcfa4e5752a7ba5529a55d6bfeca9618ce0f59924dd6ef9666e69d00cf2
@@ -1,4 +1,4 @@
1
- name: Testing on Ubuntu
1
+ name: Test
2
2
  on:
3
3
  push:
4
4
  branches: [master]
@@ -11,10 +11,9 @@ jobs:
11
11
  strategy:
12
12
  fail-fast: false
13
13
  matrix:
14
- ruby: [ '3.1', '3.0', '2.7' ]
15
- os:
16
- - ubuntu-latest
17
- name: Unit testing with Ruby ${{ matrix.ruby }} on ${{ matrix.os }}
14
+ ruby: [ '3.3', '3.2', '3.1', '3.0', '2.7' ]
15
+ os: ['ubuntu-latest', 'windows-latest']
16
+ name: Ruby ${{ matrix.ruby }} on ${{ matrix.os }}
18
17
  steps:
19
18
  - uses: actions/checkout@v2
20
19
  - name: Set up Ruby
@@ -22,9 +21,7 @@ jobs:
22
21
  with:
23
22
  ruby-version: ${{ matrix.ruby }}
24
23
  - name: Install dependencies
25
- run: |
26
- gem install bundler rake
27
- bundle install --jobs 4 --retry 3
24
+ run: bundle install --jobs 4 --retry 3
28
25
  - name: Run tests
29
26
  env:
30
27
  CI: true
data/Changelog CHANGED
@@ -1,3 +1,14 @@
1
+ 2024-10-22 version 2.4.0
2
+
3
+ * Add missing base64 and logger gem as dependency for Ruby 3.4 and 3.5.
4
+ * socket_manager: add feature to share sockets with another server.
5
+
6
+ 2023-03-14 version 2.3.2
7
+
8
+ * Accept `nil` for `ServerEngine::SocketManager::Server.open` to select path automatically
9
+ * Care excluded port ranges of Windows in `ServerEngine::SocketManager.generate_path`
10
+ * Update to Rake 13 and RSpec 3 to support running tests on Ruby 3.2
11
+
1
12
  2022-12-22 version 2.3.1
2
13
 
3
14
  * Don't treat as error when worker shuts down with exit status 0
data/README.md CHANGED
@@ -370,8 +370,8 @@ se.run
370
370
  ```ruby
371
371
  module MyServer
372
372
  def before_run
373
- @socket_manager_path = ServerEngine::SocketManager::Server.generate_path
374
- @socket_manager_server = ServerEngine::SocketManager::Server.open(@socket_manager_path)
373
+ @socket_manager_server = ServerEngine::SocketManager::Server.open
374
+ @socket_manager_path = @socket_manager_server.path
375
375
  end
376
376
 
377
377
  def after_run
@@ -413,8 +413,15 @@ se = ServerEngine.create(MyServer, MyWorker, {
413
413
  se.run
414
414
  ```
415
415
 
416
- See also [examples](https://github.com/fluent/serverengine/tree/master/examples).
416
+ Other features:
417
+
418
+ - `socket_manager_server = SocketManager::Server.share_sockets_with_another_server(path)`
419
+ - It starts a new manager server that shares all UDP/TCP sockets with the existing manager.
420
+ - We can use this for live restart for network servers.
421
+ - The old process should stop without removing the file for the socket after the new process starts.
422
+ - Limitation: This feature would not work well if the process opens new TCP ports frequently.
417
423
 
424
+ See also [examples](https://github.com/fluent/serverengine/tree/master/examples).
418
425
 
419
426
  ## Module API
420
427
 
data/examples/server.rb CHANGED
@@ -41,8 +41,8 @@ module MyServer
41
41
  attr_reader :socket_manager_path
42
42
 
43
43
  def before_run
44
- @socket_manager_path = ServerEngine::SocketManager::Server.generate_path
45
- @socket_manager_server = ServerEngine::SocketManager::Server.open(@socket_manager_path)
44
+ @socket_manager_server = ServerEngine::SocketManager::Server.open
45
+ @socket_manager_path = @socket_manager_server.path
46
46
  rescue Exception => e
47
47
  logger.error "unexpected error in server, class #{e.class}: #{e.message}"
48
48
  raise
@@ -77,34 +77,51 @@ module ServerEngine
77
77
  port = ENV['SERVERENGINE_SOCKETMANAGER_PORT']
78
78
  return port.to_i if port
79
79
 
80
- for port in get_dynamic_port_range
81
- if `netstat -na | findstr "#{port}"`.length == 0
82
- return port
83
- end
84
- end
80
+ excluded_port_ranges = get_excluded_port_ranges
81
+ get_dynamic_port_range
82
+ .reject { |port| excluded_port_ranges.any? { |range| range.cover?(port) } }
83
+ .find { |port| `netstat -na | findstr "#{port}"`.length == 0 }
85
84
  else
86
85
  base_dir = (ENV['SERVERENGINE_SOCKETMANAGER_SOCK_DIR'] || '/tmp')
87
86
  File.join(base_dir, 'SERVERENGINE_SOCKETMANAGER_' + Time.now.utc.iso8601 + '_' + Process.pid.to_s)
88
87
  end
89
88
  end
90
89
 
91
- def self.open(path)
92
- new(path)
90
+ def self.open(path = nil)
91
+ return new(path) unless path.nil?
92
+ if ServerEngine.windows?
93
+ new(0)
94
+ else
95
+ new(self.generate_path)
96
+ end
97
+ end
98
+
99
+ def self.share_sockets_with_another_server(path)
100
+ raise NotImplementedError, "Not supported on Windows." if ServerEngine.windows?
101
+ server = new(path, start: false)
102
+ server.share_sockets_with_another_server
103
+ server
93
104
  end
94
105
 
95
- def initialize(path)
106
+ def initialize(path, start: true)
96
107
  @tcp_sockets = {}
97
108
  @udp_sockets = {}
98
109
  @mutex = Mutex.new
99
- @path = start_server(path)
110
+ @path = start ? start_server(path) : path
100
111
  end
101
112
 
102
113
  attr_reader :path
114
+ attr_reader :tcp_sockets, :udp_sockets # for tests
103
115
 
104
116
  def new_client
105
117
  Client.new(@path)
106
118
  end
107
119
 
120
+ def start
121
+ start_server(path)
122
+ nil
123
+ end
124
+
108
125
  def close
109
126
  stop_server
110
127
  nil
@@ -155,9 +172,9 @@ module ServerEngine
155
172
  res = SocketManager.recv_peer(peer)
156
173
  return if res.nil?
157
174
 
158
- pid, method, bind, port = *res
175
+ pid, method, *opts = res
159
176
  begin
160
- send_socket(peer, pid, method, bind, port)
177
+ send_socket(peer, pid, method, *opts)
161
178
  rescue => e
162
179
  SocketManager.send_peer(peer, e)
163
180
  end
@@ -198,6 +215,27 @@ module ServerEngine
198
215
  return 49152..65535
199
216
  end
200
217
  end
218
+
219
+ def self.get_excluded_port_ranges
220
+ # Example output of netsh:
221
+ #
222
+ # Protocol tcp Port Exclusion Ranges
223
+ #
224
+ # Start Port End Port
225
+ # ---------- --------
226
+ # 2869 2869
227
+ # 49152 49251
228
+ # 50000 50059 *
229
+ # 57095 57194
230
+ #
231
+ # * - Administered port exclusions.
232
+ #
233
+ `netsh int ipv4 show excludedportrange tcp`
234
+ .force_encoding("ASCII-8BIT")
235
+ .lines
236
+ .map { |line| line.match(/\s*(\d+)\s*(\d+)[\s\*]*/) ? $1.to_i..$2.to_i : nil }
237
+ .compact
238
+ end
201
239
  end
202
240
  end
203
241
 
@@ -47,6 +47,38 @@ module ServerEngine
47
47
  end
48
48
 
49
49
  module ServerModule
50
+ def share_sockets_with_another_server
51
+ another_server = UNIXSocket.new(@path)
52
+ begin
53
+ idx = 0
54
+ while true
55
+ SocketManager.send_peer(another_server, [Process.pid, :share_udp, idx])
56
+ key = SocketManager.recv_peer(another_server)
57
+ break if key.nil?
58
+ @udp_sockets[key] = another_server.recv_io UDPSocket
59
+ idx += 1
60
+ end
61
+
62
+ idx = 0
63
+ while true
64
+ SocketManager.send_peer(another_server, [Process.pid, :share_tcp, idx])
65
+ key = SocketManager.recv_peer(another_server)
66
+ break if key.nil?
67
+ @tcp_sockets[key] = another_server.recv_io TCPServer
68
+ idx += 1
69
+ end
70
+
71
+ SocketManager.send_peer(another_server, [Process.pid, :share_unix])
72
+ res = SocketManager.recv_peer(another_server)
73
+ raise res if res.is_a?(Exception)
74
+ @server = another_server.recv_io UNIXServer
75
+
76
+ start_server(@path)
77
+ ensure
78
+ another_server.close
79
+ end
80
+ end
81
+
50
82
  private
51
83
 
52
84
  def listen_tcp_new(bind_ip, port)
@@ -77,15 +109,17 @@ module ServerEngine
77
109
  end
78
110
 
79
111
  def start_server(path)
80
- # return absolute path so that client can connect to this path
81
- # when client changed working directory
82
- path = File.expand_path(path)
112
+ unless @server
113
+ # return absolute path so that client can connect to this path
114
+ # when client changed working directory
115
+ path = File.expand_path(path)
83
116
 
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)
117
+ begin
118
+ old_umask = File.umask(0077) # Protect unix socket from other users
119
+ @server = UNIXServer.new(path)
120
+ ensure
121
+ File.umask(old_umask)
122
+ end
89
123
  end
90
124
 
91
125
  @thread = Thread.new do
@@ -111,19 +145,34 @@ module ServerEngine
111
145
  @thread.join if RUBY_VERSION >= "2.2"
112
146
  end
113
147
 
114
- def send_socket(peer, pid, method, bind, port)
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
123
-
124
- SocketManager.send_peer(peer, nil)
125
-
126
- peer.send_io sock
148
+ def send_socket(peer, pid, method, *opts)
149
+ case method
150
+ when :listen_tcp
151
+ bind, port = opts
152
+ sock = listen_tcp(bind, port)
153
+ SocketManager.send_peer(peer, nil)
154
+ peer.send_io sock
155
+ when :listen_udp
156
+ bind, port = opts
157
+ sock = listen_udp(bind, port)
158
+ SocketManager.send_peer(peer, nil)
159
+ peer.send_io sock
160
+ when :share_tcp
161
+ idx, = opts
162
+ key = @tcp_sockets.keys[idx]
163
+ SocketManager.send_peer(peer, key)
164
+ peer.send_io(@tcp_sockets.values[idx]) if key
165
+ when :share_udp
166
+ idx, = opts
167
+ key = @udp_sockets.keys[idx]
168
+ SocketManager.send_peer(peer, key)
169
+ peer.send_io(@udp_sockets.values[idx]) if key
170
+ when :share_unix
171
+ SocketManager.send_peer(peer, nil)
172
+ peer.send_io @server
173
+ else
174
+ raise ArgumentError, "Unknown method: #{method.inspect}"
175
+ end
127
176
  end
128
177
  end
129
178
 
@@ -108,8 +108,9 @@ module ServerEngine
108
108
  end
109
109
 
110
110
  def start_server(addr)
111
- # TODO: use TCPServer, but this is risky because using not conflict path is easy,
112
- # but using not conflict port is difficult. Then We had better implement using NamedPipe.
111
+ # We need to take care about selecting an available port.
112
+ # By passing `nil` or `0` as `addr`, an available port is automatically selected.
113
+ # However, we should consider using NamedPipe instead of TCPServer.
113
114
  @server = TCPServer.new("127.0.0.1", addr)
114
115
  @thread = Thread.new do
115
116
  begin
@@ -123,7 +124,7 @@ module ServerEngine
123
124
  end
124
125
  end
125
126
 
126
- return path
127
+ return @server.addr[1]
127
128
  end
128
129
 
129
130
  def stop_server
@@ -1,3 +1,3 @@
1
1
  module ServerEngine
2
- VERSION = "2.3.1"
2
+ VERSION = "2.4.0"
3
3
  end
data/serverengine.gemspec CHANGED
@@ -16,13 +16,15 @@ Gem::Specification.new do |gem|
16
16
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
17
17
  gem.require_paths = ["lib"]
18
18
 
19
- gem.required_ruby_version = ">= 2.1.0"
19
+ gem.required_ruby_version = ">= 2.3.0"
20
20
 
21
21
  gem.add_dependency "sigdump", ["~> 0.2.2"]
22
+ gem.add_dependency "base64", ["~> 0.1"]
23
+ gem.add_dependency "logger", ["~> 1.4"]
22
24
 
23
25
  # rake v12.x doesn't work with rspec 2. rspec should be updated to 3
24
- gem.add_development_dependency "rake", ["~> 11.0"]
25
- gem.add_development_dependency "rspec", ["~> 2.13.0"]
26
+ gem.add_development_dependency "rake", ["~> 13.0"]
27
+ gem.add_development_dependency "rspec", ["~> 3.12.0"]
26
28
 
27
29
  gem.add_development_dependency 'rake-compiler-dock', ['~> 0.5.0']
28
30
  gem.add_development_dependency 'rake-compiler', ['~> 0.9.4']
@@ -150,7 +150,7 @@ describe ServerEngine::DaemonLogger do
150
150
  end
151
151
 
152
152
  it 'inter-process locking on rotation' do
153
- pending "fork is not implemented in Windows" if ServerEngine.windows?
153
+ skip "fork is not implemented in Windows" if ServerEngine.windows?
154
154
 
155
155
  log = DaemonLogger.new("tmp/se4.log", level: 'trace', log_rotate_age: 3, log_rotate_size: 10)
156
156
  r, w = IO.pipe
@@ -175,7 +175,7 @@ describe ServerEngine::DaemonLogger do
175
175
  end
176
176
 
177
177
  it 'reopen log when path is renamed' do
178
- pending "rename isn't supported on windows" if ServerEngine.windows?
178
+ skip "rename isn't supported on windows" if ServerEngine.windows?
179
179
 
180
180
  log = DaemonLogger.new("tmp/rotate.log", { level: 'info', log_rotate_age: 0 })
181
181
 
data/spec/daemon_spec.rb CHANGED
@@ -11,7 +11,7 @@ describe ServerEngine::Daemon do
11
11
  end
12
12
 
13
13
  it 'run and graceful stop by signal' do
14
- pending "not supported signal base commands on Windows" if ServerEngine.windows?
14
+ skip "not supported signal base commands on Windows" if ServerEngine.windows?
15
15
 
16
16
  dm = Daemon.new(TestServer, TestWorker, daemonize: true, pid_path: "tmp/pid", command_sender: "signal")
17
17
  dm.main
@@ -33,7 +33,7 @@ describe ServerEngine::Daemon do
33
33
  end
34
34
 
35
35
  it 'signals' do
36
- pending "not supported signal base commands on Windows" if ServerEngine.windows?
36
+ skip "not supported signal base commands on Windows" if ServerEngine.windows?
37
37
  dm = Daemon.new(TestServer, TestWorker, daemonize: true, pid_path: "tmp/pid", command_sender: "signal")
38
38
  dm.main
39
39
 
@@ -112,7 +112,7 @@ describe ServerEngine::Daemon do
112
112
  end
113
113
 
114
114
  it 'exits with status 0 when it was stopped normally' do
115
- pending "worker type process(fork) cannot be used in Windows" if ServerEngine.windows?
115
+ skip "worker type process(fork) cannot be used in Windows" if ServerEngine.windows?
116
116
  dm = Daemon.new(
117
117
  TestServer,
118
118
  TestWorker,
@@ -135,7 +135,7 @@ describe ServerEngine::Daemon do
135
135
  end
136
136
 
137
137
  it 'exits with status of workers if worker exits with status specified in unrecoverable_exit_codes, without supervisor' do
138
- pending "worker type process(fork) cannot be used in Windows" if ServerEngine.windows?
138
+ skip "worker type process(fork) cannot be used in Windows" if ServerEngine.windows?
139
139
 
140
140
  dm = Daemon.new(
141
141
  TestServer,
@@ -159,7 +159,7 @@ describe ServerEngine::Daemon do
159
159
  end
160
160
 
161
161
  it 'exits with status of workers if worker exits with status specified in unrecoverable_exit_codes, with supervisor' do
162
- pending "worker type process(fork) cannot be used in Windows" if ServerEngine.windows?
162
+ skip "worker type process(fork) cannot be used in Windows" if ServerEngine.windows?
163
163
 
164
164
  dm = Daemon.new(
165
165
  TestServer,
@@ -17,7 +17,7 @@ require 'securerandom'
17
17
  end
18
18
 
19
19
  it 'scale up' do
20
- pending "Windows environment does not support fork" if ServerEngine.windows? && impl_class == ServerEngine::MultiProcessServer
20
+ skip "Windows environment does not support fork" if ServerEngine.windows? && impl_class == ServerEngine::MultiProcessServer
21
21
 
22
22
  config = {
23
23
  workers: 2,
@@ -50,7 +50,7 @@ require 'securerandom'
50
50
  end
51
51
 
52
52
  it 'scale down' do
53
- pending "Windows environment does not support fork" if ServerEngine.windows? && impl_class == ServerEngine::MultiProcessServer
53
+ skip "Windows environment does not support fork" if ServerEngine.windows? && impl_class == ServerEngine::MultiProcessServer
54
54
 
55
55
  config = {
56
56
  workers: 2,
@@ -98,7 +98,7 @@ end
98
98
  end
99
99
 
100
100
  it 'raises SystemExit when all workers exit with specified code by unrecoverable_exit_codes' do
101
- pending "Windows environment does not support fork" if ServerEngine.windows? && impl_class == ServerEngine::MultiProcessServer
101
+ skip "Windows environment does not support fork" if ServerEngine.windows? && impl_class == ServerEngine::MultiProcessServer
102
102
 
103
103
  config = {
104
104
  workers: 4,
@@ -127,7 +127,7 @@ end
127
127
  end
128
128
 
129
129
  it 'raises SystemExit immediately when a worker exits if stop_immediately_at_unrecoverable_exit specified' do
130
- pending "Windows environment does not support fork" if ServerEngine.windows? && impl_class == ServerEngine::MultiProcessServer
130
+ skip "Windows environment does not support fork" if ServerEngine.windows? && impl_class == ServerEngine::MultiProcessServer
131
131
 
132
132
  config = {
133
133
  workers: 4,
@@ -172,7 +172,7 @@ describe "log level for exited proccess" do
172
172
  end
173
173
 
174
174
  it 'stop' do
175
- pending "Windows environment does not support fork" if ServerEngine.windows?
175
+ skip "Windows environment does not support fork" if ServerEngine.windows?
176
176
 
177
177
  config = {
178
178
  workers: 1,
@@ -197,7 +197,7 @@ describe "log level for exited proccess" do
197
197
  end
198
198
 
199
199
  it 'non zero exit status' do
200
- pending "Windows environment does not support fork" if ServerEngine.windows?
200
+ skip "Windows environment does not support fork" if ServerEngine.windows?
201
201
 
202
202
  config = {
203
203
  workers: 1,
@@ -232,7 +232,7 @@ describe "log level for exited proccess" do
232
232
  end
233
233
 
234
234
  it 'zero exit status' do
235
- pending "Windows environment does not support fork" if ServerEngine.windows?
235
+ skip "Windows environment does not support fork" if ServerEngine.windows?
236
236
 
237
237
  config = {
238
238
  workers: 1,
@@ -1,4 +1,5 @@
1
1
  require 'socket'
2
+ require 'rr'
2
3
 
3
4
  describe ServerEngine::SocketManager do
4
5
  include_context 'test server and worker'
@@ -21,9 +22,23 @@ describe ServerEngine::SocketManager do
21
22
 
22
23
  if ServerEngine.windows?
23
24
  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)
25
+ context 'with socket path as port number' do
26
+ it 'returns a port in the dynamic port range' do
27
+ path = SocketManager::Server.generate_path
28
+ expect(path).to be_between(49152, 65535)
29
+ end
30
+
31
+ it 'returns a port which is not excluded' do
32
+ excluded_port_ranges = [
33
+ 49152..49251,
34
+ 50000..50059,
35
+ ]
36
+ RR.stub(SocketManager::Server).get_excluded_port_ranges { excluded_port_ranges }
37
+ path = SocketManager::Server.generate_path
38
+ excluded_port_ranges.each do |range|
39
+ expect(path).not_to be_between(range.first, range.last)
40
+ end
41
+ end
27
42
  end
28
43
 
29
44
  it 'can be changed via environment variable' do
@@ -33,6 +48,22 @@ describe ServerEngine::SocketManager do
33
48
  ENV.delete('SERVERENGINE_SOCKETMANAGER_PORT')
34
49
  end
35
50
  end
51
+
52
+ context 'Server.open' do
53
+ it 'returns server with automatically selected socket path as port number' do
54
+ server = SocketManager::Server.open
55
+ expect(server.path).to be_between(49152, 65535)
56
+ end
57
+ end
58
+
59
+ context 'Server.share_sockets_with_another_server' do
60
+ it 'not supported' do
61
+ server = SocketManager::Server.open(server_path)
62
+ expect { SocketManager::Server.share_sockets_with_another_server(server_path) }.to raise_error(NotImplementedError)
63
+ ensure
64
+ server.close
65
+ end
66
+ end
36
67
  else
37
68
  context 'Server.generate_path' do
38
69
  it 'returns socket path under /tmp' do
@@ -47,6 +78,172 @@ describe ServerEngine::SocketManager do
47
78
  ENV.delete('SERVERENGINE_SOCKETMANAGER_SOCK_DIR')
48
79
  end
49
80
  end
81
+
82
+ context 'Server.open' do
83
+ it 'returns server with automatically selected socket path under /tmp' do
84
+ server = SocketManager::Server.open
85
+ expect(server.path).to include('/tmp/SERVERENGINE_SOCKETMANAGER_')
86
+ end
87
+ end
88
+
89
+ context 'Server.share_sockets_with_another_server' do
90
+ it 'shares listen sockets to another server' do
91
+ server = SocketManager::Server.open(server_path)
92
+
93
+ client = SocketManager::Client.new(server_path)
94
+ tcp1 = client.listen_tcp('127.0.0.1', 55551)
95
+ udp1 = client.listen_udp('127.0.0.1', 55561)
96
+ udp2 = client.listen_udp('127.0.0.1', 55562)
97
+
98
+ another_server = SocketManager::Server.share_sockets_with_another_server(server_path)
99
+
100
+ expect([
101
+ another_server.tcp_sockets.keys,
102
+ another_server.tcp_sockets.values.map(&:addr),
103
+ another_server.udp_sockets.keys,
104
+ another_server.udp_sockets.values.map(&:addr),
105
+ ]).to eq([
106
+ server.tcp_sockets.keys,
107
+ server.tcp_sockets.values.map(&:addr),
108
+ server.udp_sockets.keys,
109
+ server.udp_sockets.values.map(&:addr),
110
+ ])
111
+ ensure
112
+ tcp1&.close
113
+ udp1&.close
114
+ udp2&.close
115
+ server&.close
116
+ another_server&.close
117
+ end
118
+
119
+ it 'takes over TCP sockets without downtime' do
120
+ manager_server = SocketManager::Server.open(server_path)
121
+ manager_client = SocketManager::Client.new(server_path)
122
+
123
+ has_server_started = false
124
+ # The old server starts listening
125
+ thread_server = Thread.new do
126
+ server = manager_client.listen_tcp('127.0.0.1', test_port)
127
+ has_server_started = true
128
+ while socket = server.accept
129
+ incr_test_state(:count)
130
+ socket.close
131
+ end
132
+ ensure
133
+ server&.close
134
+ end
135
+
136
+ sleep 0.1 until has_server_started
137
+
138
+ # The client starts sending data
139
+ thread_client = Thread.new do
140
+ 100.times do |i|
141
+ socket = TCPSocket.new('127.0.0.1', test_port)
142
+ begin
143
+ socket.write("Hello #{i}\n")
144
+ ensure
145
+ socket.close
146
+ end
147
+ sleep 0.01
148
+ end
149
+ end
150
+
151
+ sleep 0.5
152
+
153
+ # The new server shares the sockets and starts listening in parallel with the old one
154
+ thread_new_server = Thread.new do
155
+ new_manager_server = SocketManager::Server.share_sockets_with_another_server(server_path)
156
+ server = manager_client.listen_tcp('127.0.0.1', test_port)
157
+ while socket = server.accept
158
+ incr_test_state(:count)
159
+ socket.close
160
+ end
161
+ ensure
162
+ new_manager_server&.close
163
+ server&.close
164
+ end
165
+
166
+ # Stop the old server
167
+ sleep 0.1
168
+ thread_server.kill
169
+ thread_server.join
170
+
171
+ thread_client.join
172
+ wait_for_stop
173
+
174
+ # Confirm that server switching was completed without data loss
175
+ expect(test_state(:count)).to eq(100)
176
+ ensure
177
+ manager_server&.close
178
+ thread_server&.kill
179
+ thread_new_server&.kill
180
+ thread_server&.join
181
+ thread_new_server&.join
182
+ end
183
+
184
+ it 'takes over UDP sockets without downtime' do
185
+ manager_server = SocketManager::Server.open(server_path)
186
+ manager_client = SocketManager::Client.new(server_path)
187
+
188
+ has_server_started = false
189
+ # The old server starts listening
190
+ thread_server = Thread.new do
191
+ server = manager_client.listen_udp('127.0.0.1', test_port)
192
+ has_server_started = true
193
+ while server.recv(10)
194
+ incr_test_state(:count)
195
+ end
196
+ ensure
197
+ server&.close
198
+ end
199
+
200
+ sleep 0.1 until has_server_started
201
+
202
+ # The client starts sending data
203
+ thread_client = Thread.new do
204
+ 100.times do |i|
205
+ socket = UDPSocket.new
206
+ begin
207
+ socket.send("Hello #{i}\n", 0, "127.0.0.1", test_port)
208
+ ensure
209
+ socket.close
210
+ end
211
+ sleep 0.01
212
+ end
213
+ end
214
+
215
+ sleep 0.5
216
+
217
+ # The new server shares the sockets and starts listening in parallel with the old one
218
+ thread_new_server = Thread.new do
219
+ new_manager_server = SocketManager::Server.share_sockets_with_another_server(server_path)
220
+ server = manager_client.listen_udp('127.0.0.1', test_port)
221
+ while server.recv(10)
222
+ incr_test_state(:count)
223
+ end
224
+ ensure
225
+ new_manager_server&.close
226
+ server&.close
227
+ end
228
+
229
+ # Stop the old server
230
+ sleep 0.1
231
+ thread_server.kill
232
+ thread_server.join
233
+
234
+ thread_client.join
235
+ wait_for_stop
236
+
237
+ # Confirm that server switching was completed without data loss
238
+ expect(test_state(:count)).to eq(100)
239
+ ensure
240
+ manager_server&.close
241
+ thread_server&.kill
242
+ thread_new_server&.kill
243
+ thread_server&.join
244
+ thread_new_server&.join
245
+ end
246
+ end
50
247
  end
51
248
 
52
249
  context 'with thread' do
data/spec/spec_helper.rb CHANGED
@@ -1,9 +1,6 @@
1
1
  require 'bundler'
2
2
  require 'rspec'
3
-
4
- RSpec.configure do |config|
5
- config.color_enabled = true
6
- end
3
+ require 'fileutils'
7
4
 
8
5
  begin
9
6
  Bundler.setup(:default, :test)
@@ -90,7 +90,7 @@ describe ServerEngine::Supervisor do
90
90
  context "when using #{sender} as command_sender" do
91
91
 
92
92
  it 'start and graceful stop' do
93
- pending 'not supported on Windows' if ServerEngine.windows? && sender == 'signal'
93
+ skip 'not supported on Windows' if ServerEngine.windows? && sender == 'signal'
94
94
 
95
95
  sv, t = start_supervisor(command_sender: sender)
96
96
 
@@ -113,7 +113,7 @@ describe ServerEngine::Supervisor do
113
113
  end
114
114
 
115
115
  it 'immediate stop' do
116
- pending 'not supported on Windows' if ServerEngine.windows? && sender == 'signal'
116
+ skip 'not supported on Windows' if ServerEngine.windows? && sender == 'signal'
117
117
 
118
118
  sv, t = start_supervisor(command_sender: sender)
119
119
 
@@ -131,7 +131,7 @@ describe ServerEngine::Supervisor do
131
131
  end
132
132
 
133
133
  it 'graceful restart' do
134
- pending 'not supported on Windows' if ServerEngine.windows? && sender == 'signal'
134
+ skip 'not supported on Windows' if ServerEngine.windows? && sender == 'signal'
135
135
 
136
136
  sv, t = start_supervisor(command_sender: sender)
137
137
 
@@ -155,7 +155,7 @@ describe ServerEngine::Supervisor do
155
155
  end
156
156
 
157
157
  it 'immediate restart' do
158
- pending 'not supported on Windows' if ServerEngine.windows? && sender == 'signal'
158
+ skip 'not supported on Windows' if ServerEngine.windows? && sender == 'signal'
159
159
 
160
160
  sv, t = start_supervisor(command_sender: sender)
161
161
 
@@ -179,7 +179,7 @@ describe ServerEngine::Supervisor do
179
179
  end
180
180
 
181
181
  it 'reload' do
182
- pending 'not supported on Windows' if ServerEngine.windows? && sender == 'signal'
182
+ skip 'not supported on Windows' if ServerEngine.windows? && sender == 'signal'
183
183
 
184
184
  sv, t = start_supervisor(command_sender: sender)
185
185
 
@@ -200,7 +200,7 @@ describe ServerEngine::Supervisor do
200
200
  # TODO detach
201
201
 
202
202
  it 'auto restart in limited ratio' do
203
- pending 'not supported on Windows' if ServerEngine.windows? && sender == 'signal'
203
+ skip 'not supported on Windows' if ServerEngine.windows? && sender == 'signal'
204
204
 
205
205
  RR.stub(ServerEngine).dump_uncaught_error
206
206
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: serverengine
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.3.1
4
+ version: 2.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sadayuki Furuhashi
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-12-28 00:00:00.000000000 Z
11
+ date: 2024-10-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sigdump
@@ -24,34 +24,62 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: 0.2.2
27
+ - !ruby/object:Gem::Dependency
28
+ name: base64
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.1'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: logger
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.4'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.4'
27
55
  - !ruby/object:Gem::Dependency
28
56
  name: rake
29
57
  requirement: !ruby/object:Gem::Requirement
30
58
  requirements:
31
59
  - - "~>"
32
60
  - !ruby/object:Gem::Version
33
- version: '11.0'
61
+ version: '13.0'
34
62
  type: :development
35
63
  prerelease: false
36
64
  version_requirements: !ruby/object:Gem::Requirement
37
65
  requirements:
38
66
  - - "~>"
39
67
  - !ruby/object:Gem::Version
40
- version: '11.0'
68
+ version: '13.0'
41
69
  - !ruby/object:Gem::Dependency
42
70
  name: rspec
43
71
  requirement: !ruby/object:Gem::Requirement
44
72
  requirements:
45
73
  - - "~>"
46
74
  - !ruby/object:Gem::Version
47
- version: 2.13.0
75
+ version: 3.12.0
48
76
  type: :development
49
77
  prerelease: false
50
78
  version_requirements: !ruby/object:Gem::Requirement
51
79
  requirements:
52
80
  - - "~>"
53
81
  - !ruby/object:Gem::Version
54
- version: 2.13.0
82
+ version: 3.12.0
55
83
  - !ruby/object:Gem::Dependency
56
84
  name: rake-compiler-dock
57
85
  requirement: !ruby/object:Gem::Requirement
@@ -115,8 +143,7 @@ executables: []
115
143
  extensions: []
116
144
  extra_rdoc_files: []
117
145
  files:
118
- - ".github/workflows/linux.yml"
119
- - ".github/workflows/windows.yml"
146
+ - ".github/workflows/test.yml"
120
147
  - ".gitignore"
121
148
  - ".rspec"
122
149
  - Changelog
@@ -167,7 +194,7 @@ homepage: https://github.com/fluent/serverengine
167
194
  licenses:
168
195
  - Apache 2.0
169
196
  metadata: {}
170
- post_install_message:
197
+ post_install_message:
171
198
  rdoc_options: []
172
199
  require_paths:
173
200
  - lib
@@ -175,15 +202,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
175
202
  requirements:
176
203
  - - ">="
177
204
  - !ruby/object:Gem::Version
178
- version: 2.1.0
205
+ version: 2.3.0
179
206
  required_rubygems_version: !ruby/object:Gem::Requirement
180
207
  requirements:
181
208
  - - ">="
182
209
  - !ruby/object:Gem::Version
183
210
  version: '0'
184
211
  requirements: []
185
- rubygems_version: 3.3.7
186
- signing_key:
212
+ rubygems_version: 3.5.11
213
+ signing_key:
187
214
  specification_version: 4
188
215
  summary: ServerEngine - multiprocess server framework
189
216
  test_files:
@@ -1,42 +0,0 @@
1
- name: Testing on Windows
2
- on:
3
- push:
4
- branches: [master]
5
- pull_request:
6
- branches: [master]
7
-
8
- jobs:
9
- test:
10
- runs-on: ${{ matrix.os }}
11
- strategy:
12
- fail-fast: false
13
- matrix:
14
- ruby: [ '3.1', '2.7' ]
15
- os:
16
- - windows-latest
17
- include:
18
- - ruby: '3.0.3'
19
- os: windows-latest
20
- # On Ruby 3.0, we need to use fiddle 1.0.8 or later to retrieve correct
21
- # error code. In addition, we have to specify the path of fiddle by RUBYLIB
22
- # because RubyInstaller loads Ruby's bundled fiddle before initializing gem.
23
- # See also:
24
- # * https://github.com/ruby/fiddle/issues/72
25
- # * https://bugs.ruby-lang.org/issues/17813
26
- # * https://github.com/oneclick/rubyinstaller2/blob/8225034c22152d8195bc0aabc42a956c79d6c712/lib/ruby_installer/build/dll_directory.rb
27
- ruby-lib-opt: RUBYLIB=%RUNNER_TOOL_CACHE%/Ruby/3.0.3/x64/lib/ruby/gems/3.0.0/gems/fiddle-1.1.0/lib
28
-
29
- name: Unit testing with Ruby ${{ matrix.ruby }} on ${{ matrix.os }}
30
- steps:
31
- - uses: actions/checkout@v2
32
- - name: Set up Ruby
33
- uses: ruby/setup-ruby@v1
34
- with:
35
- ruby-version: ${{ matrix.ruby }}
36
- - name: Add Fiddle 1.1.0
37
- if: ${{ matrix.ruby == '3.0.3' }}
38
- run: gem install fiddle --version 1.1.0
39
- - name: Install dependencies
40
- run: ridk exec bundle install --jobs 4 --retry 3
41
- - name: Run tests
42
- run: bundle exec rake spec ${{ matrix.ruby-lib-opt }}