serverengine 1.6.4 → 2.0.0pre1

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