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
1=== modified file 'juju/hooks/invoker.py'
2--- juju/hooks/invoker.py 2013-01-15 19:53:21 +0000
3+++ juju/hooks/invoker.py 2013-02-01 14:18:29 +0000
4@@ -6,7 +6,7 @@
5 from twisted.python.failure import Failure
6
7 from juju import errors
8-from juju.state.errors import RelationStateNotFound, InvalidRelationIdentity
9+from juju.state.errors import RelationIdentNotFound, InvalidRelationIdentity
10 from juju.state.hook import RelationHookContext
11
12
13@@ -172,7 +172,8 @@
14 def start(self):
15 """Cache relation hook contexts for all relation idents."""
16 # Get all relation idents (None means "all")
17- relation_idents = set((yield self.get_relation_idents(None)))
18+ relation_idents = set((yield self._context.get_relation_idents(None)))
19+
20 if isinstance(self._context, RelationHookContext):
21 # Exclude the parent context for being looked up as a child
22 relation_idents.discard(self._context.relation_ident)
23@@ -185,8 +186,8 @@
24 relation_ident)
25 self._relation_contexts[relation_ident] = child
26 self._log.debug("Cached relation hook contexts%s: %r" % (
27- display_parent_relation_ident,
28- sorted(relation_idents)))
29+ display_parent_relation_ident,
30+ sorted(relation_idents)))
31
32 service = yield self._context.get_local_service()
33 charm = yield service.get_charm_state()
34@@ -244,10 +245,14 @@
35 if len(parts) != 2 or not parts[1].isdigit():
36 raise InvalidRelationIdentity(relation_ident)
37 else:
38- raise RelationStateNotFound()
39+ raise RelationIdentNotFound(relation_ident)
40
41+ @inlineCallbacks
42 def get_relation_idents(self, relation_name):
43- return self._context.get_relation_idents(relation_name)
44+ """Returns valid relation instances for the given name."""
45+ idents = yield self._context.get_relation_idents(relation_name)
46+ returnValue(
47+ list(set(idents).intersection(set(self._relation_contexts))))
48
49 def validate_hook(self, hook_filename):
50 """Verify that the hook_filename exists and is executable. """
51
52=== modified file 'juju/hooks/protocol.py'
53--- juju/hooks/protocol.py 2012-10-16 23:19:55 +0000
54+++ juju/hooks/protocol.py 2013-02-01 14:18:29 +0000
55@@ -54,7 +54,8 @@
56 from juju.lib import serializer
57 from juju.lib.format import get_charm_formatter, get_charm_formatter_from_env
58 from juju.state.errors import (
59- InvalidRelationIdentity, RelationStateNotFound, UnitRelationStateNotFound)
60+ InvalidRelationIdentity, RelationStateNotFound, UnitRelationStateNotFound,
61+ RelationIdentNotFound)
62 from juju.state.hook import RelationHookContext
63
64
65@@ -92,7 +93,8 @@
66 UnitRelationStateNotFound: "UnitRelationStateNotFound",
67 MustSpecifyRelationName: "MustSpecifyRelationName",
68 InvalidRelationIdentity: "InvalidRelationIdentity",
69- RelationStateNotFound: "RelationStateNotFound"
70+ RelationStateNotFound: "RelationStateNotFound",
71+ RelationIdentNotFound: "RelationIdentNotFound"
72 }
73
74
75
76=== modified file 'juju/hooks/tests/test_invoker.py'
77--- juju/hooks/tests/test_invoker.py 2013-01-15 19:53:21 +0000
78+++ juju/hooks/tests/test_invoker.py 2013-02-01 14:18:29 +0000
79@@ -24,7 +24,7 @@
80 from juju.lib.twistutils import get_module_directory
81 from juju.state import hook
82 from juju.state.endpoint import RelationEndpoint
83-from juju.state.errors import RelationStateNotFound
84+from juju.state.errors import RelationIdentNotFound
85 from juju.state.relation import RelationStateManager
86 from juju.state.tests.test_relation import RelationTestBase
87
88@@ -1046,12 +1046,12 @@
89
90 # Not yet visible relation
91 self.assertRaises(
92- RelationStateNotFound,
93+ RelationIdentNotFound,
94 exe.get_cached_relation_hook_context, "db:2")
95
96 # Nonexistent relation idents
97 self.assertRaises(
98- RelationStateNotFound,
99+ RelationIdentNotFound,
100 exe.get_cached_relation_hook_context, "db:12345")
101
102 # Reread parent and child contexts
103@@ -1156,13 +1156,13 @@
104 set((yield exe.get_context().get_relation_idents("db"))),
105 set(["db:0", "db:2"]))
106 yield self.assertRaises(
107- RelationStateNotFound,
108+ RelationIdentNotFound,
109 exe.get_cached_relation_hook_context, "db:1")
110
111 @defer.inlineCallbacks
112 def test_relation_ids(self):
113 """Verify `relation-ids` command returns ids separated by newlines."""
114- yield self.add_a_blog("wordpress2")
115+
116 hook_log = self.capture_logging("hook")
117
118 # Invoker will be in the context of the mysql/0 service unit
119@@ -1170,6 +1170,10 @@
120 "db:0", "add", "mysql/0", self.relation,
121 client_id="client_id")
122
123+ # Invoker has already been started, verify cache coherence by
124+ # adding another relation.
125+ yield self.add_a_blog("wordpress2")
126+
127 # Then verify the hook lists the relation ids corresponding to
128 # the relation name `db`
129 hook = self.create_hook("relation-ids", "db")
130@@ -1178,12 +1182,12 @@
131 yield exe.ended
132 # Smart formtting outputs one id per line
133 self.assertEqual(
134- hook_log.getvalue(), "db:0\ndb:1\n\n")
135+ hook_log.getvalue(), "db:0\n\n")
136 # But newlines are just whitespace to the shell or to Python,
137 # so they can be split accordingly, adhering to the letter of
138 # the spec
139 self.assertEqual(
140- hook_log.getvalue().split(), ["db:0", "db:1"])
141+ hook_log.getvalue().split(), ["db:0"])
142
143 @defer.inlineCallbacks
144 def test_relation_ids_json_format(self):
145@@ -1294,9 +1298,9 @@
146
147 db1 = exe.get_cached_relation_hook_context("db:1")
148 yield self.assert_zk_data(db1, {
149- "a": "42",
150- "b": "xyz",
151- "private-address": "mysql-0.example.com"})
152+ "a": "42",
153+ "b": "xyz",
154+ "private-address": "mysql-0.example.com"})
155 self.assertLogLines(
156 self.log.getvalue(),
157 ["Cached relation hook contexts on 'db:0': ['db:1']",
158@@ -1323,7 +1327,7 @@
159 self.assertEqual(result, 0)
160 yield exe.ended
161 self.assert_file(args["stdout"], "")
162- self.assert_file(args["stderr"], "Relation not found\n")
163+ self.assert_file(args["stderr"], "Relation not found for - db:12345\n")
164 self.assertEqual(
165 hook_log.getvalue(),
166 "Cached relation hook contexts on 'db:0': ['db:1']\n"
167@@ -1400,7 +1404,7 @@
168 self.assertEqual(result, 0)
169 yield exe.ended
170 self.assert_file(args["settings"], "")
171- self.assert_file(args["stderr"], "Relation not found\n")
172+ self.assert_file(args["stderr"], "Relation not found for - db:12345\n")
173 self.assertEqual(
174 hook_log.getvalue(),
175 "Cached relation hook contexts on 'db:0': ['db:1']\n"
176@@ -1493,7 +1497,7 @@
177 self.assertEqual(result, 0)
178 yield exe.ended
179 self.assert_file(args["stdout"], "")
180- self.assert_file(args["stderr"], "Relation not found\n")
181+ self.assert_file(args["stderr"], "Relation not found for - db:12345\n")
182 self.assertEqual(
183 hook_log.getvalue(),
184 "Cached relation hook contexts on 'db:0': ['db:1']\n"
185
186=== modified file 'juju/state/errors.py'
187--- juju/state/errors.py 2012-04-11 17:48:26 +0000
188+++ juju/state/errors.py 2013-02-01 14:18:29 +0000
189@@ -228,6 +228,18 @@
190 return "Relation not found"
191
192
193+class RelationIdentNotFound(StateError):
194+
195+ def __init__(self, relation_ident):
196+ # Play nice with amp oddities, where we get our own string repr
197+ if "-" in relation_ident:
198+ relation_ident = relation_ident.split("-")[-1].strip()
199+ self.relation_ident = relation_ident
200+
201+ def __str__(self):
202+ return "Relation not found for - %s" % (self.relation_ident)
203+
204+
205 class UnitRelationStateAlreadyAssigned(StateError):
206 """The unit already exists in the relation."""
207

Subscribers

People subscribed via source and target branches

to status/vote changes: