einhorn 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +236 -0
- data/README.md.in +79 -0
- data/Rakefile +19 -0
- data/bin/einhorn +284 -0
- data/bin/einhornsh +120 -0
- data/einhorn.gemspec +21 -0
- data/example/pool_worker.rb +19 -0
- data/example/thin_example +52 -0
- data/example/time_server +48 -0
- data/lib/einhorn/client.rb +48 -0
- data/lib/einhorn/command/interface.rb +336 -0
- data/lib/einhorn/command.rb +336 -0
- data/lib/einhorn/event/abstract_text_descriptor.rb +132 -0
- data/lib/einhorn/event/ack_timer.rb +20 -0
- data/lib/einhorn/event/command_server.rb +58 -0
- data/lib/einhorn/event/connection.rb +45 -0
- data/lib/einhorn/event/loop_breaker.rb +6 -0
- data/lib/einhorn/event/persistent.rb +23 -0
- data/lib/einhorn/event/timer.rb +39 -0
- data/lib/einhorn/event.rb +150 -0
- data/lib/einhorn/version.rb +3 -0
- data/lib/einhorn/worker.rb +94 -0
- data/lib/einhorn/worker_pool.rb +56 -0
- data/lib/einhorn.rb +282 -0
- data/test/test_helper.rb +7 -0
- data/test/unit/einhorn/command/interface.rb +47 -0
- data/test/unit/einhorn/command.rb +21 -0
- data/test/unit/einhorn/event.rb +89 -0
- data/test/unit/einhorn.rb +38 -0
- metadata +143 -0
data/bin/einhornsh
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'logger'
|
3
|
+
require 'optparse'
|
4
|
+
|
5
|
+
require 'readline'
|
6
|
+
|
7
|
+
require 'rubygems'
|
8
|
+
require 'einhorn'
|
9
|
+
|
10
|
+
module Einhorn
|
11
|
+
class EinhornSH
|
12
|
+
def initialize(path_to_socket)
|
13
|
+
@path_to_socket = path_to_socket
|
14
|
+
reconnect
|
15
|
+
end
|
16
|
+
|
17
|
+
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'
|
21
|
+
|
22
|
+
while line = Readline.readline('> ', true)
|
23
|
+
if ['quit', 'exit'].include?(line)
|
24
|
+
puts "Goodbye!"
|
25
|
+
return
|
26
|
+
end
|
27
|
+
|
28
|
+
begin
|
29
|
+
response = @client.command({'command' => line})
|
30
|
+
rescue Errno::EPIPE => e
|
31
|
+
puts "einhornsh: Error communicating with Einhorn: #{e} (#{e.class})"
|
32
|
+
puts "einhornsh: Attempting to reconnect..."
|
33
|
+
reconnect
|
34
|
+
|
35
|
+
retry
|
36
|
+
end
|
37
|
+
puts response['message']
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def reconnect
|
42
|
+
begin
|
43
|
+
@client = Einhorn::Client.for_path(@path_to_socket)
|
44
|
+
rescue Errno::ENOENT => e
|
45
|
+
# TODO: The exit here is a biit of a layering violation.
|
46
|
+
puts <<EOF
|
47
|
+
Could not connect to Einhorn master process:
|
48
|
+
|
49
|
+
#{e}
|
50
|
+
|
51
|
+
HINT: Are you sure you are running an Einhorn master? If so, you
|
52
|
+
should pass einhornsh the cmd_name (-c argument) provided to Einhorn.
|
53
|
+
EOF
|
54
|
+
exit(1)
|
55
|
+
end
|
56
|
+
ehlo
|
57
|
+
end
|
58
|
+
|
59
|
+
def ehlo
|
60
|
+
response = @client.command('command' => 'ehlo', 'user' => ENV['USER'])
|
61
|
+
puts response['message']
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def main
|
67
|
+
options = {}
|
68
|
+
optparse = OptionParser.new do |opts|
|
69
|
+
opts.banner = "Usage: #{$0} [options] [cmd_name]
|
70
|
+
|
71
|
+
Welcome to Einhornsh: the Einhorn shell.
|
72
|
+
|
73
|
+
Pass the cmd_name of the Einhorn master you are connecting to either
|
74
|
+
as a positional argument or using `-c`. If you're running your Einhorn
|
75
|
+
with a `-d`, provide the same argument here."
|
76
|
+
|
77
|
+
opts.on('-h', '--help', 'Display this message') do
|
78
|
+
puts opts
|
79
|
+
exit(1)
|
80
|
+
end
|
81
|
+
|
82
|
+
opts.on('-c CMD_NAME', '--command-name CMD_NAME', 'Connect to the Einhorn master with this cmd_name') do |cmd_name|
|
83
|
+
options[:cmd_name] = cmd_name
|
84
|
+
end
|
85
|
+
|
86
|
+
opts.on('-d PATH', '--socket-path PATH', 'Path to the Einhorn command socket') do |path|
|
87
|
+
options[:socket_path] = path
|
88
|
+
end
|
89
|
+
end
|
90
|
+
optparse.parse!
|
91
|
+
|
92
|
+
if ARGV.length > 1
|
93
|
+
puts optparse
|
94
|
+
return 1
|
95
|
+
end
|
96
|
+
|
97
|
+
Signal.trap("INT") {puts; exit(0)}
|
98
|
+
|
99
|
+
path_to_socket = options[:socket_path]
|
100
|
+
|
101
|
+
unless path_to_socket
|
102
|
+
cmd_name = options[:cmd_name] || ARGV[0]
|
103
|
+
path_to_socket = Einhorn::Command::Interface.default_socket_path(cmd_name)
|
104
|
+
end
|
105
|
+
|
106
|
+
sh = Einhorn::EinhornSH.new(path_to_socket)
|
107
|
+
sh.run
|
108
|
+
return 0
|
109
|
+
end
|
110
|
+
|
111
|
+
# Would be nice if this could be loadable rather than always
|
112
|
+
# executing, but when run under gem it's a bit hard to do so.
|
113
|
+
if true # $0 == __FILE__
|
114
|
+
ret = main
|
115
|
+
begin
|
116
|
+
exit(ret)
|
117
|
+
rescue TypeError
|
118
|
+
exit(0)
|
119
|
+
end
|
120
|
+
end
|
data/einhorn.gemspec
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/einhorn/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Greg Brockman"]
|
6
|
+
gem.email = ["gdb@stripe.com"]
|
7
|
+
gem.summary = "Einhorn: the language-independent shared socket manager"
|
8
|
+
gem.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."
|
9
|
+
gem.homepage = "https://github.com/stripe/einhorn"
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = "einhorn"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
# maybe swap out for YAML? Then don't need any gems.
|
17
|
+
gem.add_dependency('json')
|
18
|
+
gem.add_development_dependency('shoulda')
|
19
|
+
gem.add_development_dependency('mocha')
|
20
|
+
gem.version = Einhorn::VERSION
|
21
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Used as an example of preloading in Einhorn blog post
|
4
|
+
# (https://stripe.com/blog/meet-einhorn). Program name ends in .rb in
|
5
|
+
# order to make explicit that it's written in Ruby, though this isn't
|
6
|
+
# actually necessary for preloading to work.
|
7
|
+
#
|
8
|
+
# Run as
|
9
|
+
#
|
10
|
+
# einhorn -p ./pool_worker.rb ./pool_worker.rb
|
11
|
+
|
12
|
+
puts "From PID #{$$}: loading #{__FILE__}"
|
13
|
+
|
14
|
+
def einhorn_main
|
15
|
+
while true
|
16
|
+
puts "From PID #{$$}: Doing some work"
|
17
|
+
sleep 1
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# An example application using our patched Thin and EventMachine. You
|
4
|
+
# can obtain these from:
|
5
|
+
#
|
6
|
+
# https://github.com/stripe/thin.git, and
|
7
|
+
# https://github.com/stripe/eventmachine.git
|
8
|
+
|
9
|
+
require 'rubygems'
|
10
|
+
require 'einhorn'
|
11
|
+
|
12
|
+
# Make sure we're using the patched versions.
|
13
|
+
gem 'thin', '1.3.2.stripe.0'
|
14
|
+
gem 'eventmachine', '1.0.0.beta.4.stripe.0'
|
15
|
+
|
16
|
+
require 'thin'
|
17
|
+
|
18
|
+
class App
|
19
|
+
def initialize(id)
|
20
|
+
@id = id
|
21
|
+
end
|
22
|
+
|
23
|
+
def call(env)
|
24
|
+
return [200, {}, "[#{$$}] From server instance #{@id}: Got your request!\n"]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def einhorn_main
|
29
|
+
puts "Called with #{ARGV.inspect}"
|
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'"
|
33
|
+
end
|
34
|
+
|
35
|
+
Einhorn::Worker.graceful_shutdown do
|
36
|
+
puts "#{$$} is now exiting..."
|
37
|
+
exit(0)
|
38
|
+
end
|
39
|
+
Einhorn::Worker.ack!
|
40
|
+
|
41
|
+
EventMachine.run do
|
42
|
+
ARGV.each_with_index do |arg, i|
|
43
|
+
sock = Integer(arg)
|
44
|
+
srv = Thin::Server.new(sock, App.new(i), :reuse => true)
|
45
|
+
srv.start
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
if $0 == __FILE__
|
51
|
+
einhorn_main
|
52
|
+
end
|
data/example/time_server
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
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.
|
6
|
+
#
|
7
|
+
# Invoke through Einhorn as
|
8
|
+
#
|
9
|
+
# einhorn ./time_server srv:127.0.0.1:2345,r
|
10
|
+
#
|
11
|
+
# or, if you want to try out preloading:
|
12
|
+
#
|
13
|
+
# einhorn -p ./time_server ./time_server srv:127.0.0.1:2345,r
|
14
|
+
|
15
|
+
require 'rubygems'
|
16
|
+
require 'einhorn/worker'
|
17
|
+
|
18
|
+
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
|
24
|
+
|
25
|
+
socket = Socket.for_fd(Integer(ARGV[0]))
|
26
|
+
|
27
|
+
# Came up successfully, so let's set up graceful handler and ACK the
|
28
|
+
# master.
|
29
|
+
Einhorn::Worker.graceful_shutdown do
|
30
|
+
puts "Goodbye from #{$$}"
|
31
|
+
exit(0)
|
32
|
+
end
|
33
|
+
Einhorn::Worker.ack!
|
34
|
+
|
35
|
+
# Real work happens here.
|
36
|
+
begin
|
37
|
+
while true
|
38
|
+
accepted, _ = socket.accept
|
39
|
+
accepted.write("[#{$$}] The current time is: #{Time.now}!\n")
|
40
|
+
accepted.close
|
41
|
+
end
|
42
|
+
rescue Exception
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
if $0 == __FILE__
|
47
|
+
einhorn_main
|
48
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'set'
|
3
|
+
|
4
|
+
module Einhorn
|
5
|
+
class Client
|
6
|
+
@@responseless_commands = Set.new(['worker:ack'])
|
7
|
+
|
8
|
+
def self.for_path(path_to_socket)
|
9
|
+
socket = UNIXSocket.open(path_to_socket)
|
10
|
+
self.new(socket)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.for_fd(fileno)
|
14
|
+
socket = UNIXSocket.for_fd(fileno)
|
15
|
+
self.new(socket)
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(socket)
|
19
|
+
@socket = socket
|
20
|
+
end
|
21
|
+
|
22
|
+
def command(command_hash)
|
23
|
+
command = JSON.generate(command_hash) + "\n"
|
24
|
+
write(command)
|
25
|
+
recvmessage if expect_response?(command_hash)
|
26
|
+
end
|
27
|
+
|
28
|
+
def expect_response?(command_hash)
|
29
|
+
!@@responseless_commands.include?(command_hash['command'])
|
30
|
+
end
|
31
|
+
|
32
|
+
def close
|
33
|
+
@socket.close
|
34
|
+
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
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,336 @@
|
|
1
|
+
require 'tmpdir'
|
2
|
+
require 'socket'
|
3
|
+
|
4
|
+
module Einhorn::Command
|
5
|
+
module Interface
|
6
|
+
@@commands = {}
|
7
|
+
@@command_server = nil
|
8
|
+
|
9
|
+
def self.command_server=(server)
|
10
|
+
raise "Command server already set" if @@command_server && server
|
11
|
+
@@command_server = server
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.command_server
|
15
|
+
@@command_server
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.init
|
19
|
+
install_handlers
|
20
|
+
at_exit do
|
21
|
+
if Einhorn::TransientState.whatami == :master
|
22
|
+
to_remove = [pidfile]
|
23
|
+
# Don't nuke socket_path if we never successfully acquired it
|
24
|
+
to_remove << socket_path if @@command_server
|
25
|
+
to_remove.each do |file|
|
26
|
+
begin
|
27
|
+
File.unlink(file)
|
28
|
+
rescue Errno::ENOENT
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.persistent_init
|
36
|
+
socket = open_command_socket
|
37
|
+
Einhorn::Event::CommandServer.open(socket)
|
38
|
+
|
39
|
+
# Could also rewrite this on reload. Might be useful in case
|
40
|
+
# someone goes and accidentally clobbers/deletes. Should make
|
41
|
+
# sure that the clobber is atomic if we we were do do that.
|
42
|
+
write_pidfile
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.open_command_socket
|
46
|
+
path = socket_path
|
47
|
+
|
48
|
+
with_file_lock do
|
49
|
+
# Need to avoid time-of-check to time-of-use bugs in blowing
|
50
|
+
# away and recreating the old socketfile.
|
51
|
+
destroy_old_command_socket(path)
|
52
|
+
UNIXServer.new(path)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Lock against other Einhorn workers. Unfortunately, have to leave
|
57
|
+
# this lockfile lying around forever.
|
58
|
+
def self.with_file_lock(&blk)
|
59
|
+
path = lockfile
|
60
|
+
File.open(path, 'w', 0600) do |f|
|
61
|
+
unless f.flock(File::LOCK_EX|File::LOCK_NB)
|
62
|
+
raise "File lock already acquired by another Einhorn process. This likely indicates you tried to run Einhorn masters with the same cmd_name at the same time. This is a pretty rare race condition."
|
63
|
+
end
|
64
|
+
|
65
|
+
blk.call
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.destroy_old_command_socket(path)
|
70
|
+
# Socket isn't actually owned by anyone
|
71
|
+
begin
|
72
|
+
sock = UNIXSocket.new(path)
|
73
|
+
rescue Errno::ECONNREFUSED
|
74
|
+
# This happens with non-socket files and when the listening
|
75
|
+
# end of a socket has exited.
|
76
|
+
rescue Errno::ENOENT
|
77
|
+
# Socket doesn't exist
|
78
|
+
return
|
79
|
+
else
|
80
|
+
# Rats, it's still active
|
81
|
+
sock.close
|
82
|
+
raise Errno::EADDRINUSE.new("Another process (probably another Einhorn) is listening on the Einhorn command socket at #{path}. If you'd like to run this Einhorn as well, pass a `-d PATH_TO_SOCKET` to change the command socket location.")
|
83
|
+
end
|
84
|
+
|
85
|
+
# Socket should still exist, so don't need to handle error.
|
86
|
+
stat = File.stat(path)
|
87
|
+
unless stat.socket?
|
88
|
+
raise Errno::EADDRINUSE.new("Non-socket file present at Einhorn command socket path #{path}. Either remove that file and restart Einhorn, or pass a `-d PATH_TO_SOCKET` to change the command socket location.")
|
89
|
+
end
|
90
|
+
|
91
|
+
Einhorn.log_info("Blowing away old Einhorn command socket at #{path}. This likely indicates a previous Einhorn worker which exited uncleanly.")
|
92
|
+
# Whee, blow it away.
|
93
|
+
File.unlink(path)
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.write_pidfile
|
97
|
+
file = pidfile
|
98
|
+
Einhorn.log_info("Writing PID to #{file}")
|
99
|
+
File.open(file, 'w') {|f| f.write($$)}
|
100
|
+
end
|
101
|
+
|
102
|
+
def self.uninit
|
103
|
+
remove_handlers
|
104
|
+
end
|
105
|
+
|
106
|
+
def self.socket_path
|
107
|
+
Einhorn::State.socket_path || default_socket_path
|
108
|
+
end
|
109
|
+
|
110
|
+
def self.default_socket_path(cmd_name=nil)
|
111
|
+
cmd_name ||= Einhorn::State.cmd_name
|
112
|
+
if cmd_name
|
113
|
+
filename = "einhorn-#{cmd_name}.sock"
|
114
|
+
else
|
115
|
+
filename = "einhorn.sock"
|
116
|
+
end
|
117
|
+
File.join(Dir.tmpdir, filename)
|
118
|
+
end
|
119
|
+
|
120
|
+
def self.lockfile
|
121
|
+
Einhorn::State.lockfile || default_lockfile_path
|
122
|
+
end
|
123
|
+
|
124
|
+
def self.default_lockfile_path(cmd_name=nil)
|
125
|
+
cmd_name ||= Einhorn::State.cmd_name
|
126
|
+
if cmd_name
|
127
|
+
filename = "einhorn-#{cmd_name}.lock"
|
128
|
+
else
|
129
|
+
filename = "einhorn.lock"
|
130
|
+
end
|
131
|
+
File.join(Dir.tmpdir, filename)
|
132
|
+
end
|
133
|
+
|
134
|
+
def self.pidfile
|
135
|
+
Einhorn::State.pidfile || default_pidfile
|
136
|
+
end
|
137
|
+
|
138
|
+
def self.default_pidfile(cmd_name=nil)
|
139
|
+
cmd_name ||= Einhorn::State.cmd_name
|
140
|
+
if cmd_name
|
141
|
+
filename = "einhorn-#{cmd_name}.pid"
|
142
|
+
else
|
143
|
+
filename = "einhorn.pid"
|
144
|
+
end
|
145
|
+
File.join(Dir.tmpdir, filename)
|
146
|
+
end
|
147
|
+
|
148
|
+
## Signals
|
149
|
+
def self.install_handlers
|
150
|
+
Signal.trap("INT") do
|
151
|
+
Einhorn::Command.signal_all("USR2", Einhorn::State.children.keys)
|
152
|
+
Einhorn::State.respawn = false
|
153
|
+
end
|
154
|
+
Signal.trap("TERM") do
|
155
|
+
Einhorn::Command.signal_all("TERM", Einhorn::State.children.keys)
|
156
|
+
Einhorn::State.respawn = false
|
157
|
+
end
|
158
|
+
# Note that quit is a bit different, in that it will actually
|
159
|
+
# make Einhorn quit without waiting for children to exit.
|
160
|
+
Signal.trap("QUIT") do
|
161
|
+
Einhorn::Command.signal_all("QUIT", Einhorn::State.children.keys)
|
162
|
+
Einhorn::State.respawn = false
|
163
|
+
exit(1)
|
164
|
+
end
|
165
|
+
Signal.trap("HUP") {Einhorn::Command.reload}
|
166
|
+
Signal.trap("ALRM") {Einhorn::Command.full_upgrade}
|
167
|
+
Signal.trap("CHLD") {Einhorn::Event.break_loop}
|
168
|
+
Signal.trap("USR2") do
|
169
|
+
Einhorn::Command.signal_all("USR2", Einhorn::State.children.keys)
|
170
|
+
Einhorn::State.respawn = false
|
171
|
+
end
|
172
|
+
at_exit do
|
173
|
+
if Einhorn::State.kill_children_on_exit && Einhorn::TransientState.whatami == :master
|
174
|
+
Einhorn::Command.signal_all("USR2", Einhorn::State.children.keys)
|
175
|
+
Einhorn::State.respawn = false
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def self.remove_handlers
|
181
|
+
%w{INT TERM QUIT HUP ALRM CHLD USR2}.each do |signal|
|
182
|
+
Signal.trap(signal, "DEFAULT")
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
## Commands
|
187
|
+
def self.command(name, description=nil, &code)
|
188
|
+
@@commands[name] = {:description => description, :code => code}
|
189
|
+
end
|
190
|
+
|
191
|
+
def self.process_command(conn, command)
|
192
|
+
response = generate_response(conn, command)
|
193
|
+
if !response.nil?
|
194
|
+
send_message(conn, response)
|
195
|
+
else
|
196
|
+
conn.log_debug("Got back nil response, so not responding to command.")
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def self.send_message(conn, response)
|
201
|
+
if response.kind_of?(String)
|
202
|
+
response = {'message' => response}
|
203
|
+
end
|
204
|
+
message = pack_message(response)
|
205
|
+
conn.write(message)
|
206
|
+
end
|
207
|
+
|
208
|
+
def self.generate_response(conn, command)
|
209
|
+
begin
|
210
|
+
request = JSON.parse(command)
|
211
|
+
rescue JSON::ParserError => e
|
212
|
+
return {
|
213
|
+
'message' => "Could not parse command: #{e}"
|
214
|
+
}
|
215
|
+
end
|
216
|
+
|
217
|
+
unless command_name = request['command']
|
218
|
+
return {
|
219
|
+
'message' => 'No "command" parameter provided; not sure what you want me to do.'
|
220
|
+
}
|
221
|
+
end
|
222
|
+
|
223
|
+
if command_spec = @@commands[command_name]
|
224
|
+
conn.log_debug("Received command: #{command.inspect}")
|
225
|
+
begin
|
226
|
+
return command_spec[:code].call(conn, request)
|
227
|
+
rescue StandardError => e
|
228
|
+
msg = "Error while processing command #{command_name.inspect}: #{e} (#{e.class})\n #{e.backtrace.join("\n ")}"
|
229
|
+
conn.log_error(msg)
|
230
|
+
return msg
|
231
|
+
end
|
232
|
+
else
|
233
|
+
conn.log_debug("Received unrecognized command: #{command.inspect}")
|
234
|
+
return unrecognized_command(conn, request)
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
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
|
+
def self.command_descriptions
|
250
|
+
command_specs = @@commands.select do |_, spec|
|
251
|
+
spec[:description]
|
252
|
+
end.sort_by {|name, _| name}
|
253
|
+
|
254
|
+
command_specs.map do |name, spec|
|
255
|
+
"#{name}: #{spec[:description]}"
|
256
|
+
end.join("\n")
|
257
|
+
end
|
258
|
+
|
259
|
+
def self.unrecognized_command(conn, request)
|
260
|
+
<<EOF
|
261
|
+
Unrecognized command: #{request['command'].inspect}
|
262
|
+
|
263
|
+
#{command_descriptions}
|
264
|
+
EOF
|
265
|
+
end
|
266
|
+
|
267
|
+
# Used by workers
|
268
|
+
command 'worker:ack' do |conn, request|
|
269
|
+
if pid = request['pid']
|
270
|
+
Einhorn::Command.register_manual_ack(pid)
|
271
|
+
else
|
272
|
+
conn.log_error("Invalid request (no pid): #{request.inspect}")
|
273
|
+
end
|
274
|
+
# Throw away this connection in case the application forgets to
|
275
|
+
conn.close
|
276
|
+
nil
|
277
|
+
end
|
278
|
+
|
279
|
+
# Used by einhornsh
|
280
|
+
command 'ehlo' do |conn, request|
|
281
|
+
<<EOF
|
282
|
+
Welcome #{request['user']}! You are speaking to Einhorn Master Process #{$$}#{Einhorn::State.cmd_name ? " (#{Einhorn::State.cmd_name})" : ''}
|
283
|
+
EOF
|
284
|
+
end
|
285
|
+
|
286
|
+
command 'help', 'Print out available commands' do
|
287
|
+
"You are speaking to the Einhorn command socket. You can run the following commands:
|
288
|
+
|
289
|
+
#{command_descriptions}
|
290
|
+
"
|
291
|
+
end
|
292
|
+
|
293
|
+
command 'state', "Get a dump of Einhorn's current state" do
|
294
|
+
Einhorn::Command.dumpable_state.pretty_inspect
|
295
|
+
end
|
296
|
+
|
297
|
+
command 'reload', 'Reload Einhorn' do |conn, _|
|
298
|
+
# TODO: make reload actually work (command socket reopening is
|
299
|
+
# an issue). Would also be nice if user got a confirmation that
|
300
|
+
# the reload completed, though that's not strictly necessary.
|
301
|
+
|
302
|
+
# In the normal case, this will do a write
|
303
|
+
# synchronously. Otherwise, the bytes will be stuck into the
|
304
|
+
# buffer and lost upon reload.
|
305
|
+
send_message(conn, 'Reloading, as commanded')
|
306
|
+
Einhorn::Command.reload
|
307
|
+
|
308
|
+
# Reload should not return
|
309
|
+
raise "Not reachable"
|
310
|
+
end
|
311
|
+
|
312
|
+
command 'inc', 'Increment the number of Einhorn child processes' do
|
313
|
+
Einhorn::Command.increment
|
314
|
+
end
|
315
|
+
|
316
|
+
command 'dec', 'Decrement the number of Einhorn child processes' do
|
317
|
+
Einhorn::Command.decrement
|
318
|
+
end
|
319
|
+
|
320
|
+
command 'quieter', 'Decrease verbosity' do
|
321
|
+
Einhorn::Command.quieter
|
322
|
+
end
|
323
|
+
|
324
|
+
command 'louder', 'Increase verbosity' do
|
325
|
+
Einhorn::Command.louder
|
326
|
+
end
|
327
|
+
|
328
|
+
command 'upgrade', 'Upgrade all Einhorn workers. This may result in Einhorn reloading its own code as well.' do |conn, _|
|
329
|
+
# TODO: send confirmation when this is done
|
330
|
+
send_message(conn, 'Upgrading, as commanded')
|
331
|
+
# This or may not return
|
332
|
+
Einhorn::Command.full_upgrade
|
333
|
+
nil
|
334
|
+
end
|
335
|
+
end
|
336
|
+
end
|