zk-group 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.dotfiles/rspec-logging +4 -0
- data/.dotfiles/rvmrc +2 -0
- data/.gitignore +20 -0
- data/.gitmodules +3 -0
- data/Gemfile +36 -0
- data/Guardfile +16 -0
- data/MIT_LICENSE +22 -0
- data/README.md +41 -0
- data/Rakefile +68 -0
- data/lib/zk-group.rb +82 -0
- data/lib/zk-group/exceptions.rb +16 -0
- data/lib/zk-group/group.rb +321 -0
- data/lib/zk-group/member.rb +52 -0
- data/lib/zk-group/membership_subscription.rb +36 -0
- data/lib/zk-group/version.rb +5 -0
- data/spec/logging_progress_bar_formatter.rb +12 -0
- data/spec/shared/client_contexts.rb +14 -0
- data/spec/spec_helper.rb +48 -0
- data/spec/support/custom_matchers.rb +12 -0
- data/spec/support/exist_matcher.rb +6 -0
- data/spec/support/logging.rb +26 -0
- data/spec/support/wait_watchers.rb +48 -0
- data/spec/zk-group/group_spec.rb +202 -0
- data/spec/zk-group/member_spec.rb +61 -0
- data/zk-group.gemspec +24 -0
- metadata +113 -0
data/.dotfiles/rvmrc
ADDED
data/.gitignore
ADDED
data/.gitmodules
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
source :rubygems
|
2
|
+
|
3
|
+
# git 'git://github.com/slyphon/zookeeper.git', :ref => '8dfdd6be' do
|
4
|
+
# gem 'zookeeper', '>= 1.0.0.beta.1'
|
5
|
+
# end
|
6
|
+
|
7
|
+
git 'git://github.com/slyphon/zk', :ref => '41bfd35' do
|
8
|
+
gem 'zk'
|
9
|
+
end
|
10
|
+
|
11
|
+
gem 'pry', :group => [:development, :test]
|
12
|
+
|
13
|
+
group :test do
|
14
|
+
gem 'rspec', '~> 2.8'
|
15
|
+
end
|
16
|
+
|
17
|
+
group :docs do
|
18
|
+
gem 'yard', '~> 0.7.5'
|
19
|
+
|
20
|
+
platform :mri_19 do
|
21
|
+
gem 'redcarpet'
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
group :development do
|
26
|
+
gem 'guard', :require => false
|
27
|
+
gem 'guard-rspec', :require => false
|
28
|
+
gem 'guard-bundler', :require => false
|
29
|
+
|
30
|
+
gem 'growl' if `uname -s` =~ /darwin/i
|
31
|
+
end
|
32
|
+
|
33
|
+
# Specify your gem's dependencies in zk-group.gemspec
|
34
|
+
gemspec
|
35
|
+
|
36
|
+
|
data/Guardfile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
guard 'bundler' do
|
5
|
+
watch('Gemfile')
|
6
|
+
watch(/^.+\.gemspec/)
|
7
|
+
end
|
8
|
+
|
9
|
+
guard 'rspec', :version => 2 do
|
10
|
+
watch(%r{^spec/.+_spec\.rb$})
|
11
|
+
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
|
12
|
+
watch(%r%^lib/zk\.rb%) { "spec" }
|
13
|
+
|
14
|
+
watch('spec/spec_helper.rb') { "spec" }
|
15
|
+
end
|
16
|
+
|
data/MIT_LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (C) 2012 by Hewlett Packard Development Company, L.P.
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# ZK::Group
|
2
|
+
|
3
|
+
A Group implementation for [ZK][]. Why do you need ZK::Group? Let's say you have a bunch of services. One might even be so bold as to call it "a cluster" (especially in the presence of CTOs or investors).
|
4
|
+
|
5
|
+
If you'd like to:
|
6
|
+
|
7
|
+
* Know who in the group is currently available
|
8
|
+
* Be notified when the members of the group have changed
|
9
|
+
* What changed about the list of members
|
10
|
+
|
11
|
+
Then ZK::Group is the tool for you. Intentionally lightweight. Use it as-is or subclass and extend for _even more fun_.
|
12
|
+
|
13
|
+
_"You'll love it. It's a way of life." - The Central Scrutinizer_
|
14
|
+
|
15
|
+
[ZK]: https://github.com/slyphon/zk
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
Add this line to your application's Gemfile:
|
20
|
+
|
21
|
+
gem 'zk-group'
|
22
|
+
|
23
|
+
And then execute:
|
24
|
+
|
25
|
+
$ bundle
|
26
|
+
|
27
|
+
Or install it yourself as:
|
28
|
+
|
29
|
+
$ gem install zk-group
|
30
|
+
|
31
|
+
## Usage
|
32
|
+
|
33
|
+
TODO: Write usage instructions here
|
34
|
+
|
35
|
+
## Contributing
|
36
|
+
|
37
|
+
1. Fork it
|
38
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
39
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
40
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
41
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
release_ops_path = File.expand_path('../releaseops/lib', __FILE__)
|
2
|
+
|
3
|
+
# if the special submodule is availabe, use it
|
4
|
+
# we use a submodule because it doesn't depend on anything else (*cough* bundler)
|
5
|
+
# and can be shared across projects
|
6
|
+
#
|
7
|
+
if File.exists?(release_ops_path)
|
8
|
+
require File.join(release_ops_path, 'releaseops')
|
9
|
+
|
10
|
+
# sets up the multi-ruby zk:test_all rake tasks
|
11
|
+
ReleaseOps::TestTasks.define_for(*%w[1.8.7 1.9.2 jruby ree 1.9.3])
|
12
|
+
|
13
|
+
# sets up the task :default => 'spec:run' and defines a simple
|
14
|
+
# "run the specs with the current rvm profile" task
|
15
|
+
ReleaseOps::TestTasks.define_simple_default_for_travis
|
16
|
+
|
17
|
+
# Define a task to run code coverage tests
|
18
|
+
ReleaseOps::TestTasks.define_simplecov_tasks
|
19
|
+
|
20
|
+
# set up yard:server, yard:gems, and yard:clean tasks
|
21
|
+
# for doing documentation stuff
|
22
|
+
ReleaseOps::YardTasks.define
|
23
|
+
|
24
|
+
namespace :zk do
|
25
|
+
namespace :gems do
|
26
|
+
task :build do
|
27
|
+
require 'tmpdir'
|
28
|
+
|
29
|
+
raise "You must specify a TAG" unless ENV['TAG']
|
30
|
+
|
31
|
+
ReleaseOps.with_tmpdir(:prefix => 'zk-group') do |tmpdir|
|
32
|
+
tag = ENV['TAG']
|
33
|
+
|
34
|
+
sh "git clone . #{tmpdir}"
|
35
|
+
|
36
|
+
orig_dir = Dir.getwd
|
37
|
+
|
38
|
+
cd tmpdir do
|
39
|
+
sh "git co #{tag} && git reset --hard && git clean -fdx"
|
40
|
+
|
41
|
+
sh "rvm 1.8.7 do gem build zk-group.gemspec"
|
42
|
+
|
43
|
+
mv FileList['*.gem'], orig_dir
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
task :push do
|
49
|
+
gems = FileList['*.gem']
|
50
|
+
raise "No gemfiles to push!" if gems.empty?
|
51
|
+
|
52
|
+
gems.each do |gem|
|
53
|
+
sh "gem push #{gem}"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
task :clean do
|
58
|
+
rm_rf FileList['*.gem']
|
59
|
+
end
|
60
|
+
|
61
|
+
task :all => [:build, :push, :clean]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
task :clean => 'yard:clean'
|
67
|
+
end
|
68
|
+
|
data/lib/zk-group.rb
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
require 'zk'
|
5
|
+
|
6
|
+
module ZK
|
7
|
+
# A Group is a basic membership primitive. You pick a name
|
8
|
+
# for the group, and then join, leave, and receive updates when
|
9
|
+
# group membership changes. You can also get a list of other members of the
|
10
|
+
# group.
|
11
|
+
#
|
12
|
+
module Group
|
13
|
+
# common znode data access
|
14
|
+
module Common
|
15
|
+
# the data my znode contains
|
16
|
+
def data
|
17
|
+
zk.get(path).first
|
18
|
+
end
|
19
|
+
|
20
|
+
# Set the data in my group znode (the data at {#path})
|
21
|
+
#
|
22
|
+
# In the base implementation, no version is given, this will just
|
23
|
+
# overwrite whatever is currently there.
|
24
|
+
#
|
25
|
+
# @param [String] val the data to set
|
26
|
+
# @return [String] the data that was set
|
27
|
+
def data=(val)
|
28
|
+
zk.set(path, val)
|
29
|
+
val
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# A simple proxy for catching client errors and re-raising them as Group specific
|
34
|
+
# errors (for clearer error reporting...we hope)
|
35
|
+
#
|
36
|
+
# @private
|
37
|
+
class GroupExceptionTranslator
|
38
|
+
def initialize(zk, group)
|
39
|
+
@zk = zk
|
40
|
+
@group = group
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
def method_missing(m, *a, &b)
|
45
|
+
super unless @zk.respond_to?(m)
|
46
|
+
@zk.__send__(m, *a, &b)
|
47
|
+
rescue Exceptions::NoNode
|
48
|
+
raise Exceptions::GroupDoesNotExistError, "group at #{@group.path} has not been created yet", caller
|
49
|
+
rescue Exceptions::NodeExists
|
50
|
+
raise Exceptions::GroupAlreadyExistsError, "group at #{@group.path} already exists", caller
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# A simple proxy for catching client errors and re-raising them as Group specific
|
55
|
+
# errors (for clearer error reporting...we hope)
|
56
|
+
#
|
57
|
+
# @private
|
58
|
+
class MemberExceptionTranslator
|
59
|
+
def initialize(zk)
|
60
|
+
@zk = zk
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
def method_missing(m, *a, &b)
|
65
|
+
super unless @zk.respond_to?(m)
|
66
|
+
@zk.__send__(m, *a, &b)
|
67
|
+
rescue Exceptions::NoNode
|
68
|
+
raise Exceptions::MemberDoesNotExistError, "group at #{path} has not been created yet", caller
|
69
|
+
rescue Exceptions::NodeExists
|
70
|
+
raise Exceptions::MemberAlreadyExistsError, "group at #{path} already exists", caller
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end # Group
|
74
|
+
end # ZK
|
75
|
+
|
76
|
+
require 'zk-group/version'
|
77
|
+
require 'zk-group/exceptions'
|
78
|
+
require 'zk-group/membership_subscription'
|
79
|
+
require 'zk-group/group'
|
80
|
+
require 'zk-group/member'
|
81
|
+
|
82
|
+
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module ZK
|
2
|
+
module Exceptions
|
3
|
+
# Raised when you try to perform an operation on a group but it hasn't been
|
4
|
+
# created yet
|
5
|
+
class GroupDoesNotExistError < NoNode; end
|
6
|
+
|
7
|
+
# Raised when you try to create! a group but it already exists
|
8
|
+
class GroupAlreadyExistsError < NodeExists; end
|
9
|
+
|
10
|
+
# Raised if an operation is performed that assumes that a membership is active but it wasn't
|
11
|
+
class MemberDoesNotExistError < NoNode; end
|
12
|
+
|
13
|
+
# for symmetry with GroupAlreadyExistsError but in the base implementation, should probably never happen
|
14
|
+
class MemberAlreadyExistsError < NodeExists; end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,321 @@
|
|
1
|
+
module ZK
|
2
|
+
module Group
|
3
|
+
@@mutex = Mutex.new unless defined?(@@mutex)
|
4
|
+
|
5
|
+
DEFAULT_ROOT = '/_zk/groups'
|
6
|
+
|
7
|
+
# @private
|
8
|
+
DEFAULT_PREFIX = 'm'.freeze
|
9
|
+
|
10
|
+
class << self
|
11
|
+
# @private
|
12
|
+
def mutex
|
13
|
+
@@mutex
|
14
|
+
end
|
15
|
+
|
16
|
+
# The path under which all groups will be created.
|
17
|
+
# defaults to DEFAULT_ROOT if not set
|
18
|
+
def zk_root
|
19
|
+
@@mutex.synchronize { @@zk_root ||= DEFAULT_ROOT }
|
20
|
+
end
|
21
|
+
|
22
|
+
# Sets the default global zk root path.
|
23
|
+
def zk_root=(path)
|
24
|
+
@@mutex.synchronize { @@zk_root = path.dup.freeze }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.new(*args)
|
29
|
+
ZK::Group::Group.new(*args)
|
30
|
+
end
|
31
|
+
|
32
|
+
# The basis for forming different kinds of Groups with customizable
|
33
|
+
# memberhip policies.
|
34
|
+
class Group
|
35
|
+
extend Forwardable
|
36
|
+
include Logging
|
37
|
+
include Common
|
38
|
+
|
39
|
+
def_delegators :@mutex, :synchronize
|
40
|
+
protected :synchronize
|
41
|
+
|
42
|
+
# the ZK Client instance
|
43
|
+
attr_reader :zk
|
44
|
+
|
45
|
+
# the name for this group
|
46
|
+
attr_reader :name
|
47
|
+
|
48
|
+
# the absolute root path of this group, generally, this can be left at the default
|
49
|
+
attr_reader :root
|
50
|
+
|
51
|
+
# the combination of `"#{root}/#{name}"`
|
52
|
+
attr_reader :path
|
53
|
+
|
54
|
+
# @return [ZK::Stat] the stat from the last time we either set or retrieved
|
55
|
+
# data from the server.
|
56
|
+
# @private
|
57
|
+
attr_accessor :last_stat
|
58
|
+
|
59
|
+
# Prefix used for creating sequential nodes under {#path} that represent membership.
|
60
|
+
# The default is 'm', so for the path `/_zk/groups/foo` a member path would look like
|
61
|
+
# `/zkgroups/foo/m000000078`
|
62
|
+
#
|
63
|
+
# @return [String] the prefix
|
64
|
+
attr_accessor :prefix
|
65
|
+
|
66
|
+
def initialize(zk, name, opts={})
|
67
|
+
@orig_zk = zk
|
68
|
+
@zk = GroupExceptionTranslator.new(zk, self)
|
69
|
+
|
70
|
+
raise ArgumentError, "name must not be nil" unless name
|
71
|
+
|
72
|
+
@name = name.to_s
|
73
|
+
@root = opts[:root] || ZK::Group.zk_root
|
74
|
+
@prefix = opts[:prefix] || DEFAULT_PREFIX
|
75
|
+
@path = File.join(@root, @name)
|
76
|
+
@mutex = Monitor.new
|
77
|
+
@created = false
|
78
|
+
|
79
|
+
@known_members = []
|
80
|
+
@membership_subscriptions = []
|
81
|
+
|
82
|
+
# ThreadedCallback will queue calls to the block and deliver them one at a time
|
83
|
+
# on their own thread. This guarantees order and simplifies locking.
|
84
|
+
@broadcast_callback = ThreadedCallback.new { |event| broadcast_membership_change!(event) }
|
85
|
+
|
86
|
+
@membership_ev_sub = zk.register(path, :only => :child) do |event|
|
87
|
+
@broadcast_callback.call(event)
|
88
|
+
end
|
89
|
+
|
90
|
+
@on_connected_sub = zk.on_connected do |event|
|
91
|
+
@broadcast_callback.call(event)
|
92
|
+
end
|
93
|
+
|
94
|
+
validate!
|
95
|
+
end
|
96
|
+
|
97
|
+
# stop receiving event notifications, tracking membership changes, etc.
|
98
|
+
# XXX: what about memberships?
|
99
|
+
def close
|
100
|
+
synchronize do
|
101
|
+
return unless @created
|
102
|
+
@created = false
|
103
|
+
|
104
|
+
@broadcast_callback.shutdown
|
105
|
+
|
106
|
+
@on_connected_sub.unsubscribe
|
107
|
+
@membership_ev_sub.unsubscribe
|
108
|
+
|
109
|
+
@known_members.clear
|
110
|
+
@membership_subscriptions.each(&:unsubscribe)
|
111
|
+
@orig_zk.delete(@path, :ignore => [:no_node, :not_empty])
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# this is "are we set up" not "did *we* create the group"
|
116
|
+
def created?
|
117
|
+
synchronize { !!@created }
|
118
|
+
end
|
119
|
+
|
120
|
+
# does the group exist already?
|
121
|
+
def exists?
|
122
|
+
zk.exists?(path)
|
123
|
+
end
|
124
|
+
|
125
|
+
# creates this group, does not raise an exception if the group already
|
126
|
+
# exists.
|
127
|
+
#
|
128
|
+
# @return [String,nil] String containing the path of this group if
|
129
|
+
# created, nil if group already exists
|
130
|
+
#
|
131
|
+
# @overload create(}
|
132
|
+
# creates this group with empty data
|
133
|
+
#
|
134
|
+
# @overload create(data)
|
135
|
+
# creates this group with the given data. if the group already exists
|
136
|
+
# the data will not be written.
|
137
|
+
#
|
138
|
+
# @param [String] data the data to be set for this group
|
139
|
+
#
|
140
|
+
def create(*args)
|
141
|
+
synchronize do
|
142
|
+
begin
|
143
|
+
create!(*args)
|
144
|
+
rescue Exceptions::GroupAlreadyExistsError
|
145
|
+
# ok, this is a little odd, if you call create! and it fails, semantically
|
146
|
+
# in this method we're supposed to catch the exception and return. The problem
|
147
|
+
# is that the @known_members and @last_stat won't have been set up. we want
|
148
|
+
# both of these methods available, so we need to set that state up here, but
|
149
|
+
# only if create! fails in this particular way
|
150
|
+
@created = true
|
151
|
+
broadcast_membership_change!
|
152
|
+
nil
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# same as {#create} but raises an exception if the group already exists
|
158
|
+
#
|
159
|
+
# @raise [Exceptions::GroupAlreadyExistsError] if the group already exists
|
160
|
+
def create!(*args)
|
161
|
+
ensure_root_exists!
|
162
|
+
|
163
|
+
data = args.empty? ? '' : args.first
|
164
|
+
|
165
|
+
synchronize do
|
166
|
+
zk.create(path, data).tap do
|
167
|
+
logger.debug { "create!(#{path.inspect}, #{data.inspect}) succeeded, setting initial state" }
|
168
|
+
@created = true
|
169
|
+
broadcast_membership_change!
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
# Creates a Member object that represents 'belonging' to this group.
|
175
|
+
#
|
176
|
+
# The basic behavior is creating a unique path under the {#path} (using
|
177
|
+
# a sequential, ephemeral node).
|
178
|
+
#
|
179
|
+
# You may receive notification that the member was created before this method
|
180
|
+
# returns your Member instance. "heads up"
|
181
|
+
#
|
182
|
+
# @overload join(opts={})
|
183
|
+
# join the group and set the node's data to blank
|
184
|
+
#
|
185
|
+
# @option opts [Class] :member_class (ZK::Group::Member) an alternate
|
186
|
+
# class to manage membership in the group. if this is set to nil,
|
187
|
+
# no Member will be created and just the created path will be
|
188
|
+
# returned
|
189
|
+
#
|
190
|
+
# @overload join(data, opts={})
|
191
|
+
# join the group and set the node's initial data
|
192
|
+
#
|
193
|
+
# @option opts [Class] :member_class (ZK::Group::Member) an alternate
|
194
|
+
# class to manage membership in the group. If this is set to nil,
|
195
|
+
# no Member will be created and just the created path will be
|
196
|
+
# returned
|
197
|
+
#
|
198
|
+
# @param data [String] (nil) the data this node should have to start
|
199
|
+
# with, default is no data
|
200
|
+
#
|
201
|
+
# @return [Member] used to control a single member of the group
|
202
|
+
#
|
203
|
+
def join(*args)
|
204
|
+
# TODO: clarify how the membership interacts with the group. if you
|
205
|
+
# close the group what happens to the member?
|
206
|
+
opts = args.extract_options!
|
207
|
+
data = args.first || ''
|
208
|
+
member_class = opts.fetch(:member_class, Member)
|
209
|
+
member_path = zk.create("#{path}/#{prefix}", data, :sequence => true, :ephemeral => true)
|
210
|
+
member_class ? member_class.new(@orig_zk, self, member_path) : member_path
|
211
|
+
end
|
212
|
+
|
213
|
+
# returns the current list of member names, sorted.
|
214
|
+
#
|
215
|
+
# @option opts [true,false] :absolute (false) return member information
|
216
|
+
# as absolute znode paths.
|
217
|
+
#
|
218
|
+
# @option opts [true,false] :watch (true) causes a watch to be set on
|
219
|
+
# this group's znode for child changes. This will cause the on_membership_change
|
220
|
+
# callback to be triggered, when delivered.
|
221
|
+
#
|
222
|
+
def member_names(opts={})
|
223
|
+
watch = opts.fetch(:watch, true)
|
224
|
+
absolute = opts.fetch(:absolute, false)
|
225
|
+
|
226
|
+
zk.children(path, :watch => watch).sort.tap do |rval|
|
227
|
+
rval.map! { |n| File.join(path, n) } if absolute
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
# Register a block to be called back when the group membership changes.
|
232
|
+
#
|
233
|
+
# Notifications will be delivered concurrently (i.e. using the client's
|
234
|
+
# threadpool), but serially. In other words, when notification is
|
235
|
+
# delivered to us that the group membership has changed, we queue up
|
236
|
+
# notifications for all callbacks before handling the next event. This
|
237
|
+
# way each callback will see the same sequence of updates every other
|
238
|
+
# callback sees in order. They just may receive the notifications at
|
239
|
+
# different times.
|
240
|
+
#
|
241
|
+
# @note Due to the way ZooKeeper works, it's possible that you may not see every
|
242
|
+
# change to the membership of the group. That is *very* important to know.
|
243
|
+
# ZooKeeper _may batch updates_, so you can see a jump of members, especially
|
244
|
+
# if they're added very quickly. DO NOT assume you will receive a callback for _each
|
245
|
+
# individual membership added_.
|
246
|
+
#
|
247
|
+
# @options opts [true,false] :absolute (false) block will be called with members
|
248
|
+
# as absolute paths
|
249
|
+
#
|
250
|
+
# @yield [last_members,current_members] called when membership of the
|
251
|
+
# current group changes.
|
252
|
+
#
|
253
|
+
# @yieldparam [Array] last_members the last known membership list of the group
|
254
|
+
#
|
255
|
+
# @yieldparam [Array] current_members the list of members just retrieved from zookeeper
|
256
|
+
#
|
257
|
+
def on_membership_change(opts={}, &blk)
|
258
|
+
MembershipSubscription.new(self, opts, blk).tap do |ms|
|
259
|
+
# the watch is registered in create!
|
260
|
+
synchronize { @membership_subscriptions << ms }
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
# called by the MembershipSubscription object to deregister itself
|
265
|
+
# @private
|
266
|
+
def unregister(subscription)
|
267
|
+
synchronize do
|
268
|
+
@membership_subscriptions.delete(subscription)
|
269
|
+
end
|
270
|
+
nil
|
271
|
+
end
|
272
|
+
|
273
|
+
# @private
|
274
|
+
def broadcast_membership_change!(_ignored=nil)
|
275
|
+
synchronize do
|
276
|
+
logger.debug { "#{__method__} received event #{_ignored.inspect}" }
|
277
|
+
|
278
|
+
# we might get an on_connected event before creation
|
279
|
+
unless created?
|
280
|
+
logger.debug { "uh, created? #{created?} so returning" }
|
281
|
+
return
|
282
|
+
end
|
283
|
+
|
284
|
+
last_members, @known_members = @known_members, member_names(:watch => true)
|
285
|
+
|
286
|
+
logger.debug { "last_members: #{last_members.inspect}" }
|
287
|
+
logger.debug { "@known_members: #{@known_members.inspect}" }
|
288
|
+
|
289
|
+
# we do this check so that on a connected event, we can run this callback
|
290
|
+
# without producing false positives
|
291
|
+
#
|
292
|
+
if last_members == @known_members
|
293
|
+
logger.debug { "membership data did not actually change, not notifying" }
|
294
|
+
else
|
295
|
+
@membership_subscriptions.each do |sub|
|
296
|
+
lm, km = last_members.dup, @known_members.dup
|
297
|
+
sub.notify(lm, km)
|
298
|
+
end
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
private
|
304
|
+
# Creates a Member instance for this Group. This its own method to allow
|
305
|
+
# subclasses to override. By default, uses Member
|
306
|
+
def create_member(znode_path, member_klass)
|
307
|
+
logger.debug { "created member #{znode_path.inspect} returning object" }
|
308
|
+
member_klass.new(@orig_zk, self, znode_path)
|
309
|
+
end
|
310
|
+
|
311
|
+
def ensure_root_exists!
|
312
|
+
zk.mkdir_p(root)
|
313
|
+
end
|
314
|
+
|
315
|
+
def validate!
|
316
|
+
raise ArgumentError, "root must start with '/'" unless @root.start_with?('/')
|
317
|
+
end
|
318
|
+
end # Group
|
319
|
+
end # Group
|
320
|
+
end # ZK
|
321
|
+
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module ZK
|
2
|
+
module Group
|
3
|
+
class Member
|
4
|
+
include Common
|
5
|
+
|
6
|
+
attr_reader :zk
|
7
|
+
|
8
|
+
# @return [Group] the group instance this member belongs to
|
9
|
+
attr_reader :group
|
10
|
+
|
11
|
+
# @return [String] the relative path of this member under `group.path`
|
12
|
+
attr_reader :name
|
13
|
+
|
14
|
+
# @return [String] the absolute path of this member
|
15
|
+
attr_reader :path
|
16
|
+
|
17
|
+
def initialize(zk, group, path)
|
18
|
+
@zk = zk
|
19
|
+
@group = group
|
20
|
+
@path = path
|
21
|
+
@name = File.basename(@path)
|
22
|
+
@mutex = Mutex.new
|
23
|
+
end
|
24
|
+
|
25
|
+
# probably poor choice of name, but does this member still an active membership
|
26
|
+
# to its group (i.e. is its path still good).
|
27
|
+
#
|
28
|
+
# This will return false after leave is called.
|
29
|
+
def active?
|
30
|
+
zk.exists?(path)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Leave the group this membership is associated with.
|
34
|
+
# In the basic implementation, this is not meant to kick another member
|
35
|
+
# out of the group.
|
36
|
+
#
|
37
|
+
def leave
|
38
|
+
zk.delete(path)
|
39
|
+
end
|
40
|
+
|
41
|
+
def data
|
42
|
+
@data ||= zk.get(path).first
|
43
|
+
end
|
44
|
+
|
45
|
+
def data=(data)
|
46
|
+
@data = data
|
47
|
+
zk.set(path, data)
|
48
|
+
data
|
49
|
+
end
|
50
|
+
end # Member
|
51
|
+
end # Group
|
52
|
+
end # ZK
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module ZK
|
2
|
+
module Group
|
3
|
+
class MembershipSubscription < ZK::Subscription::Base
|
4
|
+
include ZK::Logging
|
5
|
+
|
6
|
+
attr_reader :opts
|
7
|
+
|
8
|
+
alias group parent
|
9
|
+
|
10
|
+
def initialize(group, opts, block)
|
11
|
+
super(group, block)
|
12
|
+
@opts = opts
|
13
|
+
end
|
14
|
+
|
15
|
+
def notify(last_members, current_members)
|
16
|
+
# XXX: implement this in here for now, but for very large membership lists
|
17
|
+
# it would likely be more efficient to implement this in the caller
|
18
|
+
if absolute_paths?
|
19
|
+
group_path = group.path
|
20
|
+
|
21
|
+
last_members = last_members.map { |m| File.join(group_path, m) }
|
22
|
+
current_members = current_members.map { |m| File.join(group_path, m) }
|
23
|
+
end
|
24
|
+
|
25
|
+
call(last_members, current_members)
|
26
|
+
end
|
27
|
+
|
28
|
+
def absolute_paths?
|
29
|
+
opts[:absolute]
|
30
|
+
end
|
31
|
+
|
32
|
+
protected :call
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'rspec/core/formatters/progress_formatter'
|
2
|
+
|
3
|
+
# essentially a monkey-patch to the ProgressBarFormatter, outputs
|
4
|
+
# '== #{example_proxy.description} ==' in the logs before each test. makes it
|
5
|
+
# easier to match up tests with the SQL they produce
|
6
|
+
class LoggingProgressBarFormatter < RSpec::Core::Formatters::ProgressFormatter
|
7
|
+
def example_started(example)
|
8
|
+
::Logging.logger['spec'].write(yellow("\n=====<([ #{example.full_description} ])>=====\n"))
|
9
|
+
super
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
@@ -0,0 +1,14 @@
|
|
1
|
+
shared_context 'connections' do
|
2
|
+
let(:connection_opts) { ['localhost:2181', {:thread => :per_callback}] }
|
3
|
+
|
4
|
+
before do
|
5
|
+
@zk = ZK.new(*connection_opts)
|
6
|
+
@base_path = '/zk-group'
|
7
|
+
@zk.rm_rf(@base_path)
|
8
|
+
end
|
9
|
+
|
10
|
+
after do
|
11
|
+
@zk.close! unless @zk.closed?
|
12
|
+
ZK.open(*connection_opts) { |zk| zk.rm_rf(@base_path) }
|
13
|
+
end
|
14
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler/setup'
|
3
|
+
|
4
|
+
Bundler.require(:development, :test)
|
5
|
+
|
6
|
+
require 'zk-group'
|
7
|
+
require 'benchmark'
|
8
|
+
|
9
|
+
# Requires supporting ruby files with custom matchers and macros, etc,
|
10
|
+
# in spec/support/ and its subdirectories.
|
11
|
+
Dir[File.expand_path("../{support,shared}/**/*.rb", __FILE__)].each {|f| require f}
|
12
|
+
|
13
|
+
RSpec.configure do |config|
|
14
|
+
config.mock_with :rspec
|
15
|
+
|
16
|
+
config.include(WaitWatchers)
|
17
|
+
config.extend(WaitWatchers)
|
18
|
+
|
19
|
+
config.include(SpecGlobalLogger)
|
20
|
+
config.extend(SpecGlobalLogger)
|
21
|
+
end
|
22
|
+
|
23
|
+
class ::Thread
|
24
|
+
# join with thread until given block is true, the thread joins successfully,
|
25
|
+
# or timeout seconds have passed
|
26
|
+
#
|
27
|
+
def join_until(timeout=2)
|
28
|
+
time_to_stop = Time.now + timeout
|
29
|
+
|
30
|
+
until yield
|
31
|
+
break if Time.now > time_to_stop
|
32
|
+
break if join(0)
|
33
|
+
Thread.pass
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def join_while(timeout=2)
|
38
|
+
time_to_stop = Time.now + timeout
|
39
|
+
|
40
|
+
while yield
|
41
|
+
break if Time.now > time_to_stop
|
42
|
+
break if join(0)
|
43
|
+
Thread.pass
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module ZK
|
2
|
+
LOG_FILE = File.expand_path('../../../test.log', __FILE__)
|
3
|
+
end
|
4
|
+
|
5
|
+
ZK.logger = Logger.new(ZK::LOG_FILE).tap { |log| log.level = Logger::DEBUG }
|
6
|
+
|
7
|
+
# Zookeeper.logger = ZK.logger
|
8
|
+
# Zookeeper.set_debug_level(4)
|
9
|
+
|
10
|
+
ZK.logger.debug { "LOG OPEN" }
|
11
|
+
|
12
|
+
module SpecGlobalLogger
|
13
|
+
def logger
|
14
|
+
ZK.logger
|
15
|
+
end
|
16
|
+
|
17
|
+
# sets the log level to FATAL for the duration of the block
|
18
|
+
def mute_logger
|
19
|
+
orig_level, ZK.logger.level = ZK.logger.level, Logger::FATAL
|
20
|
+
orig_zk_level, Zookeeper.debug_level = Zookeeper.debug_level, ZookeeperConstants::ZOO_LOG_LEVEL_ERROR
|
21
|
+
yield
|
22
|
+
ensure
|
23
|
+
ZK.logger.level = orig_level
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module WaitWatchers
|
2
|
+
class TimeoutError < StandardError; end
|
3
|
+
|
4
|
+
# method to wait until block passed returns truthy (false will not work) or
|
5
|
+
# timeout (default is 2 seconds) is reached raises TiemoutError on timeout
|
6
|
+
#
|
7
|
+
# @returns the truthy value
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
#
|
11
|
+
# @a = nil
|
12
|
+
#
|
13
|
+
# th = Thread.new do
|
14
|
+
# sleep(1)
|
15
|
+
# @a = :fudge
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# wait_until(2) { @a }.should == :fudge
|
19
|
+
#
|
20
|
+
def wait_until(timeout=2)
|
21
|
+
time_to_stop = Time.now + timeout
|
22
|
+
while true
|
23
|
+
rval = yield
|
24
|
+
return rval if rval
|
25
|
+
raise TimeoutError, "timeout of #{timeout}s exceeded" if Time.now > time_to_stop
|
26
|
+
Thread.pass
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# inverse of wait_until
|
31
|
+
def wait_while(timeout=2)
|
32
|
+
time_to_stop = Time.now + timeout
|
33
|
+
while true
|
34
|
+
rval = yield
|
35
|
+
return rval unless rval
|
36
|
+
raise TimeoutError, "timeout of #{timeout}s exceeded" if Time.now > time_to_stop
|
37
|
+
Thread.pass
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def report_realtime(what)
|
42
|
+
return yield
|
43
|
+
t = Benchmark.realtime { yield }
|
44
|
+
$stderr.puts "#{what}: %0.3f" % [t.to_f]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
|
@@ -0,0 +1,202 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ZK::Group::Group do
|
4
|
+
include_context 'connections'
|
5
|
+
|
6
|
+
let(:group_name) { 'the_mothers' }
|
7
|
+
let(:group_data) { 'of invention' }
|
8
|
+
let(:member_names) { %w[zappa jcb collins estrada underwood] }
|
9
|
+
|
10
|
+
subject { described_class.new(@zk, group_name, :root => @base_path) }
|
11
|
+
after { subject.close }
|
12
|
+
|
13
|
+
describe :create do
|
14
|
+
it %[should create the group with empty data] do
|
15
|
+
subject.create
|
16
|
+
@zk.stat(subject.path).should be_exist
|
17
|
+
end
|
18
|
+
|
19
|
+
it %[should create the group with specified data] do
|
20
|
+
subject.create(group_data)
|
21
|
+
@zk.get(subject.path).first.should == group_data
|
22
|
+
end
|
23
|
+
|
24
|
+
it %[should return nil if the group is not created] do
|
25
|
+
@zk.mkdir_p(subject.path)
|
26
|
+
subject.create.should be_nil
|
27
|
+
end
|
28
|
+
end # create
|
29
|
+
|
30
|
+
describe :exists? do
|
31
|
+
it %[should return false if the group has not been created] do
|
32
|
+
@zk.stat(subject.path).should_not exist
|
33
|
+
subject.should_not exist
|
34
|
+
end
|
35
|
+
|
36
|
+
it %[should return true if the group has been created] do
|
37
|
+
subject.create
|
38
|
+
@zk.stat(subject.path).should exist
|
39
|
+
subject.should exist
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe :create! do
|
44
|
+
it %[should raise GroupAlreadyExistsError if the group already exists] do
|
45
|
+
@zk.mkdir_p(subject.path)
|
46
|
+
lambda { subject.create! }.should raise_error(ZK::Exceptions::GroupAlreadyExistsError)
|
47
|
+
end
|
48
|
+
end # create!
|
49
|
+
|
50
|
+
describe :data do
|
51
|
+
it %[should return the group's data] do
|
52
|
+
@zk.mkdir_p(subject.path)
|
53
|
+
@zk.set(subject.path, group_data)
|
54
|
+
subject.data.should == group_data
|
55
|
+
end
|
56
|
+
end # data
|
57
|
+
|
58
|
+
describe :data= do
|
59
|
+
it %[should set the group's data] do
|
60
|
+
@zk.mkdir_p(subject.path)
|
61
|
+
subject.data = group_data
|
62
|
+
@zk.get(subject.path).first == group_data
|
63
|
+
end
|
64
|
+
end # data=
|
65
|
+
|
66
|
+
describe :member_names do
|
67
|
+
before do
|
68
|
+
member_names.each do |name|
|
69
|
+
@zk.mkdir_p("#{subject.path}/#{name}")
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
it %[should return a list of relative znode paths that belong to the group] do
|
74
|
+
subject.member_names.should == member_names.sort
|
75
|
+
end
|
76
|
+
|
77
|
+
it %[should return a list of absolute znode paths that belong to the group when :absolute => true is given] do
|
78
|
+
subject.member_names(:absolute => true).should == member_names.sort.map {|n| "#{subject.path}/#{n}" }
|
79
|
+
end
|
80
|
+
end # member_names
|
81
|
+
|
82
|
+
describe :join do
|
83
|
+
it %[should raise GroupDoesNotExistError if the group has not been created already] do
|
84
|
+
lambda { subject.join }.should raise_error(ZK::Exceptions::GroupDoesNotExistError)
|
85
|
+
end
|
86
|
+
|
87
|
+
it %[should return a Member object if the join succeeds] do
|
88
|
+
subject.create!
|
89
|
+
subject.join.should be_kind_of(ZK::Group::Member)
|
90
|
+
end
|
91
|
+
|
92
|
+
it %[should return just the path if :member_class => nil] do
|
93
|
+
subject.create!
|
94
|
+
|
95
|
+
subject.join(:member_class => nil).should match(%r%^#{subject.path}%)
|
96
|
+
end
|
97
|
+
end # join
|
98
|
+
|
99
|
+
describe :on_membership_change do
|
100
|
+
before { @events = [] }
|
101
|
+
|
102
|
+
describe 'with default options' do
|
103
|
+
before do
|
104
|
+
subject.on_membership_change do |old,cur|
|
105
|
+
@events << [old,cur]
|
106
|
+
end
|
107
|
+
|
108
|
+
subject.create!
|
109
|
+
end
|
110
|
+
|
111
|
+
it %[should return an object with an unsubscribe method] do
|
112
|
+
sub = subject.on_membership_change { |old,cur| }
|
113
|
+
|
114
|
+
sub.should respond_to(:unsubscribe)
|
115
|
+
end
|
116
|
+
|
117
|
+
it %[should deliver when the membership changes] do
|
118
|
+
subject.should be_created
|
119
|
+
|
120
|
+
member = subject.join
|
121
|
+
wait_while { @events.empty? }
|
122
|
+
@events.length.should == 1
|
123
|
+
|
124
|
+
old, cur = @events.first
|
125
|
+
|
126
|
+
old.should be_empty
|
127
|
+
cur.length.should == 1
|
128
|
+
cur.first.should match(/\Am\d+\Z/)
|
129
|
+
end
|
130
|
+
|
131
|
+
it %[should deliver notification when a member joins or leaves] do
|
132
|
+
subject.should be_created
|
133
|
+
|
134
|
+
members = []
|
135
|
+
|
136
|
+
10.times { members << subject.join }
|
137
|
+
|
138
|
+
# wait until we've received notification that includes our last created member
|
139
|
+
wait_until { @events.last.last.length == 10 }
|
140
|
+
|
141
|
+
# there should be a difference in the size of the group
|
142
|
+
# this won't always be true, but in this case it should be
|
143
|
+
@events.each do |old,cur|
|
144
|
+
old.length.should < cur.length
|
145
|
+
end
|
146
|
+
|
147
|
+
@events.clear
|
148
|
+
|
149
|
+
members.each { |m| m.leave }
|
150
|
+
|
151
|
+
# wait until we've received notification that includes our last created member
|
152
|
+
wait_until { @events.last.last.empty? }
|
153
|
+
|
154
|
+
@events.each do |old,cur|
|
155
|
+
old.length.should > cur.length
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
it %[should deliver all events to all listeners in order] do
|
160
|
+
other_events = []
|
161
|
+
mutex = Monitor.new
|
162
|
+
offset = 10
|
163
|
+
saw_six = false
|
164
|
+
|
165
|
+
subject.on_membership_change do |old,cur|
|
166
|
+
num = mutex.synchronize { (offset -= 1) + 1 }
|
167
|
+
sleep(num * 0.005)
|
168
|
+
other_events << [old,cur]
|
169
|
+
mutex.synchronize { saw_six = (cur.length == 6) }
|
170
|
+
end
|
171
|
+
|
172
|
+
6.times { subject.join }
|
173
|
+
|
174
|
+
wait_until { @events.last.last.length == 6 }
|
175
|
+
wait_until { @events.length == other_events.length }
|
176
|
+
|
177
|
+
@events.should == other_events
|
178
|
+
end # in order
|
179
|
+
end # default options
|
180
|
+
|
181
|
+
describe 'with :absolute => true' do
|
182
|
+
before do
|
183
|
+
subject.on_membership_change(:absolute => true) do |old,cur|
|
184
|
+
@events << [old,cur]
|
185
|
+
end
|
186
|
+
|
187
|
+
subject.create!
|
188
|
+
end
|
189
|
+
|
190
|
+
it %[should deliver the members as absolute paths if the :absolute option is true] do
|
191
|
+
member = subject.join
|
192
|
+
|
193
|
+
wait_while { @events.empty? }
|
194
|
+
@events.length.should == 1
|
195
|
+
|
196
|
+
old, cur = @events.first
|
197
|
+
|
198
|
+
cur.first.should start_with('/')
|
199
|
+
end
|
200
|
+
end # absolute
|
201
|
+
end # on_membership_changed
|
202
|
+
end # ZK::Group::Base
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ZK::Group::Member do
|
4
|
+
include_context 'connections'
|
5
|
+
|
6
|
+
let(:group_name) { 'the_mothers' }
|
7
|
+
let(:group) do
|
8
|
+
double(
|
9
|
+
name: group_name,
|
10
|
+
root: @base_path,
|
11
|
+
path: "#{ZK::Group::DEFAULT_ROOT}/#{group_name}"
|
12
|
+
)
|
13
|
+
end
|
14
|
+
|
15
|
+
let(:member_data) { "LA DI *FREAKIN* DA!" }
|
16
|
+
|
17
|
+
let(:member_path) do
|
18
|
+
@zk.mkdir_p group.path
|
19
|
+
@zk.create("#{group.path}/#{ZK::Group::DEFAULT_PREFIX}", member_data, ephemeral: true, sequential: true)
|
20
|
+
end
|
21
|
+
|
22
|
+
subject { described_class.new(@zk, group, member_path) }
|
23
|
+
|
24
|
+
describe :active? do
|
25
|
+
it %[should return true if the path exists] do
|
26
|
+
@zk.stat(member_path).should exist
|
27
|
+
subject.should be_active
|
28
|
+
end
|
29
|
+
|
30
|
+
it %[should return false if the path does not exist] do
|
31
|
+
@zk.delete(member_path)
|
32
|
+
subject.should_not be_active
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe :leave do
|
37
|
+
it %[should delete the underlying path] do
|
38
|
+
subject.leave
|
39
|
+
@zk.stat(member_path).should_not exist
|
40
|
+
end
|
41
|
+
|
42
|
+
it %[should not be active after leave] do
|
43
|
+
subject.leave
|
44
|
+
subject.should_not be_active
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe :data do
|
49
|
+
it %[should return the data in the member's node] do
|
50
|
+
subject.data.should == member_data
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
describe :data= do
|
55
|
+
it %[should set the data for the member's node] do
|
56
|
+
subject.data = "new data"
|
57
|
+
@zk.get(member_path).first.should == 'new data'
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
data/zk-group.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/zk-group/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.authors = ["Jonathan D. Simms"]
|
6
|
+
s.email = ["slyphon@gmail.com"]
|
7
|
+
s.description = %q{A Group abstraction on top of the high-level ZooKeeper library ZK}
|
8
|
+
s.summary = %q{
|
9
|
+
Provides Group-like behaviors such as listing members of a group, joining,
|
10
|
+
leaving, and notifications when group memberhsip changes
|
11
|
+
|
12
|
+
Part of the ZK project.
|
13
|
+
}
|
14
|
+
s.homepage = "https://github.com/slyphon/zk-group"
|
15
|
+
|
16
|
+
s.add_runtime_dependency 'zk', '~> 1.6.0'
|
17
|
+
|
18
|
+
s.files = `git ls-files`.split($\)
|
19
|
+
s.executables = s.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
20
|
+
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
21
|
+
s.name = "zk-group"
|
22
|
+
s.require_paths = ["lib"]
|
23
|
+
s.version = ZK::Group::VERSION
|
24
|
+
end
|
metadata
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: zk-group
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 25
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 1
|
10
|
+
version: 0.1.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Jonathan D. Simms
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2012-08-21 00:00:00 Z
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: zk
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ~>
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 15
|
29
|
+
segments:
|
30
|
+
- 1
|
31
|
+
- 6
|
32
|
+
- 0
|
33
|
+
version: 1.6.0
|
34
|
+
type: :runtime
|
35
|
+
version_requirements: *id001
|
36
|
+
description: A Group abstraction on top of the high-level ZooKeeper library ZK
|
37
|
+
email:
|
38
|
+
- slyphon@gmail.com
|
39
|
+
executables: []
|
40
|
+
|
41
|
+
extensions: []
|
42
|
+
|
43
|
+
extra_rdoc_files: []
|
44
|
+
|
45
|
+
files:
|
46
|
+
- .dotfiles/rspec-logging
|
47
|
+
- .dotfiles/rvmrc
|
48
|
+
- .gitignore
|
49
|
+
- .gitmodules
|
50
|
+
- Gemfile
|
51
|
+
- Guardfile
|
52
|
+
- MIT_LICENSE
|
53
|
+
- README.md
|
54
|
+
- Rakefile
|
55
|
+
- lib/zk-group.rb
|
56
|
+
- lib/zk-group/exceptions.rb
|
57
|
+
- lib/zk-group/group.rb
|
58
|
+
- lib/zk-group/member.rb
|
59
|
+
- lib/zk-group/membership_subscription.rb
|
60
|
+
- lib/zk-group/version.rb
|
61
|
+
- spec/logging_progress_bar_formatter.rb
|
62
|
+
- spec/shared/client_contexts.rb
|
63
|
+
- spec/spec_helper.rb
|
64
|
+
- spec/support/custom_matchers.rb
|
65
|
+
- spec/support/exist_matcher.rb
|
66
|
+
- spec/support/logging.rb
|
67
|
+
- spec/support/wait_watchers.rb
|
68
|
+
- spec/zk-group/group_spec.rb
|
69
|
+
- spec/zk-group/member_spec.rb
|
70
|
+
- zk-group.gemspec
|
71
|
+
homepage: https://github.com/slyphon/zk-group
|
72
|
+
licenses: []
|
73
|
+
|
74
|
+
post_install_message:
|
75
|
+
rdoc_options: []
|
76
|
+
|
77
|
+
require_paths:
|
78
|
+
- lib
|
79
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
80
|
+
none: false
|
81
|
+
requirements:
|
82
|
+
- - ">="
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
hash: 3
|
85
|
+
segments:
|
86
|
+
- 0
|
87
|
+
version: "0"
|
88
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ">="
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
hash: 3
|
94
|
+
segments:
|
95
|
+
- 0
|
96
|
+
version: "0"
|
97
|
+
requirements: []
|
98
|
+
|
99
|
+
rubyforge_project:
|
100
|
+
rubygems_version: 1.8.24
|
101
|
+
signing_key:
|
102
|
+
specification_version: 3
|
103
|
+
summary: Provides Group-like behaviors such as listing members of a group, joining, leaving, and notifications when group memberhsip changes Part of the ZK project.
|
104
|
+
test_files:
|
105
|
+
- spec/logging_progress_bar_formatter.rb
|
106
|
+
- spec/shared/client_contexts.rb
|
107
|
+
- spec/spec_helper.rb
|
108
|
+
- spec/support/custom_matchers.rb
|
109
|
+
- spec/support/exist_matcher.rb
|
110
|
+
- spec/support/logging.rb
|
111
|
+
- spec/support/wait_watchers.rb
|
112
|
+
- spec/zk-group/group_spec.rb
|
113
|
+
- spec/zk-group/member_spec.rb
|