einhorn 0.4.2 → 0.4.3
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.
- data/.travis.yml +6 -0
- data/README.md +12 -7
- data/Rakefile +3 -0
- data/bin/einhorn +23 -8
- data/example/thin_example +4 -4
- data/example/time_server +3 -2
- data/lib/einhorn/command/interface.rb +26 -3
- data/lib/einhorn/command.rb +21 -6
- data/lib/einhorn/event.rb +8 -6
- data/lib/einhorn/version.rb +1 -1
- data/lib/einhorn/worker.rb +19 -10
- data/lib/einhorn/worker_pool.rb +9 -3
- data/lib/einhorn.rb +47 -3
- data/test/unit/einhorn/client.rb +35 -13
- data/test/unit/einhorn/worker_pool.rb +39 -0
- data/test/unit/einhorn.rb +20 -0
- metadata +70 -74
data/.travis.yml
ADDED
data/README.md
CHANGED
@@ -80,12 +80,17 @@ Each address is specified as an ip/port pair, possibly accompanied by options:
|
|
80
80
|
ADDR := (IP:PORT)[<,OPT>...]
|
81
81
|
|
82
82
|
In the worker process, the opened file descriptors will be represented
|
83
|
-
as
|
84
|
-
|
85
|
-
options were provided in)
|
83
|
+
as file descriptor numbers in a series of environment variables named
|
84
|
+
EINHORN_FD_1, EINHORN_FD_2, etc. (respecting the order that the `-b`
|
85
|
+
options were provided in), with the total number of file descriptors
|
86
|
+
in the EINHORN_FD_COUNT environment variable:
|
86
87
|
|
87
|
-
|
88
|
-
|
88
|
+
EINHORN_FD_1="6" # 127.0.0.1:1234
|
89
|
+
EINHORN_FD_COUNT="1"
|
90
|
+
|
91
|
+
EINHORN_FD_1="6" # 127.0.0.1:1234,r
|
92
|
+
EINHORN_FD_2="7" # 127.0.0.1:1235
|
93
|
+
EINHORN_FD_COUNT="2"
|
89
94
|
|
90
95
|
Valid opts are:
|
91
96
|
|
@@ -98,7 +103,7 @@ You can for example run:
|
|
98
103
|
|
99
104
|
Which will run 4 copies of
|
100
105
|
|
101
|
-
|
106
|
+
EINHORN_FD_1=6 EINHORN_FD_COUNT=1 example/time_server
|
102
107
|
|
103
108
|
Where file descriptor 6 is a server socket bound to `127.0.0.1:2345`
|
104
109
|
and with `SO_REUSEADDR` set. It is then your application's job to
|
@@ -191,7 +196,7 @@ pass `-c <name>`.
|
|
191
196
|
|
192
197
|
### Options
|
193
198
|
|
194
|
-
-b, --bind ADDR Bind an address and add the corresponding FD
|
199
|
+
-b, --bind ADDR Bind an address and add the corresponding FD via the environment
|
195
200
|
-c, --command-name CMD_NAME Set the command name in ps to this value
|
196
201
|
-d, --socket-path PATH Where to open the Einhorn command socket
|
197
202
|
-e, --pidfile PIDFILE Where to write out the Einhorn pidfile
|
data/Rakefile
CHANGED
data/bin/einhorn
CHANGED
@@ -56,12 +56,17 @@ Each address is specified as an ip/port pair, possibly accompanied by options:
|
|
56
56
|
ADDR := (IP:PORT)[<,OPT>...]
|
57
57
|
|
58
58
|
In the worker process, the opened file descriptors will be represented
|
59
|
-
as
|
60
|
-
|
61
|
-
options were provided in)
|
59
|
+
as file descriptor numbers in a series of environment variables named
|
60
|
+
EINHORN_FD_1, EINHORN_FD_2, etc. (respecting the order that the `-b`
|
61
|
+
options were provided in), with the total number of file descriptors
|
62
|
+
in the EINHORN_FD_COUNT environment variable:
|
62
63
|
|
63
|
-
|
64
|
-
|
64
|
+
EINHORN_FD_1="6" # 127.0.0.1:1234
|
65
|
+
EINHORN_FD_COUNT="1"
|
66
|
+
|
67
|
+
EINHORN_FD_1="6" # 127.0.0.1:1234,r
|
68
|
+
EINHORN_FD_2="7" # 127.0.0.1:1235
|
69
|
+
EINHORN_FD_COUNT="2"
|
65
70
|
|
66
71
|
Valid opts are:
|
67
72
|
|
@@ -74,7 +79,7 @@ You can for example run:
|
|
74
79
|
|
75
80
|
Which will run 4 copies of
|
76
81
|
|
77
|
-
|
82
|
+
EINHORN_FD_1=6 EINHORN_FD_COUNT=1 example/time_server
|
78
83
|
|
79
84
|
Where file descriptor 6 is a server socket bound to `127.0.0.1:2345`
|
80
85
|
and with `SO_REUSEADDR` set. It is then your application's job to
|
@@ -184,7 +189,7 @@ if true # $0 == __FILE__
|
|
184
189
|
Einhorn::TransientState.environ = ENV.to_hash
|
185
190
|
|
186
191
|
optparse = OptionParser.new do |opts|
|
187
|
-
opts.on('-b ADDR', '--bind ADDR', 'Bind an address and add the corresponding FD
|
192
|
+
opts.on('-b ADDR', '--bind ADDR', 'Bind an address and add the corresponding FD via the environment') do |addr|
|
188
193
|
unless addr =~ /\A([^:]+):(\d+)((?:,\w+)*)\Z/
|
189
194
|
raise "Invalid value for #{addr.inspect}: bind address must be of the form address:port[,flags...]"
|
190
195
|
end
|
@@ -258,7 +263,7 @@ if true # $0 == __FILE__
|
|
258
263
|
end
|
259
264
|
|
260
265
|
opts.on('-q', '--quiet', 'Make output quiet (can be reconfigured on the fly)') do
|
261
|
-
Einhorn::Command.
|
266
|
+
Einhorn::Command.quieter(false)
|
262
267
|
end
|
263
268
|
|
264
269
|
opts.on('-s', '--seconds N', 'Number of seconds to wait until respawning') do |b|
|
@@ -269,6 +274,16 @@ if true # $0 == __FILE__
|
|
269
274
|
Einhorn::Command.louder(false)
|
270
275
|
end
|
271
276
|
|
277
|
+
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|
|
278
|
+
master, worker, renice_cmd = nice.split(':')
|
279
|
+
master = Integer(master) if master
|
280
|
+
worker = Integer(worker) if worker
|
281
|
+
|
282
|
+
Einhorn::State.nice[:master] = master if master
|
283
|
+
Einhorn::State.nice[:worker] = worker if worker
|
284
|
+
Einhorn::State.nice[:renice_cmd] = renice_cmd if renice_cmd
|
285
|
+
end
|
286
|
+
|
272
287
|
opts.on('--with-state-fd STATE', '[Internal option] With file descriptor containing state') do |fd|
|
273
288
|
Einhorn::TransientState.stateful = true
|
274
289
|
read = IO.for_fd(Integer(fd))
|
data/example/thin_example
CHANGED
@@ -27,10 +27,9 @@ end
|
|
27
27
|
|
28
28
|
def einhorn_main
|
29
29
|
puts "Called with #{ARGV.inspect}"
|
30
|
+
fd_count = Einhorn::Worker.einhorn_fd_count
|
30
31
|
|
31
|
-
|
32
|
-
|
33
|
-
unless einhorn_fds
|
32
|
+
unless fd_count > 0
|
34
33
|
raise "Need to call with at least one bound socket. Try running 'einhorn -b 127.0.0.1:5000,r,n -b 127.0.0.1:5001,r,n #{$0}' and then running 'curl 127.0.0.1:5000' or 'curl 127.0.0.1:5001'"
|
35
34
|
end
|
36
35
|
|
@@ -41,7 +40,8 @@ def einhorn_main
|
|
41
40
|
Einhorn::Worker.ack!
|
42
41
|
|
43
42
|
EventMachine.run do
|
44
|
-
|
43
|
+
(0...fd_count).each do |i|
|
44
|
+
sock = Einhorn::Worker.socket!(i)
|
45
45
|
srv = Thin::Server.new(sock, App.new(i), :reuse => true)
|
46
46
|
srv.start
|
47
47
|
end
|
data/example/time_server
CHANGED
@@ -2,7 +2,8 @@
|
|
2
2
|
#
|
3
3
|
# A simple example showing how to use Einhorn's shared-socket
|
4
4
|
# features. Einhorn translates the (addr:port[,flags...]) bind spec in
|
5
|
-
# into a file descriptor number in the
|
5
|
+
# into a file descriptor number in the EINHORN_FD_# environment
|
6
|
+
# variables.
|
6
7
|
#
|
7
8
|
# Invoke through Einhorn as
|
8
9
|
#
|
@@ -15,7 +16,7 @@ require 'rubygems'
|
|
15
16
|
require 'einhorn/worker'
|
16
17
|
|
17
18
|
def einhorn_main
|
18
|
-
puts "Called with ENV['
|
19
|
+
puts "Called with ENV['EINHORN_FD_1']: #{ENV['EINHORN_FD_1']}"
|
19
20
|
|
20
21
|
fd_num = Einhorn::Worker.socket!
|
21
22
|
socket = Socket.for_fd(fd_num)
|
@@ -292,9 +292,6 @@ EOF
|
|
292
292
|
# buffer and lost upon reload.
|
293
293
|
send_message(conn, 'Reloading, as commanded')
|
294
294
|
Einhorn::Command.reload
|
295
|
-
|
296
|
-
# Reload should not return
|
297
|
-
raise "Not reachable"
|
298
295
|
end
|
299
296
|
|
300
297
|
command 'inc', 'Increment the number of Einhorn child processes' do
|
@@ -361,6 +358,32 @@ EOF
|
|
361
358
|
"Einhorn is going down! #{response}"
|
362
359
|
end
|
363
360
|
|
361
|
+
command 'config', 'Merge in a new set of config options. (Note: this will likely be subsumed by config file reloading at some point.)' do |conn, request|
|
362
|
+
args = request['args']
|
363
|
+
if message = validate_args(args)
|
364
|
+
next message
|
365
|
+
end
|
366
|
+
|
367
|
+
unless args.length > 0
|
368
|
+
next 'Must pass in a YAML-encoded hash'
|
369
|
+
end
|
370
|
+
|
371
|
+
begin
|
372
|
+
# We do the joining so people don't need to worry about quoting
|
373
|
+
parsed = YAML.load(args.join(' '))
|
374
|
+
rescue ArgumentError => e
|
375
|
+
next 'Could not parse argument. Must be a YAML-encoded hash'
|
376
|
+
end
|
377
|
+
|
378
|
+
unless parsed.kind_of?(Hash)
|
379
|
+
next "Parsed argument is a #{parsed.class}, not a hash"
|
380
|
+
end
|
381
|
+
|
382
|
+
Einhorn::State.state.merge!(parsed)
|
383
|
+
|
384
|
+
"Successfully merged in config: #{parsed.inspect}"
|
385
|
+
end
|
386
|
+
|
364
387
|
def self.validate_args(args)
|
365
388
|
return 'No args provided' unless args
|
366
389
|
return 'Args must be an array' unless args.kind_of?(Array)
|
data/lib/einhorn/command.rb
CHANGED
@@ -192,13 +192,17 @@ module Einhorn
|
|
192
192
|
end
|
193
193
|
write.close
|
194
194
|
|
195
|
-
Einhorn::Event.uninit
|
196
|
-
|
197
195
|
# Reload the original environment
|
198
196
|
ENV.clear
|
199
197
|
ENV.update(Einhorn::TransientState.environ)
|
200
198
|
|
201
|
-
|
199
|
+
begin
|
200
|
+
exec [Einhorn::TransientState.script_name, Einhorn::TransientState.script_name], *(['--with-state-fd', read.fileno.to_s, '--'] + Einhorn::State.cmd)
|
201
|
+
rescue SystemCallError => e
|
202
|
+
Einhorn.log_error("Could not reload! Attempting to continue. Error was: #{e}")
|
203
|
+
Einhorn::State.reloading_for_preload_upgrade = false
|
204
|
+
read.close
|
205
|
+
end
|
202
206
|
end
|
203
207
|
|
204
208
|
def self.spinup(cmd=nil)
|
@@ -206,6 +210,7 @@ module Einhorn
|
|
206
210
|
if Einhorn::TransientState.preloaded
|
207
211
|
pid = fork do
|
208
212
|
Einhorn::TransientState.whatami = :worker
|
213
|
+
prepare_child_process
|
209
214
|
|
210
215
|
Einhorn.log_info('About to tear down Einhorn state and run einhorn_main')
|
211
216
|
Einhorn::Command::Interface.uninit
|
@@ -218,6 +223,7 @@ module Einhorn
|
|
218
223
|
else
|
219
224
|
pid = fork do
|
220
225
|
Einhorn::TransientState.whatami = :worker
|
226
|
+
prepare_child_process
|
221
227
|
|
222
228
|
Einhorn.log_info("About to exec #{cmd.inspect}")
|
223
229
|
# Here's the only case where cloexec would help. Since we
|
@@ -260,12 +266,21 @@ module Einhorn
|
|
260
266
|
Einhorn::TransientState.socket_handles << socket
|
261
267
|
ENV['EINHORN_SOCK_FD'] = socket.fileno.to_s
|
262
268
|
end
|
263
|
-
|
264
|
-
|
265
|
-
|
269
|
+
|
270
|
+
ENV['EINHORN_FD_COUNT'] = Einhorn::State.bind_fds.length.to_s
|
271
|
+
Einhorn::State.bind_fds.each_with_index {|fd, i| ENV["EINHORN_FD_#{i}"] = fd.to_s}
|
272
|
+
|
273
|
+
# EINHORN_FDS is deprecated. It was originally an attempt to
|
274
|
+
# match Upstart's nominal internal support for space-separated
|
275
|
+
# FD lists, but nobody uses that in practice, and it makes
|
276
|
+
# finding individual FDs more difficult
|
266
277
|
ENV['EINHORN_FDS'] = Einhorn::State.bind_fds.map(&:to_s).join(' ')
|
267
278
|
end
|
268
279
|
|
280
|
+
def self.prepare_child_process
|
281
|
+
Einhorn.renice_self
|
282
|
+
end
|
283
|
+
|
269
284
|
def self.full_upgrade
|
270
285
|
if Einhorn::State.path && !Einhorn::State.reloading_for_preload_upgrade
|
271
286
|
reload_for_preload_upgrade
|
data/lib/einhorn/event.rb
CHANGED
@@ -8,20 +8,22 @@ module Einhorn
|
|
8
8
|
@@writeable = {}
|
9
9
|
@@timers = {}
|
10
10
|
|
11
|
+
def self.cloexec!(fd)
|
12
|
+
fd.fcntl(Fcntl::F_SETFD, fd.fcntl(Fcntl::F_GETFD) | Fcntl::FD_CLOEXEC)
|
13
|
+
end
|
14
|
+
|
11
15
|
def self.init
|
12
16
|
readable, writeable = IO.pipe
|
13
17
|
@@loopbreak_reader = LoopBreaker.open(readable)
|
14
18
|
@@loopbreak_writer = writeable
|
15
|
-
end
|
16
19
|
|
17
|
-
|
18
|
-
|
19
|
-
@@loopbreak_reader.close
|
20
|
-
@@loopbreak_writer.close
|
20
|
+
cloexec!(readable)
|
21
|
+
cloexec!(writeable)
|
21
22
|
end
|
22
23
|
|
23
24
|
def self.close_all
|
24
|
-
|
25
|
+
@@loopbreak_reader.close
|
26
|
+
@@loopbreak_writer.close
|
25
27
|
(@@readable.values + @@writeable.values).each do |descriptors|
|
26
28
|
descriptors.each do |descriptor|
|
27
29
|
descriptor.close
|
data/lib/einhorn/version.rb
CHANGED
data/lib/einhorn/worker.rb
CHANGED
@@ -80,30 +80,39 @@ module Einhorn
|
|
80
80
|
|
81
81
|
def self.socket(number=nil)
|
82
82
|
number ||= 0
|
83
|
-
|
84
|
-
fds ? fds[number] : nil
|
83
|
+
einhorn_fd(number)
|
85
84
|
end
|
86
85
|
|
87
86
|
def self.socket!(number=nil)
|
88
87
|
number ||= 0
|
89
88
|
|
90
|
-
unless
|
91
|
-
raise "No
|
89
|
+
unless count = einhorn_fd_count
|
90
|
+
raise "No EINHORN_FD_COUNT provided in environment. Are you running under Einhorn?"
|
92
91
|
end
|
93
92
|
|
94
|
-
unless number <
|
95
|
-
raise "Only #{
|
93
|
+
unless number < count
|
94
|
+
raise "Only #{count} FDs available, but FD #{number} was requested"
|
96
95
|
end
|
97
96
|
|
98
|
-
|
97
|
+
unless fd = einhorn_fd(number)
|
98
|
+
raise "No EINHORN_FD_#{number} provided in environment. That's pretty weird"
|
99
|
+
end
|
100
|
+
|
101
|
+
fd
|
99
102
|
end
|
100
103
|
|
101
|
-
def self.
|
102
|
-
unless
|
104
|
+
def self.einhorn_fd(n)
|
105
|
+
unless raw_fd = ENV["EINHORN_FD_#{n}"]
|
103
106
|
return nil
|
104
107
|
end
|
108
|
+
Integer(raw_fd)
|
109
|
+
end
|
105
110
|
|
106
|
-
|
111
|
+
def self.einhorn_fd_count
|
112
|
+
unless raw_count = ENV['EINHORN_FD_COUNT']
|
113
|
+
return 0
|
114
|
+
end
|
115
|
+
Integer(raw_count)
|
107
116
|
end
|
108
117
|
|
109
118
|
# Call this to handle graceful shutdown requests to your app.
|
data/lib/einhorn/worker_pool.rb
CHANGED
@@ -1,17 +1,23 @@
|
|
1
1
|
module Einhorn
|
2
2
|
module WorkerPool
|
3
|
+
def self.workers_with_state
|
4
|
+
Einhorn::State.children.select do |pid, spec|
|
5
|
+
spec[:type] == :worker
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
3
9
|
def self.workers
|
4
|
-
|
10
|
+
workers_with_state.map {|pid, _| pid}
|
5
11
|
end
|
6
12
|
|
7
13
|
def self.unsignaled_workers
|
8
|
-
|
14
|
+
workers_with_state.select do |pid, spec|
|
9
15
|
spec[:signaled].length == 0
|
10
16
|
end.map {|pid, _| pid}
|
11
17
|
end
|
12
18
|
|
13
19
|
def self.modern_workers_with_state
|
14
|
-
|
20
|
+
workers_with_state.select do |pid, spec|
|
15
21
|
spec[:version] == Einhorn::State.version
|
16
22
|
end
|
17
23
|
end
|
data/lib/einhorn.rb
CHANGED
@@ -57,7 +57,8 @@ module Einhorn
|
|
57
57
|
:pidfile => nil,
|
58
58
|
:lockfile => nil,
|
59
59
|
:consecutive_deaths_before_ack => 0,
|
60
|
-
:last_upgraded => nil
|
60
|
+
:last_upgraded => nil,
|
61
|
+
:nice => {:master => nil, :worker => nil, :renice_cmd => '/usr/bin/renice'}
|
61
62
|
}
|
62
63
|
end
|
63
64
|
end
|
@@ -81,10 +82,33 @@ module Einhorn
|
|
81
82
|
|
82
83
|
def self.restore_state(state)
|
83
84
|
parsed = YAML.load(state)
|
84
|
-
|
85
|
+
updated_state, message = update_state(parsed[:state])
|
86
|
+
Einhorn::State.state = updated_state
|
85
87
|
Einhorn::Event.restore_persistent_descriptors(parsed[:persistent_descriptors])
|
86
|
-
# Do this after setting state so verbosity is
|
88
|
+
# Do this after setting state so verbosity is right
|
87
89
|
Einhorn.log_info("Using loaded state: #{parsed.inspect}")
|
90
|
+
Einhorn.log_info(message) if message
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.update_state(old_state)
|
94
|
+
# TODO: handle format updates somehow? (probably need to write
|
95
|
+
# special-case code for each)
|
96
|
+
updated_state = old_state.dup
|
97
|
+
default = Einhorn::State.default_state
|
98
|
+
added_keys = default.keys - old_state.keys
|
99
|
+
deleted_keys = old_state.keys - default.keys
|
100
|
+
return [updated_state, nil] if added_keys.length == 0 && deleted_keys.length == 0
|
101
|
+
|
102
|
+
added_keys.each {|key| updated_state[key] = default[key]}
|
103
|
+
deleted_keys.each {|key| updated_state.delete(key)}
|
104
|
+
|
105
|
+
message = []
|
106
|
+
message << "adding default values for #{added_keys.inspect}"
|
107
|
+
message << "deleting values for #{deleted_keys.inspect}"
|
108
|
+
message = "State format has changed: #{message.join(', ')}"
|
109
|
+
|
110
|
+
# Can't print yet, since state hasn't been set, so we pass along the message.
|
111
|
+
[updated_state, message]
|
88
112
|
end
|
89
113
|
|
90
114
|
def self.print_state
|
@@ -222,6 +246,25 @@ module Einhorn
|
|
222
246
|
Einhorn::State.cmd_name ? "ruby #{Einhorn::State.cmd_name}" : Einhorn::State.orig_cmd.join(' ')
|
223
247
|
end
|
224
248
|
|
249
|
+
def self.renice_self
|
250
|
+
whatami = Einhorn::TransientState.whatami
|
251
|
+
return unless nice = Einhorn::State.nice[whatami]
|
252
|
+
pid = $$
|
253
|
+
|
254
|
+
unless nice.kind_of?(Fixnum)
|
255
|
+
raise "Nice must be a fixnum: #{nice.inspect}"
|
256
|
+
end
|
257
|
+
|
258
|
+
# Explicitly don't shellescape the renice command
|
259
|
+
cmd = "#{Einhorn::State.nice[:renice_cmd]} #{nice} -p #{pid}"
|
260
|
+
log_info("Running #{cmd.inspect} to renice self to level #{nice}")
|
261
|
+
`#{cmd}`
|
262
|
+
unless $?.exitstatus == 0
|
263
|
+
# TODO: better error handling?
|
264
|
+
log_error("Renice command exited with status: #{$?.inspect}, but continuing on anyway.")
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
225
268
|
def self.socketify_env!
|
226
269
|
Einhorn::State.bind.each do |host, port, flags|
|
227
270
|
fd = bind(host, port, flags)
|
@@ -267,6 +310,7 @@ module Einhorn
|
|
267
310
|
end
|
268
311
|
|
269
312
|
set_master_ps_name
|
313
|
+
renice_self
|
270
314
|
preload
|
271
315
|
|
272
316
|
# In the middle of upgrading
|
data/test/unit/einhorn/client.rb
CHANGED
@@ -3,46 +3,68 @@ require File.expand_path(File.join(File.dirname(__FILE__), '../../test_helper'))
|
|
3
3
|
require 'einhorn'
|
4
4
|
|
5
5
|
class ClientTest < Test::Unit::TestCase
|
6
|
-
def
|
6
|
+
def unserialized_message
|
7
7
|
{:foo => ['%bar', '%baz']}
|
8
8
|
end
|
9
9
|
|
10
|
-
def
|
10
|
+
def serialized_1_8
|
11
11
|
"--- %0A:foo: %0A- \"%25bar\"%0A- \"%25baz\"%0A\n"
|
12
12
|
end
|
13
13
|
|
14
|
+
def serialized_1_9
|
15
|
+
"---%0A:foo:%0A- ! '%25bar'%0A- ! '%25baz'%0A\n"
|
16
|
+
end
|
17
|
+
|
14
18
|
context "when sending a message" do
|
15
19
|
should "write a serialized line" do
|
16
20
|
socket = mock
|
17
|
-
socket.expects(:write).with
|
18
|
-
|
21
|
+
socket.expects(:write).with do |write|
|
22
|
+
write == serialized_1_8 || write == serialized_1_9
|
23
|
+
end
|
24
|
+
Einhorn::Client::Transport.send_message(socket, unserialized_message)
|
19
25
|
end
|
20
26
|
end
|
21
27
|
|
22
28
|
context "when receiving a message" do
|
23
|
-
should "deserialize a single line" do
|
29
|
+
should "deserialize a single 1.8-style line" do
|
30
|
+
socket = mock
|
31
|
+
socket.expects(:readline).returns(serialized_1_8)
|
32
|
+
result = Einhorn::Client::Transport.receive_message(socket)
|
33
|
+
assert_equal(result, unserialized_message)
|
34
|
+
end
|
35
|
+
|
36
|
+
should "deserialize a single 1.9-style line" do
|
24
37
|
socket = mock
|
25
|
-
socket.expects(:readline).returns(
|
38
|
+
socket.expects(:readline).returns(serialized_1_9)
|
26
39
|
result = Einhorn::Client::Transport.receive_message(socket)
|
27
|
-
assert_equal(result,
|
40
|
+
assert_equal(result, unserialized_message)
|
28
41
|
end
|
29
42
|
end
|
30
43
|
|
31
44
|
context "when {de,}serializing a message" do
|
32
45
|
should "serialize and escape a message as expected" do
|
33
|
-
actual = Einhorn::Client::Transport.serialize_message(
|
34
|
-
|
46
|
+
actual = Einhorn::Client::Transport.serialize_message(unserialized_message)
|
47
|
+
assert(actual == serialized_1_8 || actual == serialized_1_9, "Actual message is #{actual.inspect}")
|
35
48
|
end
|
36
49
|
|
37
|
-
should "deserialize and unescape a message as expected" do
|
38
|
-
actual = Einhorn::Client::Transport.deserialize_message(
|
39
|
-
assert_equal(
|
50
|
+
should "deserialize and unescape a 1.8-style message as expected" do
|
51
|
+
actual = Einhorn::Client::Transport.deserialize_message(serialized_1_8)
|
52
|
+
assert_equal(unserialized_message, actual)
|
53
|
+
end
|
54
|
+
|
55
|
+
should "deserialize and unescape a 1.9-style message as expected" do
|
56
|
+
actual = Einhorn::Client::Transport.deserialize_message(serialized_1_9)
|
57
|
+
assert_equal(unserialized_message, actual)
|
40
58
|
end
|
41
59
|
|
42
60
|
should "raise an error when deserializing invalid YAML" do
|
43
61
|
invalid_serialized = "-%0A\t-"
|
44
|
-
|
62
|
+
expected = [ArgumentError]
|
63
|
+
expected << Psych::SyntaxError if defined?(Psych::SyntaxError) # 1.9
|
64
|
+
|
65
|
+
begin
|
45
66
|
Einhorn::Client::Transport.deserialize_message(invalid_serialized)
|
67
|
+
rescue *expected
|
46
68
|
end
|
47
69
|
end
|
48
70
|
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), '../../test_helper'))
|
2
|
+
|
3
|
+
require 'einhorn'
|
4
|
+
|
5
|
+
class WorkerPoolTest < Test::Unit::TestCase
|
6
|
+
def stub_children
|
7
|
+
Einhorn::State.stubs(:children).returns(
|
8
|
+
1234 => {:type => :worker, :signaled => Set.new(['INT'])},
|
9
|
+
1235 => {:type => :state_passer},
|
10
|
+
1236 => {:type => :worker, :signaled => Set.new}
|
11
|
+
)
|
12
|
+
end
|
13
|
+
|
14
|
+
context "#workers_with_state" do
|
15
|
+
setup do
|
16
|
+
stub_children
|
17
|
+
end
|
18
|
+
|
19
|
+
should "select only the workers" do
|
20
|
+
workers_with_state = Einhorn::WorkerPool.workers_with_state
|
21
|
+
# Sort only needed for Ruby 1.8
|
22
|
+
assert_equal([
|
23
|
+
[1234, {:type => :worker, :signaled => Set.new(['INT'])}],
|
24
|
+
[1236, {:type => :worker, :signaled => Set.new}]
|
25
|
+
], workers_with_state.sort)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
context "#unsignaled_workers" do
|
30
|
+
setup do
|
31
|
+
stub_children
|
32
|
+
end
|
33
|
+
|
34
|
+
should "selects unsignaled workers" do
|
35
|
+
unsignaled_workers = Einhorn::WorkerPool.unsignaled_workers
|
36
|
+
assert_equal([1236], unsignaled_workers)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/test/unit/einhorn.rb
CHANGED
@@ -35,4 +35,24 @@ class EinhornTest < Test::Unit::TestCase
|
|
35
35
|
assert_equal(['foo', '--opt=10', '10'], cmd)
|
36
36
|
end
|
37
37
|
end
|
38
|
+
|
39
|
+
context '.update_state' do
|
40
|
+
should 'correctly update keys to match new default state hash' do
|
41
|
+
Einhorn::State.stubs(:default_state).returns(:baz => 23, :foo => 1)
|
42
|
+
old_state = {:foo => 2, :bar => 2}
|
43
|
+
|
44
|
+
updated_state, message = Einhorn.update_state(old_state)
|
45
|
+
assert_equal({:baz => 23, :foo => 2}, updated_state)
|
46
|
+
assert_match(/State format has changed/, message)
|
47
|
+
end
|
48
|
+
|
49
|
+
should 'not change the state if the format has not changed' do
|
50
|
+
Einhorn::State.stubs(:default_state).returns(:baz => 23, :foo => 1)
|
51
|
+
old_state = {:baz => 14, :foo => 1234}
|
52
|
+
|
53
|
+
updated_state, message = Einhorn.update_state(old_state)
|
54
|
+
assert_equal({:baz => 14, :foo => 1234}, updated_state)
|
55
|
+
assert(message.nil?)
|
56
|
+
end
|
57
|
+
end
|
38
58
|
end
|
metadata
CHANGED
@@ -1,76 +1,78 @@
|
|
1
|
-
--- !ruby/object:Gem::Specification
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
2
|
name: einhorn
|
3
|
-
version: !ruby/object:Gem::Version
|
4
|
-
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.4.3
|
5
5
|
prerelease:
|
6
|
-
segments:
|
7
|
-
- 0
|
8
|
-
- 4
|
9
|
-
- 2
|
10
|
-
version: 0.4.2
|
11
6
|
platform: ruby
|
12
|
-
authors:
|
7
|
+
authors:
|
13
8
|
- Greg Brockman
|
14
9
|
autorequire:
|
15
10
|
bindir: bin
|
16
11
|
cert_chain: []
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
- !ruby/object:Gem::Dependency
|
12
|
+
date: 2013-03-14 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
21
15
|
name: rake
|
22
|
-
|
23
|
-
requirement: &id001 !ruby/object:Gem::Requirement
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
24
17
|
none: false
|
25
|
-
requirements:
|
26
|
-
- -
|
27
|
-
- !ruby/object:Gem::Version
|
28
|
-
|
29
|
-
segments:
|
30
|
-
- 0
|
31
|
-
version: "0"
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
32
22
|
type: :development
|
33
|
-
version_requirements: *id001
|
34
|
-
- !ruby/object:Gem::Dependency
|
35
|
-
name: shoulda
|
36
23
|
prerelease: false
|
37
|
-
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
25
|
none: false
|
39
|
-
requirements:
|
40
|
-
- -
|
41
|
-
- !ruby/object:Gem::Version
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: shoulda
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
46
38
|
type: :development
|
47
|
-
version_requirements: *id002
|
48
|
-
- !ruby/object:Gem::Dependency
|
49
|
-
name: mocha
|
50
39
|
prerelease: false
|
51
|
-
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
41
|
none: false
|
53
|
-
requirements:
|
54
|
-
- -
|
55
|
-
- !ruby/object:Gem::Version
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: mocha
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
60
54
|
type: :development
|
61
|
-
|
62
|
-
|
63
|
-
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
description: Einhorn makes it easy to run multiple instances of an application server,
|
63
|
+
all listening on the same port. You can also seamlessly restart your workers without
|
64
|
+
dropping any requests. Einhorn requires minimal application-level support, making
|
65
|
+
it easy to use with an existing project.
|
66
|
+
email:
|
64
67
|
- gdb@stripe.com
|
65
|
-
executables:
|
68
|
+
executables:
|
66
69
|
- einhorn
|
67
70
|
- einhornsh
|
68
71
|
extensions: []
|
69
|
-
|
70
72
|
extra_rdoc_files: []
|
71
|
-
|
72
|
-
files:
|
73
|
+
files:
|
73
74
|
- .gitignore
|
75
|
+
- .travis.yml
|
74
76
|
- Gemfile
|
75
77
|
- History.txt
|
76
78
|
- LICENSE
|
@@ -104,43 +106,37 @@ files:
|
|
104
106
|
- test/unit/einhorn/command.rb
|
105
107
|
- test/unit/einhorn/command/interface.rb
|
106
108
|
- test/unit/einhorn/event.rb
|
109
|
+
- test/unit/einhorn/worker_pool.rb
|
107
110
|
homepage: https://github.com/stripe/einhorn
|
108
111
|
licenses: []
|
109
|
-
|
110
112
|
post_install_message:
|
111
113
|
rdoc_options: []
|
112
|
-
|
113
|
-
require_paths:
|
114
|
+
require_paths:
|
114
115
|
- lib
|
115
|
-
required_ruby_version: !ruby/object:Gem::Requirement
|
116
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
116
117
|
none: false
|
117
|
-
requirements:
|
118
|
-
- -
|
119
|
-
- !ruby/object:Gem::Version
|
120
|
-
|
121
|
-
|
122
|
-
- 0
|
123
|
-
version: "0"
|
124
|
-
required_rubygems_version: !ruby/object:Gem::Requirement
|
118
|
+
requirements:
|
119
|
+
- - ! '>='
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
version: '0'
|
122
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
125
123
|
none: false
|
126
|
-
requirements:
|
127
|
-
- -
|
128
|
-
- !ruby/object:Gem::Version
|
129
|
-
|
130
|
-
segments:
|
131
|
-
- 0
|
132
|
-
version: "0"
|
124
|
+
requirements:
|
125
|
+
- - ! '>='
|
126
|
+
- !ruby/object:Gem::Version
|
127
|
+
version: '0'
|
133
128
|
requirements: []
|
134
|
-
|
135
129
|
rubyforge_project:
|
136
|
-
rubygems_version: 1.8.
|
130
|
+
rubygems_version: 1.8.23
|
137
131
|
signing_key:
|
138
132
|
specification_version: 3
|
139
|
-
summary:
|
140
|
-
test_files:
|
133
|
+
summary: ! 'Einhorn: the language-independent shared socket manager'
|
134
|
+
test_files:
|
141
135
|
- test/test_helper.rb
|
142
136
|
- test/unit/einhorn.rb
|
143
137
|
- test/unit/einhorn/client.rb
|
144
138
|
- test/unit/einhorn/command.rb
|
145
139
|
- test/unit/einhorn/command/interface.rb
|
146
140
|
- test/unit/einhorn/event.rb
|
141
|
+
- test/unit/einhorn/worker_pool.rb
|
142
|
+
has_rdoc:
|