arya-pandemic 0.2 → 0.2.1
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.
- data/MIT-LICENSE +22 -0
- data/Rakefile +5 -5
- data/lib/pandemic.rb +6 -4
- data/lib/pandemic/client_side/cluster_connection.rb +6 -1
- data/lib/pandemic/client_side/config.rb +2 -1
- data/lib/pandemic/connection_pool.rb +92 -16
- data/lib/pandemic/mutex_counter.rb +8 -3
- data/lib/pandemic/server_side/client.rb +18 -2
- data/lib/pandemic/server_side/peer.rb +12 -4
- data/lib/pandemic/server_side/request.rb +17 -2
- data/lib/pandemic/server_side/server.rb +1 -1
- data/lib/pandemic/util.rb +1 -12
- data/pandemic.gemspec +11 -4
- data/test/client_test.rb +80 -0
- data/test/connection_pool_test.rb +133 -0
- data/test/functional_test.rb +56 -0
- data/test/handler_test.rb +31 -0
- data/test/mutex_counter_test.rb +46 -0
- data/test/peer_test.rb +48 -0
- data/test/server_test.rb +111 -0
- data/test/test_helper.rb +24 -0
- data/test/util_test.rb +21 -0
- metadata +46 -9
data/MIT-LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2009 Arya Asemanfar
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person
|
4
|
+
obtaining a copy of this software and associated documentation
|
5
|
+
files (the "Software"), to deal in the Software without
|
6
|
+
restriction, including without limitation the rights to use,
|
7
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
copies of the Software, and to permit persons to whom the
|
9
|
+
Software is furnished to do so, subject to the following
|
10
|
+
conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be
|
13
|
+
included in all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
17
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
19
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
20
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
21
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
CHANGED
@@ -2,13 +2,13 @@ require 'rubygems'
|
|
2
2
|
require 'rake'
|
3
3
|
require 'echoe'
|
4
4
|
|
5
|
-
Echoe.new('pandemic', '0.2') do |p|
|
5
|
+
Echoe.new('pandemic', '0.2.1') do |p|
|
6
6
|
p.description = "Distribute MapReduce to any of the workers and it will spread, like a pandemic."
|
7
|
-
p.url = ""
|
7
|
+
p.url = "https://github.com/arya/pandemic/"
|
8
8
|
p.author = "Arya Asemanfar"
|
9
9
|
p.email = "aryaasemanfar@gmail.com"
|
10
|
-
p.ignore_pattern = ["tmp/*", "script/*", '
|
11
|
-
p.development_dependencies = []
|
10
|
+
p.ignore_pattern = ["tmp/*", "script/*", 'test/*']
|
11
|
+
p.development_dependencies = ["shoulda", "mocha"]
|
12
12
|
end
|
13
13
|
|
14
|
-
Dir["#{File.dirname(__FILE__)}/tasks/*.rake"].sort.each { |ext| load ext }
|
14
|
+
Dir["#{File.dirname(__FILE__)}/tasks/*.rake"].sort.each { |ext| load ext }
|
data/lib/pandemic.rb
CHANGED
@@ -25,15 +25,17 @@ require 'pandemic/client_side/connection_proxy'
|
|
25
25
|
require 'pandemic/client_side/pandemize'
|
26
26
|
|
27
27
|
# TODO:
|
28
|
+
# - tests
|
28
29
|
# - IO timeouts/robustness
|
29
30
|
# - documentation
|
30
31
|
# - PING/PONG?
|
31
32
|
|
32
|
-
$logger = Logger.new(STDOUT)
|
33
|
-
$logger.level = Logger::DEBUG
|
34
|
-
$logger.datetime_format = "%Y-%m-%d %H:%M:%S "
|
35
|
-
|
36
33
|
def epidemic!
|
34
|
+
if $pandemic_logger.nil?
|
35
|
+
$pandemic_logger = Logger.new("pandemic.log")
|
36
|
+
$pandemic_logger.level = Logger::INFO
|
37
|
+
$pandemic_logger.datetime_format = "%Y-%m-%d %H:%M:%S "
|
38
|
+
end
|
37
39
|
Pandemic::ServerSide::Server.boot
|
38
40
|
end
|
39
41
|
|
@@ -4,6 +4,7 @@ module Pandemic
|
|
4
4
|
class NotEnoughConnectionsTimeout < Exception; end
|
5
5
|
class NoNodesAvailable < Exception; end
|
6
6
|
class LostConnectionToNode < Exception; end
|
7
|
+
class NodeTimedOut < Exception; end
|
7
8
|
|
8
9
|
include Util
|
9
10
|
def initialize
|
@@ -16,6 +17,9 @@ module Pandemic
|
|
16
17
|
@connection_proxies = {}
|
17
18
|
@queue = @mutex.new_cond # TODO: there should be a queue for each group
|
18
19
|
|
20
|
+
@response_timeout = Config.response_timeout
|
21
|
+
@response_timeout = nil if @response_timeout <= 0
|
22
|
+
|
19
23
|
Config.servers.each_with_index do |server_addr, key|
|
20
24
|
@connection_proxies[key] = ConnectionProxy.new(key, self)
|
21
25
|
host, port = host_port(server_addr)
|
@@ -41,7 +45,8 @@ module Pandemic
|
|
41
45
|
begin
|
42
46
|
socket.write("#{body.size}\n#{body}")
|
43
47
|
socket.flush
|
44
|
-
|
48
|
+
is_ready = IO.select([socket], nil, nil, @response_timeout)
|
49
|
+
raise NodeTimedOut if is_ready.nil?
|
45
50
|
response_size = socket.gets
|
46
51
|
if response_size
|
47
52
|
socket.read(response_size.strip.to_i)
|
@@ -5,7 +5,7 @@ module Pandemic
|
|
5
5
|
@@load_mutex = Mutex.new
|
6
6
|
attr_accessor :config_path, :loaded
|
7
7
|
attr_accessor :servers, :max_connections_per_server, :min_connections_per_server,
|
8
|
-
:connection_wait_timeout
|
8
|
+
:connection_wait_timeout, :response_timeout
|
9
9
|
def load
|
10
10
|
@@load_mutex.synchronize do
|
11
11
|
return if self.loaded
|
@@ -20,6 +20,7 @@ module Pandemic
|
|
20
20
|
@max_connections_per_server = (yaml['max_connections_per_server'] || 1).to_i
|
21
21
|
@min_connections_per_server = (yaml['min_connections_per_server'] || 1).to_i
|
22
22
|
@connection_wait_timeout = (yaml['connection_wait_timeout'] || 1).to_f
|
23
|
+
@response_timeout = (yaml['response_timeout'] || 1).to_f
|
23
24
|
self.loaded = true
|
24
25
|
end
|
25
26
|
end
|
@@ -4,34 +4,39 @@ module Pandemic
|
|
4
4
|
class CreateConnectionUndefinedException < Exception; end
|
5
5
|
include Util
|
6
6
|
def initialize(options = {})
|
7
|
+
@connected = false
|
7
8
|
@mutex = Monitor.new
|
8
9
|
@queue = @mutex.new_cond
|
9
10
|
@available = []
|
10
11
|
@connections = []
|
11
12
|
@max_connections = options[:max_connections] || 10
|
13
|
+
@min_connections = options[:min_connections] || 1
|
12
14
|
@timeout = options[:timeout] || 3
|
13
15
|
end
|
14
16
|
|
15
17
|
def add_connection!
|
16
|
-
# bang because we're
|
17
|
-
|
18
|
-
|
19
|
-
@
|
20
|
-
@connections << conn
|
21
|
-
@available << conn
|
22
|
-
end
|
18
|
+
# bang because we're ignoring the max connections
|
19
|
+
@mutex.synchronize do
|
20
|
+
conn = create_connection
|
21
|
+
@available << conn if conn && !conn.closed?
|
23
22
|
end
|
24
23
|
end
|
25
24
|
|
26
25
|
def create_connection(&block)
|
27
26
|
if block.nil?
|
28
27
|
if @create_connection
|
29
|
-
@create_connection.call
|
28
|
+
conn = @create_connection.call
|
29
|
+
if conn && !conn.closed?
|
30
|
+
@connections << conn
|
31
|
+
@connected = true
|
32
|
+
conn
|
33
|
+
end
|
30
34
|
else
|
31
35
|
raise CreateConnectionUndefinedException.new("You must specify a block to create connections")
|
32
36
|
end
|
33
37
|
else
|
34
38
|
@create_connection = block
|
39
|
+
connect
|
35
40
|
end
|
36
41
|
end
|
37
42
|
|
@@ -45,30 +50,61 @@ module Pandemic
|
|
45
50
|
connection.close
|
46
51
|
end
|
47
52
|
end
|
53
|
+
@connections.delete(connection)
|
54
|
+
@available.delete(connection)
|
55
|
+
# this is within the mutex of the caller
|
56
|
+
@connected = false if @connections.empty?
|
48
57
|
else
|
49
58
|
@destroy_connection = block
|
50
59
|
end
|
51
60
|
end
|
52
61
|
|
62
|
+
def status_check(connection = nil, &block)
|
63
|
+
if block.nil?
|
64
|
+
if @status_check
|
65
|
+
@status_check.call(connection)
|
66
|
+
else
|
67
|
+
connection && !connection.closed?
|
68
|
+
end
|
69
|
+
else
|
70
|
+
@status_check = block
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
53
74
|
def connected?
|
54
|
-
@
|
75
|
+
@connected
|
76
|
+
end
|
77
|
+
|
78
|
+
def connect
|
79
|
+
if !connected?
|
80
|
+
@min_connections.times { add_connection! }
|
81
|
+
grim_reaper
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def available_count
|
86
|
+
@available.size
|
87
|
+
end
|
88
|
+
|
89
|
+
def connections_count
|
90
|
+
@connections.size
|
55
91
|
end
|
56
92
|
|
57
93
|
def disconnect
|
58
94
|
@mutex.synchronize do
|
59
95
|
return if @disconnecting
|
60
96
|
@disconnecting = true
|
61
|
-
@
|
97
|
+
@connected = false # we don't want anyone thinking they can use this connection
|
98
|
+
@grim_reaper.kill if @grim_reaper && @grim_reaper.alive?
|
99
|
+
|
100
|
+
@available.dup.each do |conn|
|
62
101
|
destroy_connection(conn)
|
63
|
-
@connections.delete(conn)
|
64
102
|
end
|
65
|
-
|
103
|
+
|
66
104
|
while @connections.size > 0 && @queue.wait
|
67
|
-
@available.each do |conn|
|
105
|
+
@available.dup.each do |conn|
|
68
106
|
destroy_connection(conn)
|
69
|
-
@connections.delete(conn)
|
70
107
|
end
|
71
|
-
@available = []
|
72
108
|
end
|
73
109
|
@disconnecting = false
|
74
110
|
end
|
@@ -94,7 +130,6 @@ module Pandemic
|
|
94
130
|
connection = @available.pop
|
95
131
|
break
|
96
132
|
elsif @connections.size < @max_connections && (connection = create_connection)
|
97
|
-
@connections << connection
|
98
133
|
break
|
99
134
|
elsif @queue.wait(@timeout)
|
100
135
|
next
|
@@ -113,5 +148,46 @@ module Pandemic
|
|
113
148
|
end
|
114
149
|
end
|
115
150
|
|
151
|
+
def grim_reaper
|
152
|
+
@grim_reaper.kill if @grim_reaper && @grim_reaper.alive?
|
153
|
+
@grim_reaper = Thread.new do
|
154
|
+
usage_history = []
|
155
|
+
loop do
|
156
|
+
if connected?
|
157
|
+
@mutex.synchronize do
|
158
|
+
dead = []
|
159
|
+
|
160
|
+
@connections.each do |conn|
|
161
|
+
dead << conn if !status_check(conn)
|
162
|
+
end
|
163
|
+
|
164
|
+
dead.each { |c| destroy_connection(c) }
|
165
|
+
|
166
|
+
# restore to minimum number of connections if it's too low
|
167
|
+
[@min_connections - @connections.size, 0].max.times do
|
168
|
+
add_connection!
|
169
|
+
end
|
170
|
+
|
171
|
+
usage_history.push(@available.size)
|
172
|
+
if usage_history.size >= 10
|
173
|
+
# kill the minimum number of available connections over the last 10 checks
|
174
|
+
# or the total connections minux the min connections, whichever is lower.
|
175
|
+
# this ensures that you never go below min connections
|
176
|
+
to_kill = [usage_history.min, @connections.size - @min_connections].min
|
177
|
+
[to_kill, 0].max.times do
|
178
|
+
destroy_connection(@connections.last)
|
179
|
+
end
|
180
|
+
usage_history = []
|
181
|
+
end
|
182
|
+
|
183
|
+
end # end mutex
|
184
|
+
sleep 30
|
185
|
+
else
|
186
|
+
break
|
187
|
+
end # end if connected
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
116
192
|
end
|
117
193
|
end
|
@@ -1,19 +1,24 @@
|
|
1
1
|
module Pandemic
|
2
2
|
class MutexCounter
|
3
3
|
MAX = (2 ** 30) - 1
|
4
|
-
def initialize
|
4
|
+
def initialize(max = MAX)
|
5
5
|
@mutex = Mutex.new
|
6
6
|
@counter = 0
|
7
7
|
@resets = 0
|
8
|
+
@max = max
|
8
9
|
end
|
9
10
|
|
10
11
|
def real_total
|
11
|
-
@mutex.synchronize { (@resets *
|
12
|
+
@mutex.synchronize { (@resets * @max) + @counter }
|
13
|
+
end
|
14
|
+
|
15
|
+
def value
|
16
|
+
@mutex.synchronize { @counter }
|
12
17
|
end
|
13
18
|
|
14
19
|
def inc
|
15
20
|
@mutex.synchronize do
|
16
|
-
if @counter >=
|
21
|
+
if @counter >= @max
|
17
22
|
@counter = 0 # to avoid Bignum, it's about 4x slower
|
18
23
|
@resets += 1
|
19
24
|
end
|
@@ -11,6 +11,7 @@ module Pandemic
|
|
11
11
|
@server = server
|
12
12
|
@received_requests = 0
|
13
13
|
@responded_requests = 0
|
14
|
+
@current_request = nil
|
14
15
|
end
|
15
16
|
|
16
17
|
def listen
|
@@ -38,6 +39,8 @@ module Pandemic
|
|
38
39
|
response = handle_request(body)
|
39
40
|
|
40
41
|
debug("Writing response to client")
|
42
|
+
|
43
|
+
# the connection could be closed, we'll let it be rescued if it is.
|
41
44
|
@connection.write("#{response.size}\n#{response}")
|
42
45
|
@connection.flush
|
43
46
|
@responded_requests += 1
|
@@ -46,10 +49,14 @@ module Pandemic
|
|
46
49
|
end
|
47
50
|
rescue DisconnectClient
|
48
51
|
info("Closing client connection")
|
49
|
-
|
52
|
+
close_connection
|
53
|
+
rescue Errno::EPIPE
|
54
|
+
info("Connection to client lost")
|
55
|
+
close_connection
|
50
56
|
rescue Exception => e
|
51
57
|
warn("Unhandled exception in client listen thread: #{e.inspect}")
|
52
58
|
ensure
|
59
|
+
@current_request.cancel! if @current_request
|
53
60
|
@server.client_closed(self)
|
54
61
|
end
|
55
62
|
end
|
@@ -60,12 +67,21 @@ module Pandemic
|
|
60
67
|
def close
|
61
68
|
@listener_thread.raise(DisconnectClient)
|
62
69
|
end
|
70
|
+
|
71
|
+
|
63
72
|
|
64
73
|
def handle_request(request)
|
65
|
-
@
|
74
|
+
@current_request = Request.new(request)
|
75
|
+
response = @server.handle_client_request(@current_request)
|
76
|
+
@current_request = nil
|
77
|
+
return response
|
66
78
|
end
|
67
79
|
|
68
80
|
private
|
81
|
+
def close_connection
|
82
|
+
@connection.close unless @connection.nil? || @connection.closed?
|
83
|
+
end
|
84
|
+
|
69
85
|
def signature
|
70
86
|
@signature ||= @connection.peeraddr.values_at(3,1).join(":")
|
71
87
|
end
|
@@ -15,9 +15,8 @@ module Pandemic
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def connect
|
18
|
-
return if connected?
|
19
18
|
debug("Forced connection to peer")
|
20
|
-
|
19
|
+
@connection_pool.connect
|
21
20
|
end
|
22
21
|
|
23
22
|
def disconnect
|
@@ -31,6 +30,7 @@ module Pandemic
|
|
31
30
|
|
32
31
|
def client_request(request, body)
|
33
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
34
|
# TODO: Consider adding back threads here if it will be faster that way in Ruby 1.9
|
35
35
|
@connection_pool.with_connection do |connection|
|
36
36
|
if connection && !connection.closed?
|
@@ -49,6 +49,7 @@ module Pandemic
|
|
49
49
|
debug("Adding incoming connection")
|
50
50
|
|
51
51
|
connect # if we're not connected, we should be
|
52
|
+
|
52
53
|
|
53
54
|
thread = Thread.new(conn) do |connection|
|
54
55
|
begin
|
@@ -89,10 +90,18 @@ module Pandemic
|
|
89
90
|
|
90
91
|
@connection_pool.create_connection do
|
91
92
|
connection = nil
|
93
|
+
retries = 0
|
92
94
|
begin
|
93
95
|
connection = TCPSocket.new(@host, @port)
|
94
|
-
rescue Errno::ETIMEDOUT, Errno::ECONNREFUSED
|
96
|
+
rescue Errno::ETIMEDOUT, Errno::ECONNREFUSED => e
|
95
97
|
connection = nil
|
98
|
+
debug("Connection timeout or refused: #{e.inspect}")
|
99
|
+
if retries == 0
|
100
|
+
debug("Retrying connection")
|
101
|
+
retries += 1
|
102
|
+
sleep 0.01
|
103
|
+
retry
|
104
|
+
end
|
96
105
|
rescue Exception => e
|
97
106
|
warn("Unhandled exception in create connection block: #{e.inspect}")
|
98
107
|
end
|
@@ -102,7 +111,6 @@ module Pandemic
|
|
102
111
|
end
|
103
112
|
connection
|
104
113
|
end
|
105
|
-
|
106
114
|
end
|
107
115
|
|
108
116
|
def handle_incoming_request(request, connection)
|
@@ -34,20 +34,35 @@ module Pandemic
|
|
34
34
|
@responses << response
|
35
35
|
if @max_responses && @responses.size >= @max_responses
|
36
36
|
debug("Hit max responses, waking up waiting thread")
|
37
|
-
|
37
|
+
wakeup_waiting_thread
|
38
38
|
@complete = true
|
39
39
|
end
|
40
40
|
end
|
41
41
|
end
|
42
|
+
|
43
|
+
def wakeup_waiting_thread
|
44
|
+
@waiting_thread.wakeup if @waiting_thread && @waiting_thread.status == "sleep"
|
45
|
+
end
|
42
46
|
|
43
47
|
def responses
|
44
48
|
@responses # its frozen so we don't have to worry about mutex
|
45
49
|
end
|
50
|
+
|
51
|
+
def cancel!
|
52
|
+
# consider telling peers that they should stop, but for now this is fine.
|
53
|
+
@responses_mutex.synchronize do
|
54
|
+
wakeup_waiting_thread
|
55
|
+
end
|
56
|
+
end
|
46
57
|
|
47
58
|
def wait_for_responses
|
48
59
|
return if @complete
|
49
60
|
@waiting_thread = Thread.current
|
50
|
-
|
61
|
+
if Config.response_timeout <= 0
|
62
|
+
Thread.stop
|
63
|
+
else
|
64
|
+
sleep Config.response_timeout
|
65
|
+
end
|
51
66
|
# there is a race case where if the sleep finishes,
|
52
67
|
# and response comes in and has the mutex, and then array is frozen
|
53
68
|
# it would be ideal to use monitor wait/signal here but the monitor implementation is currently flawed
|
data/lib/pandemic/util.rb
CHANGED
@@ -5,18 +5,7 @@ module Pandemic
|
|
5
5
|
end
|
6
6
|
|
7
7
|
def logger
|
8
|
-
$
|
9
|
-
end
|
10
|
-
|
11
|
-
def bm(title, &block)
|
12
|
-
@times ||= Hash.new(0)
|
13
|
-
begin
|
14
|
-
start = Time.now.to_f
|
15
|
-
yield block
|
16
|
-
ensure
|
17
|
-
@times[title] += (Time.now.to_f - start)
|
18
|
-
$stdout.puts("#{title} #{@times[title]}")
|
19
|
-
end
|
8
|
+
$pandemic_logger
|
20
9
|
end
|
21
10
|
|
22
11
|
def with_mutex(obj)
|
data/pandemic.gemspec
CHANGED
@@ -2,30 +2,37 @@
|
|
2
2
|
|
3
3
|
Gem::Specification.new do |s|
|
4
4
|
s.name = %q{pandemic}
|
5
|
-
s.version = "0.2"
|
5
|
+
s.version = "0.2.1"
|
6
6
|
|
7
7
|
s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
|
8
8
|
s.authors = ["Arya Asemanfar"]
|
9
|
-
s.date = %q{2009-03-
|
9
|
+
s.date = %q{2009-03-26}
|
10
10
|
s.description = %q{Distribute MapReduce to any of the workers and it will spread, like a pandemic.}
|
11
11
|
s.email = %q{aryaasemanfar@gmail.com}
|
12
12
|
s.extra_rdoc_files = ["lib/pandemic/client_side/cluster_connection.rb", "lib/pandemic/client_side/config.rb", "lib/pandemic/client_side/connection.rb", "lib/pandemic/client_side/connection_proxy.rb", "lib/pandemic/client_side/pandemize.rb", "lib/pandemic/connection_pool.rb", "lib/pandemic/mutex_counter.rb", "lib/pandemic/server_side/client.rb", "lib/pandemic/server_side/config.rb", "lib/pandemic/server_side/handler.rb", "lib/pandemic/server_side/peer.rb", "lib/pandemic/server_side/request.rb", "lib/pandemic/server_side/server.rb", "lib/pandemic/util.rb", "lib/pandemic.rb", "README.markdown"]
|
13
|
-
s.files = ["lib/pandemic/client_side/cluster_connection.rb", "lib/pandemic/client_side/config.rb", "lib/pandemic/client_side/connection.rb", "lib/pandemic/client_side/connection_proxy.rb", "lib/pandemic/client_side/pandemize.rb", "lib/pandemic/connection_pool.rb", "lib/pandemic/mutex_counter.rb", "lib/pandemic/server_side/client.rb", "lib/pandemic/server_side/config.rb", "lib/pandemic/server_side/handler.rb", "lib/pandemic/server_side/peer.rb", "lib/pandemic/server_side/request.rb", "lib/pandemic/server_side/server.rb", "lib/pandemic/util.rb", "lib/pandemic.rb", "Rakefile", "README.markdown", "
|
13
|
+
s.files = ["lib/pandemic/client_side/cluster_connection.rb", "lib/pandemic/client_side/config.rb", "lib/pandemic/client_side/connection.rb", "lib/pandemic/client_side/connection_proxy.rb", "lib/pandemic/client_side/pandemize.rb", "lib/pandemic/connection_pool.rb", "lib/pandemic/mutex_counter.rb", "lib/pandemic/server_side/client.rb", "lib/pandemic/server_side/config.rb", "lib/pandemic/server_side/handler.rb", "lib/pandemic/server_side/peer.rb", "lib/pandemic/server_side/request.rb", "lib/pandemic/server_side/server.rb", "lib/pandemic/util.rb", "lib/pandemic.rb", "Manifest", "MIT-LICENSE", "pandemic.gemspec", "Rakefile", "README.markdown", "test/client_test.rb", "test/connection_pool_test.rb", "test/functional_test.rb", "test/handler_test.rb", "test/mutex_counter_test.rb", "test/peer_test.rb", "test/server_test.rb", "test/test_helper.rb", "test/util_test.rb"]
|
14
14
|
s.has_rdoc = true
|
15
|
-
s.homepage = %q{}
|
15
|
+
s.homepage = %q{https://github.com/arya/pandemic/}
|
16
16
|
s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Pandemic", "--main", "README.markdown"]
|
17
17
|
s.require_paths = ["lib"]
|
18
18
|
s.rubyforge_project = %q{pandemic}
|
19
19
|
s.rubygems_version = %q{1.3.1}
|
20
20
|
s.summary = %q{Distribute MapReduce to any of the workers and it will spread, like a pandemic.}
|
21
|
+
s.test_files = ["test/client_test.rb", "test/connection_pool_test.rb", "test/functional_test.rb", "test/handler_test.rb", "test/mutex_counter_test.rb", "test/peer_test.rb", "test/server_test.rb", "test/test_helper.rb", "test/util_test.rb"]
|
21
22
|
|
22
23
|
if s.respond_to? :specification_version then
|
23
24
|
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
24
25
|
s.specification_version = 2
|
25
26
|
|
26
27
|
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
28
|
+
s.add_development_dependency(%q<shoulda>, [">= 0"])
|
29
|
+
s.add_development_dependency(%q<mocha>, [">= 0"])
|
27
30
|
else
|
31
|
+
s.add_dependency(%q<shoulda>, [">= 0"])
|
32
|
+
s.add_dependency(%q<mocha>, [">= 0"])
|
28
33
|
end
|
29
34
|
else
|
35
|
+
s.add_dependency(%q<shoulda>, [">= 0"])
|
36
|
+
s.add_dependency(%q<mocha>, [">= 0"])
|
30
37
|
end
|
31
38
|
end
|
data/test/client_test.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class ClientTest < Test::Unit::TestCase
|
4
|
+
include TestHelper
|
5
|
+
|
6
|
+
context "with a client object" do
|
7
|
+
setup do
|
8
|
+
@server = mock()
|
9
|
+
@server.expects(:running).returns(true).then.returns(false)
|
10
|
+
@connection = mock()
|
11
|
+
@connection.expects(:peeraddr).returns(['','','',''])
|
12
|
+
@connection.expects(:nil?).returns(false).at_least_once
|
13
|
+
@client = Pandemic::ServerSide::Client.new(@connection, @server)
|
14
|
+
end
|
15
|
+
|
16
|
+
should "read size from the connection" do
|
17
|
+
@connection.expects(:gets).returns("5\n")
|
18
|
+
@server.expects(:client_closed).with(@client)
|
19
|
+
@client.listen
|
20
|
+
wait_for_threads
|
21
|
+
end
|
22
|
+
|
23
|
+
should "read body from the connection" do
|
24
|
+
@connection.expects(:gets).returns("5\n")
|
25
|
+
@connection.expects(:read).with(5).returns("hello")
|
26
|
+
@server.expects(:client_closed).with(@client)
|
27
|
+
@client.listen
|
28
|
+
wait_for_threads
|
29
|
+
end
|
30
|
+
|
31
|
+
should "call handle request body on server" do
|
32
|
+
@connection.expects(:gets).returns("5\n")
|
33
|
+
@connection.expects(:read).with(5).returns("hello")
|
34
|
+
|
35
|
+
request = mock()
|
36
|
+
Pandemic::ServerSide::Request.expects(:new).returns(request)
|
37
|
+
@server.expects(:handle_client_request).with(request)
|
38
|
+
|
39
|
+
@server.expects(:client_closed).with(@client)
|
40
|
+
@client.listen
|
41
|
+
wait_for_threads
|
42
|
+
end
|
43
|
+
|
44
|
+
should "write response back to client" do
|
45
|
+
@connection.expects(:gets).returns("5\n")
|
46
|
+
@connection.expects(:read).with(5).returns("hello")
|
47
|
+
|
48
|
+
request = mock()
|
49
|
+
response = "olleh"
|
50
|
+
Pandemic::ServerSide::Request.expects(:new).returns(request)
|
51
|
+
@server.expects(:handle_client_request).with(request).returns(response)
|
52
|
+
|
53
|
+
@connection.expects(:write).with("5\n#{response}")
|
54
|
+
@connection.expects(:flush)
|
55
|
+
|
56
|
+
@server.expects(:client_closed).with(@client)
|
57
|
+
@client.listen
|
58
|
+
wait_for_threads
|
59
|
+
end
|
60
|
+
|
61
|
+
should "close the connection on nil response" do
|
62
|
+
@connection.expects(:gets).returns(nil)
|
63
|
+
@connection.expects(:close)
|
64
|
+
|
65
|
+
@server.expects(:client_closed).with(@client)
|
66
|
+
@client.listen
|
67
|
+
wait_for_threads
|
68
|
+
end
|
69
|
+
|
70
|
+
should "close the connection on disconnect" do
|
71
|
+
@connection.expects(:gets).raises(Pandemic::ServerSide::Client::DisconnectClient)
|
72
|
+
@connection.expects(:closed?).returns(false)
|
73
|
+
@connection.expects(:close)
|
74
|
+
|
75
|
+
@server.expects(:client_closed).with(@client)
|
76
|
+
@client.listen
|
77
|
+
wait_for_threads
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class ConnectionPoolTest < Test::Unit::TestCase
|
4
|
+
include TestHelper
|
5
|
+
context "without a create connection block" do
|
6
|
+
setup do
|
7
|
+
@connection_pool = Pandemic::ConnectionPool.new
|
8
|
+
end
|
9
|
+
|
10
|
+
should "raise an exception when trying to connect" do
|
11
|
+
assert_raises Pandemic::ConnectionPool::CreateConnectionUndefinedException do
|
12
|
+
@connection_pool.connect
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
context "with a connection pool" do
|
18
|
+
setup do
|
19
|
+
@connection_pool = Pandemic::ConnectionPool.new
|
20
|
+
@connection_creator = mock()
|
21
|
+
@connection_destroyer = mock()
|
22
|
+
end
|
23
|
+
|
24
|
+
should "call create connection after its defined" do
|
25
|
+
@connection_creator.expects(:create).once
|
26
|
+
@connection_pool.create_connection do
|
27
|
+
@connection_creator.create
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
should "call destroyer on disconnect" do
|
32
|
+
@connection_creator.expects(:create).once
|
33
|
+
@connection_destroyer.expects(:destroy).once
|
34
|
+
@connection_pool.create_connection do
|
35
|
+
@connection_creator.create
|
36
|
+
conn = mock()
|
37
|
+
conn.expects(:closed?).returns(false).at_least(0)
|
38
|
+
conn
|
39
|
+
end
|
40
|
+
|
41
|
+
@connection_pool.destroy_connection do
|
42
|
+
@connection_destroyer.destroy
|
43
|
+
end
|
44
|
+
|
45
|
+
@connection_pool.disconnect
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
context "with a max connections of 2" do
|
51
|
+
setup do
|
52
|
+
@connection_pool = Pandemic::ConnectionPool.new(:max_connections => 2, :timeout => 0.01)
|
53
|
+
@connection_creator = mock()
|
54
|
+
end
|
55
|
+
|
56
|
+
should "raise timeout exception when no connections available" do
|
57
|
+
@connection_creator.expects(:create).twice
|
58
|
+
@connection_pool.create_connection do
|
59
|
+
@connection_creator.create
|
60
|
+
conn = mock()
|
61
|
+
conn.expects(:closed?).returns(false).at_least(0)
|
62
|
+
conn
|
63
|
+
end
|
64
|
+
|
65
|
+
assert_raises Pandemic::ConnectionPool::TimedOutWaitingForConnectionException do
|
66
|
+
@connection_pool.with_connection do |conn1|
|
67
|
+
@connection_pool.with_connection do |conn2|
|
68
|
+
@connection_pool.with_connection do |conn3|
|
69
|
+
fail("there should only be two connections")
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
should "should checkin a connection and allow someone check the same one out" do
|
77
|
+
@connection_creator.expects(:create).twice
|
78
|
+
@connection_pool.create_connection do
|
79
|
+
@connection_creator.create
|
80
|
+
conn = mock()
|
81
|
+
conn.expects(:closed?).returns(false).at_least(0)
|
82
|
+
conn
|
83
|
+
end
|
84
|
+
|
85
|
+
@connection_pool.with_connection do |conn1|
|
86
|
+
conn2, conn3 = nil, nil
|
87
|
+
|
88
|
+
@connection_pool.with_connection do |conn|
|
89
|
+
conn2 = conn
|
90
|
+
end
|
91
|
+
@connection_pool.with_connection do |conn|
|
92
|
+
conn3 = conn
|
93
|
+
end
|
94
|
+
|
95
|
+
assert_equal conn2, conn3
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
should "should checkin connection even if there is an exception" do
|
100
|
+
@connection_creator.expects(:create).once
|
101
|
+
@connection_pool.create_connection do
|
102
|
+
@connection_creator.create
|
103
|
+
conn = mock()
|
104
|
+
conn.expects(:closed?).returns(false).at_least(0)
|
105
|
+
conn
|
106
|
+
end
|
107
|
+
before = @connection_pool.available_count
|
108
|
+
begin
|
109
|
+
@connection_pool.with_connection do |conn1|
|
110
|
+
raise TestException
|
111
|
+
end
|
112
|
+
rescue TestException
|
113
|
+
end
|
114
|
+
after = @connection_pool.available_count
|
115
|
+
|
116
|
+
assert_equal before, after
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
context "with a min connections of 2" do
|
121
|
+
setup do
|
122
|
+
@connection_pool = Pandemic::ConnectionPool.new(:min_connections => 2)
|
123
|
+
@connection_creator = mock()
|
124
|
+
end
|
125
|
+
|
126
|
+
should "call create connection twice" do
|
127
|
+
@connection_creator.expects(:create).twice
|
128
|
+
@connection_pool.create_connection do
|
129
|
+
@connection_creator.create
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class FunctionalTest < Test::Unit::TestCase
|
4
|
+
include TestHelper
|
5
|
+
should "work" do
|
6
|
+
ignore_threads = Thread.list
|
7
|
+
ARGV.replace(["-i", "0", "-c", "test/pandemic_server.yml"]) # :(
|
8
|
+
Pandemic::ClientSide::Config.config_path = "test/pandemic_client.yml"
|
9
|
+
|
10
|
+
server = epidemic!
|
11
|
+
server.handler = Class.new(Pandemic::ServerSide::Handler) do
|
12
|
+
def process(body)
|
13
|
+
body.reverse
|
14
|
+
end
|
15
|
+
end.new
|
16
|
+
server.start
|
17
|
+
|
18
|
+
client = Class.new do
|
19
|
+
include Pandemize
|
20
|
+
end.new
|
21
|
+
client.extend(Pandemize)
|
22
|
+
assert_equal "dlrow olleh", client.pandemic.request("hello world")
|
23
|
+
server.stop
|
24
|
+
wait_for_threads(ignore_threads)
|
25
|
+
end
|
26
|
+
|
27
|
+
should "work with multiple peers" do
|
28
|
+
ignore_threads = Thread.list
|
29
|
+
handler = Class.new(Pandemic::ServerSide::Handler) do
|
30
|
+
def process(body)
|
31
|
+
body.reverse
|
32
|
+
end
|
33
|
+
end.new
|
34
|
+
|
35
|
+
ARGV.replace(["-i", "0", "-c", "test/pandemic_server.yml"]) # :(
|
36
|
+
server = epidemic!
|
37
|
+
server.handler = handler
|
38
|
+
server.start
|
39
|
+
|
40
|
+
ARGV.replace(["-i", "1", "-c", "test/pandemic_server.yml"]) # :(
|
41
|
+
server2 = epidemic!
|
42
|
+
server2.handler = handler
|
43
|
+
server2.start
|
44
|
+
|
45
|
+
Pandemic::ClientSide::Config.config_path = "test/pandemic_client.yml"
|
46
|
+
|
47
|
+
client = Class.new do
|
48
|
+
include Pandemize
|
49
|
+
end.new
|
50
|
+
client.extend(Pandemize)
|
51
|
+
assert_equal "raboofraboof", client.pandemic.request("foobar")
|
52
|
+
server.stop
|
53
|
+
server2.stop
|
54
|
+
wait_for_threads(ignore_threads)
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class HandlerTest < Test::Unit::TestCase
|
4
|
+
include TestHelper
|
5
|
+
|
6
|
+
context "with a request object" do
|
7
|
+
setup do
|
8
|
+
@request = mock()
|
9
|
+
@servers = {
|
10
|
+
1 => :self,
|
11
|
+
2 => :disconnected,
|
12
|
+
3 => :connected
|
13
|
+
}
|
14
|
+
@handler = Pandemic::ServerSide::Handler.new
|
15
|
+
end
|
16
|
+
|
17
|
+
should "concatenate all the results" do
|
18
|
+
@request.expects(:responses).once.returns(%w{1 2 3})
|
19
|
+
assert_equal "123", @handler.reduce(@request)
|
20
|
+
end
|
21
|
+
|
22
|
+
should "map to all non-disconnected nodes" do
|
23
|
+
@request.expects(:body).twice.returns("123")
|
24
|
+
map = @handler.map(@request, @servers)
|
25
|
+
# see setup for @servers
|
26
|
+
assert_equal 2, map.size
|
27
|
+
assert_equal "123", map[1]
|
28
|
+
assert_equal "123", map[3]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class MutexCounterTest < Test::Unit::TestCase
|
4
|
+
context "with a new counter" do
|
5
|
+
setup do
|
6
|
+
@counter = Pandemic::MutexCounter.new
|
7
|
+
end
|
8
|
+
|
9
|
+
should "start at 0" do
|
10
|
+
assert_equal 0, @counter.value
|
11
|
+
end
|
12
|
+
|
13
|
+
should "increment to 1 after one call to inc" do
|
14
|
+
assert_equal 0, @counter.value
|
15
|
+
assert_equal 1, @counter.inc
|
16
|
+
assert_equal 1, @counter.value
|
17
|
+
end
|
18
|
+
|
19
|
+
should "be thread safe" do
|
20
|
+
# Not exactly a perfect test, but I'm not sure how to actually test
|
21
|
+
# this without putting some code in the counter for this reason.
|
22
|
+
threads = []
|
23
|
+
5.times { threads << Thread.new { 100.times { @counter.inc }}}
|
24
|
+
threads.each {|t| t.join } # make sure they're all done
|
25
|
+
assert_equal 500, @counter.value
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
context "with a max of 10" do
|
30
|
+
setup do
|
31
|
+
@counter = Pandemic::MutexCounter.new(10)
|
32
|
+
end
|
33
|
+
|
34
|
+
should "cycle from 1 to 10" do
|
35
|
+
expected = (1..10).to_a + [1]
|
36
|
+
actual = (1..11).collect { @counter.inc }
|
37
|
+
assert_equal expected, actual
|
38
|
+
end
|
39
|
+
|
40
|
+
should "have the correct 'real_total'" do
|
41
|
+
11.times { @counter.inc }
|
42
|
+
assert_equal 1, @counter.value
|
43
|
+
assert_equal 11, @counter.real_total
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/test/peer_test.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class PeerTest < Test::Unit::TestCase
|
4
|
+
include TestHelper
|
5
|
+
|
6
|
+
should "initialize a new connection pool" do
|
7
|
+
connection_pool = mock()
|
8
|
+
Pandemic::ConnectionPool.expects(:new).returns(connection_pool)
|
9
|
+
connection_pool.expects(:create_connection)
|
10
|
+
|
11
|
+
server = mock()
|
12
|
+
peer = Pandemic::ServerSide::Peer.new("localhost:4000", server)
|
13
|
+
end
|
14
|
+
|
15
|
+
should "create a tcp socket" do
|
16
|
+
connection_pool = mock()
|
17
|
+
Pandemic::ConnectionPool.expects(:new).returns(connection_pool)
|
18
|
+
connection_pool.expects(:create_connection).yields
|
19
|
+
TCPSocket.expects(:new).with("localhost", 4000)
|
20
|
+
|
21
|
+
server = mock()
|
22
|
+
peer = Pandemic::ServerSide::Peer.new("localhost:4000", server)
|
23
|
+
end
|
24
|
+
|
25
|
+
context "with conn. pool" do
|
26
|
+
setup do
|
27
|
+
@connection_pool = mock()
|
28
|
+
Pandemic::ConnectionPool.expects(:new).returns(@connection_pool)
|
29
|
+
@connection_pool.expects(:create_connection)
|
30
|
+
|
31
|
+
@server = mock()
|
32
|
+
@peer = Pandemic::ServerSide::Peer.new("localhost:4000", @server)
|
33
|
+
end
|
34
|
+
|
35
|
+
should "send client request to peer connection" do
|
36
|
+
request, body = stub(:hash => "asdasdfadsf"), "hello world"
|
37
|
+
@connection_pool.expects(:available_count => 1, :connections_count => 1)
|
38
|
+
conn = mock()
|
39
|
+
@connection_pool.expects(:with_connection).yields(conn)
|
40
|
+
|
41
|
+
conn.stubs(:closed? => false)
|
42
|
+
conn.expects(:write).with("PROCESS #{request.hash} #{body.size}\n#{body}")
|
43
|
+
conn.expects(:flush)
|
44
|
+
|
45
|
+
@peer.client_request(request, body)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/test/server_test.rb
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class ServerTest < Test::Unit::TestCase
|
4
|
+
include TestHelper
|
5
|
+
|
6
|
+
should "initialize peers" do
|
7
|
+
Pandemic::ServerSide::Config.expects(:bind_to).at_least_once.returns("localhost:4000")
|
8
|
+
Pandemic::ServerSide::Config.expects(:servers).returns(["localhost:4000", "localhost:4001"])
|
9
|
+
Pandemic::ServerSide::Peer.expects(:new).with("localhost:4001", is_a(Pandemic::ServerSide::Server))
|
10
|
+
@server = Pandemic::ServerSide::Server.new
|
11
|
+
end
|
12
|
+
|
13
|
+
context "with a server" do
|
14
|
+
setup do
|
15
|
+
Pandemic::ServerSide::Config.expects(:bind_to).at_least_once.returns("localhost:4000")
|
16
|
+
Pandemic::ServerSide::Config.expects(:servers).returns(["localhost:4000", "localhost:4001"])
|
17
|
+
@peer = mock()
|
18
|
+
Pandemic::ServerSide::Peer.expects(:new).with("localhost:4001", is_a(Pandemic::ServerSide::Server)).returns(@peer)
|
19
|
+
@server = Pandemic::ServerSide::Server.new
|
20
|
+
end
|
21
|
+
|
22
|
+
should "start a TCPServer, and connect to peers" do
|
23
|
+
ignore_threads = Thread.list
|
24
|
+
@tcpserver = mock()
|
25
|
+
TCPServer.expects(:new).with("localhost", 4000).returns(@tcpserver)
|
26
|
+
@peer.expects(:connect).once
|
27
|
+
@tcpserver.expects(:accept).twice.returns(nil).then.raises(Pandemic::ServerSide::Server::StopServer)
|
28
|
+
@tcpserver.expects(:close)
|
29
|
+
@peer.expects(:disconnect)
|
30
|
+
@server.handler = mock()
|
31
|
+
@server.start
|
32
|
+
wait_for_threads(ignore_threads)
|
33
|
+
end
|
34
|
+
|
35
|
+
should "create a new client object for a client connection" do
|
36
|
+
ignore_threads = Thread.list
|
37
|
+
|
38
|
+
@tcpserver = mock()
|
39
|
+
TCPServer.expects(:new).with("localhost", 4000).returns(@tcpserver)
|
40
|
+
@peer.expects(:connect).once
|
41
|
+
|
42
|
+
@conn = mock(:peeraddr => ['','','',''])
|
43
|
+
@tcpserver.expects(:accept).twice.returns(@conn).then.raises(Pandemic::ServerSide::Server::StopServer)
|
44
|
+
client = mock()
|
45
|
+
@conn.expects(:setsockopt).with(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
46
|
+
|
47
|
+
Pandemic::ServerSide::Client.expects(:new).with(@conn, @server).returns(client)
|
48
|
+
|
49
|
+
|
50
|
+
@conn.expects(:gets).returns("CLIENT\n")
|
51
|
+
@tcpserver.expects(:close)
|
52
|
+
@peer.expects(:disconnect)
|
53
|
+
client.expects(:listen).returns(client)
|
54
|
+
client.expects(:close).at_most_once # optional due to threaded nature, this may not actually happen
|
55
|
+
@server.handler = mock()
|
56
|
+
@server.start
|
57
|
+
wait_for_threads(ignore_threads)
|
58
|
+
end
|
59
|
+
|
60
|
+
should "connect with the corresponding peer object" do
|
61
|
+
ignore_threads = Thread.list
|
62
|
+
@tcpserver = mock()
|
63
|
+
TCPServer.expects(:new).with("localhost", 4000).returns(@tcpserver)
|
64
|
+
@peer.expects(:connect).once
|
65
|
+
|
66
|
+
@conn = mock(:peeraddr => ['','','',''])
|
67
|
+
@tcpserver.expects(:accept).twice.returns(@conn).then.raises(Pandemic::ServerSide::Server::StopServer)
|
68
|
+
@tcpserver.expects(:close)
|
69
|
+
@peer.expects(:disconnect)
|
70
|
+
|
71
|
+
@conn.expects(:setsockopt).with(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
72
|
+
@conn.expects(:gets).returns("SERVER localhost:4001\n")
|
73
|
+
|
74
|
+
@peer.expects(:host).returns("localhost")
|
75
|
+
@peer.expects(:port).returns(4001)
|
76
|
+
@peer.expects(:add_incoming_connection).with(@conn)
|
77
|
+
@server.handler = mock()
|
78
|
+
|
79
|
+
@server.start
|
80
|
+
wait_for_threads(ignore_threads)
|
81
|
+
end
|
82
|
+
|
83
|
+
should "call process on handler" do
|
84
|
+
handler = mock()
|
85
|
+
handler.expects(:process).with("body")
|
86
|
+
@server.handler = handler
|
87
|
+
@server.process("body")
|
88
|
+
end
|
89
|
+
|
90
|
+
should "map request, distribute to peers, and reduce" do
|
91
|
+
handler = mock()
|
92
|
+
request = mock()
|
93
|
+
request.expects(:hash).at_least_once.returns("abcddef134123")
|
94
|
+
@peer.expects(:connected?).returns(true)
|
95
|
+
handler.expects(:map).with(request, is_a(Hash)).returns({"localhost:4000" => "1", "localhost:4001" => "2"})
|
96
|
+
request.expects(:max_responses=).with(2)
|
97
|
+
@peer.expects(:client_request).with(request, "2")
|
98
|
+
|
99
|
+
handler.expects(:process).with("1").returns("2")
|
100
|
+
request.expects(:add_response).with("2")
|
101
|
+
|
102
|
+
request.expects(:wait_for_responses).once
|
103
|
+
handler.expects(:reduce).with(request)
|
104
|
+
|
105
|
+
@server.handler = handler
|
106
|
+
|
107
|
+
@server.handle_client_request(request)
|
108
|
+
# wait_for_threads
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
TEST_DIR = File.dirname(__FILE__)
|
2
|
+
%w(lib test).each do |dir|
|
3
|
+
$LOAD_PATH.unshift "#{TEST_DIR}/../#{dir}"
|
4
|
+
end
|
5
|
+
|
6
|
+
require 'test/unit'
|
7
|
+
require 'pandemic'
|
8
|
+
require 'rubygems'
|
9
|
+
require 'shoulda'
|
10
|
+
require 'mocha'
|
11
|
+
|
12
|
+
|
13
|
+
blackhole = StringIO.new
|
14
|
+
$pandemic_logger = Logger.new(blackhole)
|
15
|
+
|
16
|
+
module TestHelper
|
17
|
+
class TestException < Exception; end
|
18
|
+
def wait_for_threads(ignore = [Thread.current])
|
19
|
+
Thread.list.each do |thread|
|
20
|
+
next if ignore.include?(thread)
|
21
|
+
thread.join
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/test/util_test.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class UtilTest < Test::Unit::TestCase
|
4
|
+
context "with the module methods" do
|
5
|
+
setup do
|
6
|
+
@util = Object.new
|
7
|
+
@util.extend(Pandemic::Util)
|
8
|
+
end
|
9
|
+
|
10
|
+
should "parse out host and port" do
|
11
|
+
assert_equal ["localhost", 4000], @util.host_port("localhost:4000")
|
12
|
+
end
|
13
|
+
|
14
|
+
should "include the monitor mixin" do
|
15
|
+
object = Object.new
|
16
|
+
assert !object.respond_to?(:synchronize)
|
17
|
+
@util.with_mutex(object)
|
18
|
+
assert object.respond_to?(:synchronize)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: arya-pandemic
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 0.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Arya Asemanfar
|
@@ -9,10 +9,29 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-03-
|
12
|
+
date: 2009-03-26 00:00:00 -07:00
|
13
13
|
default_executable:
|
14
|
-
dependencies:
|
15
|
-
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: shoulda
|
17
|
+
type: :development
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0"
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: mocha
|
27
|
+
type: :development
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: "0"
|
34
|
+
version:
|
16
35
|
description: Distribute MapReduce to any of the workers and it will spread, like a pandemic.
|
17
36
|
email: aryaasemanfar@gmail.com
|
18
37
|
executables: []
|
@@ -52,12 +71,22 @@ files:
|
|
52
71
|
- lib/pandemic/server_side/server.rb
|
53
72
|
- lib/pandemic/util.rb
|
54
73
|
- lib/pandemic.rb
|
55
|
-
- Rakefile
|
56
|
-
- README.markdown
|
57
74
|
- Manifest
|
75
|
+
- MIT-LICENSE
|
58
76
|
- pandemic.gemspec
|
77
|
+
- Rakefile
|
78
|
+
- README.markdown
|
79
|
+
- test/client_test.rb
|
80
|
+
- test/connection_pool_test.rb
|
81
|
+
- test/functional_test.rb
|
82
|
+
- test/handler_test.rb
|
83
|
+
- test/mutex_counter_test.rb
|
84
|
+
- test/peer_test.rb
|
85
|
+
- test/server_test.rb
|
86
|
+
- test/test_helper.rb
|
87
|
+
- test/util_test.rb
|
59
88
|
has_rdoc: true
|
60
|
-
homepage:
|
89
|
+
homepage: https://github.com/arya/pandemic/
|
61
90
|
post_install_message:
|
62
91
|
rdoc_options:
|
63
92
|
- --line-numbers
|
@@ -87,5 +116,13 @@ rubygems_version: 1.2.0
|
|
87
116
|
signing_key:
|
88
117
|
specification_version: 2
|
89
118
|
summary: Distribute MapReduce to any of the workers and it will spread, like a pandemic.
|
90
|
-
test_files:
|
91
|
-
|
119
|
+
test_files:
|
120
|
+
- test/client_test.rb
|
121
|
+
- test/connection_pool_test.rb
|
122
|
+
- test/functional_test.rb
|
123
|
+
- test/handler_test.rb
|
124
|
+
- test/mutex_counter_test.rb
|
125
|
+
- test/peer_test.rb
|
126
|
+
- test/server_test.rb
|
127
|
+
- test/test_helper.rb
|
128
|
+
- test/util_test.rb
|