zeus 0.1.0 → 0.2.0.beta1

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