zeevex_cluster 0.2.1

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.
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