einhorn 0.5.4 → 0.5.5

Sign up to get free protection for your applications and to get access to all the features.
data/einhorn.gemspec CHANGED
@@ -14,13 +14,12 @@ Gem::Specification.new do |gem|
14
14
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
15
15
  gem.name = 'einhorn'
16
16
  gem.require_paths = ['lib']
17
+
17
18
  gem.add_development_dependency 'rake'
18
- gem.add_development_dependency 'minitest'
19
- gem.add_development_dependency 'mocha'
19
+ gem.add_development_dependency 'pry'
20
+ gem.add_development_dependency 'minitest', '< 5.0'
21
+ gem.add_development_dependency 'mocha', '~> 0.13'
20
22
  gem.add_development_dependency 'chalk-rake'
21
23
 
22
- gem.add_development_dependency('rake')
23
- gem.add_development_dependency('minitest', '< 5.0')
24
- gem.add_development_dependency('mocha', '~> 0.13')
25
24
  gem.version = Einhorn::VERSION
26
25
  end
data/example/time_server CHANGED
@@ -15,18 +15,36 @@
15
15
  require 'rubygems'
16
16
  require 'einhorn/worker'
17
17
 
18
+ def log(msg)
19
+ puts "=== [#{$$}] #{msg}"
20
+ end
21
+
18
22
  def einhorn_main
19
- puts "Called with ENV['EINHORN_FD_0']: #{ENV['EINHORN_FD_0']}"
23
+ log "Called with ENV['EINHORN_FD_0']: #{ENV['EINHORN_FD_0']}"
20
24
 
21
25
  fd_num = Einhorn::Worker.socket!
22
26
  socket = Socket.for_fd(fd_num)
23
27
 
28
+ sleep_before_shutdown = 0
29
+ sleep_before_ack = 0
30
+
24
31
  # Came up successfully, so let's set up graceful handler and ACK the
25
32
  # master.
26
33
  Einhorn::Worker.graceful_shutdown do
27
- puts "Goodbye from #{$$}"
34
+ if sleep_before_shutdown > 0
35
+ log "sleeping #{sleep_before_shutdown}s before shutdown"
36
+ sleep sleep_before_shutdown
37
+ end
38
+ log "Goodbye!"
28
39
  exit(0)
29
40
  end
41
+
42
+ if sleep_before_ack > 0
43
+ log "sleeping #{sleep_before_ack}s before ack"
44
+ sleep sleep_before_ack
45
+ end
46
+ log "worker ack"
47
+
30
48
  Einhorn::Worker.ack!
31
49
 
32
50
  # Real work happens here.
@@ -162,10 +162,10 @@ module Einhorn::Command
162
162
  Einhorn::Command.stop_respawning
163
163
  exit(1)
164
164
  end
165
- trap_async("HUP") {Einhorn::Command.full_upgrade}
165
+ trap_async("HUP") {Einhorn::Command.full_upgrade_smooth}
166
166
  trap_async("ALRM") do
167
167
  Einhorn.log_error("Upgrading using SIGALRM is deprecated. Please switch to SIGHUP")
168
- Einhorn::Command.full_upgrade
168
+ Einhorn::Command.full_upgrade_smooth
169
169
  end
170
170
  trap_async("CHLD") {}
171
171
  trap_async("USR2") do
@@ -292,7 +292,8 @@ EOF
292
292
  # Used by einhornsh
293
293
  command 'ehlo' do |conn, request|
294
294
  <<EOF
295
- Welcome #{request['user']}! You are speaking to Einhorn Master Process #{$$}#{Einhorn::State.cmd_name ? " (#{Einhorn::State.cmd_name})" : ''}
295
+ Welcome, #{request['user']}! You are speaking to Einhorn Master Process #{$$}#{Einhorn::State.cmd_name ? " (#{Einhorn::State.cmd_name})" : ''}.
296
+ This is Einhorn #{Einhorn::VERSION}.
296
297
  EOF
297
298
  end
298
299
 
@@ -335,14 +336,25 @@ EOF
335
336
  Einhorn::Command.louder
336
337
  end
337
338
 
338
- command 'upgrade', 'Upgrade all Einhorn workers. This may result in Einhorn reloading its own code as well.' do |conn, request|
339
+ command 'upgrade', 'Upgrade all Einhorn workers smoothly. This may result in Einhorn reloading its own code as well.' do |conn, request|
339
340
  # send first message directly for old clients that don't support request
340
341
  # ids or subscriptions. Everything else is sent tagged with request id
341
342
  # for new clients.
342
- send_message(conn, 'Upgrading, as commanded', request['id'])
343
+ send_message(conn, 'Upgrading smoothly, as commanded', request['id'])
343
344
  conn.subscribe(:upgrade, request['id'])
344
345
  # If the app is preloaded this doesn't return.
345
- Einhorn::Command.full_upgrade
346
+ Einhorn::Command.full_upgrade_smooth
347
+ nil
348
+ end
349
+
350
+ command 'upgrade_fleet', 'Upgrade all Einhorn workers a fleet at a time. This may result in Einhorn reloading its own code as well.' do |conn, request|
351
+ # send first message directly for old clients that don't support request
352
+ # ids or subscriptions. Everything else is sent tagged with request id
353
+ # for new clients.
354
+ send_message(conn, 'Upgrading fleet, as commanded', request['id'])
355
+ conn.subscribe(:upgrade, request['id'])
356
+ # If the app is preloaded this doesn't return.
357
+ Einhorn::Command.full_upgrade_fleet
346
358
  nil
347
359
  end
348
360
 
@@ -232,6 +232,8 @@ module Einhorn
232
232
  Einhorn::Event.close_all_for_worker
233
233
  Einhorn.set_argv(cmd, true)
234
234
 
235
+ reseed_random
236
+
235
237
  prepare_child_environment
236
238
  einhorn_main
237
239
  end
@@ -294,11 +296,53 @@ module Einhorn
294
296
  ENV['EINHORN_FDS'] = Einhorn::State.bind_fds.map(&:to_s).join(' ')
295
297
  end
296
298
 
299
+ # Reseed common ruby random number generators.
300
+ #
301
+ # OpenSSL::Random uses the PID to reseed after fork, which means that if a
302
+ # long-lived master process over its lifetime spawns two workers with the
303
+ # same PID, those workers will start with the same OpenSSL seed.
304
+ #
305
+ # Ruby >= 1.9 has a guard against this in SecureRandom, but any direct
306
+ # users of OpenSSL::Random will still be affected.
307
+ #
308
+ # Ruby 1.8 didn't even reseed the default random number generator used by
309
+ # Kernel#rand in certain releases.
310
+ #
311
+ # https://bugs.ruby-lang.org/issues/4579
312
+ #
313
+ def self.reseed_random
314
+ # reseed Kernel#rand
315
+ srand
316
+
317
+ # reseed OpenSSL::Random if it's loaded
318
+ if defined?(OpenSSL::Random)
319
+ if defined?(Random)
320
+ seed = Random.new_seed
321
+ else
322
+ # Ruby 1.8
323
+ seed = rand
324
+ end
325
+ OpenSSL::Random.seed(seed.to_s)
326
+ end
327
+ end
328
+
297
329
  def self.prepare_child_process
298
330
  Einhorn.renice_self
299
331
  end
300
332
 
301
- def self.full_upgrade
333
+ # @param options [Hash]
334
+ #
335
+ # @option options [Boolean] :smooth (false) Whether to perform a smooth or
336
+ # fleet upgrade. In a smooth upgrade, bring up new workers and cull old
337
+ # workers one by one as soon as there is a replacement. In a fleet
338
+ # upgrade, bring up all the new workers and don't cull any old workers
339
+ # until they're all up.
340
+ #
341
+ def self.full_upgrade(options={})
342
+ options = {:smooth => false}.merge(options)
343
+
344
+ Einhorn::State.smooth_upgrade = options.fetch(:smooth)
345
+
302
346
  if Einhorn::State.path && !Einhorn::State.reloading_for_preload_upgrade
303
347
  reload_for_preload_upgrade
304
348
  else
@@ -306,6 +350,13 @@ module Einhorn
306
350
  end
307
351
  end
308
352
 
353
+ def self.full_upgrade_smooth
354
+ full_upgrade(:smooth => true)
355
+ end
356
+ def self.full_upgrade_fleet
357
+ full_upgrade(:smooth => false)
358
+ end
359
+
309
360
  def self.reload_for_preload_upgrade
310
361
  Einhorn::State.reloading_for_preload_upgrade = true
311
362
  reload
@@ -316,7 +367,9 @@ module Einhorn
316
367
  Einhorn.log_info("Currently upgrading (#{Einhorn::WorkerPool.ack_count} / #{Einhorn::WorkerPool.ack_target} ACKs; bumping version and starting over)...", :upgrade)
317
368
  else
318
369
  Einhorn::State.upgrading = true
319
- Einhorn.log_info("Starting upgrade from version #{Einhorn::State.version}...", :upgrade)
370
+ u_type = Einhorn::State.smooth_upgrade ? 'smooth' : 'fleet'
371
+ Einhorn.log_info("Starting #{u_type} upgrade from version" +
372
+ " #{Einhorn::State.version}...", :upgrade)
320
373
  end
321
374
 
322
375
  # Reset this, since we've just upgraded to a new universe (I'm
@@ -326,7 +379,11 @@ module Einhorn
326
379
  Einhorn::State.last_upgraded = Time.now
327
380
 
328
381
  Einhorn::State.version += 1
329
- replenish_immediately
382
+ if Einhorn::State.smooth_upgrade
383
+ replenish_gradually
384
+ else
385
+ replenish_immediately
386
+ end
330
387
  end
331
388
 
332
389
  def self.cull
@@ -341,9 +398,20 @@ module Einhorn
341
398
  end
342
399
 
343
400
  old_workers = Einhorn::WorkerPool.old_workers
401
+ Einhorn.log_debug("#{acked} acked, #{unsignaled} unsignaled, #{target} target, #{old_workers.length} old workers")
344
402
  if !Einhorn::State.upgrading && old_workers.length > 0
345
403
  Einhorn.log_info("Killing off #{old_workers.length} old workers.")
346
404
  signal_all("USR2", old_workers)
405
+ elsif Einhorn::State.upgrading && Einhorn::State.smooth_upgrade
406
+ # In a smooth upgrade, kill off old workers one by one when we have
407
+ # sufficiently many new workers.
408
+ excess = (old_workers.length + acked) - target
409
+ if excess > 0
410
+ Einhorn.log_info("Smooth upgrade: killing off #{excess} old workers.")
411
+ signal_all("USR2", old_workers.take(excess))
412
+ elsif excess < 0
413
+ Einhorn.log_error("Smooth upgrade: somehow excess is #{excess}!")
414
+ end
347
415
  end
348
416
 
349
417
  if unsignaled > target
@@ -378,25 +446,45 @@ module Einhorn
378
446
  missing.times {spinup}
379
447
  end
380
448
 
381
- def self.replenish_gradually
449
+ def self.replenish_gradually(max_unacked=nil)
382
450
  return if Einhorn::TransientState.has_outstanding_spinup_timer
383
451
  return unless Einhorn::WorkerPool.missing_worker_count > 0
384
452
 
453
+ # default to spinning up at most NCPU workers at once
454
+ unless max_unacked
455
+ begin
456
+ @processor_count ||= Einhorn::Compat.processor_count
457
+ rescue => err
458
+ Einhorn.log_error(err.inspect)
459
+ @processor_count = 1
460
+ end
461
+ max_unacked = @processor_count
462
+ end
463
+
464
+ if max_unacked <= 0
465
+ raise ArgumentError.new("max_unacked must be positive")
466
+ end
467
+
385
468
  # Exponentially backoff automated spinup if we're just having
386
469
  # things die before ACKing
387
470
  spinup_interval = Einhorn::State.config[:seconds] * (1.5 ** Einhorn::State.consecutive_deaths_before_ack)
388
471
  seconds_ago = (Time.now - Einhorn::State.last_spinup).to_f
389
472
 
390
473
  if seconds_ago > spinup_interval
391
- msg = "Last spinup was #{seconds_ago}s ago, and spinup_interval is #{spinup_interval}s, so spinning up a new process"
392
-
393
- if Einhorn::State.consecutive_deaths_before_ack > 0
394
- Einhorn.log_info("#{msg} (there have been #{Einhorn::State.consecutive_deaths_before_ack} consecutive unacked worker deaths)")
474
+ unacked = Einhorn::WorkerPool.unacked_unsignaled_modern_workers.length
475
+ if unacked >= max_unacked
476
+ Einhorn.log_debug("There are #{unacked} unacked new workers, and max_unacked is #{max_unacked}, so not spinning up a new process")
395
477
  else
396
- Einhorn.log_debug(msg)
397
- end
478
+ msg = "Last spinup was #{seconds_ago}s ago, and spinup_interval is #{spinup_interval}s, so spinning up a new process"
398
479
 
399
- spinup
480
+ if Einhorn::State.consecutive_deaths_before_ack > 0
481
+ Einhorn.log_info("#{msg} (there have been #{Einhorn::State.consecutive_deaths_before_ack} consecutive unacked worker deaths)")
482
+ else
483
+ Einhorn.log_debug(msg)
484
+ end
485
+
486
+ spinup
487
+ end
400
488
  else
401
489
  Einhorn.log_debug("Last spinup was #{seconds_ago}s ago, and spinup_interval is #{spinup_interval}s, so not spinning up a new process")
402
490
  end
@@ -44,5 +44,41 @@ module Einhorn
44
44
  cloexec!(sock, false)
45
45
  sock
46
46
  end
47
+
48
+ def self.processor_count
49
+ # jruby
50
+ if defined? Java::Java
51
+ return Java::Java.lang.Runtime.getRuntime.availableProcessors
52
+ end
53
+
54
+ # linux / friends
55
+ begin
56
+ return File.read('/proc/cpuinfo').scan(/^processor\s*:/).count
57
+ rescue Errno::ENOENT
58
+ end
59
+
60
+ # OS X
61
+ if RUBY_PLATFORM =~ /darwin/
62
+ return Integer(`sysctl -n hw.logicalcpu`)
63
+ end
64
+
65
+ # windows / friends
66
+ begin
67
+ require 'win32ole'
68
+ rescue LoadError
69
+ else
70
+ wmi = WIN32OLE.connect("winmgmts://")
71
+ wmi.ExecQuery("select * from Win32_ComputerSystem").each do |system|
72
+ begin
73
+ processors = system.NumberOfLogicalProcessors
74
+ rescue
75
+ processors = 0
76
+ end
77
+ return [system.NumberOfProcessors, processors].max
78
+ end
79
+ end
80
+
81
+ raise "Failed to detect number of CPUs"
82
+ end
47
83
  end
48
84
  end
@@ -1,3 +1,3 @@
1
1
  module Einhorn
2
- VERSION = '0.5.4'
2
+ VERSION = '0.5.5'
3
3
  end
@@ -52,6 +52,16 @@ module Einhorn
52
52
  end.map {|pid, _| pid}
53
53
  end
54
54
 
55
+ def self.unacked_unsignaled_modern_workers_with_state
56
+ modern_workers_with_state.select {|pid, spec|
57
+ !spec[:acked] && spec[:signaled].length == 0
58
+ }
59
+ end
60
+
61
+ def self.unacked_unsignaled_modern_workers
62
+ unacked_unsignaled_modern_workers_with_state.map {|pid, _| pid}
63
+ end
64
+
55
65
  # Use the number of modern workers, rather than unsignaled modern
56
66
  # workers. This means if e.g. we do bunch of decs and then incs,
57
67
  # any workers which haven't died yet will count towards our number
data/lib/einhorn.rb CHANGED
@@ -55,6 +55,7 @@ module Einhorn
55
55
  :script_name => nil,
56
56
  :respawn => true,
57
57
  :upgrading => false,
58
+ :smooth_upgrade => false,
58
59
  :reloading_for_preload_upgrade => false,
59
60
  :path => nil,
60
61
  :cmd_name => nil,
@@ -12,7 +12,7 @@ class InterfaceTest < EinhornTestCase
12
12
  # Remove trailing newline
13
13
  message = message[0...-1]
14
14
  parsed = YAML.load(URI.unescape(message))
15
- parsed['message'] =~ /Welcome gdb/
15
+ parsed['message'] =~ /Welcome, gdb/
16
16
  end
17
17
  request = {
18
18
  'command' => 'ehlo',
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: einhorn
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.4
4
+ version: 0.5.5
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2014-01-11 00:00:00.000000000 Z
12
+ date: 2014-03-25 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rake
@@ -28,7 +28,7 @@ dependencies:
28
28
  - !ruby/object:Gem::Version
29
29
  version: '0'
30
30
  - !ruby/object:Gem::Dependency
31
- name: minitest
31
+ name: pry
32
32
  requirement: !ruby/object:Gem::Requirement
33
33
  none: false
34
34
  requirements:
@@ -44,39 +44,39 @@ dependencies:
44
44
  - !ruby/object:Gem::Version
45
45
  version: '0'
46
46
  - !ruby/object:Gem::Dependency
47
- name: mocha
47
+ name: minitest
48
48
  requirement: !ruby/object:Gem::Requirement
49
49
  none: false
50
50
  requirements:
51
- - - ! '>='
51
+ - - <
52
52
  - !ruby/object:Gem::Version
53
- version: '0'
53
+ version: '5.0'
54
54
  type: :development
55
55
  prerelease: false
56
56
  version_requirements: !ruby/object:Gem::Requirement
57
57
  none: false
58
58
  requirements:
59
- - - ! '>='
59
+ - - <
60
60
  - !ruby/object:Gem::Version
61
- version: '0'
61
+ version: '5.0'
62
62
  - !ruby/object:Gem::Dependency
63
- name: chalk-rake
63
+ name: mocha
64
64
  requirement: !ruby/object:Gem::Requirement
65
65
  none: false
66
66
  requirements:
67
- - - ! '>='
67
+ - - ~>
68
68
  - !ruby/object:Gem::Version
69
- version: '0'
69
+ version: '0.13'
70
70
  type: :development
71
71
  prerelease: false
72
72
  version_requirements: !ruby/object:Gem::Requirement
73
73
  none: false
74
74
  requirements:
75
- - - ! '>='
75
+ - - ~>
76
76
  - !ruby/object:Gem::Version
77
- version: '0'
77
+ version: '0.13'
78
78
  - !ruby/object:Gem::Dependency
79
- name: rake
79
+ name: chalk-rake
80
80
  requirement: !ruby/object:Gem::Requirement
81
81
  none: false
82
82
  requirements:
@@ -91,38 +91,6 @@ dependencies:
91
91
  - - ! '>='
92
92
  - !ruby/object:Gem::Version
93
93
  version: '0'
94
- - !ruby/object:Gem::Dependency
95
- name: minitest
96
- requirement: !ruby/object:Gem::Requirement
97
- none: false
98
- requirements:
99
- - - <
100
- - !ruby/object:Gem::Version
101
- version: '5.0'
102
- type: :development
103
- prerelease: false
104
- version_requirements: !ruby/object:Gem::Requirement
105
- none: false
106
- requirements:
107
- - - <
108
- - !ruby/object:Gem::Version
109
- version: '5.0'
110
- - !ruby/object:Gem::Dependency
111
- name: mocha
112
- requirement: !ruby/object:Gem::Requirement
113
- none: false
114
- requirements:
115
- - - ~>
116
- - !ruby/object:Gem::Version
117
- version: '0.13'
118
- type: :development
119
- prerelease: false
120
- version_requirements: !ruby/object:Gem::Requirement
121
- none: false
122
- requirements:
123
- - - ~>
124
- - !ruby/object:Gem::Version
125
- version: '0.13'
126
94
  description: Einhorn makes it easy to run multiple instances of an application server,
127
95
  all listening on the same port. You can also seamlessly restart your workers without
128
96
  dropping any requests. Einhorn requires minimal application-level support, making
@@ -198,7 +166,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
198
166
  version: '0'
199
167
  segments:
200
168
  - 0
201
- hash: -4111733587865506244
169
+ hash: -690549708583071819
202
170
  required_rubygems_version: !ruby/object:Gem::Requirement
203
171
  none: false
204
172
  requirements:
@@ -207,7 +175,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
207
175
  version: '0'
208
176
  segments:
209
177
  - 0
210
- hash: -4111733587865506244
178
+ hash: -690549708583071819
211
179
  requirements: []
212
180
  rubyforge_project:
213
181
  rubygems_version: 1.8.23