zeevex_cluster 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/.gitignore +5 -0
  2. data/Gemfile +22 -0
  3. data/Rakefile +44 -0
  4. data/doc/BUGS-zookeeper.txt +60 -0
  5. data/doc/TODO.txt +85 -0
  6. data/lib/zeevex_cluster/base.rb +95 -0
  7. data/lib/zeevex_cluster/coordinator/base_key_val_store.rb +85 -0
  8. data/lib/zeevex_cluster/coordinator/memcached.rb +118 -0
  9. data/lib/zeevex_cluster/coordinator/mysql.rb +396 -0
  10. data/lib/zeevex_cluster/coordinator/redis.rb +101 -0
  11. data/lib/zeevex_cluster/coordinator.rb +29 -0
  12. data/lib/zeevex_cluster/election.rb +102 -0
  13. data/lib/zeevex_cluster/message.rb +52 -0
  14. data/lib/zeevex_cluster/nil_logger.rb +7 -0
  15. data/lib/zeevex_cluster/serializer/json_hash.rb +67 -0
  16. data/lib/zeevex_cluster/serializer.rb +27 -0
  17. data/lib/zeevex_cluster/static.rb +67 -0
  18. data/lib/zeevex_cluster/strategy/base.rb +92 -0
  19. data/lib/zeevex_cluster/strategy/cas.rb +403 -0
  20. data/lib/zeevex_cluster/strategy/static.rb +55 -0
  21. data/lib/zeevex_cluster/strategy/unclustered.rb +9 -0
  22. data/lib/zeevex_cluster/strategy/zookeeper.rb +163 -0
  23. data/lib/zeevex_cluster/strategy.rb +12 -0
  24. data/lib/zeevex_cluster/synchronized.rb +46 -0
  25. data/lib/zeevex_cluster/unclustered.rb +11 -0
  26. data/lib/zeevex_cluster/util/logging.rb +7 -0
  27. data/lib/zeevex_cluster/util.rb +15 -0
  28. data/lib/zeevex_cluster/version.rb +3 -0
  29. data/lib/zeevex_cluster.rb +29 -0
  30. data/script/election.rb +46 -0
  31. data/script/memc.rb +13 -0
  32. data/script/mysql.rb +25 -0
  33. data/script/redis.rb +14 -0
  34. data/script/repl +10 -0
  35. data/script/repl.rb +8 -0
  36. data/script/ser.rb +11 -0
  37. data/script/static.rb +34 -0
  38. data/script/testall +2 -0
  39. data/spec/cluster_static_spec.rb +49 -0
  40. data/spec/cluster_unclustered_spec.rb +32 -0
  41. data/spec/coordinator/coordinator_memcached_spec.rb +102 -0
  42. data/spec/message_spec.rb +38 -0
  43. data/spec/serializer/json_hash_spec.rb +68 -0
  44. data/spec/shared_master_examples.rb +20 -0
  45. data/spec/shared_member_examples.rb +39 -0
  46. data/spec/shared_non_master_examples.rb +8 -0
  47. data/spec/spec_helper.rb +14 -0
  48. data/zeevex_cluster.gemspec +43 -0
  49. metadata +298 -0
@@ -0,0 +1,163 @@
1
+ require 'zeevex_cluster/strategy/base'
2
+ require 'zk'
3
+ require 'zk/election'
4
+ require 'zk-group'
5
+
6
+ module ZeevexCluster::Strategy
7
+ class Zookeeper < Base
8
+ def initialize(options = {})
9
+ super
10
+ ZK.logger = logger
11
+ end
12
+
13
+ def start
14
+ return true if @state == :started
15
+ setup
16
+ @state = :started
17
+ end
18
+
19
+ def stop
20
+ return true unless @state == :started
21
+ @elector.close
22
+ @grouper.close
23
+ @zk.close
24
+ exited_cluster
25
+ @state = :stopped
26
+ true
27
+ end
28
+
29
+ #def am_i_master?
30
+ # @state == :started && @elector.leader?
31
+ #end
32
+
33
+ def master_node
34
+ @state == :started && {:nodename => @elector.leader_data}
35
+ end
36
+
37
+ def members_via_election
38
+ return unless @state == :started
39
+
40
+ root = @elector.root_vote_path
41
+ @zk.children(root).select {|f| f.start_with? "ballot" }.map do |name|
42
+ @zk.get(root + '/' + name)[0]
43
+ end
44
+ end
45
+
46
+ def members
47
+ @state == :started && @members
48
+ end
49
+
50
+ def data_for_grouper_members(*members)
51
+ root = @grouper.path
52
+ Array(members).flatten.map do |name|
53
+ @zk.get(root + '/' + name)[0]
54
+ end
55
+ end
56
+
57
+ def resign(delay = nil)
58
+ return false
59
+ end
60
+
61
+ def steal_election!
62
+ raise ClusterActionFailed, 'Can not change master' unless am_i_master?
63
+ true
64
+ end
65
+
66
+ def cluster_key
67
+ [@namespace, @cluster_name].reject {|x| x.nil? || x.empty? }.join(':')
68
+ end
69
+
70
+ protected
71
+
72
+ def setup
73
+ logger.debug "ZK: setting up"
74
+
75
+ @zk = ZK.new(@options[:host] || 'localhost:2181')
76
+ @zk.wait_until_connected
77
+
78
+ @elector = ZK::Election::Candidate.new @zk, cluster_key, :data => @nodename
79
+ @grouper = ZK::Group.new @zk, cluster_key
80
+
81
+ change_cluster_status :online
82
+
83
+ @members = []
84
+
85
+ setup_winning_callback
86
+ setup_losing_callback
87
+ setup_leader_ack_callback
88
+
89
+ setup_connection_callbacks
90
+
91
+ @grouper.create
92
+ @grouper.join @nodename
93
+
94
+ @grouper.on_membership_change do |last_members, current_members|
95
+ update_membership(last_members, current_members)
96
+
97
+ end
98
+
99
+ # this thread will run until we win, in which case
100
+ # the thread will exit and we'll be master.
101
+ thr = Thread.new do
102
+ @elector.vote!
103
+ end
104
+ thr.join
105
+ logger.debug "ZK vote thread exited"
106
+ end
107
+
108
+ def update_membership(last_members, current_members)
109
+ old_membership = @members
110
+ @members = data_for_grouper_members(current_members).freeze
111
+ logger.debug "ZK: membership change from ZK::Group: from #{old_membership.inspect} to #{@members.inspect}"
112
+ run_hook :membership_change, old_membership, @members
113
+ end
114
+
115
+ def setup_winning_callback
116
+ @elector.on_winning_election do
117
+ logger.debug "ZK: winning election!"
118
+ change_my_status :master
119
+ change_master_status :good
120
+ end
121
+ end
122
+
123
+ def setup_losing_callback
124
+ @elector.on_losing_election do
125
+ logger.debug "ZK: losing election!"
126
+ change_my_status :member
127
+ end
128
+ end
129
+
130
+ def setup_leader_ack_callback
131
+ @elector.on_leader_ack do
132
+ logger.debug "ZK: leader ack!"
133
+ change_master_status :good
134
+ end
135
+ end
136
+
137
+ def exited_cluster
138
+ change_my_status :nonmember
139
+ change_master_status :unknown
140
+ change_cluster_status :offline
141
+ @state = :stopped
142
+ end
143
+
144
+ def fail_out_of_cluster
145
+ stop
146
+ if @options[:reconnect]
147
+ logger.debug 'ZK: reconnecting after failure'
148
+ start
149
+ end
150
+ end
151
+
152
+ def setup_connection_callbacks
153
+ @zk.on_expired_session do
154
+ logger.debug 'ZK: expired session'
155
+ fail_out_of_cluster
156
+ end
157
+
158
+ @zk.on_state_change do |*args|
159
+ logger.debug "ZK: state change #{args.inspect}"
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,12 @@
1
+ module ZeevexCluster
2
+ module Strategy
3
+ def self.create(ctype, options)
4
+ require 'zeevex_cluster/strategy/' + ctype.downcase
5
+ clazz = self.const_get(ctype.capitalize)
6
+ raise ArgumentError, "Unknown strategy type: #{ctype}" unless clazz
7
+ ZeevexCluster.Synchronized(clazz.new(options))
8
+ end
9
+ end
10
+ end
11
+
12
+ require 'zeevex_cluster/strategy/base'
@@ -0,0 +1,46 @@
1
+ # Alex's Ruby threading utilities - taken from https://github.com/alexdowad/showcase
2
+
3
+ require 'thread'
4
+ require 'zeevex_proxy'
5
+
6
+ # Wraps an object, synchronizes all method calls
7
+ # The wrapped object can also be set and read out
8
+ # which means this can also be used as a thread-safe reference
9
+ # (like a 'volatile' variable in Java)
10
+ class ZeevexCluster::Synchronized < ZeevexProxy::Base
11
+ def initialize(obj)
12
+ super
13
+ @mutex = ::Mutex.new
14
+ end
15
+
16
+ def _set_synchronized_object(val)
17
+ @mutex.synchronize { @obj = val }
18
+ end
19
+ def _get_synchronized_object
20
+ @mutex.synchronize { @obj }
21
+ end
22
+
23
+ def respond_to?(method)
24
+ if [:_set_synchronized_object, :_get_synchronized_object].include?(method.to_sym)
25
+ true
26
+ else
27
+ @obj.respond_to?(method)
28
+ end
29
+ end
30
+
31
+ def method_missing(method, *args, &block)
32
+ result = @mutex.synchronize { @obj.__send__(method, *args, &block) }
33
+ # result.__id__ == @obj.__id__ ? self : result
34
+ end
35
+ end
36
+
37
+ #
38
+ # make object synchronized unless already synchronized
39
+ #
40
+ def ZeevexCluster.Synchronized(obj)
41
+ if obj.respond_to?(:_get_synchronized_object)
42
+ obj
43
+ else
44
+ ZeevexCluster::Synchronized.new(obj)
45
+ end
46
+ end
@@ -0,0 +1,11 @@
1
+ require 'zeevex_cluster/static'
2
+
3
+ module ZeevexCluster
4
+ class Unclustered < Static
5
+ def initialize(options = {})
6
+ raise ArgumentError, "Cannot specify master nodename" if options.include?(:master_nodename)
7
+ options[:master_nodename] = :self
8
+ super
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,7 @@
1
+ module ZeevexCluster::Util
2
+ module Logging
3
+ def logger
4
+ @logger || ZeevexCluster.logger
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,15 @@
1
+ module ZeevexCluster
2
+ module Util
3
+ module All
4
+ def self.included(base)
5
+ base.class_eval do
6
+ include Hookem
7
+ include ZeevexCluster::Util::Logging
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
13
+
14
+ require 'zeevex_cluster/util/logging'
15
+ require 'hookem'
@@ -0,0 +1,3 @@
1
+ module ZeevexCluster
2
+ VERSION = "0.2.1"
3
+ end
@@ -0,0 +1,29 @@
1
+ module ZeevexCluster
2
+ class ClusterException < StandardError; end
3
+ class NotMaster < ClusterException; end
4
+ class AlreadyMaster < ClusterException; end
5
+ class ClusterPolicyViolation < ClusterException; end
6
+ class ClusterActionFailed < ClusterException; end
7
+
8
+ def self.logger
9
+ @logger
10
+ end
11
+
12
+ def self.logger=(logger)
13
+ @logger = ZeevexCluster::Synchronized(logger)
14
+ end
15
+ end
16
+
17
+ require 'zeevex_cluster/synchronized'
18
+
19
+ require 'logger'
20
+ require 'zeevex_cluster/nil_logger'
21
+
22
+ ZeevexCluster.logger = ZeevexCluster::NilLogger.new
23
+
24
+ require 'zeevex_cluster/util'
25
+ require 'zeevex_cluster/base'
26
+ require 'zeevex_cluster/strategy'
27
+ require 'zeevex_cluster/coordinator'
28
+ require 'zeevex_cluster/election'
29
+ require 'zeevex_cluster/message'
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env ruby
2
+ $: << File.join(File.dirname(__FILE__), "../lib")
3
+ require 'rubygems'
4
+ require 'pry'
5
+ require 'zeevex_cluster'
6
+
7
+ ctype = ARGV[0] || 'memcached'
8
+ strategy_type = 'cas'
9
+
10
+ backend_options = case ctype
11
+ when 'memcached' then {:server => '127.0.0.1', :port => 11212}
12
+ when 'redis' then {:server => '127.0.0.1', :port => 6379}
13
+ when 'mysql' then {:server => '127.0.0.1', :port => 3306,
14
+ :coordinator_options => {
15
+ :namespace => 'cmdlinetest',
16
+ :username => 'zcluster',
17
+ :password => 'zclusterp',
18
+ :database => 'zcluster'}
19
+ }
20
+ when 'zookeeper'
21
+ strategy_type = 'zookeeper'
22
+ {:reconnect => true}
23
+ else raise 'Must be memcached or redis or mysql'
24
+ end.
25
+ merge({:coordinator_type => ctype})
26
+
27
+ $c = ZeevexCluster::Election.new :backend_options => backend_options,
28
+ :cluster_name => 'foobs',
29
+ :strategy_type => strategy_type,
30
+ :nodename => "#{Socket.gethostname}:#{`tty`.chomp}",
31
+ :logger => Logger.new(STDOUT),
32
+ :hooks => {:status_change => lambda {|who, news, olds, *rest|
33
+ puts "MSC! #{news} #{olds}"} }
34
+
35
+ Pry.config.prompt = Pry::DEFAULT_PROMPT.clone
36
+ Pry.config.prompt[0] = proc do |target_self, nest_level, pry|
37
+ cstatus = case true
38
+ when $c.master? then "MASTER"
39
+ when $c.member? then "member"
40
+ else "offline"
41
+ end
42
+ mcount = $c.member? && $c.members ? $c.members.count : 0
43
+ "[#{pry.input_array.size}] #{cstatus}[#{mcount}] pry(#{Pry.view_clip(target_self)})#{":#{nest_level}" unless nest_level.zero?}> "
44
+ end
45
+
46
+ binding.pry
data/script/memc.rb ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+ $: << File.join(File.dirname(__FILE__), "../lib")
3
+ require 'pry'
4
+ require 'zeevex_cluster'
5
+ require 'zeevex_cluster/memcached'
6
+
7
+ # ZeevexCluster.logger = Logger.new(STDOUT)
8
+ $c = ZeevexCluster::Memcached.new :backend_options => {:server => '127.0.0.1', :port => 11212},
9
+ :cluster_name => 'foobs',
10
+ :nodename => "#{Socket.gethostname}:#{`tty`.chomp}",
11
+ :logger => Logger.new(STDOUT),
12
+ :hooks => {:status_change => lambda {|who, news, olds, *rest| puts "MSC! #{news} #{olds}"} }
13
+
data/script/mysql.rb ADDED
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env ruby
2
+ $: << File.join(File.dirname(__FILE__), "../lib")
3
+ require 'pry'
4
+ require 'zeevex_cluster'
5
+ require 'zeevex_cluster/primitives'
6
+ require 'zeevex_cluster/coordinator'
7
+ require 'zeevex_cluster/coordinator/mysql'
8
+
9
+ require 'mysql2'
10
+
11
+ @client ||= Mysql2::Client.new(:host => 'localhost',
12
+ :database => 'zcluster',
13
+ :username => 'zcluster',
14
+ :password => 'zclusterp',
15
+ :reconnect => true,
16
+ :symbolize_keys => true,
17
+ :database_timezone => :utc)
18
+
19
+ $c = ZeevexCluster::Coordinator::Mysql.new :server => 'localhost',
20
+ :port => 3306,
21
+ :database => 'zcluster',
22
+ :username => 'zcluster',
23
+ :password => 'zclusterp',
24
+ :nodename => 'repl',
25
+ :expiration => 120
data/script/redis.rb ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ $: << File.join(File.dirname(__FILE__), "../lib")
3
+ require 'pry'
4
+ require 'zeevex_cluster'
5
+ require 'zeevex_cluster/coordinator/redis'
6
+
7
+ # ZeevexCluster.logger = Logger.new(STDOUT)
8
+ $c = ZeevexCluster::Coordinator::Redis.new :backend_options => {:server => '127.0.0.1', :port => 6379, :expiration => 120},
9
+ :server => '127.0.0.1', :port => 6379, :expiration => 120,
10
+ :cluster_name => 'foobs',
11
+ :nodename => "#{Socket.gethostname}:#{`tty`.chomp}",
12
+ :logger => Logger.new(STDOUT),
13
+ :hooks => {:status_change => lambda {|who, news, olds, *rest| puts "MSC! #{news} #{olds}"} }
14
+
data/script/repl ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+ $: << File.join(File.dirname(__FILE__), "../lib")
3
+ require 'pry'
4
+ require 'zeevex_cluster'
5
+ require 'zeevex_cluster/util/delayed'
6
+ require 'zeevex_cluster/util/future'
7
+ require 'zeevex_cluster/util/promise'
8
+ require 'zeevex_cluster/util/delay'
9
+
10
+ binding.pry
data/script/repl.rb ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ $: << File.join(File.dirname(__FILE__), "../lib")
3
+ require 'pry'
4
+ require 'zeevex_cluster'
5
+ begin
6
+ require 'zeevex_cluster/primitives'
7
+ rescue LoadError
8
+ end
data/script/ser.rb ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+ $: << File.join(File.dirname(__FILE__), "../lib")
3
+ require 'pry'
4
+ require 'zeevex_cluster'
5
+ begin
6
+ require 'zeevex_cluster/primitives'
7
+ rescue LoadError
8
+ end
9
+
10
+ $s = ZeevexCluster::Serializer::JsonHash.new
11
+ $h = {:a_at => Time.now, :b => [Time.now], :c => {:timestamp => Time.now}}
data/script/static.rb ADDED
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env ruby
2
+ $: << File.join(File.dirname(__FILE__), "../lib")
3
+ require 'rubygems'
4
+ require 'pry'
5
+ require 'zeevex_cluster'
6
+ require 'zeevex_cluster/election'
7
+ require 'zeevex_cluster/strategy/static'
8
+ require 'zeevex_cluster/strategy/unclustered'
9
+
10
+ # ZeevexCluster.logger = Logger.new(STDOUT)
11
+ ctype = ARGV[0] || 'memcached'
12
+ mname = ARGV[1] || 'none'
13
+ nodename = ARGV[2] || "#{Socket.gethostname}:#{`tty`.chomp}"
14
+
15
+ if mname == 'self'
16
+ mname = nodename
17
+ end
18
+
19
+ backend_options = case ctype
20
+ when 'static' then {:master_nodename => mname}
21
+ when 'unclustered' then {}
22
+ else raise 'Must be static or unclustered'
23
+ end.merge(:nodename => nodename)
24
+
25
+ strategy = ZeevexCluster::Strategy.const_get(ctype.capitalize).new backend_options
26
+
27
+ $c = ZeevexCluster::Election.new :backend_options => backend_options,
28
+ :strategy => strategy,
29
+ :cluster_name => 'foobs',
30
+ :nodename => nodename,
31
+ :logger => Logger.new(STDOUT),
32
+ :hooks => {:status_change => lambda {|who, news, olds, *rest| puts "MSC! #{news} #{olds}"} }
33
+
34
+ binding.pry
data/script/testall ADDED
@@ -0,0 +1,2 @@
1
+ #!/bin/zsh
2
+ rvm ree-1.8.7@zeevex_cluster,1.9.3-p327@zeevex_cluster,jruby-1.7.0@zeevex_cluster do bundle exec rspec
@@ -0,0 +1,49 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+
3
+ describe ZeevexCluster::Static do
4
+ def new_cluster(options = {})
5
+ ZeevexCluster::Static.new({:master_nodename => :self}.merge(options))
6
+ end
7
+
8
+ context 'creation' do
9
+ it 'requires the specification of a master nodename' do
10
+ expect { ZeevexCluster::Static.new }.to raise_error(ArgumentError)
11
+ end
12
+
13
+ it 'accepts a specified nodename for self' do
14
+ ZeevexCluster::Static.new(:nodename => 'foobar', :master_nodename => 'baz').
15
+ nodename.should == 'foobar'
16
+ end
17
+
18
+ it 'defaults to our hostname if nodename not specified' do
19
+ ZeevexCluster::Static.new(:master_nodename => 'baz').
20
+ nodename.should == Socket.gethostname
21
+ end
22
+
23
+ it 'treats master nodename == :self as our own nodename' do
24
+ ZeevexCluster::Static.new(:nodename => 'foo', :master_nodename => :self).
25
+ master.should == 'foo'
26
+ end
27
+ end
28
+
29
+ context 'when this node is specified as master' do
30
+ def new_cluster(options = {})
31
+ options[:master_nodename] = :self
32
+ super
33
+ end
34
+ subject { ZeevexCluster::Static.new(:nodename => 'foo', :master_nodename => 'foo') }
35
+ it_should_behave_like 'master_node'
36
+ it_should_behave_like 'member_node'
37
+ end
38
+
39
+ context 'when this node is not specified as master' do
40
+ def new_cluster(options = {})
41
+ options[:master_nodename] = 'other'
42
+ super
43
+ end
44
+ subject { ZeevexCluster::Static.new(:nodename => 'foo', :master_nodename => 'bar') }
45
+ it_should_behave_like 'non_master_node'
46
+ it_should_behave_like 'member_node'
47
+ end
48
+
49
+ end
@@ -0,0 +1,32 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+
3
+ describe ZeevexCluster::Unclustered do
4
+ context 'creation' do
5
+ it 'does not require any options' do
6
+ expect { ZeevexCluster::Unclustered.new }.not_to raise_error(ArgumentError)
7
+ end
8
+
9
+ it 'accepts a specified nodename for self' do
10
+ ZeevexCluster::Unclustered.new(:nodename => 'foobar').
11
+ nodename.should == 'foobar'
12
+ end
13
+
14
+ it 'defaults to our hostname if nodename not specified' do
15
+ ZeevexCluster::Unclustered.new.
16
+ nodename.should == Socket.gethostname
17
+ end
18
+
19
+ it 'does not accept master nodename option' do
20
+ expect { ZeevexCluster::Unclustered.new(:master_nodename => "foo") }.
21
+ to raise_error(ArgumentError)
22
+ end
23
+
24
+ it 'should be subclass of Static cluster type' do
25
+ ZeevexCluster::Unclustered.new.should be_a(ZeevexCluster::Static)
26
+ end
27
+
28
+ it 'should be master' do
29
+ ZeevexCluster::Unclustered.new.master?.should be_true
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,102 @@
1
+ require File.join(File.dirname(__FILE__), '../spec_helper')
2
+ require 'zeevex_cluster/coordinator/memcached.rb'
3
+
4
+ describe ZeevexCluster::Coordinator::Memcached do
5
+ STORED = "STORED\r\n"
6
+ EXISTS = "EXISTS\r\n"
7
+
8
+ let :mockery do
9
+ mock()
10
+ end
11
+
12
+ let :clazz do
13
+ ZeevexCluster::Coordinator::Memcached
14
+ end
15
+
16
+ let :default_options do
17
+ {:server => '127.0.0.1', :expiration => 30, :namespace => "foo"}
18
+ end
19
+
20
+ context 'instantiation' do
21
+ it 'requires server argument' do
22
+ expect { clazz.new(:expiration => 30) }.
23
+ to raise_error(ArgumentError)
24
+ end
25
+ it 'requires expiration argument' do
26
+ expect { clazz.new(:server => '127.0.0.1') }.
27
+ to raise_error(ArgumentError)
28
+ end
29
+ it 'constructs successfully with both args' do
30
+ expect { clazz.new({:server => '127.0.0.1', :expiration => 30}) }.
31
+ not_to raise_error
32
+ end
33
+ end
34
+
35
+ context 'basic methods' do
36
+ subject { clazz.new(default_options.merge(:client => mockery)) }
37
+ it 'handles add' do
38
+ mockery.should_receive(:add).with('foo:bar', '{"$primitive":12}', 30, true).and_return(STORED)
39
+ subject.add('bar', 12).should == true
40
+ end
41
+ it 'handles set' do
42
+ mockery.should_receive(:set).with('foo:bar', '{"$primitive":13}', 30, true).and_return(STORED)
43
+ subject.set('bar', 13).should == true
44
+ end
45
+ it 'handles get' do
46
+ mockery.should_receive(:get).with('foo:bar', true).and_return('{"$primitive":14}')
47
+ subject.get('bar').should == 14
48
+ end
49
+ end
50
+
51
+ context 'cas' do
52
+ subject { clazz.new(default_options.merge(:client => mockery)) }
53
+ let :blok do
54
+ Proc.new do |value|
55
+ value
56
+ @block_called = true
57
+ end
58
+ end
59
+
60
+ it 'delegates with correct arguments' do
61
+ mockery.should_receive(:cas).with('foo:bar', 45, true).and_return(STORED)
62
+ subject.cas('bar', :expiration => 45) {|val|}
63
+ end
64
+
65
+ it 'calls block with current value and receive new value' do
66
+ mockery.should_receive(:cas) do |key, expiration, raw, &block|
67
+ block.should_not be_nil
68
+ block.call('{"$primitive":"yeeha"}').should == '{"$primitive":"yeehayeeha"}'
69
+ STORED
70
+ end
71
+ subject.cas('bar', :expiration => 45) do |val|
72
+ @block_called = true
73
+ val + val
74
+ end
75
+ @block_called.should be_true
76
+ end
77
+
78
+ it 'allows block to abort with no change to value' do
79
+ mockery.stub(:cas) do |*args, &block|
80
+ block.call '{"$primitive":7}'
81
+ end
82
+ subject.cas('bar') do |val|
83
+ raise ZeevexCluster::Coordinator::DontChange
84
+ end.should == false
85
+ end
86
+
87
+ it 'return nil if cas failed due to no key' do
88
+ mockery.should_receive(:cas).and_return(nil)
89
+ subject.cas('bar') {|val|}.should be_nil
90
+ end
91
+
92
+ it 'return true if cas succeeded' do
93
+ mockery.should_receive(:cas).and_return(STORED)
94
+ subject.cas('bar') {|val|}.should be_true
95
+ end
96
+
97
+ it 'return false if cas conflicted' do
98
+ mockery.should_receive(:cas).and_return(EXISTS)
99
+ subject.cas('bar') {|val|}.should be_false
100
+ end
101
+ end
102
+ end