Merge lp:~hazmat/txzookeeper/retry-change-utility into lp:txzookeeper

Proposed by Kapil Thangavelu
Status: Merged
Merged at revision: 30
Proposed branch: lp:~hazmat/txzookeeper/retry-change-utility
Merge into: lp:txzookeeper
Diff against target: 202 lines (+193/-0)
2 files modified
txzookeeper/tests/test_utils.py (+151/-0)
txzookeeper/utils.py (+42/-0)
To merge this branch: bzr merge lp:~hazmat/txzookeeper/retry-change-utility
Reviewer Review Type Date Requested Status
Gustavo Niemeyer Approve
Review via email: mp+33835@code.launchpad.net

Description of the change

a retry change utility function that isolates error handling responsibilities from change calculation.

To post a comment you must log in.
31. By Kapil Thangavelu

use increment function on the bad version test for clarity, remove unused change_constant function

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

Looks great, thank you!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'txzookeeper/tests/test_utils.py'
2--- txzookeeper/tests/test_utils.py 1970-01-01 00:00:00 +0000
3+++ txzookeeper/tests/test_utils.py 2010-08-26 20:30:57 +0000
4@@ -0,0 +1,151 @@
5+import zookeeper
6+
7+from twisted.internet.defer import inlineCallbacks, fail, succeed
8+
9+from txzookeeper import ZookeeperClient
10+from txzookeeper.utils import retry_change
11+from txzookeeper.tests.mocker import MATCH
12+from txzookeeper.tests import ZookeeperTestCase, utils
13+
14+MATCH_STAT = MATCH(lambda x: isinstance(x, dict))
15+
16+
17+class RetryChangeTest(ZookeeperTestCase):
18+
19+ def update_function_increment(self, content, stat):
20+ if not content:
21+ return str(0)
22+ return str(int(content)+1)
23+
24+ def setUp(self):
25+ super(RetryChangeTest, self).setUp()
26+ self.client = ZookeeperClient("127.0.0.1:2181")
27+ return self.client.connect()
28+
29+ def tearDown(self):
30+ utils.deleteTree("/", self.client.handle)
31+ self.client.close()
32+
33+ @inlineCallbacks
34+ def test_node_create(self):
35+ """
36+ retry_change will create a node if one does not exist.
37+ """
38+ #use a mock to ensure the change function is only invoked once
39+ func = self.mocker.mock()
40+ func(None, None)
41+ self.mocker.result("hello")
42+ self.mocker.replay()
43+
44+ yield retry_change(
45+ self.client, "/magic-beans", func)
46+
47+ content, stat = yield self.client.get("/magic-beans")
48+ self.assertEqual(content, "hello")
49+ self.assertEqual(stat["version"], 0)
50+
51+ @inlineCallbacks
52+ def test_node_update(self):
53+ """
54+ retry_change will update an existing node.
55+ """
56+ #use a mock to ensure the change function is only invoked once
57+ func = self.mocker.mock()
58+ func("", MATCH_STAT)
59+ self.mocker.result("hello")
60+ self.mocker.replay()
61+
62+ yield self.client.create("/magic-beans")
63+ yield retry_change(
64+ self.client, "/magic-beans", func)
65+
66+ content, stat = yield self.client.get("/magic-beans")
67+ self.assertEqual(content, "hello")
68+ self.assertEqual(stat["version"], 1)
69+
70+ def test_error_in_change_function_propogates(self):
71+ """
72+ an error in the change function propogates to the caller.
73+ """
74+
75+ def error_function(content, stat):
76+ raise SyntaxError()
77+
78+ d = retry_change(self.client, "/magic-beans", error_function)
79+ self.failUnlessFailure(d, SyntaxError)
80+ return d
81+
82+ @inlineCallbacks
83+ def test_concurrent_update_bad_version(self):
84+ """
85+ If the node is updated after the retry function has read
86+ the node but before the content is set, the retry function
87+ will perform another read/change_func/set cycle.
88+ """
89+ yield self.client.create("/animals")
90+ content, stat = yield self.client.get("/animals")
91+ yield self.client.set("/animals", "5")
92+
93+ real_get = self.client.get
94+ p_client = self.mocker.proxy(self.client)
95+ p_client.get("/animals")
96+ self.mocker.result(succeed((content, stat)))
97+
98+ p_client.get("/animals")
99+ self.mocker.call(real_get)
100+
101+ self.mocker.replay()
102+
103+ yield retry_change(
104+ p_client, "/animals", self.update_function_increment)
105+
106+ content, stat = yield real_get("/animals")
107+ self.assertEqual(content, "6")
108+ self.assertEqual(stat["version"], 2)
109+
110+ @inlineCallbacks
111+ def test_create_node_exists(self):
112+ """
113+ If the node is created after the retry function has determined
114+ the node doesn't exist but before the node is created by the
115+ retry function. the retry function will perform another
116+ read/change_func/set cycle.
117+ """
118+ yield self.client.create("/animals", "5")
119+
120+ real_get = self.client.get
121+ p_client = self.mocker.patch(self.client)
122+ p_client.get("/animals")
123+ self.mocker.result(fail(zookeeper.NoNodeException()))
124+
125+ p_client.get("/animals")
126+ self.mocker.call(real_get)
127+ self.mocker.replay()
128+
129+ yield retry_change(
130+ p_client, "/animals", self.update_function_increment)
131+
132+ content, stat = yield real_get("/animals")
133+ self.assertEqual(content, "6")
134+ self.assertEqual(stat["version"], 1)
135+
136+ def test_set_node_does_not_exist(self):
137+ """
138+ if the retry function goes to update a node which has been
139+ deleted since it was read, it will cycle through to another
140+ read/change_func set cycle.
141+ """
142+ real_get = self.client.get
143+ p_client = self.mocker.patch(self.client)
144+ p_client.get("/animals")
145+ self.mocker.result("5", {"version": 1})
146+
147+ p_client.get("/animals")
148+ self.mocker.call(real_get)
149+
150+ yield retry_change(
151+ p_client, "/animals", self.update_function_increment)
152+
153+ content, stat = yield real_get("/animals")
154+ self.assertEqual(content, "0")
155+ self.assertEqual(stat["version"], 0)
156
157=== added file 'txzookeeper/utils.py'
158--- txzookeeper/utils.py 1970-01-01 00:00:00 +0000
159+++ txzookeeper/utils.py 2010-08-26 20:30:57 +0000
160@@ -0,0 +1,42 @@
161+import zookeeper
162+from twisted.internet.defer import inlineCallbacks
163+
164+
165+@inlineCallbacks
166+def retry_change(client, path, change_function):
167+ """
168+ A utility function to execute a node change function, repeatedly
169+ in the face of transient errors.
170+
171+ @param client A connected txzookeeper client
172+
173+ @param path A path to a node that will be modified
174+
175+ @param change_function A python function that will receive two parameters
176+ the node_content and the current node stat, and will return the
177+ new node content. The function must not have side-effects as
178+ it will be called again in the event of various error conditions.
179+ ie. it must be idempotent.
180+ """
181+
182+ while True:
183+ create_mode = False
184+
185+ try:
186+ content, stat = yield client.get(path)
187+ except zookeeper.NoNodeException:
188+ create_mode = True
189+ content, stat = None, None
190+
191+ new_content = yield change_function(content, stat)
192+
193+ try:
194+ if create_mode:
195+ yield client.create(path, new_content)
196+ else:
197+ yield client.set(path, new_content, version=stat["version"])
198+ break
199+ except (zookeeper.NodeExistsException,
200+ zookeeper.NoNodeException,
201+ zookeeper.BadVersionException):
202+ pass

Subscribers

People subscribed via source and target branches

to all changes: