einhorn 0.7.0 → 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/.travis.yml +6 -4
- data/README.md +39 -3
- data/README.md.in +21 -3
- data/bin/einhorn +17 -2
- data/bin/einhornsh +7 -15
- data/einhorn.gemspec +1 -0
- data/example/pool_worker.rb +1 -1
- data/lib/einhorn.rb +55 -41
- data/lib/einhorn/client.rb +2 -3
- data/lib/einhorn/command.rb +127 -20
- data/lib/einhorn/command/interface.rb +21 -10
- data/lib/einhorn/event.rb +10 -1
- data/lib/einhorn/event/connection.rb +2 -2
- 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 +14 -9
- 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 +48 -38
@@ -148,17 +148,17 @@ module Einhorn::Command
|
|
148
148
|
## Signals
|
149
149
|
def self.install_handlers
|
150
150
|
trap_async("INT") do
|
151
|
-
Einhorn::Command.signal_all("USR2"
|
151
|
+
Einhorn::Command.signal_all("USR2")
|
152
152
|
Einhorn::Command.stop_respawning
|
153
153
|
end
|
154
154
|
trap_async("TERM") do
|
155
|
-
Einhorn::Command.signal_all("TERM"
|
155
|
+
Einhorn::Command.signal_all("TERM")
|
156
156
|
Einhorn::Command.stop_respawning
|
157
157
|
end
|
158
158
|
# Note that quit is a bit different, in that it will actually
|
159
159
|
# make Einhorn quit without waiting for children to exit.
|
160
160
|
trap_async("QUIT") do
|
161
|
-
Einhorn::Command.signal_all("QUIT"
|
161
|
+
Einhorn::Command.signal_all("QUIT")
|
162
162
|
Einhorn::Command.stop_respawning
|
163
163
|
exit(1)
|
164
164
|
end
|
@@ -169,12 +169,12 @@ module Einhorn::Command
|
|
169
169
|
end
|
170
170
|
trap_async("CHLD") {}
|
171
171
|
trap_async("USR2") do
|
172
|
-
Einhorn::Command.signal_all("USR2"
|
172
|
+
Einhorn::Command.signal_all("USR2")
|
173
173
|
Einhorn::Command.stop_respawning
|
174
174
|
end
|
175
175
|
at_exit do
|
176
176
|
if Einhorn::State.kill_children_on_exit && Einhorn::TransientState.whatami == :master
|
177
|
-
Einhorn::Command.signal_all("USR2"
|
177
|
+
Einhorn::Command.signal_all("USR2")
|
178
178
|
Einhorn::Command.stop_respawning
|
179
179
|
end
|
180
180
|
end
|
@@ -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
|
@@ -305,7 +316,7 @@ EOF
|
|
305
316
|
end
|
306
317
|
|
307
318
|
command 'state', "Get a dump of Einhorn's current state" do
|
308
|
-
YAML.dump(Einhorn::
|
319
|
+
YAML.dump({:state => Einhorn::State.dumpable_state})
|
309
320
|
end
|
310
321
|
|
311
322
|
command 'reload', 'Reload Einhorn' do |conn, request|
|
@@ -335,9 +346,9 @@ EOF
|
|
335
346
|
end
|
336
347
|
|
337
348
|
count = args[0].to_i
|
338
|
-
if count < 1 || count >
|
339
|
-
# sancheck.
|
340
|
-
next "Invalid count: '#{args[0]}'. Must be an integer in [1,
|
349
|
+
if count < 1 || count > 1000
|
350
|
+
# sancheck. 1000 is kinda arbitrary.
|
351
|
+
next "Invalid count: '#{args[0]}'. Must be an integer in [1,1000)."
|
341
352
|
end
|
342
353
|
|
343
354
|
Einhorn::Command.set_workers(count)
|
@@ -407,7 +418,7 @@ EOF
|
|
407
418
|
|
408
419
|
signal = args[0] || "USR2"
|
409
420
|
|
410
|
-
response = Einhorn::Command.signal_all(signal
|
421
|
+
response = Einhorn::Command.signal_all(signal)
|
411
422
|
Einhorn::Command.stop_respawning
|
412
423
|
|
413
424
|
"Einhorn is going down! #{response}"
|
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
|
|
@@ -57,13 +57,13 @@ module Einhorn::Event
|
|
57
57
|
end
|
58
58
|
|
59
59
|
def register!
|
60
|
-
|
60
|
+
log_debug("client connected")
|
61
61
|
Einhorn::Event.register_connection(self, @socket.fileno)
|
62
62
|
super
|
63
63
|
end
|
64
64
|
|
65
65
|
def deregister!
|
66
|
-
|
66
|
+
log_debug("client disconnected") if Einhorn::TransientState.whatami == :master
|
67
67
|
Einhorn::Event.deregister_connection(@socket.fileno)
|
68
68
|
super
|
69
69
|
end
|
@@ -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!"
|
@@ -40,28 +40,28 @@ module Helpers
|
|
40
40
|
begin
|
41
41
|
stdout, stderr = process.communicate
|
42
42
|
rescue Errno::ECHILD
|
43
|
-
# It's dead, and we're not getting anything. This is
|
44
|
-
# peaceful.
|
43
|
+
# It's dead, and we're not getting anything. This is peaceful.
|
45
44
|
end
|
46
45
|
end
|
47
46
|
yield(process) if block_given?
|
48
|
-
rescue
|
47
|
+
rescue
|
49
48
|
unless (status = process.poll) && status.exited?
|
50
49
|
process.terminate
|
51
50
|
end
|
52
51
|
raise
|
53
52
|
ensure
|
54
53
|
unless (status = process.poll) && status.exited?
|
55
|
-
|
56
|
-
|
57
|
-
|
54
|
+
for i in 1..10 do
|
55
|
+
status = process.poll
|
56
|
+
if status && status.exited?
|
57
|
+
break
|
58
58
|
end
|
59
|
-
|
59
|
+
sleep(1)
|
60
|
+
end
|
61
|
+
unless status && status.exited?
|
60
62
|
$stderr.puts "Could not get Einhorn to quit within 10 seconds, killing it forcefully..."
|
61
63
|
process.send_signal("KILL")
|
62
64
|
status = process.wait
|
63
|
-
rescue Errno::ECHILD
|
64
|
-
# Process is dead!
|
65
65
|
end
|
66
66
|
end
|
67
67
|
communicator.join
|
@@ -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
|