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.
@@ -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