serverengine 2.3.2 → 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: 7350069b97302faf3d6c50118875bf055a7094bc31845185cd4e6b4d3e0a9f61
4
- data.tar.gz: 02e1c1e261b6ea6dde6a77a984c8f94dccf237093dda7949174acd2658c9897b
3
+ metadata.gz: 516efcdedbeba3c7d7988c91cc121a37b52efca99726d833447fe374680cac43
4
+ data.tar.gz: b4265b4b425eb2d165881d579f6b2115c8e7bf2298c56f1cf75d6ed1cb804993
5
5
  SHA512:
6
- metadata.gz: 66ae31cea3ff4779a648d64c00e54ff01e76912cd2a4ce1f6105b83657bdbdaea0e9b2c54972cc171ed2aa2a01794dd4c2c3b2e73fdb1754a7013cee2fd0c1a5
7
- data.tar.gz: 8b023a4052650036480ce2f899ac367279dcc341a3d98536c7a3fc91cf227622a3707f7315a9eead3a0a2335792700723dda3a527aa422c5aa06d2ff27824b11
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.2', '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,8 @@
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
+
1
6
  2023-03-14 version 2.3.2
2
7
 
3
8
  * Accept `nil` for `ServerEngine::SocketManager::Server.open` to select path automatically
data/README.md CHANGED
@@ -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
 
@@ -96,19 +96,32 @@ module ServerEngine
96
96
  end
97
97
  end
98
98
 
99
- def initialize(path)
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
104
+ end
105
+
106
+ def initialize(path, start: true)
100
107
  @tcp_sockets = {}
101
108
  @udp_sockets = {}
102
109
  @mutex = Mutex.new
103
- @path = start_server(path)
110
+ @path = start ? start_server(path) : path
104
111
  end
105
112
 
106
113
  attr_reader :path
114
+ attr_reader :tcp_sockets, :udp_sockets # for tests
107
115
 
108
116
  def new_client
109
117
  Client.new(@path)
110
118
  end
111
119
 
120
+ def start
121
+ start_server(path)
122
+ nil
123
+ end
124
+
112
125
  def close
113
126
  stop_server
114
127
  nil
@@ -159,9 +172,9 @@ module ServerEngine
159
172
  res = SocketManager.recv_peer(peer)
160
173
  return if res.nil?
161
174
 
162
- pid, method, bind, port = *res
175
+ pid, method, *opts = res
163
176
  begin
164
- send_socket(peer, pid, method, bind, port)
177
+ send_socket(peer, pid, method, *opts)
165
178
  rescue => e
166
179
  SocketManager.send_peer(peer, e)
167
180
  end
@@ -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
 
@@ -1,3 +1,3 @@
1
1
  module ServerEngine
2
- VERSION = "2.3.2"
2
+ VERSION = "2.4.0"
3
3
  end
data/serverengine.gemspec CHANGED
@@ -19,6 +19,8 @@ Gem::Specification.new do |gem|
19
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
26
  gem.add_development_dependency "rake", ["~> 13.0"]
@@ -55,6 +55,15 @@ describe ServerEngine::SocketManager do
55
55
  expect(server.path).to be_between(49152, 65535)
56
56
  end
57
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
58
67
  else
59
68
  context 'Server.generate_path' do
60
69
  it 'returns socket path under /tmp' do
@@ -76,6 +85,165 @@ describe ServerEngine::SocketManager do
76
85
  expect(server.path).to include('/tmp/SERVERENGINE_SOCKETMANAGER_')
77
86
  end
78
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
79
247
  end
80
248
 
81
249
  context 'with thread' do
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.2
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: 2023-03-14 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,6 +24,34 @@ 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
@@ -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
@@ -182,8 +209,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
182
209
  - !ruby/object:Gem::Version
183
210
  version: '0'
184
211
  requirements: []
185
- rubygems_version: 3.4.1
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.2', '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 }}