Merge lp:~danilo/charms/trusty/haproxy/merge-services-fix into lp:charms/trusty/haproxy

Proposed by Данило Шеган
Status: Merged
Merged at revision: 94
Proposed branch: lp:~danilo/charms/trusty/haproxy/merge-services-fix
Merge into: lp:charms/trusty/haproxy
Diff against target: 152 lines (+93/-17)
2 files modified
hooks/hooks.py (+53/-17)
hooks/tests/test_reverseproxy_hooks.py (+40/-0)
To merge this branch: bzr merge lp:~danilo/charms/trusty/haproxy/merge-services-fix
Reviewer Review Type Date Requested Status
David Britton (community) Approve
Whit Morriss (community) Needs Fixing
Björn Tillenius (community) Approve
Review via email: mp+259233@code.launchpad.net

Description of the change

Merge service backends properly even when new service has fewer backends than the old one

This is a problem we've noticed with Landscape deployments (trying to use multiple backends per unit), and we've started hitting this reliably (see linked bug).

The solution is simple: do not attempt to merge new_service backends which might not exist. Test is included.

Fix has been tested and works properly with landscape-charm at least.

FWIW, I've confirmed on haproxy-unit-ip:10000 (user haproxy, pass haproxy by default) that it does indeed work properly.

To post a comment you must log in.
Revision history for this message
Björn Tillenius (bjornt) :
review: Needs Information
94. By Данило Шеган

Refactor merge_service and improve test.

95. By Данило Шеган

Drop useless sorting of lists containing dicts.

96. By Данило Шеган

Add exception tests.

Revision history for this message
Данило Шеган (danilo) wrote :

I kind of tried to stay away from refactoring the method: I've suspected the problem already, but got tricked by my tests using assertItemsEqual which always passed :/

Now I do a full refactor, stressing readability and correctness over concise code (eg. even dropping use of groupby() to weed out duplicate entries).

97. By Данило Шеган

Uncomment apt-get that got in by accident.

Revision history for this message
Björn Tillenius (bjornt) wrote :

Thanks for doing the refactoring. It looks much better now, but there are still some minor issues. +1 after you've addressed them.

review: Approve
Revision history for this message
Björn Tillenius (bjornt) wrote :

I've addressed the review comments here:

 lp:~bjornt/charms/trusty/haproxy/merge-services-fix/

Please merge it in before landing.

Revision history for this message
Björn Tillenius (bjornt) :
Revision history for this message
Whit Morriss (whitmo) wrote :

Gave each branch a lookover and tested merging them and running tests

 - tests are missing an install for flake8
 - lint returns following errors:
     F401 'groupby' imported but unused
     E111 indentation is not a multiple of four

Deployment tests pass. Merge the Bjorn's branch and fix up the flake8 install and minor lint errors and should be good to go.

review: Needs Fixing
Revision history for this message
Björn Tillenius (bjornt) wrote :

> Gave each branch a lookover and tested merging them and running tests
>
> - tests are missing an install for flake8
> - lint returns following errors:
> F401 'groupby' imported but unused
> E111 indentation is not a multiple of four
>
> Deployment tests pass. Merge the Bjorn's branch and fix up the flake8 install
> and minor lint errors and should be good to go.

Thanks for the review Whit. I fixed the lint added python-flake8 to be installed
in my branch, since Danilo is away.

Revision history for this message
Whit Morriss (whitmo) wrote :

Thanks Bjorn!

-w

On Fri, May 15, 2015 at 11:40 AM, Björn Tillenius <email address hidden>
wrote:

> > Gave each branch a lookover and tested merging them and running tests
> >
> > - tests are missing an install for flake8
> > - lint returns following errors:
> > F401 'groupby' imported but unused
> > E111 indentation is not a multiple of four
> >
> > Deployment tests pass. Merge the Bjorn's branch and fix up the flake8
> install
> > and minor lint errors and should be good to go.
>
> Thanks for the review Whit. I fixed the lint added python-flake8 to be
> installed
> in my branch, since Danilo is away.
> --
>
> https://code.launchpad.net/~danilo/charms/trusty/haproxy/merge-services-fix/+merge/259233
> You are reviewing the proposed merge of
> lp:~danilo/charms/trusty/haproxy/merge-services-fix into
> lp:charms/trusty/haproxy.
>

--
---------------
D. Whit Morriss
Developer, Juju Ecosystem
Canonical USA

Revision history for this message
David Britton (dpb) :
review: Approve
Revision history for this message
Данило Шеган (danilo) wrote :

I was going to merge Bjorn's branch, but I see that it's all been handled. Thanks all!

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'hooks/hooks.py'
2--- hooks/hooks.py 2015-04-14 16:46:13 +0000
3+++ hooks/hooks.py 2015-05-15 14:11:47 +0000
4@@ -12,6 +12,7 @@
5 import pwd
6
7 from itertools import izip, tee, groupby
8+from operator import itemgetter
9
10 from charmhelpers.core.host import pwgen, lsb_release
11 from charmhelpers.core.hookenv import (
12@@ -96,6 +97,10 @@
13 ]
14
15
16+class InvalidRelationDataError(Exception):
17+ """Invalid data has been provided in the relation."""
18+
19+
20 # #############################################################################
21 # Supporting functions
22 # #############################################################################
23@@ -496,30 +501,61 @@
24 return services
25
26
27+def _add_items_if_missing(target, additions):
28+ """
29+ Append items from `additions` to `target` if they are not present already.
30+
31+ Returns a new list.
32+ """
33+ result = target[:]
34+ for addition in additions:
35+ if addition not in result:
36+ result.append(addition)
37+ return result
38+
39+
40 def merge_service(old_service, new_service):
41 """
42- Helper function to merge two serivce entries correctly.
43+ Helper function to merge two service entries correctly.
44 Everything will get trampled (preferring old_service), except "servers"
45 which will be unioned acrosss both entries, stripping strict dups.
46 """
47- service = {}
48 service = new_service.copy()
49 service.update(old_service)
50- if "servers" in service:
51- # Merge all 'servers' entries of the default backend
52- servers = service["servers"]
53- if "servers" in new_service:
54- servers.extend(new_service["servers"])
55- servers.sort()
56- service["servers"] = list(x for x, _ in groupby(servers))
57- if "backends" in service and "backends" in new_service:
58- # Merge all 'servers' entries of the additional backends
59- for i, backend in enumerate(service["backends"]):
60- servers = backend["servers"]
61- servers.extend(new_service["backends"][i]["servers"])
62- servers.sort()
63- service["backends"][i]["servers"] = list(
64- x for x, _ in groupby(servers))
65+
66+ # Merge all 'servers' entries of the default backend.
67+ if "servers" in old_service and "servers" in new_service:
68+ service["servers"] = _add_items_if_missing(
69+ old_service["servers"], new_service["servers"])
70+
71+ # Merge all 'backends' and their contained "servers".
72+ if "backends" in old_service and "backends" in new_service:
73+ backends_by_name = {}
74+ # Populate backends_by_name with backends from (old_)service.
75+ for backend in service["backends"]:
76+ backend_name = backend.get("backend_name")
77+ if backend_name is None:
78+ raise InvalidRelationDataError(
79+ "Each backend must have backend_name.")
80+ backends_by_name.setdefault(backend_name, backend)
81+
82+ # Go through backends in new_service and add them to backends_by_name,
83+ # merging 'servers' while at it.
84+ for backend in new_service["backends"]:
85+ backend_name = backend.get("backend_name")
86+ if backend_name is None:
87+ raise InvalidRelationDataError(
88+ "Each backend must have backend_name.")
89+ if backend_name in backends_by_name:
90+ # Merge servers.
91+ target_backend = backends_by_name[backend_name]
92+ target_backend["servers"] = _add_items_if_missing(
93+ target_backend["servers"], backend["servers"])
94+ else:
95+ backends_by_name[backend_name] = backend
96+
97+ service["backends"] = sorted(
98+ backends_by_name.values(), key=itemgetter('backend_name'))
99 return service
100
101
102
103=== modified file 'hooks/tests/test_reverseproxy_hooks.py'
104--- hooks/tests/test_reverseproxy_hooks.py 2015-04-14 14:21:45 +0000
105+++ hooks/tests/test_reverseproxy_hooks.py 2015-05-15 14:11:47 +0000
106@@ -489,6 +489,46 @@
107 expected = {'service_name': 'left', 'foo': 'bar', 'bar': 'baz'}
108 self.assertEqual(expected, hooks.merge_service(s1, s2))
109
110+ def test_merge_service_old_backend_without_name(self):
111+ """Backends in old_service without name raise an exception."""
112+
113+ s1 = {'backends': [{'servers': []}]}
114+ s2 = {'backends': []}
115+ self.assertRaises(
116+ hooks.InvalidRelationDataError, hooks.merge_service, s1, s2)
117+
118+ def test_merge_service_new_backend_without_name(self):
119+ """Backends in new_service without name raise an exception."""
120+
121+ s1 = {'backends': []}
122+ s2 = {'backends': [{'servers': []}]}
123+ self.assertRaises(
124+ hooks.InvalidRelationDataError, hooks.merge_service, s1, s2)
125+
126+ def test_merge_service_backend_name_matching(self):
127+ """Backends are merged by backend_name."""
128+
129+ s1 = {'backends': [
130+ {'backend_name': 'api',
131+ 'servers': [['api-0', '10.0.0.1', 9080, []]]},
132+ {'backend_name': 'webapp',
133+ 'servers': [['webapp-0', '10.0.0.1', 8090, []]]},
134+ ]}
135+ s2 = {'backends': [
136+ {'backend_name': 'webapp',
137+ 'servers': [['webapp-1', '10.0.0.2', 8090, []]]},
138+ ]}
139+ expected = {
140+ 'backends': [
141+ {'backend_name': 'api',
142+ 'servers': [['api-0', '10.0.0.1', 9080, []]]},
143+ {'backend_name': 'webapp',
144+ 'servers': [['webapp-0', '10.0.0.1', 8090, []],
145+ ['webapp-1', '10.0.0.2', 8090, []]]},
146+ ]
147+ }
148+ self.assertEqual(expected, hooks.merge_service(s1, s2))
149+
150 def test_join_reverseproxy_relation(self):
151 """
152 When haproxy joins a reverseproxy relation it advertises its public

Subscribers

People subscribed via source and target branches