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.
- 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
|