mperham-politics 0.1.0 → 0.2.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.
- data/History.rdoc +4 -0
- data/README.rdoc +10 -5
- data/examples/token_worker_example.rb +35 -0
- data/lib/init.rb +1 -1
- data/lib/politics/discoverable_node.rb +2 -2
- data/lib/politics/static_queue_worker.rb +269 -0
- data/lib/politics/token_worker.rb +12 -5
- data/lib/politics/version.rb +1 -1
- data/lib/politics.rb +6 -3
- data/test/static_queue_worker_test.rb +42 -0
- data/test/test_helper.rb +2 -1
- metadata +21 -12
- data/Rakefile +0 -34
- data/lib/politics/bucket_worker.rb +0 -107
- data/test/bucket_worker_test.rb +0 -51
- data/test/political_test.rb +0 -32
data/History.rdoc
CHANGED
data/README.rdoc
CHANGED
@@ -15,9 +15,9 @@ for fault tolerance. This introduces the problem of coordination between those
|
|
15
15
|
Specifically, how do you keep those processes from stepping on each other's electronic
|
16
16
|
toes? There are several answers:
|
17
17
|
|
18
|
-
1. Break the processing into
|
19
|
-
work on it, and
|
20
|
-
|
18
|
+
1. Break the processing into N 'buckets'. Have an individual process fetch a bucket,
|
19
|
+
work on it, and ask for another. This is a very scalable solution as it allows N workers
|
20
|
+
to work on different parts of the same task concurrently. See the +StaticQueueWorker+ mixin.
|
21
21
|
1. Elect a leader for a short period of time. The leader is the process which performs the
|
22
22
|
actual processing. After a length of time, a new leader is elected from the group. This
|
23
23
|
is fault tolerant but not as scalable, as only one process is performing the task at a given
|
@@ -25,8 +25,13 @@ toes? There are several answers:
|
|
25
25
|
|
26
26
|
== Dependencies
|
27
27
|
|
28
|
-
|
29
|
-
|
28
|
+
StaticQueueWorker mixin
|
29
|
+
* memcached - the mechanism to elect a leader amongst a set of peers.
|
30
|
+
* DRb - the mechanism to communicate between peers.
|
31
|
+
* mDNS - the mechanism to discover peers.
|
32
|
+
|
33
|
+
TokenWorker mixin
|
34
|
+
* memcached - the mechanism to elect a leader amongst a set of peers.
|
30
35
|
|
31
36
|
|
32
37
|
= Author
|
@@ -0,0 +1,35 @@
|
|
1
|
+
#gem 'mperham-politics'
|
2
|
+
require 'politics'
|
3
|
+
require 'politics/token_worker'
|
4
|
+
|
5
|
+
# Test this example by starting memcached locally and then in two irb sessions, run this:
|
6
|
+
#
|
7
|
+
#
|
8
|
+
# You can then watch as one of them is elected leader. You can kill the leader and verify
|
9
|
+
# the backup process is elected after approximately iteration_length seconds.
|
10
|
+
#
|
11
|
+
=begin
|
12
|
+
require 'token_worker_example'
|
13
|
+
p = Politics::TokenWorkerExample.new
|
14
|
+
p.start
|
15
|
+
=end
|
16
|
+
module Politics
|
17
|
+
class TokenWorkerExample
|
18
|
+
include Politics::TokenWorker
|
19
|
+
|
20
|
+
def initialize
|
21
|
+
register_worker 'token-example', :iteration_length => 10, :servers => memcached_servers
|
22
|
+
end
|
23
|
+
|
24
|
+
def start
|
25
|
+
process do
|
26
|
+
puts "PID #{$$} processing at #{Time.now}..."
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def memcached_servers
|
31
|
+
['localhost:11211']
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
data/lib/init.rb
CHANGED
@@ -42,7 +42,7 @@ module Politics
|
|
42
42
|
self.group = group
|
43
43
|
start_drb
|
44
44
|
register_with_bonjour(group)
|
45
|
-
Politics.
|
45
|
+
Politics::log.info { "Registered #{self} in group #{group} with RID #{rid}" }
|
46
46
|
sleep 0.5
|
47
47
|
find_replicas(0)
|
48
48
|
end
|
@@ -73,7 +73,7 @@ module Politics
|
|
73
73
|
sleep 0.2
|
74
74
|
find_replicas(count + 1)
|
75
75
|
end
|
76
|
-
|
76
|
+
Politics::log.info { "Found #{replicas.size} peers: #{replicas.keys.sort.inspect}" } if count == 0
|
77
77
|
replicas
|
78
78
|
end
|
79
79
|
|
@@ -0,0 +1,269 @@
|
|
1
|
+
puts 'hello'
|
2
|
+
require 'socket'
|
3
|
+
require 'ipaddr'
|
4
|
+
require 'uri'
|
5
|
+
require 'drb'
|
6
|
+
|
7
|
+
begin
|
8
|
+
require 'net/dns/mdns-sd'
|
9
|
+
require 'net/dns/resolv-mdns'
|
10
|
+
require 'net/dns/resolv-replace'
|
11
|
+
rescue LoadError => e
|
12
|
+
puts "Unable to load net-mdns, please run `sudo gem install net-mdns`: #{e.message}"
|
13
|
+
exit(1)
|
14
|
+
end
|
15
|
+
|
16
|
+
begin
|
17
|
+
require 'memcache'
|
18
|
+
rescue LoadError => e
|
19
|
+
puts "Unable to load memcache client, please run `sudo gem install memcache-client`: #{e.message}"
|
20
|
+
exit(1)
|
21
|
+
end
|
22
|
+
|
23
|
+
module Politics
|
24
|
+
|
25
|
+
# The StaticQueueWorker mixin allows a processing daemon to "lease" or checkout
|
26
|
+
# a portion of a problem space to ensure no other process is processing that same
|
27
|
+
# space at the same time. The processing space is cut into N "buckets", each of
|
28
|
+
# which is placed in a queue. Processes then fetch entries from the queue
|
29
|
+
# and process them. It is up to the application to map the bucket number onto its
|
30
|
+
# specific problem space.
|
31
|
+
#
|
32
|
+
# Note that memcached is used for leader election. The leader owns the queue during
|
33
|
+
# the iteration period and other peers fetch buckets from the current leader during the
|
34
|
+
# iteration.
|
35
|
+
#
|
36
|
+
# The leader hands out buckets in order. Once all the buckets have been processed, the
|
37
|
+
# leader returns nil to the processors which causes them to sleep until the end of the
|
38
|
+
# iteration. Then everyone wakes up, a new leader is elected, and the processing starts
|
39
|
+
# all over again.
|
40
|
+
#
|
41
|
+
# DRb and mDNS are used for peer discovery and communication.
|
42
|
+
#
|
43
|
+
# Example usage:
|
44
|
+
#
|
45
|
+
# class Analyzer
|
46
|
+
# include Politics::StaticQueueWorker
|
47
|
+
# TOTAL_BUCKETS = 16
|
48
|
+
#
|
49
|
+
# def start
|
50
|
+
# register_worker(self.class.name, TOTAL_BUCKETS)
|
51
|
+
# process_bucket do |bucket|
|
52
|
+
# puts "Analyzing bucket #{bucket} of #{TOTAL_BUCKETS}"
|
53
|
+
# sleep 5
|
54
|
+
# end
|
55
|
+
# end
|
56
|
+
# end
|
57
|
+
#
|
58
|
+
# Note: process_bucket never returns i.e. this should be the main loop of your processing daemon.
|
59
|
+
#
|
60
|
+
module StaticQueueWorker
|
61
|
+
|
62
|
+
def self.included(model) #:nodoc:
|
63
|
+
model.class_eval do
|
64
|
+
attr_accessor :group_name, :iteration_length
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Register this process as able to work on buckets.
|
69
|
+
def register_worker(name, bucket_count, config={})
|
70
|
+
options = { :iteration_length => 60, :servers => ['127.0.0.1:11211'] }
|
71
|
+
options.merge!(config)
|
72
|
+
|
73
|
+
self.group_name = name
|
74
|
+
self.iteration_length = options[:iteration_length]
|
75
|
+
@memcache_client = client_for(Array(options[:servers]))
|
76
|
+
|
77
|
+
@buckets = []
|
78
|
+
@bucket_count = bucket_count
|
79
|
+
initialize_buckets
|
80
|
+
|
81
|
+
register_with_bonjour
|
82
|
+
|
83
|
+
log.info { "Registered #{self} in group #{group_name} at port #{@port}" }
|
84
|
+
end
|
85
|
+
|
86
|
+
# Fetch a bucket out of the queue and pass it to the given block to be processed.
|
87
|
+
#
|
88
|
+
# +bucket+:: The bucket number to process, within the range 0...TOTAL_BUCKETS
|
89
|
+
def process_bucket(&block)
|
90
|
+
raise ArgumentError, "process_bucket requires a block!" unless block_given?
|
91
|
+
raise ArgumentError, "You must call register_worker before processing!" unless @memcache_client
|
92
|
+
|
93
|
+
begin
|
94
|
+
nominate
|
95
|
+
if leader?
|
96
|
+
# Drb thread handles leader duties
|
97
|
+
log.info { "#{@uri} has been elected leader" }
|
98
|
+
relax until_next_iteration
|
99
|
+
initialize_buckets
|
100
|
+
else
|
101
|
+
# Get a bucket from the leader and process it
|
102
|
+
begin
|
103
|
+
bucket_process(*leader.bucket_request, &block)
|
104
|
+
rescue DRb::DRbError => dre
|
105
|
+
log.error { "Error talking to leader: #{dre.message}" }
|
106
|
+
relax until_next_iteration
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end while loop?
|
110
|
+
end
|
111
|
+
|
112
|
+
def bucket_request
|
113
|
+
if leader?
|
114
|
+
[@buckets.pop, until_next_iteration]
|
115
|
+
else
|
116
|
+
:not_leader
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
def bucket_process(bucket, sleep_time)
|
123
|
+
case bucket
|
124
|
+
when nil
|
125
|
+
# No more buckets to process this iteration
|
126
|
+
log.info { "No more buckets in this iteration, sleeping for #{sleep_time} sec" }
|
127
|
+
sleep sleep_time
|
128
|
+
when :not_leader
|
129
|
+
# Uh oh, race condition? Invalid any local cache and check again
|
130
|
+
log.warn { "Recv'd NOT_LEADER from peer." }
|
131
|
+
relax 1
|
132
|
+
@leader_uri = nil
|
133
|
+
else
|
134
|
+
log.info { "#{@uri} is processing #{bucket}"}
|
135
|
+
yield bucket
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def log
|
140
|
+
@logger ||= Logger.new(STDOUT)
|
141
|
+
end
|
142
|
+
|
143
|
+
def initialize_buckets
|
144
|
+
@buckets.clear
|
145
|
+
@bucket_count.times { |idx| @buckets << idx }
|
146
|
+
end
|
147
|
+
|
148
|
+
def replicas
|
149
|
+
@replicas ||= []
|
150
|
+
end
|
151
|
+
|
152
|
+
def leader
|
153
|
+
name = leader_uri
|
154
|
+
repl = nil
|
155
|
+
while replicas.empty? or repl == nil
|
156
|
+
repl = replicas.detect { |replica| replica.__drburi == name }
|
157
|
+
unless repl
|
158
|
+
relax 1
|
159
|
+
bonjour_scan do |replica|
|
160
|
+
replicas << replica
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
repl
|
165
|
+
end
|
166
|
+
|
167
|
+
def until_next_iteration
|
168
|
+
left = iteration_length - (Time.now - @nominated_at)
|
169
|
+
left > 0 ? left : 0
|
170
|
+
end
|
171
|
+
|
172
|
+
def loop?
|
173
|
+
true
|
174
|
+
end
|
175
|
+
|
176
|
+
def token
|
177
|
+
"#{group_name}_token"
|
178
|
+
end
|
179
|
+
|
180
|
+
def cleanup
|
181
|
+
at_exit do
|
182
|
+
@memcache_client.delete(token) if leader?
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def pause_until_expiry(elapsed)
|
187
|
+
pause_time = (iteration_length - elapsed).to_f
|
188
|
+
if pause_time > 0
|
189
|
+
relax(pause_time)
|
190
|
+
else
|
191
|
+
raise ArgumentError, "Negative iteration time left. Assuming the worst and exiting... #{iteration_length}/#{elapsed}"
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def relax(time)
|
196
|
+
sleep time
|
197
|
+
end
|
198
|
+
|
199
|
+
# Nominate ourself as leader by contacting the memcached server
|
200
|
+
# and attempting to add the token with our name attached.
|
201
|
+
def nominate
|
202
|
+
@memcache_client.add(token, @uri, iteration_length)
|
203
|
+
@nominated_at = Time.now
|
204
|
+
@leader_uri = nil
|
205
|
+
end
|
206
|
+
|
207
|
+
def leader_uri
|
208
|
+
@leader_uri ||= @memcache_client.get(token)
|
209
|
+
end
|
210
|
+
|
211
|
+
# Check to see if we are leader by looking at the process name
|
212
|
+
# associated with the token.
|
213
|
+
def leader?
|
214
|
+
until_next_iteration > 0 && @uri == leader_uri
|
215
|
+
end
|
216
|
+
|
217
|
+
# Easy to mock or monkey-patch if another MemCache client is preferred.
|
218
|
+
def client_for(servers)
|
219
|
+
MemCache.new(servers)
|
220
|
+
end
|
221
|
+
|
222
|
+
def time_for(&block)
|
223
|
+
a = Time.now
|
224
|
+
yield
|
225
|
+
Time.now - a
|
226
|
+
end
|
227
|
+
|
228
|
+
|
229
|
+
def register_with_bonjour
|
230
|
+
server = DRb.start_service(nil, self)
|
231
|
+
@uri = DRb.uri
|
232
|
+
@port = URI.parse(DRb.uri).port
|
233
|
+
|
234
|
+
# Register our DRb server with Bonjour.
|
235
|
+
handle = Net::DNS::MDNSSD.register("#{self.group_name}-#{local_ip}-#{$$}",
|
236
|
+
"_#{group_name}._tcp", 'local', @port)
|
237
|
+
|
238
|
+
['INT', 'TERM'].each { |signal|
|
239
|
+
trap(signal) do
|
240
|
+
handle.stop
|
241
|
+
server.stop_service
|
242
|
+
end
|
243
|
+
}
|
244
|
+
end
|
245
|
+
|
246
|
+
def bonjour_scan
|
247
|
+
Net::DNS::MDNSSD.browse("_#{group_name}._tcp") do |b|
|
248
|
+
Net::DNS::MDNSSD.resolve(b.name, b.type) do |r|
|
249
|
+
drburl = "druby://#{r.target}:#{r.port}"
|
250
|
+
replica = DRbObject.new(nil, drburl)
|
251
|
+
yield replica
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
# http://coderrr.wordpress.com/2008/05/28/get-your-local-ip-address/
|
257
|
+
def local_ip
|
258
|
+
orig, Socket.do_not_reverse_lookup = Socket.do_not_reverse_lookup, true # turn off reverse DNS resolution temporarily
|
259
|
+
|
260
|
+
UDPSocket.open do |s|
|
261
|
+
s.connect '64.233.187.99', 1
|
262
|
+
IPAddr.new(s.addr.last).to_i
|
263
|
+
end
|
264
|
+
ensure
|
265
|
+
Socket.do_not_reverse_lookup = orig
|
266
|
+
end
|
267
|
+
|
268
|
+
end
|
269
|
+
end
|
@@ -90,7 +90,7 @@ module Politics
|
|
90
90
|
|
91
91
|
time = 0
|
92
92
|
if leader?
|
93
|
-
|
93
|
+
Politics::log.info { "#{worker_name} elected leader at #{Time.now}" }
|
94
94
|
# If we are the master worker, do the work.
|
95
95
|
time = time_for do
|
96
96
|
result = block.call(*args)
|
@@ -98,11 +98,16 @@ module Politics
|
|
98
98
|
end
|
99
99
|
|
100
100
|
pause_until_expiry(time)
|
101
|
+
reset_state
|
101
102
|
end while loop?
|
102
103
|
end
|
103
104
|
|
104
105
|
private
|
105
106
|
|
107
|
+
def reset_state
|
108
|
+
@leader = nil
|
109
|
+
end
|
110
|
+
|
106
111
|
def verify_registration
|
107
112
|
unless self.class.worker_instance
|
108
113
|
raise ArgumentError, "Cannot call process without first calling register_worker"
|
@@ -118,7 +123,7 @@ module Politics
|
|
118
123
|
|
119
124
|
def cleanup
|
120
125
|
at_exit do
|
121
|
-
memcache_client.delete(token)
|
126
|
+
memcache_client.delete(token) if leader?
|
122
127
|
end
|
123
128
|
end
|
124
129
|
|
@@ -144,8 +149,10 @@ module Politics
|
|
144
149
|
# Check to see if we are leader by looking at the process name
|
145
150
|
# associated with the token.
|
146
151
|
def leader?
|
147
|
-
|
148
|
-
|
152
|
+
@leader ||= begin
|
153
|
+
master_worker = memcache_client.get(token)
|
154
|
+
worker_name == master_worker
|
155
|
+
end
|
149
156
|
end
|
150
157
|
|
151
158
|
# Easy to mock or monkey-patch if another MemCache client is preferred.
|
@@ -159,4 +166,4 @@ module Politics
|
|
159
166
|
Time.now - a
|
160
167
|
end
|
161
168
|
end
|
162
|
-
end
|
169
|
+
end
|
data/lib/politics/version.rb
CHANGED
data/lib/politics.rb
CHANGED
@@ -0,0 +1,42 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper'
|
2
|
+
|
3
|
+
Thread.abort_on_exception = true
|
4
|
+
|
5
|
+
class Worker
|
6
|
+
include Politics::StaticQueueWorker
|
7
|
+
def initialize
|
8
|
+
register_worker 'worker', 10, :iteration_length => 10
|
9
|
+
end
|
10
|
+
|
11
|
+
def start
|
12
|
+
process_bucket do |bucket|
|
13
|
+
sleep 1
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class StaticQueueWorkerTest < Test::Unit::TestCase
|
19
|
+
|
20
|
+
context "nodes" do
|
21
|
+
setup do
|
22
|
+
@nodes = []
|
23
|
+
5.times do
|
24
|
+
@nodes << nil
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
should "start up" do
|
29
|
+
processes = @nodes.map do
|
30
|
+
fork do
|
31
|
+
['INT', 'TERM'].each { |signal|
|
32
|
+
trap(signal) { exit(0) }
|
33
|
+
}
|
34
|
+
Worker.new.start
|
35
|
+
end
|
36
|
+
end
|
37
|
+
sleep 10
|
38
|
+
puts "Terminating"
|
39
|
+
Process.kill('INT', *processes)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/test/test_helper.rb
CHANGED
@@ -14,5 +14,6 @@ rescue LoadError => e
|
|
14
14
|
puts "Please install mocha: `sudo gem install mocha`"
|
15
15
|
end
|
16
16
|
|
17
|
+
$:.unshift(File.dirname(__FILE__) + '/../lib')
|
17
18
|
require File.dirname(__FILE__) + '/../lib/init'
|
18
|
-
Politics::
|
19
|
+
#Politics::log.level = Logger::WARN
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mperham-politics
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mike Perham
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2008-
|
12
|
+
date: 2008-10-27 00:00:00 -07:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
@@ -19,16 +19,25 @@ dependencies:
|
|
19
19
|
requirements:
|
20
20
|
- - ">="
|
21
21
|
- !ruby/object:Gem::Version
|
22
|
-
version:
|
22
|
+
version: 1.5.0.3
|
23
23
|
version:
|
24
24
|
- !ruby/object:Gem::Dependency
|
25
|
-
name:
|
25
|
+
name: starling-starling
|
26
26
|
version_requirement:
|
27
27
|
version_requirements: !ruby/object:Gem::Requirement
|
28
28
|
requirements:
|
29
29
|
- - ">="
|
30
30
|
- !ruby/object:Gem::Version
|
31
|
-
version:
|
31
|
+
version: 0.9.8
|
32
|
+
version:
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: net-mdns
|
35
|
+
version_requirement:
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: "0.4"
|
32
41
|
version:
|
33
42
|
description: Algorithms and Tools for Distributed Computing in Ruby.
|
34
43
|
email: mperham@gmail.com
|
@@ -41,17 +50,18 @@ extra_rdoc_files:
|
|
41
50
|
- History.rdoc
|
42
51
|
- LICENSE
|
43
52
|
files:
|
44
|
-
- README.rdoc
|
45
|
-
- LICENSE
|
46
|
-
- History.rdoc
|
47
|
-
- Rakefile
|
48
53
|
- lib/init.rb
|
49
54
|
- lib/politics
|
50
|
-
- lib/politics/bucket_worker.rb
|
51
55
|
- lib/politics/discoverable_node.rb
|
56
|
+
- lib/politics/static_queue_worker.rb
|
52
57
|
- lib/politics/token_worker.rb
|
53
58
|
- lib/politics/version.rb
|
54
59
|
- lib/politics.rb
|
60
|
+
- examples/queue_worker_example.rb
|
61
|
+
- examples/token_worker_example.rb
|
62
|
+
- README.rdoc
|
63
|
+
- History.rdoc
|
64
|
+
- LICENSE
|
55
65
|
has_rdoc: true
|
56
66
|
homepage: http://github.com/mperham/politics/
|
57
67
|
post_install_message:
|
@@ -87,7 +97,6 @@ signing_key:
|
|
87
97
|
specification_version: 2
|
88
98
|
summary: Algorithms and Tools for Distributed Computing in Ruby.
|
89
99
|
test_files:
|
90
|
-
- test/
|
91
|
-
- test/political_test.rb
|
100
|
+
- test/static_queue_worker_test.rb
|
92
101
|
- test/test_helper.rb
|
93
102
|
- test/token_worker_test.rb
|
data/Rakefile
DELETED
@@ -1,34 +0,0 @@
|
|
1
|
-
require 'echoe'
|
2
|
-
|
3
|
-
require File.dirname(__FILE__) << "/lib/politics/version"
|
4
|
-
|
5
|
-
Echoe.new 'politics' do |p|
|
6
|
-
p.version = Politics::Version::STRING
|
7
|
-
p.author = "Mike Perham"
|
8
|
-
p.email = 'mperham@gmail.com'
|
9
|
-
p.project = 'politics'
|
10
|
-
p.summary = "Algorithms and Tools for Distributed Computing in Ruby."
|
11
|
-
p.url = "http://github.com/mperham/politics"
|
12
|
-
p.dependencies = %w(memcache-client)
|
13
|
-
p.development_dependencies = []
|
14
|
-
p.include_rakefile = true
|
15
|
-
p.rubygems_version = nil
|
16
|
-
end
|
17
|
-
|
18
|
-
|
19
|
-
require 'rake/testtask'
|
20
|
-
|
21
|
-
desc "Run tests"
|
22
|
-
Rake::TestTask.new do |t|
|
23
|
-
t.libs << ['test', 'lib']
|
24
|
-
t.test_files = FileList['test/*_test.rb']
|
25
|
-
end
|
26
|
-
|
27
|
-
desc "Create rdoc"
|
28
|
-
Rake::RDocTask.new do |rd|
|
29
|
-
rd.main = "README.rdoc"
|
30
|
-
rd.rdoc_files.include("README.rdoc", "History.rdoc", "lib/**/*.rb")
|
31
|
-
end
|
32
|
-
|
33
|
-
|
34
|
-
task :default => :test
|
@@ -1,107 +0,0 @@
|
|
1
|
-
begin
|
2
|
-
require 'starling'
|
3
|
-
rescue LoadError => e
|
4
|
-
puts "Unable to load starling, please run `sudo gem install starling`: #{e.message}"
|
5
|
-
exit(1)
|
6
|
-
end
|
7
|
-
|
8
|
-
module Politics
|
9
|
-
|
10
|
-
# The BucketWorker mixin allows a processing daemon to "lease" or checkout
|
11
|
-
# a portion of a problem space to ensure no other process is processing that same
|
12
|
-
# space at the same time. The processing space is cut into N "buckets", each of
|
13
|
-
# which is placed in a queue. Processes then fetch entries from the queue
|
14
|
-
# and process them. It is up to the application to map the bucket number onto its
|
15
|
-
# specific problem space.
|
16
|
-
#
|
17
|
-
# Note that the Starling queue server is the single point of failure with this
|
18
|
-
# mechanism. Only you can decide if this is an acceptable tradeoff for your needs.
|
19
|
-
#
|
20
|
-
# Example usage:
|
21
|
-
#
|
22
|
-
# class Analyzer
|
23
|
-
# include Politics::BucketWorker
|
24
|
-
# TOTAL_BUCKETS = 16
|
25
|
-
#
|
26
|
-
# def start
|
27
|
-
# register_worker(self.class.name)
|
28
|
-
# create_buckets(TOTAL_BUCKETS) if master?
|
29
|
-
# process_bucket do |bucket|
|
30
|
-
# puts "Analyzing bucket #{bucket} of #{TOTAL_BUCKETS}"
|
31
|
-
# sleep 5
|
32
|
-
# end
|
33
|
-
# end
|
34
|
-
#
|
35
|
-
# def master?
|
36
|
-
# # TODO Add your own logic here to denote a 'master' process
|
37
|
-
# ARGV.include? '-m'
|
38
|
-
# end
|
39
|
-
# end
|
40
|
-
#
|
41
|
-
# Note: process_bucket never returns i.e. this should be the main loop of your processing daemon.
|
42
|
-
#
|
43
|
-
module BucketWorker
|
44
|
-
|
45
|
-
def self.included(model) #:nodoc:
|
46
|
-
model.class_eval do
|
47
|
-
attr_accessor :starling_client, :bucket_count, :queue_name
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
# Register this process as able to work on buckets.
|
52
|
-
#
|
53
|
-
# +name+:: The name of the queue to access
|
54
|
-
# +servers+:: The starling server(s) to use, defaults to +['localhost:22122']+
|
55
|
-
def register_worker(name, servers=['localhost:22122'])
|
56
|
-
self.queue_name = name
|
57
|
-
self.starling_client = client_for(Array(servers))
|
58
|
-
end
|
59
|
-
|
60
|
-
# Create the given number of buckets. Should ONLY be called by a single worker.
|
61
|
-
# TODO Obviously a major weakness of this algorithm. Is there a cleaner way?
|
62
|
-
#
|
63
|
-
# +bucket_count+:: The number of buckets to create
|
64
|
-
def create_buckets(bucket_count)
|
65
|
-
starling_client.flush(queue_name)
|
66
|
-
bucket_count.times do |count|
|
67
|
-
starling_client.set(queue_name, count)
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
# Fetch a bucket out of the queue and pass it to the given block to be processed.
|
72
|
-
# Once processing has completed, it will put the bucket back onto the queue for processing
|
73
|
-
# by a BucketWorker again, possibly immediately, depending on the number of buckets vs
|
74
|
-
# number of workers.
|
75
|
-
#
|
76
|
-
# +bucket+:: The bucket number to process, within the range 0...TOTAL_BUCKETS
|
77
|
-
def process_bucket
|
78
|
-
raise ArgumentError, "process_bucket requires a block!" unless block_given?
|
79
|
-
raise ArgumentError, "You must call register_worker before processing!" unless starling_client
|
80
|
-
|
81
|
-
begin
|
82
|
-
bucket = get_bucket
|
83
|
-
yield bucket
|
84
|
-
ensure
|
85
|
-
push_bucket(bucket)
|
86
|
-
end while loop?
|
87
|
-
end
|
88
|
-
|
89
|
-
private
|
90
|
-
|
91
|
-
def get_bucket
|
92
|
-
starling_client.get(queue_name)
|
93
|
-
end
|
94
|
-
|
95
|
-
def push_bucket(bucket)
|
96
|
-
starling_client.set(queue_name, bucket)
|
97
|
-
end
|
98
|
-
|
99
|
-
def client_for(servers)
|
100
|
-
Starling.new(servers)
|
101
|
-
end
|
102
|
-
|
103
|
-
def loop?
|
104
|
-
true
|
105
|
-
end
|
106
|
-
end
|
107
|
-
end
|
data/test/bucket_worker_test.rb
DELETED
@@ -1,51 +0,0 @@
|
|
1
|
-
require 'test_helper'
|
2
|
-
|
3
|
-
class BucketWorkerTest < Test::Unit::TestCase
|
4
|
-
|
5
|
-
context "bucket workers" do
|
6
|
-
setup do
|
7
|
-
@harness = Class.new
|
8
|
-
@harness.send(:include, Politics::BucketWorker)
|
9
|
-
@harness.any_instance.stubs(:loop?).returns(false)
|
10
|
-
@worker = @harness.new
|
11
|
-
end
|
12
|
-
|
13
|
-
should "have instance property accessors" do
|
14
|
-
assert @worker.bucket_count = 20
|
15
|
-
assert_equal 20, @worker.bucket_count
|
16
|
-
end
|
17
|
-
|
18
|
-
should 'register correctly' do
|
19
|
-
@worker.register_worker('testing')
|
20
|
-
@worker.register_worker('testing', ['localhost:5555,localhost:12121'])
|
21
|
-
end
|
22
|
-
|
23
|
-
should 'process a bucket' do
|
24
|
-
@worker.register_worker('testing')
|
25
|
-
@worker.starling_client.expects(:get).returns(4)
|
26
|
-
@worker.starling_client.expects(:set).with('testing', 4).returns(nil)
|
27
|
-
processed = false
|
28
|
-
@worker.process_bucket do |bucket|
|
29
|
-
assert_equal 4, bucket
|
30
|
-
processed = true
|
31
|
-
end
|
32
|
-
assert processed
|
33
|
-
end
|
34
|
-
|
35
|
-
should 'not allow processing without block' do
|
36
|
-
assert_raises ArgumentError do
|
37
|
-
@worker.register_worker('hello')
|
38
|
-
@worker.process_bucket
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
should 'not allow processing without registration' do
|
43
|
-
assert_raises ArgumentError do
|
44
|
-
@worker.process_bucket do
|
45
|
-
fail 'Should not process!'
|
46
|
-
end
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
end
|
51
|
-
end
|
data/test/political_test.rb
DELETED
@@ -1,32 +0,0 @@
|
|
1
|
-
require 'rubygems'
|
2
|
-
require 'test/unit'
|
3
|
-
require 'shoulda'
|
4
|
-
require File.dirname(__FILE__) + '/../lib/init'
|
5
|
-
|
6
|
-
Thread.abort_on_exception = true
|
7
|
-
|
8
|
-
class PoliticalTest < Test::Unit::TestCase
|
9
|
-
|
10
|
-
context "nodes" do
|
11
|
-
setup do
|
12
|
-
@nodes = []
|
13
|
-
5.times do
|
14
|
-
Object.send(:include, Politics::DiscoverableNode)
|
15
|
-
node = Object.new
|
16
|
-
@nodes << node
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
should "start up" do
|
21
|
-
processes = @nodes.map do |node|
|
22
|
-
fork do
|
23
|
-
['INT', 'TERM'].each { |signal|
|
24
|
-
trap(signal) { exit(0) }
|
25
|
-
}
|
26
|
-
node.register
|
27
|
-
end
|
28
|
-
end
|
29
|
-
Process.wait
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|