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,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