zk-group 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,4 @@
1
+ --color
2
+ --require ./spec/logging_progress_bar_formatter.rb
3
+ --format LoggingProgressBarFormatter
4
+
@@ -0,0 +1,2 @@
1
+ rvm 1.9.3@zk-group --create
2
+
@@ -0,0 +1,20 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .rspec
19
+ .rvmrc
20
+ *.log
@@ -0,0 +1,3 @@
1
+ [submodule "releaseops"]
2
+ path = releaseops
3
+ url = git@github.com:slyphon/releaseops.git
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
+
@@ -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
+
@@ -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.
@@ -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
@@ -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
+
@@ -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,5 @@
1
+ module ZK
2
+ module Group
3
+ VERSION = "0.1.1"
4
+ end
5
+ end
@@ -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
@@ -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,12 @@
1
+ RSpec::Matchers.define :exist do
2
+ match do |actual|
3
+ actual.exists?
4
+ end
5
+ end
6
+
7
+ RSpec::Matchers.define :start_with do |expected|
8
+ match do |actual|
9
+ actual.start_with?(expected)
10
+ end
11
+ end
12
+
@@ -0,0 +1,6 @@
1
+ RSpec::Matchers.define :exist do
2
+ match do |actual|
3
+ actual.exists?
4
+ end
5
+ end
6
+
@@ -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
+
@@ -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