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 +15 -0
- data/README.md +5 -0
- data/README.md.in +2 -5
- data/Rakefile +0 -7
- data/bin/einhorn +13 -0
- data/einhorn.gemspec +1 -0
- data/lib/einhorn/command/interface.rb +2 -2
- data/lib/einhorn/command.rb +21 -23
- data/lib/einhorn/version.rb +1 -1
- data/lib/einhorn.rb +87 -10
- data/test/integration/_lib/fixtures/env_printer/env_printer.rb +26 -0
- data/test/integration/_lib/fixtures/upgrade_project/upgrading_server.rb +22 -0
- data/test/integration/_lib/helpers/einhorn_helpers.rb +139 -0
- data/test/integration/_lib/helpers.rb +4 -0
- data/test/integration/_lib.rb +6 -0
- data/test/integration/startup.rb +31 -0
- data/test/integration/upgrading.rb +128 -0
- metadata +33 -17
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.
|
65
|
-
|
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
|
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
|
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.
|
data/lib/einhorn/command.rb
CHANGED
@@ -171,13 +171,13 @@ module Einhorn
|
|
171
171
|
end
|
172
172
|
|
173
173
|
def self.dumpable_state
|
174
|
-
global_state = Einhorn::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.
|
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
|
-
|
225
|
-
|
226
|
-
|
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
|
230
|
-
|
231
|
-
|
232
|
-
|
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.
|
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.
|
390
|
-
Einhorn::State.
|
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
|
-
|
442
|
-
Einhorn.
|
439
|
+
else
|
440
|
+
Einhorn.log_debug("Not killing old workers, as excess is #{excess}.")
|
443
441
|
end
|
444
442
|
end
|
445
443
|
|
data/lib/einhorn/version.rb
CHANGED
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
|
-
:
|
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
|
-
|
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 -
|
113
|
-
deleted_keys =
|
114
|
-
return [updated_state,
|
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.
|
433
|
+
if Einhorn::State.reloading_for_upgrade
|
357
434
|
Einhorn::Command.upgrade_workers
|
358
|
-
Einhorn::State.
|
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,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
|
-
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-
|
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
|
-
|
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:
|
184
|
+
rubygems_version: 2.2.2
|
176
185
|
signing_key:
|
177
|
-
specification_version:
|
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
|