Merge lp:~juju/txzookeeper/basic-node-api into lp:txzookeeper

Proposed by Kapil Thangavelu
Status: Merged
Merged at revision: 22
Proposed branch: lp:~juju/txzookeeper/basic-node-api
Merge into: lp:txzookeeper
Diff against target: 554 lines (+545/-0)
2 files modified
txzookeeper/node.py (+229/-0)
txzookeeper/tests/test_node.py (+316/-0)
To merge this branch: bzr merge lp:~juju/txzookeeper/basic-node-api
Reviewer Review Type Date Requested Status
Gustavo Niemeyer Approve
Review via email: mp+25465@code.launchpad.net

Description of the change

a minimal node abstraction api, with get/set data/acl and node iteration.

To post a comment you must log in.
Revision history for this message
Gustavo Niemeyer (niemeyer) wrote :

Just for the record, as we discussed over the phone:

[1]

The notification abstractions hide a bit the intention behind the way that the ZooKeeper API is designed: watchers are set up on data retrieval because it only makes sense to be told about changes once you know the existing value, and once changes happen the client must ask again about the value and reset the notification if it's still interested in knowing about changes.

As a consequence of this model, fast subsequent changes won't create a swarm effect, because all the clients are told about is that the value isn't the same they originally knew about anymore, but more than one change may have happened, and that's still alright.

With that in mind, I suggest having something like this, in addition to get_data() and get_children():

    def get_data_and_watch(self):
        (...)
        return data_deferred, watch_deferred

    def get_children_and_watch(self):
        (...)
        return children_deferred, watch_deferred

This uses twisted deferreds as well.

[2]

This isn't about a specific action required.. just a feeling I had.

I understand the intention behind the internal keeping of the node versioning, and I think it's a good model to start with. It's just not entirely clear to me how we'll want these things to behave in practice, but we'll certainly figure it out.

review: Needs Fixing
lp:~juju/txzookeeper/basic-node-api updated
19. By Kapil Thangavelu

node - remove subscriptions methods, add additional data accessors which also return on change deferreds

20. By Kapil Thangavelu

re-enable the subscription tests as data accessors with watches, add an additional test for bad version error handling.

21. By Kapil Thangavelu

rename exists_with_watch to exists_and_watch for consistency.

Revision history for this message
Kapil Thangavelu (hazmat) wrote :

On Wed, 19 May 2010 05:43:22 -0400, Gustavo Niemeyer
<email address hidden> wrote:

> Review: Needs Fixing
> Just for the record, as we discussed over the phone:
>
> [1]
>
> The notification abstractions hide a bit the intention behind the way
> that the ZooKeeper API is designed: watchers are set up on data
> retrieval because it only makes sense to be told about changes once you
> know the existing value, and once changes happen the client must ask
> again about the value and reset the notification if it's still
> interested in knowing about changes.
>
> As a consequence of this model, fast subsequent changes won't create a
> swarm effect, because all the clients are told about is that the value
> isn't the same they originally knew about anymore, but more than one
> change may have happened, and that's still alright.
>
> With that in mind, I suggest having something like this, in addition to
> get_data() and get_children():
>
> def get_data_and_watch(self):
> (...)
> return data_deferred, watch_deferred
>
> def get_children_and_watch(self):
> (...)
> return children_deferred, watch_deferred
>
> This uses twisted deferreds as well.

i've gone and ahead and switched the node implementation to utilize the
accesssors with explicit watches. i ended up adding an additional one for
exists_and_watch, as its nesc. to detect node creation.

>
> [2]
>
> This isn't about a specific action required.. just a feeling I had.
>
> I understand the intention behind the internal keeping of the node
> versioning, and I think it's a good model to start with. It's just not
> entirely clear to me how we'll want these things to behave in practice,
> but we'll certainly figure it out.
>

best usage is still tbd but there should be some value to the abstraction
layer else we're just using the client api. i think we'll end up
revisiting with some more experience.

cheers,

Kapil

Revision history for this message
Kapil Thangavelu (hazmat) wrote :

On Wed, 19 May 2010 05:43:22 -0400, Gustavo Niemeyer
<email address hidden> wrote:

> Review: Needs Fixing
> Just for the record, as we discussed over the phone:
>
> [1]
>
> The notification abstractions hide a bit the intention behind the way
> that the ZooKeeper API is designed: watchers are set up on data
> retrieval because it only makes sense to be told about changes once you
> know the existing value, and once changes happen the client must ask
> again about the value and reset the notification if it's still
> interested in knowing about changes.
>
> As a consequence of this model, fast subsequent changes won't create a
> swarm effect, because all the clients are told about is that the value
> isn't the same they originally knew about anymore, but more than one
> change may have happened, and that's still alright.
>
> With that in mind, I suggest having something like this, in addition to
> get_data() and get_children():
>
> def get_data_and_watch(self):
> (...)
> return data_deferred, watch_deferred
>
> def get_children_and_watch(self):
> (...)
> return children_deferred, watch_deferred
>
> This uses twisted deferreds as well.

I've gone ahead and implemented the data accessors with watches.
>
> [2]
>
> This isn't about a specific action required.. just a feeling I had.
>
> I understand the intention behind the internal keeping of the node
> versioning, and I think it's a good model to start with. It's just not
> entirely clear to me how we'll want these things to behave in practice,
> but we'll certainly figure it out.

noted. we'll revisit based on experience.

Revision history for this message
Gustavo Niemeyer (niemeyer) wrote :

[3]

Thanks for the watch implementation. It looks sensible.

As we discussed, one thing I wonder is if we should change the watch parameter into an Event object with "node" (the object, rather than the path), "kind", and "status". Might potentially make it more comfortable to consume the event.

[4]

+ def __cmp__(self, other):
+ return cmp(self.path, other.path)

This should be turned into __eq__ I believe, otherwise we'll get weird calls like node1 < node2 working in an arbitrary way, when they should error out.

[5]

+ if self._node_exists:
+ # if the node is deleted while we have this cached state we should
+ # covert the set to a create. XXX ??? Should we

On a second look, this starts to smell a bit strange. If we look at the algorithms created around ZooKeeper, they're generally very sensitive about when things are created, changed, deleted, etc. Having our Node API doing things behind the programmer's back feels like a hole we shouldn't get into. It reminds me of the sqlite Python wrapper, which does a lot of "smart" magic with the transaction, and as a side effect turns transactions into something unreliable (which is very ironic).

I understand and agree that having a create() method in a Node object which already exists may feel a bit weird from an OO perspective, but I really think we should go towards this way if we want to make the logic being created on top of this really easy to follow.

If we don't do this, we'll always to have to keep in mind "Ohh, this method call actually *creates* the node too!" when thinking of higher-level algorithms on top of ZooKeeper, which feels undesirable.

What do you think?

review: Needs Fixing
lp:~juju/txzookeeper/basic-node-api updated
22. By Kapil Thangavelu

use failure trap for excpetion handlers, properties for node name&path.

23. By Kapil Thangavelu

explicit create method, explicit errors on get data, and set data on non existant nodes.

24. By Kapil Thangavelu

node no longer tracks existance, just last node version stat

25. By Kapil Thangavelu

node event object to aggregate watch callback values

26. By Kapil Thangavelu

node event encapsulates node object that the watch was fired on

Revision history for this message
Kapil Thangavelu (hazmat) wrote :

[3] There's now an event object wrapping the watch callback values along with the node object.

[4] This is for sorting node collections, and is deterministic. It might be uintended in the node1 < node2 case. Alternative would be either providing a list compare function in the module, or letting users define one. Providing it on the class directly depends on how common the sorting nodes would be vs. confusion on other __cmp__ usages.

[5] All the implicit stuff has been yanked except the last version tracking. There is now an explicit create method on the node.

lp:~juju/txzookeeper/basic-node-api updated
27. By Kapil Thangavelu

test for nodeevent repr

Revision history for this message
Gustavo Niemeyer (niemeyer) wrote :

Nice improvements again, thank you!

Only one comment, and +1!

[6]

+ def __init__(self, event_type, connection_state, node):
+ self.type = event_type
+ self.connection_state = connection_state
+ self.node = node
+ self.path = node.path
+
+ @property
+ def kind(self):
+ return self.kind_map[self.type]

It'd be nice to have the naming convention consistent here. event.type returns an integer, and event.kind returns a string, but they both mean exactly the same thing with a different data type. As a side effect, a few weeks from now it will be hard to remember whether the string is the type or the kind. I suggest something like kind and kind_name, or type and type_name.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'txzookeeper/node.py'
--- txzookeeper/node.py 1970-01-01 00:00:00 +0000
+++ txzookeeper/node.py 2010-06-11 16:18:25 +0000
@@ -0,0 +1,229 @@
1from zookeeper import NoNodeException, BadVersionException
2from twisted.internet.defer import Deferred
3from txzookeeper.client import ZOO_OPEN_ACL_UNSAFE
4
5
6class NodeEvent(object):
7 """
8 A node event is returned when a watch deferred fires. It denotes some event
9 on the zookeeper node that the watch was requested on.
10
11 @ivar path: Path to the node the event was about.
12
13 @ivar type: An integer corresponding to the event type. The symbolic
14 name mapping is available from the zookeeper module attributes. For
15 convience one is included on the C{NodeEvent} class.
16
17 @ivar kind: A symbolic name for the event's type.
18
19 @ivar connection_state: integer representing the state of the
20 zookeeper connection.
21 """
22
23 __slots__ = ('type', 'connection_state', 'path', 'node')
24
25 kind_map = {
26 1: 'created',
27 2: 'deleted',
28 3: 'changed',
29 4: 'child'}
30
31 def __init__(self, event_type, connection_state, node):
32 self.type = event_type
33 self.connection_state = connection_state
34 self.node = node
35 self.path = node.path
36
37 @property
38 def kind(self):
39 return self.kind_map[self.type]
40
41 def __repr__(self):
42 return "<NodeEvent %s at %r>"%(self.kind, self.path)
43
44
45class ZNode(object):
46 """
47 A minimal object abstraction over a zookeeper node, utilizes no caching
48 outside of trying to keep track of node existance and the last read
49 version. It will attempt to utilize the last read version when modifying
50 the node. On a bad version exception this values are cleared and the
51 except reraised (errback chain continue.)
52 """
53
54 def __init__(self, path, context):
55 self._path = path
56 self._name = path.split("/")[-1]
57 self._context = context
58 self._node_stat = None
59
60 @property
61 def path(self):
62 """
63 The path to the node from the zookeeper root.
64 """
65 return self._path
66
67 @property
68 def name(self):
69 """
70 The name of the node in its container.
71 """
72 return self._name
73
74 def _get_version(self):
75 if not self._node_stat:
76 return -1
77 return self._node_stat["version"]
78
79 def _on_error_bad_version(self, failure):
80 failure.trap(BadVersionException)
81 self._node_stat = None
82 return failure
83
84 def create(self, data="", acl=None, flags=0):
85 """
86 Create the node with the given data, acl, and persistence flags. If no
87 acl is given the default public acl is used. The default creation flag
88 is persistent.
89 """
90 if acl is None:
91 acl = [ZOO_OPEN_ACL_UNSAFE]
92 d = self._context.create(self.path, data, acl, flags)
93 return d
94
95 def _on_exists_success(self, node_stat):
96 if node_stat is None:
97 return False
98 self._node_stat = node_stat
99 return True
100
101 def exists(self):
102 """
103 Does the node exist or not, returns a boolean.
104 """
105 d = self._context.exists(self.path)
106 d.addCallback(self._on_exists_success)
107 return d
108
109 def exists_and_watch(self):
110 """
111 Returns a boolean based on the node's existence. Also returns a
112 deferred that fires when the node is modified/created/added/deleted.
113 """
114 node_changed = Deferred()
115
116 def on_node_event(event, state, path):
117 return node_changed.callback(
118 NodeEvent(event, state, self))
119
120 d = self._context.exists(self.path, on_node_event)
121 d.addCallback(self._on_exists_success)
122 return d, node_changed
123
124 def _on_get_node_error(self, failure):
125 failure.trap(NoNodeException)
126 self._node_stat = None
127 return failure
128
129 def _on_get_node_success(self, data):
130 (node_data, node_stat) = data
131 self._node_stat = node_stat
132 return node_data
133
134 def get_data(self):
135 """Retrieve the node's data."""
136 d = self._context.get(self.path)
137 d.addCallback(self._on_get_node_success)
138 d.addErrback(self._on_get_node_error)
139 return d
140
141 def get_data_and_watch(self):
142 """
143 Retrieve the node's data and a deferred that fires when this data
144 changes.
145 """
146 node_changed = Deferred()
147
148 def on_node_change(event, status, path):
149 node_changed.callback(
150 NodeEvent(event, status, self))
151
152 d = self._context.get(self.path, watcher=on_node_change)
153 d.addCallback(self._on_get_node_success)
154 d.addErrback(self._on_get_node_error)
155 return d, node_changed
156
157 def set_data(self, data):
158 """Set the node's data."""
159
160 def on_success(value):
161 self._node_stat = None
162 return self
163
164 version = self._get_version()
165 d = self._context.set(self.path, data, version)
166 d.addErrback(self._on_error_bad_version)
167 d.addCallback(on_success)
168 return d
169
170 def get_acl(self):
171 """
172 Get the ACL for this node. An ACL is a list of access control
173 entity dictionaries.
174 """
175
176 def on_success((acl, stat)):
177 if stat is not None:
178 self._node_stat = stat
179 return acl
180 d = self._context.get_acl(self.path)
181 d.addCallback(on_success)
182 return d
183
184 def set_acl(self, acl):
185 """
186 Set the ACL for this node.
187 """
188 d = self._context.set_acl(
189 self.path, acl, self._get_version())
190 d.addErrback(self._on_error_bad_version)
191 return d
192
193 def _on_get_children_filter_results(self, children, prefix):
194 if prefix:
195 children = [
196 name for name in children if name.startswith(prefix)]
197 return [
198 self.__class__("/".join((self.path, name)), self._context)
199 for name in children]
200
201 def get_children(self, prefix=None):
202 """
203 Get the children of this node, as ZNode objects. Optionally
204 a name prefix may be passed which the child node must abide.
205 """
206 d = self._context.get_children(self.path)
207 d.addCallback(self._on_get_children_filter_results, prefix)
208 return d
209
210 def get_children_and_watch(self, prefix=None):
211 """
212 Get the children of this node, as ZNode objects, and also return
213 a deferred that fires if a child is added or deleted. Optionally
214 a name prefix may be passed which the child node must abide.
215 """
216 children_changed = Deferred()
217
218 def on_child_added_removed(event, status, path):
219 # path is the container not the child.
220 children_changed.callback(
221 NodeEvent(event, status, self))
222
223 d = self._context.get_children(
224 self.path, watcher=on_child_added_removed)
225 d.addCallback(self._on_get_children_filter_results, prefix)
226 return d, children_changed
227
228 def __cmp__(self, other):
229 return cmp(self.path, other.path)
0230
=== added file 'txzookeeper/tests/test_node.py'
--- txzookeeper/tests/test_node.py 1970-01-01 00:00:00 +0000
+++ txzookeeper/tests/test_node.py 2010-06-11 16:18:25 +0000
@@ -0,0 +1,316 @@
1import zookeeper
2import hashlib
3import base64
4
5from twisted.internet.defer import Deferred, inlineCallbacks
6from twisted.python.failure import Failure
7from txzookeeper.node import ZNode
8from txzookeeper.tests import TestCase
9from txzookeeper.tests.utils import deleteTree
10from txzookeeper import ZookeeperClient
11
12
13class NodeTest(TestCase):
14
15 def setUp(self):
16 super(NodeTest, self).setUp()
17 zookeeper.set_debug_level(zookeeper.LOG_LEVEL_ERROR)
18 self.client = ZookeeperClient("127.0.0.1:2181", 2000)
19 d = self.client.connect()
20 self.client2 = None
21
22 def create_zoo(client):
23 client.create("/zoo")
24 d.addCallback(create_zoo)
25 return d
26
27 def tearDown(self):
28 super(NodeTest, self).tearDown()
29 deleteTree(handle=self.client.handle)
30 if self.client.connected:
31 self.client.close()
32 if self.client2 and self.client2.connected:
33 self.client2.close()
34 zookeeper.set_debug_level(zookeeper.LOG_LEVEL_DEBUG)
35
36 def _make_digest_identity(self, credentials):
37 user, password = credentials.split(":")
38 digest = hashlib.new("sha1", credentials).digest()
39 return "%s:%s"%(user, base64.b64encode(digest))
40
41 def test_node_name_and_path(self):
42 """
43 Each node has name and path.
44 """
45 node = ZNode("/zoo/rabbit", self.client)
46 self.assertEqual(node.name, "rabbit")
47 self.assertEqual(node.path, "/zoo/rabbit")
48
49 @inlineCallbacks
50 def test_node_exists_nonexistant(self):
51 """
52 A node knows whether it exists or not.
53 """
54 node = ZNode("/zoo/rabbit", self.client)
55 exists = yield node.exists()
56 self.assertFalse(exists)
57
58 @inlineCallbacks
59 def test_node_set_data_on_nonexistant(self):
60 """
61 Setting data on a non existant node raises a no node exception.
62 """
63 node = ZNode("/zoo/rabbit", self.client)
64 d = node.set_data("big furry ears")
65 self.failUnlessFailure(d, zookeeper.NoNodeException)
66 yield d
67
68 @inlineCallbacks
69 def test_node_create_set_data(self):
70 """
71 A node can be created and have its data set.
72 """
73 node = ZNode("/zoo/rabbit", self.client)
74 data = "big furry ears"
75 yield node.create(data)
76 exists = yield self.client.exists("/zoo/rabbit")
77 self.assertTrue(exists)
78 node_data = yield node.get_data()
79 self.assertEqual(data, node_data)
80 data = data*2
81
82 yield node.set_data(data)
83 node_data = yield node.get_data()
84 self.assertEqual(data, node_data)
85
86 @inlineCallbacks
87 def test_node_get_data(self):
88 """
89 Data can be fetched from a node.
90 """
91 yield self.client.create("/zoo/giraffe", "mouse")
92 data = yield ZNode("/zoo/giraffe", self.client).get_data()
93 self.assertEqual(data, "mouse")
94
95 @inlineCallbacks
96 def test_node_get_data_nonexistant(self):
97 """
98 Attempting to fetch data from a nonexistant node returns
99 a non existant error.
100 """
101 d = ZNode("/zoo/giraffe", self.client).get_data()
102 self.failUnlessFailure(d, zookeeper.NoNodeException)
103 yield d
104
105 @inlineCallbacks
106 def test_node_get_acl(self):
107 """
108 The ACL for a node can be retrieved.
109 """
110 yield self.client.create("/zoo/giraffe")
111 acl = yield ZNode("/zoo/giraffe", self.client).get_acl()
112 self.assertEqual(len(acl), 1)
113 self.assertEqual(acl[0]['scheme'], 'world')
114
115 def test_node_get_acl_nonexistant(self):
116 """
117 The fetching the ACL for a non-existant node results in an error.
118 """
119 node = ZNode("/zoo/giraffe", self.client)
120
121 def assert_failed(failed):
122 if not isinstance(failed, Failure):
123 self.fail("Should have failed")
124 self.assertTrue(
125 isinstance(failed.value, zookeeper.NoNodeException))
126 d = node.get_acl()
127 d.addBoth(assert_failed)
128 return d
129
130 @inlineCallbacks
131 def test_node_set_acl(self):
132 """
133 The ACL for a node can be modified.
134 """
135 path = yield self.client.create("/zoo/giraffe")
136 credentials = "zebra:moon"
137 acl = [{"id": self._make_digest_identity(credentials),
138 "scheme": "digest",
139 "perms":zookeeper.PERM_ALL}]
140 node = ZNode(path, self.client)
141 # little hack around slow auth issue 770 zookeeper
142 d = self.client.add_auth("digest", credentials)
143 yield node.set_acl(acl)
144 yield d
145 node_acl, stat = yield self.client.get_acl(path)
146 self.assertEqual(node_acl, acl)
147
148 @inlineCallbacks
149 def test_node_set_data_update_with_cached_exists(self):
150 """
151 Data can be set on an existing node, updating it
152 in place.
153 """
154 node = ZNode("/zoo/monkey", self.client)
155 yield self.client.create("/zoo/monkey", "stripes")
156 exists = yield node.exists()
157 self.assertTrue(exists)
158 yield node.set_data("banana")
159 data, stat = yield self.client.get("/zoo/monkey")
160 self.assertEqual(data, "banana")
161
162 @inlineCallbacks
163 def test_node_set_data_update_with_invalid_cached_exists(self):
164 """
165 If a node is deleted, attempting to set data on it
166 raises a no node exception.
167 """
168 node = ZNode("/zoo/monkey", self.client)
169 yield self.client.create("/zoo/monkey", "stripes")
170 exists = yield node.exists()
171 self.assertTrue(exists)
172 yield self.client.delete("/zoo/monkey")
173 d = node.set_data("banana")
174 self.failUnlessFailure(d, zookeeper.NoNodeException)
175 yield d
176
177 @inlineCallbacks
178 def test_node_set_data_update_with_exists(self):
179 """
180 Data can be set on an existing node, updating it
181 in place.
182 """
183 node = ZNode("/zoo/monkey", self.client)
184 yield self.client.create("/zoo/monkey", "stripes")
185 yield node.set_data("banana")
186 data, stat = yield self.client.get("/zoo/monkey")
187 self.assertEqual(data, "banana")
188
189 @inlineCallbacks
190 def test_node_exists_with_watch_nonexistant(self):
191 """
192 The node's existance can be checked with the exist_watch api
193 a deferred will be returned and any node level events,
194 created, deleted, modified invoke the callback. You can
195 get these create event callbacks for non existant nodes.
196 """
197 node = ZNode("/zoo/elephant", self.client)
198 exists, watch = yield node.exists_and_watch()
199
200 self.client.create("/zoo/elephant")
201 event = yield watch
202 self.assertEqual(event.type, zookeeper.CREATED_EVENT)
203 self.assertEqual(event.path, node.path)
204
205 @inlineCallbacks
206 def test_node_get_data_with_watch_on_update(self):
207 """
208 Subscribing to a node will get node update events.
209 """
210 yield self.client.create("/zoo/elephant")
211
212 node = ZNode("/zoo/elephant", self.client)
213 data, watch = yield node.get_data_and_watch()
214 yield self.client.set("/zoo/elephant")
215 event = yield watch
216 self.assertEqual(event.type, zookeeper.CHANGED_EVENT)
217 self.assertEqual(event.path, "/zoo/elephant")
218
219 @inlineCallbacks
220 def test_node_get_data_with_watch_on_delete(self):
221 """
222 Subscribing to a node will get node deletion events.
223 """
224 yield self.client.create("/zoo/elephant")
225
226 node = ZNode("/zoo/elephant", self.client)
227 data, watch = yield node.get_data_and_watch()
228
229 yield self.client.delete("/zoo/elephant")
230 event = yield watch
231 self.assertEqual(event.type, zookeeper.DELETED_EVENT)
232 self.assertEqual(event.path, "/zoo/elephant")
233
234 @inlineCallbacks
235 def test_node_children(self):
236 """
237 A node's children can be introspected.
238 """
239 node = ZNode("/zoo", self.client)
240 node_path_a = yield self.client.create("/zoo/lion")
241 node_path_b = yield self.client.create("/zoo/tiger")
242 children = yield node.get_children()
243 children.sort()
244 self.assertEqual(children[0].path, node_path_a)
245 self.assertEqual(children[1].path, node_path_b)
246
247 @inlineCallbacks
248 def test_node_children_by_prefix(self):
249 """
250 A node's children can be introspected optionally with a prefix.
251 """
252 node = ZNode("/zoo", self.client)
253 node_path_a = yield self.client.create("/zoo/lion")
254 yield self.client.create("/zoo/tiger")
255 children = yield node.get_children("lion")
256 children.sort()
257 self.assertEqual(children[0].path, node_path_a)
258 self.assertEqual(len(children), 1)
259
260 @inlineCallbacks
261 def test_node_get_children_with_watch_create(self):
262 """
263 A node's children can explicitly be watched to given existance
264 events for node creation and destruction.
265 """
266 node = ZNode("/zoo", self.client)
267 children, watch = yield node.get_children_and_watch()
268 yield self.client.create("/zoo/lion")
269 event = yield watch
270 self.assertEqual(event.path, "/zoo")
271 self.assertEqual(event.type, zookeeper.CHILD_EVENT)
272 self.assertEqual(event.kind, "child")
273
274 @inlineCallbacks
275 def test_node_get_children_with_watch_delete(self):
276 """
277 A node's children can explicitly be watched to given existance
278 events for node creation and destruction.
279 """
280 node = ZNode("/zoo", self.client)
281 yield self.client.create("/zoo/lion")
282 children, watch = yield node.get_children_and_watch()
283 yield self.client.delete("/zoo/lion")
284 event = yield watch
285 self.assertEqual(event.path, "/zoo")
286 self.assertEqual(event.type, zookeeper.CHILD_EVENT)
287 self.assertEqual(repr(event),
288 "<NodeEvent child at '/zoo'>")
289
290 @inlineCallbacks
291 def test_bad_version_error(self):
292 """
293 The node captures the node version on any read operations, which
294 it utilizes for write operations. On a concurrent modification error
295 the node return a bad version error, this also clears the cached
296 state so subsequent modifications will be against the latest version,
297 unless the cache is seeded again by a read operation.
298 """
299 node = ZNode("/zoo/lion", self.client)
300
301 self.client2 = ZookeeperClient("127.0.0.1:2181")
302 yield self.client2.connect()
303
304 yield self.client.create("/zoo/lion", "mouse")
305 yield node.get_data()
306 yield self.client2.set("/zoo/lion", "den2")
307 data = yield self.client.exists("/zoo/lion")
308 self.assertEqual(data['version'], 1)
309 d = node.set_data("zebra")
310 self.failUnlessFailure(d, zookeeper.BadVersionException)
311 yield d
312
313 # after failure the cache is deleted, and a set proceeds
314 yield node.set_data("zebra")
315 data = yield node.get_data()
316 self.assertEqual(data, "zebra")

Subscribers

People subscribed via source and target branches

to all changes: