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