zeus 0.1.0 → 0.2.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/zeus/server.rb CHANGED
@@ -2,296 +2,134 @@ require 'json'
2
2
  require 'socket'
3
3
 
4
4
  require 'rb-kqueue'
5
+
5
6
  require 'zeus/process'
7
+ require 'zeus/dsl'
8
+ require 'zeus/server/file_monitor'
9
+ require 'zeus/server/client_handler'
10
+ require 'zeus/server/process_tree_monitor'
11
+ require 'zeus/server/acceptor_registration_monitor'
12
+ require 'zeus/server/acceptor'
6
13
 
7
14
  module Zeus
8
- module Server
15
+ class Server
16
+
9
17
  def self.define!(&b)
10
- @@root = Stage.new("(root)")
11
- @@root.instance_eval(&b)
12
- @@files = {}
18
+ @@definition = Zeus::DSL::Evaluator.new.instance_eval(&b)
13
19
  end
14
20
 
15
- def self.pid_has_file(pid, file)
16
- @@files[file] ||= []
17
- @@files[file] << pid
21
+ def self.acceptors
22
+ @@definition.acceptors
18
23
  end
19
24
 
20
- def self.killall_with_file(file)
21
- pids = @@files[file]
22
- @@process_tree.kill_nodes_with_feature(file)
25
+ attr_reader :client_handler, :acceptor_registration_monitor
26
+ def initialize
27
+ @file_monitor = FileMonitor.new(&method(:dependency_did_change))
28
+ @acceptor_registration_monitor = AcceptorRegistrationMonitor.new
29
+ @process_tree_monitor = ProcessTreeMonitor.new
30
+ @client_handler = ClientHandler.new(acceptor_registration_monitor)
31
+
32
+ # TODO: deprecate Zeus::Server.define! maybe. We can do that better...
33
+ @plan = @@definition.to_domain_object(self)
23
34
  end
24
35
 
25
- TARGET_FD_LIMIT = 8192
36
+ def dependency_did_change(file)
37
+ @process_tree_monitor.kill_nodes_with_feature(file)
38
+ end
26
39
 
27
- def self.configure_number_of_file_descriptors
28
- limit = Process.getrlimit(Process::RLIMIT_NOFILE)
29
- if limit[0] < TARGET_FD_LIMIT && limit[1] >= TARGET_FD_LIMIT
30
- Process.setrlimit(Process::RLIMIT_NOFILE, TARGET_FD_LIMIT)
31
- else
32
- puts "\x1b[33m[zeus] Warning: increase the max number of file descriptors. If you have a large project, this max cause a crash in about 10 seconds.\x1b[0m"
40
+ PID_TYPE = "P"
41
+ def w_pid line
42
+ begin
43
+ @w_msg.send(PID_TYPE + line, 0)
44
+ rescue Errno::ENOBUFS
45
+ sleep 0.2
46
+ retry
33
47
  end
34
48
  end
35
49
 
36
- def self.notify(event)
37
- if event.flags.include?(:delete)
38
- # file was deleted, so we need to close and reopen it.
39
- event.watcher.disable!
40
- begin
41
- @@queue.watch_file(event.watcher.path, :write, :extend, :rename, :delete, &method(:notify))
42
- rescue Errno::ENOENT
43
- lost_files << event.watcher.path
44
- end
50
+ FEATURE_TYPE = "F"
51
+ def w_feature line
52
+ begin
53
+ @w_msg.send(FEATURE_TYPE + line, 0)
54
+ rescue Errno::ENOBUFS
55
+ sleep 0.2
56
+ retry
45
57
  end
46
- puts "\x1b[37m[zeus] dependency change: #{event.watcher.path}\x1b[0m"
47
- killall_with_file(event.watcher.path)
48
58
  end
49
59
 
50
- def self.run
60
+ def run
51
61
  $0 = "zeus master"
52
- configure_number_of_file_descriptors
53
62
  trap("INT") { exit 0 }
54
63
  at_exit { Process.killall_descendants(9) }
55
64
 
56
- $r_features, $w_features = IO.pipe
57
- $w_features.sync = true
58
-
59
- $r_pids, $w_pids = IO.pipe
60
- $w_pids.sync = true
61
-
62
- @@process_tree = ProcessTree.new
63
- @@root_stage_pid = @@root.run
65
+ @r_msg, @w_msg = Socket.pair(:UNIX, :DGRAM)
64
66
 
65
- @@queue = KQueue::Queue.new
67
+ # boot the actual app
68
+ @plan.run
69
+ @w_msg.close
66
70
 
67
- lost_files = []
68
-
69
- @@file_watchers = {}
70
71
  loop do
71
- @@queue.poll
72
+ @file_monitor.process_events
73
+
74
+ datasources = [@r_msg,
75
+ @acceptor_registration_monitor.datasource, @client_handler.datasource]
72
76
 
73
77
  # TODO: It would be really nice if we could put the queue poller in the select somehow.
74
78
  # --investigate kqueue. Is this possible?
75
- rs, _, _ = IO.select([$r_features, $r_pids], [], [], 1)
79
+ begin
80
+ rs, _, _ = IO.select(datasources, [], [], 1)
81
+ rescue Errno::EBADF
82
+ puts "EBADF" unless defined?($asdf)
83
+ sleep 1
84
+ $asdf = true
85
+ end
76
86
  rs.each do |r|
77
87
  case r
78
- when $r_pids ; handle_pid_message(r.readline)
79
- when $r_features ; handle_feature_message(r.readline)
88
+ when @acceptor_registration_monitor.datasource
89
+ @acceptor_registration_monitor.on_datasource_event
90
+ when @r_msg ; handle_messages
91
+ when @client_handler.datasource
92
+ @client_handler.on_datasource_event
80
93
  end
81
94
  end if rs
82
95
  end
83
96
 
84
97
  end
85
98
 
86
- class ProcessTree
87
- class Node
88
- attr_accessor :pid, :children, :features
89
- def initialize(pid)
90
- @pid, @children, @features = pid, [], {}
91
- end
92
-
93
- def add_child(node)
94
- self.children << node
95
- end
96
-
97
- def add_feature(feature)
98
- self.features[feature] = true
99
- end
100
-
101
- def has_feature?(feature)
102
- self.features[feature] == true
103
- end
104
-
105
- def inspect
106
- "(#{pid}:#{features.size}:[#{children.map(&:inspect).join(",")}])"
107
- end
108
-
109
- end
110
-
111
- def inspect
112
- @root.inspect
113
- end
114
-
115
- def initialize
116
- @root = Node.new(Process.pid)
117
- @nodes_by_pid = {Process.pid => @root}
118
- end
119
-
120
- def node_for_pid(pid)
121
- @nodes_by_pid[pid.to_i] ||= Node.new(pid.to_i)
122
- end
123
-
124
- def process_has_parent(pid, ppid)
125
- curr = node_for_pid(pid)
126
- base = node_for_pid(ppid)
127
- base.add_child(curr)
128
- end
129
-
130
- def process_has_feature(pid, feature)
131
- node = node_for_pid(pid)
132
- node.add_feature(feature)
133
- end
134
-
135
- def kill_node(node)
136
- @nodes_by_pid.delete(node.pid)
137
- # recall that this process explicitly traps INT -> exit 0
138
- Process.kill("INT", node.pid)
139
- end
140
-
141
- def kill_nodes_with_feature(file, base = @root)
142
- if base.has_feature?(file)
143
- if base == @root.children[0] || base == @root
144
- puts "\x1b[31mOne of zeus's dependencies changed. Not killing zeus. You may have to restart the server.\x1b[0m"
145
- return false
146
- end
147
- kill_node(base)
148
- return true
149
- else
150
- base.children.dup.each do |node|
151
- if kill_nodes_with_feature(file, node)
152
- base.children.delete(node)
153
- end
99
+ def handle_messages
100
+ loop do
101
+ begin
102
+ data = @r_msg.recv_nonblock(1024)
103
+ case data[0]
104
+ when FEATURE_TYPE
105
+ handle_feature_message(data[1..-1])
106
+ when PID_TYPE
107
+ handle_pid_message(data[1..-1])
108
+ else
109
+ raise "Unrecognized message"
154
110
  end
155
- return false
111
+ rescue Errno::EAGAIN
112
+ break
156
113
  end
157
114
  end
158
-
159
115
  end
160
116
 
161
- def self.handle_pid_message(data)
117
+ def handle_pid_message(data)
162
118
  data =~ /(\d+):(\d+)/
163
- pid, ppid = $1.to_i, $2.to_i
164
- @@process_tree.process_has_parent(pid, ppid)
119
+ pid, ppid = $1.to_i, $2.to_i
120
+ @process_tree_monitor.process_has_parent(pid, ppid)
165
121
  end
166
122
 
167
- def self.handle_feature_message(data)
123
+ def handle_feature_message(data)
168
124
  data =~ /(\d+):(.*)/
169
- pid, file = $1.to_i, $2
170
- @@process_tree.process_has_feature(pid, file)
171
- return if @@file_watchers[file]
172
- begin
173
- @@file_watchers[file] = true
174
- @@queue.watch_file(file.chomp, :write, :extend, :rename, :delete, &method(:notify))
175
- # rescue Errno::EMFILE
176
- # exit 1
177
- rescue Errno::ENOENT
178
- puts "No file found at #{file.chomp}"
179
- end
180
- end
181
-
182
- class Stage
183
- attr_reader :pid
184
- def initialize(name)
185
- @name = name
186
- @stages, @actions = [], []
187
- end
188
-
189
- def action(&b)
190
- @actions << b
191
- end
192
-
193
- def stage(name, &b)
194
- @stages << Stage.new(name).tap { |s| s.instance_eval(&b) }
195
- end
196
-
197
- def acceptor(name, socket, &b)
198
- @stages << Acceptor.new(name, socket, &b)
199
- end
200
-
201
- # There are a few things we want to accomplish:
202
- # 1. Running all the actions (each time this stage is killed and restarted)
203
- # 2. Starting all the substages (and restarting them when necessary)
204
- # 3. Starting all the acceptors (and restarting them when necessary)
205
- def run
206
- @pid = fork {
207
- $0 = "zeus spawner: #{@name}"
208
- pid = Process.pid
209
- $w_pids.puts "#{pid}:#{Process.ppid}\n"
210
- puts "\x1b[35m[zeus] starting spawner `#{@name}`\x1b[0m"
211
- trap("INT") {
212
- puts "\x1b[35m[zeus] killing spawner `#{@name}`\x1b[0m"
213
- exit 0
214
- }
215
-
216
- @actions.each(&:call)
217
-
218
- $LOADED_FEATURES.each do |f|
219
- $w_features.puts "#{pid}:#{f}\n"
220
- end
221
-
222
- pids = {}
223
- @stages.each do |stage|
224
- pids[stage.run] = stage
225
- end
226
-
227
- loop do
228
- begin
229
- pid = Process.wait
230
- rescue Errno::ECHILD
231
- raise "Stage `#{@name}` has no children. All terminal nodes must be acceptors"
232
- end
233
- if (status = $?.exitstatus) > 0
234
- exit status
235
- else # restart the stage that died.
236
- stage = pids[pid]
237
- pids[stage.run] = stage
238
- end
239
- end
240
-
241
- }
242
- end
243
-
125
+ pid, file = $1.to_i, $2
126
+ @process_tree_monitor.process_has_feature(pid, file)
127
+ @file_monitor.watch(file)
244
128
  end
245
129
 
246
- class Acceptor
247
- attr_reader :pid
248
- def initialize(name, socket, &b)
249
- @name = name
250
- @socket = socket
251
- @action = b
252
- end
253
-
254
- def run
255
- @pid = fork {
256
- $0 = "zeus acceptor: #{@name}"
257
- pid = Process.pid
258
- $w_pids.puts "#{pid}:#{Process.ppid}\n"
259
- $LOADED_FEATURES.each do |f|
260
- $w_features.puts "#{pid}:#{f}\n"
261
- end
262
- puts "\x1b[35m[zeus] starting acceptor `#{@name}`\x1b[0m"
263
- trap("INT") {
264
- puts "\x1b[35m[zeus] killing acceptor `#{@name}`\x1b[0m"
265
- exit 0
266
- }
267
-
268
- File.unlink(@socket) rescue nil
269
- server = UNIXServer.new(@socket)
270
- loop do
271
- ActiveRecord::Base.clear_all_connections! # TODO : refactor
272
- client = server.accept
273
- child = fork do
274
- ActiveRecord::Base.establish_connection # TODO :refactor
275
- ActiveSupport::DescendantsTracker.clear
276
- ActiveSupport::Dependencies.clear
277
-
278
- terminal = client.recv_io
279
- arguments = JSON.load(client.gets.strip)
280
-
281
- client << $$ << "\n"
282
- $stdin.reopen(terminal)
283
- $stdout.reopen(terminal)
284
- $stderr.reopen(terminal)
285
- ARGV.replace(arguments)
286
-
287
- @action.call
288
- end
289
- Process.detach(child)
290
- client.close
291
- end
292
- }
293
- end
294
-
130
+ def self.pid_has_file(pid, file)
131
+ @@files[file] ||= []
132
+ @@files[file] << pid
295
133
  end
296
134
 
297
135
  end
@@ -0,0 +1,81 @@
1
+ require 'json'
2
+ require 'socket'
3
+
4
+ # See Zeus::Server::ClientHandler for relevant documentation
5
+ module Zeus
6
+ class Server
7
+ class Acceptor
8
+
9
+ attr_accessor :name, :aliases, :description, :action
10
+ def initialize(server)
11
+ @server = server
12
+ @client_handler = server.client_handler
13
+ @registration_monitor = server.acceptor_registration_monitor
14
+ end
15
+
16
+ def register_with_client_handler(pid)
17
+ @a, @b = Socket.pair(:UNIX, :STREAM)
18
+ @s_client_handler = UNIXSocket.for_fd(@a.fileno)
19
+ @s_acceptor = UNIXSocket.for_fd(@b.fileno)
20
+
21
+ @s_acceptor.puts registration_data(pid)
22
+
23
+ @registration_monitor.acceptor_registration_socket.send_io(@s_client_handler)
24
+ end
25
+
26
+ def registration_data(pid)
27
+ {pid: pid, commands: [name, *aliases], description: description}.to_json
28
+ end
29
+
30
+ def run
31
+ fork {
32
+ $0 = "zeus acceptor: #{@name}"
33
+ pid = Process.pid
34
+
35
+ register_with_client_handler(pid)
36
+
37
+ @server.w_pid "#{pid}:#{Process.ppid}"
38
+
39
+ puts "\x1b[35m[zeus] starting acceptor `#{@name}`\x1b[0m"
40
+ trap("INT") {
41
+ puts "\x1b[35m[zeus] killing acceptor `#{@name}`\x1b[0m"
42
+ exit 0
43
+ }
44
+
45
+ $LOADED_FEATURES.each do |f|
46
+ @server.w_feature "#{pid}:#{f}"
47
+ end
48
+
49
+ loop do
50
+ prefork_action!
51
+ terminal = @s_acceptor.recv_io
52
+ arguments = JSON.parse(@s_acceptor.readline.chomp)
53
+ child = fork do
54
+ postfork_action!
55
+ @s_acceptor << $$ << "\n"
56
+ $stdin.reopen(terminal)
57
+ $stdout.reopen(terminal)
58
+ $stderr.reopen(terminal)
59
+ ARGV.replace(arguments)
60
+
61
+ @action.call
62
+ end
63
+ Process.detach(child)
64
+ terminal.close
65
+ end
66
+ }
67
+ end
68
+
69
+ def prefork_action! # TODO : refactor
70
+ ActiveRecord::Base.clear_all_connections!
71
+ end
72
+
73
+ def postfork_action! # TODO :refactor
74
+ ActiveRecord::Base.establish_connection
75
+ ActiveSupport::DescendantsTracker.clear
76
+ ActiveSupport::Dependencies.clear
77
+ end
78
+
79
+ end
80
+ end
81
+ end