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 +15 -0
- data/README.md +29 -14
- data/bin/einhorn +41 -26
- data/bin/einhornsh +46 -15
- data/einhorn.gemspec +2 -2
- data/example/thin_example +5 -4
- data/example/time_server +7 -11
- data/lib/einhorn/client.rb +29 -16
- data/lib/einhorn/command/interface.rb +78 -21
- data/lib/einhorn/command.rb +61 -16
- data/lib/einhorn/version.rb +1 -1
- data/lib/einhorn/worker.rb +28 -3
- data/lib/einhorn/worker_pool.rb +4 -0
- data/lib/einhorn.rb +17 -3
- data/test/test_helper.rb +1 -0
- data/test/unit/einhorn/client.rb +49 -0
- data/test/unit/einhorn/command/interface.rb +6 -4
- data/test/unit/einhorn/event.rb +1 -1
- metadata +15 -13
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.
|
72
|
-
|
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
|
-
|
75
|
-
|
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
|
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
|
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 `-
|
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 `
|
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.
|
171
|
-
|
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, --
|
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.
|
48
|
-
|
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
|
-
|
51
|
-
|
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
|
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
|
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 `-
|
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 `
|
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.
|
147
|
-
|
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', '--
|
173
|
-
|
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
|
-
|
19
|
-
|
20
|
-
|
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
|
-
|
24
|
-
|
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' =>
|
31
|
+
response = @client.command({'command' => command, 'args' => args})
|
30
32
|
rescue Errno::EPIPE => e
|
31
|
-
|
32
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
121
|
+
Einhorn::EinhornSH.emit(optparse, true)
|
94
122
|
return 1
|
95
123
|
end
|
96
124
|
|
97
|
-
Signal.trap("INT")
|
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
|
-
|
17
|
-
gem.
|
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
|
-
|
32
|
-
|
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
|
-
|
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
|
5
|
-
#
|
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
|
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 -
|
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 #{
|
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
|
-
|
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.
|
data/lib/einhorn/client.rb
CHANGED
@@ -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
|
-
|
24
|
-
|
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::
|
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::
|
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::
|
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::
|
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::
|
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
|
-
|
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 =
|
211
|
-
rescue
|
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
|
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
|
data/lib/einhorn/command.rb
CHANGED
@@ -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
|
97
|
-
|
98
|
-
|
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
|
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
|
-
|
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
|
-
|
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.
|
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['
|
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
|
-
|
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
|
-
|
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
|
data/lib/einhorn/version.rb
CHANGED
data/lib/einhorn/worker.rb
CHANGED
@@ -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['
|
42
|
-
# Must run the master with the -
|
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
|
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)
|
data/lib/einhorn/worker_pool.rb
CHANGED
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
@@ -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
|
-
|
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,
|
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,
|
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,
|
46
|
+
Interface.process_command(conn, YAML.dump(request))
|
45
47
|
end
|
46
48
|
end
|
47
49
|
end
|
data/test/unit/einhorn/event.rb
CHANGED
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:
|
4
|
+
hash: 15
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
-
|
10
|
-
version: 0.
|
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-
|
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
|
-
|
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:
|