serverengine 2.2.5 → 2.3.1

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: 2175217415cd5751a1bd4a8d60c5abdb64e80429e722c41e8c1e97ec02b1e22a
4
- data.tar.gz: 63a7af53a0dc906bb962394f902d1486f7958950c9b3b9bf41206e56789d83ad
3
+ metadata.gz: 8b802588a6e93b69c2dc9810ab33b9e3195e14d93c5f977961a04cc6d88cacf1
4
+ data.tar.gz: 9921383ec5ae39cfe982da9ce8c0b432bbc553c2ea5d2090a2d849ec2500c230
5
5
  SHA512:
6
- metadata.gz: 85880cc1ec9b83e03b2cbe80756253e156e46954117ae0a008388bfccf3d412b96a54911f9f002b6387ec2dfa9ef35dd3a40bff1dff22f7b5ca180f667d88611
7
- data.tar.gz: 624a1e6397faa39af9b871ef91571b432263d6b2b595fc4bdfaa9244a6153b25a67fe80c38c6f814c94db27b0dddcc1e4e19ae08515c8e48c2ee6208478775d0
6
+ metadata.gz: ea4c0e4b34490665e4287c0f43e4ea1f32c10fe022464a69326505ec21aad2650e53b842c2aaeec2a969d914f73715ee059b2bdbb3d72d14b0bce51192bf217c
7
+ data.tar.gz: 3e30810b08b8667f2295b41fca60c4683126d8936e32169d0789b8ec02ba3965fc631dbe069c9a8cf057e109ccaa9ad3ad14f4507b2388dcd9ae27463ce39651
@@ -11,7 +11,7 @@ jobs:
11
11
  strategy:
12
12
  fail-fast: false
13
13
  matrix:
14
- ruby: [ '3.1', '3.0', '2.7', '2.6' ]
14
+ ruby: [ '3.1', '3.0', '2.7' ]
15
15
  os:
16
16
  - ubuntu-latest
17
17
  name: Unit testing with Ruby ${{ matrix.ruby }} on ${{ matrix.os }}
@@ -11,7 +11,7 @@ jobs:
11
11
  strategy:
12
12
  fail-fast: false
13
13
  matrix:
14
- ruby: [ '3.1', '2.7', '2.6' ]
14
+ ruby: [ '3.1', '2.7' ]
15
15
  os:
16
16
  - windows-latest
17
17
  include:
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 wait time before starting a new worker (default: 0) [dynamic reloadable]
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
- @worker.logger.error "Worker #{@wid} finished unexpectedly with #{ServerEngine.format_join_status(stat)}"
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
@@ -39,8 +39,11 @@ module ServerEngine
39
39
  def initialize(worker, thread)
40
40
  @worker = worker
41
41
  @thread = thread
42
+ @restart_at = nil
42
43
  end
43
44
 
45
+ attr_accessor :restart_at
46
+
44
47
  def send_stop(stop_graceful)
45
48
  Thread.new do
46
49
  begin
@@ -55,8 +55,8 @@ module ServerEngine
55
55
 
56
56
  def run
57
57
  while true
58
- num_alive = keepalive_workers
59
- break if num_alive == 0
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
- num_alive = 0
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
- num_alive += 1
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
- @monitors[wid] = delayed_start_worker(wid)
120
- num_alive += 1
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 num_alive
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
- now = Time.now.to_f
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
- @command_sender_pipe.write(command) if @command_sender_pipe
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
@@ -80,7 +80,7 @@ module ServerEngine
80
80
  if @command_pipe
81
81
  Thread.new do
82
82
  until @command_pipe.closed?
83
- case @command_pipe.gets.chomp
83
+ case @command_pipe.gets&.chomp
84
84
  when "GRACEFUL_STOP"
85
85
  s.stop(true)
86
86
  when "IMMEDIATE_STOP"
@@ -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
@@ -1,3 +1,3 @@
1
1
  module ServerEngine
2
- VERSION = "2.2.5"
2
+ VERSION = "2.3.1"
3
3
  end
@@ -99,7 +99,6 @@ module ServerEngine
99
99
  extend Fiddle::Importer
100
100
 
101
101
  dlload "kernel32"
102
- extern "int GetModuleFileNameA(int, char *, int)"
103
102
  extern "int CloseHandle(int)"
104
103
 
105
104
  dlload RbConfig::CONFIG['LIBRUBY_SO']
data/lib/serverengine.rb CHANGED
@@ -35,12 +35,27 @@ module ServerEngine
35
35
 
36
36
  def self.ruby_bin_path
37
37
  if ServerEngine.windows?
38
- require 'windows/library'
39
- ruby_path = "\0" * 256
40
- Windows::Library::GetModuleFileName.call(0, ruby_path, 256)
41
- return ruby_path.rstrip.gsub(/\\/, '/')
38
+ ServerEngine::Win32.ruby_bin_path
42
39
  else
43
- return File.join(RbConfig::CONFIG["bindir"], RbConfig::CONFIG["RUBY_INSTALL_NAME"]) + RbConfig::CONFIG["EXEEXT"]
40
+ File.join(RbConfig::CONFIG["bindir"], RbConfig::CONFIG["RUBY_INSTALL_NAME"]) + RbConfig::CONFIG["EXEEXT"]
41
+ end
42
+ end
43
+
44
+ if ServerEngine.windows?
45
+ module Win32
46
+ require 'fiddle/import'
47
+
48
+ extend Fiddle::Importer
49
+
50
+ dlload "kernel32"
51
+ extern "int GetModuleFileNameW(int, void *, int)"
52
+
53
+ def self.ruby_bin_path
54
+ ruby_bin_path_buf = Fiddle::Pointer.malloc(1024)
55
+ len = GetModuleFileNameW(0, ruby_bin_path_buf, ruby_bin_path_buf.size / 2)
56
+ path_bytes = ruby_bin_path_buf[0, len * 2]
57
+ path_bytes.encode('UTF-8', 'UTF-16LE').gsub(/\\/, '/')
58
+ end
44
59
  end
45
60
  end
46
61
  end
data/serverengine.gemspec CHANGED
@@ -27,11 +27,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
- # build gem for a certain platform. see also Rakefile
31
- fake_platform = ENV['GEM_BUILD_FAKE_PLATFORM'].to_s
32
- gem.platform = fake_platform unless fake_platform.empty?
33
- if /mswin|mingw/ =~ fake_platform || (/mswin|mingw/ =~ RUBY_PLATFORM && fake_platform.empty?)
34
- # windows dependencies
35
- gem.add_runtime_dependency("windows-pr", ["~> 1.2.5"])
36
- end
30
+ gem.add_development_dependency "timecop", ["~> 0.9.5"]
31
+ gem.add_development_dependency "rr", ["~> 3.1"]
37
32
  end
@@ -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 = {workers: 2, log_stdout: false, log_stderr: false}
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 = {workers: 2, log_stdout: false, log_stderr: false}
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 = {workers: 4, log_stdout: false, log_stderr: false, unrecoverable_exit_codes: [3, 4, 5]}
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 = {workers: 4, log_stdout: false, log_stderr: false, unrecoverable_exit_codes: [3, 4, 5], stop_immediately_at_unrecoverable_exit: true}
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
- context 'with command_sender=pipe' do
7
- it 'starts worker processes' do
8
- config = {workers: 2, command_sender: 'pipe', log_stdout: false, log_stderr: false}
7
+ 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
- s = ServerEngine::MultiSpawnServer.new(TestWorker) { config.dup }
11
- t = Thread.new { s.main }
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
- begin
14
- wait_for_fork
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
- Timeout.timeout(5) do
17
- sleep(0.5) until test_state(:worker_run) == 2
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
- 5.times do
169
- # repeats 5 times because signal handlers
169
+ # This means this worker will automatically finish after 50 seconds.
170
+ 10.times do
171
+ # repeats multiple times because signal handlers
170
172
  # interrupts wait
171
173
  @stop_flag.wait(5.0)
172
174
  end
@@ -252,16 +254,25 @@ end
252
254
 
253
255
  shared_context 'test server and worker' do
254
256
  before { reset_test_state }
257
+ after { Timecop.return }
258
+
259
+ unless self.const_defined?(:WAIT_RATIO)
260
+ if ServerEngine.windows?
261
+ WAIT_RATIO = 2
262
+ else
263
+ WAIT_RATIO = 1
264
+ end
265
+ end
255
266
 
256
267
  def wait_for_fork
257
- sleep 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
@@ -1,4 +1,9 @@
1
1
  require 'bundler'
2
+ require 'rspec'
3
+
4
+ RSpec.configure do |config|
5
+ config.color_enabled = true
6
+ end
2
7
 
3
8
  begin
4
9
  Bundler.setup(:default, :test)
@@ -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: STDOUT)
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(STDOUT)
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(STDOUT)
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
- expect(WinSock.last_error).to be Windows::Error::WSAENOTSOCK
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.2.5
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-01-13 00:00:00.000000000 Z
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.2.5
185
+ rubygems_version: 3.3.7
158
186
  signing_key:
159
187
  specification_version: 4
160
188
  summary: ServerEngine - multiprocess server framework