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