Merge lp:~thisfred/u1db/fahrenheit-503 into lp:u1db

Proposed by Eric Casteleijn
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
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.

To post a comment you must log in.
Revision history for this message
Eric Casteleijn (thisfred) wrote :

Also, I didn't do the C part :)

Revision history for this message
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://bugs.launchpad.net/u1db/+bug/999562
>
> For more details, see:
> https://code.launchpad.net/~thisfred/u1db/fahrenheit-503/+merge/111673
>
>
>
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://enigmail.mozdev.org/

iEYEARECAAYFAk/lxZAACgkQJdeBCYSNAANsKgCdHi2AP+LHwujBk0CF2uSefq5A
EyoAniMCJKhL/gnqqC5R9oR8p5GRJaIW
=Be8c
-----END PGP SIGNATURE-----

Revision history for this message
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

Revision history for this message
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?

Revision history for this message
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.

Revision history for this message
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://enigmail.mozdev.org/

iEYEARECAAYFAk/pZekACgkQJdeBCYSNAANvDQCgrDP7bLBHPo3cx+oyFE0MZU3o
oxcAoIFsODdNuYmOHMxHRu1yqSJ0IRqJ
=9At7
-----END PGP SIGNATURE-----

Revision history for this message
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

lp:~thisfred/u1db/fahrenheit-503 updated
340. By Eric Casteleijn

resolved conflicts

Revision history for this message
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://bugs.launchpad.net/u1db/+bug/999562
>
> For more details, see:
> https://code.launchpad.net/~thisfred/u1db/fahrenheit-503/+merge/111673
>
> 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://enigmail.mozdev.org/

iEYEARECAAYFAk/sYF8ACgkQJdeBCYSNAAM0AwCfQSd/tAkxpVPWfbZ1TkJ2HokI
kxEAoL4NEVUXstcY+jcxKT1diBMbqhZh
=pj+o
-----END PGP SIGNATURE-----

Revision history for this message
Eric Casteleijn (thisfred) wrote :

Yep, that's how I did it. :)

lp:~thisfred/u1db/fahrenheit-503 updated
341. By Eric Casteleijn

resolved conflict

Revision history for this message
Samuele Pedroni (pedronis) wrote :

looks good

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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()

Subscribers

People subscribed via source and target branches