/
einhorn
executable file
·320 lines (237 loc) · 9.95 KB
/
einhorn
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
#!/usr/bin/env ruby
# Author: Greg Brockman <gdb@stripe.com>
require 'rubygems'
require 'einhorn'
module Einhorn
module Executable
def self.einhorn_usage(long)
usage = <<EOF
## Usage
Einhorn is the language-independent shared socket manager. Run
`einhorn -h` to see detailed usage. At a high level, usage looks like
the following:
einhorn [options] program
Einhorn will open one or more shared sockets and run multiple copies
of your process. You can seamlessly reload your code, dynamically
reconfigure Einhorn, and more.
EOF
if long
usage << <<EOF
## Overview
To set Einhorn up as a master process running 3 copies of `sleep 5`:
$ einhorn -n 3 sleep 5
You can communicate your running Einhorn process via `einhornsh`:
$ einhornsh
Welcome gdb! You are speaking to Einhorn Master Process 11902
Enter 'help' if you're not sure what to do.
Type "quit" or "exit" to quit at any time
> help
You are speaking to the Einhorn command socket. You can run the following commands:
...
### Server sockets
If your process is a server and listens on one or more sockets,
Einhorn can open these sockets and pass them to the workers. You can
specify the addresses to bind by passing one or more `-b ADDR`
arguments:
einhorn -b 127.0.0.1:1234 my-command
einhorn -b 127.0.0.1:1234,r -b 127.0.0.1:1235 my-command
Each address is specified as an ip/port pair, possibly accompanied by options:
ADDR := (IP:PORT)[<,OPT>...]
In the worker process, the opened file descriptors will be represented
as file descriptor numbers in a series of environment variables named
EINHORN_FD_0, EINHORN_FD_1, etc. (respecting the order that the `-b`
options were provided in), with the total number of file descriptors
in the EINHORN_FD_COUNT environment variable:
EINHORN_FD_0="6" # 127.0.0.1:1234
EINHORN_FD_COUNT="1"
EINHORN_FD_0="6" # 127.0.0.1:1234,r
EINHORN_FD_1="7" # 127.0.0.1:1235
EINHORN_FD_COUNT="2"
Valid opts are:
r, so_reuseaddr: set SO_REUSEADDR on the server socket
n, o_nonblock: set O_NONBLOCK on the server socket
You can for example run:
$ einhorn -b 127.0.0.1:2345,r -m manual -n 4 -- example/time_server
Which will run 4 copies of
EINHORN_FD_0=6 EINHORN_FD_COUNT=1 example/time_server
Where file descriptor 6 is a server socket bound to `127.0.0.1:2345`
and with `SO_REUSEADDR` set. It is then your application's job to
figure out how to `accept()` on this file descriptor.
### Command socket
Einhorn opens a UNIX socket to which you can send commands (run
`help` in `einhornsh` to see what admin commands you can
run). Einhorn relies on file permissions to ensure that no malicious
users can gain access. Run with a `-d DIRECTORY` to change the
directory where the socket will live.
Note that the command socket uses a line-oriented YAML protocol, and
you should ensure you trust clients to send arbitrary YAML messages
into your process.
### Seamless upgrades
You can cause your code to be seamlessly reloaded by upgrading the
worker code on disk and running
$ einhornsh
...
> upgrade
Once the new workers have been spawned, Einhorn will send each old
worker a SIGUSR2. SIGUSR2 should be interpreted as a request for a
graceful shutdown.
### ACKs
After Einhorn spawns a worker, it will only consider the worker up
once it has received an ACK. Currently two ACK mechanisms are
supported: manual and timer.
#### Manual ACK
A manual ACK (configured by providing a `-m manual`) requires your
application to send a command to the command socket once it's
ready. This is the safest ACK mechanism. If you're writing in Ruby,
just do
require 'einhorn/worker'
Einhorn::Worker.ack!
in your worker code. If you're writing in a different language, or
don't want to include Einhorn in your namespace, you can send the
string
{"command":"worker:ack", "pid":PID}
to the UNIX socket pointed to by the environment variable
`EINHORN_SOCK_PATH`. (Be sure to include a trailing newline.)
To make things even easier, you can pass a `-g` to Einhorn, in which
case you just need to `write()` the above message to the open file
descriptor pointed to by `EINHORN_SOCK_FD`.
(See `lib/einhorn/worker.rb` for details of these and other socket
discovery mechanisms.)
#### Timer ACK [default]
By default, Einhorn will use a timer ACK of 1 second. That means that
if your process hasn't exited after 1 second, it is considered ACK'd
and healthy. You can modify this timeout to be more appropriate for
your application (and even set to 0 if desired). Just pass a `-m
FLOAT`.
### Preloading
If you're running a Ruby process, Einhorn can optionally preload its
code, so it only has to load the code once per upgrade rather than
once per worker process. This also saves on memory overhead, since all
of the code in these processes will be stored only once using your
operating system's copy-on-write features.
To use preloading, just give Einhorn a `-p PATH_TO_CODE`, and make
sure you've defined an `einhorn_main` method.
In order to maximize compatibility, we've worked to minimize Einhorn's
dependencies. It has no dependencies outside of the Ruby standard
library.
### Command name
You can set the name that Einhorn and your workers show in PS. Just
pass `-c <name>`.
EOF
end
usage << <<EOF
### Options
EOF
end
end
end
# Would be nice if this could be loadable rather than always
# executing, but when run under gem it's a bit hard to do so.
if true # $0 == __FILE__
Einhorn::TransientState.script_name = $0
Einhorn::TransientState.argv = ARGV.dup
Einhorn::TransientState.environ = ENV.to_hash
Einhorn.initialize_plugins
optparse = OptionParser.new do |opts|
opts.on('-b ADDR', '--bind ADDR', 'Bind an address and add the corresponding FD via the environment') do |addr|
unless addr =~ /\A([^:]+):(\d+)((?:,\w+)*)\Z/
raise "Invalid value for #{addr.inspect}: bind address must be of the form address:port[,flags...]"
end
host = $1
port = Integer($2)
flags = $3.split(',').select {|flag| flag.length > 0}.map {|flag| flag.downcase}
Einhorn::State.bind << [host, port, flags]
end
opts.on('-c CMD_NAME', '--command-name CMD_NAME', 'Set the command name in ps to this value') do |cmd_name|
Einhorn::State.cmd_name = cmd_name
end
opts.on('-d PATH', '--socket-path PATH', 'Where to open the Einhorn command socket') do |path|
Einhorn::State.socket_path = path
end
opts.on('-e PIDFILE', '--pidfile PIDFILE', 'Where to write out the Einhorn pidfile') do |pidfile|
Einhorn::State.pidfile = pidfile
end
opts.on('-f LOCKFILE', '--lockfile LOCKFILE', 'Where to store the Einhorn lockfile') do |lockfile|
Einhorn::State.lockfile = lockfile
end
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
Einhorn::State.command_socket_as_fd = true
end
opts.on('-h', '--help', 'Display this message') do
opts.banner = Einhorn::Executable.einhorn_usage(true)
puts opts
exit(1)
end
opts.on('-k', '--kill-children-on-exit', 'If Einhorn exits unexpectedly, gracefully kill all its children') do
Einhorn::State.kill_children_on_exit = true
end
opts.on('-l', '--backlog N', 'Connection backlog (assuming this is a server)') do |b|
Einhorn::State.config[:backlog] = b.to_i
end
opts.on('-m MODE', '--ack-mode MODE', 'What kinds of ACK to expect from workers. Choices: FLOAT (number of seconds until assumed alive), manual (process will speak to command socket when ready). Default is MODE=1.') do |mode|
# Try manual
if mode == 'manual'
Einhorn::State.ack_mode = {:type => :manual}
next
end
# Try float
begin
parsed = Float(mode)
rescue ArgumentError
else
Einhorn::State.ack_mode = {:type => :timer, :timeout => parsed}
next
end
# Give up
raise "Invalid ack-mode #{mode.inspect} (valid modes: FLOAT or manual)"
end
opts.on('-n', '--number N', 'Number of copies to spin up') do |n|
Einhorn::State.config[:number] = n.to_i
end
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|
Einhorn::State.path = path
end
opts.on('-q', '--quiet', 'Make output quiet (can be reconfigured on the fly)') do
Einhorn::Command.quieter(false)
end
opts.on('-s', '--seconds N', 'Number of seconds to wait until respawning') do |b|
Einhorn::State.config[:seconds] = s.to_i
end
opts.on('-v', '--verbose', 'Make output verbose (can be reconfigured on the fly)') do
Einhorn::Command.louder(false)
end
Einhorn.plugins_send(:optparse, opts)
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|
master, worker, renice_cmd = nice.split(':')
master = Integer(master) if master
worker = Integer(worker) if worker
Einhorn::State.nice[:master] = master if master
Einhorn::State.nice[:worker] = worker if worker
Einhorn::State.nice[:renice_cmd] = renice_cmd if renice_cmd
end
opts.on('--with-state-fd STATE', '[Internal option] With file descriptor containing state') do |fd|
Einhorn::TransientState.stateful = true
read = IO.for_fd(Integer(fd))
state = read.read
read.close
Einhorn.restore_state(state)
end
opts.on('--version', 'Show version') do
puts Einhorn::VERSION
exit
end
end
optparse.order!
if ARGV.length < 1
optparse.banner = Einhorn::Executable.einhorn_usage(false)
puts optparse
exit(1)
end
Einhorn.plugins_send(:post_optparse)
ret = Einhorn.run
begin
exit(ret)
rescue TypeError
exit(0)
end
end