einhorn 0.5.7 → 0.6.0

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 ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ ZTAzM2VhNjlkNDFhM2VlYzE4MjcwMjk3MmI3YzQwMzRkN2MxZmM0Ng==
5
+ data.tar.gz: !binary |-
6
+ NTdmZGE0ZDVjMjYyODdlODIyZWM0Y2ExYWE1MWViNmQ3MjEzNWFlMQ==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ ZjgzZGVlZmMwNzZmNGFjMDkzODZlNTJhZjdiNDdjY2I0MmRiOGE0NmQzYjkz
10
+ Mzc0NjY0MGQ1NWYxMzVkMjI4Nzg0MWNkZGI4MmEwMmIzZWI1MDk5ZDRkZmRh
11
+ YzBkYTAxMWU1MTVhZWU1ZTM1NDMzMGJlOTgyY2U0MTI5OTM3ZGI=
12
+ data.tar.gz: !binary |-
13
+ MjQyZTI4NjI0Njg4NDJmMDBlMWQwYTY4MDU3OWRiZTFhOGJlNDg1YmNkZGJl
14
+ Y2EzNWMzYjk5NTA3NzU1NzhiN2JmYzUwZjgzYjUxOWU2NTBhM2U0MDA4OTdm
15
+ Njc4NzQ1MDI0ZWY5Zjk4MDk1NzllODk2NWYzMDE3MTllODM5Njk=
data/README.md CHANGED
@@ -211,7 +211,12 @@ pass `-c <name>`.
211
211
  -q, --quiet Make output quiet (can be reconfigured on the fly)
212
212
  -s, --seconds N Number of seconds to wait until respawning
213
213
  -v, --verbose Make output verbose (can be reconfigured on the fly)
214
+ --drop-env-var VAR_NAME Delete VAR_NAME from the environment that is restored on upgrade
215
+ --reexec-as=CMDLINE Substitute CMDLINE for \"einhorn\" when upgrading
216
+ --nice MASTER[:WORKER=0][:RENICE_CMD=/usr/bin/renice]
217
+ Unix nice level at which to run the einhorn processes. If not running as root, make sure to ulimit -e as appopriate.
214
218
  --with-state-fd STATE [Internal option] With file descriptor containing state
219
+ --upgrade-check [Internal option] Check if Einhorn can exec itself and exit with status 0 before loading code
215
220
  --version Show version
216
221
 
217
222
 
data/README.md.in CHANGED
@@ -61,11 +61,8 @@ servers) to a wider array of applications.
61
61
  See https://stripe.com/blog/meet-einhorn for more background.
62
62
 
63
63
  Stripe currently uses Einhorn in production for a number of
64
- services. Our Thin + EventMachine servers currently require patches to
65
- both Thin and EventMachine (to support file-descriptor passing). You
66
- can obtain these patches from our public forks of the
67
- [respective](https://github.com/stripe/thin)
68
- [projects](https://github.com/stripe/eventmachine). Check out
64
+ services. You can use Conrad Irwin's thin-attach_socket gem along with
65
+ EventMachine-LE to support file-descriptor passing. Check out
69
66
  `example/thin_example` for an example of running Thin under Einhorn.
70
67
 
71
68
  ## Compatibility
data/Rakefile CHANGED
@@ -11,13 +11,6 @@ task :readme do
11
11
  File.open('README.md', 'w') {|f| f.write(readme)}
12
12
  end
13
13
 
14
- Rake::TestTask.new do |t|
15
- t.libs = ["lib"]
16
- # t.warning = true
17
- t.verbose = true
18
- t.test_files = FileList['test/unit/**/*.rb']
19
- end
20
-
21
14
  task :default => :test do
22
15
  end
23
16
  require 'bundler/setup'
data/bin/einhorn CHANGED
@@ -275,6 +275,14 @@ if true # $0 == __FILE__
275
275
  Einhorn::Command.louder(false)
276
276
  end
277
277
 
278
+ opts.on('--drop-env-var VAR_NAME', 'Delete VAR_NAME from the environment that is restored on upgrade') do |var|
279
+ Einhorn::State.drop_environment_variables << var
280
+ end
281
+
282
+ opts.on('--reexec-as=CMDLINE', 'Substitute CMDLINE for \"einhorn\" when upgrading') do |cmdline|
283
+ Einhorn::State.reexec_commandline = Shellwords.shellsplit(cmdline)
284
+ end
285
+
278
286
  Einhorn.plugins_send(:optparse, opts)
279
287
 
280
288
  opts.on('--nice MASTER[:WORKER=0][:RENICE_CMD=/usr/bin/renice]', 'Unix nice level at which to run the einhorn processes. If not running as root, make sure to ulimit -e as appopriate.') do |nice|
@@ -296,6 +304,11 @@ if true # $0 == __FILE__
296
304
  Einhorn.restore_state(state)
297
305
  end
298
306
 
307
+ opts.on('--upgrade-check', '[Internal option] Check if Einhorn can exec itself and exit with status 0 before loading code') do
308
+ Einhorn.dump_environment_info
309
+ exit 0
310
+ end
311
+
299
312
  opts.on('--version', 'Show version') do
300
313
  puts Einhorn::VERSION
301
314
  exit
data/einhorn.gemspec CHANGED
@@ -20,6 +20,7 @@ Gem::Specification.new do |gem|
20
20
  gem.add_development_dependency 'minitest', '< 5.0'
21
21
  gem.add_development_dependency 'mocha', '~> 0.13'
22
22
  gem.add_development_dependency 'chalk-rake'
23
+ gem.add_development_dependency 'subprocess'
23
24
 
24
25
  gem.version = Einhorn::VERSION
25
26
  end
@@ -351,7 +351,7 @@ EOF
351
351
  Einhorn::Command.louder
352
352
  end
353
353
 
354
- command 'upgrade', 'Upgrade all Einhorn workers smoothly. This may result in Einhorn reloading its own code as well.' do |conn, request|
354
+ command 'upgrade', 'Upgrade all Einhorn workers smoothly. This causes Einhorn to reload its own code as well.' do |conn, request|
355
355
  # send first message directly for old clients that don't support request
356
356
  # ids or subscriptions. Everything else is sent tagged with request id
357
357
  # for new clients.
@@ -362,7 +362,7 @@ EOF
362
362
  nil
363
363
  end
364
364
 
365
- 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|
365
+ command 'upgrade_fleet', 'Upgrade all Einhorn workers a fleet at a time. This causes Einhorn to reload its own code as well.' do |conn, request|
366
366
  # send first message directly for old clients that don't support request
367
367
  # ids or subscriptions. Everything else is sent tagged with request id
368
368
  # for new clients.
@@ -171,13 +171,13 @@ module Einhorn
171
171
  end
172
172
 
173
173
  def self.dumpable_state
174
- global_state = Einhorn::State.state
174
+ global_state = Einhorn::State.dumpable_state
175
175
  descriptor_state = Einhorn::Event.persistent_descriptors.map do |descriptor|
176
176
  descriptor.to_state
177
177
  end
178
178
  plugin_state = {}
179
179
  Einhorn.plugins.each do |name, plugin|
180
- plugin_state[name] = plugin::State.state if plugin.const_defined?(:State)
180
+ plugin_state[name] = plugin::State.dumpable_state if plugin.const_defined?(:State)
181
181
  end
182
182
 
183
183
  {
@@ -193,7 +193,7 @@ module Einhorn
193
193
  return
194
194
  end
195
195
 
196
- Einhorn.log_info("Reloading einhorn (#{Einhorn::TransientState.script_name})...")
196
+ Einhorn.log_info("Reloading einhorn master (#{Einhorn::TransientState.script_name})...", :reload)
197
197
 
198
198
  # In case there's anything lurking
199
199
  $stdout.flush
@@ -221,19 +221,22 @@ module Einhorn
221
221
  end
222
222
  write.close
223
223
 
224
- # Reload the original environment
225
- ENV.clear
226
- ENV.update(Einhorn::TransientState.environ)
224
+ unless Einhorn.can_safely_reload?
225
+ Einhorn.log_error("Can not initiate einhorn master reload safely, aborting", :reload)
226
+ Einhorn::State.reloading_for_upgrade = false
227
+ read.close
228
+ return
229
+ end
227
230
 
228
231
  begin
229
- Einhorn::Compat.exec(
230
- Einhorn::TransientState.script_name,
231
- ['--with-state-fd', read.fileno.to_s, '--'] + Einhorn::State.cmd,
232
- :close_others => false
233
- )
232
+ Einhorn.initialize_reload_environment
233
+ respawn_commandline = Einhorn.upgrade_commandline(['--with-state-fd', read.fileno.to_s])
234
+ respawn_commandline << { :close_others => false }
235
+ Einhorn.log_info("About to re-exec einhorn master as #{respawn_commandline.inspect}", :reload)
236
+ Einhorn::Compat.exec(*respawn_commandline)
234
237
  rescue SystemCallError => e
235
- Einhorn.log_error("Could not reload! Attempting to continue. Error was: #{e}")
236
- Einhorn::State.reloading_for_preload_upgrade = false
238
+ Einhorn.log_error("Could not reload! Attempting to continue. Error was: #{e}", :reload)
239
+ Einhorn::State.reloading_for_upgrade = false
237
240
  read.close
238
241
  end
239
242
  end
@@ -371,12 +374,7 @@ module Einhorn
371
374
  options = {:smooth => false}.merge(options)
372
375
 
373
376
  Einhorn::State.smooth_upgrade = options.fetch(:smooth)
374
-
375
- if Einhorn::State.path && !Einhorn::State.reloading_for_preload_upgrade
376
- reload_for_preload_upgrade
377
- else
378
- upgrade_workers
379
- end
377
+ reload_for_upgrade
380
378
  end
381
379
 
382
380
  def self.full_upgrade_smooth
@@ -386,8 +384,8 @@ module Einhorn
386
384
  full_upgrade(:smooth => false)
387
385
  end
388
386
 
389
- def self.reload_for_preload_upgrade
390
- Einhorn::State.reloading_for_preload_upgrade = true
387
+ def self.reload_for_upgrade
388
+ Einhorn::State.reloading_for_upgrade = true
391
389
  reload
392
390
  end
393
391
 
@@ -438,8 +436,8 @@ module Einhorn
438
436
  if excess > 0
439
437
  Einhorn.log_info("Smooth upgrade: killing off #{excess} old workers.", :upgrade)
440
438
  signal_all("USR2", old_workers.take(excess))
441
- elsif excess < 0
442
- Einhorn.log_error("Smooth upgrade: somehow excess is #{excess}!", :upgrade)
439
+ else
440
+ Einhorn.log_debug("Not killing old workers, as excess is #{excess}.")
443
441
  end
444
442
  end
445
443
 
@@ -1,3 +1,3 @@
1
1
  module Einhorn
2
- VERSION = '0.5.7'
2
+ VERSION = '0.6.0'
3
3
  end
data/lib/einhorn.rb CHANGED
@@ -5,6 +5,7 @@ require 'set'
5
5
  require 'socket'
6
6
  require 'tmpdir'
7
7
  require 'yaml'
8
+ require 'shellwords'
8
9
 
9
10
  require 'einhorn/third/little-plugger'
10
11
 
@@ -22,6 +23,7 @@ module Einhorn
22
23
  def default_state; raise NotImplementedError.new('Override in extended modules'); end
23
24
  def state; @state ||= default_state; end
24
25
  def state=(v); @state = v; end
26
+ def dumpable_state; state; end
25
27
 
26
28
  def method_missing(name, *args)
27
29
  if (name.to_s =~ /(.*)=$/) && state.has_key?($1.to_sym)
@@ -41,6 +43,9 @@ module Einhorn
41
43
 
42
44
  module State
43
45
  extend AbstractState
46
+
47
+ # WARNING: Don't change or remove these variables without thinking
48
+ # about backwards/forwards compatibility for upgrades/downgrades
44
49
  def self.default_state
45
50
  {
46
51
  :children => {},
@@ -56,7 +61,7 @@ module Einhorn
56
61
  :respawn => true,
57
62
  :upgrading => false,
58
63
  :smooth_upgrade => false,
59
- :reloading_for_preload_upgrade => false,
64
+ :reloading_for_upgrade => false,
60
65
  :path => nil,
61
66
  :cmd_name => nil,
62
67
  :verbosity => 1,
@@ -70,9 +75,17 @@ module Einhorn
70
75
  :lockfile => nil,
71
76
  :consecutive_deaths_before_ack => 0,
72
77
  :last_upgraded => nil,
73
- :nice => {:master => nil, :worker => nil, :renice_cmd => '/usr/bin/renice'}
78
+ :nice => {:master => nil, :worker => nil, :renice_cmd => '/usr/bin/renice'},
79
+ :reexec_commandline => nil,
80
+ :drop_environment_variables => [],
74
81
  }
75
82
  end
83
+
84
+ def self.dumpable_state
85
+ dump = state
86
+ dump[:reloading_for_preload_upgrade] = dump[:reloading_for_upgrade]
87
+ dump
88
+ end
76
89
  end
77
90
 
78
91
  module TransientState
@@ -105,18 +118,27 @@ module Einhorn
105
118
  end
106
119
 
107
120
  def self.update_state(store, store_name, old_state)
108
- # TODO: handle format updates somehow? (probably need to write
109
- # special-case code for each)
121
+ message = []
110
122
  updated_state = old_state.dup
123
+
124
+ # Handle changes in state format updates from previous einhorn versions
125
+ if store == Einhorn::State
126
+ # TODO: Drop this backwards compatibility hack when we hit 0.7
127
+ if updated_state.include?(:reloading_for_preload_upgrade) &&
128
+ !updated_state.include?(:reloading_for_upgrade)
129
+ updated_state[:reloading_for_upgrade] = updated_state.delete(:reloading_for_preload_upgrade)
130
+ message << "upgraded :reloading_for_preload_upgrade to :reloading_for_upgrade"
131
+ end
132
+ end
133
+
111
134
  default = store.default_state
112
- added_keys = default.keys - old_state.keys
113
- deleted_keys = old_state.keys - default.keys
114
- return [updated_state, nil] if added_keys.length == 0 && deleted_keys.length == 0
135
+ added_keys = default.keys - updated_state.keys
136
+ deleted_keys = updated_state.keys - default.keys
137
+ return [updated_state, message.first] if added_keys.length == 0 && deleted_keys.length == 0
115
138
 
116
139
  added_keys.each {|key| updated_state[key] = default[key]}
117
140
  deleted_keys.each {|key| updated_state.delete(key)}
118
141
 
119
- message = []
120
142
  message << "adding default values for #{added_keys.inspect}"
121
143
  message << "deleting values for #{deleted_keys.inspect}"
122
144
  message = "State format for #{store_name} has changed: #{message.join(', ')}"
@@ -329,6 +351,61 @@ module Einhorn
329
351
  end
330
352
  end
331
353
 
354
+ # Construct and a command and args that can be used to re-exec
355
+ # Einhorn for upgrades.
356
+ def self.upgrade_commandline(einhorn_flags=[])
357
+ cmdline = []
358
+ if Einhorn::State.reexec_commandline
359
+ cmdline += Einhorn::State.reexec_commandline
360
+ else
361
+ cmdline << Einhorn::TransientState.script_name
362
+ end
363
+ cmdline += einhorn_flags
364
+ cmdline << '--'
365
+ cmdline += Einhorn::State.cmd
366
+ [cmdline[0], cmdline[1..-1]]
367
+ end
368
+
369
+ # Returns true if a reload of the einhorn master via re-execing is
370
+ # not likely to be completely unsafe (that is, the new process's
371
+ # environment won't prevent it from loading its code on exec).
372
+ def self.can_safely_reload?
373
+ upgrade_sentinel = fork do
374
+ Einhorn::TransientState.whatami = :upgrade_sentinel
375
+ Einhorn.initialize_reload_environment
376
+ Einhorn::Compat.exec(*Einhorn.upgrade_commandline(['--upgrade-check']))
377
+ end
378
+ Process.wait(upgrade_sentinel)
379
+ $?.exitstatus.zero?
380
+ end
381
+
382
+ # Set up the environment for reloading the einhorn master:
383
+ # 1. Clear the current process's environment,
384
+ # 2. Set it to the environmment at startup
385
+ # 3. Delete all variables marked to be dropped via `--drop-env-var`
386
+ #
387
+ # This method is safe to call in the master only before `exec`ing
388
+ # something.
389
+ def self.initialize_reload_environment
390
+ ENV.clear
391
+ ENV.update(Einhorn::TransientState.environ)
392
+ Einhorn::State.drop_environment_variables.each do |var|
393
+ ENV.delete(var)
394
+ end
395
+ end
396
+
397
+ # Log info about the environment as observed by ruby on
398
+ # startup. Currently, this means the bundler and rbenv versions.
399
+ def self.dump_environment_info
400
+ log_info("Running under Ruby #{RUBY_VERSION}", :environment)
401
+ log_info("Rbenv ruby version: #{ENV['RBENV_VERSION']}", :environment) if ENV['RBENV_VERSION']
402
+ begin
403
+ bundler_gem = Gem::Specification.find_by_name('bundler')
404
+ log_info("Using Bundler #{bundler_gem.version.to_s}", :environment)
405
+ rescue Gem::LoadError
406
+ end
407
+ end
408
+
332
409
  def self.run
333
410
  Einhorn::Command::Interface.init
334
411
  Einhorn::Event.init
@@ -353,9 +430,9 @@ module Einhorn
353
430
  preload
354
431
 
355
432
  # In the middle of upgrading
356
- if Einhorn::State.reloading_for_preload_upgrade
433
+ if Einhorn::State.reloading_for_upgrade
357
434
  Einhorn::Command.upgrade_workers
358
- Einhorn::State.reloading_for_preload_upgrade = false
435
+ Einhorn::State.reloading_for_upgrade = false
359
436
  end
360
437
 
361
438
  while Einhorn::State.respawn || Einhorn::State.children.size > 0
@@ -0,0 +1,26 @@
1
+ require 'bundler/setup'
2
+ require 'socket'
3
+ require 'einhorn/worker'
4
+
5
+ def einhorn_main
6
+ $stderr.puts "Worker starting up!"
7
+ serv = Socket.for_fd(ENV['EINHORN_FD_0'].to_i)
8
+ $stderr.puts "Worker has a socket"
9
+ Einhorn::Worker.ack!
10
+ $stderr.puts "Worker sent ack to einhorn"
11
+ $stdout.puts "Environment from #{Process.pid} is: #{ENV.inspect}"
12
+ while true
13
+ s, addrinfo = serv.accept
14
+ $stderr.puts "Worker got a socket!"
15
+ output = ""
16
+ ARGV.each do |variable_to_write|
17
+ output += ENV[variable_to_write].to_s
18
+ end
19
+ s.write(output)
20
+ s.flush
21
+ s.close
22
+ $stderr.puts "Worker closed its socket"
23
+ end
24
+ end
25
+
26
+ einhorn_main if $0 == __FILE__
@@ -0,0 +1,22 @@
1
+ require 'bundler/setup'
2
+ require 'socket'
3
+ require 'einhorn/worker'
4
+
5
+ def einhorn_main
6
+ version = File.read(File.join(File.dirname(__FILE__), "version"))
7
+ $stderr.puts "Worker starting up!"
8
+ serv = Socket.for_fd(ENV['EINHORN_FD_0'].to_i)
9
+ $stderr.puts "Worker has a socket"
10
+ Einhorn::Worker.ack!
11
+ $stderr.puts "Worker sent ack to einhorn"
12
+ while true
13
+ s, addrinfo = serv.accept
14
+ $stderr.puts "Worker got a socket!"
15
+ s.write(version)
16
+ s.flush
17
+ s.close
18
+ $stderr.puts "Worker closed its socket"
19
+ end
20
+ end
21
+
22
+ einhorn_main if $0 == __FILE__
@@ -0,0 +1,139 @@
1
+ require 'subprocess'
2
+ require 'timeout'
3
+ require 'tmpdir'
4
+
5
+ module Helpers
6
+ module EinhornHelpers
7
+ def einhorn_code_dir
8
+ File.expand_path('../../../../', File.dirname(__FILE__))
9
+ end
10
+
11
+ def default_einhorn_command
12
+ ['bundle', 'exec', File.expand_path('bin/einhorn', einhorn_code_dir)]
13
+ end
14
+
15
+ def with_running_einhorn(cmdline, options = {})
16
+ options = options.dup
17
+ einhorn_command = options.delete(:einhorn_command) { default_einhorn_command }
18
+ expected_exit_code = options.delete(:expected_exit_code) { nil }
19
+ output_callback = options.delete(:output_callback) { nil }
20
+
21
+ stdout, stderr = "", ""
22
+ communicator = nil
23
+ process = Bundler.with_clean_env do
24
+ default_options = {
25
+ :stdout => Subprocess::PIPE,
26
+ :stderr => Subprocess::PIPE,
27
+ :stdin => '/dev/null',
28
+ :cwd => einhorn_code_dir
29
+ }
30
+ Subprocess::Process.new(Array(einhorn_command) + cmdline, default_options.merge(options))
31
+ end
32
+
33
+ status = nil
34
+ begin
35
+ communicator = Thread.new do
36
+ begin
37
+ stdout, stderr = process.communicate
38
+ rescue Errno::ECHILD
39
+ # It's dead, and we're not getting anything. This is
40
+ # peaceful.
41
+ end
42
+ end
43
+ yield(process) if block_given?
44
+ rescue Exception => e
45
+ unless (status = process.poll) && status.exited?
46
+ process.terminate
47
+ end
48
+ raise
49
+ ensure
50
+ unless (status = process.poll) && status.exited?
51
+ begin
52
+ Timeout.timeout(10) do # (Argh, I'm so sorry)
53
+ status = process.wait
54
+ end
55
+ rescue Timeout::Error
56
+ $stderr.puts "Could not get Einhorn to quit within 10 seconds, killing it forcefully..."
57
+ process.send_signal("KILL")
58
+ status = process.wait
59
+ rescue Errno::ECHILD
60
+ # Process is dead!
61
+ end
62
+ end
63
+ communicator.join
64
+ output_callback.call(stdout, stderr) if output_callback
65
+ end
66
+ end
67
+
68
+ def einhornsh(commandline, options = {})
69
+ Subprocess.check_call(%W{bundle exec #{File.expand_path('bin/einhornsh')}} + commandline,
70
+ {
71
+ :stdin => '/dev/null',
72
+ :stdout => '/dev/null',
73
+ :stderr => '/dev/null'
74
+ }.merge(options))
75
+ end
76
+
77
+ def fixture_path(name)
78
+ File.expand_path(File.join('../fixtures', name), File.dirname(__FILE__))
79
+ end
80
+
81
+ # Creates a new temporary directory with the initial contents from
82
+ # test/integration/_lib/fixtures/{name} and returns the path to
83
+ # it. The contents of this directory are temporary and can be
84
+ # safely overwritten.
85
+ def prepare_fixture_directory(name)
86
+ @fixtured_dirs ||= Set.new
87
+ new_dir = Dir.mktmpdir(name)
88
+ @fixtured_dirs << new_dir
89
+ FileUtils.cp_r(File.join(fixture_path(name), '.'), new_dir)
90
+
91
+ new_dir
92
+ end
93
+
94
+ def cleanup_fixtured_directories
95
+ (@fixtured_dirs || []).each { |dir| FileUtils.rm_rf(dir) }
96
+ end
97
+
98
+ def find_free_port(host='127.0.0.1')
99
+ open_port = TCPServer.new(host, 0)
100
+ open_port.addr[1]
101
+ ensure
102
+ open_port.close
103
+ end
104
+
105
+ def wait_for_open_port
106
+ max_retries = 50
107
+ begin
108
+ read_from_port
109
+ rescue Errno::ECONNREFUSED
110
+ max_retries -= 1
111
+ if max_retries <= 0
112
+ raise
113
+ else
114
+ sleep 0.1
115
+ retry
116
+ end
117
+ end
118
+ end
119
+
120
+
121
+ def read_from_port
122
+ ewouldblock = RUBY_VERSION >= '1.9.0' ? IO::WaitWritable : Errno::EINPROGRESS
123
+ socket = Socket.new(Socket::PF_INET, Socket::SOCK_STREAM, 0)
124
+ sockaddr = Socket.pack_sockaddr_in(@port, '127.0.0.1')
125
+ begin
126
+ socket.connect_nonblock(sockaddr)
127
+ rescue ewouldblock
128
+ IO.select(nil, [socket], [], 5)
129
+ begin
130
+ socket.connect_nonblock(sockaddr)
131
+ rescue Errno::EISCONN
132
+ end
133
+ end
134
+ socket.read.chomp
135
+ ensure
136
+ socket.close if socket
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,4 @@
1
+ module Helpers
2
+ end
3
+
4
+ require(File.expand_path('helpers/einhorn_helpers', File.dirname(__FILE__)))
@@ -0,0 +1,6 @@
1
+ require(File.expand_path('../_lib', File.dirname(__FILE__)))
2
+ require(File.expand_path('_lib/helpers', File.dirname(__FILE__)))
3
+
4
+ class EinhornIntegrationTestCase < EinhornTestCase
5
+
6
+ end
@@ -0,0 +1,31 @@
1
+ require(File.expand_path('_lib', File.dirname(__FILE__)))
2
+
3
+ class StartupTest < EinhornIntegrationTestCase
4
+ include Helpers::EinhornHelpers
5
+
6
+ describe 'when invoked without args' do
7
+ it 'prints usage and exits with 1' do
8
+ assert_raises(Subprocess::NonZeroExit) do
9
+ Subprocess.check_call(default_einhorn_command,
10
+ :stdout => Subprocess::PIPE,
11
+ :stderr => Subprocess::PIPE) do |einhorn|
12
+ stdout, stderr = einhorn.communicate
13
+ assert_match(/\A## Usage/, stdout)
14
+ assert_equal(1, einhorn.wait.exitstatus)
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ describe 'when invoked with --upgrade-check' do
21
+ it 'successfully exits' do
22
+ Subprocess.check_call(default_einhorn_command + %w[--upgrade-check],
23
+ :stdout => Subprocess::PIPE,
24
+ :stderr => Subprocess::PIPE) do |einhorn|
25
+ stdout, stderr = einhorn.communicate
26
+ status = einhorn.wait
27
+ assert_equal(0, status.exitstatus)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,128 @@
1
+ require(File.expand_path('_lib', File.dirname(__FILE__)))
2
+ require 'socket'
3
+
4
+ class UpgradeTests < EinhornIntegrationTestCase
5
+ include Helpers::EinhornHelpers
6
+
7
+ describe 'when upgrading a running einhorn without preloading' do
8
+ before do
9
+ @dir = prepare_fixture_directory('upgrade_project')
10
+ @port = find_free_port
11
+ @server_program = File.join(@dir, "upgrading_server.rb")
12
+ @socket_path = File.join(@dir, "einhorn.sock")
13
+ end
14
+ after { cleanup_fixtured_directories }
15
+
16
+ it 'can restart' do
17
+ File.open(File.join(@dir, "version"), 'w') { |f| f.write("0") }
18
+ with_running_einhorn(%W{einhorn -m manual -b 127.0.0.1:#{@port} -d #{@socket_path} -- ruby #{@server_program}}) do |process|
19
+ wait_for_open_port
20
+ assert_equal("0", read_from_port, "Should report the initial version")
21
+
22
+ File.open(File.join(@dir, "version"), 'w') { |f| f.write("1") }
23
+ einhornsh(%W{-d #{@socket_path} -e upgrade})
24
+ assert_equal("1", read_from_port, "Should report the upgraded version")
25
+
26
+ process.terminate
27
+ end
28
+ end
29
+ end
30
+
31
+ describe 'handling environments on upgrade' do
32
+ before do
33
+ @dir = prepare_fixture_directory('env_printer')
34
+ @port = find_free_port
35
+ @server_program = File.join(@dir, "env_printer.rb")
36
+ @socket_path = File.join(@dir, "einhorn.sock")
37
+ end
38
+ after { cleanup_fixtured_directories }
39
+
40
+ describe 'when running with --reexec-as' do
41
+ it 'preserves environment variables across restarts' do
42
+ # exec the new einhorn with the same environment:
43
+ reexec_cmdline = 'env VAR=a bundle exec einhorn'
44
+
45
+ with_running_einhorn(%W{einhorn -m manual -b 127.0.0.1:#{@port} --reexec-as=#{reexec_cmdline} -d #{@socket_path} -- ruby #{@server_program} VAR},
46
+ :env => ENV.to_hash.merge({'VAR' => 'a'})) do |process|
47
+
48
+ wait_for_open_port
49
+ einhornsh(%W{-d #{@socket_path} -e upgrade})
50
+ assert_equal("a", read_from_port, "Should report the upgraded version")
51
+
52
+ process.terminate
53
+ end
54
+ end
55
+
56
+ describe 'without preloading' do
57
+ it 'can update environment variables when the reexec command line says to' do
58
+ # exec the new einhorn with the same environment:
59
+ reexec_cmdline = 'env VAR=b OINK=b bundle exec einhorn'
60
+
61
+ with_running_einhorn(%W{einhorn -m manual -b 127.0.0.1:#{@port} --reexec-as=#{reexec_cmdline} -d #{@socket_path} -- ruby #{@server_program} VAR},
62
+ :env => ENV.to_hash.merge({'VAR' => 'a'})) do |process|
63
+
64
+ wait_for_open_port
65
+ einhornsh(%W{-d #{@socket_path} -e upgrade})
66
+ assert_equal("b", read_from_port, "Should report the upgraded version")
67
+
68
+ process.terminate
69
+ end
70
+ end
71
+ end
72
+
73
+ describe 'with preloading' do
74
+ it 'can update environment variables on preloaded code when the reexec command line says to' do
75
+ # exec the new einhorn with the same environment:
76
+ reexec_cmdline = 'env VAR=b OINK=b bundle exec einhorn'
77
+
78
+ with_running_einhorn(%W{einhorn -m manual -p #{@server_program} -b 127.0.0.1:#{@port} --reexec-as=#{reexec_cmdline} -d #{@socket_path} -- ruby #{@server_program} VAR},
79
+ :env => ENV.to_hash.merge({'VAR' => 'a'})) do |process|
80
+
81
+ wait_for_open_port
82
+ einhornsh(%W{-d #{@socket_path} -e upgrade})
83
+ assert_equal("b", read_from_port, "Should report the upgraded version")
84
+
85
+ process.terminate
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+
92
+ describe 'when invoked with --drop-env-var' do
93
+ before do
94
+ @dir = prepare_fixture_directory('env_printer')
95
+ @port = find_free_port
96
+ @server_program = File.join(@dir, "env_printer.rb")
97
+ @socket_path = File.join(@dir, "einhorn.sock")
98
+ end
99
+ after { cleanup_fixtured_directories }
100
+
101
+ it %{removes the variable from its children's environment} do
102
+ with_running_einhorn(%W{einhorn -m manual -b 127.0.0.1:#{@port} --drop-env-var=VAR -d #{@socket_path} -- ruby #{@server_program} VAR},
103
+ :env => ENV.to_hash.merge({'VAR' => 'a'})) do |process|
104
+ wait_for_open_port
105
+ assert_equal("a", read_from_port, "Should report $VAR initially")
106
+
107
+ einhornsh(%W{-d #{@socket_path} -e upgrade})
108
+ assert_equal("", read_from_port, "Should have dropped the variable post-upgrade")
109
+
110
+ process.terminate
111
+ end
112
+ end
113
+
114
+ it %{causes an upgrade with --reexec-as to not clobber the new environment} do
115
+ reexec_cmdline = 'env VAR2=b bundle exec einhorn'
116
+ with_running_einhorn(%W{einhorn -m manual -b 127.0.0.1:#{@port} --drop-env-var=VAR1 --drop-env-var=VAR2 -d #{@socket_path} --reexec-as=#{reexec_cmdline} -- ruby #{@server_program} VAR1 VAR2},
117
+ :env => ENV.to_hash.merge({'VAR1' => 'a', 'VAR2' => 'a'})) do |process|
118
+ wait_for_open_port
119
+ assert_equal("aa", read_from_port, "Should report both $VAR1 and $VAR2 initially")
120
+
121
+ einhornsh(%W{-d #{@socket_path} -e upgrade})
122
+ assert_equal("b", read_from_port, "Should have dropped $VAR1 post-upgrade and re-set $VAR2")
123
+
124
+ process.terminate
125
+ end
126
+ end
127
+ end
128
+ end
metadata CHANGED
@@ -1,20 +1,18 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: einhorn
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.7
5
- prerelease:
4
+ version: 0.6.0
6
5
  platform: ruby
7
6
  authors:
8
7
  - Greg Brockman
9
8
  autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2014-04-05 00:00:00.000000000 Z
11
+ date: 2014-08-06 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: rake
16
15
  requirement: !ruby/object:Gem::Requirement
17
- none: false
18
16
  requirements:
19
17
  - - ! '>='
20
18
  - !ruby/object:Gem::Version
@@ -22,7 +20,6 @@ dependencies:
22
20
  type: :development
23
21
  prerelease: false
24
22
  version_requirements: !ruby/object:Gem::Requirement
25
- none: false
26
23
  requirements:
27
24
  - - ! '>='
28
25
  - !ruby/object:Gem::Version
@@ -30,7 +27,6 @@ dependencies:
30
27
  - !ruby/object:Gem::Dependency
31
28
  name: pry
32
29
  requirement: !ruby/object:Gem::Requirement
33
- none: false
34
30
  requirements:
35
31
  - - ! '>='
36
32
  - !ruby/object:Gem::Version
@@ -38,7 +34,6 @@ dependencies:
38
34
  type: :development
39
35
  prerelease: false
40
36
  version_requirements: !ruby/object:Gem::Requirement
41
- none: false
42
37
  requirements:
43
38
  - - ! '>='
44
39
  - !ruby/object:Gem::Version
@@ -46,7 +41,6 @@ dependencies:
46
41
  - !ruby/object:Gem::Dependency
47
42
  name: minitest
48
43
  requirement: !ruby/object:Gem::Requirement
49
- none: false
50
44
  requirements:
51
45
  - - <
52
46
  - !ruby/object:Gem::Version
@@ -54,7 +48,6 @@ dependencies:
54
48
  type: :development
55
49
  prerelease: false
56
50
  version_requirements: !ruby/object:Gem::Requirement
57
- none: false
58
51
  requirements:
59
52
  - - <
60
53
  - !ruby/object:Gem::Version
@@ -62,7 +55,6 @@ dependencies:
62
55
  - !ruby/object:Gem::Dependency
63
56
  name: mocha
64
57
  requirement: !ruby/object:Gem::Requirement
65
- none: false
66
58
  requirements:
67
59
  - - ~>
68
60
  - !ruby/object:Gem::Version
@@ -70,7 +62,6 @@ dependencies:
70
62
  type: :development
71
63
  prerelease: false
72
64
  version_requirements: !ruby/object:Gem::Requirement
73
- none: false
74
65
  requirements:
75
66
  - - ~>
76
67
  - !ruby/object:Gem::Version
@@ -78,7 +69,6 @@ dependencies:
78
69
  - !ruby/object:Gem::Dependency
79
70
  name: chalk-rake
80
71
  requirement: !ruby/object:Gem::Requirement
81
- none: false
82
72
  requirements:
83
73
  - - ! '>='
84
74
  - !ruby/object:Gem::Version
@@ -86,7 +76,20 @@ dependencies:
86
76
  type: :development
87
77
  prerelease: false
88
78
  version_requirements: !ruby/object:Gem::Requirement
89
- none: false
79
+ requirements:
80
+ - - ! '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: subprocess
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ! '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
90
93
  requirements:
91
94
  - - ! '>='
92
95
  - !ruby/object:Gem::Version
@@ -145,6 +148,13 @@ files:
145
148
  - lib/einhorn/worker.rb
146
149
  - lib/einhorn/worker_pool.rb
147
150
  - test/_lib.rb
151
+ - test/integration/_lib.rb
152
+ - test/integration/_lib/fixtures/env_printer/env_printer.rb
153
+ - test/integration/_lib/fixtures/upgrade_project/upgrading_server.rb
154
+ - test/integration/_lib/helpers.rb
155
+ - test/integration/_lib/helpers/einhorn_helpers.rb
156
+ - test/integration/startup.rb
157
+ - test/integration/upgrading.rb
148
158
  - test/unit/einhorn.rb
149
159
  - test/unit/einhorn/client.rb
150
160
  - test/unit/einhorn/command.rb
@@ -154,30 +164,36 @@ files:
154
164
  homepage: https://github.com/stripe/einhorn
155
165
  licenses:
156
166
  - MIT
167
+ metadata: {}
157
168
  post_install_message:
158
169
  rdoc_options: []
159
170
  require_paths:
160
171
  - lib
161
172
  required_ruby_version: !ruby/object:Gem::Requirement
162
- none: false
163
173
  requirements:
164
174
  - - ! '>='
165
175
  - !ruby/object:Gem::Version
166
176
  version: '0'
167
177
  required_rubygems_version: !ruby/object:Gem::Requirement
168
- none: false
169
178
  requirements:
170
179
  - - ! '>='
171
180
  - !ruby/object:Gem::Version
172
181
  version: '0'
173
182
  requirements: []
174
183
  rubyforge_project:
175
- rubygems_version: 1.8.23.2
184
+ rubygems_version: 2.2.2
176
185
  signing_key:
177
- specification_version: 3
186
+ specification_version: 4
178
187
  summary: ! 'Einhorn: the language-independent shared socket manager'
179
188
  test_files:
180
189
  - test/_lib.rb
190
+ - test/integration/_lib.rb
191
+ - test/integration/_lib/fixtures/env_printer/env_printer.rb
192
+ - test/integration/_lib/fixtures/upgrade_project/upgrading_server.rb
193
+ - test/integration/_lib/helpers.rb
194
+ - test/integration/_lib/helpers/einhorn_helpers.rb
195
+ - test/integration/startup.rb
196
+ - test/integration/upgrading.rb
181
197
  - test/unit/einhorn.rb
182
198
  - test/unit/einhorn/client.rb
183
199
  - test/unit/einhorn/command.rb