einhorn 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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