zeevex_cluster 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/Gemfile +22 -0
- data/Rakefile +44 -0
- data/doc/BUGS-zookeeper.txt +60 -0
- data/doc/TODO.txt +85 -0
- data/lib/zeevex_cluster/base.rb +95 -0
- data/lib/zeevex_cluster/coordinator/base_key_val_store.rb +85 -0
- data/lib/zeevex_cluster/coordinator/memcached.rb +118 -0
- data/lib/zeevex_cluster/coordinator/mysql.rb +396 -0
- data/lib/zeevex_cluster/coordinator/redis.rb +101 -0
- data/lib/zeevex_cluster/coordinator.rb +29 -0
- data/lib/zeevex_cluster/election.rb +102 -0
- data/lib/zeevex_cluster/message.rb +52 -0
- data/lib/zeevex_cluster/nil_logger.rb +7 -0
- data/lib/zeevex_cluster/serializer/json_hash.rb +67 -0
- data/lib/zeevex_cluster/serializer.rb +27 -0
- data/lib/zeevex_cluster/static.rb +67 -0
- data/lib/zeevex_cluster/strategy/base.rb +92 -0
- data/lib/zeevex_cluster/strategy/cas.rb +403 -0
- data/lib/zeevex_cluster/strategy/static.rb +55 -0
- data/lib/zeevex_cluster/strategy/unclustered.rb +9 -0
- data/lib/zeevex_cluster/strategy/zookeeper.rb +163 -0
- data/lib/zeevex_cluster/strategy.rb +12 -0
- data/lib/zeevex_cluster/synchronized.rb +46 -0
- data/lib/zeevex_cluster/unclustered.rb +11 -0
- data/lib/zeevex_cluster/util/logging.rb +7 -0
- data/lib/zeevex_cluster/util.rb +15 -0
- data/lib/zeevex_cluster/version.rb +3 -0
- data/lib/zeevex_cluster.rb +29 -0
- data/script/election.rb +46 -0
- data/script/memc.rb +13 -0
- data/script/mysql.rb +25 -0
- data/script/redis.rb +14 -0
- data/script/repl +10 -0
- data/script/repl.rb +8 -0
- data/script/ser.rb +11 -0
- data/script/static.rb +34 -0
- data/script/testall +2 -0
- data/spec/cluster_static_spec.rb +49 -0
- data/spec/cluster_unclustered_spec.rb +32 -0
- data/spec/coordinator/coordinator_memcached_spec.rb +102 -0
- data/spec/message_spec.rb +38 -0
- data/spec/serializer/json_hash_spec.rb +68 -0
- data/spec/shared_master_examples.rb +20 -0
- data/spec/shared_member_examples.rb +39 -0
- data/spec/shared_non_master_examples.rb +8 -0
- data/spec/spec_helper.rb +14 -0
- data/zeevex_cluster.gemspec +43 -0
- 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,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'
|
data/script/election.rb
ADDED
@@ -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
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,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
|