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 ADDED
@@ -0,0 +1,5 @@
1
+ = Changelog
2
+
3
+ == 0.1.0 (2008-10-07)
4
+
5
+ * Add BucketWorker and TokenWorker mixins.
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,4 @@
1
+ require 'politics'
2
+ require 'politics/token_worker'
3
+ require 'politics/bucket_worker'
4
+ require 'politics/discoverable_node'
@@ -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
@@ -0,0 +1,5 @@
1
+ module Politics
2
+ module Version
3
+ STRING = "0.1.0"
4
+ end
5
+ end
data/lib/politics.rb ADDED
@@ -0,0 +1,9 @@
1
+ require 'logger'
2
+
3
+ module Politics
4
+ LOG = Logger.new(STDOUT)
5
+
6
+ def self.log(msg)
7
+ LOG.info(msg)
8
+ end
9
+ end
@@ -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
@@ -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