einhorn 0.3.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,15 @@
1
+ === 0.4.0 2012-09-26
2
+
3
+ * Switch the command-socket protocol from line-oriented JSON to
4
+ line-oriented YAML. If you've written your own client to communicate
5
+ with the einhorn command-socket, you will need to update it. (The
6
+ bundled einhorn/client is already updated.)
7
+ * Have the 'state' command return a YAML'd state rather than a #pretty_inspect'd
8
+ state
9
+ * Made einhornsh script-friendly
10
+ * Switched over to address specification via -b option and environment variables;
11
+ deprecated but didn't remove old interface.
12
+ * Allow einhorn to signal a given worker multiple times
13
+ * Add 'signal' and 'die' commands to Einhornsh
14
+ * Add exponential backoff to spinup if new processes are dying before being acked
15
+ * Add last_upgraded field to State
data/README.md CHANGED
@@ -68,15 +68,25 @@ You can communicate your running Einhorn process via `einhornsh`:
68
68
  ### Server sockets
69
69
 
70
70
  If your process is a server and listens on one or more sockets,
71
- Einhorn can open these sockets and pass them to the workers. Program
72
- arguments of the form
71
+ Einhorn can open these sockets and pass them to the workers. You can
72
+ specify the addresses to bind by passing one or more `-b ADDR`
73
+ arguments:
73
74
 
74
- srv:(IP:PORT)[<,OPT>...]
75
- --MY-OPT=srv:(IP:PORT)[<,OPT>...]
75
+ einhorn -b 127.0.0.1:1234 my-command
76
+ einhorn -b 127.0.0.1:1234,r -b 127.0.0.1:1235 my-command
77
+
78
+ Each address is specified as an ip/port pair, possibly accompanied by options:
79
+
80
+ ADDR := (IP:PORT)[<,OPT>...]
81
+
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):
86
+
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
76
89
 
77
- Will be interpreted as a request to open a server socket bound to
78
- IP:PORT. The argument will be replaced with `FD` and `---MY-OPT=FD`,
79
- respectively, where `FD` is the file descriptor number of the socket.
80
90
  Valid opts are:
81
91
 
82
92
  r, so_reuseaddr: set SO_REUSEADDR on the server socket
@@ -84,11 +94,11 @@ Valid opts are:
84
94
 
85
95
  You can for example run:
86
96
 
87
- $ einhorn -m manual -n 4 example/time_server srv:127.0.0.1:2345,r
97
+ $ einhorn -b 127.0.0.1:2345,r -m manual -n 4 -- example/time_server
88
98
 
89
99
  Which will run 4 copies of
90
100
 
91
- example/time_server 6
101
+ EINHORN_FDS=6 example/time_server
92
102
 
93
103
  Where file descriptor 6 is a server socket bound to `127.0.0.1:2345`
94
104
  and with `SO_REUSEADDR` set. It is then your application's job to
@@ -102,6 +112,10 @@ run). Einhorn relies on file permissions to ensure that no malicious
102
112
  users can gain access. Run with a `-d DIRECTORY` to change the
103
113
  directory where the socket will live.
104
114
 
115
+ Note that the command socket uses a line-oriented YAML protocol, and
116
+ you should ensure you trust clients to send arbitrary YAML messages
117
+ into your process.
118
+
105
119
  ### Seamless upgrades
106
120
 
107
121
  You can cause your code to be seamlessly reloaded by upgrading the
@@ -140,9 +154,9 @@ string
140
154
  to the UNIX socket pointed to by the environment variable
141
155
  `EINHORN_SOCK_PATH`. (Be sure to include a trailing newline.)
142
156
 
143
- To make things even easier, you can pass a `-b` to Einhorn, in which
157
+ To make things even easier, you can pass a `-g` to Einhorn, in which
144
158
  case you just need to `write()` the above message to the open file
145
- descriptor pointed to by `EINHORN_FD`.
159
+ descriptor pointed to by `EINHORN_SOCK_FD`.
146
160
 
147
161
  (See `lib/einhorn/worker.rb` for details of these and other socket
148
162
  discovery mechanisms.)
@@ -167,8 +181,8 @@ To use preloading, just give Einhorn a `-p PATH_TO_CODE`, and make
167
181
  sure you've defined an `einhorn_main` method.
168
182
 
169
183
  In order to maximize compatibility, we've worked to minimize Einhorn's
170
- dependencies. At the moment Einhorn imports 'rubygems' and 'json'. (If
171
- these turn out to be issues, we could probably find a workaround.)
184
+ dependencies. It has no dependencies outside of the Ruby standard
185
+ library.
172
186
 
173
187
  ### Command name
174
188
 
@@ -177,11 +191,12 @@ pass `-c <name>`.
177
191
 
178
192
  ### Options
179
193
 
180
- -b, --command-socket-as-fd Leave the command socket open as a file descriptor, passed in the EINHORN_FD environment variable. This allows your worker processes to ACK without needing to know where on the filesystem the command socket lives.
194
+ -b, --bind ADDR Bind an address and add the corresponding FD to EINHORN_FDS
181
195
  -c, --command-name CMD_NAME Set the command name in ps to this value
182
196
  -d, --socket-path PATH Where to open the Einhorn command socket
183
197
  -e, --pidfile PIDFILE Where to write out the Einhorn pidfile
184
198
  -f, --lockfile LOCKFILE Where to store the Einhorn lockfile
199
+ -g, --command-socket-as-fd Leave the command socket open as a file descriptor, passed in the EINHORN_SOCK_FD environment variable. This allows your worker processes to ACK without needing to know where on the filesystem the command socket lives.
185
200
  -h, --help Display this message
186
201
  -k, --kill-children-on-exit If Einhorn exits unexpectedly, gracefully kill all its children
187
202
  -l, --backlog N Connection backlog (assuming this is a server)
data/bin/einhorn CHANGED
@@ -44,15 +44,25 @@ You can communicate your running Einhorn process via `einhornsh`:
44
44
  ### Server sockets
45
45
 
46
46
  If your process is a server and listens on one or more sockets,
47
- Einhorn can open these sockets and pass them to the workers. Program
48
- arguments of the form
47
+ Einhorn can open these sockets and pass them to the workers. You can
48
+ specify the addresses to bind by passing one or more `-b ADDR`
49
+ arguments:
49
50
 
50
- srv:(IP:PORT)[<,OPT>...]
51
- --MY-OPT=srv:(IP:PORT)[<,OPT>...]
51
+ einhorn -b 127.0.0.1:1234 my-command
52
+ einhorn -b 127.0.0.1:1234,r -b 127.0.0.1:1235 my-command
53
+
54
+ Each address is specified as an ip/port pair, possibly accompanied by options:
55
+
56
+ ADDR := (IP:PORT)[<,OPT>...]
57
+
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):
62
+
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
52
65
 
53
- Will be interpreted as a request to open a server socket bound to
54
- IP:PORT. The argument will be replaced with `FD` and `---MY-OPT=FD`,
55
- respectively, where `FD` is the file descriptor number of the socket.
56
66
  Valid opts are:
57
67
 
58
68
  r, so_reuseaddr: set SO_REUSEADDR on the server socket
@@ -60,11 +70,11 @@ Valid opts are:
60
70
 
61
71
  You can for example run:
62
72
 
63
- $ einhorn -m manual -n 4 example/time_server srv:127.0.0.1:2345,r
73
+ $ einhorn -b 127.0.0.1:2345,r -m manual -n 4 -- example/time_server
64
74
 
65
75
  Which will run 4 copies of
66
76
 
67
- example/time_server 6
77
+ EINHORN_FDS=6 example/time_server
68
78
 
69
79
  Where file descriptor 6 is a server socket bound to `127.0.0.1:2345`
70
80
  and with `SO_REUSEADDR` set. It is then your application's job to
@@ -78,6 +88,10 @@ run). Einhorn relies on file permissions to ensure that no malicious
78
88
  users can gain access. Run with a `-d DIRECTORY` to change the
79
89
  directory where the socket will live.
80
90
 
91
+ Note that the command socket uses a line-oriented YAML protocol, and
92
+ you should ensure you trust clients to send arbitrary YAML messages
93
+ into your process.
94
+
81
95
  ### Seamless upgrades
82
96
 
83
97
  You can cause your code to be seamlessly reloaded by upgrading the
@@ -116,9 +130,9 @@ string
116
130
  to the UNIX socket pointed to by the environment variable
117
131
  `EINHORN_SOCK_PATH`. (Be sure to include a trailing newline.)
118
132
 
119
- To make things even easier, you can pass a `-b` to Einhorn, in which
133
+ To make things even easier, you can pass a `-g` to Einhorn, in which
120
134
  case you just need to `write()` the above message to the open file
121
- descriptor pointed to by `EINHORN_FD`.
135
+ descriptor pointed to by `EINHORN_SOCK_FD`.
122
136
 
123
137
  (See `lib/einhorn/worker.rb` for details of these and other socket
124
138
  discovery mechanisms.)
@@ -143,8 +157,8 @@ To use preloading, just give Einhorn a `-p PATH_TO_CODE`, and make
143
157
  sure you've defined an `einhorn_main` method.
144
158
 
145
159
  In order to maximize compatibility, we've worked to minimize Einhorn's
146
- dependencies. At the moment Einhorn imports 'rubygems' and 'json'. (If
147
- these turn out to be issues, we could probably find a workaround.)
160
+ dependencies. It has no dependencies outside of the Ruby standard
161
+ library.
148
162
 
149
163
  ### Command name
150
164
 
@@ -167,14 +181,21 @@ end
167
181
  if true # $0 == __FILE__
168
182
  Einhorn::TransientState.script_name = $0
169
183
  Einhorn::TransientState.argv = ARGV.dup
184
+ Einhorn::TransientState.environ = ENV.to_hash
170
185
 
171
186
  optparse = OptionParser.new do |opts|
172
- opts.on('-b', '--command-socket-as-fd', 'Leave the command socket open as a file descriptor, passed in the EINHORN_FD environment variable. This allows your worker processes to ACK without needing to know where on the filesystem the command socket lives.') do
173
- Einhorn::State.command_socket_as_fd = true
187
+ opts.on('-b ADDR', '--bind ADDR', 'Bind an address and add the corresponding FD to EINHORN_FDS') do |addr|
188
+ unless addr =~ /\A([^:]+):(\d+)((?:,\w+)*)\Z/
189
+ raise "Invalid value for #{addr.inspect}: bind address must be of the form address:port[,flags...]"
190
+ end
191
+
192
+ host = $1
193
+ port = Integer($2)
194
+ flags = $3.split(',').select {|flag| flag.length > 0}.map {|flag| flag.downcase}
195
+ Einhorn::State.bind << [host, port, flags]
174
196
  end
175
197
 
176
198
  opts.on('-c CMD_NAME', '--command-name CMD_NAME', 'Set the command name in ps to this value') do |cmd_name|
177
- Einhorn::TransientState.stateful = false
178
199
  Einhorn::State.cmd_name = cmd_name
179
200
  end
180
201
 
@@ -190,6 +211,10 @@ if true # $0 == __FILE__
190
211
  Einhorn::State.lockfile = lockfile
191
212
  end
192
213
 
214
+ opts.on('-g', '--command-socket-as-fd', 'Leave the command socket open as a file descriptor, passed in the EINHORN_SOCK_FD environment variable. This allows your worker processes to ACK without needing to know where on the filesystem the command socket lives.') do
215
+ Einhorn::State.command_socket_as_fd = true
216
+ end
217
+
193
218
  opts.on('-h', '--help', 'Display this message') do
194
219
  opts.banner = Einhorn::Executable.einhorn_usage(true)
195
220
  puts opts
@@ -201,8 +226,6 @@ if true # $0 == __FILE__
201
226
  end
202
227
 
203
228
  opts.on('-l', '--backlog N', 'Connection backlog (assuming this is a server)') do |b|
204
- raise "Cannot pass options if stateful" if Einhorn::TransientState.stateful
205
- Einhorn::TransientState.stateful = false
206
229
  Einhorn::State.config[:backlog] = b.to_i
207
230
  end
208
231
 
@@ -227,13 +250,10 @@ if true # $0 == __FILE__
227
250
  end
228
251
 
229
252
  opts.on('-n', '--number N', 'Number of copies to spin up') do |n|
230
- raise "Cannot pass options if stateful" if Einhorn::TransientState.stateful
231
- Einhorn::TransientState.stateful = false
232
253
  Einhorn::State.config[:number] = n.to_i
233
254
  end
234
255
 
235
256
  opts.on('-p PATH', '--preload PATH', 'Load this code into memory, and fork but do not exec upon spawn. Must define an "einhorn_main" method') do |path|
236
- Einhorn::TransientState.stateful = false
237
257
  Einhorn::State.path = path
238
258
  end
239
259
 
@@ -242,8 +262,6 @@ if true # $0 == __FILE__
242
262
  end
243
263
 
244
264
  opts.on('-s', '--seconds N', 'Number of seconds to wait until respawning') do |b|
245
- raise "Cannot pass options if stateful" if Einhorn::TransientState.stateful
246
- Einhorn::TransientState.stateful = false
247
265
  Einhorn::State.config[:seconds] = s.to_i
248
266
  end
249
267
 
@@ -252,9 +270,6 @@ if true # $0 == __FILE__
252
270
  end
253
271
 
254
272
  opts.on('--with-state-fd STATE', '[Internal option] With file descriptor containing state') do |fd|
255
- raise "Cannot be stateful if options are passed" unless Einhorn::TransientState.stateful.nil?
256
- Einhorn::TransientState.stateful = true
257
-
258
273
  read = IO.for_fd(Integer(fd))
259
274
  state = read.read
260
275
  read.close
data/bin/einhornsh CHANGED
@@ -3,6 +3,7 @@ require 'logger'
3
3
  require 'optparse'
4
4
 
5
5
  require 'readline'
6
+ require 'shellwords'
6
7
 
7
8
  require 'rubygems'
8
9
  require 'einhorn'
@@ -15,35 +16,46 @@ module Einhorn
15
16
  end
16
17
 
17
18
  def run
18
- puts "Enter 'help' if you're not sure what to do."
19
- puts
20
- puts 'Type "quit" or "exit" to quit at any time'
19
+ emit("Enter 'help' if you're not sure what to do.")
20
+ emit
21
+ emit('Type "quit" or "exit" to quit at any time')
21
22
 
22
23
  while line = Readline.readline('> ', true)
23
- if ['quit', 'exit'].include?(line)
24
- puts "Goodbye!"
24
+ command, args = parse_command(line)
25
+ if ['quit', 'exit'].include?(command)
26
+ emit("Goodbye!")
25
27
  return
26
28
  end
27
29
 
28
30
  begin
29
- response = @client.command({'command' => line})
31
+ response = @client.command({'command' => command, 'args' => args})
30
32
  rescue Errno::EPIPE => e
31
- puts "einhornsh: Error communicating with Einhorn: #{e} (#{e.class})"
32
- puts "einhornsh: Attempting to reconnect..."
33
+ emit("einhornsh: Error communicating with Einhorn: #{e} (#{e.class})")
34
+ emit("einhornsh: Attempting to reconnect...")
33
35
  reconnect
34
36
 
35
37
  retry
36
38
  end
37
- puts response['message']
39
+
40
+ if response.kind_of?(Hash)
41
+ puts response['message']
42
+ else
43
+ puts "Invalid response type #{response.class}: #{response.inspect}"
44
+ end
38
45
  end
39
46
  end
40
47
 
48
+ def parse_command(line)
49
+ command, *args = Shellwords.shellsplit(line)
50
+ [command, args]
51
+ end
52
+
41
53
  def reconnect
42
54
  begin
43
55
  @client = Einhorn::Client.for_path(@path_to_socket)
44
56
  rescue Errno::ENOENT => e
45
57
  # TODO: The exit here is a biit of a layering violation.
46
- puts <<EOF
58
+ Einhorn::EinhornSH.emit(<<EOF, true)
47
59
  Could not connect to Einhorn master process:
48
60
 
49
61
  #{e}
@@ -53,12 +65,28 @@ should pass einhornsh the cmd_name (-c argument) provided to Einhorn.
53
65
  EOF
54
66
  exit(1)
55
67
  end
56
- ehlo
68
+ ehlo if interactive?
57
69
  end
58
70
 
59
71
  def ehlo
60
72
  response = @client.command('command' => 'ehlo', 'user' => ENV['USER'])
61
- puts response['message']
73
+ emit(response['message'])
74
+ end
75
+
76
+ def self.emit(message=nil, force=false)
77
+ $stderr.puts(message || '') if interactive? || force
78
+ end
79
+
80
+ def self.interactive?
81
+ $stdin.isatty
82
+ end
83
+
84
+ def emit(*args)
85
+ self.class.emit(*args)
86
+ end
87
+
88
+ def interactive?
89
+ self.class.interactive?
62
90
  end
63
91
  end
64
92
  end
@@ -75,7 +103,7 @@ as a positional argument or using `-c`. If you're running your Einhorn
75
103
  with a `-d`, provide the same argument here."
76
104
 
77
105
  opts.on('-h', '--help', 'Display this message') do
78
- puts opts
106
+ Einhorn::EinhornSH.emit(opts, true)
79
107
  exit(1)
80
108
  end
81
109
 
@@ -90,11 +118,14 @@ with a `-d`, provide the same argument here."
90
118
  optparse.parse!
91
119
 
92
120
  if ARGV.length > 1
93
- puts optparse
121
+ Einhorn::EinhornSH.emit(optparse, true)
94
122
  return 1
95
123
  end
96
124
 
97
- Signal.trap("INT") {puts; exit(0)}
125
+ Signal.trap("INT") do
126
+ Einhorn::EinhornSH.emit
127
+ exit(0)
128
+ end
98
129
 
99
130
  path_to_socket = options[:socket_path]
100
131
 
data/einhorn.gemspec CHANGED
@@ -13,8 +13,8 @@ Gem::Specification.new do |gem|
13
13
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
14
  gem.name = "einhorn"
15
15
  gem.require_paths = ["lib"]
16
- # maybe swap out for YAML? Then don't need any gems.
17
- gem.add_dependency('json')
16
+
17
+ gem.add_development_dependency('rake')
18
18
  gem.add_development_dependency('shoulda')
19
19
  gem.add_development_dependency('mocha')
20
20
  gem.version = Einhorn::VERSION
data/example/thin_example CHANGED
@@ -28,8 +28,10 @@ end
28
28
  def einhorn_main
29
29
  puts "Called with #{ARGV.inspect}"
30
30
 
31
- if ARGV.length == 0
32
- raise "Need to call with at least one argument. Try running 'einhorn #{$0} srv:127.0.0.1:5000,r,n srv:127.0.0.1:5001,r,n' and then running 'curl 127.0.0.1:5000' or 'curl 127.0.0.1:5001'"
31
+ einhorn_fds = Einhorn::Worker.einhorn_fds
32
+
33
+ unless einhorn_fds
34
+ 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'"
33
35
  end
34
36
 
35
37
  Einhorn::Worker.graceful_shutdown do
@@ -39,8 +41,7 @@ def einhorn_main
39
41
  Einhorn::Worker.ack!
40
42
 
41
43
  EventMachine.run do
42
- ARGV.each_with_index do |arg, i|
43
- sock = Integer(arg)
44
+ einhorn_fds.each_with_index do |sock, i|
44
45
  srv = Thin::Server.new(sock, App.new(i), :reuse => true)
45
46
  srv.start
46
47
  end
data/example/time_server CHANGED
@@ -1,28 +1,24 @@
1
1
  #!/usr/bin/env ruby
2
2
  #
3
3
  # A simple example showing how to use Einhorn's shared-socket
4
- # features. Einhorn translates the srv:(addr:port[,flags...]) spec in
5
- # the arg string into a file descriptor number.
4
+ # features. Einhorn translates the (addr:port[,flags...]) bind spec in
5
+ # into a file descriptor number in the EINHORN_FDS environment variable.
6
6
  #
7
7
  # Invoke through Einhorn as
8
8
  #
9
- # einhorn ./time_server srv:127.0.0.1:2345,r
9
+ # einhorn -b 127.0.0.1:2345,r ./time_server
10
10
  #
11
11
  # or, if you want to try out preloading:
12
12
  #
13
- # einhorn -p ./time_server ./time_server srv:127.0.0.1:2345,r
14
-
13
+ # einhorn -b 127.0.0.1:2345,r -p ./time_server ./time_server
15
14
  require 'rubygems'
16
15
  require 'einhorn/worker'
17
16
 
18
17
  def einhorn_main
19
- puts "Called with #{ARGV.inspect}"
20
-
21
- if ARGV.length != 1
22
- raise "Need to call with a port spec as the first argument. Try running 'einhorn #{$0} srv:127.0.0.1:2345,r' and then running 'nc 127.0.0.1 2345'"
23
- end
18
+ puts "Called with ENV['EINHORN_FDS']: #{ENV['EINHORN_FDS']}"
24
19
 
25
- socket = Socket.for_fd(Integer(ARGV[0]))
20
+ fd_num = Einhorn::Worker.socket!
21
+ socket = Socket.for_fd(fd_num)
26
22
 
27
23
  # Came up successfully, so let's set up graceful handler and ACK the
28
24
  # master.
@@ -1,8 +1,34 @@
1
- require 'json'
2
1
  require 'set'
2
+ require 'uri'
3
+ require 'yaml'
3
4
 
4
5
  module Einhorn
5
6
  class Client
7
+ # Keep this in this file so client can be loaded entirely
8
+ # standalone by user code.
9
+ module Transport
10
+ def self.send_message(socket, message)
11
+ line = serialize_message(message)
12
+ socket.write(line)
13
+ end
14
+
15
+ def self.receive_message(socket)
16
+ line = socket.readline
17
+ deserialize_message(line)
18
+ end
19
+
20
+ def self.serialize_message(message)
21
+ serialized = YAML.dump(message)
22
+ escaped = URI.escape(serialized, "%\n")
23
+ escaped + "\n"
24
+ end
25
+
26
+ def self.deserialize_message(line)
27
+ serialized = URI.unescape(line)
28
+ YAML.load(serialized)
29
+ end
30
+ end
31
+
6
32
  @@responseless_commands = Set.new(['worker:ack'])
7
33
 
8
34
  def self.for_path(path_to_socket)
@@ -20,9 +46,8 @@ module Einhorn
20
46
  end
21
47
 
22
48
  def command(command_hash)
23
- command = JSON.generate(command_hash) + "\n"
24
- write(command)
25
- recvmessage if expect_response?(command_hash)
49
+ Transport.send_message(@socket, command_hash)
50
+ Transport.receive_message(@socket) if expect_response?(command_hash)
26
51
  end
27
52
 
28
53
  def expect_response?(command_hash)
@@ -32,17 +57,5 @@ module Einhorn
32
57
  def close
33
58
  @socket.close
34
59
  end
35
-
36
- private
37
-
38
- def write(bytes)
39
- @socket.write(bytes)
40
- end
41
-
42
- # TODO: use a streaming JSON parser instead?
43
- def recvmessage
44
- line = @socket.readline
45
- JSON.parse(line)
46
- end
47
60
  end
48
61
  end
@@ -148,17 +148,17 @@ module Einhorn::Command
148
148
  ## Signals
149
149
  def self.install_handlers
150
150
  Signal.trap("INT") do
151
- Einhorn::Command.signal_all("USR2", Einhorn::State.children.keys)
151
+ Einhorn::Command.signal_all("USR2", Einhorn::WorkerPool.workers)
152
152
  Einhorn::State.respawn = false
153
153
  end
154
154
  Signal.trap("TERM") do
155
- Einhorn::Command.signal_all("TERM", Einhorn::State.children.keys)
155
+ Einhorn::Command.signal_all("TERM", Einhorn::WorkerPool.workers)
156
156
  Einhorn::State.respawn = false
157
157
  end
158
158
  # Note that quit is a bit different, in that it will actually
159
159
  # make Einhorn quit without waiting for children to exit.
160
160
  Signal.trap("QUIT") do
161
- Einhorn::Command.signal_all("QUIT", Einhorn::State.children.keys)
161
+ Einhorn::Command.signal_all("QUIT", Einhorn::WorkerPool.workers)
162
162
  Einhorn::State.respawn = false
163
163
  exit(1)
164
164
  end
@@ -166,12 +166,12 @@ module Einhorn::Command
166
166
  Signal.trap("ALRM") {Einhorn::Command.full_upgrade}
167
167
  Signal.trap("CHLD") {Einhorn::Event.break_loop}
168
168
  Signal.trap("USR2") do
169
- Einhorn::Command.signal_all("USR2", Einhorn::State.children.keys)
169
+ Einhorn::Command.signal_all("USR2", Einhorn::WorkerPool.workers)
170
170
  Einhorn::State.respawn = false
171
171
  end
172
172
  at_exit do
173
173
  if Einhorn::State.kill_children_on_exit && Einhorn::TransientState.whatami == :master
174
- Einhorn::Command.signal_all("USR2", Einhorn::State.children.keys)
174
+ Einhorn::Command.signal_all("USR2", Einhorn::WorkerPool.workers)
175
175
  Einhorn::State.respawn = false
176
176
  end
177
177
  end
@@ -201,14 +201,13 @@ module Einhorn::Command
201
201
  if response.kind_of?(String)
202
202
  response = {'message' => response}
203
203
  end
204
- message = pack_message(response)
205
- conn.write(message)
204
+ Einhorn::Client::Transport.send_message(conn, response)
206
205
  end
207
206
 
208
207
  def self.generate_response(conn, command)
209
208
  begin
210
- request = JSON.parse(command)
211
- rescue JSON::ParserError => e
209
+ request = Einhorn::Client::Transport.deserialize_message(command)
210
+ rescue ArgumentError => e
212
211
  return {
213
212
  'message' => "Could not parse command: #{e}"
214
213
  }
@@ -235,17 +234,6 @@ module Einhorn::Command
235
234
  end
236
235
  end
237
236
 
238
- def self.pack_message(message_struct)
239
- begin
240
- JSON.generate(message_struct) + "\n"
241
- rescue JSON::GeneratorError => e
242
- response = {
243
- 'message' => "Error generating JSON message for #{message_struct.inspect} (this indicates a bug): #{e}"
244
- }
245
- JSON.generate(response) + "\n"
246
- end
247
- end
248
-
249
237
  def self.command_descriptions
250
238
  command_specs = @@commands.select do |_, spec|
251
239
  spec[:description]
@@ -291,7 +279,7 @@ EOF
291
279
  end
292
280
 
293
281
  command 'state', "Get a dump of Einhorn's current state" do
294
- Einhorn::Command.dumpable_state.pretty_inspect
282
+ YAML.dump(Einhorn::Command.dumpable_state)
295
283
  end
296
284
 
297
285
  command 'reload', 'Reload Einhorn' do |conn, _|
@@ -332,5 +320,74 @@ EOF
332
320
  Einhorn::Command.full_upgrade
333
321
  nil
334
322
  end
323
+
324
+ command 'signal', 'Send one or more signals to all workers (args: SIG1 [SIG2 ...])' do |conn, request|
325
+ args = request['args']
326
+ if message = validate_args(args)
327
+ next message
328
+ end
329
+
330
+ args = normalize_signals(args)
331
+
332
+ if message = validate_signals(args)
333
+ next message
334
+ end
335
+
336
+ results = args.map do |signal|
337
+ Einhorn::Command.signal_all(signal, nil, false)
338
+ end
339
+
340
+ results.join("\n")
341
+ end
342
+
343
+ command 'die', 'Send SIGNAL (default: SIGUSR2) to all workers, stop spawning new ones, and exit once all workers die (args: [SIGNAL])' do |conn, request|
344
+ # TODO: dedup this code with signal
345
+ args = request['args']
346
+ if message = validate_args(args)
347
+ next message
348
+ end
349
+
350
+ args = normalize_signals(args)
351
+
352
+ if message = validate_signals(args)
353
+ next message
354
+ end
355
+
356
+ signal = args[0] || "USR2"
357
+
358
+ response = Einhorn::Command.signal_all(signal, Einhorn::WorkerPool.workers)
359
+ Einhorn::State.respawn = false
360
+
361
+ "Einhorn is going down! #{response}"
362
+ end
363
+
364
+ def self.validate_args(args)
365
+ return 'No args provided' unless args
366
+ return 'Args must be an array' unless args.kind_of?(Array)
367
+
368
+ args.each do |arg|
369
+ return "Argument is a #{arg.class}, not a string: #{arg.inspect}" unless arg.kind_of?(String)
370
+ end
371
+
372
+ nil
373
+ end
374
+
375
+ def self.validate_signals(args)
376
+ args.each do |signal|
377
+ unless Signal.list.include?(signal)
378
+ return "Invalid signal: #{signal.inspect}"
379
+ end
380
+ end
381
+
382
+ nil
383
+ end
384
+
385
+ def self.normalize_signals(args)
386
+ args.map do |signal|
387
+ signal = signal.upcase
388
+ signal = $1 if signal =~ /\ASIG(.*)\Z/
389
+ signal
390
+ end
391
+ end
335
392
  end
336
393
  end
@@ -1,7 +1,6 @@
1
1
  require 'pp'
2
2
  require 'set'
3
3
  require 'tmpdir'
4
- require 'json'
5
4
 
6
5
  require 'einhorn/command/interface'
7
6
 
@@ -30,9 +29,17 @@ module Einhorn
30
29
 
31
30
  Einhorn::State.children.delete(pid)
32
31
 
32
+ # Unacked worker
33
+ if spec[:type] == :worker && !spec[:acked]
34
+ Einhorn::State.consecutive_deaths_before_ack += 1
35
+ extra = ' before it was ACKed'
36
+ else
37
+ extra = nil
38
+ end
39
+
33
40
  case type = spec[:type]
34
41
  when :worker
35
- Einhorn.log_info("===> Exited worker #{pid.inspect}")
42
+ Einhorn.log_info("===> Exited worker #{pid.inspect}#{extra}")
36
43
  when :state_passer
37
44
  Einhorn.log_debug("===> Exited state passing process #{pid.inspect}")
38
45
  else
@@ -78,13 +85,23 @@ module Einhorn
78
85
  return
79
86
  end
80
87
 
88
+ if Einhorn::State.consecutive_deaths_before_ack > 0
89
+ extra = ", breaking the streak of #{Einhorn::State.consecutive_deaths_before_ack} consecutive unacked workers dying"
90
+ else
91
+ extra = nil
92
+ end
93
+ Einhorn::State.consecutive_deaths_before_ack = 0
94
+
81
95
  spec[:acked] = true
82
- Einhorn.log_info("Up to #{Einhorn::WorkerPool.ack_count} / #{Einhorn::WorkerPool.ack_target} #{Einhorn::State.ack_mode[:type]} ACKs")
96
+ Einhorn.log_info("Up to #{Einhorn::WorkerPool.ack_count} / #{Einhorn::WorkerPool.ack_target} #{Einhorn::State.ack_mode[:type]} ACKs#{extra}")
83
97
  # Could call cull here directly instead, I believe.
84
98
  Einhorn::Event.break_loop
85
99
  end
86
100
 
87
- def self.signal_all(signal, children)
101
+ def self.signal_all(signal, children=nil, record=true)
102
+ children ||= Einhorn::WorkerPool.workers
103
+
104
+ signaled = []
88
105
  Einhorn.log_info("Sending #{signal} to #{children.inspect}")
89
106
 
90
107
  children.each do |child|
@@ -93,17 +110,22 @@ module Einhorn
93
110
  next
94
111
  end
95
112
 
96
- if spec[:signaled].include?(child)
97
- Einhorn.log_error("Not sending #{signal} to already-signaled child #{child.inspect}. The fact we tried this probably indicates a bug in Einhorn.")
98
- next
113
+ if record
114
+ if spec[:signaled].include?(signal)
115
+ Einhorn.log_error("Re-sending #{signal} to already-signaled child #{child.inspect}. It may be slow to spin down, or it may be swallowing #{signal}s.")
116
+ end
117
+ spec[:signaled].add(signal)
99
118
  end
100
- spec[:signaled].add(child)
101
119
 
102
120
  begin
103
121
  Process.kill(signal, child)
104
122
  rescue Errno::ESRCH
123
+ else
124
+ signaled << child
105
125
  end
106
126
  end
127
+
128
+ "Successfully sent #{signal}s to #{signaled.length} processes: #{signaled.inspect}"
107
129
  end
108
130
 
109
131
  def self.increment
@@ -118,7 +140,7 @@ module Einhorn
118
140
  def self.decrement
119
141
  if Einhorn::State.config[:number] <= 1
120
142
  output = "Can't decrease number of workers (already at #{Einhorn::State.config[:number]}). Run kill #{$$} if you really want to kill einhorn."
121
- $stderr.puts output
143
+ $stderr.puts(output)
122
144
  return output
123
145
  end
124
146
 
@@ -172,6 +194,10 @@ module Einhorn
172
194
 
173
195
  Einhorn::Event.uninit
174
196
 
197
+ # Reload the original environment
198
+ ENV.clear
199
+ ENV.update(Einhorn::TransientState.environ)
200
+
175
201
  exec [Einhorn::TransientState.script_name, Einhorn::TransientState.script_name], *(['--with-state-fd', read.fileno.to_s, '--'] + Einhorn::State.cmd)
176
202
  end
177
203
 
@@ -186,7 +212,7 @@ module Einhorn
186
212
  Einhorn::Event.close_all_for_worker
187
213
  Einhorn.set_argv(cmd, true)
188
214
 
189
- pass_command_socket_info
215
+ prepare_child_environment
190
216
  einhorn_main
191
217
  end
192
218
  else
@@ -200,7 +226,7 @@ module Einhorn
200
226
  # cloexec on everything.
201
227
  Einhorn::Event.close_all_for_worker
202
228
 
203
- pass_command_socket_info
229
+ prepare_child_environment
204
230
  exec [cmd[0], cmd[0]], *cmd[1..-1]
205
231
  end
206
232
  end
@@ -225,15 +251,19 @@ module Einhorn
225
251
  end
226
252
  end
227
253
 
228
- def self.pass_command_socket_info
254
+ def self.prepare_child_environment
229
255
  # This is run from the child
230
256
  ENV['EINHORN_MASTER_PID'] = Process.ppid.to_s
231
257
  ENV['EINHORN_SOCK_PATH'] = Einhorn::Command::Interface.socket_path
232
258
  if Einhorn::State.command_socket_as_fd
233
259
  socket = UNIXSocket.open(Einhorn::Command::Interface.socket_path)
234
260
  Einhorn::TransientState.socket_handles << socket
235
- ENV['EINHORN_FD'] = socket.fileno.to_s
261
+ ENV['EINHORN_SOCK_FD'] = socket.fileno.to_s
236
262
  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.)
266
+ ENV['EINHORN_FDS'] = Einhorn::State.bind_fds.map(&:to_s).join(' ')
237
267
  end
238
268
 
239
269
  def self.full_upgrade
@@ -257,6 +287,12 @@ module Einhorn
257
287
  Einhorn.log_info("Starting upgrade to #{Einhorn::State.version}...")
258
288
  end
259
289
 
290
+ # Reset this, since we've just upgraded to a new universe (I'm
291
+ # not positive this is the right behavior, but it's not
292
+ # obviously wrong.)
293
+ Einhorn::State.consecutive_deaths_before_ack = 0
294
+ Einhorn::State.last_upgraded = Time.now
295
+
260
296
  Einhorn::State.version += 1
261
297
  replenish_immediately
262
298
  end
@@ -307,14 +343,23 @@ module Einhorn
307
343
  return if Einhorn::TransientState.has_outstanding_spinup_timer
308
344
  return unless Einhorn::WorkerPool.missing_worker_count > 0
309
345
 
310
- spinup_interval = Einhorn::State.config[:seconds]
346
+ # Exponentially backoff automated spinup if we're just having
347
+ # things die before ACKing
348
+ spinup_interval = Einhorn::State.config[:seconds] * (1.5 ** Einhorn::State.consecutive_deaths_before_ack)
311
349
  seconds_ago = (Time.now - Einhorn::State.last_spinup).to_f
312
350
 
313
351
  if seconds_ago > spinup_interval
314
- Einhorn.log_debug("Last spinup was #{seconds_ago}s ago, and spinup_interval is #{spinup_interval}, so spinning up a new process")
352
+ msg = "Last spinup was #{seconds_ago}s ago, and spinup_interval is #{spinup_interval}s, so spinning up a new process"
353
+
354
+ if Einhorn::State.consecutive_deaths_before_ack > 0
355
+ Einhorn.log_info("#{msg} (there have been #{Einhorn::State.consecutive_deaths_before_ack} consecutive unacked worker deaths)")
356
+ else
357
+ Einhorn.log_debug(msg)
358
+ end
359
+
315
360
  spinup
316
361
  else
317
- Einhorn.log_debug("Last spinup was #{seconds_ago}s ago, and spinup_interval is #{spinup_interval}, so not spinning up a new process")
362
+ Einhorn.log_debug("Last spinup was #{seconds_ago}s ago, and spinup_interval is #{spinup_interval}s, so not spinning up a new process")
318
363
  end
319
364
 
320
365
  Einhorn::TransientState.has_outstanding_spinup_timer = true
@@ -1,3 +1,3 @@
1
1
  module Einhorn
2
- VERSION = '0.3.2'
2
+ VERSION = '0.4.0'
3
3
  end
@@ -38,8 +38,8 @@ module Einhorn
38
38
  #
39
39
  # @discovery: How to discover the master process's command socket.
40
40
  # :env: Discover the path from ENV['EINHORN_SOCK_PATH']
41
- # :fd: Just use the file descriptor in ENV['EINHORN_FD'].
42
- # Must run the master with the -b flag. This is mostly
41
+ # :fd: Just use the file descriptor in ENV['EINHORN_SOCK_FD'].
42
+ # Must run the master with the -g flag. This is mostly
43
43
  # useful if you don't have a nice library like Einhorn::Worker.
44
44
  # Then @arg being true causes the FD to be left open after ACK;
45
45
  # otherwise it is closed.
@@ -57,7 +57,7 @@ module Einhorn
57
57
  socket = ENV['EINHORN_SOCK_PATH']
58
58
  client = Einhorn::Client.for_path(socket)
59
59
  when :fd
60
- raise "No EINHORN_FD provided in environment. Did you run einhorn with the -b flag?" unless fd_str = ENV['EINHORN_FD']
60
+ raise "No EINHORN_SOCK_FD provided in environment. Did you run einhorn with the -g flag?" unless fd_str = ENV['EINHORN_SOCK_FD']
61
61
 
62
62
  fd = Integer(fd_str)
63
63
  client = Einhorn::Client.for_fd(fd)
@@ -78,6 +78,31 @@ module Einhorn
78
78
  true
79
79
  end
80
80
 
81
+ def self.socket(number=0)
82
+ fds = einhorn_fds
83
+ fds ? fds[number] : nil
84
+ end
85
+
86
+ def self.socket!(number=0)
87
+ unless fds = einhorn_fds
88
+ raise "No EINHORN_FDS provided in environment. Are you running under Einhorn?"
89
+ end
90
+
91
+ unless number < fds.length
92
+ raise "Only #{fds.length} FDs available, but FD #{number} was requested"
93
+ end
94
+
95
+ fds[number]
96
+ end
97
+
98
+ def self.einhorn_fds
99
+ unless raw_fds = ENV['EINHORN_FDS']
100
+ return nil
101
+ end
102
+
103
+ raw_fds.split(' ').map {|fd| Integer(fd)}
104
+ end
105
+
81
106
  # Call this to handle graceful shutdown requests to your app.
82
107
  def self.graceful_shutdown(&blk)
83
108
  Signal.trap('USR2', &blk)
@@ -1,5 +1,9 @@
1
1
  module Einhorn
2
2
  module WorkerPool
3
+ def self.workers
4
+ Einhorn::State.children.keys
5
+ end
6
+
3
7
  def self.unsignaled_workers
4
8
  Einhorn::State.children.select do |pid, spec|
5
9
  spec[:signaled].length == 0
data/lib/einhorn.rb CHANGED
@@ -6,8 +6,6 @@ require 'socket'
6
6
  require 'tmpdir'
7
7
  require 'yaml'
8
8
 
9
- require 'rubygems'
10
-
11
9
  module Einhorn
12
10
  module AbstractState
13
11
  def default_state; raise NotImplementedError.new('Override in extended modules'); end
@@ -40,6 +38,8 @@ module Einhorn
40
38
  :version => 0,
41
39
  :sockets => {},
42
40
  :orig_cmd => nil,
41
+ :bind => [],
42
+ :bind_fds => [],
43
43
  :cmd => nil,
44
44
  :script_name => nil,
45
45
  :respawn => true,
@@ -55,7 +55,9 @@ module Einhorn
55
55
  :command_socket_as_fd => false,
56
56
  :socket_path => nil,
57
57
  :pidfile => nil,
58
- :lockfile => nil
58
+ :lockfile => nil,
59
+ :consecutive_deaths_before_ack => 0,
60
+ :last_upgraded => nil
59
61
  }
60
62
  end
61
63
  end
@@ -68,6 +70,7 @@ module Einhorn
68
70
  :preloaded => false,
69
71
  :script_name => nil,
70
72
  :argv => [],
73
+ :environ => {},
71
74
  :has_outstanding_spinup_timer => false,
72
75
  :stateful => nil,
73
76
  # Holds references so that the GC doesn't go and close your sockets.
@@ -219,9 +222,19 @@ module Einhorn
219
222
  Einhorn::State.cmd_name ? "ruby #{Einhorn::State.cmd_name}" : Einhorn::State.orig_cmd.join(' ')
220
223
  end
221
224
 
225
+ def self.socketify_env!
226
+ Einhorn::State.bind.each do |host, port, flags|
227
+ fd = bind(host, port, flags)
228
+ Einhorn::State.bind_fds << fd
229
+ end
230
+ end
231
+
232
+ # This duplicates some code from the environment path, but is
233
+ # deprecated so that's ok.
222
234
  def self.socketify!(cmd)
223
235
  cmd.map! do |arg|
224
236
  if arg =~ /^(.*=|)srv:([^:]+):(\d+)((?:,\w+)*)$/
237
+ log_error("Using deprecated command-line configuration for Einhorn; should upgrade to the environment variable interface.")
225
238
  opt = $1
226
239
  host = $2
227
240
  port = $3
@@ -249,6 +262,7 @@ module Einhorn
249
262
  Einhorn::State.cmd = ARGV.dup
250
263
  # TODO: don't actually alter ARGV[0]?
251
264
  Einhorn::State.cmd[0] = which(Einhorn::State.cmd[0])
265
+ socketify_env!
252
266
  socketify!(Einhorn::State.cmd)
253
267
  end
254
268
 
data/test/test_helper.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'rubygems'
2
+ require 'bundler/setup'
2
3
 
3
4
  require 'test/unit'
4
5
  require 'mocha'
@@ -0,0 +1,49 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '../../test_helper'))
2
+
3
+ require 'einhorn'
4
+
5
+ class ClientTest < Test::Unit::TestCase
6
+ def message
7
+ {:foo => ['%bar', '%baz']}
8
+ end
9
+
10
+ def serialized
11
+ "--- %0A:foo: %0A- \"%25bar\"%0A- \"%25baz\"%0A\n"
12
+ end
13
+
14
+ context "when sending a message" do
15
+ should "write a serialized line" do
16
+ socket = mock
17
+ socket.expects(:write).with(serialized)
18
+ Einhorn::Client::Transport.send_message(socket, message)
19
+ end
20
+ end
21
+
22
+ context "when receiving a message" do
23
+ should "deserialize a single line" do
24
+ socket = mock
25
+ socket.expects(:readline).returns(serialized)
26
+ result = Einhorn::Client::Transport.receive_message(socket)
27
+ assert_equal(result, message)
28
+ end
29
+ end
30
+
31
+ context "when {de,}serializing a message" do
32
+ should "serialize and escape a message as expected" do
33
+ actual = Einhorn::Client::Transport.serialize_message(message)
34
+ assert_equal(serialized, actual)
35
+ end
36
+
37
+ should "deserialize and unescape a message as expected" do
38
+ actual = Einhorn::Client::Transport.deserialize_message(serialized)
39
+ assert_equal(message, actual)
40
+ end
41
+
42
+ should "raise an error when deserializing invalid YAML" do
43
+ invalid_serialized = "-%0A\t-"
44
+ assert_raises(ArgumentError) do
45
+ Einhorn::Client::Transport.deserialize_message(invalid_serialized)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -9,14 +9,16 @@ class InterfaceTest < Test::Unit::TestCase
9
9
  should "call that command" do
10
10
  conn = stub(:log_debug => nil)
11
11
  conn.expects(:write).once.with do |message|
12
- parsed = JSON.parse(message)
12
+ # Remove trailing newline
13
+ message = message[0...-1]
14
+ parsed = YAML.load(URI.unescape(message))
13
15
  parsed['message'] =~ /Welcome gdb/
14
16
  end
15
17
  request = {
16
18
  'command' => 'ehlo',
17
19
  'user' => 'gdb'
18
20
  }
19
- Interface.process_command(conn, JSON.generate(request))
21
+ Interface.process_command(conn, YAML.dump(request))
20
22
  end
21
23
  end
22
24
 
@@ -27,7 +29,7 @@ class InterfaceTest < Test::Unit::TestCase
27
29
  request = {
28
30
  'command' => 'made-up',
29
31
  }
30
- Interface.process_command(conn, JSON.generate(request))
32
+ Interface.process_command(conn, YAML.dump(request))
31
33
  end
32
34
  end
33
35
 
@@ -41,7 +43,7 @@ class InterfaceTest < Test::Unit::TestCase
41
43
  'pid' => 1234
42
44
  }
43
45
  Einhorn::Command.expects(:register_manual_ack).once.with(1234)
44
- Interface.process_command(conn, JSON.generate(request))
46
+ Interface.process_command(conn, YAML.dump(request))
45
47
  end
46
48
  end
47
49
  end
@@ -14,7 +14,7 @@ module Einhorn::Event
14
14
  end
15
15
 
16
16
  class EventTest < Test::Unit::TestCase
17
- context "when run the event loop" do
17
+ context "when running the event loop" do
18
18
  setup do
19
19
  Einhorn::Event.reset
20
20
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: einhorn
3
3
  version: !ruby/object:Gem::Version
4
- hash: 23
4
+ hash: 15
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
- - 3
9
- - 2
10
- version: 0.3.2
8
+ - 4
9
+ - 0
10
+ version: 0.4.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Greg Brockman
@@ -15,11 +15,9 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2012-07-10 00:00:00 Z
18
+ date: 2012-09-27 00:00:00 Z
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
21
- name: json
22
- prerelease: false
23
21
  requirement: &id001 !ruby/object:Gem::Requirement
24
22
  none: false
25
23
  requirements:
@@ -29,11 +27,11 @@ dependencies:
29
27
  segments:
30
28
  - 0
31
29
  version: "0"
32
- type: :runtime
30
+ prerelease: false
31
+ type: :development
32
+ name: rake
33
33
  version_requirements: *id001
34
34
  - !ruby/object:Gem::Dependency
35
- name: shoulda
36
- prerelease: false
37
35
  requirement: &id002 !ruby/object:Gem::Requirement
38
36
  none: false
39
37
  requirements:
@@ -43,11 +41,11 @@ dependencies:
43
41
  segments:
44
42
  - 0
45
43
  version: "0"
44
+ prerelease: false
46
45
  type: :development
46
+ name: shoulda
47
47
  version_requirements: *id002
48
48
  - !ruby/object:Gem::Dependency
49
- name: mocha
50
- prerelease: false
51
49
  requirement: &id003 !ruby/object:Gem::Requirement
52
50
  none: false
53
51
  requirements:
@@ -57,7 +55,9 @@ dependencies:
57
55
  segments:
58
56
  - 0
59
57
  version: "0"
58
+ prerelease: false
60
59
  type: :development
60
+ name: mocha
61
61
  version_requirements: *id003
62
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
63
  email:
@@ -72,6 +72,7 @@ extra_rdoc_files: []
72
72
  files:
73
73
  - .gitignore
74
74
  - Gemfile
75
+ - History.txt
75
76
  - LICENSE
76
77
  - README.md
77
78
  - README.md.in
@@ -99,6 +100,7 @@ files:
99
100
  - lib/einhorn/worker_pool.rb
100
101
  - test/test_helper.rb
101
102
  - test/unit/einhorn.rb
103
+ - test/unit/einhorn/client.rb
102
104
  - test/unit/einhorn/command.rb
103
105
  - test/unit/einhorn/command/interface.rb
104
106
  - test/unit/einhorn/event.rb
@@ -138,7 +140,7 @@ summary: "Einhorn: the language-independent shared socket manager"
138
140
  test_files:
139
141
  - test/test_helper.rb
140
142
  - test/unit/einhorn.rb
143
+ - test/unit/einhorn/client.rb
141
144
  - test/unit/einhorn/command.rb
142
145
  - test/unit/einhorn/command/interface.rb
143
146
  - test/unit/einhorn/event.rb
144
- has_rdoc: