arya-pandemic 0.2 → 0.2.1

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