Merge lp:~hazmat/pyjuju/resolved-state-api into lp:pyjuju

Proposed by Kapil Thangavelu
Status: Merged
Approved by: Gustavo Niemeyer
Approved revision: 202
Merged at revision: 222
Proposed branch: lp:~hazmat/pyjuju/resolved-state-api
Merge into: lp:pyjuju
Diff against target: 792 lines (+537/-22)
9 files modified
ensemble/state/errors.py (+25/-0)
ensemble/state/service.py (+194/-2)
ensemble/state/tests/test_errors.py (+20/-1)
ensemble/state/tests/test_service.py (+216/-2)
ensemble/state/tests/test_utils.py (+26/-12)
ensemble/state/utils.py (+18/-2)
ensemble/unit/lifecycle.py (+1/-0)
ensemble/unit/tests/test_workflow.py (+21/-1)
ensemble/unit/workflow.py (+16/-2)
To merge this branch: bzr merge lp:~hazmat/pyjuju/resolved-state-api
Reviewer Review Type Date Requested Status
Gustavo Niemeyer Approve
Review via email: mp+58579@code.launchpad.net

Description of the change

I'm not particularly thrilled with the method names, but this branch implements a get/set/del/watch api for unit resolved and unit relation resolved, to enable sending resolve requests to the unit agent.

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

Some comments:

[1]

+class ServiceUnitRelationResolvedAlreadyEnabled(StateError):
+ """The unit has already been marked resolved.

Documentation needs updating.

[2]

+ def get_resolved(self):
+ """Get the value of the resolved flag if any.

The result of this method isn't a flag.

[3]

+ def get_resolved(self):
(...)
+ except zookeeper.NoNodeException:
+ # We get to the same end state.
+ returnValue(None)

This is a bit misleading. We're just reading, so there's no "end state" to be
achieved. The existence of the file is actually what determines if resolved
was requested or not.

[4]

+ unit_resolve_path = "/units/%s/resolved" % self.internal_id

Given that this was used 5 times, it would be handy to have something like:

    @property
    def _unit_resolved_path
        return "/units/%s/resolved" % self.internal_id

Then you can use self._unit_resolved_path everywhere.

[5]

+ rel_resolved_path = "/units/%s/relation_resolved" % self._internal_id

Our file paths so far are all dashed. It'd be good to keep the convention
inside ZooKeeper too.

May also be factored out into a property as per the point above.

[6]

+ def set_relation_resolved(self, relation_map):
+ """Mark a unit's relations as in need of being resolved.

Doesn't this actually mean something like:

    "Mark the problem in the unit's relation as being resolved."

IOW, it's not requesting for the problem to be resolved, but stating
that it has been.

[7]

+ :param relation_map: A map of internal relation ids, to retry booleans.

This feels like a non-ideal API for a couple of reasons:

a) It's exposing internal ids rather than using the relation names as we have
   elsewhere.

b) set_relation_resolved({"db": True, "cache": False}) feels like marking db as
   resolved, and cache as unresolved.

It's a bit hard to suggest something else without knowing exactly how you
intend to use. Let's talk about it.

[8]

+ d1_keys = set(d1.keys())
+ d2_keys = set(d2.keys())
+
+ must_match = d1_keys.intersection(d2_keys)

As a hint, this could be spelled as:

must_match = set(d1).intersection(d2):

[9]

+ running = workflow_state in ("up",)

FWIW, this pattern feels slightly weird when there's a single value.
workflow_state == "up" would be more straightforward.

[10]

+ self.assertIdentical(results.pop(0).type_name, "created")

assertIdentical is being overused a bit. There's no reason why these should be
allocated in the same memory location.

review: Needs Fixing
Revision history for this message
Kapil Thangavelu (hazmat) wrote :
Download full text (3.3 KiB)

Excerpts from Gustavo Niemeyer's message of Tue Apr 26 18:04:27 UTC 2011:
> Review: Needs Fixing
> Some comments:
>
>
> [1]
>
> +class ServiceUnitRelationResolvedAlreadyEnabled(StateError):
> + """The unit has already been marked resolved.
>
> Documentation needs updating.

how so?

>
> [2]
>
> + def get_resolved(self):
> + """Get the value of the resolved flag if any.
>
> The result of this method isn't a flag.
>

fixed.

> [3]
>
> + def get_resolved(self):
> (...)
> + except zookeeper.NoNodeException:
> + # We get to the same end state.
> + returnValue(None)
>
> This is a bit misleading. We're just reading, so there's no "end state" to be
> achieved. The existence of the file is actually what determines if resolved
> was requested or not.
>

fixed.

> [4]
>
> + unit_resolve_path = "/units/%s/resolved" % self.internal_id
>
> Given that this was used 5 times, it would be handy to have something like:
>
> @property
> def _unit_resolved_path
> return "/units/%s/resolved" % self.internal_id
>
> Then you can use self._unit_resolved_path everywhere.
>

done.

> [5]
>
> + rel_resolved_path = "/units/%s/relation_resolved" % self._internal_id
>
> Our file paths so far are all dashed. It'd be good to keep the convention
> inside ZooKeeper too.
>
> May also be factored out into a property as per the point above.

done
>
> [6]
>
> + def set_relation_resolved(self, relation_map):
> + """Mark a unit's relations as in need of being resolved.
>
> Doesn't this actually mean something like:
>
> "Mark the problem in the unit's relation as being resolved."
>
> IOW, it's not requesting for the problem to be resolved, but stating
> that it has been.
>

It is actually making a request for the problem to be solved, not solving the
problem directly, as there are behavioral aspects to transitions that need be
done inside of the unit agent.

> [7]
>
> + :param relation_map: A map of internal relation ids, to retry booleans.
>
> This feels like a non-ideal API for a couple of reasons:
>
> a) It's exposing internal ids rather than using the relation names as we have
> elsewhere.
>
> b) set_relation_resolved({"db": True, "cache": False}) feels like marking db as
> resolved, and cache as unresolved.
>
> It's a bit hard to suggest something else without knowing exactly how you
> intend to use. Let's talk about it.
>

As per irc discussion, we've deferred a) to a latter time, there's a commit in the branch
which does switch the syntax to

set_relation_resolved(db, retry_hooks=True)

but its then inconsistent with its get_resolved api which retrieves a dictionary of
relation ids : retry hook boolean

> [8]
>
> + d1_keys = set(d1.keys())
> + d2_keys = set(d2.keys())
> +
> + must_match = d1_keys.intersection(d2_keys)
>
> As a hint, this could be spelled as:
>
> must_match = set(d1).intersection(d2):

fixed.

>
> [9]
>
> + running = workflow_state in ("up",)
>
> FWIW, this pattern feels slightly weird when there's a single value.
> workflow_state == "up" would be more straightforward.
>
> [10]
>
> + self.assert...

Read more...

lp:~hazmat/pyjuju/resolved-state-api updated
202. By Kapil Thangavelu

merge trunk and pep8ify it.

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

Looks good, +1. Just two follow ups on your questioning above:

> > [1]
> >
> > +class ServiceUnitRelationResolvedAlreadyEnabled(StateError):
> > + """The unit has already been marked resolved.
> >
> > Documentation needs updating.
>
> how so?

s/unit/relation/

> > [6]
> >
> > + def set_relation_resolved(self, relation_map):
> > + """Mark a unit's relations as in need of being resolved.
> >
> > Doesn't this actually mean something like:
> >
> > "Mark the problem in the unit's relation as being resolved."
> >
> > IOW, it's not requesting for the problem to be resolved, but stating
> > that it has been.
> >
>
> It is actually making a request for the problem to be solved, not solving the
> problem directly, as there are behavioral aspects to transitions that need be
> done inside of the unit agent.

By the time someone calls "resolved", the *problem* which caused the hook to fail
must already have been fixed (hence the 'd' in "resolved"). We're not fixing the
problem with that action, we're simply stating that it's fine to go ahead because
someone else hopefully already fixed it.

review: Approve
lp:~hazmat/pyjuju/resolved-state-api updated
203. By Kapil Thangavelu

merge trunk conflict

204. By Kapil Thangavelu

merge trunk

205. By Kapil Thangavelu

document set_relation_resolved api todo discussion

206. By Kapil Thangavelu

doc string cleanups per review.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'ensemble/state/errors.py'
--- ensemble/state/errors.py 2011-05-04 07:40:25 +0000
+++ ensemble/state/errors.py 2011-05-05 16:20:19 +0000
@@ -21,6 +21,7 @@
21 """Exception value to denote watching should stop.21 """Exception value to denote watching should stop.
22 """22 """
2323
24
24class StateNotFound(StateError):25class StateNotFound(StateError):
25 """State not found.26 """State not found.
2627
@@ -151,6 +152,30 @@
151 self.unit_name152 self.unit_name
152153
153154
155class ServiceUnitResolvedAlreadyEnabled(StateError):
156 """The unit has already been marked resolved.
157 """
158
159 def __init__(self, unit_name):
160 self.unit_name = unit_name
161
162 def __str__(self):
163 return "Service unit %r is already marked as resolved." % (
164 self.unit_name)
165
166
167class ServiceUnitRelationResolvedAlreadyEnabled(StateError):
168 """The relation has already been marked resolved.
169 """
170
171 def __init__(self, unit_name):
172 self.unit_name = unit_name
173
174 def __str__(self):
175 return "Service unit %r already has relations marked as resolved." % (
176 self.unit_name)
177
178
154class RelationAlreadyExists(StateError):179class RelationAlreadyExists(StateError):
155180
156 def __init__(self, *endpoints):181 def __init__(self, *endpoints):
157182
=== modified file 'ensemble/state/service.py'
--- ensemble/state/service.py 2011-05-05 09:41:11 +0000
+++ ensemble/state/service.py 2011-05-05 16:20:19 +0000
@@ -15,11 +15,16 @@
15 StateChanged, ServiceStateNotFound, ServiceUnitStateNotFound,15 StateChanged, ServiceStateNotFound, ServiceUnitStateNotFound,
16 ServiceUnitStateMachineAlreadyAssigned, ServiceStateNameInUse,16 ServiceUnitStateMachineAlreadyAssigned, ServiceStateNameInUse,
17 BadDescriptor, BadServiceStateName, NoUnusedMachines,17 BadDescriptor, BadServiceStateName, NoUnusedMachines,
18 ServiceUnitDebugAlreadyEnabled)18 ServiceUnitDebugAlreadyEnabled, ServiceUnitResolvedAlreadyEnabled,
19 ServiceUnitRelationResolvedAlreadyEnabled)
19from ensemble.state.formula import FormulaStateManager20from ensemble.state.formula import FormulaStateManager
20from ensemble.state.relation import ServiceRelationState, RelationStateManager21from ensemble.state.relation import ServiceRelationState, RelationStateManager
21from ensemble.state.machine import _public_machine_id, MachineState22from ensemble.state.machine import _public_machine_id, MachineState
22from ensemble.state.utils import remove_tree, YAMLState23
24from ensemble.state.utils import remove_tree, dict_merge, YAMLState
25
26RETRY_HOOKS = 1000
27NO_HOOKS = 1001
2328
2429
25class ServiceStateManager(StateBase):30class ServiceStateManager(StateBase):
@@ -877,6 +882,193 @@
877 callback_d.addCallback(882 callback_d.addCallback(
878 lambda x: watch_d.addCallback(watcher) and x)883 lambda x: watch_d.addCallback(watcher) and x)
879884
885 @property
886 def _unit_resolve_path(self):
887 return "/units/%s/resolved" % self.internal_id
888
889 @inlineCallbacks
890 def set_resolved(self, retry):
891 """Mark the unit as in need of being resolved.
892
893 :param retry: A boolean denoting if hooks should fire as a result
894 of the retry.
895
896 The resolved setting is set by the command line to inform
897 a unit to attempt an retry transition from an error state.
898 """
899
900 if not retry in (RETRY_HOOKS, NO_HOOKS):
901 raise ValueError("invalid retry value %r" % retry)
902
903 try:
904 yield self._client.create(
905 self._unit_resolve_path, yaml.safe_dump({"retry": retry}))
906 except zookeeper.NodeExistsException:
907 raise ServiceUnitResolvedAlreadyEnabled(self.unit_name)
908
909 @inlineCallbacks
910 def get_resolved(self):
911 """Get the value of the resolved setting if any.
912
913 The resolved setting is retrieved by the unit agent and if
914 found instructs it to attempt an retry transition from an
915 error state.
916 """
917 try:
918 content, stat = yield self._client.get(self._unit_resolve_path)
919 except zookeeper.NoNodeException:
920 # Return a default value.
921 returnValue(None)
922 returnValue(yaml.load(content))
923
924 @inlineCallbacks
925 def clear_resolved(self):
926 """Remove any resolved setting on the unit."""
927 try:
928 yield self._client.delete(self._unit_resolve_path)
929 except zookeeper.NoNodeException:
930 # We get to the same end state.
931 pass
932
933 @inlineCallbacks
934 def watch_resolved(self, callback):
935 """Set a callback to be invoked when an unit is marked resolved.
936
937 :param callback: The callback recieves a single parameter, the
938 change event. The watcher always recieve an initial
939 boolean value invocation denoting the existence of the
940 resolved setting. Subsequent invocations will be with change
941 events.
942 """
943 @inlineCallbacks
944 def watcher(change_event):
945 if not self._client.connected:
946 returnValue(None)
947 exists_d, watch_d = self._client.exists_and_watch(
948 self._unit_resolve_path)
949 yield callback(change_event)
950 watch_d.addCallback(watcher)
951
952 exists_d, watch_d = self._client.exists_and_watch(
953 self._unit_resolve_path)
954 exists = yield exists_d
955
956 # Setup the watch deferred callback after the user defined callback
957 # has returned successfully from the existence invocation.
958 callback_d = maybeDeferred(callback, bool(exists))
959 callback_d.addCallback(
960 lambda x: watch_d.addCallback(watcher) and x)
961
962 @property
963 def _relation_resolved_path(self):
964 return "/units/%s/relation-resolved" % self.internal_id
965
966 @inlineCallbacks
967 def set_relation_resolved(self, relation_map):
968 """Mark a unit's relations as being resolved.
969
970 The unit agent will watch this setting and unblock the unit,
971 via manipulation of the unit workflow and lifecycle.
972
973 :param relation_map: A map of internal relation ids, to retry hook
974 values either ensemble.state.service.NO_HOOKS or
975 RETRY_HOOKS.
976
977 TODO:
978 The api currently takes internal relation ids, this should be
979 cleaned up with a refactor to state request protocol objects.
980 Only public names should be exposed beyond the state api.
981
982 There's an ongoing discussion on whether this needs to support
983 retries. Currently it doesn't, and this could the arg to this
984 method could just be a list of relations. Supporting retries
985 would mean capturing enough information to retry the hook, and
986 has reconciliation issues wrt to what's current at the time of
987 re-execution. The existing hook scheduler automatically
988 performs merges of redundant events. The retry could execute a
989 relation change hook, for a remote unit that has already
990 departed at the time of re-execution (and for which we have
991 a pending hook execution), which would be inconsistent, wrt
992 to what would be exposed via the hook cli api. With support
993 for on disk persistence and recovery, some of this temporal
994 synchronization would already be in place.
995 """
996 if not isinstance(relation_map, dict):
997 raise ValueError(
998 "Relation map must be a dictionary %r" % relation_map)
999
1000 if [v for v in relation_map.values() if v not in (
1001 RETRY_HOOKS, NO_HOOKS)]:
1002
1003 print relation_map
1004 raise ValueError("Invalid setting for retry hook")
1005
1006 def update_relation_resolved(content, stat):
1007 if not content:
1008 return yaml.safe_dump(relation_map)
1009
1010 content = yaml.safe_dump(
1011 dict_merge(yaml.load(content), relation_map))
1012 return content
1013
1014 try:
1015 yield retry_change(
1016 self._client,
1017 self._relation_resolved_path,
1018 update_relation_resolved)
1019 except StateChanged:
1020 raise ServiceUnitRelationResolvedAlreadyEnabled(self.unit_name)
1021 returnValue(True)
1022
1023 @inlineCallbacks
1024 def get_relation_resolved(self):
1025 """Retrieve any resolved flags set for this unit's relations.
1026 """
1027 try:
1028 content, stat = yield self._client.get(
1029 self._relation_resolved_path)
1030 except zookeeper.NoNodeException:
1031 returnValue(None)
1032 returnValue(yaml.load(content))
1033
1034 @inlineCallbacks
1035 def clear_relation_resolved(self):
1036 try:
1037 yield self._client.delete(self._relation_resolved_path)
1038 except zookeeper.NoNodeException:
1039 # We get to the same end state.
1040 pass
1041
1042 @inlineCallbacks
1043 def watch_relation_resolved(self, callback):
1044 """Set a callback to be invoked when a unit's relations are resolved.
1045
1046 :param callback: The callback recieves a single parameter, the
1047 change event. The watcher always recieve an initial
1048 boolean value invocation denoting the existence of the
1049 resolved setting. Subsequent invocations will be with change
1050 events.
1051 """
1052 @inlineCallbacks
1053 def watcher(change_event):
1054 if not self._client.connected:
1055 returnValue(None)
1056 exists_d, watch_d = self._client.exists_and_watch(
1057 self._relation_resolved_path)
1058 yield callback(change_event)
1059 watch_d.addCallback(watcher)
1060
1061 exists_d, watch_d = self._client.exists_and_watch(
1062 self._relation_resolved_path)
1063
1064 exists = yield exists_d
1065
1066 # Setup the watch deferred callback after the user defined callback
1067 # has returned successfully from the existence invocation.
1068 callback_d = maybeDeferred(callback, bool(exists))
1069 callback_d.addCallback(
1070 lambda x: watch_d.addCallback(watcher) and x)
1071
8801072
881def _parse_unit_name(unit_name):1073def _parse_unit_name(unit_name):
882 """Parse a unit's name into the service name and its sequence.1074 """Parse a unit's name into the service name and its sequence.
8831075
=== modified file 'ensemble/state/tests/test_errors.py'
--- ensemble/state/tests/test_errors.py 2011-02-23 17:47:46 +0000
+++ ensemble/state/tests/test_errors.py 2011-05-05 16:20:19 +0000
@@ -11,7 +11,9 @@
11 UnitRelationStateAlreadyAssigned, UnknownRelationRole,11 UnitRelationStateAlreadyAssigned, UnknownRelationRole,
12 BadDescriptor, DuplicateEndpoints, IncompatibleEndpoints,12 BadDescriptor, DuplicateEndpoints, IncompatibleEndpoints,
13 NoMatchingEndpoints, AmbiguousEndpoints,13 NoMatchingEndpoints, AmbiguousEndpoints,
14 ServiceUnitStateMachineNotAssigned, ServiceUnitDebugAlreadyEnabled)14 ServiceUnitStateMachineNotAssigned, ServiceUnitDebugAlreadyEnabled,
15 ServiceUnitResolvedAlreadyEnabled,
16 ServiceUnitRelationResolvedAlreadyEnabled)
1517
1618
17class StateErrorsTest(TestCase):19class StateErrorsTest(TestCase):
@@ -89,6 +91,23 @@
89 str(error),91 str(error),
90 "Service unit 'wordpress/0' is already in debug mode.")92 "Service unit 'wordpress/0' is already in debug mode.")
9193
94 def test_unit_already_in_resolved_mode(self):
95 error = ServiceUnitResolvedAlreadyEnabled("wordpress/0")
96 self.assertIsStateError(error)
97 self.assertEquals(error.unit_name, "wordpress/0")
98 self.assertEquals(
99 str(error),
100 "Service unit 'wordpress/0' is already marked as resolved.")
101
102 def test_unit_already_in_relation_resolved_mode(self):
103 error = ServiceUnitRelationResolvedAlreadyEnabled("wordpress/0")
104 self.assertIsStateError(error)
105 self.assertEquals(error.unit_name, "wordpress/0")
106 self.assertEquals(
107 str(error),
108 "Service unit %r already has relations marked as resolved." % (
109 "wordpress/0"))
110
92 def test_service_name_in_use(self):111 def test_service_name_in_use(self):
93 error = ServiceStateNameInUse("wordpress")112 error = ServiceStateNameInUse("wordpress")
94 self.assertIsStateError(error)113 self.assertIsStateError(error)
95114
=== modified file 'ensemble/state/tests/test_service.py'
--- ensemble/state/tests/test_service.py 2011-05-05 09:41:11 +0000
+++ ensemble/state/tests/test_service.py 2011-05-05 16:20:19 +0000
@@ -9,14 +9,15 @@
9from ensemble.formula.tests.test_metadata import test_repository_path9from ensemble.formula.tests.test_metadata import test_repository_path
10from ensemble.state.endpoint import RelationEndpoint10from ensemble.state.endpoint import RelationEndpoint
11from ensemble.state.formula import FormulaStateManager11from ensemble.state.formula import FormulaStateManager
12from ensemble.state.service import ServiceStateManager12from ensemble.state.service import ServiceStateManager, NO_HOOKS, RETRY_HOOKS
13from ensemble.state.machine import MachineStateManager13from ensemble.state.machine import MachineStateManager
14from ensemble.state.relation import RelationStateManager14from ensemble.state.relation import RelationStateManager
15from ensemble.state.errors import (15from ensemble.state.errors import (
16 StateChanged, ServiceStateNotFound, ServiceUnitStateNotFound,16 StateChanged, ServiceStateNotFound, ServiceUnitStateNotFound,
17 ServiceUnitStateMachineAlreadyAssigned, ServiceStateNameInUse,17 ServiceUnitStateMachineAlreadyAssigned, ServiceStateNameInUse,
18 BadDescriptor, BadServiceStateName, ServiceUnitDebugAlreadyEnabled,18 BadDescriptor, BadServiceStateName, ServiceUnitDebugAlreadyEnabled,
19 MachineStateNotFound, NoUnusedMachines)19 MachineStateNotFound, NoUnusedMachines, ServiceUnitResolvedAlreadyEnabled,
20 ServiceUnitRelationResolvedAlreadyEnabled)
2021
2122
22from ensemble.state.tests.common import StateTestBase23from ensemble.state.tests.common import StateTestBase
@@ -739,6 +740,213 @@
739 None)740 None)
740741
741 @inlineCallbacks742 @inlineCallbacks
743 def test_get_set_clear_resolved(self):
744 """The a unit can be set to resolved to mark a future transition, with
745 an optional retry flag."""
746
747 unit_state = yield self.get_unit_state()
748
749 self.assertIdentical((yield unit_state.get_resolved()), None)
750 yield unit_state.set_resolved(NO_HOOKS)
751
752 yield self.assertFailure(
753 unit_state.set_resolved(NO_HOOKS),
754 ServiceUnitResolvedAlreadyEnabled)
755 yield self.assertEqual(
756
757 (yield unit_state.get_resolved()), {"retry": NO_HOOKS})
758
759 yield unit_state.clear_resolved()
760 self.assertIdentical((yield unit_state.get_resolved()), None)
761 yield unit_state.clear_resolved()
762
763 yield self.assertFailure(unit_state.set_resolved(None), ValueError)
764
765 @inlineCallbacks
766 def test_watch_resolved(self):
767 """A unit resolved watch can be instituted on a permanent basis."""
768 unit_state = yield self.get_unit_state()
769
770 results = []
771
772 def callback(value):
773 results.append(value)
774
775 unit_state.watch_resolved(callback)
776 yield unit_state.set_resolved(RETRY_HOOKS)
777 yield unit_state.clear_resolved()
778 yield unit_state.set_resolved(NO_HOOKS)
779
780 yield self.poke_zk()
781
782 self.assertEqual(len(results), 4)
783 self.assertIdentical(results.pop(0), False)
784 self.assertEqual(results.pop(0).type_name, "created")
785 self.assertEqual(results.pop(0).type_name, "deleted")
786 self.assertEqual(results.pop(0).type_name, "created")
787
788 self.assertEqual(
789 (yield unit_state.get_resolved()),
790 {"retry": NO_HOOKS})
791
792 @inlineCallbacks
793 def test_get_set_clear_relation_resolved(self):
794 """The a unit's realtions can be set to resolved to mark a
795 future transition, with an optional retry flag."""
796
797 unit_state = yield self.get_unit_state()
798
799 self.assertIdentical((yield unit_state.get_relation_resolved()), None)
800 yield unit_state.set_relation_resolved({"0": RETRY_HOOKS})
801
802 # Trying to set a conflicting raises an error
803 yield self.assertFailure(
804 unit_state.set_relation_resolved({"0": NO_HOOKS}),
805 ServiceUnitRelationResolvedAlreadyEnabled)
806
807 # Doing the same thing is fine
808 yield unit_state.set_relation_resolved({"0": RETRY_HOOKS}),
809
810 # Its fine to put in new values
811 yield unit_state.set_relation_resolved({"21": RETRY_HOOKS})
812 yield self.assertEqual(
813 (yield unit_state.get_relation_resolved()),
814 {"0": RETRY_HOOKS, "21": RETRY_HOOKS})
815
816 yield unit_state.clear_relation_resolved()
817 self.assertIdentical((yield unit_state.get_relation_resolved()), None)
818 yield unit_state.clear_relation_resolved()
819
820 yield self.assertFailure(
821 unit_state.set_relation_resolved(True), ValueError)
822 yield self.assertFailure(
823 unit_state.set_relation_resolved(None), ValueError)
824
825 @inlineCallbacks
826 def test_watch_relation_resolved(self):
827 """A unit resolved watch can be instituted on a permanent basis."""
828 unit_state = yield self.get_unit_state()
829
830 results = []
831
832 def callback(value):
833 results.append(value)
834
835 unit_state.watch_relation_resolved(callback)
836 yield unit_state.set_relation_resolved({"0": RETRY_HOOKS})
837 yield unit_state.clear_relation_resolved()
838 yield unit_state.set_relation_resolved({"0": NO_HOOKS})
839
840 yield self.poke_zk()
841
842 self.assertEqual(len(results), 4)
843 self.assertIdentical(results.pop(0), False)
844 self.assertEqual(results.pop(0).type_name, "created")
845 self.assertEqual(results.pop(0).type_name, "deleted")
846 self.assertEqual(results.pop(0).type_name, "created")
847
848 self.assertEqual(
849 (yield unit_state.get_relation_resolved()),
850 {"0": NO_HOOKS})
851
852 @inlineCallbacks
853 def test_watch_resolved_slow_callback(self):
854 """A slow watch callback is still invoked serially."""
855 unit_state = yield self.get_unit_state()
856
857 callbacks = [Deferred() for i in range(5)]
858 results = []
859 contents = []
860
861 @inlineCallbacks
862 def watch(value):
863 results.append(value)
864 yield callbacks[len(results) - 1]
865 contents.append((yield unit_state.get_resolved()))
866
867 yield unit_state.watch_resolved(watch)
868
869 # These get collapsed into a single event
870 yield unit_state.set_resolved(RETRY_HOOKS)
871 yield unit_state.clear_resolved()
872 yield self.poke_zk()
873
874 # Verify the callback hasn't completed
875 self.assertEqual(len(results), 1)
876 self.assertEqual(len(contents), 0)
877
878 # Let it finish
879 callbacks[0].callback(True)
880 yield self.poke_zk()
881
882 # Verify result counts
883 self.assertEqual(len(results), 2)
884 self.assertEqual(len(contents), 1)
885
886 # Verify result values. Even though we have created event, the
887 # setting retrieved shows the hook is not enabled.
888 self.assertEqual(results[-1].type_name, "created")
889 self.assertEqual(contents[-1], None)
890
891 yield unit_state.set_resolved(NO_HOOKS)
892 callbacks[1].callback(True)
893 yield self.poke_zk()
894
895 self.assertEqual(len(results), 3)
896 self.assertEqual(contents[-1], {"retry": NO_HOOKS})
897
898 # Clear out any pending activity.
899 yield self.poke_zk()
900
901 @inlineCallbacks
902 def test_watch_relation_resolved_slow_callback(self):
903 """A slow watch callback is still invoked serially."""
904 unit_state = yield self.get_unit_state()
905
906 callbacks = [Deferred() for i in range(5)]
907 results = []
908 contents = []
909
910 @inlineCallbacks
911 def watch(value):
912 results.append(value)
913 yield callbacks[len(results) - 1]
914 contents.append((yield unit_state.get_relation_resolved()))
915
916 yield unit_state.watch_relation_resolved(watch)
917
918 # These get collapsed into a single event
919 yield unit_state.set_relation_resolved({"0": RETRY_HOOKS})
920 yield unit_state.clear_relation_resolved()
921 yield self.poke_zk()
922
923 # Verify the callback hasn't completed
924 self.assertEqual(len(results), 1)
925 self.assertEqual(len(contents), 0)
926
927 # Let it finish
928 callbacks[0].callback(True)
929 yield self.poke_zk()
930
931 # Verify result counts
932 self.assertEqual(len(results), 2)
933 self.assertEqual(len(contents), 1)
934
935 # Verify result values. Even though we have created event, the
936 # setting retrieved shows the hook is not enabled.
937 self.assertEqual(results[-1].type_name, "created")
938 self.assertEqual(contents[-1], None)
939
940 yield unit_state.set_relation_resolved({"0": RETRY_HOOKS})
941 callbacks[1].callback(True)
942 yield self.poke_zk()
943
944 self.assertEqual(len(results), 3)
945 self.assertEqual(contents[-1], {"0": RETRY_HOOKS})
946 # Clear out any pending activity.
947 yield self.poke_zk()
948
949 @inlineCallbacks
742 def test_set_and_clear_upgrade_flag(self):950 def test_set_and_clear_upgrade_flag(self):
743 """An upgrade flag can be set on a unit."""951 """An upgrade flag can be set on a unit."""
744952
@@ -871,6 +1079,9 @@
871 self.assertEqual(results[-1].type_name, "created")1079 self.assertEqual(results[-1].type_name, "created")
872 self.assertEqual(contents[-1], True)1080 self.assertEqual(contents[-1], True)
8731081
1082 # Clear out any pending activity.
1083 yield self.poke_zk()
1084
874 @inlineCallbacks1085 @inlineCallbacks
875 def test_enable_debug_hook(self):1086 def test_enable_debug_hook(self):
876 """Unit hook debugging can be enabled on the unit state."""1087 """Unit hook debugging can be enabled on the unit state."""
@@ -1063,6 +1274,9 @@
1063 self.assertEqual(results[-1].type_name, "created")1274 self.assertEqual(results[-1].type_name, "created")
1064 self.assertEqual(contents[-1], {"debug_hooks": ["*"]})1275 self.assertEqual(contents[-1], {"debug_hooks": ["*"]})
10651276
1277 # Clear out any pending activity.
1278 yield self.poke_zk()
1279
1066 @inlineCallbacks1280 @inlineCallbacks
1067 def test_service_unit_agent(self):1281 def test_service_unit_agent(self):
1068 """A service unit state has an associated unit agent."""1282 """A service unit state has an associated unit agent."""
10691283
=== modified file 'ensemble/state/tests/test_utils.py'
--- ensemble/state/tests/test_utils.py 2011-05-03 08:54:13 +0000
+++ ensemble/state/tests/test_utils.py 2011-05-05 16:20:19 +0000
@@ -12,9 +12,10 @@
12import yaml12import yaml
1313
14from ensemble.lib.testing import TestCase14from ensemble.lib.testing import TestCase
15from ensemble.state.errors import StateNotFound15from ensemble.state.errors import StateChanged, StateNotFound
16from ensemble.state.utils import (PortWatcher, remove_tree,16from ensemble.state.utils import (PortWatcher, remove_tree, dict_merge,
17 get_open_port, YAMLState)17 get_open_port, YAMLState)
18
18from ensemble.tests.common import get_test_zookeeper_address19from ensemble.tests.common import get_test_zookeeper_address
1920
2021
@@ -206,6 +207,26 @@
206 self.assertNotIn("zoo", children)207 self.assertNotIn("zoo", children)
207208
208209
210class DictMergeTest(TestCase):
211
212 def test_merge_no_match(self):
213 self.assertEqual(
214 dict_merge(dict(a=1), dict(b=2)),
215 dict(a=1, b=2))
216
217 def test_merge_matching_keys_same_value(self):
218 self.assertEqual(
219 dict_merge(dict(a=1, b=2), dict(b=2, c=1)),
220 dict(a=1, b=2, c=1))
221
222 def test_merge_conflict(self):
223 self.assertRaises(
224 StateChanged,
225 dict_merge,
226 dict(a=1, b=3),
227 dict(b=2, c=1))
228
229
209class OpenPortTest(TestCase):230class OpenPortTest(TestCase):
210231
211 def test_get_open_port(self):232 def test_get_open_port(self):
@@ -301,7 +322,6 @@
301 zk_data = yaml.load(zk_data)322 zk_data = yaml.load(zk_data)
302 self.assertEqual(zk_data, options)323 self.assertEqual(zk_data, options)
303324
304
305 @inlineCallbacks325 @inlineCallbacks
306 def test_conflict_on_set(self):326 def test_conflict_on_set(self):
307 """Version conflict error tests.327 """Version conflict error tests.
@@ -357,8 +377,8 @@
357 yield node.read()377 yield node.read()
358378
359 options = dict(alpha="beta", one=1)379 options = dict(alpha="beta", one=1)
360 node["alpha"] = "beta"380 node["alpha"] = "beta"
361 node["one"] = 1381 node["one"] = 1
362 yield node.write()382 yield node.write()
363383
364 # a local get should reflect proper data384 # a local get should reflect proper data
@@ -369,7 +389,6 @@
369 zk_data = yaml.load(zk_data)389 zk_data = yaml.load(zk_data)
370 self.assertEqual(zk_data, options)390 self.assertEqual(zk_data, options)
371391
372
373 @inlineCallbacks392 @inlineCallbacks
374 def test_multiple_reads(self):393 def test_multiple_reads(self):
375 """Calling read resets state to ZK after multiple round-trips."""394 """Calling read resets state to ZK after multiple round-trips."""
@@ -415,7 +434,7 @@
415434
416 result = node.pop("foo")435 result = node.pop("foo")
417 self.assertEqual(result, "bar")436 self.assertEqual(result, "bar")
418 self.assertEqual(node, {"alpha": "beta",})437 self.assertEqual(node, {"alpha": "beta"})
419438
420 node["delta"] = "gamma"439 node["delta"] = "gamma"
421 self.assertEqual(set(node.keys()), set(("alpha", "delta")))440 self.assertEqual(set(node.keys()), set(("alpha", "delta")))
@@ -424,7 +443,6 @@
424 self.assertIn(("alpha", "beta"), result)443 self.assertIn(("alpha", "beta"), result)
425 self.assertIn(("delta", "gamma"), result)444 self.assertIn(("delta", "gamma"), result)
426445
427
428 @inlineCallbacks446 @inlineCallbacks
429 def test_del_empties_state(self):447 def test_del_empties_state(self):
430 d = YAMLState(self.client, self.path)448 d = YAMLState(self.client, self.path)
@@ -500,12 +518,8 @@
500 yield d1.read()518 yield d1.read()
501 self.assertEquals(d1, d2)519 self.assertEquals(d1, d2)
502520
503
504 @inlineCallbacks521 @inlineCallbacks
505 def test_read_requires_node(self):522 def test_read_requires_node(self):
506 """Validate that read raises when required=True."""523 """Validate that read raises when required=True."""
507 d1 = YAMLState(self.client, self.path)524 d1 = YAMLState(self.client, self.path)
508 yield self.assertFailure(d1.read(True), StateNotFound)525 yield self.assertFailure(d1.read(True), StateNotFound)
509
510
511
512526
=== modified file 'ensemble/state/utils.py'
--- ensemble/state/utils.py 2011-05-03 08:54:13 +0000
+++ ensemble/state/utils.py 2011-05-05 16:20:19 +0000
@@ -9,8 +9,10 @@
9import yaml9import yaml
10import zookeeper10import zookeeper
1111
12from ensemble.state.errors import StateChanged
12from ensemble.state.errors import StateNotFound13from ensemble.state.errors import StateNotFound
1314
15
14class PortWatcher(object):16class PortWatcher(object):
1517
16 def __init__(self, host, port, timeout, listen=False):18 def __init__(self, host, port, timeout, listen=False):
@@ -91,6 +93,22 @@
91 return port93 return port
9294
9395
96def dict_merge(d1, d2):
97 """Return a union of dicts if they have no conflicting values.
98
99 Else raise a StateChanged error.
100 """
101 must_match = set(d1).intersection(d2)
102 for k in must_match:
103 if not d1[k] == d2[k]:
104 raise StateChanged()
105
106 d = {}
107 d.update(d1)
108 d.update(d2)
109 return d
110
111
94class YAMLState(DictMixin):112class YAMLState(DictMixin):
95 """Provides a dict like interface around a Zookeeper node113 """Provides a dict like interface around a Zookeeper node
96 containing serialised YAML data. The dict provided represents the114 containing serialised YAML data. The dict provided represents the
@@ -144,7 +162,6 @@
144 if required:162 if required:
145 raise StateNotFound(self._path)163 raise StateNotFound(self._path)
146164
147
148 def _check(self):165 def _check(self):
149 """Verify that sync was called for operations which expect it."""166 """Verify that sync was called for operations which expect it."""
150 if self._pristine_cache is None:167 if self._pristine_cache is None:
@@ -164,7 +181,6 @@
164 self._check()181 self._check()
165 self._cache[key] = value182 self._cache[key] = value
166183
167
168 def __delitem__(self, key):184 def __delitem__(self, key):
169 self._check()185 self._check()
170 del self._cache[key]186 del self._cache[key]
171187
=== modified file 'ensemble/unit/lifecycle.py'
--- ensemble/unit/lifecycle.py 2011-05-05 09:41:11 +0000
+++ ensemble/unit/lifecycle.py 2011-05-05 16:20:19 +0000
@@ -106,6 +106,7 @@
106 # Stop relation lifecycles106 # Stop relation lifecycles
107 if self._relations:107 if self._relations:
108 self._log.debug("stopping relation lifecycles")108 self._log.debug("stopping relation lifecycles")
109
109 for workflow in self._relations.values():110 for workflow in self._relations.values():
110 yield workflow.transition_state("down")111 yield workflow.transition_state("down")
111112
112113
=== modified file 'ensemble/unit/tests/test_workflow.py'
--- ensemble/unit/tests/test_workflow.py 2011-05-05 09:41:11 +0000
+++ ensemble/unit/tests/test_workflow.py 2011-05-05 16:20:19 +0000
@@ -10,7 +10,7 @@
1010
11from ensemble.unit.workflow import (11from ensemble.unit.workflow import (
12 UnitWorkflowState, RelationWorkflowState, WorkflowStateClient,12 UnitWorkflowState, RelationWorkflowState, WorkflowStateClient,
13 is_unit_running)13 is_unit_running, is_relation_running)
1414
1515
16class WorkflowTestBase(LifecycleTestBase):16class WorkflowTestBase(LifecycleTestBase):
@@ -367,6 +367,26 @@
367 self.state_directory)367 self.state_directory)
368368
369 @inlineCallbacks369 @inlineCallbacks
370 def test_is_relation_running(self):
371 """The unit relation's workflow state can be categorized as a
372 boolean.
373 """
374 running, state = yield is_relation_running(
375 self.client, self.states["unit_relation"])
376 self.assertIdentical(running, False)
377 self.assertIdentical(state, None)
378 yield self.workflow.fire_transition("start")
379 running, state = yield is_relation_running(
380 self.client, self.states["unit_relation"])
381 self.assertIdentical(running, True)
382 self.assertEqual(state, "up")
383 yield self.workflow.fire_transition("stop")
384 running, state = yield is_relation_running(
385 self.client, self.states["unit_relation"])
386 self.assertIdentical(running, False)
387 self.assertEqual(state, "down")
388
389 @inlineCallbacks
370 def test_up_down_cycle(self):390 def test_up_down_cycle(self):
371 """The workflow can be transition from up to down, and back.391 """The workflow can be transition from up to down, and back.
372 """392 """
373393
=== modified file 'ensemble/unit/workflow.py'
--- ensemble/unit/workflow.py 2011-05-05 09:41:11 +0000
+++ ensemble/unit/workflow.py 2011-05-05 16:20:19 +0000
@@ -78,12 +78,26 @@
78 """Is the service unit in a running state.78 """Is the service unit in a running state.
7979
80 Returns a boolean which is true if the unit is running, and80 Returns a boolean which is true if the unit is running, and
81 the unit state in two element tuple.81 the unit workflow state in a two element tuple.
82 """82 """
83 workflow_state = yield WorkflowStateClient(client, unit).get_state()83 workflow_state = yield WorkflowStateClient(client, unit).get_state()
84 if not workflow_state:84 if not workflow_state:
85 returnValue((False, None))85 returnValue((False, None))
86 running = workflow_state in ("started",)86 running = workflow_state == "started"
87 returnValue((running, workflow_state))
88
89
90@inlineCallbacks
91def is_relation_running(client, relation):
92 """Is the unit relation in a running state.
93
94 Returns a boolean which is true if the relation is running, and
95 the unit relation workflow state in a two element tuple.
96 """
97 workflow_state = yield WorkflowStateClient(client, relation).get_state()
98 if not workflow_state:
99 returnValue((False, None))
100 running = workflow_state == "up"
87 returnValue((running, workflow_state))101 returnValue((running, workflow_state))
88102
89103

Subscribers

People subscribed via source and target branches

to status/vote changes: