einhorn 0.7.4 → 0.8.2
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.
- 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
|