arya-pandemic 0.2.3 → 0.3.0

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,3 @@
1
+ servers:
2
+ - localhost:4000
3
+ - localhost:4001
@@ -0,0 +1,3 @@
1
+ servers:
2
+ - localhost:4000
3
+ - localhost:4001
@@ -0,0 +1,47 @@
1
+ require 'rubygems'
2
+ require 'pandemic'
3
+ require 'json'
4
+
5
+ class WordCounter < Pandemic::ServerSide::Handler
6
+ def map(request, servers)
7
+ # select only the alive servers (non-disconnected)
8
+ only_alive = servers.keys.select{|k| servers[k] != :disconnected}
9
+
10
+ mapping = {}
11
+ intervals = (request.body.size / only_alive.size.to_f).floor
12
+
13
+ pos = 0
14
+ only_alive.size.times do |i|
15
+ if i == only_alive.size - 1 # last peer gets the rest
16
+ mapping[only_alive[i]] = request.body[pos..-1]
17
+ else
18
+ next_pos = request.body[(pos + intervals)..-1].index(/ /) + pos + intervals
19
+ mapping[only_alive[i]] = request.body[pos...next_pos]
20
+ pos = next_pos
21
+ end
22
+ end
23
+ mapping
24
+ end
25
+
26
+ def process(text)
27
+ counts = Hash.new(0)
28
+ text.scan(/\w+/) do |word|
29
+ counts[word.strip.downcase] += 1
30
+ end
31
+ counts.to_json
32
+ end
33
+
34
+ def reduce(request)
35
+ total_counts = Hash.new(0)
36
+ request.responses.each do |counts|
37
+ JSON.parse(counts).each do |word, count|
38
+ total_counts[word] += count
39
+ end
40
+ end
41
+ total_counts.to_json
42
+ end
43
+ end
44
+
45
+ server = epidemic!
46
+ server.handler = WordCounter
47
+ server.start.join
data/lib/pandemic.rb CHANGED
@@ -17,6 +17,7 @@ require 'pandemic/server_side/server'
17
17
  require 'pandemic/server_side/peer'
18
18
  require 'pandemic/server_side/request'
19
19
  require 'pandemic/server_side/handler'
20
+ require 'pandemic/server_side/processor'
20
21
 
21
22
  require 'pandemic/client_side/config'
22
23
  require 'pandemic/client_side/cluster_connection'
@@ -33,13 +34,13 @@ require 'pandemic/client_side/pandemize'
33
34
  TCP_NO_DELAY_AVAILABLE =
34
35
  RUBY_VERSION < '1.9' ? Socket.constants.include?('TCP_NODELAY') : Socket.constants.include?(:TCP_NODELAY)
35
36
 
36
- def epidemic!
37
+ def epidemic!(bind_to = nil)
37
38
  if $pandemic_logger.nil?
38
39
  $pandemic_logger = Logger.new("pandemic.log")
39
40
  $pandemic_logger.level = Logger::INFO
40
41
  $pandemic_logger.datetime_format = "%Y-%m-%d %H:%M:%S "
41
42
  end
42
- Pandemic::ServerSide::Server.boot
43
+ Pandemic::ServerSide::Server.boot(bind_to)
43
44
  end
44
45
 
45
46
  ::Pandemize = Pandemic::ClientSide::Pandemize
@@ -11,6 +11,7 @@ module Pandemic
11
11
  def real_total
12
12
  @mutex.synchronize { (@resets * @max) + @counter }
13
13
  end
14
+ alias_method :to_i, :real_total
14
15
 
15
16
  def value
16
17
  @mutex.synchronize { @counter }
@@ -25,5 +26,25 @@ module Pandemic
25
26
  @counter += 1
26
27
  end
27
28
  end
29
+ alias_method :next, :inc
30
+ alias_method :succ, :inc
31
+
32
+ # decr only to zero
33
+ def decr
34
+ @mutex.synchronize do
35
+ if @counter > 0
36
+ @counter -= 1
37
+ else
38
+ if @resets > 1
39
+ @resets -= 1
40
+ @counter = @max
41
+ end
42
+ end
43
+ @counter
44
+ end
45
+ end
46
+ alias_method :pred, :decr
47
+ alias_method :prev, :decr
48
+
28
49
  end
29
50
  end
@@ -2,7 +2,7 @@ module Pandemic
2
2
  module ServerSide
3
3
  class Config
4
4
  class << self
5
- attr_accessor :bind_to, :servers, :response_timeout
5
+ attr_accessor :bind_to, :servers, :response_timeout, :fork_for_processor
6
6
  def load
7
7
  path = extract_config_path
8
8
  yaml = YAML.load_file(path)
@@ -10,9 +10,11 @@ module Pandemic
10
10
  @server_map = yaml['servers'] || []
11
11
  @servers = @server_map.is_a?(Hash) ? @server_map.values : @server_map
12
12
  @servers = @servers.collect { |s| s.is_a?(Hash) ? s.keys.first : s }
13
-
13
+
14
14
  @response_timeout = (yaml['response_timeout'] || 1).to_f
15
15
  @bind_to = extract_bind_to
16
+ @fork_for_processor = yaml['fork_for_processor']
17
+
16
18
  raise "Interface to bind to is nil." unless @bind_to
17
19
  end
18
20
 
@@ -22,6 +22,10 @@ module Pandemic
22
22
  def process(body)
23
23
  body
24
24
  end
25
+
26
+ def filter_alive(servers)
27
+ servers.keys.select{|k| servers[k] != :disconnected}
28
+ end
25
29
  end
26
30
  end
27
31
  end
@@ -0,0 +1,90 @@
1
+ module Pandemic
2
+ module ServerSide
3
+ class Processor
4
+ def initialize(handler)
5
+ read_from_parent, write_to_child = IO.pipe
6
+ read_from_child, write_to_parent = IO.pipe
7
+
8
+ @child_process_id = fork
9
+ if @child_process_id
10
+ # I'm the parent
11
+ write_to_parent.close
12
+ read_from_parent.close
13
+ @out = write_to_child
14
+ @in = read_from_child
15
+ else
16
+ # I'm the child
17
+ write_to_child.close
18
+ read_from_child.close
19
+ @out = write_to_parent
20
+ @in = read_from_parent
21
+ @handler = handler.new
22
+ wait_for_jobs
23
+ end
24
+ end
25
+
26
+ def process(body)
27
+ if parent?
28
+ @out.puts(body.size.to_s)
29
+ @out.write(body)
30
+ ready, = IO.select([@in], nil, nil)
31
+ if ready
32
+ size = @in.gets.to_i
33
+ result = @in.read(size)
34
+ return result
35
+ end
36
+ else
37
+ return @handler.process(body)
38
+ end
39
+ end
40
+
41
+ def close(status = 0)
42
+ if parent? && child_alive?
43
+ Process.detach(@child_process_id)
44
+ @out.puts(status.to_s)
45
+ @out.close
46
+ @in.close
47
+ else
48
+ Process.exit!(status)
49
+ end
50
+ end
51
+
52
+ def closed?
53
+ !child_alive?
54
+ end
55
+
56
+ private
57
+ def wait_for_jobs
58
+ if child?
59
+ while true
60
+ ready, = IO.select([@in], nil, nil)
61
+ if ready
62
+ size = @in.gets.to_i
63
+ if size > 0
64
+ body = @in.read(size)
65
+ result = process(body)
66
+ @out.puts(result.size.to_s)
67
+ @out.write(result)
68
+ else
69
+ self.close(size)
70
+ break
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ def parent?
78
+ !!@child_process_id
79
+ end
80
+
81
+ def child?
82
+ !parent?
83
+ end
84
+
85
+ def child_alive?
86
+ parent? && !@in.closed?
87
+ end
88
+ end
89
+ end
90
+ end
@@ -4,10 +4,10 @@ module Pandemic
4
4
  include Util
5
5
  class StopServer < Exception; end
6
6
  class << self
7
- def boot
7
+ def boot(bind_to = nil)
8
8
  Config.load
9
9
  # Process.setrlimit(Process::RLIMIT_NOFILE, 4096) # arbitrary high number of max file descriptors.
10
- server = self.new
10
+ server = self.new(bind_to || Config.bind_to)
11
11
  set_signal_traps(server)
12
12
  server
13
13
  end
@@ -26,23 +26,25 @@ module Pandemic
26
26
  end
27
27
  end
28
28
  attr_reader :host, :port, :running
29
- def initialize
30
- @host, @port = host_port(Config.bind_to)
31
-
29
+ def initialize(bind_to)
30
+ @host, @port = host_port(bind_to)
32
31
  @clients = []
33
32
  @total_clients = 0
34
33
  @clients_mutex = Mutex.new
34
+ @num_jobs_processed = MutexCounter.new
35
+ @num_jobs_entered = MutexCounter.new
35
36
 
36
37
  @peers = {}
37
38
  @servers = Config.servers
38
39
  @servers.each do |peer|
39
- next if peer == Config.bind_to # not a peer, it's itself
40
+ next if peer == bind_to # not a peer, it's itself
40
41
  @peers[peer] = Peer.new(peer, self)
41
42
  end
42
43
  end
43
44
 
44
45
  def handler=(handler)
45
46
  @handler = handler
47
+ @handler_instance = handler.new
46
48
  end
47
49
 
48
50
  def start
@@ -113,7 +115,7 @@ module Pandemic
113
115
 
114
116
  def handle_client_request(request)
115
117
  info("Handling client request")
116
- map = @handler.map(request, connection_statuses)
118
+ map = @handler_instance.map(request, connection_statuses)
117
119
  request.max_responses = map.size
118
120
  debug("Sending client request to #{map.size} handlers (#{request.hash})")
119
121
 
@@ -129,7 +131,7 @@ module Pandemic
129
131
  begin
130
132
  request.add_response(self.process(map[signature]))
131
133
  rescue Exception => e
132
- warn("Unhandled exception in local processing: #{e.inspect}")
134
+ warn("Unhandled exception in local processing: #{e.inspect}#{e.backtrace.join("\n")}}")
133
135
  end
134
136
  end
135
137
  end
@@ -138,11 +140,18 @@ module Pandemic
138
140
  request.wait_for_responses
139
141
 
140
142
  debug("Done waiting for responses, calling reduce")
141
- @handler.reduce(request)
143
+ @handler_instance.reduce(request)
142
144
  end
143
145
 
144
146
  def process(body)
145
- @handler.process(body)
147
+ @num_jobs_entered.inc
148
+ response = if Config.fork_for_processor
149
+ self.processor.with_connection {|con| con.process(body) }
150
+ else
151
+ @handler_instance.process(body)
152
+ end
153
+ @num_jobs_processed.inc
154
+ response
146
155
  end
147
156
 
148
157
  def signature
@@ -166,6 +175,14 @@ module Pandemic
166
175
  end
167
176
  end
168
177
 
178
+ def processor
179
+ @processor ||= begin
180
+ processor = ConnectionPool.new
181
+ processor.create_connection { Processor.new(@handler) }
182
+ processor
183
+ end
184
+ end
185
+
169
186
  private
170
187
  def print_stats(connection)
171
188
  begin
@@ -180,6 +197,8 @@ module Pandemic
180
197
  str << "Total Requests: #{stats[:num_requests]}"
181
198
  str << "Pending Requests: #{stats[:pending_requests]}"
182
199
  str << "Late Responses: #{stats[:late_responses]}"
200
+ str << "Total Jobs Processed: #{stats[:total_jobs_processed]}"
201
+ str << "Pending Jobs: #{stats[:jobs_pending]}"
183
202
  connection.puts(str.join("\n"))
184
203
  end while (s = connection.gets) && (s.strip == "stats" || s.strip == "")
185
204
  connection.close if connection && !connection.closed?
@@ -212,10 +231,10 @@ module Pandemic
212
231
  end
213
232
  counts
214
233
  end
215
-
234
+ results[:total_jobs_processed] = @num_jobs_processed.to_i
235
+ results[:jobs_pending] = @num_jobs_entered.to_i - results[:total_jobs_processed]
216
236
  results[:num_requests] = Request.total_request_count
217
237
  results[:late_responses] = Request.total_late_responses
218
-
219
238
  results[:pending_requests] = @clients_mutex.synchronize do
220
239
  @clients.inject(0) do |pending, client|
221
240
  pending + (client.received_requests - client.responded_requests)
data/pandemic.gemspec CHANGED
@@ -2,15 +2,15 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = %q{pandemic}
5
- s.version = "0.2.3"
5
+ s.version = "0.3.0"
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-04-10}
9
+ s.date = %q{2009-05-27}
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
- 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", "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"]
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/processor.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 = ["examples/client/client.rb", "examples/client/constitution.txt", "examples/client/pandemic_client.yml", "examples/server/pandemic_server.yml", "examples/server/word_count_server.rb", "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/processor.rb", "lib/pandemic/server_side/request.rb", "lib/pandemic/server_side/server.rb", "lib/pandemic/util.rb", "lib/pandemic.rb", "Manifest", "MIT-LICENSE", "Rakefile", "README.markdown", "pandemic.gemspec", "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/processor_test.rb", "test/server_test.rb", "test/test_helper.rb", "test/util_test.rb"]
14
14
  s.has_rdoc = true
15
15
  s.homepage = %q{https://github.com/arya/pandemic/}
16
16
  s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Pandemic", "--main", "README.markdown"]
@@ -18,7 +18,7 @@ Gem::Specification.new do |s|
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
+ 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/processor_test.rb", "test/server_test.rb", "test/test_helper.rb", "test/util_test.rb"]
22
22
 
23
23
  if s.respond_to? :specification_version then
24
24
  current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
@@ -12,7 +12,7 @@ class FunctionalTest < Test::Unit::TestCase
12
12
  def process(body)
13
13
  body.reverse
14
14
  end
15
- end.new
15
+ end
16
16
  server.start
17
17
 
18
18
  client = Class.new do
@@ -30,7 +30,7 @@ class FunctionalTest < Test::Unit::TestCase
30
30
  def process(body)
31
31
  body.reverse
32
32
  end
33
- end.new
33
+ end
34
34
 
35
35
  ARGV.replace(["-i", "0", "-c", "test/pandemic_server.yml"]) # :(
36
36
  server = epidemic!
@@ -16,6 +16,24 @@ class MutexCounterTest < Test::Unit::TestCase
16
16
  assert_equal 1, @counter.value
17
17
  end
18
18
 
19
+ should "decrement to 0 after one call to inc" do
20
+ assert_equal 0, @counter.value
21
+ assert_equal 1, @counter.inc
22
+ assert_equal 1, @counter.value
23
+ assert_equal 0, @counter.decr
24
+ assert_equal 0, @counter.value
25
+ end
26
+
27
+ should "only decrement to 0" do
28
+ assert_equal 0, @counter.value
29
+ assert_equal 1, @counter.inc
30
+ assert_equal 1, @counter.value
31
+ assert_equal 0, @counter.decr
32
+ assert_equal 0, @counter.value
33
+ assert_equal 0, @counter.decr
34
+ assert_equal 0, @counter.value
35
+ end
36
+
19
37
  should "be thread safe" do
20
38
  # Not exactly a perfect test, but I'm not sure how to actually test
21
39
  # this without putting some code in the counter for this reason.
@@ -42,5 +60,14 @@ class MutexCounterTest < Test::Unit::TestCase
42
60
  assert_equal 1, @counter.value
43
61
  assert_equal 11, @counter.real_total
44
62
  end
63
+
64
+ should "maintain correct 'real_total' with decrement" do
65
+ 11.times { @counter.inc }
66
+ assert_equal 1, @counter.value
67
+ assert_equal 11, @counter.real_total
68
+ assert_equal 0, @counter.decr
69
+ assert_equal 10, @counter.real_total
70
+ end
71
+
45
72
  end
46
73
  end