pandemic 0.5.4
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +22 -0
- data/Manifest +27 -0
- data/README.markdown +133 -0
- data/Rakefile +14 -0
- data/examples/client/client.rb +17 -0
- data/examples/client/constitution.txt +865 -0
- data/examples/client/pandemic_client.yml +3 -0
- data/examples/server/pandemic_server.yml +3 -0
- data/examples/server/word_count_server.rb +47 -0
- data/lib/pandemic.rb +42 -0
- data/lib/pandemic/client_side/cluster_connection.rb +194 -0
- data/lib/pandemic/client_side/config.rb +34 -0
- data/lib/pandemic/client_side/connection.rb +40 -0
- data/lib/pandemic/client_side/connection_proxy.rb +15 -0
- data/lib/pandemic/client_side/pandemize.rb +17 -0
- data/lib/pandemic/connection_pool.rb +194 -0
- data/lib/pandemic/mutex_counter.rb +50 -0
- data/lib/pandemic/requests_per_second.rb +31 -0
- data/lib/pandemic/server_side/client.rb +123 -0
- data/lib/pandemic/server_side/config.rb +74 -0
- data/lib/pandemic/server_side/handler.rb +31 -0
- data/lib/pandemic/server_side/peer.rb +211 -0
- data/lib/pandemic/server_side/processor.rb +90 -0
- data/lib/pandemic/server_side/request.rb +92 -0
- data/lib/pandemic/server_side/server.rb +285 -0
- data/lib/pandemic/util.rb +15 -0
- data/pandemic.gemspec +37 -0
- data/test/client_test.rb +87 -0
- data/test/connection_pool_test.rb +133 -0
- data/test/functional_test.rb +57 -0
- data/test/handler_test.rb +31 -0
- data/test/mutex_counter_test.rb +73 -0
- data/test/peer_test.rb +48 -0
- data/test/processor_test.rb +33 -0
- data/test/server_test.rb +171 -0
- data/test/test_helper.rb +24 -0
- data/test/util_test.rb +21 -0
- metadata +141 -0
@@ -0,0 +1,90 @@
|
|
1
|
+
module Pandemic
|
2
|
+
module ServerSide
|
3
|
+
class Processor
|
4
|
+
def initialize(handler)
|
5
|
+
read_from_parent, write_to_child = IO.pipe
|
6
|
+
read_from_child, write_to_parent = IO.pipe
|
7
|
+
|
8
|
+
@child_process_id = fork
|
9
|
+
if @child_process_id
|
10
|
+
# I'm the parent
|
11
|
+
write_to_parent.close
|
12
|
+
read_from_parent.close
|
13
|
+
@out = write_to_child
|
14
|
+
@in = read_from_child
|
15
|
+
else
|
16
|
+
# I'm the child
|
17
|
+
write_to_child.close
|
18
|
+
read_from_child.close
|
19
|
+
@out = write_to_parent
|
20
|
+
@in = read_from_parent
|
21
|
+
@handler = handler.new
|
22
|
+
wait_for_jobs
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def process(body)
|
27
|
+
if parent?
|
28
|
+
@out.puts(body.size.to_s)
|
29
|
+
@out.write(body)
|
30
|
+
ready, = IO.select([@in], nil, nil)
|
31
|
+
if ready
|
32
|
+
size = @in.gets.to_i
|
33
|
+
result = @in.read(size)
|
34
|
+
return result
|
35
|
+
end
|
36
|
+
else
|
37
|
+
return @handler.process(body)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def close(status = 0)
|
42
|
+
if parent? && child_alive?
|
43
|
+
Process.detach(@child_process_id)
|
44
|
+
@out.puts(status.to_s)
|
45
|
+
@out.close
|
46
|
+
@in.close
|
47
|
+
else
|
48
|
+
Process.exit!(status)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def closed?
|
53
|
+
!child_alive?
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
def wait_for_jobs
|
58
|
+
if child?
|
59
|
+
while true
|
60
|
+
ready, = IO.select([@in], nil, nil)
|
61
|
+
if ready
|
62
|
+
size = @in.gets.to_i
|
63
|
+
if size > 0
|
64
|
+
body = @in.read(size)
|
65
|
+
result = process(body)
|
66
|
+
@out.puts(result.size.to_s)
|
67
|
+
@out.write(result)
|
68
|
+
else
|
69
|
+
self.close(size)
|
70
|
+
break
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def parent?
|
78
|
+
!!@child_process_id
|
79
|
+
end
|
80
|
+
|
81
|
+
def child?
|
82
|
+
!parent?
|
83
|
+
end
|
84
|
+
|
85
|
+
def child_alive?
|
86
|
+
parent? && !@in.closed?
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module Pandemic
|
2
|
+
module ServerSide
|
3
|
+
class Request
|
4
|
+
include Util
|
5
|
+
|
6
|
+
@@request_count = MutexCounter.new
|
7
|
+
@@late_responses = MutexCounter.new
|
8
|
+
attr_reader :body
|
9
|
+
attr_accessor :max_responses
|
10
|
+
attr_accessor :data
|
11
|
+
|
12
|
+
def self.total_request_count
|
13
|
+
@@request_count.real_total
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.total_late_responses
|
17
|
+
@@late_responses.real_total
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(body)
|
21
|
+
@request_number = @@request_count.inc
|
22
|
+
@body = body
|
23
|
+
@responses = []
|
24
|
+
@responses_mutex = Monitor.new
|
25
|
+
@waiter = @responses_mutex.new_cond
|
26
|
+
@complete = false
|
27
|
+
end
|
28
|
+
|
29
|
+
def add_response(response)
|
30
|
+
@responses_mutex.synchronize do
|
31
|
+
if @responses.frozen? # too late
|
32
|
+
@@late_responses.inc
|
33
|
+
return
|
34
|
+
end
|
35
|
+
# debug("Adding response")
|
36
|
+
@responses << response
|
37
|
+
if @max_responses && @responses.size >= @max_responses
|
38
|
+
# debug("Hit max responses, waking up waiting thread")
|
39
|
+
wakeup_waiting_thread
|
40
|
+
@complete = true
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def wakeup_waiting_thread
|
46
|
+
@waiter.signal if @waiter
|
47
|
+
end
|
48
|
+
|
49
|
+
def responses
|
50
|
+
@responses # its frozen so we don't have to worry about mutex
|
51
|
+
end
|
52
|
+
|
53
|
+
def cancel!
|
54
|
+
# consider telling peers that they should stop, but for now this is fine.
|
55
|
+
@responses_mutex.synchronize do
|
56
|
+
wakeup_waiting_thread
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def wait_for_responses
|
61
|
+
@responses_mutex.synchronize do
|
62
|
+
return if @complete
|
63
|
+
if Config.response_timeout <= 0
|
64
|
+
@waiter.wait
|
65
|
+
elsif !MONITOR_TIMEOUT_AVAILABLE
|
66
|
+
Thread.new do
|
67
|
+
sleep Config.response_timeout
|
68
|
+
wakeup_waiting_thread
|
69
|
+
end
|
70
|
+
@waiter.wait
|
71
|
+
else
|
72
|
+
@waiter.wait(Config.response_timeout)
|
73
|
+
end
|
74
|
+
@responses.freeze
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def hash
|
79
|
+
@hash ||= Digest::MD5.hexdigest("#{@request_number} #{@body}")[0,10]
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
def debug(msg)
|
84
|
+
logger.debug("Request #{hash}") {msg}
|
85
|
+
end
|
86
|
+
|
87
|
+
def info(msg)
|
88
|
+
logger.info("Request #{hash}") {msg}
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,285 @@
|
|
1
|
+
module Pandemic
|
2
|
+
module ServerSide
|
3
|
+
class Server
|
4
|
+
include Util
|
5
|
+
class StopServer < Exception; end
|
6
|
+
class << self
|
7
|
+
def boot(bind_to = nil)
|
8
|
+
Config.load
|
9
|
+
# Process.setrlimit(Process::RLIMIT_NOFILE, 4096) # arbitrary high number of max file descriptors.
|
10
|
+
server = self.new(bind_to || Config.bind_to)
|
11
|
+
set_signal_traps(server)
|
12
|
+
server
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
def set_signal_traps(server)
|
17
|
+
interrupt_tries = 0
|
18
|
+
Signal.trap(Signal.list["INT"]) do
|
19
|
+
interrupt_tries += 1
|
20
|
+
if interrupt_tries == 1
|
21
|
+
server.stop
|
22
|
+
else
|
23
|
+
exit
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
attr_reader :host, :port, :running
|
29
|
+
def initialize(bind_to)
|
30
|
+
write_pid_file
|
31
|
+
|
32
|
+
@host, @port = host_port(bind_to)
|
33
|
+
@clients = []
|
34
|
+
@total_clients = 0
|
35
|
+
@clients_mutex = Mutex.new
|
36
|
+
@num_jobs_processed = MutexCounter.new
|
37
|
+
@num_jobs_entered = MutexCounter.new
|
38
|
+
@requests_per_second = RequestsPerSecond.new(10)
|
39
|
+
|
40
|
+
@peers = with_mutex({})
|
41
|
+
@servers = Config.servers
|
42
|
+
@servers.each do |peer|
|
43
|
+
next if peer == bind_to # not a peer, it's itself
|
44
|
+
@peers[peer] = Peer.new(peer, self)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def handler=(handler)
|
49
|
+
@handler = handler
|
50
|
+
@handler_instance = handler.new
|
51
|
+
end
|
52
|
+
|
53
|
+
def start
|
54
|
+
raise "You must specify a handler" unless @handler
|
55
|
+
|
56
|
+
@listener = TCPServer.new(@host, @port)
|
57
|
+
@running = true
|
58
|
+
@running_since = Time.now
|
59
|
+
|
60
|
+
# debug("Connecting to peers")
|
61
|
+
@peers.values.each { |peer| peer.connect }
|
62
|
+
|
63
|
+
@listener_thread = Thread.new do
|
64
|
+
begin
|
65
|
+
while @running
|
66
|
+
begin
|
67
|
+
# debug("Listening")
|
68
|
+
conn = @listener.accept
|
69
|
+
Thread.new(conn) { |c| handle_connection(c) }
|
70
|
+
rescue Errno::ECONNABORTED, Errno::EINTR # TODO: what else can wrong here? this should be more robust.
|
71
|
+
debug("Connection accepted aborted")
|
72
|
+
conn.close if conn && !conn.closed?
|
73
|
+
end
|
74
|
+
end
|
75
|
+
rescue StopServer
|
76
|
+
info("Stopping server")
|
77
|
+
remove_pid_file
|
78
|
+
@listener.close if @listener
|
79
|
+
@peers.values.each { |p| p.disconnect }
|
80
|
+
@clients.each {|c| c.close }
|
81
|
+
self.processor.disconnect if Config.fork_for_processor
|
82
|
+
rescue Exception => e
|
83
|
+
warn("Unhandled exception in server listening thread:\n#{e.inspect}\n#{e.backtrace.join("\n")}")
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def stop
|
89
|
+
@running = false
|
90
|
+
@listener_thread.raise(StopServer)
|
91
|
+
end
|
92
|
+
|
93
|
+
def handle_connection(connection)
|
94
|
+
begin
|
95
|
+
connection.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) if TCP_NO_DELAY_AVAILABLE
|
96
|
+
|
97
|
+
identification = connection.gets.strip
|
98
|
+
# info("Incoming connection from #{connection.peeraddr.values_at(3,1).join(":")} (#{identification})")
|
99
|
+
if identification =~ /^SERVER ([a-zA-Z0-9.]+:[0-9]+)$/
|
100
|
+
# debug("Recognized as peer")
|
101
|
+
host, port = host_port($1)
|
102
|
+
matching_peer = @peers.values.detect { |peer| [peer.host, peer.port] == [host, port] }
|
103
|
+
if matching_peer
|
104
|
+
# debug("Found matching peer")
|
105
|
+
else
|
106
|
+
# debug("Didn't find matching peer, adding it")
|
107
|
+
matching_peer = @peers.synchronize do
|
108
|
+
hostport = "#{host}:#{port}"
|
109
|
+
@servers.push(hostport) unless @servers.include?(hostport)
|
110
|
+
@peers[hostport] ||= Peer.new(hostport, self)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
matching_peer.add_incoming_connection(connection)
|
114
|
+
elsif identification =~ /^CLIENT$/
|
115
|
+
# debug("Recognized as client")
|
116
|
+
@clients_mutex.synchronize do
|
117
|
+
@clients << Client.new(connection, self).listen
|
118
|
+
@total_clients += 1
|
119
|
+
end
|
120
|
+
elsif identification =~ /^stats$/
|
121
|
+
# debug("Stats request received")
|
122
|
+
print_stats(connection)
|
123
|
+
else
|
124
|
+
debug("Unrecognized connection. Closing.")
|
125
|
+
connection.close # i dunno you
|
126
|
+
end
|
127
|
+
rescue Exception => e
|
128
|
+
warn("Unhandled exception in handle connection method:\n#{e.inspect}\n#{e.backtrace.join("\n")}")
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def handle_client_request(request)
|
133
|
+
# info("Handling client request")
|
134
|
+
map = @handler_instance.partition(request, connection_statuses)
|
135
|
+
request.max_responses = map.size
|
136
|
+
# debug("Sending client request to #{map.size} handlers (#{request.hash})")
|
137
|
+
|
138
|
+
map.each do |peer, body|
|
139
|
+
if @peers[peer]
|
140
|
+
@peers[peer].client_request(request, body)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
if map[signature]
|
145
|
+
# debug("Processing #{request.hash}")
|
146
|
+
Thread.new do
|
147
|
+
begin
|
148
|
+
request.add_response(self.process(map[signature]))
|
149
|
+
rescue Exception => e
|
150
|
+
warn("Unhandled exception in local processing:\n#{e.inspect}#{e.backtrace.join("\n")}}")
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
@requests_per_second.hit
|
156
|
+
|
157
|
+
# debug("Waiting for responses")
|
158
|
+
request.wait_for_responses
|
159
|
+
|
160
|
+
# debug("Done waiting for responses, calling reduce")
|
161
|
+
@handler_instance.reduce(request)
|
162
|
+
end
|
163
|
+
|
164
|
+
def process(body)
|
165
|
+
@num_jobs_entered.inc
|
166
|
+
response = if Config.fork_for_processor
|
167
|
+
self.processor.with_connection {|con| con.process(body) }
|
168
|
+
else
|
169
|
+
@handler_instance.process(body)
|
170
|
+
end
|
171
|
+
@num_jobs_processed.inc
|
172
|
+
response
|
173
|
+
end
|
174
|
+
|
175
|
+
def signature
|
176
|
+
@signature ||= "#{@host}:#{@port}"
|
177
|
+
end
|
178
|
+
|
179
|
+
def connection_statuses
|
180
|
+
@servers.inject({}) do |statuses, server|
|
181
|
+
if server == signature
|
182
|
+
statuses[server] = :self
|
183
|
+
else
|
184
|
+
statuses[server] = @peers[server].connected? ? :connected : :disconnected
|
185
|
+
end
|
186
|
+
statuses
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def client_closed(client)
|
191
|
+
@clients_mutex.synchronize do
|
192
|
+
@clients.delete(client)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def processor
|
197
|
+
@processor ||= begin
|
198
|
+
processor = ConnectionPool.new
|
199
|
+
processor.create_connection { Processor.new(@handler) }
|
200
|
+
processor
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
private
|
205
|
+
def print_stats(connection)
|
206
|
+
begin
|
207
|
+
stats = collect_stats
|
208
|
+
str = []
|
209
|
+
str << "Uptime: #{stats[:uptime]}"
|
210
|
+
str << "Number of Threads: #{stats[:num_threads]}"
|
211
|
+
str << "Connected Clients: #{stats[:connected_clients]}"
|
212
|
+
str << "Clients Ever: #{stats[:total_clients]}"
|
213
|
+
str << "Connected Peers: #{stats[:connected_peers]}"
|
214
|
+
str << "Disconnected Peers: #{stats[:disconnected_peers]}"
|
215
|
+
str << "Total Requests: #{stats[:num_requests]}"
|
216
|
+
str << "Pending Requests: #{stats[:pending_requests]}"
|
217
|
+
str << "Late Responses: #{stats[:late_responses]}"
|
218
|
+
str << "Total Jobs Processed: #{stats[:total_jobs_processed]}"
|
219
|
+
str << "Pending Jobs: #{stats[:jobs_pending]}"
|
220
|
+
str << "Requests per Second (10 sec): #{"%.1f" % stats[:rps_10]}"
|
221
|
+
str << "Requests per Second (lifetime): #{"%.1f" % stats[:rps_lifetime]}"
|
222
|
+
connection.puts(str.join("\n"))
|
223
|
+
end while (s = connection.gets) && (s.strip == "stats" || s.strip == "")
|
224
|
+
connection.close if connection && !connection.closed?
|
225
|
+
end
|
226
|
+
|
227
|
+
def debug(msg)
|
228
|
+
logger.debug("Server #{signature}") {msg}
|
229
|
+
end
|
230
|
+
|
231
|
+
def info(msg)
|
232
|
+
logger.info("Server #{signature}") {msg}
|
233
|
+
end
|
234
|
+
|
235
|
+
def warn(msg)
|
236
|
+
logger.warn("Server #{signature}") {msg}
|
237
|
+
end
|
238
|
+
|
239
|
+
def collect_stats
|
240
|
+
results = {}
|
241
|
+
results[:num_threads] = Thread.list.size
|
242
|
+
results[:connected_clients], results[:total_clients] = \
|
243
|
+
@clients_mutex.synchronize { [@clients.size, @total_clients] }
|
244
|
+
|
245
|
+
results[:connected_peers], results[:disconnected_peers] = \
|
246
|
+
connection_statuses.inject([0,0]) do |counts, (server,status)|
|
247
|
+
if status == :connected
|
248
|
+
counts[0] += 1
|
249
|
+
elsif status == :disconnected
|
250
|
+
counts[1] += 1
|
251
|
+
end
|
252
|
+
counts
|
253
|
+
end
|
254
|
+
results[:total_jobs_processed] = @num_jobs_processed.to_i
|
255
|
+
results[:jobs_pending] = @num_jobs_entered.to_i - results[:total_jobs_processed]
|
256
|
+
results[:num_requests] = Request.total_request_count
|
257
|
+
results[:late_responses] = Request.total_late_responses
|
258
|
+
results[:pending_requests] = @clients_mutex.synchronize do
|
259
|
+
@clients.inject(0) do |pending, client|
|
260
|
+
pending + (client.received_requests - client.responded_requests.to_i)
|
261
|
+
end
|
262
|
+
end
|
263
|
+
results[:rps_10] = @requests_per_second.rps
|
264
|
+
|
265
|
+
results[:uptime] = Time.now - @running_since
|
266
|
+
results[:rps_lifetime] = results[:num_requests] / results[:uptime]
|
267
|
+
|
268
|
+
results
|
269
|
+
end
|
270
|
+
|
271
|
+
def remove_pid_file
|
272
|
+
File.unlink(Config.pid_file) if Config.pid_file && File.exists?(Config.pid_file)
|
273
|
+
end
|
274
|
+
|
275
|
+
def write_pid_file
|
276
|
+
return if Config.pid_file.nil?
|
277
|
+
File.open(Config.pid_file,"w") do |f|
|
278
|
+
f.write(Process.pid)
|
279
|
+
File.chmod(0644, Config.pid_file)
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|