einhorn 0.7.4 → 0.8.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +37 -0
- data/README.md.in +21 -3
- data/bin/einhorn +17 -2
- data/example/pool_worker.rb +1 -1
- data/lib/einhorn.rb +40 -6
- data/lib/einhorn/client.rb +2 -3
- data/lib/einhorn/command.rb +103 -15
- data/lib/einhorn/command/interface.rb +11 -0
- data/lib/einhorn/event.rb +10 -1
- data/lib/einhorn/prctl.rb +26 -0
- data/lib/einhorn/prctl_linux.rb +49 -0
- data/lib/einhorn/version.rb +1 -1
- data/lib/einhorn/worker.rb +47 -25
- data/test/integration/_lib/fixtures/exit_during_upgrade/exiting_server.rb +1 -0
- data/test/integration/_lib/fixtures/pdeathsig_printer/pdeathsig_printer.rb +29 -0
- data/test/integration/_lib/fixtures/signal_timeout/sleepy_server.rb +23 -0
- data/test/integration/_lib/fixtures/upgrade_project/upgrading_server.rb +2 -0
- data/test/integration/_lib/helpers/einhorn_helpers.rb +5 -0
- data/test/integration/pdeathsig.rb +26 -0
- data/test/integration/upgrading.rb +47 -0
- data/test/unit/_lib/bad_worker.rb +7 -0
- data/test/unit/_lib/sleep_worker.rb +5 -0
- data/test/unit/einhorn.rb +41 -3
- data/test/unit/einhorn/command.rb +114 -0
- metadata +36 -47
@@ -289,6 +289,17 @@ EOF
|
|
289
289
|
nil
|
290
290
|
end
|
291
291
|
|
292
|
+
command 'worker:ping' do |conn, request|
|
293
|
+
if pid = request['pid']
|
294
|
+
Einhorn::Command.register_ping(pid, request['request_id'])
|
295
|
+
else
|
296
|
+
conn.log_error("Invalid request (no pid): #{request.inspect}")
|
297
|
+
end
|
298
|
+
# Throw away this connection in case the application forgets to
|
299
|
+
conn.close
|
300
|
+
nil
|
301
|
+
end
|
302
|
+
|
292
303
|
# Used by einhornsh
|
293
304
|
command 'ehlo' do |conn, request|
|
294
305
|
<<EOF
|
data/lib/einhorn/event.rb
CHANGED
@@ -4,6 +4,7 @@ module Einhorn
|
|
4
4
|
module Event
|
5
5
|
@@loopbreak_reader = nil
|
6
6
|
@@loopbreak_writer = nil
|
7
|
+
@@default_timeout = nil
|
7
8
|
@@signal_actions = []
|
8
9
|
@@readable = {}
|
9
10
|
@@writeable = {}
|
@@ -120,7 +121,7 @@ module Einhorn
|
|
120
121
|
if expires_at = @@timers.keys.sort[0]
|
121
122
|
expires_at - Time.now
|
122
123
|
else
|
123
|
-
|
124
|
+
@@default_timeout
|
124
125
|
end
|
125
126
|
end
|
126
127
|
|
@@ -165,6 +166,14 @@ module Einhorn
|
|
165
166
|
Einhorn.log_error("Loop break pipe is full -- probably means that we are quite backlogged")
|
166
167
|
end
|
167
168
|
end
|
169
|
+
|
170
|
+
def self.default_timeout=(val)
|
171
|
+
@@default_timeout = val.to_i == 0 ? nil : val.to_i
|
172
|
+
end
|
173
|
+
|
174
|
+
def self.default_timeout
|
175
|
+
@@default_timeout
|
176
|
+
end
|
168
177
|
end
|
169
178
|
end
|
170
179
|
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Einhorn
|
2
|
+
class PrctlAbstract
|
3
|
+
def self.get_pdeathsig
|
4
|
+
raise NotImplementedError
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.set_pdeathsig(signal)
|
8
|
+
raise NotImplementedError
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class PrctlUnimplemented < PrctlAbstract
|
13
|
+
# Deliberately empty; NotImplementedError is intended
|
14
|
+
end
|
15
|
+
|
16
|
+
if RUBY_PLATFORM =~ /linux/ then
|
17
|
+
begin
|
18
|
+
require 'einhorn/prctl_linux'
|
19
|
+
Prctl = PrctlLinux
|
20
|
+
rescue LoadError
|
21
|
+
Prctl = PrctlUnimplemented
|
22
|
+
end
|
23
|
+
else
|
24
|
+
Prctl = PrctlUnimplemented
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'fiddle'
|
2
|
+
require 'fiddle/import'
|
3
|
+
|
4
|
+
module Einhorn
|
5
|
+
module PrctlRaw
|
6
|
+
extend Fiddle::Importer
|
7
|
+
dlload Fiddle.dlopen(nil) # libc
|
8
|
+
extern 'int prctl(int, unsigned long, unsigned long, unsigned long, unsigned long)'
|
9
|
+
|
10
|
+
# From linux/prctl.h
|
11
|
+
SET_PDEATHSIG = 1
|
12
|
+
GET_PDEATHSIG = 2
|
13
|
+
end
|
14
|
+
|
15
|
+
class PrctlLinux < PrctlAbstract
|
16
|
+
# Reading integers is hard with fiddle. :(
|
17
|
+
IntStruct = Fiddle::CStructBuilder.create(Fiddle::CStruct, [Fiddle::TYPE_INT], ['i'])
|
18
|
+
|
19
|
+
def self.get_pdeathsig
|
20
|
+
out = IntStruct.malloc
|
21
|
+
out.i = 0
|
22
|
+
if PrctlRaw.prctl(PrctlRaw::GET_PDEATHSIG, out.to_i, 0, 0, 0) != 0 then
|
23
|
+
raise SystemCallError.new("get_pdeathsig", Fiddle.last_error)
|
24
|
+
end
|
25
|
+
|
26
|
+
signo = out.i
|
27
|
+
if signo == 0 then
|
28
|
+
return nil
|
29
|
+
end
|
30
|
+
|
31
|
+
return Signal.signame(signo)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.set_pdeathsig(signal)
|
35
|
+
case
|
36
|
+
when signal == nil
|
37
|
+
signo = 0
|
38
|
+
when signal.instance_of?(String)
|
39
|
+
signo = Signal.list.fetch(signal)
|
40
|
+
else
|
41
|
+
signo = signal
|
42
|
+
end
|
43
|
+
|
44
|
+
if PrctlRaw.prctl(PrctlRaw::SET_PDEATHSIG, signo, 0, 0, 0) != 0 then
|
45
|
+
raise SystemCallError.new("set_pdeathsig(#{signal})", Fiddle.last_error)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/lib/einhorn/version.rb
CHANGED
data/lib/einhorn/worker.rb
CHANGED
@@ -66,33 +66,28 @@ module Einhorn
|
|
66
66
|
# be useful for anything. Maybe if it's always fd 3, because then
|
67
67
|
# the user wouldn't have to provide an arg.
|
68
68
|
def self.ack!(discovery=:env, arg=nil)
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
case discovery
|
73
|
-
when :env
|
74
|
-
socket = ENV['EINHORN_SOCK_PATH']
|
75
|
-
client = Einhorn::Client.for_path(socket)
|
76
|
-
when :fd
|
77
|
-
raise "No EINHORN_SOCK_FD provided in environment. Did you run einhorn with the -g flag?" unless fd_str = ENV['EINHORN_SOCK_FD']
|
78
|
-
|
79
|
-
fd = Integer(fd_str)
|
80
|
-
client = Einhorn::Client.for_fd(fd)
|
81
|
-
close_after_use = false if arg
|
82
|
-
when :direct
|
83
|
-
socket = arg
|
84
|
-
client = Einhorn::Client.for_path(socket)
|
85
|
-
else
|
86
|
-
raise "Unrecognized socket discovery mechanism: #{discovery.inspect}. Must be one of :filesystem, :argv, or :direct"
|
69
|
+
handle_command_socket(discovery, arg) do |client|
|
70
|
+
client.send_command('command' => 'worker:ack', 'pid' => $$)
|
87
71
|
end
|
72
|
+
end
|
88
73
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
74
|
+
# Call this to indicate your child process is up and in a healthy state.
|
75
|
+
# Arguments:
|
76
|
+
#
|
77
|
+
# @request_id: Identifies the request ID of the worker, can be used to debug wedged workers.
|
78
|
+
#
|
79
|
+
# @discovery: How to discover the master process's command socket.
|
80
|
+
# :env: Discover the path from ENV['EINHORN_SOCK_PATH']
|
81
|
+
# :fd: Just use the file descriptor in ENV['EINHORN_SOCK_FD'].
|
82
|
+
# Must run the master with the -g flag. This is mostly
|
83
|
+
# useful if you don't have a nice library like Einhorn::Worker.
|
84
|
+
# Then @arg being true causes the FD to be left open after ACK;
|
85
|
+
# otherwise it is closed.
|
86
|
+
# :direct: Provide the path to the command socket in @arg.
|
87
|
+
def self.ping!(request_id, discovery=:env, arg=nil)
|
88
|
+
handle_command_socket(discovery, arg) do |client|
|
89
|
+
client.send_command('command' => 'worker:ping', 'pid' => $$, 'request_id' => request_id)
|
90
|
+
end
|
96
91
|
end
|
97
92
|
|
98
93
|
def self.socket(number=nil)
|
@@ -139,6 +134,33 @@ module Einhorn
|
|
139
134
|
|
140
135
|
private
|
141
136
|
|
137
|
+
def self.handle_command_socket(discovery, contextual_arg)
|
138
|
+
ensure_worker!
|
139
|
+
close_after_use = true
|
140
|
+
|
141
|
+
case discovery
|
142
|
+
when :env
|
143
|
+
socket = ENV['EINHORN_SOCK_PATH']
|
144
|
+
client = Einhorn::Client.for_path(socket)
|
145
|
+
when :fd
|
146
|
+
raise "No EINHORN_SOCK_FD provided in environment. Did you run einhorn with the -g flag?" unless fd_str = ENV['EINHORN_SOCK_FD']
|
147
|
+
|
148
|
+
fd = Integer(fd_str)
|
149
|
+
client = Einhorn::Client.for_fd(fd)
|
150
|
+
close_after_use = false if contextual_arg
|
151
|
+
when :direct
|
152
|
+
socket = contextual_arg
|
153
|
+
client = Einhorn::Client.for_path(socket)
|
154
|
+
else
|
155
|
+
raise "Unrecognized socket discovery mechanism: #{discovery.inspect}. Must be one of :filesystem, :argv, or :direct"
|
156
|
+
end
|
157
|
+
|
158
|
+
yield client
|
159
|
+
client.close if close_after_use
|
160
|
+
|
161
|
+
true
|
162
|
+
end
|
163
|
+
|
142
164
|
def self.socket_from_filesystem(cmd_name)
|
143
165
|
ppid = Process.ppid
|
144
166
|
socket_path_file = Einhorn::Command::Interface.socket_path_file(ppid)
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
require 'socket'
|
3
|
+
require 'einhorn/worker'
|
4
|
+
require 'einhorn/prctl'
|
5
|
+
|
6
|
+
def einhorn_main
|
7
|
+
serv = Socket.for_fd(Einhorn::Worker.socket!)
|
8
|
+
|
9
|
+
Signal.trap("USR2") { exit }
|
10
|
+
|
11
|
+
begin
|
12
|
+
output = Einhorn::Prctl.get_pdeathsig
|
13
|
+
if output == nil then
|
14
|
+
output = "nil"
|
15
|
+
end
|
16
|
+
rescue NotImplementedError
|
17
|
+
output = "not implemented"
|
18
|
+
end
|
19
|
+
|
20
|
+
Einhorn::Worker.ack!
|
21
|
+
while true
|
22
|
+
s, _ = serv.accept
|
23
|
+
s.write(output)
|
24
|
+
s.flush
|
25
|
+
s.close
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
einhorn_main if $0 == __FILE__
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
require 'socket'
|
3
|
+
require 'einhorn/worker'
|
4
|
+
|
5
|
+
def einhorn_main
|
6
|
+
serv = Socket.for_fd(Einhorn::Worker.socket!)
|
7
|
+
Einhorn::Worker.ack!
|
8
|
+
Einhorn::Worker.ping!("id-1")
|
9
|
+
|
10
|
+
Signal.trap('USR2') do
|
11
|
+
sleep ENV.fetch("TRAP_SLEEP").to_i
|
12
|
+
exit
|
13
|
+
end
|
14
|
+
|
15
|
+
while true
|
16
|
+
s, _ = serv.accept
|
17
|
+
s.write($$)
|
18
|
+
s.flush
|
19
|
+
s.close
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
einhorn_main if $0 == __FILE__
|
@@ -9,6 +9,8 @@ def einhorn_main
|
|
9
9
|
$stderr.puts "Worker has a socket"
|
10
10
|
Einhorn::Worker.ack!
|
11
11
|
$stderr.puts "Worker sent ack to einhorn"
|
12
|
+
Einhorn::Worker.ping!("id-1")
|
13
|
+
$stderr.puts "Worker has sent a ping to einhorn"
|
12
14
|
while true
|
13
15
|
s, addrinfo = serv.accept
|
14
16
|
$stderr.puts "Worker got a socket!"
|
@@ -106,6 +106,11 @@ module Helpers
|
|
106
106
|
open_port.close
|
107
107
|
end
|
108
108
|
|
109
|
+
def get_state(client)
|
110
|
+
client.send_command('command' => 'state')
|
111
|
+
YAML.load(client.receive_message['message'])[:state]
|
112
|
+
end
|
113
|
+
|
109
114
|
def wait_for_open_port
|
110
115
|
max_retries = 50
|
111
116
|
begin
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require(File.expand_path('_lib', File.dirname(__FILE__)))
|
2
|
+
|
3
|
+
class PdeathsigTest < EinhornIntegrationTestCase
|
4
|
+
include Helpers::EinhornHelpers
|
5
|
+
|
6
|
+
describe 'when run with -k' do
|
7
|
+
before do
|
8
|
+
@dir = prepare_fixture_directory('pdeathsig_printer')
|
9
|
+
@port = find_free_port
|
10
|
+
@server_program = File.join(@dir, 'pdeathsig_printer.rb')
|
11
|
+
@socket_path = File.join(@dir, 'einhorn.sock')
|
12
|
+
end
|
13
|
+
after { cleanup_fixtured_directories }
|
14
|
+
|
15
|
+
it 'sets pdeathsig to USR2 in the child process' do
|
16
|
+
with_running_einhorn(%W{einhorn -m manual -b 127.0.0.1:#{@port} -d #{@socket_path} -k -- ruby #{@server_program}}) do |process|
|
17
|
+
wait_for_open_port
|
18
|
+
output = read_from_port
|
19
|
+
if output != "not implemented" then
|
20
|
+
assert_equal("USR2", output)
|
21
|
+
end
|
22
|
+
process.terminate
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -78,6 +78,10 @@ class UpgradeTests < EinhornIntegrationTestCase
|
|
78
78
|
state = YAML.load(resp['message'])
|
79
79
|
assert_equal(1, state[:state][:children].count)
|
80
80
|
|
81
|
+
child = state[:state][:children].first.last
|
82
|
+
assert_in_delta(child[:pinged_at], Time.now, 60)
|
83
|
+
assert_equal("id-1", child[:pinged_request_id])
|
84
|
+
|
81
85
|
process.terminate
|
82
86
|
end
|
83
87
|
end
|
@@ -154,4 +158,47 @@ class UpgradeTests < EinhornIntegrationTestCase
|
|
154
158
|
end
|
155
159
|
end
|
156
160
|
end
|
161
|
+
|
162
|
+
describe "with --signal-timeout" do
|
163
|
+
before do
|
164
|
+
@dir = prepare_fixture_directory('signal_timeout')
|
165
|
+
@port = find_free_port
|
166
|
+
@server_program = File.join(@dir, "sleepy_server.rb")
|
167
|
+
@socket_path = File.join(@dir, "einhorn.sock")
|
168
|
+
end
|
169
|
+
|
170
|
+
after { cleanup_fixtured_directories }
|
171
|
+
|
172
|
+
it 'issues a SIGKILL to outdated children when signal-timeout has passed' do
|
173
|
+
signal_timeout = 2
|
174
|
+
sleep_for = 10
|
175
|
+
cmd = %W{
|
176
|
+
einhorn
|
177
|
+
-b 127.0.0.1:#{@port}
|
178
|
+
-d #{@socket_path}
|
179
|
+
--signal-timeout #{signal_timeout}
|
180
|
+
-- ruby #{@server_program}
|
181
|
+
}
|
182
|
+
|
183
|
+
with_running_einhorn(cmd, env: ENV.to_h.merge({'TRAP_SLEEP' => sleep_for.to_s})) do |process|
|
184
|
+
wait_for_open_port
|
185
|
+
client = Einhorn::Client.for_path(@socket_path)
|
186
|
+
einhornsh(%W{-d #{@socket_path} -e upgrade})
|
187
|
+
|
188
|
+
state = get_state(client)
|
189
|
+
assert_equal(2, state[:children].count)
|
190
|
+
signaled_children = state[:children].select{|_,c| c[:signaled].length > 0}
|
191
|
+
assert_equal(1, signaled_children.length)
|
192
|
+
|
193
|
+
sleep(signal_timeout * 2)
|
194
|
+
|
195
|
+
state = get_state(client)
|
196
|
+
assert_equal(1, state[:children].count)
|
197
|
+
signaled_children = state[:children].select{|_,c| c[:signaled].length > 0}
|
198
|
+
assert_equal(0, signaled_children.length)
|
199
|
+
|
200
|
+
process.terminate
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
157
204
|
end
|
data/test/unit/einhorn.rb
CHANGED
@@ -10,7 +10,7 @@ class EinhornTest < EinhornTestCase
|
|
10
10
|
|
11
11
|
it "correctly parses srv: arguments" do
|
12
12
|
cmd = ['foo', 'srv:1.2.3.4:123,llama,test', 'bar']
|
13
|
-
Einhorn.expects(:bind).once.with('1.2.3.4', '123', ['llama', 'test']).returns(4)
|
13
|
+
Einhorn.expects(:bind).once.with('1.2.3.4', '123', ['llama', 'test']).returns([4, 10087])
|
14
14
|
|
15
15
|
Einhorn.socketify!(cmd)
|
16
16
|
|
@@ -19,7 +19,7 @@ class EinhornTest < EinhornTestCase
|
|
19
19
|
|
20
20
|
it "correctly parses --opt=srv: arguments" do
|
21
21
|
cmd = ['foo', '--opt=srv:1.2.3.4:456', 'baz']
|
22
|
-
Einhorn.expects(:bind).once.with('1.2.3.4', '456', []).returns(5)
|
22
|
+
Einhorn.expects(:bind).once.with('1.2.3.4', '456', []).returns([5, 10088])
|
23
23
|
|
24
24
|
Einhorn.socketify!(cmd)
|
25
25
|
|
@@ -28,7 +28,7 @@ class EinhornTest < EinhornTestCase
|
|
28
28
|
|
29
29
|
it "uses the same fd number for the same server spec" do
|
30
30
|
cmd = ['foo', '--opt=srv:1.2.3.4:8910', 'srv:1.2.3.4:8910']
|
31
|
-
Einhorn.expects(:bind).once.with('1.2.3.4', '8910', []).returns(10)
|
31
|
+
Einhorn.expects(:bind).once.with('1.2.3.4', '8910', []).returns([10, 10089])
|
32
32
|
|
33
33
|
Einhorn.socketify!(cmd)
|
34
34
|
|
@@ -55,4 +55,42 @@ class EinhornTest < EinhornTestCase
|
|
55
55
|
assert(message.nil?)
|
56
56
|
end
|
57
57
|
end
|
58
|
+
|
59
|
+
describe ".preload" do
|
60
|
+
before do
|
61
|
+
Einhorn::State.preloaded = false
|
62
|
+
end
|
63
|
+
|
64
|
+
it "updates preload on success" do
|
65
|
+
Einhorn.stubs(:set_argv).returns
|
66
|
+
# preloads the sleep worker since it has einhorn main
|
67
|
+
Einhorn::State.path = "#{__dir__}/_lib/sleep_worker.rb"
|
68
|
+
assert_equal(false, Einhorn::State.preloaded)
|
69
|
+
Einhorn.preload
|
70
|
+
assert_equal(true, Einhorn::State.preloaded)
|
71
|
+
# Attempt another preload
|
72
|
+
Einhorn.preload
|
73
|
+
assert_equal(true, Einhorn::State.preloaded)
|
74
|
+
end
|
75
|
+
|
76
|
+
it "updates preload to failed with previous success" do
|
77
|
+
Einhorn.stubs(:set_argv).returns
|
78
|
+
Einhorn::State.path = "#{__dir__}/_lib/sleep_worker.rb"
|
79
|
+
assert_equal(false, Einhorn::State.preloaded)
|
80
|
+
Einhorn.preload
|
81
|
+
assert_equal(true, Einhorn::State.preloaded)
|
82
|
+
# Change path to bad worker and preload again, should be false
|
83
|
+
Einhorn::State.path = "#{__dir__}/_lib/bad_worker.rb"
|
84
|
+
Einhorn.preload
|
85
|
+
assert_equal(false, Einhorn::State.preloaded)
|
86
|
+
end
|
87
|
+
|
88
|
+
it "preload is false after failing" do
|
89
|
+
Einhorn.stubs(:set_argv).returns
|
90
|
+
Einhorn::State.path = "#{__dir__}/bad_worker.rb"
|
91
|
+
assert_equal(false, Einhorn::State.preloaded)
|
92
|
+
Einhorn.preload
|
93
|
+
assert_equal(false, Einhorn::State.preloaded)
|
94
|
+
end
|
95
|
+
end
|
58
96
|
end
|