einhorn 0.7.3 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/Changes.md +10 -0
  3. data/{LICENSE → LICENSE.txt} +0 -0
  4. data/README.md +36 -30
  5. data/bin/einhorn +17 -2
  6. data/einhorn.gemspec +23 -21
  7. data/example/pool_worker.rb +1 -1
  8. data/example/thin_example +8 -8
  9. data/example/time_server +5 -5
  10. data/lib/einhorn/client.rb +8 -9
  11. data/lib/einhorn/command/interface.rb +100 -95
  12. data/lib/einhorn/command.rb +170 -88
  13. data/lib/einhorn/compat.rb +7 -7
  14. data/lib/einhorn/event/abstract_text_descriptor.rb +31 -35
  15. data/lib/einhorn/event/ack_timer.rb +2 -2
  16. data/lib/einhorn/event/command_server.rb +7 -9
  17. data/lib/einhorn/event/connection.rb +1 -3
  18. data/lib/einhorn/event/loop_breaker.rb +2 -1
  19. data/lib/einhorn/event/persistent.rb +2 -2
  20. data/lib/einhorn/event/timer.rb +4 -4
  21. data/lib/einhorn/event.rb +29 -20
  22. data/lib/einhorn/prctl.rb +26 -0
  23. data/lib/einhorn/prctl_linux.rb +48 -0
  24. data/lib/einhorn/safe_yaml.rb +17 -0
  25. data/lib/einhorn/version.rb +1 -1
  26. data/lib/einhorn/worker.rb +67 -49
  27. data/lib/einhorn/worker_pool.rb +9 -9
  28. data/lib/einhorn.rb +164 -155
  29. metadata +42 -137
  30. data/.gitignore +0 -17
  31. data/.travis.yml +0 -10
  32. data/CONTRIBUTORS +0 -6
  33. data/Gemfile +0 -11
  34. data/History.txt +0 -4
  35. data/README.md.in +0 -76
  36. data/Rakefile +0 -27
  37. data/test/_lib.rb +0 -12
  38. data/test/integration/_lib/fixtures/env_printer/env_printer.rb +0 -26
  39. data/test/integration/_lib/fixtures/exit_during_upgrade/exiting_server.rb +0 -22
  40. data/test/integration/_lib/fixtures/exit_during_upgrade/upgrade_reexec.rb +0 -6
  41. data/test/integration/_lib/fixtures/upgrade_project/upgrading_server.rb +0 -22
  42. data/test/integration/_lib/helpers/einhorn_helpers.rb +0 -143
  43. data/test/integration/_lib/helpers.rb +0 -4
  44. data/test/integration/_lib.rb +0 -6
  45. data/test/integration/startup.rb +0 -31
  46. data/test/integration/upgrading.rb +0 -157
  47. data/test/unit/einhorn/client.rb +0 -88
  48. data/test/unit/einhorn/command/interface.rb +0 -49
  49. data/test/unit/einhorn/command.rb +0 -21
  50. data/test/unit/einhorn/event.rb +0 -89
  51. data/test/unit/einhorn/worker_pool.rb +0 -39
  52. data/test/unit/einhorn.rb +0 -58
@@ -1,28 +1,25 @@
1
- require 'pp'
2
- require 'set'
3
- require 'tmpdir'
1
+ require "pp"
2
+ require "set"
3
+ require "tmpdir"
4
4
 
5
- require 'einhorn/command/interface'
5
+ require "einhorn/command/interface"
6
+ require "einhorn/prctl"
6
7
 
7
8
  module Einhorn
8
9
  module Command
9
10
  def self.reap
10
- begin
11
- while true
12
- Einhorn.log_debug('Going to reap a child process')
13
-
14
- pid = Process.wait(-1, Process::WNOHANG)
15
- return unless pid
16
- mourn(pid)
17
- Einhorn::Event.break_loop
18
- end
19
- rescue Errno::ECHILD
20
- end
11
+ loop do
12
+ Einhorn.log_debug("Going to reap a child process")
13
+ pid = Process.wait(-1, Process::WNOHANG)
14
+ return unless pid
15
+ cleanup(pid)
16
+ Einhorn::Event.break_loop
17
+ end
18
+ rescue Errno::ECHILD
21
19
  end
22
20
 
23
- # Mourn the death of your child
24
- def self.mourn(pid)
25
- unless spec = Einhorn::State.children[pid]
21
+ def self.cleanup(pid)
22
+ unless (spec = Einhorn::State.children[pid])
26
23
  Einhorn.log_error("Could not find any config for exited child #{pid.inspect}! This probably indicates a bug in Einhorn.")
27
24
  return
28
25
  end
@@ -32,7 +29,7 @@ module Einhorn
32
29
  # Unacked worker
33
30
  if spec[:type] == :worker && !spec[:acked]
34
31
  Einhorn::State.consecutive_deaths_before_ack += 1
35
- extra = ' before it was ACKed'
32
+ extra = " before it was ACKed"
36
33
  else
37
34
  extra = nil
38
35
  end
@@ -40,11 +37,23 @@ module Einhorn
40
37
  case type = spec[:type]
41
38
  when :worker
42
39
  Einhorn.log_info("===> Exited worker #{pid.inspect}#{extra}", :upgrade)
40
+ when :state_passer
41
+ Einhorn.log_debug("===> Exited state passing process #{pid.inspect}", :upgrade)
43
42
  else
44
43
  Einhorn.log_error("===> Exited process #{pid.inspect} has unrecgonized type #{type.inspect}: #{spec.inspect}", :upgrade)
45
44
  end
46
45
  end
47
46
 
47
+ def self.register_ping(pid, request_id)
48
+ unless (spec = Einhorn::State.children[pid])
49
+ Einhorn.log_error("Could not find state for PID #{pid.inspect}; ignoring ACK.")
50
+ return
51
+ end
52
+
53
+ spec[:pinged_at] = Time.now
54
+ spec[:pinged_request_id] = request_id
55
+ end
56
+
48
57
  def self.register_manual_ack(pid)
49
58
  ack_mode = Einhorn::State.ack_mode
50
59
  unless ack_mode[:type] == :manual
@@ -73,7 +82,7 @@ module Einhorn
73
82
  end
74
83
 
75
84
  def self.register_ack(pid)
76
- unless spec = Einhorn::State.children[pid]
85
+ unless (spec = Einhorn::State.children[pid])
77
86
  Einhorn.log_error("Could not find state for PID #{pid.inspect}; ignoring ACK.")
78
87
  return
79
88
  end
@@ -83,10 +92,8 @@ module Einhorn
83
92
  return
84
93
  end
85
94
 
86
- if Einhorn::State.consecutive_deaths_before_ack > 0
87
- extra = ", breaking the streak of #{Einhorn::State.consecutive_deaths_before_ack} consecutive unacked workers dying"
88
- else
89
- extra = nil
95
+ extra = if Einhorn::State.consecutive_deaths_before_ack > 0
96
+ ", breaking the streak of #{Einhorn::State.consecutive_deaths_before_ack} consecutive unacked workers dying"
90
97
  end
91
98
  Einhorn::State.consecutive_deaths_before_ack = 0
92
99
 
@@ -96,14 +103,14 @@ module Einhorn
96
103
  Einhorn::Event.break_loop
97
104
  end
98
105
 
99
- def self.signal_all(signal, children=nil, record=true)
106
+ def self.signal_all(signal, children = nil, record = true)
100
107
  children ||= Einhorn::WorkerPool.workers
101
-
102
108
  signaled = {}
109
+
103
110
  Einhorn.log_info("Sending #{signal} to #{children.inspect}", :upgrade)
104
111
 
105
112
  children.each do |child|
106
- unless spec = Einhorn::State.children[child]
113
+ unless (spec = Einhorn::State.children[child])
107
114
  Einhorn.log_error("Trying to send #{signal} to dead child #{child.inspect}. The fact we tried this probably indicates a bug in Einhorn.", :upgrade)
108
115
  next
109
116
  end
@@ -113,11 +120,13 @@ module Einhorn
113
120
  Einhorn.log_error("Re-sending #{signal} to already-signaled child #{child.inspect}. It may be slow to spin down, or it may be swallowing #{signal}s.", :upgrade)
114
121
  end
115
122
  spec[:signaled].add(signal)
123
+ spec[:last_signaled_at] = Time.now
116
124
  end
117
125
 
118
126
  begin
119
127
  Process.kill(signal, child)
120
128
  rescue Errno::ESRCH
129
+ Einhorn.log_debug("Attempted to #{signal} child #{child.inspect} but the process does not exist", :upgrade)
121
130
  else
122
131
  signaled[child] = spec
123
132
  end
@@ -127,7 +136,7 @@ module Einhorn
127
136
  Einhorn::Event::Timer.open(Einhorn::State.signal_timeout) do
128
137
  children.each do |child|
129
138
  spec = Einhorn::State.children[child]
130
- next unless spec # Process is already dead and removed by mourn
139
+ next unless spec # Process is already dead and removed by cleanup
131
140
  signaled_spec = signaled[child]
132
141
  next unless signaled_spec # We got ESRCH when trying to signal
133
142
  if spec[:spinup_time] != signaled_spec[:spinup_time]
@@ -137,15 +146,15 @@ module Einhorn
137
146
 
138
147
  Einhorn.log_info("Child #{child.inspect} is still active after #{Einhorn::State.signal_timeout}s. Sending SIGKILL.")
139
148
  begin
140
- Process.kill('KILL', child)
149
+ Process.kill("KILL", child)
141
150
  rescue Errno::ESRCH
142
151
  end
143
- spec[:signaled].add('KILL')
152
+ spec[:signaled].add("KILL")
144
153
  end
145
154
  end
146
- end
147
155
 
148
- "Successfully sent #{signal}s to #{signaled.length} processes: #{signaled.keys}"
156
+ Einhorn.log_info("Successfully sent #{signal}s to #{signaled.length} processes: #{signaled.keys}")
157
+ end
149
158
  end
150
159
 
151
160
  def self.increment
@@ -153,14 +162,14 @@ module Einhorn
153
162
  old = Einhorn::State.config[:number]
154
163
  new = (Einhorn::State.config[:number] += 1)
155
164
  output = "Incrementing number of workers from #{old} -> #{new}"
156
- $stderr.puts(output)
165
+ warn(output)
157
166
  output
158
167
  end
159
168
 
160
169
  def self.decrement
161
170
  if Einhorn::State.config[:number] <= 1
162
171
  output = "Can't decrease number of workers (already at #{Einhorn::State.config[:number]}). Run kill #{$$} if you really want to kill einhorn."
163
- $stderr.puts(output)
172
+ warn(output)
164
173
  return output
165
174
  end
166
175
 
@@ -168,7 +177,7 @@ module Einhorn
168
177
  old = Einhorn::State.config[:number]
169
178
  new = (Einhorn::State.config[:number] -= 1)
170
179
  output = "Decrementing number of workers from #{old} -> #{new}"
171
- $stderr.puts(output)
180
+ warn(output)
172
181
  output
173
182
  end
174
183
 
@@ -181,12 +190,12 @@ module Einhorn
181
190
  old = Einhorn::State.config[:number]
182
191
  Einhorn::State.config[:number] = new
183
192
  output = "Altering worker count, #{old} -> #{new}. Will "
184
- if old < new
185
- output << "spin up additional workers."
193
+ output << if old < new
194
+ "spin up additional workers."
186
195
  else
187
- output << "gracefully terminate workers."
196
+ "gracefully terminate workers."
188
197
  end
189
- $stderr.puts(output)
198
+ warn(output)
190
199
  output
191
200
  end
192
201
 
@@ -197,8 +206,8 @@ module Einhorn
197
206
  end
198
207
 
199
208
  {
200
- :state => global_state,
201
- :persistent_descriptors => descriptor_state,
209
+ state: global_state,
210
+ persistent_descriptors: descriptor_state
202
211
  }
203
212
  end
204
213
 
@@ -218,6 +227,7 @@ module Einhorn
218
227
 
219
228
  fork do
220
229
  Einhorn::TransientState.whatami = :state_passer
230
+ Einhorn::State.children[Process.pid] = {type: :state_passer}
221
231
  Einhorn::State.generation += 1
222
232
  read.close
223
233
 
@@ -242,8 +252,8 @@ module Einhorn
242
252
 
243
253
  begin
244
254
  Einhorn.initialize_reload_environment
245
- respawn_commandline = Einhorn.upgrade_commandline(['--with-state-fd', read.fileno.to_s])
246
- respawn_commandline << { :close_others => false }
255
+ respawn_commandline = Einhorn.upgrade_commandline(["--with-state-fd", read.fileno.to_s])
256
+ respawn_commandline << {close_others: false}
247
257
  Einhorn.log_info("About to re-exec einhorn master as #{respawn_commandline.inspect}", :reload)
248
258
  Einhorn::Compat.exec(*respawn_commandline)
249
259
  rescue SystemCallError => e
@@ -260,30 +270,34 @@ module Einhorn
260
270
  end
261
271
  end
262
272
 
263
- def self.spinup(cmd=nil)
273
+ def self.spinup(cmd = nil)
264
274
  cmd ||= Einhorn::State.cmd
265
275
  index = next_index
266
- if Einhorn::TransientState.preloaded
267
- pid = fork do
276
+ expected_ppid = Process.pid
277
+ pid = if Einhorn::State.preloaded
278
+ fork do
268
279
  Einhorn::TransientState.whatami = :worker
269
280
  prepare_child_process
270
281
 
271
- Einhorn.log_info('About to tear down Einhorn state and run einhorn_main')
282
+ Einhorn.log_info("About to tear down Einhorn state and run einhorn_main")
272
283
  Einhorn::Command::Interface.uninit
273
284
  Einhorn::Event.close_all_for_worker
274
285
  Einhorn.set_argv(cmd, true)
275
286
 
276
287
  reseed_random
277
288
 
289
+ setup_parent_watch(expected_ppid)
290
+
278
291
  prepare_child_environment(index)
279
292
  einhorn_main
280
293
  end
281
294
  else
282
- pid = fork do
295
+ fork do
283
296
  Einhorn::TransientState.whatami = :worker
284
297
  prepare_child_process
285
298
 
286
299
  Einhorn.log_info("About to exec #{cmd.inspect}")
300
+ Einhorn::Command::Interface.uninit
287
301
  # Here's the only case where cloexec would help. Since we
288
302
  # have to track and manually close FDs for other cases, we
289
303
  # may as well just reuse close_all rather than also set
@@ -292,20 +306,23 @@ module Einhorn
292
306
  # Note that Ruby 1.9's close_others option is useful here.
293
307
  Einhorn::Event.close_all_for_worker
294
308
 
309
+ setup_parent_watch(expected_ppid)
310
+
295
311
  prepare_child_environment(index)
296
- Einhorn::Compat.exec(cmd[0], cmd[1..-1], :close_others => false)
312
+ Einhorn::Compat.exec(cmd[0], cmd[1..-1], close_others: false)
297
313
  end
298
314
  end
299
315
 
300
316
  Einhorn.log_info("===> Launched #{pid} (index: #{index})", :upgrade)
301
317
  Einhorn::State.last_spinup = Time.now
302
318
  Einhorn::State.children[pid] = {
303
- :type => :worker,
304
- :version => Einhorn::State.version,
305
- :acked => false,
306
- :signaled => Set.new,
307
- :index => index,
308
- :spinup_time => Einhorn::State.last_spinup,
319
+ type: :worker,
320
+ version: Einhorn::State.version,
321
+ acked: false,
322
+ signaled: Set.new,
323
+ last_signaled_at: nil,
324
+ index: index,
325
+ spinup_time: Einhorn::State.last_spinup
309
326
  }
310
327
 
311
328
  # Set up whatever's needed for ACKing
@@ -314,6 +331,7 @@ module Einhorn
314
331
  when :timer
315
332
  Einhorn::Event::ACKTimer.open(ack_mode[:timeout], pid)
316
333
  when :manual
334
+ # nothing to do
317
335
  else
318
336
  Einhorn.log_error("Unrecognized ACK mode #{type.inspect}")
319
337
  end
@@ -321,24 +339,18 @@ module Einhorn
321
339
 
322
340
  def self.prepare_child_environment(index)
323
341
  # This is run from the child
324
- ENV['EINHORN_MASTER_PID'] = Process.ppid.to_s
325
- ENV['EINHORN_SOCK_PATH'] = Einhorn::Command::Interface.socket_path
342
+ ENV["EINHORN_MASTER_PID"] = Process.ppid.to_s
343
+ ENV["EINHORN_SOCK_PATH"] = Einhorn::Command::Interface.socket_path
326
344
  if Einhorn::State.command_socket_as_fd
327
345
  socket = UNIXSocket.open(Einhorn::Command::Interface.socket_path)
328
346
  Einhorn::TransientState.socket_handles << socket
329
- ENV['EINHORN_SOCK_FD'] = socket.fileno.to_s
347
+ ENV["EINHORN_SOCK_FD"] = socket.fileno.to_s
330
348
  end
331
349
 
332
- ENV['EINHORN_FD_COUNT'] = Einhorn::State.bind_fds.length.to_s
333
- Einhorn::State.bind_fds.each_with_index {|fd, i| ENV["EINHORN_FD_#{i}"] = fd.to_s}
334
-
335
- ENV['EINHORN_CHILD_INDEX'] = index.to_s
350
+ ENV["EINHORN_FD_COUNT"] = Einhorn::State.bind_fds.length.to_s
351
+ Einhorn::State.bind_fds.each_with_index { |fd, i| ENV["EINHORN_FD_#{i}"] = fd.to_s }
336
352
 
337
- # EINHORN_FDS is deprecated. It was originally an attempt to
338
- # match Upstart's nominal internal support for space-separated
339
- # FD lists, but nobody uses that in practice, and it makes
340
- # finding individual FDs more difficult
341
- ENV['EINHORN_FDS'] = Einhorn::State.bind_fds.map(&:to_s).join(' ')
353
+ ENV["EINHORN_CHILD_INDEX"] = index.to_s
342
354
  end
343
355
 
344
356
  # Reseed common ruby random number generators.
@@ -361,11 +373,11 @@ module Einhorn
361
373
 
362
374
  # reseed OpenSSL::Random if it's loaded
363
375
  if defined?(OpenSSL::Random)
364
- if defined?(Random)
365
- seed = Random.new_seed
376
+ seed = if defined?(Random)
377
+ Random.new_seed
366
378
  else
367
379
  # Ruby 1.8
368
- seed = rand
380
+ rand
369
381
  end
370
382
  OpenSSL::Random.seed(seed.to_s)
371
383
  end
@@ -376,6 +388,24 @@ module Einhorn
376
388
  Einhorn.renice_self
377
389
  end
378
390
 
391
+ def self.setup_parent_watch(expected_ppid)
392
+ if Einhorn::State.kill_children_on_exit
393
+ begin
394
+ # NB: Having the USR2 signal handler set to terminate (the default) at
395
+ # this point is required. If it's set to a ruby handler, there are
396
+ # race conditions that could cause the worker to leak.
397
+
398
+ Einhorn::Prctl.set_pdeathsig("USR2")
399
+ if Process.ppid != expected_ppid
400
+ Einhorn.log_error("Parent process died before we set pdeathsig; cowardly refusing to exec child process.")
401
+ exit(1)
402
+ end
403
+ rescue NotImplementedError
404
+ # Unsupported OS; silently continue.
405
+ end
406
+ end
407
+ end
408
+
379
409
  # @param options [Hash]
380
410
  #
381
411
  # @option options [Boolean] :smooth (false) Whether to perform a smooth or
@@ -384,18 +414,19 @@ module Einhorn
384
414
  # upgrade, bring up all the new workers and don't cull any old workers
385
415
  # until they're all up.
386
416
  #
387
- def self.full_upgrade(options={})
388
- options = {:smooth => false}.merge(options)
417
+ def self.full_upgrade(options = {})
418
+ options = {smooth: false}.merge(options)
389
419
 
390
420
  Einhorn::State.smooth_upgrade = options.fetch(:smooth)
391
421
  reload_for_upgrade
392
422
  end
393
423
 
394
424
  def self.full_upgrade_smooth
395
- full_upgrade(:smooth => true)
425
+ full_upgrade(smooth: true)
396
426
  end
427
+
397
428
  def self.full_upgrade_fleet
398
- full_upgrade(:smooth => false)
429
+ full_upgrade(smooth: false)
399
430
  end
400
431
 
401
432
  def self.reload_for_upgrade
@@ -408,8 +439,8 @@ module Einhorn
408
439
  Einhorn.log_info("Currently upgrading (#{Einhorn::WorkerPool.ack_count} / #{Einhorn::WorkerPool.ack_target} ACKs; bumping version and starting over)...", :upgrade)
409
440
  else
410
441
  Einhorn::State.upgrading = true
411
- u_type = Einhorn::State.smooth_upgrade ? 'smooth' : 'fleet'
412
- Einhorn.log_info("Starting #{u_type} upgrade from version" +
442
+ u_type = Einhorn::State.smooth_upgrade ? "smooth" : "fleet"
443
+ Einhorn.log_info("Starting #{u_type} upgrade from version" \
413
444
  " #{Einhorn::State.version}...", :upgrade)
414
445
  end
415
446
 
@@ -456,10 +487,45 @@ module Einhorn
456
487
  end
457
488
 
458
489
  if unsignaled > target
459
- excess = Einhorn::WorkerPool.unsignaled_modern_workers_with_priority[0...(unsignaled-target)]
490
+ excess = Einhorn::WorkerPool.unsignaled_modern_workers_with_priority[0...(unsignaled - target)]
460
491
  Einhorn.log_info("Have too many workers at the current version, so killing off #{excess.length} of them.")
461
492
  signal_all("USR2", excess)
462
493
  end
494
+
495
+ # Ensure all signaled workers that have outlived signal_timeout get killed.
496
+ kill_expired_signaled_workers if Einhorn::State.signal_timeout
497
+ end
498
+
499
+ def self.kill_expired_signaled_workers
500
+ now = Time.now
501
+ children = Einhorn::State.children.select do |_, c|
502
+ # Only interested in USR2 signaled workers
503
+ next unless c[:signaled] && c[:signaled].length > 0
504
+ next unless c[:signaled].include?("USR2")
505
+
506
+ # Ignore processes that have received KILL since it can't be trapped.
507
+ next if c[:signaled].include?("KILL")
508
+
509
+ # Filter out those children that have not reached signal_timeout yet.
510
+ next unless c[:last_signaled_at]
511
+ expires_at = c[:last_signaled_at] + Einhorn::State.signal_timeout
512
+ next unless now >= expires_at
513
+
514
+ true
515
+ end
516
+
517
+ Einhorn.log_info("#{children.size} expired signaled workers found.") if children.size > 0
518
+ children.each do |pid, child|
519
+ Einhorn.log_info("Child #{pid.inspect} was signaled #{(child[:last_signaled_at] - now).abs.to_i}s ago. Sending SIGKILL as it is still active after #{Einhorn::State.signal_timeout}s timeout.", :upgrade)
520
+ begin
521
+ Process.kill("KILL", pid)
522
+ rescue Errno::ESRCH
523
+ Einhorn.log_debug("Attempted to SIGKILL child #{pid.inspect} but the process does not exist.")
524
+ end
525
+
526
+ child[:signaled].add("KILL")
527
+ child[:last_signaled_at] = Time.now
528
+ end
463
529
  end
464
530
 
465
531
  def self.stop_respawning
@@ -484,7 +550,7 @@ module Einhorn
484
550
  return
485
551
  end
486
552
  Einhorn.log_info("Launching #{missing} new workers")
487
- missing.times {spinup}
553
+ missing.times { spinup }
488
554
  end
489
555
 
490
556
  # Unbounded exponential backoff is not a thing: we run into problems if
@@ -493,10 +559,12 @@ module Einhorn
493
559
  # don't wait until the heat death of the universe to spin up new capacity.
494
560
  MAX_SPINUP_INTERVAL = 30.0
495
561
 
496
- def self.replenish_gradually(max_unacked=nil)
562
+ def self.replenish_gradually(max_unacked = nil)
497
563
  return if Einhorn::TransientState.has_outstanding_spinup_timer
498
564
  return unless Einhorn::WorkerPool.missing_worker_count > 0
499
565
 
566
+ max_unacked ||= Einhorn::State.config[:max_unacked]
567
+
500
568
  # default to spinning up at most NCPU workers at once
501
569
  unless max_unacked
502
570
  begin
@@ -514,16 +582,13 @@ module Einhorn
514
582
 
515
583
  # Exponentially backoff automated spinup if we're just having
516
584
  # things die before ACKing
517
- spinup_interval = Einhorn::State.config[:seconds] * (1.5 ** Einhorn::State.consecutive_deaths_before_ack)
585
+ spinup_interval = Einhorn::State.config[:seconds] * (1.5**Einhorn::State.consecutive_deaths_before_ack)
518
586
  spinup_interval = [spinup_interval, MAX_SPINUP_INTERVAL].min
519
587
  seconds_ago = (Time.now - Einhorn::State.last_spinup).to_f
520
588
 
521
589
  if seconds_ago > spinup_interval
522
- unacked = Einhorn::WorkerPool.unacked_unsignaled_modern_workers.length
523
- if unacked >= max_unacked
524
- Einhorn.log_debug("There are #{unacked} unacked new workers, and max_unacked is #{max_unacked}, so not spinning up a new process")
525
- else
526
- msg = "Last spinup was #{seconds_ago}s ago, and spinup_interval is #{spinup_interval}s, so spinning up a new process"
590
+ if trigger_spinup?(max_unacked)
591
+ msg = "Last spinup was #{seconds_ago}s ago, and spinup_interval is #{spinup_interval}s, so spinning up a new process."
527
592
 
528
593
  if Einhorn::State.consecutive_deaths_before_ack > 0
529
594
  Einhorn.log_info("#{msg} (there have been #{Einhorn::State.consecutive_deaths_before_ack} consecutive unacked worker deaths)", :upgrade)
@@ -534,7 +599,7 @@ module Einhorn
534
599
  spinup
535
600
  end
536
601
  else
537
- Einhorn.log_debug("Last spinup was #{seconds_ago}s ago, and spinup_interval is #{spinup_interval}s, so not spinning up a new process")
602
+ Einhorn.log_debug("Last spinup was #{seconds_ago}s ago, and spinup_interval is #{spinup_interval}s, so not spinning up a new process.")
538
603
  end
539
604
 
540
605
  Einhorn::TransientState.has_outstanding_spinup_timer = true
@@ -544,18 +609,35 @@ module Einhorn
544
609
  end
545
610
  end
546
611
 
547
- def self.quieter(log=true)
612
+ def self.quieter(log = true)
548
613
  Einhorn::State.verbosity += 1 if Einhorn::State.verbosity < 2
549
614
  output = "Verbosity set to #{Einhorn::State.verbosity}"
550
615
  Einhorn.log_info(output) if log
551
616
  output
552
617
  end
553
618
 
554
- def self.louder(log=true)
619
+ def self.louder(log = true)
555
620
  Einhorn::State.verbosity -= 1 if Einhorn::State.verbosity > 0
556
621
  output = "Verbosity set to #{Einhorn::State.verbosity}"
557
622
  Einhorn.log_info(output) if log
558
623
  output
559
624
  end
625
+
626
+ def self.trigger_spinup?(max_unacked)
627
+ unacked = Einhorn::WorkerPool.unacked_unsignaled_modern_workers.length
628
+ if unacked >= max_unacked
629
+ Einhorn.log_info("There are #{unacked} unacked new workers, and max_unacked is #{max_unacked}, so not spinning up a new process.")
630
+ return false
631
+ elsif Einhorn::State.config[:max_upgrade_additional]
632
+ capacity_exceeded = (Einhorn::State.config[:number] + Einhorn::State.config[:max_upgrade_additional]) - Einhorn::WorkerPool.workers_with_state.length
633
+ if capacity_exceeded < 0
634
+ Einhorn.log_info("Over worker capacity by #{capacity_exceeded.abs} during upgrade, #{Einhorn::WorkerPool.modern_workers.length} new workers of #{Einhorn::WorkerPool.workers_with_state.length} total. Waiting for old workers to exit before spinning up a process.")
635
+
636
+ return false
637
+ end
638
+ end
639
+
640
+ true
641
+ end
560
642
  end
561
643
  end
@@ -11,10 +11,10 @@ module Einhorn
11
11
 
12
12
  def self.cloexec!(fd, enable)
13
13
  original = fd.fcntl(Fcntl::F_GETFD)
14
- if enable
15
- new = original | Fcntl::FD_CLOEXEC
14
+ new = if enable
15
+ original | Fcntl::FD_CLOEXEC
16
16
  else
17
- new = original & (-Fcntl::FD_CLOEXEC-1)
17
+ original & (-Fcntl::FD_CLOEXEC - 1)
18
18
  end
19
19
  fd.fcntl(Fcntl::F_SETFD, new)
20
20
  end
@@ -24,7 +24,7 @@ module Einhorn
24
24
  end
25
25
 
26
26
  # Opts are ignored in Ruby 1.8
27
- def self.exec(script, args, opts={})
27
+ def self.exec(script, args, opts = {})
28
28
  cmd = [script, script]
29
29
  begin
30
30
  Kernel.exec(cmd, *(args + [opts]))
@@ -53,18 +53,18 @@ module Einhorn
53
53
 
54
54
  # linux / friends
55
55
  begin
56
- return File.read('/proc/cpuinfo').scan(/^processor\s*:/).count
56
+ return File.read("/proc/cpuinfo").scan(/^processor\s*:/).count
57
57
  rescue Errno::ENOENT
58
58
  end
59
59
 
60
60
  # OS X
61
- if RUBY_PLATFORM =~ /darwin/
61
+ if RUBY_PLATFORM.match?(/darwin/)
62
62
  return Integer(`sysctl -n hw.logicalcpu`)
63
63
  end
64
64
 
65
65
  # windows / friends
66
66
  begin
67
- require 'win32ole'
67
+ require "win32ole"
68
68
  rescue LoadError
69
69
  else
70
70
  wmi = WIN32OLE.connect("winmgmts://")