mperham-politics 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.rdoc +5 -0
- data/LICENSE +20 -0
- data/README.rdoc +39 -0
- data/Rakefile +34 -0
- data/lib/init.rb +4 -0
- data/lib/politics/bucket_worker.rb +107 -0
- data/lib/politics/discoverable_node.rb +137 -0
- data/lib/politics/token_worker.rb +162 -0
- data/lib/politics/version.rb +5 -0
- data/lib/politics.rb +9 -0
- data/test/bucket_worker_test.rb +51 -0
- data/test/political_test.rb +32 -0
- data/test/test_helper.rb +18 -0
- data/test/token_worker_test.rb +67 -0
- metadata +93 -0
data/History.rdoc
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2008 Mike Perham
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
= Politics
|
2
|
+
|
3
|
+
Politics is a Ruby library providing utilities and algorithms for solving common distributed
|
4
|
+
computing problems. Distributed Computing and Politics have a number of things in common:
|
5
|
+
1) they can be beautiful in theory but get really ugly in reality; 2) after working with
|
6
|
+
either for a few weeks/months/years (depending on your moral flexibility) you'll find yourself
|
7
|
+
intellectually devoid, a hollow shell of a man/woman/cybernetic killing machine.
|
8
|
+
|
9
|
+
So the name is to be taken tongue in cheek. Onto the real details.
|
10
|
+
|
11
|
+
== Common Problems in Distributed Computing
|
12
|
+
|
13
|
+
Ruby services are often deployed as a cloud of many processes across several machines,
|
14
|
+
for fault tolerance. This introduces the problem of coordination between those processes.
|
15
|
+
Specifically, how do you keep those processes from stepping on each other's electronic
|
16
|
+
toes? There are several answers:
|
17
|
+
|
18
|
+
1. Break the processing into several parts. Have an individual process "checkout" a part,
|
19
|
+
work on it, and then return the part for processing. This is a very scalable solution
|
20
|
+
as it allows N workers to work on the same task concurrently. See the +BucketWorker+ mixin.
|
21
|
+
1. Elect a leader for a short period of time. The leader is the process which performs the
|
22
|
+
actual processing. After a length of time, a new leader is elected from the group. This
|
23
|
+
is fault tolerant but not as scalable, as only one process is performing the task at a given
|
24
|
+
point in time. See the +TokenWorker+ and +PaxosMember+ mixins.
|
25
|
+
|
26
|
+
== Dependencies
|
27
|
+
|
28
|
+
The BucketWorker mixin uses the Starling queue server as the mechanism to hand out parts. The
|
29
|
+
TokenWorker mixin uses the memcached server as the mechanism to elect a leader.
|
30
|
+
|
31
|
+
|
32
|
+
= Author
|
33
|
+
|
34
|
+
Name:: Mike Perham
|
35
|
+
Email:: mailto:mperham@gmail.com
|
36
|
+
Twitter:: http://twitter.com/mperham
|
37
|
+
|
38
|
+
This software is free for you to use as you'd like. If you find it useful, please consider giving
|
39
|
+
me a recommendation at {Working with Rails}[http://workingwithrails.com/person/10797-mike-perham].
|
data/Rakefile
ADDED
@@ -0,0 +1,34 @@
|
|
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
|
data/lib/init.rb
ADDED
@@ -0,0 +1,107 @@
|
|
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
|
@@ -0,0 +1,137 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'ipaddr'
|
3
|
+
require 'uri'
|
4
|
+
require 'drb'
|
5
|
+
|
6
|
+
require 'net/dns/mdns-sd'
|
7
|
+
require 'net/dns/resolv-mdns'
|
8
|
+
require 'net/dns/resolv-replace'
|
9
|
+
|
10
|
+
=begin
|
11
|
+
IRB setup:
|
12
|
+
require 'lib/politics'
|
13
|
+
require 'lib/politics/discoverable_node'
|
14
|
+
require 'lib/politics/convention'
|
15
|
+
Object.send(:include, Election::Candidate)
|
16
|
+
p = Object.new
|
17
|
+
p.register
|
18
|
+
=end
|
19
|
+
|
20
|
+
module Politics
|
21
|
+
|
22
|
+
# A module to solve the Group Membership problem in distributed computing.
|
23
|
+
# The "group" is the cloud of processes which are replicas and need to coordinate.
|
24
|
+
# Handling group membership is the first step in solving distributed computing
|
25
|
+
# problems. There are two issues:
|
26
|
+
# 1) replica discovery
|
27
|
+
# 2) controlling and maintaining a consistent group of replicas in each replica
|
28
|
+
#
|
29
|
+
# Peer discovery is implemented using Bonjour for local network auto-discovery.
|
30
|
+
# Each process registers itself on the network as a process of a given type.
|
31
|
+
# Each process then queries the network for other replicas of the same type.
|
32
|
+
#
|
33
|
+
# The replicas then run the Multi-Paxos algorithm to provide consensus on a given
|
34
|
+
# replica set. The algorithm is robust in the face of crash failures, but not
|
35
|
+
# Byzantine failures.
|
36
|
+
module DiscoverableNode
|
37
|
+
|
38
|
+
attr_accessor :group
|
39
|
+
attr_accessor :coordinator
|
40
|
+
|
41
|
+
def register(group='foo')
|
42
|
+
self.group = group
|
43
|
+
start_drb
|
44
|
+
register_with_bonjour(group)
|
45
|
+
Politics.log "Registered #{self} in group #{group} with RID #{rid}"
|
46
|
+
sleep 0.5
|
47
|
+
find_replicas(0)
|
48
|
+
end
|
49
|
+
|
50
|
+
def replicas
|
51
|
+
@replicas ||= {}
|
52
|
+
end
|
53
|
+
|
54
|
+
def find_replicas(count)
|
55
|
+
replicas.clear if count % 5 == 0
|
56
|
+
return if count > 10 # Guaranteed to terminate, but not successfully :-(
|
57
|
+
|
58
|
+
#puts "Finding replicas"
|
59
|
+
peer_set = []
|
60
|
+
bonjour_scan do |replica|
|
61
|
+
(his_rid, his_peers) = replica.hello(rid)
|
62
|
+
unless replicas.has_key?(his_rid)
|
63
|
+
replicas[his_rid] = replica
|
64
|
+
end
|
65
|
+
his_peers.each do |peer|
|
66
|
+
peer_set << peer unless peer_set.include? peer
|
67
|
+
end
|
68
|
+
end
|
69
|
+
#p [peer_set.sort, replicas.keys.sort]
|
70
|
+
if peer_set.sort != replicas.keys.sort
|
71
|
+
# Recursively call ourselves until the network has settled down and all
|
72
|
+
# peers have reached agreement on the peer group membership.
|
73
|
+
sleep 0.2
|
74
|
+
find_replicas(count + 1)
|
75
|
+
end
|
76
|
+
puts "Found #{replicas.size} peers: #{replicas.keys.sort.inspect}" if count == 0
|
77
|
+
replicas
|
78
|
+
end
|
79
|
+
|
80
|
+
# Called for one peer to introduce itself to another peer. The caller
|
81
|
+
# sends his RID, the responder sends his RID and his list of current peer
|
82
|
+
# RIDs.
|
83
|
+
def hello(remote_rid)
|
84
|
+
[rid, replicas.keys]
|
85
|
+
end
|
86
|
+
|
87
|
+
# A process's Replica ID is its PID + a random 16-bit value. We don't want
|
88
|
+
# weigh solely based on PID or IP as that may unduly load one machine.
|
89
|
+
def rid
|
90
|
+
@rid ||= begin
|
91
|
+
rand(65536) + $$
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
def register_with_bonjour(group)
|
98
|
+
# Register our DRb server with Bonjour.
|
99
|
+
handle = Net::DNS::MDNSSD.register("#{self.group}-#{local_ip}-#{$$}",
|
100
|
+
"_#{self.group}._tcp", 'local', @port)
|
101
|
+
|
102
|
+
['INT', 'TERM'].each { |signal|
|
103
|
+
trap(signal) { handle.stop }
|
104
|
+
}
|
105
|
+
end
|
106
|
+
|
107
|
+
def start_drb
|
108
|
+
server = DRb.start_service(nil, self)
|
109
|
+
@port = URI.parse(DRb.uri).port
|
110
|
+
['INT', 'TERM'].each { |signal|
|
111
|
+
trap(signal) { server.stop_service }
|
112
|
+
}
|
113
|
+
end
|
114
|
+
|
115
|
+
def bonjour_scan
|
116
|
+
Net::DNS::MDNSSD.browse("_#{@group}._tcp") do |b|
|
117
|
+
Net::DNS::MDNSSD.resolve(b.name, b.type) do |r|
|
118
|
+
drburl = "druby://#{r.target}:#{r.port}"
|
119
|
+
replica = DRbObject.new(nil, drburl)
|
120
|
+
yield replica
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# http://coderrr.wordpress.com/2008/05/28/get-your-local-ip-address/
|
126
|
+
def local_ip
|
127
|
+
orig, Socket.do_not_reverse_lookup = Socket.do_not_reverse_lookup, true # turn off reverse DNS resolution temporarily
|
128
|
+
|
129
|
+
UDPSocket.open do |s|
|
130
|
+
s.connect '64.233.187.99', 1
|
131
|
+
IPAddr.new(s.addr.last).to_i
|
132
|
+
end
|
133
|
+
ensure
|
134
|
+
Socket.do_not_reverse_lookup = orig
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
begin
|
2
|
+
require 'memcache'
|
3
|
+
rescue LoadError => e
|
4
|
+
puts "Unable to load memcache client, please run `sudo gem install memcache-client`: #{e.message}"
|
5
|
+
exit(1)
|
6
|
+
end
|
7
|
+
|
8
|
+
module Politics
|
9
|
+
|
10
|
+
# An algorithm to provide leader election between a set of identical processing daemons.
|
11
|
+
#
|
12
|
+
# Each TokenWorker is an instance which needs to perform some processing.
|
13
|
+
# The worker instance must obtain the leader token before performing some task.
|
14
|
+
# We use a memcached server as a central token authority to provide a shared,
|
15
|
+
# network-wide view for all processors. This reliance on a single resource means
|
16
|
+
# if your memcached server goes down, so do the processors. Oftentimes,
|
17
|
+
# this is an acceptable trade-off since many high-traffic web sites would
|
18
|
+
# not be useable without memcached running anyhow.
|
19
|
+
#
|
20
|
+
# Essentially each TokenWorker attempts to elect itself every +:iteration_length+
|
21
|
+
# seconds by simply setting a key in memcached to its own name. Memcached tracks
|
22
|
+
# which name got there first. The key expires after +:iteration_length+ seconds.
|
23
|
+
#
|
24
|
+
# Example usage:
|
25
|
+
# class Analyzer
|
26
|
+
# include Politics::TokenWorker
|
27
|
+
#
|
28
|
+
# def initialize
|
29
|
+
# register_worker 'analyzer', :iteration_length => 120, :servers => ['localhost:11211']
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# def start
|
33
|
+
# process do
|
34
|
+
# # do analysis here, will only be done when this process
|
35
|
+
# # is actually elected leader, otherwise it will sleep for
|
36
|
+
# # iteration_length seconds.
|
37
|
+
# end
|
38
|
+
# end
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# Notes:
|
42
|
+
# * This will not work with multiple instances in the same Ruby process.
|
43
|
+
# The library is only designed to elect a leader from a set of processes, not instances within
|
44
|
+
# a single process.
|
45
|
+
# * The algorithm makes no attempt to keep the same leader during the next iteration.
|
46
|
+
# This can often times be quite beneficial (e.g. leveraging a warm cache from the last iteration)
|
47
|
+
# for performance but is left to the reader to implement.
|
48
|
+
module TokenWorker
|
49
|
+
|
50
|
+
def self.included(model) #:nodoc:
|
51
|
+
model.class_eval do
|
52
|
+
attr_accessor :memcache_client, :token, :iteration_length, :worker_name
|
53
|
+
class << self
|
54
|
+
attr_accessor :worker_instance #:nodoc:
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Register this instance as a worker.
|
60
|
+
#
|
61
|
+
# Options:
|
62
|
+
# +:iteration_length+:: The length of a processing iteration, in seconds. The
|
63
|
+
# leader's 'reign' lasts for this length of time.
|
64
|
+
# +:servers+:: An array of memcached server strings
|
65
|
+
def register_worker(name, config={})
|
66
|
+
# track the latest instance of this class, there's really only supposed to be
|
67
|
+
# a single TokenWorker instance per process.
|
68
|
+
self.class.worker_instance = self
|
69
|
+
|
70
|
+
options = { :iteration_length => 60, :servers => ['localhost:11211'] }
|
71
|
+
options.merge!(config)
|
72
|
+
|
73
|
+
self.token = "#{name}_token"
|
74
|
+
self.memcache_client = client_for(Array(options[:servers]))
|
75
|
+
self.iteration_length = options[:iteration_length]
|
76
|
+
self.worker_name = "#{Socket.gethostname}:#{$$}"
|
77
|
+
|
78
|
+
cleanup
|
79
|
+
end
|
80
|
+
|
81
|
+
def process(*args, &block)
|
82
|
+
verify_registration
|
83
|
+
|
84
|
+
begin
|
85
|
+
# Try to add our name as the worker with the master token.
|
86
|
+
# If another process got there first, this is a noop.
|
87
|
+
# We add an expiry so that the master token will constantly
|
88
|
+
# need to be refreshed (in case the current leader dies).
|
89
|
+
nominate
|
90
|
+
|
91
|
+
time = 0
|
92
|
+
if leader?
|
93
|
+
LOG.info { "I've been elected leader: #{worker_name}" }
|
94
|
+
# If we are the master worker, do the work.
|
95
|
+
time = time_for do
|
96
|
+
result = block.call(*args)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
pause_until_expiry(time)
|
101
|
+
end while loop?
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
def verify_registration
|
107
|
+
unless self.class.worker_instance
|
108
|
+
raise ArgumentError, "Cannot call process without first calling register_worker"
|
109
|
+
end
|
110
|
+
unless self.class.worker_instance == self
|
111
|
+
raise SecurityError, "Only one instance of #{self.class} per process. Another instance was created after this one."
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def loop?
|
116
|
+
true
|
117
|
+
end
|
118
|
+
|
119
|
+
def cleanup
|
120
|
+
at_exit do
|
121
|
+
memcache_client.delete(token)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def pause_until_expiry(elapsed)
|
126
|
+
pause_time = (iteration_length - elapsed).to_f
|
127
|
+
if pause_time > 0
|
128
|
+
relax(pause_time)
|
129
|
+
else
|
130
|
+
raise ArgumentError, "Negative iteration time left. Assuming the worst and exiting... #{iteration_length}/#{elapsed}"
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def relax(time)
|
135
|
+
sleep time
|
136
|
+
end
|
137
|
+
|
138
|
+
# Nominate ourself as leader by contacting the memcached server
|
139
|
+
# and attempting to add the token with our name attached.
|
140
|
+
def nominate
|
141
|
+
memcache_client.add(token, worker_name, iteration_length)
|
142
|
+
end
|
143
|
+
|
144
|
+
# Check to see if we are leader by looking at the process name
|
145
|
+
# associated with the token.
|
146
|
+
def leader?
|
147
|
+
master_worker = memcache_client.get(token)
|
148
|
+
worker_name == master_worker
|
149
|
+
end
|
150
|
+
|
151
|
+
# Easy to mock or monkey-patch if another MemCache client is preferred.
|
152
|
+
def client_for(servers)
|
153
|
+
MemCache.new(servers)
|
154
|
+
end
|
155
|
+
|
156
|
+
def time_for(&block)
|
157
|
+
a = Time.now
|
158
|
+
yield
|
159
|
+
Time.now - a
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
data/lib/politics.rb
ADDED
@@ -0,0 +1,51 @@
|
|
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
|
@@ -0,0 +1,32 @@
|
|
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
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'test/unit'
|
3
|
+
|
4
|
+
begin
|
5
|
+
gem 'thoughtbot-shoulda', '>=2.0.2'
|
6
|
+
require 'shoulda'
|
7
|
+
rescue GemError, LoadError => e
|
8
|
+
puts "Please install shoulda: `sudo gem install thoughtbot-shoulda -s http://gems.github.com`"
|
9
|
+
end
|
10
|
+
|
11
|
+
begin
|
12
|
+
require 'mocha'
|
13
|
+
rescue LoadError => e
|
14
|
+
puts "Please install mocha: `sudo gem install mocha`"
|
15
|
+
end
|
16
|
+
|
17
|
+
require File.dirname(__FILE__) + '/../lib/init'
|
18
|
+
Politics::LOG.level = Logger::WARN
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class TokenWorkerTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
context "token workers" do
|
6
|
+
setup do
|
7
|
+
@harness = Class.new
|
8
|
+
@harness.send(:include, Politics::TokenWorker)
|
9
|
+
@harness.any_instance.stubs(:cleanup)
|
10
|
+
@harness.any_instance.stubs(:loop?).returns(false)
|
11
|
+
@harness.any_instance.stubs(:pause_until_expiry)
|
12
|
+
@harness.any_instance.stubs(:relax)
|
13
|
+
|
14
|
+
@worker = @harness.new
|
15
|
+
end
|
16
|
+
|
17
|
+
should "test_instance_property_accessors" do
|
18
|
+
assert @worker.iteration_length = 20
|
19
|
+
assert_equal 20, @worker.iteration_length
|
20
|
+
end
|
21
|
+
|
22
|
+
should 'test_tracks_a_registered_singleton' do
|
23
|
+
assert_nil @worker.class.worker_instance
|
24
|
+
@worker.register_worker('testing')
|
25
|
+
assert_equal @worker.class.worker_instance, @worker
|
26
|
+
end
|
27
|
+
|
28
|
+
should 'not process if they are not leader' do
|
29
|
+
@worker.expects(:nominate)
|
30
|
+
@worker.expects(:leader?).returns(false)
|
31
|
+
@worker.register_worker('testing')
|
32
|
+
@worker.process do
|
33
|
+
assert false
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
should 'process if they are leader' do
|
38
|
+
@worker.expects(:nominate)
|
39
|
+
@worker.expects(:leader?).returns(true)
|
40
|
+
@worker.register_worker('testing')
|
41
|
+
|
42
|
+
worked = 0
|
43
|
+
@worker.process do
|
44
|
+
worked += 1
|
45
|
+
end
|
46
|
+
|
47
|
+
assert_equal 1, worked
|
48
|
+
end
|
49
|
+
|
50
|
+
should 'not allow processing without registration' do
|
51
|
+
assert_raises ArgumentError do
|
52
|
+
@worker.process
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
should 'not allow processing by old instances' do
|
57
|
+
@worker.register_worker('testing')
|
58
|
+
|
59
|
+
foo = @worker.class.new
|
60
|
+
foo.register_worker('testing')
|
61
|
+
|
62
|
+
assert_raises SecurityError do
|
63
|
+
@worker.process
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
metadata
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mperham-politics
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Mike Perham
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-08-13 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: fiveruns-memcache-client
|
17
|
+
version_requirement:
|
18
|
+
version_requirements: !ruby/object:Gem::Requirement
|
19
|
+
requirements:
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: "0"
|
23
|
+
version:
|
24
|
+
- !ruby/object:Gem::Dependency
|
25
|
+
name: fiveruns-starling
|
26
|
+
version_requirement:
|
27
|
+
version_requirements: !ruby/object:Gem::Requirement
|
28
|
+
requirements:
|
29
|
+
- - ">="
|
30
|
+
- !ruby/object:Gem::Version
|
31
|
+
version: "0"
|
32
|
+
version:
|
33
|
+
description: Algorithms and Tools for Distributed Computing in Ruby.
|
34
|
+
email: mperham@gmail.com
|
35
|
+
executables: []
|
36
|
+
|
37
|
+
extensions: []
|
38
|
+
|
39
|
+
extra_rdoc_files:
|
40
|
+
- README.rdoc
|
41
|
+
- History.rdoc
|
42
|
+
- LICENSE
|
43
|
+
files:
|
44
|
+
- README.rdoc
|
45
|
+
- LICENSE
|
46
|
+
- History.rdoc
|
47
|
+
- Rakefile
|
48
|
+
- lib/init.rb
|
49
|
+
- lib/politics
|
50
|
+
- lib/politics/bucket_worker.rb
|
51
|
+
- lib/politics/discoverable_node.rb
|
52
|
+
- lib/politics/token_worker.rb
|
53
|
+
- lib/politics/version.rb
|
54
|
+
- lib/politics.rb
|
55
|
+
has_rdoc: true
|
56
|
+
homepage: http://github.com/mperham/politics/
|
57
|
+
post_install_message:
|
58
|
+
rdoc_options:
|
59
|
+
- --quiet
|
60
|
+
- --title
|
61
|
+
- Politics documentation
|
62
|
+
- --opname
|
63
|
+
- index.html
|
64
|
+
- --line-numbers
|
65
|
+
- --main
|
66
|
+
- README.rdoc
|
67
|
+
- --inline-source
|
68
|
+
require_paths:
|
69
|
+
- lib
|
70
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: "0"
|
75
|
+
version:
|
76
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
77
|
+
requirements:
|
78
|
+
- - ">="
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: "0"
|
81
|
+
version:
|
82
|
+
requirements: []
|
83
|
+
|
84
|
+
rubyforge_project:
|
85
|
+
rubygems_version: 1.2.0
|
86
|
+
signing_key:
|
87
|
+
specification_version: 2
|
88
|
+
summary: Algorithms and Tools for Distributed Computing in Ruby.
|
89
|
+
test_files:
|
90
|
+
- test/bucket_worker_test.rb
|
91
|
+
- test/political_test.rb
|
92
|
+
- test/test_helper.rb
|
93
|
+
- test/token_worker_test.rb
|