mperham-politics 0.1.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 +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
|