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 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