Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

executable file 299 lines (221 sloc) 9.318 kb
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
#!/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 a space-separated list of file descriptor numbers in the
EINHORN_FDS environment variable (respecting the order that the `-b`
options were provided in):

EINHORN_FDS="6" # 127.0.0.1:1234
EINHORN_FDS="6 7" # 127.0.0.1:1234,r 127.0.0.1:1235

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_FDS=6 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

  optparse = OptionParser.new do |opts|
    opts.on('-b ADDR', '--bind ADDR', 'Bind an address and add the corresponding FD to EINHORN_FDS') 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.louder(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

    opts.on('--with-state-fd STATE', '[Internal option] With file descriptor containing state') do |fd|
      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

  ret = Einhorn.run
  begin
    exit(ret)
  rescue TypeError
    exit(0)
  end
end
Something went wrong with that request. Please try again.