arya-pandemic 0.2

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.
@@ -0,0 +1,24 @@
1
+ module Pandemic
2
+ class MutexCounter
3
+ MAX = (2 ** 30) - 1
4
+ def initialize
5
+ @mutex = Mutex.new
6
+ @counter = 0
7
+ @resets = 0
8
+ end
9
+
10
+ def real_total
11
+ @mutex.synchronize { (@resets * MAX) + @counter }
12
+ end
13
+
14
+ def inc
15
+ @mutex.synchronize do
16
+ if @counter >= MAX
17
+ @counter = 0 # to avoid Bignum, it's about 4x slower
18
+ @resets += 1
19
+ end
20
+ @counter += 1
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,86 @@
1
+ module Pandemic
2
+ module ServerSide
3
+ class Client
4
+ class DisconnectClient < Exception; end
5
+ include Util
6
+
7
+ attr_accessor :received_requests, :responded_requests
8
+
9
+ def initialize(connection, server)
10
+ @connection = connection
11
+ @server = server
12
+ @received_requests = 0
13
+ @responded_requests = 0
14
+ end
15
+
16
+ def listen
17
+ unless @connection.nil?
18
+ @listener_thread.kill if @listener_thread
19
+ @listener_thread = Thread.new do
20
+ begin
21
+ while @server.running
22
+ debug("Waiting for incoming request")
23
+ request = @connection.gets
24
+ info("Received incoming request")
25
+ @received_requests += 1
26
+
27
+ if request.nil?
28
+ debug("Incoming request is nil")
29
+ @connection.close
30
+ @connection = nil
31
+ break
32
+ elsif request.strip! =~ /^([0-9]+)$/ # currently only asking for request size
33
+ size = $1.to_i
34
+ debug("Reading request body (size #{size})")
35
+ body = @connection.read(size)
36
+ debug("Finished reading request body")
37
+
38
+ response = handle_request(body)
39
+
40
+ debug("Writing response to client")
41
+ @connection.write("#{response.size}\n#{response}")
42
+ @connection.flush
43
+ @responded_requests += 1
44
+ debug("Finished writing response to client")
45
+ end
46
+ end
47
+ rescue DisconnectClient
48
+ info("Closing client connection")
49
+ @connection.close unless @connection.nil? || @connection.closed?
50
+ rescue Exception => e
51
+ warn("Unhandled exception in client listen thread: #{e.inspect}")
52
+ ensure
53
+ @server.client_closed(self)
54
+ end
55
+ end
56
+ end
57
+ return self
58
+ end
59
+
60
+ def close
61
+ @listener_thread.raise(DisconnectClient)
62
+ end
63
+
64
+ def handle_request(request)
65
+ @server.handle_client_request(Request.new(request))
66
+ end
67
+
68
+ private
69
+ def signature
70
+ @signature ||= @connection.peeraddr.values_at(3,1).join(":")
71
+ end
72
+
73
+ def debug(msg)
74
+ logger.debug("Client #{signature}") {msg}
75
+ end
76
+
77
+ def info(msg)
78
+ logger.info("Client #{signature}") {msg}
79
+ end
80
+
81
+ def warn(msg)
82
+ logger.warn("Client #{signature}") {msg}
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,55 @@
1
+ module Pandemic
2
+ module ServerSide
3
+ class Config
4
+ class << self
5
+ attr_accessor :bind_to, :servers, :response_timeout
6
+ def load
7
+ path = extract_config_path
8
+ yaml = YAML.load_file(path)
9
+
10
+ @server_map = yaml['servers'] || []
11
+ @servers = @server_map.is_a?(Hash) ? @server_map.values : @server_map
12
+ @servers = @servers.collect { |s| s.is_a?(Hash) ? s.keys.first : s }
13
+
14
+ @response_timeout = (yaml['response_timeout'] || 1).to_f
15
+ @bind_to = extract_bind_to
16
+ raise "Interface to bind to is nil." unless @bind_to
17
+ end
18
+
19
+ def get(*args)
20
+ args.size == 1 ? @options[args.first] : @options.values_at(*args) if @options
21
+ end
22
+
23
+ private
24
+ def extract_bind_to
25
+ index = ARGV.index('-i')
26
+ index2 = ARGV.index('-a')
27
+
28
+ if index && (key = ARGV[index + 1])
29
+ key = key.to_i if @server_map.is_a?(Array)
30
+ server = @server_map[key]
31
+ if server.is_a?(Hash)
32
+ @options = server.values.first # there should only be one
33
+ @server_map[key].keys.first
34
+ else
35
+ server
36
+ end
37
+ elsif index2 && (host = ARGV[index2 + 1])
38
+ host
39
+ else
40
+ raise "You must specify which interface to bind to."
41
+ end
42
+ end
43
+
44
+ def extract_config_path
45
+ index = ARGV.index('-c')
46
+ if index && (path = ARGV[index + 1])
47
+ path
48
+ else
49
+ "pandemic_server.yml"
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,27 @@
1
+ module Pandemic
2
+ module ServerSide
3
+ class Handler
4
+ def config
5
+ Config
6
+ end
7
+
8
+ def map(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
+ end
26
+ end
27
+ end
@@ -0,0 +1,203 @@
1
+ module Pandemic
2
+ module ServerSide
3
+ class Peer
4
+ class PeerUnavailableException < Exception; 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
+ return if connected?
19
+ debug("Forced connection to peer")
20
+ 1.times { @connection_pool.add_connection! }
21
+ end
22
+
23
+ def disconnect
24
+ debug("Disconnecting from peer")
25
+ @connection_pool.disconnect
26
+ end
27
+
28
+ def connected?
29
+ @connection_pool.connected?
30
+ end
31
+
32
+ def client_request(request, body)
33
+ debug("Sending client's request to peer")
34
+ # TODO: Consider adding back threads here if it will be faster that way in Ruby 1.9
35
+ @connection_pool.with_connection do |connection|
36
+ if connection && !connection.closed?
37
+ @pending_requests.synchronize do
38
+ @pending_requests[request.hash] = request
39
+ end
40
+ debug("Writing client's request")
41
+ connection.write("PROCESS #{request.hash} #{body.size}\n#{body}")
42
+ connection.flush
43
+ debug("Finished writing client's request")
44
+ end # TODO: else? fail silently? reconnect?
45
+ end
46
+ end
47
+
48
+ def add_incoming_connection(conn)
49
+ debug("Adding incoming connection")
50
+
51
+ connect # if we're not connected, we should be
52
+
53
+ thread = Thread.new(conn) do |connection|
54
+ begin
55
+ debug("Incoming connection thread started")
56
+ while @server.running
57
+ debug("Listening for incoming requests")
58
+ request = connection.gets
59
+ debug("Read incoming request from peer")
60
+
61
+ if request.nil?
62
+ debug("Incoming connection request is nil")
63
+ break
64
+ else
65
+ debug("Received incoming (#{request.strip})")
66
+ handle_incoming_request(request, connection) if request =~ /^PROCESS/
67
+ handle_incoming_response(request, connection) if request =~ /^RESPONSE/
68
+ end
69
+ end
70
+ rescue Exception => e
71
+ warn("Unhandled exception in peer listener thread: #{e.inspect}")
72
+ ensure
73
+ debug("Incoming connection closing")
74
+ conn.close if conn && !conn.closed?
75
+ @inc_threads_mutex.synchronize { @incoming_connection_listeners.delete(Thread.current)}
76
+ if @incoming_connection_listeners.empty?
77
+ disconnect
78
+ end
79
+ end
80
+ end
81
+
82
+ @inc_threads_mutex.synchronize {@incoming_connection_listeners.push(thread) if thread.alive? }
83
+ end
84
+
85
+ private
86
+ def initialize_connection_pool
87
+ return if @connection_pool
88
+ @connection_pool = ConnectionPool.new
89
+
90
+ @connection_pool.create_connection do
91
+ connection = nil
92
+ begin
93
+ connection = TCPSocket.new(@host, @port)
94
+ rescue Errno::ETIMEDOUT, Errno::ECONNREFUSED
95
+ connection = nil
96
+ rescue Exception => e
97
+ warn("Unhandled exception in create connection block: #{e.inspect}")
98
+ end
99
+ if connection
100
+ connection.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) if Socket.constants.include?('TCP_NODELAY')
101
+ connection.write("SERVER #{@server.signature}\n")
102
+ end
103
+ connection
104
+ end
105
+
106
+ end
107
+
108
+ def handle_incoming_request(request, connection)
109
+ debug("Identified as request")
110
+ if request.strip =~ /^PROCESS ([A-Za-z0-9]+) ([0-9]+)$/
111
+ hash = $1
112
+ size = $2.to_i
113
+ debug("Incoming request: #{hash} #{size}")
114
+ begin
115
+ debug("Reading request body")
116
+ request_body = connection.read(size)
117
+ debug("Finished reading request body")
118
+ rescue EOFError, TruncatedDataError
119
+ debug("Failed to read request body")
120
+ # TODO: what to do here?
121
+ return false
122
+ rescue Exception => e
123
+ warn("Unhandled exception in incoming request read: #{e.inspect}")
124
+ end
125
+ debug("Processing body")
126
+ process_request(hash, request_body)
127
+ else
128
+ warn("Malformed incoming request: #{request.strip}")
129
+ # when the incoming request was malformed
130
+ # TODO: what to do here?
131
+ end
132
+ end
133
+
134
+ def handle_incoming_response(response, connection)
135
+ if response.strip =~ /^RESPONSE ([A-Za-z0-9]+) ([0-9]+)$/
136
+ hash = $1
137
+ size = $2.to_i
138
+ debug("Incoming response: #{hash} #{size}")
139
+ begin
140
+ debug("Reading response body")
141
+ response_body = connection.read(size)
142
+ debug("Finished reading response body")
143
+ rescue EOFError, TruncatedDataError
144
+ debug("Failed to read response body")
145
+ # TODO: what to do here?
146
+ return false
147
+ rescue Exception => e
148
+ warn("Unhandled exception in incoming response read: #{e.inspect}")
149
+ end
150
+ process_response(hash, response_body)
151
+ else
152
+ warn("Malformed incoming response: #{response.strip}")
153
+ # when the incoming response was malformed
154
+ # TODO: what to do here?
155
+ end
156
+ end
157
+
158
+
159
+ def process_request(hash, body)
160
+ Thread.new do
161
+ begin
162
+ debug("Starting processing thread (#{hash})")
163
+ response = @server.process(body)
164
+ debug("Processing finished (#{hash})")
165
+ @connection_pool.with_connection do |connection|
166
+ debug( "Sending response (#{hash})")
167
+ connection.write("RESPONSE #{hash} #{response.size}\n#{response}")
168
+ connection.flush
169
+ debug( "Finished sending response (#{hash})")
170
+ end
171
+ rescue Exception => e
172
+ warn("Unhandled exception in process request thread: #{e.inspect}")
173
+ end
174
+ end
175
+ end
176
+
177
+ def process_response(hash, body)
178
+ Thread.new do
179
+ begin
180
+ debug("Finding original request (#{hash})")
181
+ original_request = @pending_requests.synchronize { @pending_requests.delete(hash) }
182
+ if original_request
183
+ debug("Found original request, adding response")
184
+ original_request.add_response(body)
185
+ else
186
+ warn("Original response not found (#{hash})")
187
+ end
188
+ rescue Exception => e
189
+ warn("Unhandled exception in process response thread: #{e.inspect}")
190
+ end
191
+ end
192
+ end
193
+
194
+ def debug(msg)
195
+ logger.debug("Peer #{@host}:#{@port}") { msg }
196
+ end
197
+
198
+ def warn(msg)
199
+ logger.warn("Peer #{@host}:#{@port}") { msg }
200
+ end
201
+ end
202
+ end
203
+ end
@@ -0,0 +1,72 @@
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
+
11
+ def self.total_request_count
12
+ @@request_count.real_total
13
+ end
14
+
15
+ def self.total_late_responses
16
+ @@late_responses.real_total
17
+ end
18
+
19
+ def initialize(body)
20
+ @request_number = @@request_count.inc
21
+ @body = body
22
+ @responses = []
23
+ @responses_mutex = Mutex.new
24
+ @complete = false
25
+ end
26
+
27
+ def add_response(response)
28
+ @responses_mutex.synchronize do
29
+ if @responses.frozen? # too late
30
+ @@late_responses.inc
31
+ return
32
+ end
33
+ debug("Adding response")
34
+ @responses << response
35
+ if @max_responses && @responses.size >= @max_responses
36
+ debug("Hit max responses, waking up waiting thread")
37
+ @waiting_thread.wakeup if @waiting_thread && @waiting_thread.status == "sleep"
38
+ @complete = true
39
+ end
40
+ end
41
+ end
42
+
43
+ def responses
44
+ @responses # its frozen so we don't have to worry about mutex
45
+ end
46
+
47
+ def wait_for_responses
48
+ return if @complete
49
+ @waiting_thread = Thread.current
50
+ sleep Config.response_timeout
51
+ # there is a race case where if the sleep finishes,
52
+ # and response comes in and has the mutex, and then array is frozen
53
+ # it would be ideal to use monitor wait/signal here but the monitor implementation is currently flawed
54
+ @responses_mutex.synchronize { @responses.freeze }
55
+ @waiting_thread = nil
56
+ end
57
+
58
+ def hash
59
+ @hash ||= Digest::MD5.hexdigest("#{@request_number} #{@body}")[0,10]
60
+ end
61
+
62
+ private
63
+ def debug(msg)
64
+ logger.debug("Request #{hash}") {msg}
65
+ end
66
+
67
+ def info(msg)
68
+ logger.info("Request #{hash}") {msg}
69
+ end
70
+ end
71
+ end
72
+ end