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,50 @@
1
+ module Pandemic
2
+ class MutexCounter
3
+ MAX = (2 ** 30) - 1
4
+ def initialize(max = MAX)
5
+ @mutex = Mutex.new
6
+ @counter = 0
7
+ @resets = 0
8
+ @max = max
9
+ end
10
+
11
+ def real_total
12
+ @mutex.synchronize { (@resets * @max) + @counter }
13
+ end
14
+ alias_method :to_i, :real_total
15
+
16
+ def value
17
+ @mutex.synchronize { @counter }
18
+ end
19
+
20
+ def inc
21
+ @mutex.synchronize do
22
+ if @counter >= @max
23
+ @counter = 0 # to avoid Bignum, it's about 4x slower
24
+ @resets += 1
25
+ end
26
+ @counter += 1
27
+ end
28
+ end
29
+ alias_method :next, :inc
30
+ alias_method :succ, :inc
31
+
32
+ # decr only to zero
33
+ def decr
34
+ @mutex.synchronize do
35
+ if @counter > 0
36
+ @counter -= 1
37
+ else
38
+ if @resets > 1
39
+ @resets -= 1
40
+ @counter = @max
41
+ end
42
+ end
43
+ @counter
44
+ end
45
+ end
46
+ alias_method :pred, :decr
47
+ alias_method :prev, :decr
48
+
49
+ end
50
+ end
@@ -0,0 +1,31 @@
1
+ module Pandemic
2
+ class RequestsPerSecond
3
+ def initialize(sample_size = 10)
4
+ @hits = Array.new(sample_size + 2)
5
+ @last_hit_at = nil
6
+ end
7
+
8
+ def hit(now = Time.now.to_i)
9
+ key = now % @hits.size
10
+ if @hits[key].nil? || @hits[key][0] != now
11
+ @hits[key] = [now, 0]
12
+ end
13
+ @hits[key][1] += 1
14
+ @last_hit_at = now
15
+ end
16
+
17
+ def rps(now = Time.now.to_i)
18
+ return 0 if @last_hit_at.nil?
19
+ entries_to_go_back = @hits.size - (now - @last_hit_at) - 2
20
+ return 0 if entries_to_go_back <= 0
21
+ sum = 0
22
+ entries_to_go_back.times do |i|
23
+ now -= 1
24
+ if @hits[now % @hits.size] && @hits[now % @hits.size][0] == now
25
+ sum += @hits[now % @hits.size][1]
26
+ end
27
+ end
28
+ return sum.to_f / (@hits.size - 2)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,123 @@
1
+ module Pandemic
2
+ module ServerSide
3
+ class Client
4
+ REQUEST_FLAGS = {:async => 'a'}
5
+ EMPTY_STRING = ""
6
+ REQUEST_REGEXP = /^([0-9]+)(?: ([#{REQUEST_FLAGS.values.join('')}]*))?$/
7
+ class DisconnectClient < Exception; end
8
+ include Util
9
+
10
+ attr_accessor :received_requests, :responded_requests
11
+
12
+ def initialize(connection, server)
13
+ @connection = connection
14
+ @server = server
15
+ @received_requests = 0
16
+ @responded_requests = MutexCounter.new
17
+ @current_request = nil
18
+ end
19
+
20
+ def listen
21
+ unless @connection.nil?
22
+ @listener_thread.kill if @listener_thread
23
+ @listener_thread = Thread.new do
24
+ begin
25
+ while @server.running
26
+ # debug("Waiting for incoming request")
27
+ request = @connection.gets
28
+ # info("Received incoming request")
29
+ @received_requests += 1
30
+
31
+ if request.nil?
32
+ # debug("Incoming request is nil")
33
+ @connection.close
34
+ @connection = nil
35
+ break
36
+ elsif request.strip! =~ REQUEST_REGEXP
37
+ size, flags = $1.to_i, $2.to_s.split(EMPTY_STRING)
38
+ # debug("Reading request body (size #{size})")
39
+ body = @connection.read(size)
40
+ # debug("Finished reading request body")
41
+ if flags.include?(REQUEST_FLAGS[:async])
42
+ Thread.new do
43
+ handle_request(body)
44
+ @responded_requests.inc
45
+ end
46
+ else
47
+ response = handle_request(body)
48
+ if response
49
+ # debug("Writing response to client")
50
+
51
+ # the connection could be closed, we'll let it be rescued if it is.
52
+ @connection.write("#{response.size}\n#{response}")
53
+ @connection.flush
54
+ # debug("Finished writing response to client")
55
+ else
56
+ # debug("Writing error code to client")
57
+
58
+ @connection.write("-1\n")
59
+ @connection.flush
60
+ # debug("Finished writing error code to client")
61
+ end
62
+ @responded_requests.inc
63
+ end
64
+ end
65
+ end
66
+ rescue DisconnectClient
67
+ info("Closing client connection")
68
+ close_connection
69
+ rescue Errno::EPIPE
70
+ info("Connection to client lost")
71
+ close_connection
72
+ rescue Exception => e
73
+ warn("Unhandled exception in client listen thread:\n#{e.inspect}\n#{e.backtrace.join("\n")}")
74
+ ensure
75
+ @current_request.cancel! if @current_request
76
+ @server.client_closed(self)
77
+ end
78
+ end
79
+ end
80
+ return self
81
+ end
82
+
83
+ def close
84
+ @listener_thread.raise(DisconnectClient)
85
+ end
86
+
87
+
88
+
89
+ def handle_request(request)
90
+ @current_request = Request.new(request)
91
+ response = begin
92
+ @server.handle_client_request(@current_request)
93
+ rescue Exception => e
94
+ warn("Unhandled exception in handle client request:\n#{e.inspect}\n#{e.backtrace.join("\n")}")
95
+ nil
96
+ end
97
+ @current_request = nil
98
+ return response
99
+ end
100
+
101
+ private
102
+ def close_connection
103
+ @connection.close unless @connection.nil? || @connection.closed?
104
+ end
105
+
106
+ def signature
107
+ @signature ||= @connection.peeraddr.values_at(3,1).join(":")
108
+ end
109
+
110
+ def debug(msg)
111
+ logger.debug("Client #{signature}") {msg}
112
+ end
113
+
114
+ def info(msg)
115
+ logger.info("Client #{signature}") {msg}
116
+ end
117
+
118
+ def warn(msg)
119
+ logger.warn("Client #{signature}") {msg}
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,74 @@
1
+ module Pandemic
2
+ module ServerSide
3
+ class Config
4
+ class << self
5
+ attr_accessor :bind_to, :servers, :response_timeout, :fork_for_processor, :pid_file
6
+ def load
7
+ parse_args!
8
+ raise "Interface to bind to is nil." unless @bind_to
9
+ end
10
+
11
+ def get(*args)
12
+ args.size == 1 ? @options[args.first] : @options.values_at(*args) if @options
13
+ end
14
+
15
+ private
16
+
17
+ def parse_args!
18
+ config_path = "pandemic_server.yml"
19
+ index = nil
20
+ attach = nil
21
+
22
+ @bind_to = nil
23
+ @pid_file = nil
24
+ OptionParser.new do |opts|
25
+ opts.on("-c", "--config [CONFIG-PATH]", "Specify the path to the config file") do |path|
26
+ config_path = path
27
+ end
28
+
29
+ opts.on("-i", "--index [SERVER-INDEX]", "Specify the index of the server to attach to from the YAML file") do |i|
30
+ index = i
31
+ end
32
+
33
+ opts.on("-a", "--attach [SERVER:PORT]", "Specify the host and port to attach to") do |a|
34
+ attach = a
35
+ end
36
+
37
+ opts.on("-P", "--pid-file [PATH]", "Specify the path to write the PID to") do |path|
38
+ @pid_file = path
39
+ end
40
+ end.parse!
41
+
42
+ read_config_file(config_path)
43
+
44
+ if index
45
+ index = index.to_i if @server_map.is_a?(Array)
46
+ server = @server_map[index]
47
+
48
+ @bind_to = if server.is_a?(Hash)
49
+ @options = server.values.first # there should only be one
50
+ @server_map[index].keys.first
51
+ else
52
+ server
53
+ end
54
+ elsif attach
55
+ @bind_to = attach
56
+ end
57
+
58
+ end
59
+
60
+ def read_config_file(path)
61
+ yaml = YAML.load_file(path)
62
+
63
+ @server_map = yaml['servers'] || []
64
+ @servers = @server_map.is_a?(Hash) ? @server_map.values : @server_map
65
+ @servers = @servers.collect { |s| s.is_a?(Hash) ? s.keys.first : s }
66
+
67
+ @response_timeout = (yaml['response_timeout'] || 1).to_f
68
+
69
+ @fork_for_processor = yaml['fork_for_processor']
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,31 @@
1
+ module Pandemic
2
+ module ServerSide
3
+ class Handler
4
+ def config
5
+ Config
6
+ end
7
+
8
+ def partition(request, servers)
9
+ map = {}
10
+ servers.each do |server, status|
11
+ if status != :disconnected
12
+ map[server] = request.body
13
+ end
14
+ end
15
+ map
16
+ end
17
+
18
+ def reduce(request)
19
+ request.responses.join("")
20
+ end
21
+
22
+ def process(body)
23
+ body
24
+ end
25
+
26
+ def filter_alive(servers)
27
+ servers.keys.select{|k| servers[k] != :disconnected}
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,211 @@
1
+ module Pandemic
2
+ module ServerSide
3
+ class Peer
4
+ class PeerUnavailableException < StandardError; end
5
+ include Util
6
+ attr_reader :host, :port
7
+
8
+ def initialize(addr, server)
9
+ @host, @port = host_port(addr)
10
+ @server = server
11
+ @pending_requests = with_mutex({})
12
+ @incoming_connection_listeners = []
13
+ @inc_threads_mutex = Mutex.new
14
+ initialize_connection_pool
15
+ end
16
+
17
+ def connect
18
+ # debug("Forced connection to peer")
19
+ @connection_pool.connect
20
+ end
21
+
22
+ def disconnect
23
+ # debug("Disconnecting from peer")
24
+ @connection_pool.disconnect
25
+ end
26
+
27
+ def connected?
28
+ @connection_pool.connected?
29
+ end
30
+
31
+ def client_request(request, body)
32
+ # debug("Sending client's request to peer")
33
+ # debug("Connection pool has #{@connection_pool.available_count} of #{@connection_pool.connections_count} connections available")
34
+
35
+ successful = true
36
+ @pending_requests.synchronize do
37
+ @pending_requests[request.hash] = request
38
+ end
39
+ begin
40
+ @connection_pool.with_connection do |connection|
41
+ if connection && !connection.closed?
42
+ # debug("Writing client's request")
43
+ connection.write("P#{request.hash}#{[body.size].pack('N')}#{body}")
44
+ connection.flush
45
+ # debug("Finished writing client's request")
46
+ else
47
+ successful = false
48
+ end
49
+ end
50
+ rescue Exception
51
+ @pending_requests.synchronize { @pending_requests.delete(request.hash) }
52
+ raise
53
+ else
54
+ if !successful
55
+ @pending_requests.synchronize { @pending_requests.delete(request.hash) }
56
+ end
57
+ end
58
+ end
59
+
60
+ def add_incoming_connection(conn)
61
+ # debug("Adding incoming connection")
62
+
63
+ connect # if we're not connected, we should be
64
+
65
+
66
+ thread = Thread.new(conn) do |connection|
67
+ begin
68
+ # debug("Incoming connection thread started")
69
+ while @server.running
70
+ # debug("Listening for incoming requests")
71
+ request = connection.read(15)
72
+ # debug("Read incoming request from peer")
73
+
74
+ if request.nil?
75
+ # debug("Incoming connection request is nil")
76
+ break
77
+ else
78
+ # debug("Received incoming (#{request.strip})")
79
+ handle_incoming_request(request, connection) if request =~ /^P/
80
+ handle_incoming_response(request, connection) if request =~ /^R/
81
+ end
82
+ end
83
+ rescue Exception => e
84
+ warn("Unhandled exception in peer listener thread:\n#{e.inspect}\n#{e.backtrace.join("\n")}")
85
+ ensure
86
+ # debug("Incoming connection closing")
87
+ conn.close if conn && !conn.closed?
88
+ @inc_threads_mutex.synchronize { @incoming_connection_listeners.delete(Thread.current)}
89
+ if @incoming_connection_listeners.empty?
90
+ disconnect
91
+ end
92
+ end
93
+ end
94
+
95
+ @inc_threads_mutex.synchronize {@incoming_connection_listeners.push(thread) if thread.alive? }
96
+ end
97
+
98
+ private
99
+ def initialize_connection_pool
100
+ return if @connection_pool
101
+ @connection_pool = ConnectionPool.new(:connect_at_define => false)
102
+
103
+ @connection_pool.create_connection do
104
+ connection = nil
105
+ retries = 0
106
+ begin
107
+ connection = TCPSocket.new(@host, @port)
108
+ rescue Errno::ETIMEDOUT, Errno::ECONNREFUSED => e
109
+ connection = nil
110
+ # debug("Connection timeout or refused: #{e.inspect}")
111
+ if retries == 0
112
+ # debug("Retrying connection")
113
+ retries += 1
114
+ sleep 0.01
115
+ retry
116
+ end
117
+ rescue Exception => e
118
+ warn("Unhandled exception in create connection block:\n#{e.inspect}\n#{e.backtrace.join("\n")}")
119
+ end
120
+ if connection
121
+ connection.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) if TCP_NO_DELAY_AVAILABLE
122
+ connection.write("SERVER #{@server.signature}\n")
123
+ end
124
+ connection
125
+ end
126
+ end
127
+
128
+ def handle_incoming_request(request, connection)
129
+ # debug("Identified as request")
130
+ hash = request[1,10]
131
+ size = request[11, 4].unpack('N').first
132
+ # debug("Incoming request: #{hash} #{size}")
133
+ begin
134
+ # debug("Reading request body")
135
+ request_body = connection.read(size)
136
+ # debug("Finished reading request body")
137
+ rescue EOFError, TruncatedDataError
138
+ # debug("Failed to read request body")
139
+ # TODO: what to do here?
140
+ return false
141
+ rescue Exception => e
142
+ warn("Unhandled exception in incoming request read:\n#{e.inspect}\n#{e.backtrace.join("\n")}")
143
+ end
144
+ # debug("Processing body")
145
+ process_request(hash, request_body)
146
+ end
147
+
148
+ def handle_incoming_response(response, connection)
149
+ hash = response[1,10]
150
+ size = response[11, 4].unpack('N').first
151
+ # debug("Incoming response: #{hash} #{size}")
152
+ begin
153
+ # debug("Reading response body")
154
+ response_body = connection.read(size)
155
+ # debug("Finished reading response body")
156
+ rescue EOFError, TruncatedDataError
157
+ # debug("Failed to read response body")
158
+ # TODO: what to do here?
159
+ return false
160
+ rescue Exception => e
161
+ warn("Unhandled exception in incoming response read:\n#{e.inspect}\n#{e.backtrace.join("\n")}")
162
+ end
163
+ process_response(hash, response_body)
164
+ end
165
+
166
+
167
+ def process_request(hash, body)
168
+ Thread.new do
169
+ begin
170
+ # debug("Starting processing thread (#{hash})")
171
+ response = @server.process(body)
172
+ # debug("Processing finished (#{hash})")
173
+ @connection_pool.with_connection do |connection|
174
+ # debug( "Sending response (#{hash})")
175
+ connection.write("R#{hash}#{[response.size].pack('N')}#{response}")
176
+ connection.flush
177
+ # debug( "Finished sending response (#{hash})")
178
+ end
179
+ rescue Exception => e
180
+ warn("Unhandled exception in process request thread:\n#{e.inspect}\n#{e.backtrace.join("\n")}")
181
+ end
182
+ end
183
+ end
184
+
185
+ def process_response(hash, body)
186
+ Thread.new do
187
+ begin
188
+ # debug("Finding original request (#{hash})")
189
+ original_request = @pending_requests.synchronize { @pending_requests.delete(hash) }
190
+ if original_request
191
+ # debug("Found original request, adding response")
192
+ original_request.add_response(body)
193
+ else
194
+ warn("Original response not found (#{hash})")
195
+ end
196
+ rescue Exception => e
197
+ warn("Unhandled exception in process response thread:\n#{e.inspect}\n#{e.backtrace.join("\n")}")
198
+ end
199
+ end
200
+ end
201
+
202
+ def debug(msg)
203
+ logger.debug("Peer #{@host}:#{@port}") { msg }
204
+ end
205
+
206
+ def warn(msg)
207
+ logger.warn("Peer #{@host}:#{@port}") { msg }
208
+ end
209
+ end
210
+ end
211
+ end