einhorn 0.4.2 → 0.4.3

Sign up to get free protection for your applications and to get access to all the features.
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.8.7
4
+ - 1.9.2
5
+ - 1.9.3
6
+ - ree
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 a space-separated list of file descriptor numbers in the
84
- EINHORN_FDS environment variable (respecting the order that the `-b`
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
- EINHORN_FDS="6" # 127.0.0.1:1234
88
- EINHORN_FDS="6 7" # 127.0.0.1:1234,r 127.0.0.1:1235
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
- EINHORN_FDS=6 example/time_server
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 to EINHORN_FDS
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
@@ -17,3 +17,6 @@ Rake::TestTask.new do |t|
17
17
  t.verbose = true
18
18
  t.test_files = FileList['test/unit/**/*.rb']
19
19
  end
20
+
21
+ task :default => :test do
22
+ end
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 a space-separated list of file descriptor numbers in the
60
- EINHORN_FDS environment variable (respecting the order that the `-b`
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
- EINHORN_FDS="6" # 127.0.0.1:1234
64
- EINHORN_FDS="6 7" # 127.0.0.1:1234,r 127.0.0.1:1235
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
- EINHORN_FDS=6 example/time_server
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 to EINHORN_FDS') do |addr|
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.louder(false)
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
- einhorn_fds = Einhorn::Worker.einhorn_fds
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
- einhorn_fds.each_with_index do |sock, i|
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 EINHORN_FDS environment variable.
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['EINHORN_FDS']: #{ENV['EINHORN_FDS']}"
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)
@@ -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
- exec [Einhorn::TransientState.script_name, Einhorn::TransientState.script_name], *(['--with-state-fd', read.fileno.to_s, '--'] + Einhorn::State.cmd)
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
- # Try to match Upstart's internal support for space-separated FD
264
- # lists. (I don't think anyone actually uses that functionality,
265
- # but seems reasonable enough.)
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
- def self.uninit
18
- # These don't need to persist across Einhorn reloads, so let's not keep.
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
- uninit
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
@@ -1,3 +1,3 @@
1
1
  module Einhorn
2
- VERSION = '0.4.2'
2
+ VERSION = '0.4.3'
3
3
  end
@@ -80,30 +80,39 @@ module Einhorn
80
80
 
81
81
  def self.socket(number=nil)
82
82
  number ||= 0
83
- fds = einhorn_fds
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 fds = einhorn_fds
91
- raise "No EINHORN_FDS provided in environment. Are you running under Einhorn?"
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 < fds.length
95
- raise "Only #{fds.length} FDs available, but FD #{number} was requested"
93
+ unless number < count
94
+ raise "Only #{count} FDs available, but FD #{number} was requested"
96
95
  end
97
96
 
98
- fds[number]
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.einhorn_fds
102
- unless raw_fds = ENV['EINHORN_FDS']
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
- raw_fds.split(' ').map {|fd| Integer(fd)}
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.
@@ -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
- Einhorn::State.children.keys
10
+ workers_with_state.map {|pid, _| pid}
5
11
  end
6
12
 
7
13
  def self.unsignaled_workers
8
- Einhorn::State.children.select do |pid, spec|
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
- Einhorn::State.children.select do |pid, spec|
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
- Einhorn::State.state = parsed[:state]
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 right9
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
@@ -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 message
6
+ def unserialized_message
7
7
  {:foo => ['%bar', '%baz']}
8
8
  end
9
9
 
10
- def serialized
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(serialized)
18
- Einhorn::Client::Transport.send_message(socket, message)
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(serialized)
38
+ socket.expects(:readline).returns(serialized_1_9)
26
39
  result = Einhorn::Client::Transport.receive_message(socket)
27
- assert_equal(result, message)
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(message)
34
- assert_equal(serialized, actual)
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(serialized)
39
- assert_equal(message, actual)
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
- assert_raises(ArgumentError) do
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
- hash: 11
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
- date: 2012-10-11 00:00:00 Z
19
- dependencies:
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
- prerelease: false
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
- hash: 3
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
- requirement: &id002 !ruby/object:Gem::Requirement
24
+ version_requirements: !ruby/object:Gem::Requirement
38
25
  none: false
39
- requirements:
40
- - - ">="
41
- - !ruby/object:Gem::Version
42
- hash: 3
43
- segments:
44
- - 0
45
- version: "0"
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
- requirement: &id003 !ruby/object:Gem::Requirement
40
+ version_requirements: !ruby/object:Gem::Requirement
52
41
  none: false
53
- requirements:
54
- - - ">="
55
- - !ruby/object:Gem::Version
56
- hash: 3
57
- segments:
58
- - 0
59
- version: "0"
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
- version_requirements: *id003
62
- description: Einhorn makes it easy to run multiple instances of an application server, all listening on the same port. You can also seamlessly restart your workers without dropping any requests. Einhorn requires minimal application-level support, making it easy to use with an existing project.
63
- email:
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
- hash: 3
121
- segments:
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
- hash: 3
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.24
130
+ rubygems_version: 1.8.23
137
131
  signing_key:
138
132
  specification_version: 3
139
- summary: "Einhorn: the language-independent shared socket manager"
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: