zk-group 0.1.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/.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
|