zeus 0.4.5 → 0.4.6

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