Merge lp:~allenap/maas/retry-context-not-active into lp:~maas-committers/maas/trunk

Proposed by Gavin Panella
Status: Merged
Approved by: Gavin Panella
Approved revision: no longer in the source branch.
Merged at revision: 5823
Proposed branch: lp:~allenap/maas/retry-context-not-active
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 173 lines (+90/-19)
3 files modified
src/maasserver/utils/orm.py (+1/-0)
src/maasserver/utils/tests/test_views.py (+63/-0)
src/maasserver/utils/views.py (+26/-19)
To merge this branch: bzr merge lp:~allenap/maas/retry-context-not-active
Reviewer Review Type Date Requested Status
Blake Rouse (community) Approve
Review via email: mp+320083@code.launchpad.net

Commit message

Use the retry context in web requests too.

Previously the retry context was only activated when using the @transactional decorator.

To post a comment you must log in.
Revision history for this message
Blake Rouse (blake-rouse) wrote :

Looks good.

I am guessing this issue only popped up because of the changes you made to the static IP address allocation at the beginning of the cycle?

review: Approve
Revision history for this message
Gavin Panella (allenap) wrote :

> I am guessing this issue only popped up because of the changes you
> made to the static IP address allocation at the beginning of the
> cycle?

Yeah :-/

Thanks for the review.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/utils/orm.py'
2--- src/maasserver/utils/orm.py 2016-12-07 12:46:14 +0000
3+++ src/maasserver/utils/orm.py 2017-03-16 16:43:12 +0000
4@@ -24,6 +24,7 @@
5 'post_commit_do',
6 'psql_array',
7 'request_transaction_retry',
8+ 'retry_context',
9 'retry_on_retryable_failure',
10 'savepoint',
11 'TotallyDisconnected',
12
13=== modified file 'src/maasserver/utils/tests/test_views.py'
14--- src/maasserver/utils/tests/test_views.py 2017-01-28 00:51:47 +0000
15+++ src/maasserver/utils/tests/test_views.py 2017-03-16 16:43:12 +0000
16@@ -7,6 +7,7 @@
17
18 import http.client
19 import io
20+from itertools import count
21 import logging
22 from random import (
23 randint,
24@@ -40,6 +41,8 @@
25 from maasserver.utils.orm import (
26 make_deadlock_failure,
27 post_commit_hooks,
28+ request_transaction_retry,
29+ retry_context,
30 validate_in_transaction,
31 )
32 from maasserver.utils.views import HttpResponseConflict
33@@ -386,6 +389,49 @@
34 response.reason_phrase,
35 Equals(REASON_PHRASES[http.client.CONFLICT]))
36
37+ def test__get_response_prepare_retry_context_before_each_try(self):
38+
39+ class ObserveContext:
40+
41+ def __init__(self, callback, name):
42+ self.callback = callback
43+ self.name = name
44+
45+ def __enter__(self):
46+ self.callback("%s:enter" % self.name)
47+
48+ def __exit__(self, *exc_info):
49+ self.callback("%s:exit" % self.name)
50+
51+ counter = count(1)
52+ calls = []
53+
54+ def get_response_and_retry(request):
55+ calls.append("get_response")
56+ request_transaction_retry(ObserveContext(
57+ calls.append, "context#%d" % next(counter)))
58+
59+ get_response = self.patch(WSGIHandler, "get_response")
60+ get_response.side_effect = get_response_and_retry
61+
62+ reset_request = self.patch_autospec(views, "reset_request")
63+ reset_request.side_effect = lambda request: request
64+
65+ request = make_request()
66+ request.path = factory.make_name("path")
67+ handler = views.WebApplicationHandler(3)
68+ handler.get_response(request)
69+
70+ self.assertThat(calls, Equals([
71+ "get_response", # 1st attempt.
72+ "context#1:enter", # Retry requested, enter 1st new context.
73+ "get_response", # 2nd attempt.
74+ "context#2:enter", # Retry requested, enter 2nd new context.
75+ "get_response", # 3rd attempt, and last.
76+ "context#2:exit", # Exit 2nd context.
77+ "context#1:exit", # Exit 1st context.
78+ ]))
79+
80 def test__get_response_logs_retry_and_resets_request(self):
81 timeout = 1.0 + (random() * 99)
82 handler = views.WebApplicationHandler(2, timeout)
83@@ -430,6 +476,23 @@
84
85 self.assertThat(get_response, MockCalledOnceWith(request))
86
87+ def test__get_response_is_in_retry_context_in_transaction(self):
88+ handler = views.WebApplicationHandler(2)
89+
90+ def check_retry_context_active(request):
91+ self.assertThat(retry_context.active, Is(True))
92+
93+ get_response = self.patch(WSGIHandler, "get_response")
94+ get_response.side_effect = check_retry_context_active
95+
96+ request = make_request()
97+ request.path = factory.make_name("path")
98+
99+ self.assertThat(retry_context.active, Is(False))
100+ handler.get_response(request)
101+ self.assertThat(retry_context.active, Is(False))
102+ self.assertThat(get_response, MockCalledOnceWith(request))
103+
104 def test__get_response_restores_files_across_requests(self):
105 handler = views.WebApplicationHandler(3)
106 file_content = sample_binary_data
107
108=== modified file 'src/maasserver/utils/views.py'
109--- src/maasserver/utils/views.py 2016-10-21 06:19:34 +0000
110+++ src/maasserver/utils/views.py 2017-03-16 16:43:12 +0000
111@@ -23,6 +23,8 @@
112 gen_retry_intervals,
113 is_retryable_failure,
114 post_commit_hooks,
115+ retry_context,
116+ RetryTransaction,
117 )
118 from piston3.authentication import initialize_server_request
119 from piston3.models import Nonce
120@@ -145,7 +147,9 @@
121 # Add it to the retry set if this response was caused by a
122 # retryable failure.
123 exc_type, exc_value, exc_traceback = exc_info
124- if is_retryable_failure(exc_value):
125+ if isinstance(exc_value, RetryTransaction):
126+ self.__retry.add(response)
127+ elif is_retryable_failure(exc_value):
128 self.__retry.add(response)
129 else:
130 logger.error(
131@@ -220,21 +224,24 @@
132 retry_attempts = self.__retry_attempts
133 retry_set = self.__retry
134
135- for attempt in count(1):
136- response = get_response(request)
137- if response in retry_set:
138- elapsed, remaining, wait = next(retry_details)
139- if attempt == retry_attempts or wait == 0:
140- # Time's up: this was the final attempt.
141- log_final_failed_attempt(request, attempt, elapsed)
142- conflict_response = HttpResponseConflict(response)
143- conflict_response.render()
144- return conflict_response
145-
146- # We'll retry after a brief interlude.
147- log_failed_attempt(request, attempt, elapsed, remaining, wait)
148- delete_oauth_nonce(request)
149- request = reset_request(request)
150- sleep(wait)
151- else:
152- return response
153+ with retry_context:
154+ for attempt in count(1):
155+ retry_context.prepare()
156+ response = get_response(request)
157+ if response in retry_set:
158+ elapsed, remaining, wait = next(retry_details)
159+ if attempt == retry_attempts or wait == 0:
160+ # Time's up: this was the final attempt.
161+ log_final_failed_attempt(request, attempt, elapsed)
162+ conflict_response = HttpResponseConflict(response)
163+ conflict_response.render()
164+ return conflict_response
165+ else:
166+ # We'll retry after a brief interlude.
167+ log_failed_attempt(
168+ request, attempt, elapsed, remaining, wait)
169+ delete_oauth_nonce(request)
170+ request = reset_request(request)
171+ sleep(wait)
172+ else:
173+ return response