einhorn 0.7.0 → 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/.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
|