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