einhorn 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -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