zeus 0.4.5 → 0.4.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,22 +2,21 @@ require 'json'
2
2
  require 'socket'
3
3
  require 'forwardable'
4
4
 
5
+ require 'zeus/server/stage'
6
+ require 'zeus/server/acceptor'
7
+ require 'zeus/server/file_monitor'
8
+ require 'zeus/server/load_tracking'
9
+ require 'zeus/server/client_handler'
10
+ require 'zeus/server/command_runner'
11
+ require 'zeus/server/process_tree_monitor'
12
+ require 'zeus/server/acceptor_registration_monitor'
13
+
5
14
  module Zeus
6
15
  class Server
7
16
  extend Forwardable
8
17
 
9
- autoload :Stage, 'zeus/server/stage'
10
- autoload :Acceptor, 'zeus/server/acceptor'
11
- autoload :FileMonitor, 'zeus/server/file_monitor'
12
- autoload :LoadTracking, 'zeus/server/load_tracking'
13
- autoload :ForkedProcess, 'zeus/server/forked_process'
14
- autoload :ClientHandler, 'zeus/server/client_handler'
15
- autoload :CommandRunner, 'zeus/server/command_runner'
16
- autoload :ProcessTreeMonitor, 'zeus/server/process_tree_monitor'
17
- autoload :AcceptorRegistrationMonitor, 'zeus/server/acceptor_registration_monitor'
18
-
19
18
  def self.define!(&b)
20
- @@definition = Zeus::DSL::Evaluator.new.instance_eval(&b)
19
+ @@definition = Zeus::Plan::Evaluator.new.instance_eval(&b)
21
20
  end
22
21
 
23
22
  def self.acceptors
@@ -44,16 +43,12 @@ module Zeus
44
43
  def run
45
44
  $0 = "zeus master"
46
45
  trap("TERM") { exit 0 }
47
- trap("INT") {
48
- puts "\n\x1b[31mExiting\x1b[0m"
49
- exit 0
50
- }
51
-
46
+ trap("INT") { puts "\n\x1b[31mExiting\x1b[0m" ; exit }
52
47
  LoadTracking.server = self
53
48
 
54
49
  @plan.run(true) # boot the actual app
55
50
  master = Process.pid
56
- at_exit { @process_tree_monitor.kill_all_nodes if Process.pid == master }
51
+ at_exit { cleanup_all_children if Process.pid == master }
57
52
  monitors.each(&:close_child_socket)
58
53
 
59
54
  runloop!
@@ -61,6 +56,11 @@ module Zeus
61
56
  File.unlink(Zeus::SOCKET_NAME)
62
57
  end
63
58
 
59
+ def cleanup_all_children
60
+ @process_tree_monitor.kill_all_nodes
61
+ @file_monitor.kill_wrapper
62
+ end
63
+
64
64
  def add_extra_feature(full_expanded_path)
65
65
  $extra_loaded_features ||= []
66
66
  $extra_loaded_features << full_expanded_path
@@ -1,7 +1,6 @@
1
1
  require 'json'
2
2
  require 'socket'
3
3
 
4
- # See Zeus::Server::ClientHandler for relevant documentation
5
4
  module Zeus
6
5
  class Server
7
6
  class Acceptor
@@ -17,8 +16,7 @@ module Zeus
17
16
  def run
18
17
  register_with_client_handler(Process.pid)
19
18
  Zeus.ui.info("starting #{process_type} `#{@name}`")
20
-
21
- thread_with_backtrace_on_error { runloop! }
19
+ Zeus.thread_with_backtrace_on_error { runloop! }
22
20
  end
23
21
 
24
22
  private
@@ -30,6 +28,7 @@ module Zeus
30
28
  def register_with_client_handler(pid)
31
29
  @s_client_handler, @s_acceptor = UNIXSocket.pair
32
30
  @s_acceptor.puts registration_data(pid)
31
+ at_exit { @s_client_handler.close ; @s_acceptor.close }
33
32
  @server.__CHILD__register_acceptor(@s_client_handler)
34
33
  end
35
34
 
@@ -39,36 +38,20 @@ module Zeus
39
38
 
40
39
  def accept_connection
41
40
  terminal = @s_acceptor.recv_io # blocking
41
+ exit_status_socket = @s_acceptor.recv_io
42
42
  arguments = JSON.parse(@s_acceptor.readline.chomp)
43
43
 
44
- [terminal, arguments]
44
+ [terminal, exit_status_socket, arguments]
45
45
  end
46
46
 
47
47
  def process_type
48
48
  "acceptor"
49
49
  end
50
50
 
51
- def print_error(io, error)
52
- io.puts "#{error.backtrace[0]}: #{error.message} (#{error.class})"
53
- error.backtrace[1..-1].each do |line|
54
- io.puts "\tfrom #{line}"
55
- end
56
- end
57
-
58
- def thread_with_backtrace_on_error(&b)
59
- Thread.new {
60
- begin
61
- b.call
62
- rescue => e
63
- print_error($stdout, e)
64
- end
65
- }
66
- end
67
-
68
51
  def runloop!
69
52
  loop do
70
- terminal, arguments = accept_connection # blocking
71
- command_runner.run(terminal, arguments)
53
+ terminal, exit_status_socket, arguments = accept_connection # blocking
54
+ command_runner.run(terminal, exit_status_socket, arguments)
72
55
  end
73
56
  end
74
57
 
@@ -85,7 +68,7 @@ module Zeus
85
68
  terminal = @s_acceptor.recv_io
86
69
  _ = @s_acceptor.readline
87
70
  @s_acceptor << NOT_A_PID << "\n"
88
- print_error(terminal, @error)
71
+ ErrorPrinter.new(@error).write_to(terminal)
89
72
  terminal.close
90
73
  end
91
74
  end
@@ -5,8 +5,8 @@ module Zeus
5
5
  def datasource ; @sock ; end
6
6
  def on_datasource_event ; handle_message ; end
7
7
  # @__CHILD__sock is not closed here, as it's used by the master to respond
8
- # for unbooted acceptors
9
- def close_child_socket ; end
8
+ # on behalf of unbooted acceptors
9
+ def close_child_socket ; end
10
10
  def close_parent_socket ; @sock.close ; end
11
11
 
12
12
  def initialize
@@ -26,7 +26,6 @@ module Zeus
26
26
  case type
27
27
  when 'wait' ; handle_wait(io, data)
28
28
  when 'registration' ; handle_registration(io, data)
29
- when 'deregistration' ; handle_deregistration(io, data)
30
29
  else raise "invalid message"
31
30
  end
32
31
  end
@@ -37,11 +36,6 @@ module Zeus
37
36
  @pings[command] << io
38
37
  end
39
38
 
40
- def handle_deregistration(io, data)
41
- pid = data['pid'].to_i
42
- @acceptors.reject!{|acc|acc.pid == pid}
43
- end
44
-
45
39
  def handle_registration(io, data)
46
40
  pid = data['pid'].to_i
47
41
  commands = data['commands']
@@ -62,7 +56,6 @@ module Zeus
62
56
  end
63
57
  end
64
58
 
65
-
66
59
  module ChildProcessApi
67
60
 
68
61
  def __CHILD__find_acceptor_for_command(command)
@@ -78,6 +71,5 @@ module Zeus
78
71
  end ; include ChildProcessApi
79
72
 
80
73
  end
81
-
82
74
  end
83
75
  end
@@ -1,28 +1,9 @@
1
1
  require 'socket'
2
2
  require 'json'
3
3
 
4
+ # This class is a little confusing. See the docs/ directory for guidance.
4
5
  module Zeus
5
6
  class Server
6
- # The model here is kind of convoluted, so here's an explanation of what's
7
- # happening with all these sockets:
8
- #
9
- # #### Acceptor Registration
10
- # 1. ClientHandler creates a socketpair for Acceptor registration (S_REG)
11
- # 2. When an acceptor is spawned, it:
12
- # 1. Creates a new socketpair for communication with clienthandler (S_ACC)
13
- # 2. Sends one side of S_ACC over S_REG to clienthandler.
14
- # 3. Sends a JSON-encoded hash of `pid`, `commands`, and `description`. over S_REG.
15
- # 3. ClientHandler received first the IO and then the JSON hash, and stores them for later reference.
16
- #
17
- # #### Running a command
18
- # 1. ClientHandler has a UNIXServer (SVR) listening.
19
- # 2. ClientHandler has a socketpair with the acceptor referenced by the command (see Registration) (S_ACC)
20
- # 3. When clienthandler received a connection (S_CLI) on SVR:
21
- # 1. ClientHandler sends S_CLI over S_ACC, so the acceptor can communicate with the server's client.
22
- # 2. ClientHandler sends a JSON-encoded array of `arguments` over S_ACC
23
- # 3. Acceptor sends the newly-forked worker PID over S_ACC to clienthandler.
24
- # 4. ClientHandler forwards the pid to the client over S_CLI.
25
- #
26
7
  class ClientHandler
27
8
  def datasource ; @listener ; end
28
9
  def on_datasource_event ; handle_server_connection ; end
@@ -43,20 +24,15 @@ module Zeus
43
24
 
44
25
  private
45
26
 
46
- # client clienthandler acceptor
47
- # 1 ----------> | {command: String, arguments: [String]}
48
- # 2 ----------> | Terminal IO
49
- # 3 -----------> | Terminal IO
50
- # 4 -----------> | Arguments (json array)
51
- # 5 <----------- | pid
52
- # 6 <--------- | pid
27
+ # See docs/client_server_handshake.md for details
53
28
  def handle_server_connection
54
29
  s_client = @listener.accept
55
30
 
56
- data = JSON.parse(s_client.readline.chomp) # step 1
31
+ data = JSON.parse(s_client.readline.chomp)
57
32
  command, arguments = data.values_at('command', 'arguments')
58
33
 
59
- client_terminal = s_client.recv_io # step 2
34
+ client_terminal = s_client.recv_io
35
+ exit_status_socket = s_client.recv_io
60
36
 
61
37
  Thread.new {
62
38
  # This is a little ugly. Gist: Try to handshake the client to the acceptor.
@@ -64,7 +40,7 @@ module Zeus
64
40
  # REATTEMPT_HANDSHAKE. We catch that exit code and try once more.
65
41
  begin
66
42
  loop do
67
- pid = fork { handshake_client_to_acceptor(s_client, command, arguments, client_terminal) ; exit }
43
+ pid = fork { handshake_client_to_acceptor(s_client, command, arguments, client_terminal, exit_status_socket) ; exit }
68
44
  Process.wait(pid)
69
45
  break if $?.exitstatus != REATTEMPT_HANDSHAKE
70
46
  end
@@ -75,22 +51,22 @@ module Zeus
75
51
  }
76
52
  end
77
53
 
78
- def handshake_client_to_acceptor(s_client, command, arguments, client_terminal)
54
+ def handshake_client_to_acceptor(s_client, command, arguments, client_terminal, exit_status_socket)
79
55
  unless @acceptor_commands.include?(command.to_s)
80
56
  msg = "no such command `#{command}`."
81
57
  return exit_with_message(s_client, client_terminal, msg)
82
58
  end
83
59
 
84
- unless acceptor = send_io_to_acceptor(client_terminal, command) # step 3
60
+ unless acceptor = send_io_to_acceptor(client_terminal, exit_status_socket, command)
85
61
  wait_for_acceptor(s_client, client_terminal, command)
86
62
  exit REATTEMPT_HANDSHAKE
87
63
  end
88
64
 
89
65
  Zeus.ui.info "accepting connection for #{command}"
90
66
 
91
- acceptor.socket.puts arguments.to_json # step 4
92
- pid = acceptor.socket.readline.chomp.to_i # step 5
93
- s_client.puts pid # step 6
67
+ acceptor.socket.puts arguments.to_json
68
+ pid = acceptor.socket.readline.chomp.to_i
69
+ s_client.puts pid
94
70
  s_client.close
95
71
  end
96
72
 
@@ -113,17 +89,18 @@ module Zeus
113
89
  s.readline # wait until acceptor is booted
114
90
  end
115
91
 
116
- def send_io_to_acceptor(io, command)
92
+ def send_io_to_acceptor(io, io2, command)
117
93
  return false unless acceptor = @server.__CHILD__find_acceptor_for_command(command)
118
94
  return false unless usock = UNIXSocket.for_fd(acceptor.socket.fileno)
119
95
  usock.send_io(io)
96
+ usock.send_io(io2)
120
97
  io.close
98
+ io2.close
121
99
  return acceptor
122
100
  rescue Errno::EPIPE
123
101
  return false
124
102
  end
125
103
 
126
-
127
104
  end
128
105
  end
129
106
  end
@@ -8,36 +8,56 @@ module Zeus
8
8
  @s_acceptor = s_acceptor
9
9
  end
10
10
 
11
- def run(terminal, arguments)
12
- child = fork { _run(terminal, arguments) }
11
+ def run(terminal, exit_status_socket, arguments)
12
+ child = fork { _run(terminal, exit_status_socket, arguments) }
13
13
  terminal.close
14
+ exit_status_socket.close
14
15
  Process.detach(child)
15
16
  child
16
17
  end
17
18
 
18
19
  private
19
20
 
20
- def _run(terminal, arguments)
21
+ def _run(terminal, exit_status_socket, arguments)
21
22
  $0 = "zeus runner: #{@name}"
23
+ @exit_status_socket = exit_status_socket
24
+ @terminal = terminal
22
25
  Process.setsid
23
26
  reconnect_activerecord!
24
27
  @s_acceptor << $$ << "\n"
25
- $stdin.reopen(terminal)
26
- $stdout.reopen(terminal)
27
- $stderr.reopen(terminal)
28
+ reopen_streams(terminal, terminal, terminal)
28
29
  ARGV.replace(arguments)
29
30
 
31
+ return_process_exit_status
32
+
33
+ run_action
34
+ end
35
+
36
+ def return_process_exit_status
37
+ at_exit do
38
+ if $!.nil? || $!.is_a?(SystemExit) && $!.success?
39
+ @exit_status_socket.puts(0)
40
+ else
41
+ code = $!.is_a?(SystemExit) ? $!.status : 1
42
+ @exit_status_socket.puts(code)
43
+ end
44
+
45
+ @exit_status_socket.close
46
+ @terminal.close
47
+ end
48
+ end
49
+
50
+ def run_action
30
51
  @action.call
31
- ensure
32
- # TODO this is a whole lot of voodoo that I don't really understand.
33
- # I need to figure out how best to make the process disconenct cleanly.
34
- dnw, dnr = File.open("/dev/null", "w+"), File.open("/dev/null", "r+")
35
- $stderr.reopen(dnr)
36
- $stdout.reopen(dnr)
37
- terminal.close
38
- $stdin.reopen(dnw)
39
- Process.kill(9, $$)
40
- exit 0
52
+ rescue StandardError => error
53
+ ErrorPrinter.new(error).write_to($stderr)
54
+ raise
55
+ end
56
+
57
+ def reopen_streams(i, o, e)
58
+ $stdin.reopen(i)
59
+ $stdout.reopen(o)
60
+ $stderr.reopen(e)
41
61
  end
42
62
 
43
63
  def reconnect_activerecord!
@@ -1,7 +1,8 @@
1
+ require 'zeus/server/file_monitor/fsevent'
2
+
1
3
  module Zeus
2
4
  class Server
3
5
  module FileMonitor
4
- autoload :FSEvent, 'zeus/server/file_monitor/fsevent'
5
6
  end
6
7
  end
7
8
  end
@@ -14,7 +14,7 @@ module Zeus
14
14
 
15
15
  def initialize(&change_callback)
16
16
  @change_callback = change_callback
17
- @io_in, @io_out, _ = open_wrapper
17
+ @io_in, @io_out, @wrapper_thread = open_wrapper
18
18
  @givenpath_to_realpath = {}
19
19
  @realpath_to_givenpath = {}
20
20
  @buffer = ""
@@ -38,6 +38,11 @@ module Zeus
38
38
  true
39
39
  end
40
40
 
41
+ def kill_wrapper
42
+ Process.kill(9, @wrapper_thread.pid)
43
+ rescue Errno::ESRCH # already dead. SIGINT to master causes this often.
44
+ end
45
+
41
46
  private
42
47
 
43
48
  def realpath(file)
@@ -1,31 +1,63 @@
1
+ require 'zeus/server/stage/error_state'
2
+ require 'zeus/server/stage/feature_notifier'
3
+
1
4
  module Zeus
2
5
  class Server
3
6
  # NONE of the code in the module is run in the master process,
4
7
  # so every communication to the master must be done with IPC.
5
- class Stage < ForkedProcess
6
- attr_accessor :stages, :actions
8
+ class Stage
9
+
10
+ attr_accessor :name, :stages, :actions
11
+ def initialize(server)
12
+ @server = server
13
+ end
7
14
 
8
15
  def descendent_acceptors
9
16
  @stages.map(&:descendent_acceptors).flatten
10
17
  end
11
18
 
19
+ def run(close_parent_sockets = false)
20
+ @pid = fork {
21
+ setup_fork(close_parent_sockets)
22
+ run_actions
23
+ feature_notifier.notify_new_features
24
+ start_child_stages
25
+ handle_child_exit_loop!
26
+ }
27
+ end
12
28
 
13
- def after_setup
14
- begin
15
- @actions.each(&:call)
16
- rescue => e
17
- handle_load_error(e)
18
- end
29
+ private
30
+
31
+ def setup_fork(close_parent_sockets)
32
+ $0 = "zeus #{process_type}: #{@name}"
33
+ @server.__CHILD__close_parent_sockets if close_parent_sockets
34
+ notify_started
35
+ trap("INT") { exit }
36
+ trap("TERM") { notify_terminated ; exit }
37
+ ActiveRecord::Base.clear_all_connections! rescue nil
38
+ end
39
+
40
+ def feature_notifier
41
+ FeatureNotifier.new(@server, @name)
19
42
  end
20
43
 
21
- def after_notify
44
+ def start_child_stages
22
45
  @pids = {}
23
46
  @stages.each do |stage|
24
47
  @pids[stage.run] = stage
25
48
  end
26
49
  end
27
50
 
28
- def runloop!
51
+ def run_actions
52
+ begin
53
+ @actions.each(&:call)
54
+ rescue => e
55
+ extend(ErrorState)
56
+ handle_load_error(e)
57
+ end
58
+ end
59
+
60
+ def handle_child_exit_loop!
29
61
  loop do
30
62
  begin
31
63
  pid = Process.wait
@@ -37,41 +69,18 @@ module Zeus
37
69
  end
38
70
  end
39
71
 
40
- private
41
-
42
- def register_acceptors_as_errors(e)
43
- descendent_acceptors.each do |acc|
44
- acc = acc.extend(Acceptor::ErrorState)
45
- acc.error = e
46
- acc.run
47
- end
48
- end
49
-
50
- def process_type
51
- "spawner"
72
+ def notify_started
73
+ @server.__CHILD__stage_starting_with_pid(@name, Process.pid)
74
+ Zeus.ui.info("starting #{process_type} `#{@name}`")
52
75
  end
53
76
 
54
- def full_path_of_file_from_error(e)
55
- errored_file = e.backtrace[0].scan(/(.+?):\d+:in/)[0][0]
56
-
57
- # handle relative paths
58
- unless errored_file =~ /^\//
59
- errored_file = File.expand_path(errored_file, Dir.pwd)
60
- end
77
+ def notify_terminated
78
+ Zeus.ui.info("killing #{process_type} `#{@name}`")
61
79
  end
62
80
 
63
- def handle_load_error(e)
64
- errored_file = full_path_of_file_from_error(e)
65
81
 
66
- # register all the decendent acceptors as stubs with errors
67
- register_acceptors_as_errors(e)
68
-
69
- notify_feature(errored_file)
70
- $LOADED_FEATURES.each { |f| notify_feature(f) }
71
-
72
- # we do not need to do anything. We wait, until a dependency changes.
73
- # At that point, we get killed and restarted.
74
- sleep
82
+ def process_type
83
+ "spawner"
75
84
  end
76
85
 
77
86
  end