arya-pandemic 0.2.3 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Manifest +6 -1
- data/README.markdown +18 -1
- data/Rakefile +1 -1
- data/examples/client/client.rb +17 -0
- data/examples/client/constitution.txt +865 -0
- data/examples/client/pandemic_client.yml +3 -0
- data/examples/server/pandemic_server.yml +3 -0
- data/examples/server/word_count_server.rb +47 -0
- data/lib/pandemic.rb +3 -2
- data/lib/pandemic/mutex_counter.rb +21 -0
- data/lib/pandemic/server_side/config.rb +4 -2
- data/lib/pandemic/server_side/handler.rb +4 -0
- data/lib/pandemic/server_side/processor.rb +90 -0
- data/lib/pandemic/server_side/server.rb +31 -12
- data/pandemic.gemspec +5 -5
- data/test/functional_test.rb +2 -2
- data/test/mutex_counter_test.rb +27 -0
- data/test/processor_test.rb +33 -0
- data/test/server_test.rb +11 -9
- metadata +12 -3
@@ -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
|
|
@@ -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(
|
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 ==
|
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 = @
|
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
|
-
@
|
143
|
+
@handler_instance.reduce(request)
|
142
144
|
end
|
143
145
|
|
144
146
|
def process(body)
|
145
|
-
@
|
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.
|
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-
|
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", "
|
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
|
data/test/functional_test.rb
CHANGED
@@ -12,7 +12,7 @@ class FunctionalTest < Test::Unit::TestCase
|
|
12
12
|
def process(body)
|
13
13
|
body.reverse
|
14
14
|
end
|
15
|
-
end
|
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
|
33
|
+
end
|
34
34
|
|
35
35
|
ARGV.replace(["-i", "0", "-c", "test/pandemic_server.yml"]) # :(
|
36
36
|
server = epidemic!
|
data/test/mutex_counter_test.rb
CHANGED
@@ -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
|