Merge lp:~thisfred/u1db/fahrenheit-503 into lp:u1db
- fahrenheit-503
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Eric Casteleijn | ||||
Approved revision: | 341 | ||||
Merged at revision: | 337 | ||||
Proposed branch: | lp:~thisfred/u1db/fahrenheit-503 | ||||
Merge into: | lp:u1db | ||||
Diff against target: |
538 lines (+240/-53) 9 files modified
include/u1db/u1db.h (+1/-0) src/u1db_http_sync_target.c (+124/-48) u1db/__init__.py (+2/-0) u1db/errors.py (+1/-0) u1db/remote/http_app.py (+1/-2) u1db/remote/http_client.py (+13/-3) u1db/tests/c_backend_wrapper.pyx (+3/-0) u1db/tests/test_c_backend.py (+42/-0) u1db/tests/test_http_client.py (+53/-0) |
||||
To merge this branch: | bzr merge lp:~thisfred/u1db/fahrenheit-503 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Samuele Pedroni | Approve | ||
Review via email: mp+111673@code.launchpad.net |
Commit message
Added retry with backoff logic on 503s from a server.
Description of the change
Added retry with backoff logic on 503s from a server.
This is a request for feedback maybe more than a merge proposal: What I ended up doing is *extremely simple*. It may be too simple, so that's one question. the current tests for Unavailable exercise this code, and the other client tests exercise the success case.
I would like to add a test that succeeds on the second (or any but the first try) but I am unsure how to do it, which is where I may have to admit that I over simplified.
Eric Casteleijn (thisfred) wrote : | # |
John A Meinel (jameinel) wrote : | # |
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
On 6/22/2012 10:28 PM, Eric Casteleijn wrote:
> Eric Casteleijn has proposed merging
> lp:~thisfred/u1db/fahrenheit-503 into lp:u1db.
>
> Requested reviews: Ubuntu One hackers (ubuntuone-hackers) Related
> bugs: Bug #999562 in U1DB: "retry logic on 503 "
> https:/
>
> For more details, see:
> https:/
>
>
>
Added retry with backoff logic on 503s from a server.
>
> This is a request for feedback maybe more than a merge proposal:
> What I ended up doing is *extremely simple*. It may be too simple,
> so that's one question. the current tests for Unavailable exercise
> this code, and the other client tests exercise the success case.
>
> I would like to add a test that succeeds on the second (or any but
> the first try) but I am unsure how to do it, which is where I may
> have to admit that I over simplified.
>
We at least discussed at one point giving the server the ability to
tell clients to back off for a particular amount of time. (So if we
got a thundering horde, we could get them split up by random time, and
so we can slow down clients when load is high.)
That was more about when to check for updates, rather than during a
503, but something we might want to think about.
I will say, I don't think we want to hide a sleep + retry that is
10-40s long. You really don't want your app to think it is busy and
hang for that long when nothing is going on.
So I think the 2, 5 might be reasonable, but above that, the app
itself should be aware and retry the operation. At least, that's my 2c.
John
=:->
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (Cygwin)
Comment: Using GnuPG with Mozilla - http://
iEYEARECAAYFAk/
EyoAniMCJKhL/
=Be8c
-----END PGP SIGNATURE-----
Samuele Pedroni (pedronis) wrote : | # |
> So I think the 2, 5 might be reasonable, but above that, the app
> itself should be aware and retry the operation. At least, that's my 2c.
I agree on this point
Eric Casteleijn (thisfred) wrote : | # |
I changed the delays to be 1, 1, 2, and 4 seconds. I don't know if that's still too much of a maximum total wait.
I'm having a hard time adding tests for this: should the HTTPApp be made capable of sending 503s, and then made to do so in the sync tests?
Eric Casteleijn (thisfred) wrote : | # |
I mean I have a hard time testing the C http sync implementation. I do have a test for the python client.
John A Meinel (jameinel) wrote : | # |
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
On 06/25/2012 08:12 PM, Eric Casteleijn wrote:
> I mean I have a hard time testing the C http sync implementation. I
> do have a test for the python client.
>
I think it would be reasonable to have an HTTP implementation that can
return 503s. We'd want it for integration testing as well. So some
tweaking there would be quite reasonable.
John
=:->
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.11 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://
iEYEARECAAYFAk/
oxcAoIFsODdNuYm
=9At7
-----END PGP SIGNATURE-----
Eric Casteleijn (thisfred) wrote : | # |
Working c backend tests, which shook out some errors. Unfortunately, calling curl_easy_perform repeatedly requires setting the options again, or it will hang. Since the options are different for the different requests, a single retry function was no longer feasible.
Also: I would like to set the RETRY_DELAYS all to 0 for the tests (which now take a few seconds longer) but I am unsure how to do this from Cython
- 340. By Eric Casteleijn
-
resolved conflicts
John A Meinel (jameinel) wrote : | # |
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
On 06/27/2012 08:51 PM, Eric Casteleijn wrote:
> Eric Casteleijn has proposed merging
> lp:~thisfred/u1db/fahrenheit-503 into lp:u1db.
>
> Requested reviews: Ubuntu One hackers (ubuntuone-hackers) Related
> bugs: Bug #999562 in U1DB: "retry logic on 503 "
> https:/
>
> For more details, see:
> https:/
>
> Added retry with backoff logic on 503s from a server.
>
> This is a request for feedback maybe more than a merge proposal:
> What I ended up doing is *extremely simple*. It may be too simple,
> so that's one question. the current tests for Unavailable exercise
> this code, and the other client tests exercise the success case.
>
> I would like to add a test that succeeds on the second (or any but
> the first try) but I am unsure how to do it, which is where I may
> have to admit that I over simplified.
>
If you want to have a test that succeeds on the second try, I think it
is just a matter of poking at your 'wrapper':
You have code like this:
+ def wrapper(instance, *args, **kwargs):
+ tries.append(None)
+ raise errors.Unavailable
You just change it to:
+ def wrapper(instance, *args, **kwargs):
+ if tries:
+ return orig(instance, *args, **kwargs)
+ tries.append(None)
+ raise errors.Unavailable
Or whatever the signature is. So on the *first* call, it raises
unavailable, and logs that it was called. And on subsequent calls it
just passes through to the original value.
It would be really neat if we had an actual HTTP service that could be
taught to do this, but in the short term, the above seems fine.
(I haven't looked deeply at the code yet.)
John
=:->
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.11 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://
iEYEARECAAYFAk/
kxEAoL4NEVUXstc
=pj+o
-----END PGP SIGNATURE-----
Eric Casteleijn (thisfred) wrote : | # |
Yep, that's how I did it. :)
- 341. By Eric Casteleijn
-
resolved conflict
Preview Diff
1 | === modified file 'include/u1db/u1db.h' |
2 | --- include/u1db/u1db.h 2012-06-22 00:03:55 +0000 |
3 | +++ include/u1db/u1db.h 2012-06-29 17:03:18 +0000 |
4 | @@ -72,6 +72,7 @@ |
5 | #define U1DB_INVALID_GLOBBING -19 |
6 | #define U1DB_INVALID_TRANSACTION_ID -20 |
7 | #define U1DB_INVALID_GENERATION -21 |
8 | +#define U1DB_TARGET_UNAVAILABLE -22 |
9 | #define U1DB_INTERNAL_ERROR -999 |
10 | |
11 | // Used by put_doc_if_newer |
12 | |
13 | === modified file 'src/u1db_http_sync_target.c' |
14 | --- src/u1db_http_sync_target.c 2012-06-22 00:03:55 +0000 |
15 | +++ src/u1db_http_sync_target.c 2012-06-29 17:03:18 +0000 |
16 | @@ -24,10 +24,18 @@ |
17 | #include <json/json.h> |
18 | #include <curl/curl.h> |
19 | #include <oauth.h> |
20 | +#include <sys/socket.h> |
21 | +#include <sys/time.h> |
22 | |
23 | #ifndef max |
24 | #define max(a, b) ((a) > (b) ? (a) : (b)) |
25 | #endif // max |
26 | +#define TRIES 4 |
27 | +#ifndef RETRY_DELAYS |
28 | +#define RETRY_DELAYS {1, 1, 2, 4} |
29 | +#endif |
30 | + |
31 | +int retry_delays[] = RETRY_DELAYS; |
32 | |
33 | struct _http_state; |
34 | struct _http_request; |
35 | @@ -357,7 +365,6 @@ |
36 | return U1DB_OK; |
37 | } |
38 | |
39 | - |
40 | static int |
41 | st_http_get_sync_info(u1db_sync_target *st, |
42 | const char *source_replica_uid, |
43 | @@ -368,9 +375,11 @@ |
44 | struct _http_request req = {0}; |
45 | char *url = NULL; |
46 | const char *tmp = NULL; |
47 | - int status; |
48 | + int status = U1DB_OK; |
49 | long http_code; |
50 | struct curl_slist *headers = NULL; |
51 | + int attempt = 0; |
52 | + struct timeval timeout; |
53 | |
54 | json_object *json = NULL, *obj = NULL; |
55 | |
56 | @@ -393,23 +402,42 @@ |
57 | req.state = state; |
58 | status = u1db__format_sync_url(st, source_replica_uid, &url); |
59 | if (status != U1DB_OK) { goto finish; } |
60 | - status = curl_easy_setopt(state->curl, CURLOPT_HTTPGET, 1L); |
61 | - if (status != CURLE_OK) { goto finish; } |
62 | - // status = curl_easy_setopt(state->curl, CURLOPT_USERAGENT, "..."); |
63 | - status = curl_easy_setopt(state->curl, CURLOPT_URL, url); |
64 | - if (status != CURLE_OK) { goto finish; } |
65 | - req.body_buffer = req.header_buffer = NULL; |
66 | - status = simple_set_curl_data(state->curl, &req, &req, NULL); |
67 | - if (status != CURLE_OK) { goto finish; } |
68 | - status = maybe_sign_url(st, "GET", url, &headers); |
69 | + for (;;) { |
70 | + status = curl_easy_setopt(state->curl, CURLOPT_HTTPGET, 1L); |
71 | + if (status != CURLE_OK) { goto finish; } |
72 | + // status = curl_easy_setopt(state->curl, CURLOPT_USERAGENT, "..."); |
73 | + status = curl_easy_setopt(state->curl, CURLOPT_URL, url); |
74 | + if (status != CURLE_OK) { goto finish; } |
75 | + req.body_buffer = req.header_buffer = NULL; |
76 | + status = simple_set_curl_data(state->curl, &req, &req, NULL); |
77 | + if (status != CURLE_OK) { goto finish; } |
78 | + status = maybe_sign_url(st, "GET", url, &headers); |
79 | + if (status != U1DB_OK) { goto finish; } |
80 | + status = curl_easy_setopt(state->curl, CURLOPT_HTTPHEADER, headers); |
81 | + if (status != CURLE_OK) { goto finish; } |
82 | + // Now do the GET |
83 | + status = curl_easy_perform(state->curl); |
84 | + if (status != CURLE_OK) { |
85 | + goto finish; } |
86 | + status = curl_easy_getinfo( |
87 | + state->curl, CURLINFO_RESPONSE_CODE, &http_code); |
88 | + if (status != CURLE_OK) { |
89 | + goto finish; } |
90 | + status = U1DB_OK; |
91 | + if (http_code == 503) { |
92 | + status = U1DB_TARGET_UNAVAILABLE; |
93 | + if (attempt < TRIES) { |
94 | + timeout.tv_sec = retry_delays[attempt]; |
95 | + timeout.tv_usec = 0; |
96 | + select(0, NULL, NULL, NULL, &timeout); |
97 | + attempt++; |
98 | + req.num_body_bytes = 0; |
99 | + continue; |
100 | + } |
101 | + } |
102 | + break; |
103 | + } |
104 | if (status != U1DB_OK) { goto finish; } |
105 | - status = curl_easy_setopt(state->curl, CURLOPT_HTTPHEADER, headers); |
106 | - if (status != CURLE_OK) { goto finish; } |
107 | - // Now do the GET |
108 | - status = curl_easy_perform(state->curl); |
109 | - if (status != CURLE_OK) { goto finish; } |
110 | - status = curl_easy_getinfo(state->curl, CURLINFO_RESPONSE_CODE, &http_code); |
111 | - if (status != CURLE_OK) { goto finish; } |
112 | if (http_code != 200) { // 201 for created? shouldn't happen on GET |
113 | status = http_code; |
114 | goto finish; |
115 | @@ -541,6 +569,8 @@ |
116 | const char *raw_body = NULL; |
117 | int raw_len; |
118 | struct curl_slist *headers = NULL; |
119 | + int attempt = 0; |
120 | + struct timeval timeout; |
121 | |
122 | if (st == NULL || source_replica_uid == NULL || st->implementation == NULL) |
123 | { |
124 | @@ -572,27 +602,46 @@ |
125 | // confirmation of the post. |
126 | headers = curl_slist_append(headers, "Expect:"); |
127 | |
128 | - status = curl_easy_setopt(state->curl, CURLOPT_URL, url); |
129 | - if (status != CURLE_OK) { goto finish; } |
130 | - status = curl_easy_setopt(state->curl, CURLOPT_HTTPHEADER, headers); |
131 | - if (status != CURLE_OK) { goto finish; } |
132 | - status = curl_easy_setopt(state->curl, CURLOPT_UPLOAD, 1L); |
133 | - if (status != CURLE_OK) { goto finish; } |
134 | - status = curl_easy_setopt(state->curl, CURLOPT_PUT, 1L); |
135 | - if (status != CURLE_OK) { goto finish; } |
136 | - status = simple_set_curl_data(state->curl, &req, &req, &req); |
137 | - if (status != CURLE_OK) { goto finish; } |
138 | - status = curl_easy_setopt(state->curl, CURLOPT_INFILESIZE_LARGE, |
139 | - (curl_off_t)req.num_put_bytes); |
140 | - if (status != CURLE_OK) { goto finish; } |
141 | - status = maybe_sign_url(st, "PUT", url, &headers); |
142 | + for (;;) { |
143 | + status = curl_easy_setopt(state->curl, CURLOPT_URL, url); |
144 | + if (status != CURLE_OK) { goto finish; } |
145 | + status = curl_easy_setopt(state->curl, CURLOPT_HTTPHEADER, headers); |
146 | + if (status != CURLE_OK) { goto finish; } |
147 | + status = curl_easy_setopt(state->curl, CURLOPT_UPLOAD, 1L); |
148 | + if (status != CURLE_OK) { goto finish; } |
149 | + status = curl_easy_setopt(state->curl, CURLOPT_PUT, 1L); |
150 | + if (status != CURLE_OK) { goto finish; } |
151 | + status = simple_set_curl_data(state->curl, &req, &req, &req); |
152 | + if (status != CURLE_OK) { goto finish; } |
153 | + status = curl_easy_setopt(state->curl, CURLOPT_INFILESIZE_LARGE, |
154 | + (curl_off_t)req.num_put_bytes); |
155 | + if (status != CURLE_OK) { goto finish; } |
156 | + status = maybe_sign_url(st, "PUT", url, &headers); |
157 | + if (status != U1DB_OK) { goto finish; } |
158 | + |
159 | + // Now actually send the data |
160 | + status = curl_easy_perform(state->curl); |
161 | + if (status != CURLE_OK) { |
162 | + goto finish; } |
163 | + status = curl_easy_getinfo( |
164 | + state->curl, CURLINFO_RESPONSE_CODE, &http_code); |
165 | + if (status != CURLE_OK) { |
166 | + goto finish; } |
167 | + status = U1DB_OK; |
168 | + if (http_code == 503) { |
169 | + status = U1DB_TARGET_UNAVAILABLE; |
170 | + if (attempt < TRIES) { |
171 | + timeout.tv_sec = retry_delays[attempt]; |
172 | + timeout.tv_usec = 0; |
173 | + select(0, NULL, NULL, NULL, &timeout); |
174 | + attempt++; |
175 | + req.num_body_bytes = 0; |
176 | + continue; |
177 | + } |
178 | + } |
179 | + break; |
180 | + } |
181 | if (status != U1DB_OK) { goto finish; } |
182 | - |
183 | - // Now actually send the data |
184 | - status = curl_easy_perform(state->curl); |
185 | - if (status != CURLE_OK) { goto finish; } |
186 | - status = curl_easy_getinfo(state->curl, CURLINFO_RESPONSE_CODE, &http_code); |
187 | - if (status != CURLE_OK) { goto finish; } |
188 | if (http_code != 200 && http_code != 201) { |
189 | status = http_code; |
190 | goto finish; |
191 | @@ -753,6 +802,8 @@ |
192 | char *url = NULL; |
193 | struct _http_state *state; |
194 | struct curl_slist *headers = NULL; |
195 | + int attempt = 0; |
196 | + struct timeval timeout; |
197 | |
198 | fputs("\r\n]", temp_fd); |
199 | status = impl_as_http_state(st->implementation, &state); |
200 | @@ -761,18 +812,38 @@ |
201 | } |
202 | status = u1db__format_sync_url(st, source_replica_uid, &url); |
203 | if (status != U1DB_OK) { goto finish; } |
204 | - status = curl_easy_setopt(state->curl, CURLOPT_URL, url); |
205 | - if (status != CURLE_OK) { goto finish; } |
206 | - status = setup_curl_for_sync(state->curl, &headers, req, temp_fd); |
207 | - if (status != CURLE_OK) { goto finish; } |
208 | - status = maybe_sign_url(st, "POST", url, &headers); |
209 | + for (;;) { |
210 | + status = curl_easy_setopt(state->curl, CURLOPT_URL, url); |
211 | + if (status != CURLE_OK) { goto finish; } |
212 | + status = setup_curl_for_sync(state->curl, &headers, req, temp_fd); |
213 | + if (status != CURLE_OK) { goto finish; } |
214 | + status = maybe_sign_url(st, "POST", url, &headers); |
215 | + if (status != U1DB_OK) { goto finish; } |
216 | + // Now send off the messages, and handle the returned content. |
217 | + status = curl_easy_perform(state->curl); |
218 | + if (status != CURLE_OK) { |
219 | + goto finish; } |
220 | + status = curl_easy_getinfo( |
221 | + state->curl, CURLINFO_RESPONSE_CODE, &http_code); |
222 | + if (status != CURLE_OK) { |
223 | + goto finish; } |
224 | + status = U1DB_OK; |
225 | + if (http_code == 503) { |
226 | + status = U1DB_TARGET_UNAVAILABLE; |
227 | + if (attempt < TRIES) { |
228 | + timeout.tv_sec = retry_delays[attempt]; |
229 | + timeout.tv_usec = 0; |
230 | + select(0, NULL, NULL, NULL, &timeout); |
231 | + attempt++; |
232 | + req->num_body_bytes = 0; |
233 | + continue; |
234 | + } |
235 | + } |
236 | + break; |
237 | + } |
238 | if (status != U1DB_OK) { goto finish; } |
239 | - // Now send off the messages, and handle the returned content. |
240 | - status = curl_easy_perform(state->curl); |
241 | - if (status != CURLE_OK) { goto finish; } |
242 | - status = curl_easy_getinfo(state->curl, CURLINFO_RESPONSE_CODE, &http_code); |
243 | - if (status != CURLE_OK) { goto finish; } |
244 | if (http_code != 200 && http_code != 201) { |
245 | + printf("broken 0\n"); |
246 | status = U1DB_BROKEN_SYNC_STREAM; |
247 | goto finish; |
248 | } |
249 | @@ -802,29 +873,34 @@ |
250 | |
251 | json = json_tokener_parse(response); |
252 | if (json == NULL || !json_object_is_type(json, json_type_array)) { |
253 | + printf("broken 1, response: %s\n", response); |
254 | status = U1DB_BROKEN_SYNC_STREAM; |
255 | goto finish; |
256 | } |
257 | doc_count = json_object_array_length(json); |
258 | if (doc_count < 1) { |
259 | // the first response is the new_generation info, so it must exist |
260 | + printf("broken 2\n"); |
261 | status = U1DB_BROKEN_SYNC_STREAM; |
262 | goto finish; |
263 | } |
264 | obj = json_object_array_get_idx(json, 0); |
265 | attr = json_object_object_get(obj, "new_generation"); |
266 | if (attr == NULL) { |
267 | + printf("broken 3\n"); |
268 | status = U1DB_BROKEN_SYNC_STREAM; |
269 | goto finish; |
270 | } |
271 | *target_gen = json_object_get_int(attr); |
272 | attr = json_object_object_get(obj, "new_transaction_id"); |
273 | if (attr == NULL) { |
274 | + printf("broken 4\n"); |
275 | status = U1DB_BROKEN_SYNC_STREAM; |
276 | goto finish; |
277 | } |
278 | tmp = json_object_get_string(attr); |
279 | if (tmp == NULL) { |
280 | + printf("broken 5\n"); |
281 | status = U1DB_BROKEN_SYNC_STREAM; |
282 | goto finish; |
283 | } |
284 | |
285 | === modified file 'u1db/__init__.py' |
286 | --- u1db/__init__.py 2012-06-22 00:03:55 +0000 |
287 | +++ u1db/__init__.py 2012-06-29 17:03:18 +0000 |
288 | @@ -32,6 +32,8 @@ |
289 | :param path: The filesystem path for the database to open. |
290 | :param create: True/False, should the database be created if it doesn't |
291 | already exist? |
292 | + :param document_factory: A function that will be called with the same |
293 | + parameters as Document.__init__. |
294 | :return: An instance of Database. |
295 | """ |
296 | from u1db.backends import sqlite_backend |
297 | |
298 | === modified file 'u1db/errors.py' |
299 | --- u1db/errors.py 2012-06-22 00:03:55 +0000 |
300 | +++ u1db/errors.py 2012-06-29 17:03:18 +0000 |
301 | @@ -137,6 +137,7 @@ |
302 | |
303 | wire_description = None |
304 | |
305 | + |
306 | # mapping wire (transimission) descriptions/tags for errors to the exceptions |
307 | wire_description_to_exc = dict( |
308 | (x.wire_description, x) for x in globals().values() |
309 | |
310 | === modified file 'u1db/remote/http_app.py' |
311 | --- u1db/remote/http_app.py 2012-06-14 23:28:58 +0000 |
312 | +++ u1db/remote/http_app.py 2012-06-29 17:03:18 +0000 |
313 | @@ -514,8 +514,7 @@ |
314 | except errors.U1DBError, e: |
315 | self.request_u1db_error(environ, e) |
316 | status = http_errors.wire_description_to_status.get( |
317 | - e.wire_description, |
318 | - 500) |
319 | + e.wire_description, 500) |
320 | responder.send_response_json(status, error=e.wire_description) |
321 | except BadRequest: |
322 | self.request_bad_request(environ) |
323 | |
324 | === modified file 'u1db/remote/http_client.py' |
325 | --- u1db/remote/http_client.py 2012-05-25 19:00:46 +0000 |
326 | +++ u1db/remote/http_client.py 2012-06-29 17:03:18 +0000 |
327 | @@ -25,6 +25,7 @@ |
328 | import urlparse |
329 | import urllib |
330 | |
331 | +from time import sleep |
332 | from u1db import ( |
333 | errors, |
334 | ) |
335 | @@ -33,8 +34,8 @@ |
336 | ) |
337 | |
338 | from u1db.remote.ssl_match_hostname import ( |
339 | + CertificateError, |
340 | match_hostname, |
341 | - CertificateError, |
342 | ) |
343 | |
344 | # Ubuntu/debian |
345 | @@ -90,6 +91,10 @@ |
346 | # attacks for example) one would need HTTPS |
347 | oauth_signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1() |
348 | |
349 | + # Will use these delays to retry on 503 befor finally giving up. The final |
350 | + # 0 is there to not wait after the final try fails. |
351 | + _delays = (1, 1, 2, 4, 0) |
352 | + |
353 | def __init__(self, url): |
354 | self._url = urlparse.urlsplit(url) |
355 | self._conn = None |
356 | @@ -186,8 +191,13 @@ |
357 | headers['content-type'] = content_type |
358 | headers.update( |
359 | self._sign_request(method, unquoted_url, encoded_params)) |
360 | - self._conn.request(method, url_query, body, headers) |
361 | - return self._response() |
362 | + for delay in self._delays: |
363 | + try: |
364 | + self._conn.request(method, url_query, body, headers) |
365 | + return self._response() |
366 | + except errors.Unavailable, e: |
367 | + sleep(delay) |
368 | + raise e |
369 | |
370 | def _request_json(self, method, url_parts, params=None, body=None, |
371 | content_type=None): |
372 | |
373 | === modified file 'u1db/tests/c_backend_wrapper.pyx' |
374 | --- u1db/tests/c_backend_wrapper.pyx 2012-06-22 00:03:55 +0000 |
375 | +++ u1db/tests/c_backend_wrapper.pyx 2012-06-29 17:03:18 +0000 |
376 | @@ -138,6 +138,7 @@ |
377 | int U1DB_INVALID_GENERATION |
378 | int U1DB_INVALID_TRANSACTION_ID |
379 | int U1DB_INTERNAL_ERROR |
380 | + int U1DB_TARGET_UNAVAILABLE |
381 | |
382 | int U1DB_INSERTED |
383 | int U1DB_SUPERSEDED |
384 | @@ -596,6 +597,8 @@ |
385 | raise errors.InvalidGeneration |
386 | if status == U1DB_INVALID_TRANSACTION_ID: |
387 | raise errors.InvalidTransactionId |
388 | + if status == U1DB_TARGET_UNAVAILABLE: |
389 | + raise errors.Unavailable |
390 | if status == U1DB_INVALID_JSON: |
391 | raise errors.InvalidJSON |
392 | raise RuntimeError('%s (status: %s)' % (context, status)) |
393 | |
394 | === modified file 'u1db/tests/test_c_backend.py' |
395 | --- u1db/tests/test_c_backend.py 2012-06-29 16:18:14 +0000 |
396 | +++ u1db/tests/test_c_backend.py 2012-06-29 17:03:18 +0000 |
397 | @@ -429,6 +429,48 @@ |
398 | self.assertGetDoc(db, mem_doc.doc_id, mem_doc.rev, mem_doc.get_json(), |
399 | False) |
400 | |
401 | + def test_unavailable(self): |
402 | + mem_db = self.request_state._create_database('test.db') |
403 | + mem_db.create_doc(tests.nested_doc) |
404 | + tries = [] |
405 | + |
406 | + def wrapper(instance, *args, **kwargs): |
407 | + tries.append(None) |
408 | + raise errors.Unavailable |
409 | + |
410 | + mem_db.whats_changed = wrapper |
411 | + url = self.getURL('test.db') |
412 | + target = c_backend_wrapper.create_http_sync_target(url) |
413 | + db = c_backend_wrapper.CDatabase(':memory:') |
414 | + db.create_doc(tests.simple_doc) |
415 | + self.assertRaises( |
416 | + errors.Unavailable, c_backend_wrapper.sync_db_to_target, db, |
417 | + target) |
418 | + self.assertEqual(5, len(tries)) |
419 | + |
420 | + def test_unavailable_then_available(self): |
421 | + mem_db = self.request_state._create_database('test.db') |
422 | + mem_doc = mem_db.create_doc(tests.nested_doc) |
423 | + orig_whatschanged = mem_db.whats_changed |
424 | + tries = [] |
425 | + |
426 | + def wrapper(instance, *args, **kwargs): |
427 | + if len(tries) < 1: |
428 | + tries.append(None) |
429 | + raise errors.Unavailable |
430 | + return orig_whatschanged(instance, *args, **kwargs) |
431 | + |
432 | + mem_db.whats_changed = wrapper |
433 | + url = self.getURL('test.db') |
434 | + target = c_backend_wrapper.create_http_sync_target(url) |
435 | + db = c_backend_wrapper.CDatabase(':memory:') |
436 | + doc = db.create_doc(tests.simple_doc) |
437 | + c_backend_wrapper.sync_db_to_target(db, target) |
438 | + self.assertEqual(1, len(tries)) |
439 | + self.assertGetDoc(mem_db, doc.doc_id, doc.rev, doc.get_json(), False) |
440 | + self.assertGetDoc(db, mem_doc.doc_id, mem_doc.rev, mem_doc.get_json(), |
441 | + False) |
442 | + |
443 | |
444 | class TestSyncCtoOAuthHTTPViaC(tests.TestCaseWithServer): |
445 | |
446 | |
447 | === modified file 'u1db/tests/test_http_client.py' |
448 | --- u1db/tests/test_http_client.py 2012-05-25 19:00:46 +0000 |
449 | +++ u1db/tests/test_http_client.py 2012-06-29 17:03:18 +0000 |
450 | @@ -43,6 +43,10 @@ |
451 | |
452 | class TestHTTPClientBase(tests.TestCaseWithServer): |
453 | |
454 | + def setUp(self): |
455 | + super(TestHTTPClientBase, self).setUp() |
456 | + self.errors = 0 |
457 | + |
458 | def app(self, environ, start_response): |
459 | if environ['PATH_INFO'].endswith('echo'): |
460 | start_response("200 OK", [('Content-Type', 'application/json')]) |
461 | @@ -54,7 +58,37 @@ |
462 | content_length = int(environ['CONTENT_LENGTH']) |
463 | ret['body'] = environ['wsgi.input'].read(content_length) |
464 | return [simplejson.dumps(ret)] |
465 | + elif environ['PATH_INFO'].endswith('error_then_accept'): |
466 | + if self.errors >= 3: |
467 | + start_response( |
468 | + "200 OK", [('Content-Type', 'application/json')]) |
469 | + ret = {} |
470 | + for name in ('REQUEST_METHOD', 'PATH_INFO', 'QUERY_STRING'): |
471 | + ret[name] = environ[name] |
472 | + if environ['REQUEST_METHOD'] in ('PUT', 'POST'): |
473 | + ret['CONTENT_TYPE'] = environ['CONTENT_TYPE'] |
474 | + content_length = int(environ['CONTENT_LENGTH']) |
475 | + ret['body'] = '{"oki": "doki"}' |
476 | + return [simplejson.dumps(ret)] |
477 | + self.errors += 1 |
478 | + content_length = int(environ['CONTENT_LENGTH']) |
479 | + error = simplejson.loads( |
480 | + environ['wsgi.input'].read(content_length)) |
481 | + response = error['response'] |
482 | + # In debug mode, wsgiref has an assertion that the status parameter |
483 | + # is a 'str' object. However error['status'] returns a unicode |
484 | + # object. |
485 | + status = str(error['status']) |
486 | + if isinstance(response, unicode): |
487 | + response = str(response) |
488 | + if isinstance(response, str): |
489 | + start_response(status, [('Content-Type', 'text/plain')]) |
490 | + return [str(response)] |
491 | + else: |
492 | + start_response(status, [('Content-Type', 'application/json')]) |
493 | + return [simplejson.dumps(response)] |
494 | elif environ['PATH_INFO'].endswith('error'): |
495 | + self.errors += 1 |
496 | content_length = int(environ['CONTENT_LENGTH']) |
497 | error = simplejson.loads( |
498 | environ['wsgi.input'].read(content_length)) |
499 | @@ -204,13 +238,31 @@ |
500 | |
501 | def test_unavailable_proper(self): |
502 | cli = self.getClient() |
503 | + cli._delays = (0, 0, 0, 0, 0) |
504 | self.assertRaises(errors.Unavailable, |
505 | cli._request_json, 'POST', ['error'], {}, |
506 | {'status': "503 Service Unavailable", |
507 | 'response': {"error": "unavailable"}}) |
508 | + self.assertEqual(5, self.errors) |
509 | + |
510 | + def test_unavailable_then_available(self): |
511 | + cli = self.getClient() |
512 | + cli._delays = (0, 0, 0, 0, 0) |
513 | + res, headers = cli._request_json( |
514 | + 'POST', ['error_then_accept'], {'b': 2}, |
515 | + {'status': "503 Service Unavailable", |
516 | + 'response': {"error": "unavailable"}}) |
517 | + self.assertEqual('application/json', headers['content-type']) |
518 | + self.assertEqual({'CONTENT_TYPE': 'application/json', |
519 | + 'PATH_INFO': '/dbase/error_then_accept', |
520 | + 'QUERY_STRING': 'b=2', |
521 | + 'body': '{"oki": "doki"}', |
522 | + 'REQUEST_METHOD': 'POST'}, res) |
523 | + self.assertEqual(3, self.errors) |
524 | |
525 | def test_unavailable_random_source(self): |
526 | cli = self.getClient() |
527 | + cli._delays = (0, 0, 0, 0, 0) |
528 | try: |
529 | cli._request_json('POST', ['error'], {}, |
530 | {'status': "503 Service Unavailable", |
531 | @@ -221,6 +273,7 @@ |
532 | self.assertEqual(503, e.status) |
533 | self.assertEqual("random unavailable.", e.message) |
534 | self.assertTrue("content-type" in e.headers) |
535 | + self.assertEqual(5, self.errors) |
536 | |
537 | def test_generic_u1db_error(self): |
538 | cli = self.getClient() |
Also, I didn't do the C part :)