einhorn 0.7.4 → 0.8.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
- nil
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
@@ -1,3 +1,3 @@
1
1
  module Einhorn
2
- VERSION = '0.7.4'
2
+ VERSION = '0.8.2'
3
3
  end
@@ -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
- ensure_worker!
70
- close_after_use = true
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
- client.send_command({
90
- 'command' => 'worker:ack',
91
- 'pid' => $$
92
- })
93
-
94
- client.close if close_after_use
95
- true
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)
@@ -5,6 +5,7 @@ require 'einhorn/worker'
5
5
  def einhorn_main
6
6
  serv = Socket.for_fd(Einhorn::Worker.socket!)
7
7
  Einhorn::Worker.ack!
8
+ Einhorn::Worker.ping!("id-1")
8
9
 
9
10
  Signal.trap('USR2') do
10
11
  sleep 3
@@ -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
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ raise 'This worker has an error during preload'
4
+
5
+ def einhorn_main
6
+
7
+ end
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ def einhorn_main
4
+ sleep 1
5
+ end
@@ -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