pandemic 0.5.4
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/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
|