einhorn 0.5.7 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
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