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.
@@ -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/*", 'config.yml']
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 }
@@ -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
- # IO.select([socket])
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 ignorings the max connections
17
- conn = create_connection
18
- if conn
19
- @mutex.synchronize do
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
- @mutex.synchronize { @connections.size > 0 }
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
- @available.each do |conn|
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
- @available = []
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 * MAX) + @counter }
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 >= MAX
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
- @connection.close unless @connection.nil? || @connection.closed?
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
- @server.handle_client_request(Request.new(request))
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
- 1.times { @connection_pool.add_connection! }
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
- @waiting_thread.wakeup if @waiting_thread && @waiting_thread.status == "sleep"
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
- sleep Config.response_timeout
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
@@ -148,7 +148,7 @@ module Pandemic
148
148
  def signature
149
149
  "#{@host}:#{@port}"
150
150
  end
151
-
151
+
152
152
  def connection_statuses
153
153
  @servers.inject({}) do |statuses, server|
154
154
  if server == signature
@@ -5,18 +5,7 @@ module Pandemic
5
5
  end
6
6
 
7
7
  def logger
8
- $logger
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)
@@ -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-10}
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", "Manifest", "pandemic.gemspec"]
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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: "0.2"
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-10 00:00:00 -07:00
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