pandemic 0.5.4

Sign up to get free protection for your applications and to get access to all the features.
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