Merge lp:~hazmat/pyjuju/sane-relation-ids into lp:pyjuju

Proposed by Kapil Thangavelu
Status: Merged
Merged at revision: 612
Proposed branch: lp:~hazmat/pyjuju/sane-relation-ids
Merge into: lp:pyjuju
Diff against target: 206 lines (+44/-21)
4 files modified
juju/hooks/invoker.py (+11/-6)
juju/hooks/protocol.py (+4/-2)
juju/hooks/tests/test_invoker.py (+17/-13)
juju/state/errors.py (+12/-0)
To merge this branch: bzr merge lp:~hazmat/pyjuju/sane-relation-ids
Reviewer Review Type Date Requested Status
Juju Engineering Pending
Review via email: mp+146033@code.launchpad.net

Description of the change

Fix relation-ids cache coherency for hooks.

Relation-ids was answering questions about relations
directly from state, whilst other pieces of relation-*
cli was using a hook execution cache. This led to
odd races where a hook would query relation-ids
and then attempt to use relation-list/etc against
that relation resulting in a relation not found error.

This branch also does a drive by to improve the error
reporting for relation not found to include the relation
id where applicable.

https://codereview.appspot.com/7241062/

To post a comment you must log in.
Revision history for this message
Kapil Thangavelu (hazmat) wrote :
Download full text (7.8 KiB)

Reviewers: mp+146033_code.launchpad.net,

Message:
Please take a look.

Description:
Fix relation-ids cache coherency for hooks.

Relation-ids was answering questions about relations
directly from state, whilst other pieces of relation-*
cli was using a hook execution cache. This led to
odd races where a hook would query relation-ids
and then attempt to use relation-list/etc against
that relation resulting in a relation not found error.

This branch also does a drive by to improve the error
reporting for relation not found to include the relation
id where applicable.

https://code.launchpad.net/~hazmat/juju/sane-relation-ids/+merge/146033

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/7241062/

Affected files:
   A [revision details]
   M juju/hooks/invoker.py
   M juju/hooks/protocol.py
   M juju/hooks/tests/test_invoker.py
   M juju/state/errors.py

Index: [revision details]
=== added file '[revision details]'
--- [revision details] 2012-01-01 00:00:00 +0000
+++ [revision details] 2012-01-01 00:00:00 +0000
@@ -0,0 +1,2 @@
+Old revision: <email address hidden>
+New revision: <email address hidden>

Index: juju/hooks/invoker.py
=== modified file 'juju/hooks/invoker.py'
--- juju/hooks/invoker.py 2013-01-15 19:53:21 +0000
+++ juju/hooks/invoker.py 2013-02-01 03:35:02 +0000
@@ -6,7 +6,7 @@
  from twisted.python.failure import Failure

  from juju import errors
-from juju.state.errors import RelationStateNotFound,
InvalidRelationIdentity
+from juju.state.errors import RelationIdentNotFound,
InvalidRelationIdentity
  from juju.state.hook import RelationHookContext

@@ -172,7 +172,8 @@
      def start(self):
          """Cache relation hook contexts for all relation idents."""
          # Get all relation idents (None means "all")
- relation_idents = set((yield self.get_relation_idents(None)))
+ relation_idents = set((yield
self._context.get_relation_idents(None)))
+ #relation_idents = set((yield self.get_relation_idents(None)))
          if isinstance(self._context, RelationHookContext):
              # Exclude the parent context for being looked up as a child
              relation_idents.discard(self._context.relation_ident)
@@ -244,10 +245,14 @@
              if len(parts) != 2 or not parts[1].isdigit():
                  raise InvalidRelationIdentity(relation_ident)
              else:
- raise RelationStateNotFound()
+ raise RelationIdentNotFound(relation_ident)

+ @inlineCallbacks
      def get_relation_idents(self, relation_name):
- return self._context.get_relation_idents(relation_name)
+ """Returns valid relation instances for the given name."""
+ idents = yield self._context.get_relation_idents(relation_name)
+ returnValue(
+ list(set(idents).intersection(set(self._relation_contexts))))

      def validate_hook(self, hook_filename):
          """Verify that the hook_filename exists and is executable. """

Index: juju/hooks/protocol.py
=== modified file 'juju/hooks/protocol.py'
--- juju/hooks/protocol.py 2012-10-16 23:19:55 +0...

Read more...

lp:~hazmat/pyjuju/sane-relation-ids updated
615. By Kapil Thangavelu

a rel-id cache coherence test

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

LGTM, subtle issue you're fixing there.

https://codereview.appspot.com/7241062/diff/3001/juju/hooks/invoker.py
File juju/hooks/invoker.py (right):

https://codereview.appspot.com/7241062/diff/3001/juju/hooks/invoker.py#newcode255
juju/hooks/invoker.py:255:
list(set(idents).intersection(set(self._relation_contexts))))
It doesn't look like you test this method after its change to be a
generator. You previously returned the generator though it looks like so
it would have been yielded on anyway, I suspect this is still ok.

https://codereview.appspot.com/7241062/

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

thanks for the review

https://codereview.appspot.com/7241062/diff/3001/juju/hooks/invoker.py
File juju/hooks/invoker.py (right):

https://codereview.appspot.com/7241062/diff/3001/juju/hooks/invoker.py#newcode255
juju/hooks/invoker.py:255:
list(set(idents).intersection(set(self._relation_contexts))))
On 2013/02/01 16:11:10, bcsaller wrote:
> It doesn't look like you test this method after its change to be a
generator.
> You previously returned the generator though it looks like so it would
have been
> yielded on anyway, I suspect this is still ok.

it was returning a deferred with eventual value of list of strings
idents in either case, so should be fine (and verified end2end by
existing test suite).

https://codereview.appspot.com/7241062/

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

*** Submitted:

Fix relation-ids cache coherency for hooks.

Relation-ids was answering questions about relations
directly from state, whilst other pieces of relation-*
cli was using a hook execution cache. This led to
odd races where a hook would query relation-ids
and then attempt to use relation-list/etc against
that relation resulting in a relation not found error.

This branch also does a drive by to improve the error
reporting for relation not found to include the relation
id where applicable.

R=bcsaller
CC=
https://codereview.appspot.com/7241062

https://codereview.appspot.com/7241062/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'juju/hooks/invoker.py'
--- juju/hooks/invoker.py 2013-01-15 19:53:21 +0000
+++ juju/hooks/invoker.py 2013-02-01 14:18:29 +0000
@@ -6,7 +6,7 @@
6from twisted.python.failure import Failure6from twisted.python.failure import Failure
77
8from juju import errors8from juju import errors
9from juju.state.errors import RelationStateNotFound, InvalidRelationIdentity9from juju.state.errors import RelationIdentNotFound, InvalidRelationIdentity
10from juju.state.hook import RelationHookContext10from juju.state.hook import RelationHookContext
1111
1212
@@ -172,7 +172,8 @@
172 def start(self):172 def start(self):
173 """Cache relation hook contexts for all relation idents."""173 """Cache relation hook contexts for all relation idents."""
174 # Get all relation idents (None means "all")174 # Get all relation idents (None means "all")
175 relation_idents = set((yield self.get_relation_idents(None)))175 relation_idents = set((yield self._context.get_relation_idents(None)))
176
176 if isinstance(self._context, RelationHookContext):177 if isinstance(self._context, RelationHookContext):
177 # Exclude the parent context for being looked up as a child178 # Exclude the parent context for being looked up as a child
178 relation_idents.discard(self._context.relation_ident)179 relation_idents.discard(self._context.relation_ident)
@@ -185,8 +186,8 @@
185 relation_ident)186 relation_ident)
186 self._relation_contexts[relation_ident] = child187 self._relation_contexts[relation_ident] = child
187 self._log.debug("Cached relation hook contexts%s: %r" % (188 self._log.debug("Cached relation hook contexts%s: %r" % (
188 display_parent_relation_ident,189 display_parent_relation_ident,
189 sorted(relation_idents)))190 sorted(relation_idents)))
190191
191 service = yield self._context.get_local_service()192 service = yield self._context.get_local_service()
192 charm = yield service.get_charm_state()193 charm = yield service.get_charm_state()
@@ -244,10 +245,14 @@
244 if len(parts) != 2 or not parts[1].isdigit():245 if len(parts) != 2 or not parts[1].isdigit():
245 raise InvalidRelationIdentity(relation_ident)246 raise InvalidRelationIdentity(relation_ident)
246 else:247 else:
247 raise RelationStateNotFound()248 raise RelationIdentNotFound(relation_ident)
248249
250 @inlineCallbacks
249 def get_relation_idents(self, relation_name):251 def get_relation_idents(self, relation_name):
250 return self._context.get_relation_idents(relation_name)252 """Returns valid relation instances for the given name."""
253 idents = yield self._context.get_relation_idents(relation_name)
254 returnValue(
255 list(set(idents).intersection(set(self._relation_contexts))))
251256
252 def validate_hook(self, hook_filename):257 def validate_hook(self, hook_filename):
253 """Verify that the hook_filename exists and is executable. """258 """Verify that the hook_filename exists and is executable. """
254259
=== modified file 'juju/hooks/protocol.py'
--- juju/hooks/protocol.py 2012-10-16 23:19:55 +0000
+++ juju/hooks/protocol.py 2013-02-01 14:18:29 +0000
@@ -54,7 +54,8 @@
54from juju.lib import serializer54from juju.lib import serializer
55from juju.lib.format import get_charm_formatter, get_charm_formatter_from_env55from juju.lib.format import get_charm_formatter, get_charm_formatter_from_env
56from juju.state.errors import (56from juju.state.errors import (
57 InvalidRelationIdentity, RelationStateNotFound, UnitRelationStateNotFound)57 InvalidRelationIdentity, RelationStateNotFound, UnitRelationStateNotFound,
58 RelationIdentNotFound)
58from juju.state.hook import RelationHookContext59from juju.state.hook import RelationHookContext
5960
6061
@@ -92,7 +93,8 @@
92 UnitRelationStateNotFound: "UnitRelationStateNotFound",93 UnitRelationStateNotFound: "UnitRelationStateNotFound",
93 MustSpecifyRelationName: "MustSpecifyRelationName",94 MustSpecifyRelationName: "MustSpecifyRelationName",
94 InvalidRelationIdentity: "InvalidRelationIdentity",95 InvalidRelationIdentity: "InvalidRelationIdentity",
95 RelationStateNotFound: "RelationStateNotFound"96 RelationStateNotFound: "RelationStateNotFound",
97 RelationIdentNotFound: "RelationIdentNotFound"
96 }98 }
9799
98100
99101
=== modified file 'juju/hooks/tests/test_invoker.py'
--- juju/hooks/tests/test_invoker.py 2013-01-15 19:53:21 +0000
+++ juju/hooks/tests/test_invoker.py 2013-02-01 14:18:29 +0000
@@ -24,7 +24,7 @@
24from juju.lib.twistutils import get_module_directory24from juju.lib.twistutils import get_module_directory
25from juju.state import hook25from juju.state import hook
26from juju.state.endpoint import RelationEndpoint26from juju.state.endpoint import RelationEndpoint
27from juju.state.errors import RelationStateNotFound27from juju.state.errors import RelationIdentNotFound
28from juju.state.relation import RelationStateManager28from juju.state.relation import RelationStateManager
29from juju.state.tests.test_relation import RelationTestBase29from juju.state.tests.test_relation import RelationTestBase
3030
@@ -1046,12 +1046,12 @@
10461046
1047 # Not yet visible relation1047 # Not yet visible relation
1048 self.assertRaises(1048 self.assertRaises(
1049 RelationStateNotFound,1049 RelationIdentNotFound,
1050 exe.get_cached_relation_hook_context, "db:2")1050 exe.get_cached_relation_hook_context, "db:2")
10511051
1052 # Nonexistent relation idents1052 # Nonexistent relation idents
1053 self.assertRaises(1053 self.assertRaises(
1054 RelationStateNotFound,1054 RelationIdentNotFound,
1055 exe.get_cached_relation_hook_context, "db:12345")1055 exe.get_cached_relation_hook_context, "db:12345")
10561056
1057 # Reread parent and child contexts1057 # Reread parent and child contexts
@@ -1156,13 +1156,13 @@
1156 set((yield exe.get_context().get_relation_idents("db"))),1156 set((yield exe.get_context().get_relation_idents("db"))),
1157 set(["db:0", "db:2"]))1157 set(["db:0", "db:2"]))
1158 yield self.assertRaises(1158 yield self.assertRaises(
1159 RelationStateNotFound,1159 RelationIdentNotFound,
1160 exe.get_cached_relation_hook_context, "db:1")1160 exe.get_cached_relation_hook_context, "db:1")
11611161
1162 @defer.inlineCallbacks1162 @defer.inlineCallbacks
1163 def test_relation_ids(self):1163 def test_relation_ids(self):
1164 """Verify `relation-ids` command returns ids separated by newlines."""1164 """Verify `relation-ids` command returns ids separated by newlines."""
1165 yield self.add_a_blog("wordpress2")1165
1166 hook_log = self.capture_logging("hook")1166 hook_log = self.capture_logging("hook")
11671167
1168 # Invoker will be in the context of the mysql/0 service unit1168 # Invoker will be in the context of the mysql/0 service unit
@@ -1170,6 +1170,10 @@
1170 "db:0", "add", "mysql/0", self.relation,1170 "db:0", "add", "mysql/0", self.relation,
1171 client_id="client_id")1171 client_id="client_id")
11721172
1173 # Invoker has already been started, verify cache coherence by
1174 # adding another relation.
1175 yield self.add_a_blog("wordpress2")
1176
1173 # Then verify the hook lists the relation ids corresponding to1177 # Then verify the hook lists the relation ids corresponding to
1174 # the relation name `db`1178 # the relation name `db`
1175 hook = self.create_hook("relation-ids", "db")1179 hook = self.create_hook("relation-ids", "db")
@@ -1178,12 +1182,12 @@
1178 yield exe.ended1182 yield exe.ended
1179 # Smart formtting outputs one id per line1183 # Smart formtting outputs one id per line
1180 self.assertEqual(1184 self.assertEqual(
1181 hook_log.getvalue(), "db:0\ndb:1\n\n")1185 hook_log.getvalue(), "db:0\n\n")
1182 # But newlines are just whitespace to the shell or to Python,1186 # But newlines are just whitespace to the shell or to Python,
1183 # so they can be split accordingly, adhering to the letter of1187 # so they can be split accordingly, adhering to the letter of
1184 # the spec1188 # the spec
1185 self.assertEqual(1189 self.assertEqual(
1186 hook_log.getvalue().split(), ["db:0", "db:1"])1190 hook_log.getvalue().split(), ["db:0"])
11871191
1188 @defer.inlineCallbacks1192 @defer.inlineCallbacks
1189 def test_relation_ids_json_format(self):1193 def test_relation_ids_json_format(self):
@@ -1294,9 +1298,9 @@
12941298
1295 db1 = exe.get_cached_relation_hook_context("db:1")1299 db1 = exe.get_cached_relation_hook_context("db:1")
1296 yield self.assert_zk_data(db1, {1300 yield self.assert_zk_data(db1, {
1297 "a": "42",1301 "a": "42",
1298 "b": "xyz",1302 "b": "xyz",
1299 "private-address": "mysql-0.example.com"})1303 "private-address": "mysql-0.example.com"})
1300 self.assertLogLines(1304 self.assertLogLines(
1301 self.log.getvalue(),1305 self.log.getvalue(),
1302 ["Cached relation hook contexts on 'db:0': ['db:1']",1306 ["Cached relation hook contexts on 'db:0': ['db:1']",
@@ -1323,7 +1327,7 @@
1323 self.assertEqual(result, 0)1327 self.assertEqual(result, 0)
1324 yield exe.ended1328 yield exe.ended
1325 self.assert_file(args["stdout"], "")1329 self.assert_file(args["stdout"], "")
1326 self.assert_file(args["stderr"], "Relation not found\n")1330 self.assert_file(args["stderr"], "Relation not found for - db:12345\n")
1327 self.assertEqual(1331 self.assertEqual(
1328 hook_log.getvalue(),1332 hook_log.getvalue(),
1329 "Cached relation hook contexts on 'db:0': ['db:1']\n"1333 "Cached relation hook contexts on 'db:0': ['db:1']\n"
@@ -1400,7 +1404,7 @@
1400 self.assertEqual(result, 0)1404 self.assertEqual(result, 0)
1401 yield exe.ended1405 yield exe.ended
1402 self.assert_file(args["settings"], "")1406 self.assert_file(args["settings"], "")
1403 self.assert_file(args["stderr"], "Relation not found\n")1407 self.assert_file(args["stderr"], "Relation not found for - db:12345\n")
1404 self.assertEqual(1408 self.assertEqual(
1405 hook_log.getvalue(),1409 hook_log.getvalue(),
1406 "Cached relation hook contexts on 'db:0': ['db:1']\n"1410 "Cached relation hook contexts on 'db:0': ['db:1']\n"
@@ -1493,7 +1497,7 @@
1493 self.assertEqual(result, 0)1497 self.assertEqual(result, 0)
1494 yield exe.ended1498 yield exe.ended
1495 self.assert_file(args["stdout"], "")1499 self.assert_file(args["stdout"], "")
1496 self.assert_file(args["stderr"], "Relation not found\n")1500 self.assert_file(args["stderr"], "Relation not found for - db:12345\n")
1497 self.assertEqual(1501 self.assertEqual(
1498 hook_log.getvalue(),1502 hook_log.getvalue(),
1499 "Cached relation hook contexts on 'db:0': ['db:1']\n"1503 "Cached relation hook contexts on 'db:0': ['db:1']\n"
15001504
=== modified file 'juju/state/errors.py'
--- juju/state/errors.py 2012-04-11 17:48:26 +0000
+++ juju/state/errors.py 2013-02-01 14:18:29 +0000
@@ -228,6 +228,18 @@
228 return "Relation not found"228 return "Relation not found"
229229
230230
231class RelationIdentNotFound(StateError):
232
233 def __init__(self, relation_ident):
234 # Play nice with amp oddities, where we get our own string repr
235 if "-" in relation_ident:
236 relation_ident = relation_ident.split("-")[-1].strip()
237 self.relation_ident = relation_ident
238
239 def __str__(self):
240 return "Relation not found for - %s" % (self.relation_ident)
241
242
231class UnitRelationStateAlreadyAssigned(StateError):243class UnitRelationStateAlreadyAssigned(StateError):
232 """The unit already exists in the relation."""244 """The unit already exists in the relation."""
233245

Subscribers

People subscribed via source and target branches

to status/vote changes: