serverengine 2.3.2 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
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 }}