einhorn 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,150 @@
1
+ require 'set'
2
+
3
+ module Einhorn
4
+ module Event
5
+ @@loopbreak_reader = nil
6
+ @@loopbreak_writer = nil
7
+ @@readable = {}
8
+ @@writeable = {}
9
+ @@timers = {}
10
+
11
+ def self.init
12
+ readable, writeable = IO.pipe
13
+ @@loopbreak_reader = LoopBreaker.open(readable)
14
+ @@loopbreak_writer = writeable
15
+ end
16
+
17
+ def self.uninit
18
+ # These don't need to persist across Einhorn reloads, so let's not keep.
19
+ @@loopbreak_reader.close
20
+ @@loopbreak_writer.close
21
+ end
22
+
23
+ def self.close_all
24
+ uninit
25
+ (@@readable.values + @@writeable.values).each do |descriptors|
26
+ descriptors.each do |descriptor|
27
+ descriptor.close
28
+ end
29
+ end
30
+ end
31
+
32
+ def self.close_all_for_worker
33
+ close_all
34
+ end
35
+
36
+ def self.persistent_descriptors
37
+ descriptor_sets = @@readable.values + @@writeable.values + @@timers.values
38
+ descriptors = descriptor_sets.inject {|a, b| a | b}
39
+ descriptors.select {|descriptor| Einhorn::Event::Persistent.persistent?(descriptor)}
40
+ end
41
+
42
+ def self.restore_persistent_descriptors(persistent_descriptors)
43
+ persistent_descriptors.each do |descriptor_state|
44
+ Einhorn::Event::Persistent.from_state(descriptor_state)
45
+ end
46
+ end
47
+
48
+ def self.register_readable(reader)
49
+ @@readable[reader.to_io] ||= Set.new
50
+ @@readable[reader.to_io] << reader
51
+ end
52
+
53
+ def self.deregister_readable(reader)
54
+ readers = @@readable[reader.to_io]
55
+ readers.delete(reader)
56
+ @@readable.delete(reader.to_io) if readers.length == 0
57
+ end
58
+
59
+ def self.readable_fds
60
+ readers = @@readable.keys
61
+ Einhorn.log_debug("Readable fds are #{readers.inspect}")
62
+ readers
63
+ end
64
+
65
+ def self.register_writeable(writer)
66
+ @@writeable[writer.to_io] ||= Set.new
67
+ @@writeable[writer.to_io] << writer
68
+ end
69
+
70
+ def self.deregister_writeable(writer)
71
+ writers = @@writeable[writer.to_io]
72
+ writers.delete(writer)
73
+ @@readable.delete(writer.to_io) if writers.length == 0
74
+ end
75
+
76
+ def self.writeable_fds
77
+ writers = @@writeable.select do |io, writers|
78
+ writers.any? {|writer| writer.write_pending?}
79
+ end.map {|io, writers| io}
80
+ Einhorn.log_debug("Writeable fds are #{writers.inspect}")
81
+ writers
82
+ end
83
+
84
+ def self.register_timer(timer)
85
+ @@timers[timer.expires_at] ||= Set.new
86
+ @@timers[timer.expires_at] << timer
87
+ end
88
+
89
+ def self.deregister_timer(timer)
90
+ timers = @@timers[timer.expires_at]
91
+ timers.delete(timer)
92
+ @@timers.delete(timer.expires_at) if timers.length == 0
93
+ end
94
+
95
+ def self.loop_once
96
+ run_selectables
97
+ run_timers
98
+ end
99
+
100
+ def self.timeout
101
+ # (expires_at of the next timer) - now
102
+ if expires_at = @@timers.keys.sort[0]
103
+ expires_at - Time.now
104
+ else
105
+ nil
106
+ end
107
+ end
108
+
109
+ def self.run_selectables
110
+ time = timeout
111
+ Einhorn.log_debug("Loop timeout is #{time.inspect}")
112
+ # Time's already up
113
+ return if time && time < 0
114
+
115
+ readable, writeable, _ = IO.select(readable_fds, writeable_fds, nil, time)
116
+ (readable || []).each do |io|
117
+ @@readable[io].each {|reader| reader.notify_readable}
118
+ end
119
+
120
+ (writeable || []).each do |io|
121
+ @@writeable[io].each {|writer| writer.notify_writeable}
122
+ end
123
+ end
124
+
125
+ def self.run_timers
126
+ @@timers.select {|expires_at, _| expires_at <= Time.now}.each do |expires_at, timers|
127
+ # Going to be modifying the set, so let's dup it.
128
+ timers.dup.each {|timer| timer.ring!}
129
+ end
130
+ end
131
+
132
+ def self.break_loop
133
+ Einhorn.log_debug("Breaking the loop")
134
+ begin
135
+ @@loopbreak_writer.write_nonblock('a')
136
+ rescue Errno::EWOULDBLOCK, Errno::EAGAIN
137
+ Einhorn.log_error("Loop break pipe is full -- probably means that we are quite backlogged")
138
+ end
139
+ end
140
+ end
141
+ end
142
+
143
+ require 'einhorn/event/persistent'
144
+ require 'einhorn/event/timer'
145
+
146
+ require 'einhorn/event/abstract_text_descriptor'
147
+ require 'einhorn/event/ack_timer'
148
+ require 'einhorn/event/command_server'
149
+ require 'einhorn/event/connection'
150
+ require 'einhorn/event/loop_breaker'
@@ -0,0 +1,3 @@
1
+ module Einhorn
2
+ VERSION = '0.3.0'
3
+ end
@@ -0,0 +1,94 @@
1
+ require 'einhorn/client'
2
+ require 'einhorn/command/interface'
3
+
4
+ module Einhorn
5
+ module Worker
6
+ class WorkerError < RuntimeError; end
7
+
8
+ def self.is_worker?
9
+ begin
10
+ ensure_worker!
11
+ rescue WorkerError
12
+ false
13
+ else
14
+ true
15
+ end
16
+ end
17
+
18
+ def self.ensure_worker!
19
+ # Make sure that EINHORN_MASTER_PID is my parent
20
+ if ppid_s = ENV['EINHORN_MASTER_PID']
21
+ ppid = ppid_s.to_i
22
+ raise WorkerError.new("EINHORN_MASTER_PID environment variable is #{ppid_s.inspect}, but my parent's pid is #{Process.ppid.inspect}. This probably means that I am a subprocess of an Einhorn worker, but am not one myself.") unless Process.ppid == ppid
23
+ true
24
+ else
25
+ raise WorkerError.new("No EINHORN_MASTER_PID environment variable set. Are you running your process under Einhorn?") unless Process.ppid == ppid
26
+ end
27
+ end
28
+
29
+ def self.ack(*args)
30
+ begin
31
+ ack!(*args)
32
+ rescue WorkerError
33
+ end
34
+ end
35
+
36
+ # Call this once your app is up and running in a good state.
37
+ # Arguments:
38
+ #
39
+ # @discovery: How to discover the master process's command socket.
40
+ # :env: Discover the path from ENV['EINHORN_SOCK_PATH']
41
+ # :fd: Just use the file descriptor in ENV['EINHORN_FD'].
42
+ # Must run the master with the -b flag. This is mostly
43
+ # useful if you don't have a nice library like Einhorn::Worker.
44
+ # Then @arg being true causes the FD to be left open after ACK;
45
+ # otherwise it is closed.
46
+ # :direct: Provide the path to the command socket in @arg.
47
+ #
48
+ # TODO: add a :fileno option? Easy to implement; not sure if it'd
49
+ # be useful for anything. Maybe if it's always fd 3, because then
50
+ # the user wouldn't have to provide an arg.
51
+ def self.ack!(discovery=:env, arg=nil)
52
+ ensure_worker!
53
+ close_after_use = true
54
+
55
+ case discovery
56
+ when :env
57
+ socket = ENV['EINHORN_SOCK_PATH']
58
+ client = Einhorn::Client.for_path(socket)
59
+ when :fd
60
+ raise "No EINHORN_FD provided in environment. Did you run einhorn with the -b flag?" unless fd_str = ENV['EINHORN_FD']
61
+
62
+ fd = Integer(fd_str)
63
+ client = Einhorn::Client.for_fd(fd)
64
+ close_after_use = false if arg
65
+ when :direct
66
+ socket = arg
67
+ client = Einhorn::Client.for_path(socket)
68
+ else
69
+ raise "Unrecognized socket discovery mechanism: #{discovery.inspect}. Must be one of :filesystem, :argv, or :direct"
70
+ end
71
+
72
+ client.command({
73
+ 'command' => 'worker:ack',
74
+ 'pid' => $$
75
+ })
76
+
77
+ client.close if close_after_use
78
+ true
79
+ end
80
+
81
+ # Call this to handle graceful shutdown requests to your app.
82
+ def self.graceful_shutdown(&blk)
83
+ Signal.trap('USR2', &blk)
84
+ end
85
+
86
+ private
87
+
88
+ def self.socket_from_filesystem(cmd_name)
89
+ ppid = Process.ppid
90
+ socket_path_file = Einhorn::Command::Interface.socket_path_file(ppid)
91
+ File.read(socket_path_file)
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,56 @@
1
+ module Einhorn
2
+ module WorkerPool
3
+ def self.unsignaled_workers
4
+ Einhorn::State.children.select do |pid, spec|
5
+ spec[:signaled].length == 0
6
+ end.map {|pid, _| pid}
7
+ end
8
+
9
+ def self.modern_workers_with_state
10
+ Einhorn::State.children.select do |pid, spec|
11
+ spec[:version] == Einhorn::State.version
12
+ end
13
+ end
14
+
15
+ def self.acked_modern_workers_with_state
16
+ modern_workers_with_state.select {|pid, spec| spec[:acked]}
17
+ end
18
+
19
+ def self.modern_workers
20
+ modern_workers_with_state.map {|pid, _| pid}
21
+ end
22
+
23
+ def self.acked_modern_workers
24
+ acked_modern_workers_with_state.map {|pid, _| pid}
25
+ end
26
+
27
+ def self.acked_unsignaled_modern_workers
28
+ acked_modern_workers_with_state.select do |_, spec|
29
+ spec[:signaled].length == 0
30
+ end.map {|pid, _| pid}
31
+ end
32
+
33
+ # Use the number of modern workers, rather than unsignaled modern
34
+ # workers. This means if e.g. we do bunch of decs and then incs,
35
+ # any workers which haven't died yet will count towards our number
36
+ # of workers. Since workers really should be dying shortly after
37
+ # they are USR2'd, that indicates a bad state and we shouldn't
38
+ # make it worse by spinning up more processes. Once they die,
39
+ # order will be restored.
40
+ def self.missing_worker_count
41
+ ack_target - modern_workers.length
42
+ end
43
+
44
+ def self.ack_count
45
+ acked_unsignaled_modern_workers.length
46
+ end
47
+
48
+ def self.ack_target
49
+ Einhorn::State.config[:number]
50
+ end
51
+
52
+ def self.old_workers
53
+ unsignaled_workers - modern_workers
54
+ end
55
+ end
56
+ end
data/lib/einhorn.rb ADDED
@@ -0,0 +1,282 @@
1
+ require 'fcntl'
2
+ require 'optparse'
3
+ require 'pp'
4
+ require 'set'
5
+ require 'socket'
6
+ require 'tmpdir'
7
+ require 'yaml'
8
+
9
+ require 'rubygems'
10
+
11
+ module Einhorn
12
+ module AbstractState
13
+ def default_state; raise NotImplementedError.new('Override in extended modules'); end
14
+ def state; @state ||= default_state; end
15
+ def state=(v); @state = v; end
16
+
17
+ def method_missing(name, *args)
18
+ if (name.to_s =~ /(.*)=$/) && state.has_key?($1.to_sym)
19
+ state.send(:[]=, $1.to_sym, *args)
20
+ elsif state.has_key?(name)
21
+ state[name]
22
+ else
23
+ ds = default_state
24
+ if ds.has_key?(name)
25
+ ds[name]
26
+ else
27
+ super
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ module State
34
+ extend AbstractState
35
+ def self.default_state
36
+ {
37
+ :children => {},
38
+ :config => {:number => 1, :backlog => 100, :seconds => 1},
39
+ :versions => {},
40
+ :version => 0,
41
+ :sockets => {},
42
+ :orig_cmd => nil,
43
+ :cmd => nil,
44
+ :script_name => nil,
45
+ :respawn => true,
46
+ :upgrading => false,
47
+ :reloading_for_preload_upgrade => false,
48
+ :path => nil,
49
+ :cmd_name => nil,
50
+ :verbosity => 1,
51
+ :generation => 0,
52
+ :last_spinup => nil,
53
+ :ack_mode => {:type => :timer, :timeout => 1},
54
+ :kill_children_on_exit => false,
55
+ :command_socket_as_fd => false,
56
+ :socket_path => nil,
57
+ :pidfile => nil,
58
+ :lockfile => nil
59
+ }
60
+ end
61
+ end
62
+
63
+ module TransientState
64
+ extend AbstractState
65
+ def self.default_state
66
+ {
67
+ :whatami => :master,
68
+ :preloaded => false,
69
+ :script_name => nil,
70
+ :argv => [],
71
+ :has_outstanding_spinup_timer => false,
72
+ :stateful => nil,
73
+ # Holds references so that the GC doesn't go and close your sockets.
74
+ :socket_handles => Set.new
75
+ }
76
+ end
77
+ end
78
+
79
+ def self.restore_state(state)
80
+ parsed = YAML.load(state)
81
+ Einhorn::State.state = parsed[:state]
82
+ Einhorn::Event.restore_persistent_descriptors(parsed[:persistent_descriptors])
83
+ # Do this after setting state so verbosity is right9
84
+ Einhorn.log_info("Using loaded state: #{parsed.inspect}")
85
+ end
86
+
87
+ def self.print_state
88
+ log_info(Einhorn::State.state.pretty_inspect)
89
+ end
90
+
91
+ def self.bind(addr, port, flags)
92
+ log_info("Binding to #{addr}:#{port} with flags #{flags.inspect}")
93
+ sd = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
94
+
95
+ if flags.include?('r') || flags.include?('so_reuseaddr')
96
+ sd.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1)
97
+ end
98
+
99
+ sd.bind(Socket.pack_sockaddr_in(port, addr))
100
+ sd.listen(Einhorn::State.config[:backlog])
101
+
102
+ if flags.include?('n') || flags.include?('o_nonblock')
103
+ fl = sd.fcntl(Fcntl::F_GETFL)
104
+ sd.fcntl(Fcntl::F_SETFL, fl | Fcntl::O_NONBLOCK)
105
+ end
106
+
107
+ Einhorn::TransientState.socket_handles << sd
108
+ sd.fileno
109
+ end
110
+
111
+ # Implement these ourselves so it plays nicely with state persistence
112
+ def self.log_debug(msg)
113
+ $stderr.puts("#{log_tag} DEBUG: #{msg}") if Einhorn::State.verbosity <= 0
114
+ end
115
+ def self.log_info(msg)
116
+ $stderr.puts("#{log_tag} INFO: #{msg}") if Einhorn::State.verbosity <= 1
117
+ end
118
+ def self.log_error(msg)
119
+ $stderr.puts("#{log_tag} ERROR: #{msg}") if Einhorn::State.verbosity <= 2
120
+ end
121
+
122
+ private
123
+
124
+ def self.log_tag
125
+ case whatami = Einhorn::TransientState.whatami
126
+ when :master
127
+ "[MASTER #{$$}]"
128
+ when :worker
129
+ "[WORKER #{$$}]"
130
+ when :state_passer
131
+ "[STATE_PASSER #{$$}]"
132
+ else
133
+ "[UNKNOWN (#{whatami.inspect}) #{$$}]"
134
+ end
135
+ end
136
+
137
+ public
138
+
139
+ def self.which(cmd)
140
+ if cmd.include?('/')
141
+ return cmd if File.exists?(cmd)
142
+ raise "Could not find #{cmd}"
143
+ else
144
+ ENV['PATH'].split(':').each do |f|
145
+ abs = File.join(f, cmd)
146
+ return abs if File.exists?(abs)
147
+ end
148
+ raise "Could not find #{cmd} in PATH"
149
+ end
150
+ end
151
+
152
+ # Not really a thing, but whatever.
153
+ def self.is_script(file)
154
+ File.open(file) do |f|
155
+ bytes = f.read(2)
156
+ bytes == '#!'
157
+ end
158
+ end
159
+
160
+ def self.preload
161
+ if path = Einhorn::State.path
162
+ set_argv(Einhorn::State.cmd, false)
163
+
164
+ begin
165
+ # If it's not going to be requireable, then load it.
166
+ if !path.end_with?('.rb') && File.exists?(path)
167
+ log_info("Loading #{path} (if this hangs, make sure your code can be properly loaded as a library)")
168
+ load path
169
+ else
170
+ log_info("Requiring #{path} (if this hangs, make sure your code can be properly loaded as a library)")
171
+ require path
172
+ end
173
+ rescue Exception => e
174
+ log_info("Proceeding with postload -- could not load #{path}: #{e} (#{e.class})\n #{e.backtrace.join("\n ")}")
175
+ else
176
+ if defined?(einhorn_main)
177
+ log_info("Successfully loaded #{path}")
178
+ Einhorn::TransientState.preloaded = true
179
+ else
180
+ log_info("Proceeding with postload -- loaded #{path}, but no einhorn_main method was defined")
181
+ end
182
+ end
183
+ end
184
+ end
185
+
186
+ def self.set_argv(cmd, set_ps_name)
187
+ # TODO: clean up this hack
188
+ idx = 0
189
+ if cmd[0] =~ /(^|\/)ruby$/
190
+ idx = 1
191
+ elsif !is_script(cmd[0])
192
+ log_info("WARNING: Going to set $0 to #{cmd[idx]}, but it doesn't look like a script")
193
+ end
194
+
195
+ if set_ps_name
196
+ # Note this will mess up $0 if we try using it in our code, but
197
+ # we don't so that's basically ok. It's a bit annoying that this
198
+ # is how Ruby exposes changing the output of ps. Note that Ruby
199
+ # doesn't seem to shrink your cmdline buffer, so ps just ends up
200
+ # having lots of trailing spaces if we set $0 to something
201
+ # short. In the future, we could try to not pass einhorn's
202
+ # state in ARGV.
203
+ $0 = worker_ps_name
204
+ end
205
+
206
+ ARGV[0..-1] = cmd[idx+1..-1]
207
+ log_info("Set#{set_ps_name ? " $0 = #{$0.inspect}, " : nil} ARGV = #{ARGV.inspect}")
208
+ end
209
+
210
+ def self.set_master_ps_name
211
+ $0 = master_ps_name
212
+ end
213
+
214
+ def self.master_ps_name
215
+ "einhorn: #{worker_ps_name}"
216
+ end
217
+
218
+ def self.worker_ps_name
219
+ Einhorn::State.cmd_name ? "ruby #{Einhorn::State.cmd_name}" : Einhorn::State.orig_cmd.join(' ')
220
+ end
221
+
222
+ def self.socketify!(cmd)
223
+ cmd.map! do |arg|
224
+ if arg =~ /^(.*=|)srv:([^:]+):(\d+)((?:,\w+)*)$/
225
+ opt = $1
226
+ host = $2
227
+ port = $3
228
+ flags = $4.split(',').select {|flag| flag.length > 0}.map {|flag| flag.downcase}
229
+ fd = (Einhorn::State.sockets[[host, port]] ||= bind(host, port, flags))
230
+ "#{opt}#{fd}"
231
+ else
232
+ arg
233
+ end
234
+ end
235
+ end
236
+
237
+ def self.run
238
+ Einhorn::Command::Interface.init
239
+ Einhorn::Event.init
240
+
241
+ unless Einhorn::TransientState.stateful
242
+ if Einhorn::State.config[:number] < 1
243
+ log_error("You need to spin up at least at least 1 copy of the process")
244
+ return
245
+ end
246
+ Einhorn::Command::Interface.persistent_init
247
+
248
+ Einhorn::State.orig_cmd = ARGV.dup
249
+ Einhorn::State.cmd = ARGV.dup
250
+ # TODO: don't actually alter ARGV[0]?
251
+ Einhorn::State.cmd[0] = which(Einhorn::State.cmd[0])
252
+ socketify!(Einhorn::State.cmd)
253
+ end
254
+
255
+ set_master_ps_name
256
+ preload
257
+
258
+ # In the middle of upgrading
259
+ if Einhorn::State.reloading_for_preload_upgrade
260
+ Einhorn::Command.upgrade_workers
261
+ Einhorn::State.reloading_for_preload_upgrade = false
262
+ end
263
+
264
+ while Einhorn::State.respawn || Einhorn::State.children.size > 0
265
+ log_debug("Entering event loop")
266
+ # All of these are non-blocking
267
+ Einhorn::Command.reap
268
+ Einhorn::Command.replenish
269
+ Einhorn::Command.cull
270
+
271
+ # Make sure to do this last, as it's blocking.
272
+ Einhorn::Event.loop_once
273
+ end
274
+ end
275
+ end
276
+
277
+ require 'einhorn/command'
278
+ require 'einhorn/client'
279
+ require 'einhorn/event'
280
+ require 'einhorn/worker'
281
+ require 'einhorn/worker_pool'
282
+ require 'einhorn/version'
@@ -0,0 +1,7 @@
1
+ require 'rubygems'
2
+
3
+ require 'test/unit'
4
+ require 'mocha'
5
+ require 'shoulda'
6
+
7
+ $:.unshift(File.join(File.dirname(__FILE__), '../lib'))
@@ -0,0 +1,47 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '../../../test_helper'))
2
+
3
+ require 'einhorn'
4
+
5
+ class InterfaceTest < Test::Unit::TestCase
6
+ include Einhorn::Command
7
+
8
+ context "when a command is received" do
9
+ should "call that command" do
10
+ conn = stub(:log_debug => nil)
11
+ conn.expects(:write).once.with do |message|
12
+ parsed = JSON.parse(message)
13
+ parsed['message'] =~ /Welcome gdb/
14
+ end
15
+ request = {
16
+ 'command' => 'ehlo',
17
+ 'user' => 'gdb'
18
+ }
19
+ Interface.process_command(conn, JSON.generate(request))
20
+ end
21
+ end
22
+
23
+ context "when an unrecognized command is received" do
24
+ should "call the unrecognized_command method" do
25
+ conn = stub(:log_debug => nil)
26
+ Interface.expects(:unrecognized_command).once
27
+ request = {
28
+ 'command' => 'made-up',
29
+ }
30
+ Interface.process_command(conn, JSON.generate(request))
31
+ end
32
+ end
33
+
34
+ context "when a worker ack is received" do
35
+ should "register ack and close the connection" do
36
+ conn = stub(:log_debug => nil)
37
+ conn.expects(:close).once
38
+ conn.expects(:write).never
39
+ request = {
40
+ 'command' => 'worker:ack',
41
+ 'pid' => 1234
42
+ }
43
+ Einhorn::Command.expects(:register_manual_ack).once.with(1234)
44
+ Interface.process_command(conn, JSON.generate(request))
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,21 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '../../test_helper'))
2
+
3
+ require 'einhorn'
4
+
5
+ class CommandTest < Test::Unit::TestCase
6
+ include Einhorn
7
+
8
+ context "when running quieter" do
9
+ should "increase the verbosity threshold" do
10
+ Einhorn::State.stubs(:verbosity => 1)
11
+ Einhorn::State.expects(:verbosity=).once.with(2).returns(2)
12
+ Command.quieter
13
+ end
14
+
15
+ should "max out at 2" do
16
+ Einhorn::State.stubs(:verbosity => 2)
17
+ Einhorn::State.expects(:verbosity=).never
18
+ Command.quieter
19
+ end
20
+ end
21
+ end