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.
Files changed (38) hide show
  1. data/MIT-LICENSE +22 -0
  2. data/Manifest +27 -0
  3. data/README.markdown +133 -0
  4. data/Rakefile +14 -0
  5. data/examples/client/client.rb +17 -0
  6. data/examples/client/constitution.txt +865 -0
  7. data/examples/client/pandemic_client.yml +3 -0
  8. data/examples/server/pandemic_server.yml +3 -0
  9. data/examples/server/word_count_server.rb +47 -0
  10. data/lib/pandemic.rb +42 -0
  11. data/lib/pandemic/client_side/cluster_connection.rb +194 -0
  12. data/lib/pandemic/client_side/config.rb +34 -0
  13. data/lib/pandemic/client_side/connection.rb +40 -0
  14. data/lib/pandemic/client_side/connection_proxy.rb +15 -0
  15. data/lib/pandemic/client_side/pandemize.rb +17 -0
  16. data/lib/pandemic/connection_pool.rb +194 -0
  17. data/lib/pandemic/mutex_counter.rb +50 -0
  18. data/lib/pandemic/requests_per_second.rb +31 -0
  19. data/lib/pandemic/server_side/client.rb +123 -0
  20. data/lib/pandemic/server_side/config.rb +74 -0
  21. data/lib/pandemic/server_side/handler.rb +31 -0
  22. data/lib/pandemic/server_side/peer.rb +211 -0
  23. data/lib/pandemic/server_side/processor.rb +90 -0
  24. data/lib/pandemic/server_side/request.rb +92 -0
  25. data/lib/pandemic/server_side/server.rb +285 -0
  26. data/lib/pandemic/util.rb +15 -0
  27. data/pandemic.gemspec +37 -0
  28. data/test/client_test.rb +87 -0
  29. data/test/connection_pool_test.rb +133 -0
  30. data/test/functional_test.rb +57 -0
  31. data/test/handler_test.rb +31 -0
  32. data/test/mutex_counter_test.rb +73 -0
  33. data/test/peer_test.rb +48 -0
  34. data/test/processor_test.rb +33 -0
  35. data/test/server_test.rb +171 -0
  36. data/test/test_helper.rb +24 -0
  37. data/test/util_test.rb +21 -0
  38. 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