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/README.md +24 -6
- data/bin/zeus +34 -4
- data/lib/tiny_thor.rb +96 -0
- data/lib/zeus.rb +16 -1
- data/lib/zeus/cli.rb +72 -0
- data/lib/zeus/client.rb +6 -17
- data/lib/zeus/dsl.rb +88 -0
- data/lib/zeus/init.rb +17 -0
- data/lib/zeus/server.rb +82 -244
- data/lib/zeus/server/acceptor.rb +81 -0
- data/lib/zeus/server/acceptor_registration_monitor.rb +44 -0
- data/lib/zeus/server/client_handler.rb +88 -0
- data/lib/zeus/server/file_monitor.rb +57 -0
- data/lib/zeus/server/process_tree_monitor.rb +98 -0
- data/lib/zeus/server/stage.rb +57 -0
- data/{examples → lib/zeus/templates}/rails.rb +11 -11
- data/lib/zeus/ui.rb +56 -0
- data/lib/zeus/version.rb +1 -1
- data/zeus.gemspec +1 -1
- metadata +20 -9
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
|
-
|
15
|
+
class Server
|
16
|
+
|
9
17
|
def self.define!(&b)
|
10
|
-
@@
|
11
|
-
@@root.instance_eval(&b)
|
12
|
-
@@files = {}
|
18
|
+
@@definition = Zeus::DSL::Evaluator.new.instance_eval(&b)
|
13
19
|
end
|
14
20
|
|
15
|
-
def self.
|
16
|
-
@@
|
17
|
-
@@files[file] << pid
|
21
|
+
def self.acceptors
|
22
|
+
@@definition.acceptors
|
18
23
|
end
|
19
24
|
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
-
|
36
|
+
def dependency_did_change(file)
|
37
|
+
@process_tree_monitor.kill_nodes_with_feature(file)
|
38
|
+
end
|
26
39
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
79
|
-
|
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
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
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
|
-
|
111
|
+
rescue Errno::EAGAIN
|
112
|
+
break
|
156
113
|
end
|
157
114
|
end
|
158
|
-
|
159
115
|
end
|
160
116
|
|
161
|
-
def
|
117
|
+
def handle_pid_message(data)
|
162
118
|
data =~ /(\d+):(\d+)/
|
163
|
-
|
164
|
-
|
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
|
123
|
+
def handle_feature_message(data)
|
168
124
|
data =~ /(\d+):(.*)/
|
169
|
-
|
170
|
-
|
171
|
-
|
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
|
-
|
247
|
-
|
248
|
-
|
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
|