serverengine 2.2.5 → 2.3.1
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 +4 -4
- data/.github/workflows/linux.yml +1 -1
- data/.github/workflows/windows.yml +1 -1
- data/Changelog +17 -0
- data/README.md +3 -2
- data/Rakefile +0 -16
- data/lib/serverengine/daemon_logger.rb +37 -0
- data/lib/serverengine/multi_process_server.rb +7 -1
- data/lib/serverengine/multi_thread_server.rb +3 -0
- data/lib/serverengine/multi_worker_server.rb +35 -12
- data/lib/serverengine/process_manager.rb +9 -1
- data/lib/serverengine/server.rb +1 -1
- data/lib/serverengine/socket_manager.rb +2 -0
- data/lib/serverengine/version.rb +1 -1
- data/lib/serverengine/winsock.rb +0 -1
- data/lib/serverengine.rb +20 -5
- data/serverengine.gemspec +2 -7
- data/spec/daemon_logger_spec.rb +25 -0
- data/spec/daemon_spec.rb +11 -0
- data/spec/multi_process_server_spec.rb +152 -6
- data/spec/multi_spawn_server_spec.rb +212 -13
- data/spec/server_worker_context.rb +16 -5
- data/spec/spec_helper.rb +5 -0
- data/spec/supervisor_spec.rb +19 -3
- data/spec/winsock_spec.rb +2 -3
- metadata +31 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8b802588a6e93b69c2dc9810ab33b9e3195e14d93c5f977961a04cc6d88cacf1
|
4
|
+
data.tar.gz: 9921383ec5ae39cfe982da9ce8c0b432bbc553c2ea5d2090a2d849ec2500c230
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ea4c0e4b34490665e4287c0f43e4ea1f32c10fe022464a69326505ec21aad2650e53b842c2aaeec2a969d914f73715ee059b2bdbb3d72d14b0bce51192bf217c
|
7
|
+
data.tar.gz: 3e30810b08b8667f2295b41fca60c4683126d8936e32169d0789b8ec02ba3965fc631dbe069c9a8cf057e109ccaa9ad3ad14f4507b2388dcd9ae27463ce39651
|
data/.github/workflows/linux.yml
CHANGED
data/Changelog
CHANGED
@@ -1,6 +1,23 @@
|
|
1
|
+
2022-12-22 version 2.3.1
|
2
|
+
|
3
|
+
* Don't treat as error when worker shuts down with exit status 0
|
4
|
+
|
5
|
+
2022-06-13 version 2.3.0
|
6
|
+
|
7
|
+
* Add restart_worker_interval option to prevent workers restart immediately
|
8
|
+
after kill
|
9
|
+
* Reopen log file when rotation done by external tool is detected
|
10
|
+
* Fix unexpected behavior of start_worker_delay option
|
11
|
+
* Remove windows-pr dependency
|
12
|
+
* Fix a potential crash that command_sender_pipe of ProcessManager::Monitor
|
13
|
+
raises error on shutdown
|
14
|
+
* Allow to load serverengine/socket_manager without servernegine/utils
|
15
|
+
* Fix unstable tests
|
16
|
+
|
1
17
|
2022-01-13 version 2.2.5:
|
2
18
|
|
3
19
|
* Fix DLL load error on Ruby 3.1 on Windows
|
20
|
+
* Treat as error when worker shuts down unexpectedly
|
4
21
|
|
5
22
|
2021-05-24 version 2.2.4:
|
6
23
|
|
data/README.md
CHANGED
@@ -478,10 +478,11 @@ Available methods are different depending on `worker_type`. ServerEngine support
|
|
478
478
|
- **disable_reload** disables USR2 signal (default: false)
|
479
479
|
- **server_restart_wait** sets wait time before restarting server after last restarting (default: 1.0) [dynamic reloadable]
|
480
480
|
- **server_detach_wait** sets wait time before starting live restart (default: 10.0) [dynamic reloadable]
|
481
|
-
- Multithread server and multiprocess server: available only when `worker_type` is thread or process
|
481
|
+
- Multithread server and multiprocess server: available only when `worker_type` is "thread" or "process" or "spawn"
|
482
482
|
- **workers** sets number of workers (default: 1) [dynamic reloadable]
|
483
|
-
- **start_worker_delay** sets
|
483
|
+
- **start_worker_delay** sets the delay between each worker-start when starting/restarting multiple workers at once (default: 0) [dynamic reloadable]
|
484
484
|
- **start_worker_delay_rand** randomizes start_worker_delay at this ratio (default: 0.2) [dynamic reloadable]
|
485
|
+
- **restart_worker_interval** sets wait time before restarting a stopped worker (default: 0) [dynamic reloadable]
|
485
486
|
- Multiprocess server: available only when `worker_type` is "process"
|
486
487
|
- **worker_process_name** changes process name ($0) of workers [dynamic reloadable]
|
487
488
|
- **worker_heartbeat_interval** sets interval of heartbeats in seconds (default: 1.0) [dynamic reloadable]
|
data/Rakefile
CHANGED
@@ -8,19 +8,3 @@ require 'rspec/core/rake_task'
|
|
8
8
|
|
9
9
|
RSpec::Core::RakeTask.new(:spec)
|
10
10
|
task :default => [:spec, :build]
|
11
|
-
|
12
|
-
# 1. update Changelog and lib/serverengine/version.rb
|
13
|
-
# 2. bundle && bundle exec rake build:all
|
14
|
-
# 3. release 3 packages built on pkg/ directory
|
15
|
-
namespace :build do
|
16
|
-
desc 'Build gems for all platforms'
|
17
|
-
task :all do
|
18
|
-
Bundler.with_clean_env do
|
19
|
-
%w[ruby x86-mingw32 x64-mingw32].each do |name|
|
20
|
-
ENV['GEM_BUILD_FAKE_PLATFORM'] = name
|
21
|
-
Rake::Task["build"].execute
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
@@ -55,6 +55,9 @@ module ServerEngine
|
|
55
55
|
# update path string
|
56
56
|
old_file_dev = @file_dev
|
57
57
|
@file_dev = LogDevice.new(logdev, shift_age: @rotate_age, shift_size: @rotate_size)
|
58
|
+
# Enable to detect rotation done by external tools.
|
59
|
+
# Otherwise it continues writing logs to old file unexpectedly.
|
60
|
+
@file_dev.extend(RotationAware)
|
58
61
|
old_file_dev.close if old_file_dev
|
59
62
|
@logdev = @file_dev
|
60
63
|
end
|
@@ -130,6 +133,40 @@ module ServerEngine
|
|
130
133
|
nil
|
131
134
|
end
|
132
135
|
|
136
|
+
module RotationAware
|
137
|
+
def self.extended(obj)
|
138
|
+
obj.update_ino
|
139
|
+
end
|
140
|
+
|
141
|
+
def update_ino
|
142
|
+
(@ino_mutex ||= Mutex.new).synchronize do
|
143
|
+
@ino = File.stat(filename).ino rescue nil
|
144
|
+
@last_ino_time = Time.now
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def reopen(log = nil)
|
149
|
+
super(log)
|
150
|
+
update_ino
|
151
|
+
end
|
152
|
+
|
153
|
+
def reopen!
|
154
|
+
super
|
155
|
+
update_ino
|
156
|
+
end
|
157
|
+
|
158
|
+
def write(message)
|
159
|
+
reopen_needed = false
|
160
|
+
@ino_mutex.synchronize do
|
161
|
+
if (Time.now - @last_ino_time).abs > 1
|
162
|
+
ino = File.stat(filename).ino rescue nil
|
163
|
+
reopen_needed = true if ino && ino != @ino
|
164
|
+
end
|
165
|
+
end
|
166
|
+
reopen! if reopen_needed
|
167
|
+
super(message)
|
168
|
+
end
|
169
|
+
end
|
133
170
|
end
|
134
171
|
|
135
172
|
end
|
@@ -105,9 +105,11 @@ module ServerEngine
|
|
105
105
|
@unrecoverable_exit_codes = unrecoverable_exit_codes
|
106
106
|
@unrecoverable_exit = false
|
107
107
|
@exitstatus = nil
|
108
|
+
@restart_at = nil
|
108
109
|
end
|
109
110
|
|
110
111
|
attr_reader :exitstatus
|
112
|
+
attr_accessor :restart_at
|
111
113
|
|
112
114
|
def send_stop(stop_graceful)
|
113
115
|
@stop = true
|
@@ -145,7 +147,11 @@ module ServerEngine
|
|
145
147
|
if @stop
|
146
148
|
@worker.logger.info "Worker #{@wid} finished with #{ServerEngine.format_join_status(stat)}"
|
147
149
|
else
|
148
|
-
|
150
|
+
if stat.is_a?(Process::Status) && stat.success?
|
151
|
+
@worker.logger.info "Worker #{@wid} exited with #{ServerEngine.format_join_status(stat)}"
|
152
|
+
else
|
153
|
+
@worker.logger.error "Worker #{@wid} exited unexpectedly with #{ServerEngine.format_join_status(stat)}"
|
154
|
+
end
|
149
155
|
end
|
150
156
|
if stat.is_a?(Process::Status) && stat.exited? && @unrecoverable_exit_codes.include?(stat.exitstatus)
|
151
157
|
@unrecoverable_exit = true
|
@@ -55,8 +55,8 @@ module ServerEngine
|
|
55
55
|
|
56
56
|
def run
|
57
57
|
while true
|
58
|
-
|
59
|
-
break if
|
58
|
+
num_alive_or_restarting = keepalive_workers
|
59
|
+
break if num_alive_or_restarting == 0
|
60
60
|
wait_tick
|
61
61
|
end
|
62
62
|
end
|
@@ -85,6 +85,7 @@ module ServerEngine
|
|
85
85
|
|
86
86
|
@start_worker_delay = @config[:start_worker_delay] || 0
|
87
87
|
@start_worker_delay_rand = @config[:start_worker_delay_rand] || 0.2
|
88
|
+
@restart_worker_interval = @config[:restart_worker_interval] || 0
|
88
89
|
|
89
90
|
scale_workers(@config[:workers] || 1)
|
90
91
|
|
@@ -96,12 +97,12 @@ module ServerEngine
|
|
96
97
|
end
|
97
98
|
|
98
99
|
def keepalive_workers
|
99
|
-
|
100
|
+
num_alive_or_restarting = 0
|
100
101
|
|
101
102
|
@monitors.each_with_index do |m,wid|
|
102
103
|
if m && m.alive?
|
103
104
|
# alive
|
104
|
-
|
105
|
+
num_alive_or_restarting += 1
|
105
106
|
|
106
107
|
elsif m && m.respond_to?(:recoverable?) && !m.recoverable?
|
107
108
|
# exited, with unrecoverable exit code
|
@@ -116,8 +117,12 @@ module ServerEngine
|
|
116
117
|
elsif wid < @num_workers
|
117
118
|
# scale up or reboot
|
118
119
|
unless @stop
|
119
|
-
|
120
|
-
|
120
|
+
if m
|
121
|
+
restart_worker(wid)
|
122
|
+
else
|
123
|
+
start_new_worker(wid)
|
124
|
+
end
|
125
|
+
num_alive_or_restarting += 1
|
121
126
|
end
|
122
127
|
|
123
128
|
elsif m
|
@@ -126,7 +131,27 @@ module ServerEngine
|
|
126
131
|
end
|
127
132
|
end
|
128
133
|
|
129
|
-
return
|
134
|
+
return num_alive_or_restarting
|
135
|
+
end
|
136
|
+
|
137
|
+
def start_new_worker(wid)
|
138
|
+
delayed_start_worker(wid)
|
139
|
+
end
|
140
|
+
|
141
|
+
def restart_worker(wid)
|
142
|
+
m = @monitors[wid]
|
143
|
+
|
144
|
+
is_already_restarting = !m.restart_at.nil?
|
145
|
+
if is_already_restarting
|
146
|
+
delayed_start_worker(wid) if m.restart_at <= Time.now()
|
147
|
+
return
|
148
|
+
end
|
149
|
+
|
150
|
+
if @restart_worker_interval > 0
|
151
|
+
m.restart_at = Time.now() + @restart_worker_interval
|
152
|
+
else
|
153
|
+
delayed_start_worker(wid)
|
154
|
+
end
|
130
155
|
end
|
131
156
|
|
132
157
|
def delayed_start_worker(wid)
|
@@ -135,15 +160,13 @@ module ServerEngine
|
|
135
160
|
Kernel.rand * @start_worker_delay * @start_worker_delay_rand -
|
136
161
|
@start_worker_delay * @start_worker_delay_rand / 2
|
137
162
|
|
138
|
-
|
139
|
-
|
140
|
-
wait = delay - (now - @last_start_worker_time)
|
163
|
+
wait = delay - (Time.now.to_f - @last_start_worker_time)
|
141
164
|
sleep wait if wait > 0
|
142
165
|
|
143
|
-
@last_start_worker_time = now
|
166
|
+
@last_start_worker_time = Time.now.to_f
|
144
167
|
end
|
145
168
|
|
146
|
-
start_worker(wid)
|
169
|
+
@monitors[wid] = start_worker(wid)
|
147
170
|
end
|
148
171
|
end
|
149
172
|
|
@@ -333,7 +333,15 @@ module ServerEngine
|
|
333
333
|
end
|
334
334
|
|
335
335
|
def send_command(command)
|
336
|
-
|
336
|
+
pid = @pid
|
337
|
+
return false unless pid
|
338
|
+
|
339
|
+
begin
|
340
|
+
@command_sender_pipe.write(command)
|
341
|
+
return true
|
342
|
+
rescue #Errno::EPIPE
|
343
|
+
return false
|
344
|
+
end
|
337
345
|
end
|
338
346
|
|
339
347
|
def try_join
|
data/lib/serverengine/server.rb
CHANGED
@@ -22,6 +22,8 @@ require 'securerandom'
|
|
22
22
|
require 'json'
|
23
23
|
require 'base64'
|
24
24
|
|
25
|
+
require_relative 'utils' # for ServerEngine.windows?
|
26
|
+
|
25
27
|
module ServerEngine
|
26
28
|
module SocketManager
|
27
29
|
# This token is used for communication between peers. If token is mismatched, messages will be discarded
|
data/lib/serverengine/version.rb
CHANGED
data/lib/serverengine/winsock.rb
CHANGED
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
|
-
|
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
|
-
|
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,6 @@ 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
|
-
|
31
|
-
|
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"]
|
31
|
+
gem.add_development_dependency "rr", ["~> 3.1"]
|
37
32
|
end
|
data/spec/daemon_logger_spec.rb
CHANGED
@@ -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
|
data/spec/daemon_spec.rb
CHANGED
@@ -2,6 +2,14 @@
|
|
2
2
|
describe ServerEngine::Daemon do
|
3
3
|
include_context 'test server and worker'
|
4
4
|
|
5
|
+
before do
|
6
|
+
@log_path = "tmp/multi-worker-test-#{SecureRandom.hex(10)}.log"
|
7
|
+
end
|
8
|
+
|
9
|
+
after do
|
10
|
+
FileUtils.rm_rf(@log_path)
|
11
|
+
end
|
12
|
+
|
5
13
|
it 'run and graceful stop by signal' do
|
6
14
|
pending "not supported signal base commands on Windows" if ServerEngine.windows?
|
7
15
|
|
@@ -111,6 +119,7 @@ describe ServerEngine::Daemon do
|
|
111
119
|
daemonize: false,
|
112
120
|
supervisor: false,
|
113
121
|
pid_path: "tmp/pid",
|
122
|
+
logger: ServerEngine::DaemonLogger.new(@log_path),
|
114
123
|
log_stdout: false,
|
115
124
|
log_stderr: false,
|
116
125
|
unrecoverable_exit_codes: [3,4,5],
|
@@ -135,6 +144,7 @@ describe ServerEngine::Daemon do
|
|
135
144
|
supervisor: false,
|
136
145
|
worker_type: 'process',
|
137
146
|
pid_path: "tmp/pid",
|
147
|
+
logger: ServerEngine::DaemonLogger.new(@log_path),
|
138
148
|
log_stdout: false,
|
139
149
|
log_stderr: false,
|
140
150
|
unrecoverable_exit_codes: [3,4,5],
|
@@ -158,6 +168,7 @@ describe ServerEngine::Daemon do
|
|
158
168
|
supervisor: true,
|
159
169
|
worker_type: 'process',
|
160
170
|
pid_path: "tmp/pid",
|
171
|
+
logger: ServerEngine::DaemonLogger.new(@log_path),
|
161
172
|
log_stdout: false,
|
162
173
|
log_stderr: false,
|
163
174
|
unrecoverable_exit_codes: [3,4,5],
|
@@ -1,13 +1,30 @@
|
|
1
|
+
require 'timeout'
|
2
|
+
require 'securerandom'
|
3
|
+
|
1
4
|
[ServerEngine::MultiThreadServer, ServerEngine::MultiProcessServer].each do |impl_class|
|
2
5
|
# MultiProcessServer uses fork(2) internally, then it doesn't support Windows.
|
3
6
|
|
4
7
|
describe impl_class do
|
5
8
|
include_context 'test server and worker'
|
6
9
|
|
10
|
+
before do
|
11
|
+
@log_path = "tmp/multi-worker-test-#{SecureRandom.hex(10)}.log"
|
12
|
+
@logger = ServerEngine::DaemonLogger.new(@log_path)
|
13
|
+
end
|
14
|
+
|
15
|
+
after do
|
16
|
+
FileUtils.rm_rf(@log_path)
|
17
|
+
end
|
18
|
+
|
7
19
|
it 'scale up' do
|
8
20
|
pending "Windows environment does not support fork" if ServerEngine.windows? && impl_class == ServerEngine::MultiProcessServer
|
9
21
|
|
10
|
-
config = {
|
22
|
+
config = {
|
23
|
+
workers: 2,
|
24
|
+
logger: @logger,
|
25
|
+
log_stdout: false,
|
26
|
+
log_stderr: false,
|
27
|
+
}
|
11
28
|
|
12
29
|
s = impl_class.new(TestWorker) { config.dup }
|
13
30
|
t = Thread.new { s.main }
|
@@ -35,7 +52,12 @@
|
|
35
52
|
it 'scale down' do
|
36
53
|
pending "Windows environment does not support fork" if ServerEngine.windows? && impl_class == ServerEngine::MultiProcessServer
|
37
54
|
|
38
|
-
config = {
|
55
|
+
config = {
|
56
|
+
workers: 2,
|
57
|
+
logger: @logger,
|
58
|
+
log_stdout: false,
|
59
|
+
log_stderr: false
|
60
|
+
}
|
39
61
|
|
40
62
|
s = impl_class.new(TestWorker) { config.dup }
|
41
63
|
t = Thread.new { s.main }
|
@@ -59,12 +81,32 @@
|
|
59
81
|
|
60
82
|
test_state(:worker_stop).should == 3
|
61
83
|
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
[ServerEngine::MultiProcessServer].each do |impl_class|
|
88
|
+
describe impl_class do
|
89
|
+
include_context 'test server and worker'
|
90
|
+
|
91
|
+
before do
|
92
|
+
@log_path = "tmp/multi-worker-test-#{SecureRandom.hex(10)}.log"
|
93
|
+
@logger = ServerEngine::DaemonLogger.new(@log_path)
|
94
|
+
end
|
95
|
+
|
96
|
+
after do
|
97
|
+
FileUtils.rm_rf(@log_path)
|
98
|
+
end
|
62
99
|
|
63
100
|
it 'raises SystemExit when all workers exit with specified code by unrecoverable_exit_codes' do
|
64
|
-
pending "unrecoverable_exit_codes supported only for multi process workers" if impl_class == ServerEngine::MultiThreadServer
|
65
101
|
pending "Windows environment does not support fork" if ServerEngine.windows? && impl_class == ServerEngine::MultiProcessServer
|
66
102
|
|
67
|
-
config = {
|
103
|
+
config = {
|
104
|
+
workers: 4,
|
105
|
+
logger: @logger,
|
106
|
+
log_stdout: false,
|
107
|
+
log_stderr: false,
|
108
|
+
unrecoverable_exit_codes: [3, 4, 5]
|
109
|
+
}
|
68
110
|
|
69
111
|
s = impl_class.new(TestExitWorker) { config.dup }
|
70
112
|
raised_error = nil
|
@@ -85,10 +127,16 @@
|
|
85
127
|
end
|
86
128
|
|
87
129
|
it 'raises SystemExit immediately when a worker exits if stop_immediately_at_unrecoverable_exit specified' do
|
88
|
-
pending "unrecoverable_exit_codes supported only for multi process workers" if impl_class == ServerEngine::MultiThreadServer
|
89
130
|
pending "Windows environment does not support fork" if ServerEngine.windows? && impl_class == ServerEngine::MultiProcessServer
|
90
131
|
|
91
|
-
config = {
|
132
|
+
config = {
|
133
|
+
workers: 4,
|
134
|
+
logger: @logger,
|
135
|
+
log_stdout: false,
|
136
|
+
log_stderr: false,
|
137
|
+
unrecoverable_exit_codes: [3, 4, 5],
|
138
|
+
stop_immediately_at_unrecoverable_exit: true
|
139
|
+
}
|
92
140
|
|
93
141
|
s = impl_class.new(TestExitWorker) { config.dup }
|
94
142
|
raised_error = nil
|
@@ -111,3 +159,101 @@
|
|
111
159
|
end
|
112
160
|
end
|
113
161
|
end
|
162
|
+
|
163
|
+
describe "log level for exited proccess" do
|
164
|
+
include_context 'test server and worker'
|
165
|
+
|
166
|
+
before do
|
167
|
+
@log_path = "tmp/multi-process-log-level-test-#{SecureRandom.hex(10)}.log"
|
168
|
+
end
|
169
|
+
|
170
|
+
after do
|
171
|
+
FileUtils.rm_rf(@log_path)
|
172
|
+
end
|
173
|
+
|
174
|
+
it 'stop' do
|
175
|
+
pending "Windows environment does not support fork" if ServerEngine.windows?
|
176
|
+
|
177
|
+
config = {
|
178
|
+
workers: 1,
|
179
|
+
logger: ServerEngine::DaemonLogger.new(@log_path),
|
180
|
+
log_stdout: false,
|
181
|
+
log_stderr: false,
|
182
|
+
}
|
183
|
+
|
184
|
+
s = ServerEngine::MultiProcessServer.new(TestWorker) { config.dup }
|
185
|
+
t = Thread.new { s.main }
|
186
|
+
|
187
|
+
begin
|
188
|
+
wait_for_fork
|
189
|
+
test_state(:worker_run).should == 1
|
190
|
+
ensure
|
191
|
+
s.stop(true)
|
192
|
+
t.join
|
193
|
+
end
|
194
|
+
|
195
|
+
log_lines = File.read(@log_path).split("\n")
|
196
|
+
expect(log_lines[2]).to match(/^I, \[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d+ #\d+\] INFO -- : Worker 0 finished with status 0$/)
|
197
|
+
end
|
198
|
+
|
199
|
+
it 'non zero exit status' do
|
200
|
+
pending "Windows environment does not support fork" if ServerEngine.windows?
|
201
|
+
|
202
|
+
config = {
|
203
|
+
workers: 1,
|
204
|
+
logger: ServerEngine::DaemonLogger.new(@log_path),
|
205
|
+
log_stdout: false,
|
206
|
+
log_stderr: false,
|
207
|
+
unrecoverable_exit_codes: [5],
|
208
|
+
}
|
209
|
+
|
210
|
+
s = ServerEngine::MultiProcessServer.new(TestExitWorker) { config.dup }
|
211
|
+
raised_error = nil
|
212
|
+
Thread.new do
|
213
|
+
begin
|
214
|
+
s.main
|
215
|
+
rescue SystemExit => e
|
216
|
+
raised_error = e
|
217
|
+
end
|
218
|
+
end.join
|
219
|
+
|
220
|
+
test_state(:worker_stop).to_i.should == 0
|
221
|
+
raised_error.status.should == 5
|
222
|
+
log_lines = File.read(@log_path).split("\n")
|
223
|
+
expect(log_lines[1]).to match(/^E, \[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d+ #\d+\] ERROR -- : Worker 0 exited unexpectedly with status 5$/)
|
224
|
+
end
|
225
|
+
|
226
|
+
module TestNormalExitWorker
|
227
|
+
include TestExitWorker
|
228
|
+
def initialize
|
229
|
+
super
|
230
|
+
@exit_code = 0
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
it 'zero exit status' do
|
235
|
+
pending "Windows environment does not support fork" if ServerEngine.windows?
|
236
|
+
|
237
|
+
config = {
|
238
|
+
workers: 1,
|
239
|
+
logger: ServerEngine::DaemonLogger.new(@log_path),
|
240
|
+
log_stdout: false,
|
241
|
+
log_stderr: false,
|
242
|
+
}
|
243
|
+
|
244
|
+
s = ServerEngine::MultiProcessServer.new(TestNormalExitWorker) { config.dup }
|
245
|
+
t = Thread.new { s.main }
|
246
|
+
|
247
|
+
begin
|
248
|
+
Timeout.timeout(5) do
|
249
|
+
sleep 1 until File.read(@log_path).include?("INFO -- : Worker 0 exited with status 0")
|
250
|
+
end
|
251
|
+
ensure
|
252
|
+
s.stop(true)
|
253
|
+
t.join
|
254
|
+
end
|
255
|
+
|
256
|
+
log_lines = File.read(@log_path).split("\n")
|
257
|
+
expect(log_lines[1]).to match(/^I, \[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d+ #\d+\] INFO -- : Worker 0 exited with status 0$/)
|
258
|
+
end
|
259
|
+
end
|
@@ -1,25 +1,224 @@
|
|
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
|
-
|
7
|
-
|
8
|
-
|
7
|
+
before do
|
8
|
+
@log_path = "tmp/multi-worker-test-#{SecureRandom.hex(10)}.log"
|
9
|
+
@logger = ServerEngine::DaemonLogger.new(@log_path)
|
10
|
+
end
|
11
|
+
|
12
|
+
after do
|
13
|
+
FileUtils.rm_rf(@log_path)
|
14
|
+
end
|
15
|
+
|
16
|
+
describe 'starts worker processes' do
|
17
|
+
context 'with command_sender=pipe' do
|
18
|
+
it do
|
19
|
+
config = {
|
20
|
+
workers: 2,
|
21
|
+
command_sender: 'pipe',
|
22
|
+
logger: @logger,
|
23
|
+
log_stdout: false,
|
24
|
+
log_stderr: false
|
25
|
+
}
|
26
|
+
|
27
|
+
s = ServerEngine::MultiSpawnServer.new(TestWorker) { config.dup }
|
28
|
+
t = Thread.new { s.main }
|
29
|
+
|
30
|
+
begin
|
31
|
+
wait_for_fork
|
32
|
+
|
33
|
+
Timeout.timeout(5) do
|
34
|
+
sleep(0.5) until test_state(:worker_run) == 2
|
35
|
+
end
|
36
|
+
test_state(:worker_run).should == 2
|
37
|
+
ensure
|
38
|
+
s.stop(true)
|
39
|
+
t.join
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe 'keepalive_workers' do
|
46
|
+
let(:config) {
|
47
|
+
{
|
48
|
+
workers: workers,
|
49
|
+
command_sender: 'pipe',
|
50
|
+
logger: @logger,
|
51
|
+
log_stdout: false,
|
52
|
+
log_stderr: false,
|
53
|
+
start_worker_delay: start_worker_delay,
|
54
|
+
start_worker_delay_rand: 0,
|
55
|
+
restart_worker_interval: restart_worker_interval,
|
56
|
+
}
|
57
|
+
}
|
58
|
+
let(:workers) { 3 }
|
59
|
+
let(:server) { ServerEngine::MultiSpawnServer.new(TestWorker) { config.dup } }
|
60
|
+
let(:monitors) { server.instance_variable_get(:@monitors) }
|
61
|
+
|
62
|
+
context 'default' do
|
63
|
+
let(:start_worker_delay) { 0 }
|
64
|
+
let(:restart_worker_interval) { 0 }
|
65
|
+
|
66
|
+
it do
|
67
|
+
t = Thread.new { server.main }
|
68
|
+
|
69
|
+
begin
|
70
|
+
wait_for_fork
|
71
|
+
|
72
|
+
Timeout.timeout(5) do
|
73
|
+
sleep(0.5) until monitors.count { |m| m && m.alive? } == workers
|
74
|
+
end
|
75
|
+
|
76
|
+
monitors.each do |m|
|
77
|
+
m.send_stop(true)
|
78
|
+
end
|
79
|
+
|
80
|
+
# To prevent the judge before stopping once
|
81
|
+
wait_for_stop
|
82
|
+
|
83
|
+
-> {
|
84
|
+
Timeout.timeout(5) do
|
85
|
+
sleep(0.5) until monitors.count { |m| m.alive? } == workers
|
86
|
+
end
|
87
|
+
}.should_not raise_error, "Not all workers restarted correctly."
|
88
|
+
ensure
|
89
|
+
server.stop(true)
|
90
|
+
t.join
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
context 'with only restart_worker_interval' do
|
96
|
+
let(:start_worker_delay) { 0 }
|
97
|
+
let(:restart_worker_interval) { 10 }
|
98
|
+
|
99
|
+
it do
|
100
|
+
t = Thread.new { server.main }
|
101
|
+
|
102
|
+
begin
|
103
|
+
wait_for_fork
|
104
|
+
|
105
|
+
# Wait for initial starting
|
106
|
+
Timeout.timeout(5) do
|
107
|
+
sleep(0.5) until monitors.count { |m| m && m.alive? } == workers
|
108
|
+
end
|
109
|
+
|
110
|
+
monitors.each do |m|
|
111
|
+
m.send_stop(true)
|
112
|
+
end
|
113
|
+
|
114
|
+
# Wait for all workers to stop and to be set restarting time
|
115
|
+
Timeout.timeout(5) do
|
116
|
+
sleep(0.5) until monitors.count { |m| m.alive? || m.restart_at.nil? } == 0
|
117
|
+
end
|
118
|
+
|
119
|
+
Timecop.freeze
|
120
|
+
|
121
|
+
mergin_time = 3
|
122
|
+
|
123
|
+
Timecop.freeze(Time.now + restart_worker_interval - mergin_time)
|
124
|
+
sleep(1.5)
|
125
|
+
monitors.count { |m| m.alive? }.should == 0
|
126
|
+
|
127
|
+
Timecop.freeze(Time.now + 2 * mergin_time)
|
128
|
+
-> {
|
129
|
+
Timeout.timeout(5) do
|
130
|
+
sleep(0.5) until monitors.count { |m| m.alive? } == workers
|
131
|
+
end
|
132
|
+
}.should_not raise_error, "Not all workers restarted correctly."
|
133
|
+
ensure
|
134
|
+
server.stop(true)
|
135
|
+
t.join
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
context 'with only start_worker_delay' do
|
141
|
+
let(:start_worker_delay) { 3 }
|
142
|
+
let(:restart_worker_interval) { 0 }
|
143
|
+
|
144
|
+
it do
|
145
|
+
t = Thread.new { server.main }
|
146
|
+
|
147
|
+
begin
|
148
|
+
wait_for_fork
|
149
|
+
|
150
|
+
# Initial starts are delayed too, so set longer timeout.
|
151
|
+
# (`start_worker_delay` uses `sleep` inside, so Timecop can't skip this wait.)
|
152
|
+
Timeout.timeout(start_worker_delay * workers) do
|
153
|
+
sleep(0.5) until monitors.count { |m| m && m.alive? } == workers
|
154
|
+
end
|
155
|
+
|
156
|
+
# Skip time to avoid getting a delay for the initial starts.
|
157
|
+
Timecop.travel(Time.now + start_worker_delay)
|
158
|
+
|
159
|
+
monitors.each do |m|
|
160
|
+
m.send_stop(true)
|
161
|
+
end
|
162
|
+
|
163
|
+
sleep(3)
|
164
|
+
|
165
|
+
# The first worker should restart immediately.
|
166
|
+
monitors.count { |m| m.alive? }.should satisfy { |c| 0 < c && c < workers }
|
167
|
+
|
168
|
+
# `start_worker_delay` uses `sleep` inside, so Timecop can't skip this wait.
|
169
|
+
sleep(start_worker_delay * workers)
|
170
|
+
monitors.count { |m| m.alive? }.should == workers
|
171
|
+
ensure
|
172
|
+
server.stop(true)
|
173
|
+
t.join
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
context 'with both options' do
|
179
|
+
let(:start_worker_delay) { 3 }
|
180
|
+
let(:restart_worker_interval) { 10 }
|
181
|
+
|
182
|
+
it do
|
183
|
+
t = Thread.new { server.main }
|
184
|
+
|
185
|
+
begin
|
186
|
+
wait_for_fork
|
187
|
+
|
188
|
+
# Initial starts are delayed too, so set longer timeout.
|
189
|
+
# (`start_worker_delay` uses `sleep` inside, so Timecop can't skip this wait.)
|
190
|
+
Timeout.timeout(start_worker_delay * workers) do
|
191
|
+
sleep(0.5) until monitors.count { |m| m && m.alive? } == workers
|
192
|
+
end
|
193
|
+
|
194
|
+
monitors.each do |m|
|
195
|
+
m.send_stop(true)
|
196
|
+
end
|
197
|
+
|
198
|
+
# Wait for all workers to stop and to be set restarting time
|
199
|
+
Timeout.timeout(5) do
|
200
|
+
sleep(0.5) until monitors.count { |m| m.alive? || m.restart_at.nil? } == 0
|
201
|
+
end
|
202
|
+
|
203
|
+
Timecop.freeze
|
204
|
+
|
205
|
+
mergin_time = 3
|
9
206
|
|
10
|
-
|
11
|
-
|
207
|
+
Timecop.freeze(Time.now + restart_worker_interval - mergin_time)
|
208
|
+
sleep(1.5)
|
209
|
+
monitors.count { |m| m.alive? }.should == 0
|
12
210
|
|
13
|
-
|
14
|
-
|
211
|
+
Timecop.travel(Time.now + 2 * mergin_time)
|
212
|
+
sleep(1.5)
|
213
|
+
monitors.count { |m| m.alive? }.should satisfy { |c| 0 < c && c < workers }
|
15
214
|
|
16
|
-
|
17
|
-
sleep(
|
215
|
+
# `start_worker_delay` uses `sleep` inside, so Timecop can't skip this wait.
|
216
|
+
sleep(start_worker_delay * workers)
|
217
|
+
monitors.count { |m| m.alive? }.should == workers
|
218
|
+
ensure
|
219
|
+
server.stop(true)
|
220
|
+
t.join
|
18
221
|
end
|
19
|
-
test_state(:worker_run).should == 2
|
20
|
-
ensure
|
21
|
-
s.stop(true)
|
22
|
-
t.join
|
23
222
|
end
|
24
223
|
end
|
25
224
|
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
|
-
|
169
|
-
|
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 1.5
|
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
|
data/spec/spec_helper.rb
CHANGED
data/spec/supervisor_spec.rb
CHANGED
@@ -1,8 +1,12 @@
|
|
1
|
+
require 'rr'
|
1
2
|
|
2
3
|
describe ServerEngine::Supervisor do
|
3
4
|
include_context 'test server and worker'
|
4
5
|
|
5
6
|
def start_supervisor(worker = nil, **config)
|
7
|
+
config[:log] ||= @log_path
|
8
|
+
config[:log_stdout] ||= false
|
9
|
+
config[:log_stderr] ||= false
|
6
10
|
if ServerEngine.windows?
|
7
11
|
config[:windows_daemon_cmdline] = windows_supervisor_cmdline(nil, worker, config)
|
8
12
|
end
|
@@ -13,6 +17,8 @@ describe ServerEngine::Supervisor do
|
|
13
17
|
end
|
14
18
|
|
15
19
|
def start_daemon(**config)
|
20
|
+
config[:log_stdout] ||= false
|
21
|
+
config[:log_stderr] ||= false
|
16
22
|
if ServerEngine.windows?
|
17
23
|
config[:windows_daemon_cmdline] = windows_daemon_cmdline
|
18
24
|
end
|
@@ -22,9 +28,17 @@ describe ServerEngine::Supervisor do
|
|
22
28
|
return daemon, t
|
23
29
|
end
|
24
30
|
|
31
|
+
before do
|
32
|
+
@log_path = "tmp/supervisor-test-#{SecureRandom.hex(10)}.log"
|
33
|
+
end
|
34
|
+
|
35
|
+
after do
|
36
|
+
FileUtils.rm_rf(@log_path)
|
37
|
+
end
|
38
|
+
|
25
39
|
context 'when :log=IO option is given' do
|
26
40
|
it 'can start' do
|
27
|
-
daemon, t = start_daemon(log:
|
41
|
+
daemon, t = start_daemon(log: File.open(@log_path, "wb"))
|
28
42
|
|
29
43
|
begin
|
30
44
|
wait_for_fork
|
@@ -40,7 +54,7 @@ describe ServerEngine::Supervisor do
|
|
40
54
|
|
41
55
|
context 'when :logger option is given' do
|
42
56
|
it 'uses specified logger instance' do
|
43
|
-
logger = ServerEngine::DaemonLogger.new(
|
57
|
+
logger = ServerEngine::DaemonLogger.new(@log_path)
|
44
58
|
daemon, t = start_daemon(logger: logger)
|
45
59
|
|
46
60
|
begin
|
@@ -57,7 +71,7 @@ describe ServerEngine::Supervisor do
|
|
57
71
|
|
58
72
|
context 'when both :logger and :log options are given' do
|
59
73
|
it 'start ignoring :log' do
|
60
|
-
logger = ServerEngine::DaemonLogger.new(
|
74
|
+
logger = ServerEngine::DaemonLogger.new(@log_path)
|
61
75
|
daemon, t = start_daemon(logger: logger, log: STDERR)
|
62
76
|
|
63
77
|
begin
|
@@ -188,6 +202,8 @@ describe ServerEngine::Supervisor do
|
|
188
202
|
it 'auto restart in limited ratio' do
|
189
203
|
pending 'not supported on Windows' if ServerEngine.windows? && sender == 'signal'
|
190
204
|
|
205
|
+
RR.stub(ServerEngine).dump_uncaught_error
|
206
|
+
|
191
207
|
sv, t = start_supervisor(RunErrorWorker, server_restart_wait: 1, command_sender: sender)
|
192
208
|
|
193
209
|
begin
|
data/spec/winsock_spec.rb
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
require 'windows/error' if ServerEngine.windows?
|
2
|
-
|
3
1
|
describe ServerEngine::WinSock do
|
4
2
|
# On Ruby 3.0, you need to use fiddle 1.0.8 or later to retrieve a correct
|
5
3
|
# error code. In addition, you need to specify the path of fiddle by RUBYLIB
|
@@ -12,7 +10,8 @@ describe ServerEngine::WinSock do
|
|
12
10
|
context 'last_error' do
|
13
11
|
it 'bind error' do
|
14
12
|
expect(WinSock.bind(0, nil, 0)).to be -1
|
15
|
-
|
13
|
+
WSAENOTSOCK = 10038
|
14
|
+
expect(WinSock.last_error).to be WSAENOTSOCK
|
16
15
|
end
|
17
16
|
end
|
18
17
|
end if ServerEngine.windows?
|
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.
|
4
|
+
version: 2.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sadayuki Furuhashi
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-12-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: sigdump
|
@@ -80,6 +80,34 @@ dependencies:
|
|
80
80
|
- - "~>"
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: 0.9.4
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: timecop
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 0.9.5
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 0.9.5
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rr
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '3.1'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '3.1'
|
83
111
|
description: A framework to implement robust multiprocess servers like Unicorn
|
84
112
|
email:
|
85
113
|
- frsyuki@gmail.com
|
@@ -154,7 +182,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
154
182
|
- !ruby/object:Gem::Version
|
155
183
|
version: '0'
|
156
184
|
requirements: []
|
157
|
-
rubygems_version: 3.
|
185
|
+
rubygems_version: 3.3.7
|
158
186
|
signing_key:
|
159
187
|
specification_version: 4
|
160
188
|
summary: ServerEngine - multiprocess server framework
|