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
@@ -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,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'
|
data/test/test_helper.rb
ADDED
@@ -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
|