serverengine 1.6.4 → 2.0.0pre1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/Changelog +11 -0
  4. data/README.md +31 -3
  5. data/Rakefile +16 -3
  6. data/appveyor.yml +11 -5
  7. data/examples/server.rb +138 -0
  8. data/examples/spawn_worker_script.rb +38 -0
  9. data/lib/serverengine/blocking_flag.rb +2 -3
  10. data/lib/serverengine/command_sender.rb +89 -0
  11. data/lib/serverengine/config_loader.rb +2 -0
  12. data/lib/serverengine/daemon.rb +114 -86
  13. data/lib/serverengine/daemon_logger.rb +3 -139
  14. data/lib/serverengine/embedded_server.rb +2 -0
  15. data/lib/serverengine/multi_process_server.rb +28 -7
  16. data/lib/serverengine/multi_spawn_server.rb +17 -18
  17. data/lib/serverengine/multi_thread_server.rb +6 -0
  18. data/lib/serverengine/multi_worker_server.rb +14 -0
  19. data/lib/serverengine/privilege.rb +57 -0
  20. data/lib/serverengine/process_manager.rb +66 -48
  21. data/lib/serverengine/server.rb +45 -11
  22. data/lib/serverengine/signal_thread.rb +0 -2
  23. data/lib/serverengine/signals.rb +31 -0
  24. data/lib/serverengine/socket_manager.rb +3 -5
  25. data/lib/serverengine/socket_manager_unix.rb +1 -0
  26. data/lib/serverengine/socket_manager_win.rb +4 -2
  27. data/lib/serverengine/supervisor.rb +105 -25
  28. data/lib/serverengine/utils.rb +23 -0
  29. data/lib/serverengine/version.rb +1 -1
  30. data/lib/serverengine/worker.rb +10 -7
  31. data/lib/serverengine.rb +9 -27
  32. data/serverengine.gemspec +12 -1
  33. data/spec/daemon_logger_spec.rb +17 -12
  34. data/spec/daemon_spec.rb +147 -24
  35. data/spec/multi_process_server_spec.rb +59 -7
  36. data/spec/server_worker_context.rb +104 -0
  37. data/spec/signal_thread_spec.rb +61 -56
  38. data/spec/supervisor_spec.rb +113 -99
  39. metadata +40 -6
@@ -15,9 +15,12 @@
15
15
  # See the License for the specific language governing permissions and
16
16
  # limitations under the License.
17
17
  #
18
- module ServerEngine
18
+ require 'serverengine/command_sender'
19
+ require 'serverengine/config_loader'
20
+ require 'serverengine/privilege'
21
+ require 'serverengine/supervisor'
19
22
 
20
- require 'shellwords'
23
+ module ServerEngine
21
24
 
22
25
  class Daemon
23
26
  include ConfigLoader
@@ -47,55 +50,19 @@ module ServerEngine
47
50
  @chuser = @config[:chuser]
48
51
  @chgroup = @config[:chgroup]
49
52
  @chumask = @config[:chumask]
50
- end
51
-
52
- # server is available when run() is called. It is a Supervisor instance if supervisor is set to true. Otherwise a Server instance.
53
- attr_reader :server
54
-
55
- module Signals
56
- GRACEFUL_STOP = :TERM
57
- IMMEDIATE_STOP = :QUIT
58
- GRACEFUL_RESTART = :USR1
59
- IMMEDIATE_RESTART = :HUP
60
- RELOAD = :USR2
61
- DETACH = :INT
62
- DUMP = :CONT
63
- end
64
53
 
65
- def self.get_etc_passwd(user)
66
- if user.to_i.to_s == user
67
- Etc.getpwuid(user.to_i)
54
+ @pid = nil
55
+ @command_pipe = @config.fetch(:command_pipe, nil)
56
+ @command_sender = @config.fetch(:command_sender, ServerEngine.windows? ? "pipe" : "signal")
57
+ if @command_sender == "pipe"
58
+ extend ServerEngine::CommandSender::Pipe
68
59
  else
69
- Etc.getpwnam(user)
60
+ extend ServerEngine::CommandSender::Signal
70
61
  end
71
62
  end
72
63
 
73
- def self.get_etc_group(group)
74
- if group.to_i.to_s == group
75
- Etc.getgrgid(group.to_i)
76
- else
77
- Etc.getgrnam(group)
78
- end
79
- end
80
-
81
- def self.change_privilege(user, group)
82
- if user
83
- etc_pw = Daemon.get_etc_passwd(user)
84
- user_groups = [etc_pw.gid]
85
- Etc.setgrent
86
- Etc.group { |gr| user_groups << gr.gid if gr.mem.include?(etc_pw.name) } # emulate 'id -G'
87
-
88
- Process.groups = Process.groups | user_groups
89
- Process::UID.change_privilege(etc_pw.uid)
90
- end
91
-
92
- if group
93
- etc_group = Daemon.get_etc_group(group)
94
- Process::GID.change_privilege(etc_group.gid)
95
- end
96
-
97
- nil
98
- end
64
+ # server is available when run() is called. It is a Supervisor instance if supervisor is set to true. Otherwise a Server instance.
65
+ attr_reader :server
99
66
 
100
67
  def run
101
68
  begin
@@ -113,7 +80,7 @@ module ServerEngine
113
80
  def server_main
114
81
  $0 = @daemon_process_name if @daemon_process_name
115
82
 
116
- Daemon.change_privilege(@chuser, @chgroup)
83
+ Privilege.change(@chuser, @chgroup)
117
84
  File.umask(@chumask) if @chumask
118
85
 
119
86
  s = create_server(create_logger)
@@ -128,72 +95,133 @@ module ServerEngine
128
95
  end
129
96
 
130
97
  def main
131
- unless @daemonize
98
+ if @daemonize
99
+ if @command_sender == "pipe"
100
+ inpipe, @command_sender_pipe = IO.pipe
101
+ @command_sender_pipe.sync = true
102
+ @command_sender_pipe.binmode
103
+ end
104
+
105
+ if ServerEngine.windows?
106
+ ret = daemonize_with_spawn(inpipe)
107
+ else
108
+ ret = daemonize_with_double_fork(inpipe)
109
+ end
110
+
111
+ if @command_sender == "pipe"
112
+ inpipe.close
113
+ end
114
+ return ret
115
+ else
116
+ @pid = Process.pid
132
117
  s = create_server(create_logger)
133
118
  s.install_signal_handlers
134
- s.main
119
+ begin
120
+ s.main
121
+ rescue SystemExit => e
122
+ return e.status
123
+ end
135
124
  return 0
136
125
  end
126
+ end
137
127
 
138
- rpipe = nil
139
- if ServerEngine.windows?
140
- windows_daemon_cmdline = config[:windows_daemon_cmdline]
141
- pid = Process.spawn(*Array(windows_daemon_cmdline))
142
- else
143
- rpipe, wpipe = IO.pipe
144
- wpipe.sync = true
128
+ def daemonize_with_spawn(inpipe)
129
+ windows_daemon_cmdline = config[:windows_daemon_cmdline]
130
+ config = {}
131
+ if @command_sender == "pipe"
132
+ config[:in] = inpipe
133
+ end
134
+ @pid = Process.spawn(*Array(windows_daemon_cmdline), config)
145
135
 
146
- Process.fork do
147
- begin
148
- rpipe.close
136
+ write_pid_file
137
+ end
149
138
 
150
- Process.setsid
151
- Process.fork do
152
- $0 = @daemon_process_name if @daemon_process_name
153
- wpipe.write "#{Process.pid}\n"
139
+ def daemonize_with_double_fork(inpipe)
140
+ rpipe, wpipe = IO.pipe
141
+ wpipe.sync = true
154
142
 
155
- Daemon.change_privilege(@chuser, @chgroup)
156
- File.umask(@chumask) if @chumask
143
+ Process.fork do
144
+ begin
145
+ rpipe.close
146
+ if @command_sender == "pipe"
147
+ @command_sender_pipe.close
148
+ end
149
+
150
+ Process.setsid
151
+ Process.fork do
152
+ $0 = @daemon_process_name if @daemon_process_name
153
+ wpipe.write "#{Process.pid}\n"
157
154
 
158
- s = create_server(create_logger)
155
+ Privilege.change(@chuser, @chgroup)
156
+ File.umask(@chumask) if @chumask
159
157
 
160
- STDIN.reopen(File::NULL)
161
- STDOUT.reopen(File::NULL, "wb")
162
- STDERR.reopen(File::NULL, "wb")
158
+ s = create_server(create_logger)
159
+ if @command_sender == "pipe"
160
+ s.instance_variable_set(:@command_pipe, inpipe)
161
+ end
163
162
 
164
- s.install_signal_handlers
163
+ STDIN.reopen(File::NULL)
164
+ STDOUT.reopen(File::NULL, "wb")
165
+ STDERR.reopen(File::NULL, "wb")
165
166
 
166
- wpipe.write "\n"
167
- wpipe.close
167
+ s.install_signal_handlers
168
168
 
169
+ wpipe.write "\n"
170
+ wpipe.close
171
+
172
+ begin
169
173
  s.main
174
+ rescue SystemExit => e
175
+ exit e.status
170
176
  end
171
-
172
- exit 0
173
- ensure
174
- exit! @daemonize_error_exit_code
175
177
  end
178
+
179
+ exit 0
180
+ ensure
181
+ exit! @daemonize_error_exit_code
176
182
  end
183
+ end
177
184
 
178
- wpipe.close
185
+ wpipe.close
186
+ @pid = rpipe.gets.to_i
187
+ data = rpipe.read
188
+ rpipe.close
179
189
 
180
- pid = rpipe.gets.to_i
190
+ if data != "\n"
191
+ return @daemonize_error_exit_code
181
192
  end
182
193
 
194
+ write_pid_file
195
+
196
+ return 0
197
+ end
198
+
199
+ def write_pid_file
183
200
  if @pid_path
184
201
  File.open(@pid_path, "w") {|f|
185
- f.write "#{pid}\n"
202
+ f.write "#{@pid}\n"
186
203
  }
187
204
  end
205
+ end
188
206
 
189
- unless ServerEngine.windows?
190
- data = rpipe.read
191
- if data != "\n"
192
- return @daemonize_error_exit_code
193
- end
194
- end
207
+ def stop(graceful)
208
+ _stop(graceful)
209
+ end
195
210
 
196
- return 0
211
+ def restart(graceful)
212
+ _restart(graceful)
213
+ end
214
+
215
+ def reload
216
+ _reload
217
+ end
218
+
219
+ def detach
220
+ _detach
221
+ end
222
+
223
+ def dump
224
+ _dump
197
225
  end
198
226
 
199
227
  private
@@ -15,9 +15,9 @@
15
15
  # See the License for the specific language governing permissions and
16
16
  # limitations under the License.
17
17
  #
18
- module ServerEngine
18
+ require 'logger'
19
19
 
20
- require 'logger'
20
+ module ServerEngine
21
21
 
22
22
  class ::Logger::LogDevice
23
23
  def reopen!
@@ -34,14 +34,6 @@ module ServerEngine
34
34
  @rotate_size = config[:log_rotate_size] || 1048576
35
35
  @file_dev = nil
36
36
 
37
- if RUBY_VERSION < "2.1.0"
38
- # Ruby < 2.1.0 has a problem around log rotation with multiprocess:
39
- # https://github.com/ruby/ruby/pull/428
40
- @logdev_class = MultiprocessFileLogDevice
41
- else
42
- @logdev_class = LogDevice
43
- end
44
-
45
37
  super(nil)
46
38
 
47
39
  self.level = config[:log_level] || 'debug'
@@ -62,7 +54,7 @@ module ServerEngine
62
54
  elsif !@file_dev || @file_dev.filename != logdev
63
55
  # update path string
64
56
  old_file_dev = @file_dev
65
- @file_dev = @logdev_class.new(logdev, shift_age: @rotate_age, shift_size: @rotate_size)
57
+ @file_dev = LogDevice.new(logdev, shift_age: @rotate_age, shift_size: @rotate_size)
66
58
  old_file_dev.close if old_file_dev
67
59
  @logdev = @file_dev
68
60
  end
@@ -138,134 +130,6 @@ module ServerEngine
138
130
  nil
139
131
  end
140
132
 
141
- class MultiprocessFileLogDevice
142
- def initialize(path, opts={})
143
- @shift_age = opts[:shift_age] || 7
144
- @shift_size = opts[:shift_size] || 1024*1024
145
- @mutex = Mutex.new
146
- self.path = path
147
- end
148
-
149
- def write(data)
150
- # it's hard to remove this synchronize because IO#write raises
151
- # Errno::ENOENT if IO#reopen is running concurrently.
152
- @mutex.synchronize do
153
- unless @file
154
- return nil
155
- end
156
- log_rotate_or_reopen
157
- @file.write(data)
158
- end
159
- rescue Exception => e
160
- warn "log writing failed: #{e}"
161
- end
162
-
163
- def path=(path)
164
- @mutex.synchronize do
165
- old_file = @file
166
- file = open_logfile(path)
167
- begin
168
- @file = file
169
- @path = path
170
- file = old_file
171
- ensure
172
- file.close if file
173
- end
174
- end
175
- return path
176
- end
177
-
178
- def close
179
- @mutex.synchronize do
180
- @file.close
181
- @file = nil
182
- end
183
- nil
184
- end
185
-
186
- def reopen!
187
- @mutex.synchronize do
188
- if @file
189
- @file.reopen(@path, 'a')
190
- @file.sync = true
191
- end
192
- end
193
- true
194
- end
195
-
196
- # for compatibility with Logger::LogDevice
197
- def dev
198
- @file
199
- end
200
-
201
- # for compatibility with Logger::LogDevice
202
- def filename
203
- @path
204
- end
205
-
206
- private
207
-
208
- def open_logfile(path)
209
- return nil unless path
210
- file = File.open(path, 'a')
211
- file.sync = true
212
- return file
213
- end
214
-
215
- def log_rotate_or_reopen
216
- stat = @file.stat
217
- if stat.size <= @shift_size
218
- return
219
- end
220
-
221
- # inter-process locking
222
- retry_limit = 8
223
- retry_sleep = 0.1
224
- begin
225
- # 1) other process is log-rotating now
226
- # 2) other process log rotated
227
- # 3) no active processes
228
- lock = File.open(@path, File::WRONLY | File::APPEND)
229
- begin
230
- lock.flock(File::LOCK_EX)
231
- ino = lock.stat.ino
232
- if ino == File.stat(@path).ino and ino == stat.ino
233
- # 3)
234
- log_rotate
235
- else
236
- @file.reopen(@path, 'a')
237
- @file.sync = true
238
- end
239
- ensure
240
- lock.close
241
- end
242
- rescue Errno::ENOENT => e
243
- raise e if retry_limit <= 0
244
- sleep retry_sleep
245
- retry_limit -= 1
246
- retry_sleep *= 2
247
- retry
248
- end
249
-
250
- rescue => e
251
- warn "log rotation inter-process lock failed: #{e}"
252
- end
253
-
254
- def log_rotate
255
- (@shift_age-2).downto(0) do |i|
256
- old_path = "#{@path}.#{i}"
257
- shift_path = "#{@path}.#{i+1}"
258
- if FileTest.exist?(old_path)
259
- File.rename(old_path, shift_path)
260
- end
261
- end
262
- File.rename(@path, "#{@path}.0")
263
- @file.reopen(@path, 'a')
264
- @file.sync = true
265
- rescue => e
266
- warn "log rotation failed: #{e}"
267
- end
268
- end
269
133
  end
270
134
 
271
135
  end
@@ -15,6 +15,8 @@
15
15
  # See the License for the specific language governing permissions and
16
16
  # limitations under the License.
17
17
  #
18
+ require 'serverengine/server'
19
+
18
20
  module ServerEngine
19
21
 
20
22
  class EmbeddedServer < Server
@@ -15,14 +15,20 @@
15
15
  # See the License for the specific language governing permissions and
16
16
  # limitations under the License.
17
17
  #
18
+ require 'serverengine/signals'
19
+ require 'serverengine/daemon'
20
+ require 'serverengine/process_manager'
21
+ require 'serverengine/multi_worker_server'
22
+ require 'serverengine/privilege'
23
+
18
24
  module ServerEngine
19
25
 
20
26
  class MultiProcessServer < MultiWorkerServer
21
27
  def initialize(worker_module, load_config_proc={}, &block)
22
28
  @pm = ProcessManager.new(
23
29
  auto_tick: false,
24
- graceful_kill_signal: Daemon::Signals::GRACEFUL_STOP,
25
- immediate_kill_signal: Daemon::Signals::IMMEDIATE_STOP,
30
+ graceful_kill_signal: Signals::GRACEFUL_STOP,
31
+ immediate_kill_signal: Signals::IMMEDIATE_STOP,
26
32
  enable_heartbeat: true,
27
33
  auto_heartbeat: true,
28
34
  on_heartbeat_error: Proc.new do
@@ -34,6 +40,7 @@ module ServerEngine
34
40
  super(worker_module, load_config_proc, &block)
35
41
 
36
42
  @worker_process_name = @config[:worker_process_name]
43
+ @unrecoverable_exit_codes = @config.fetch(:unrecoverable_exit_codes, [])
37
44
  end
38
45
 
39
46
  def run
@@ -70,7 +77,7 @@ module ServerEngine
70
77
  $0 = @worker_process_name % [wid] if @worker_process_name
71
78
  w.install_signal_handlers
72
79
 
73
- Daemon.change_privilege(@chuser, @chgroup)
80
+ Privilege.change(@chuser, @chgroup)
74
81
  File.umask(@chumask) if @chumask
75
82
 
76
83
  ## recreate the logger created at Server#main
@@ -83,7 +90,7 @@ module ServerEngine
83
90
  w.after_start
84
91
  end
85
92
 
86
- return WorkerMonitor.new(w, wid, pmon)
93
+ return WorkerMonitor.new(w, wid, pmon, unrecoverable_exit_codes: @unrecoverable_exit_codes)
87
94
  end
88
95
 
89
96
  def wait_tick
@@ -91,12 +98,18 @@ module ServerEngine
91
98
  end
92
99
 
93
100
  class WorkerMonitor
94
- def initialize(worker, wid, pmon)
101
+ def initialize(worker, wid, pmon, reload_signal = Signals::RELOAD, unrecoverable_exit_codes: [])
95
102
  @worker = worker
96
103
  @wid = wid
97
104
  @pmon = pmon
105
+ @reload_signal = reload_signal
106
+ @unrecoverable_exit_codes = unrecoverable_exit_codes
107
+ @unrecoverable_exit = false
108
+ @exitstatus = nil
98
109
  end
99
110
 
111
+ attr_reader :exitstatus
112
+
100
113
  def send_stop(stop_graceful)
101
114
  @stop = true
102
115
  if stop_graceful
@@ -108,7 +121,7 @@ module ServerEngine
108
121
  end
109
122
 
110
123
  def send_reload
111
- @pmon.send_signal(Daemon::Signals::RELOAD) if @pmon
124
+ @pmon.send_signal(@reload_signal) if @pmon
112
125
  nil
113
126
  end
114
127
 
@@ -121,13 +134,21 @@ module ServerEngine
121
134
  return false unless @pmon
122
135
 
123
136
  if stat = @pmon.try_join
124
- @worker.logger.info "Worker #{@wid} finished#{@stop ? '' : ' unexpectedly'} with #{ProcessManager.format_join_status(stat)}"
137
+ @worker.logger.info "Worker #{@wid} finished#{@stop ? '' : ' unexpectedly'} with #{ServerEngine.format_join_status(stat)}"
138
+ if stat.is_a?(Process::Status) && stat.exited? && @unrecoverable_exit_codes.include?(stat.exitstatus)
139
+ @unrecoverable_exit = true
140
+ @exitstatus = stat.exitstatus
141
+ end
125
142
  @pmon = nil
126
143
  return false
127
144
  else
128
145
  return true
129
146
  end
130
147
  end
148
+
149
+ def recoverable?
150
+ !@unrecoverable_exit
151
+ end
131
152
  end
132
153
  end
133
154
 
@@ -15,6 +15,10 @@
15
15
  # See the License for the specific language governing permissions and
16
16
  # limitations under the License.
17
17
  #
18
+ require 'serverengine/signals'
19
+ require 'serverengine/process_manager'
20
+ require 'serverengine/multi_worker_server'
21
+
18
22
  module ServerEngine
19
23
 
20
24
  class MultiSpawnServer < MultiWorkerServer
@@ -22,15 +26,15 @@ module ServerEngine
22
26
  if ServerEngine.windows?
23
27
  @pm = ProcessManager.new(
24
28
  auto_tick: false,
25
- graceful_kill_signal: Daemon::Signals::GRACEFUL_STOP,
29
+ graceful_kill_signal: Signals::GRACEFUL_STOP,
26
30
  immediate_kill_signal: false,
27
31
  enable_heartbeat: false,
28
32
  )
29
33
  else
30
34
  @pm = ProcessManager.new(
31
35
  auto_tick: false,
32
- graceful_kill_signal: Daemon::Signals::GRACEFUL_STOP,
33
- immediate_kill_signal: Daemon::Signals::IMMEDIATE_STOP,
36
+ graceful_kill_signal: Signals::GRACEFUL_STOP,
37
+ immediate_kill_signal: Signals::IMMEDIATE_STOP,
34
38
  enable_heartbeat: false,
35
39
  )
36
40
  end
@@ -38,6 +42,15 @@ module ServerEngine
38
42
  super(worker_module, load_config_proc, &block)
39
43
 
40
44
  @reload_signal = @config[:worker_reload_signal]
45
+ @unrecoverable_exit_codes = @config.fetch(:unrecoverable_exit_codes, [])
46
+ @pm.command_sender = @command_sender
47
+ end
48
+
49
+ def stop(stop_graceful)
50
+ if @command_sender == "pipe"
51
+ @pm.command_sender_pipe.write(stop_graceful ? "GRACEFUL_STOP\n" : "IMMEDIATE_STOP\n")
52
+ end
53
+ super
41
54
  end
42
55
 
43
56
  def run
@@ -71,26 +84,12 @@ module ServerEngine
71
84
  w.after_start
72
85
  end
73
86
 
74
- return WorkerMonitor.new(w, wid, pmon, @reload_signal)
87
+ return MultiProcessServer::WorkerMonitor.new(w, wid, pmon, @reload_signal, unrecoverable_exit_codes: @unrecoverable_exit_codes)
75
88
  end
76
89
 
77
90
  def wait_tick
78
91
  @pm.tick(0.5)
79
92
  end
80
-
81
- class WorkerMonitor < MultiProcessServer::WorkerMonitor
82
- def initialize(worker, wid, pmon, reload_signal)
83
- super(worker, wid, pmon)
84
- @reload_signal = reload_signal
85
- end
86
-
87
- def send_reload
88
- if @reload_signal
89
- @pmon.send_signal(@reload_signal) if @pmon
90
- end
91
- nil
92
- end
93
- end
94
93
  end
95
94
 
96
95
  end
@@ -15,6 +15,8 @@
15
15
  # See the License for the specific language governing permissions and
16
16
  # limitations under the License.
17
17
  #
18
+ require 'serverengine/multi_worker_server'
19
+
18
20
  module ServerEngine
19
21
 
20
22
  class MultiThreadServer < MultiWorkerServer
@@ -68,6 +70,10 @@ module ServerEngine
68
70
  def alive?
69
71
  @thread.alive?
70
72
  end
73
+
74
+ def recoverable?
75
+ true
76
+ end
71
77
  end
72
78
  end
73
79
 
@@ -15,6 +15,8 @@
15
15
  # See the License for the specific language governing permissions and
16
16
  # limitations under the License.
17
17
  #
18
+ require 'serverengine/server'
19
+
18
20
  module ServerEngine
19
21
 
20
22
  class MultiWorkerServer < Server
@@ -23,6 +25,8 @@ module ServerEngine
23
25
  @last_start_worker_time = 0
24
26
 
25
27
  super(worker_module, load_config_proc, &block)
28
+
29
+ @stop_immediately_at_unrecoverable_exit = @config.fetch(:stop_immediately_at_unrecoverable_exit, false)
26
30
  end
27
31
 
28
32
  def stop(stop_graceful)
@@ -99,6 +103,16 @@ module ServerEngine
99
103
  # alive
100
104
  num_alive += 1
101
105
 
106
+ elsif m && m.respond_to?(:recoverable?) && !m.recoverable?
107
+ # exited, with unrecoverable exit code
108
+ if @stop_immediately_at_unrecoverable_exit
109
+ stop(true) # graceful stop for workers
110
+ # @stop is set by Server#stop
111
+ end
112
+ # server will stop when all workers exited in this state
113
+ # the last status will be used for server/supervisor/daemon
114
+ @stop_status = m.exitstatus if m.exitstatus
115
+
102
116
  elsif wid < @num_workers
103
117
  # scale up or reboot
104
118
  unless @stop