Merge lp:~cmiller/desktopcouch/views-with-reconnector-proxy into lp:desktopcouch
- views-with-reconnector-proxy
- Merge into trunk
Proposed by
Chad Miller
Status: | Rejected |
---|---|
Rejected by: | Chad Miller |
Proposed branch: | lp:~cmiller/desktopcouch/views-with-reconnector-proxy |
Merge into: | lp:desktopcouch |
Diff against target: |
431 lines (+145/-21) 4 files modified
desktopcouch/records/server.py (+11/-3) desktopcouch/records/server_base.py (+50/-14) desktopcouch/records/tests/test_server.py (+83/-3) desktopcouch/stop_local_couchdb.py (+1/-1) |
To merge this branch: | bzr merge lp:~cmiller/desktopcouch/views-with-reconnector-proxy |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Eric Casteleijn (community) | Approve | ||
Review via email: mp+33471@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
- 172. By Chad Miller
-
First pass at adding tests to find and fix problems when the couchdb crashes.
We crash the couchdb in tests. Whee!
Revision history for this message
Stuart Colville (muffinresearch) wrote : | # |
Running the tests I get a lot of failures. Here's the output of running the tests in case it helps https:/
Looking at the commit message for r172 suggests this may be a work in progress, if that's the case could you change the status of this merge request?
Revision history for this message
Chad Miller (cmiller) wrote : | # |
Yes, Stuart, I intended to merge this, proposed it, then starting adding tests to crash couchdb to make sure we recover well. This is not appropriate to merge now.
VDS is taking over this branch.
Unmerged revisions
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'desktopcouch/records/server.py' |
2 | --- desktopcouch/records/server.py 2010-02-26 22:23:42 +0000 |
3 | +++ desktopcouch/records/server.py 2010-08-25 21:55:59 +0000 |
4 | @@ -49,9 +49,17 @@ |
5 | def __init__(self, database, uri=None, record_factory=None, create=False, |
6 | server_class=OAuthCapableServer, oauth_tokens=None, |
7 | ctx=desktopcouch.local_files.DEFAULT_CONTEXT): |
8 | - if not uri: |
9 | - port = desktopcouch.find_port(ctx=ctx) |
10 | - uri = "http://localhost:%s" % port |
11 | + self.ctx = ctx |
12 | + self.server_uri = uri |
13 | super(CouchDatabase, self).__init__( |
14 | database, uri, record_factory=record_factory, create=create, |
15 | server_class=server_class, oauth_tokens=oauth_tokens, ctx=ctx) |
16 | + |
17 | + def _reconnect(self): |
18 | + if not self.server_uri: |
19 | + port = desktopcouch.find_port(ctx=self.ctx) |
20 | + uri = "http://localhost:%s" % port |
21 | + else: |
22 | + uri = self.server_uri |
23 | + super(CouchDatabase, self)._reconnect(uri=uri) |
24 | + |
25 | |
26 | === modified file 'desktopcouch/records/server_base.py' |
27 | --- desktopcouch/records/server_base.py 2010-07-02 15:32:25 +0000 |
28 | +++ desktopcouch/records/server_base.py 2010-08-25 21:55:59 +0000 |
29 | @@ -22,7 +22,7 @@ |
30 | """The Desktop Couch Records API.""" |
31 | |
32 | import httplib2, urlparse, cgi, copy |
33 | -from time import time |
34 | +from time import time, sleep |
35 | import socket |
36 | |
37 | # please keep desktopcouch python 2.5 compatible for now |
38 | @@ -150,6 +150,8 @@ |
39 | self._server_class_extras = server_class_extras |
40 | self.record_factory = record_factory or Record |
41 | self.server_uri = uri |
42 | + self._server = None |
43 | + self.db = None |
44 | self._reconnect() |
45 | self._changes_since = self.db.info()["update_seq"] |
46 | self._changes_last_used = 0 # Immediate run works. |
47 | @@ -160,32 +162,40 @@ |
48 | ex.args == ("'NoneType' object has no attribute 'makefile'",) |
49 | |
50 | def with_reconnects(self, func, *args, **kwargs): |
51 | - for retry in (2, 1, None): |
52 | + for retry in (3, 2, 1, None): |
53 | try: |
54 | return func(*args, **kwargs) |
55 | except Exception, e: |
56 | if self._is_bug_lp539674(e) and retry: |
57 | - logging.warn("DB connection timed out. Reconnecting.") |
58 | + logging.warn("DB connection failed. Reconnecting.") |
59 | self._reconnect() |
60 | continue |
61 | elif isinstance(e, socket.error): |
62 | logging.warn("Other socket error %s. Reconnecting.", e) |
63 | - time.sleep(0.3) |
64 | + sleep(0.3) |
65 | self._reconnect() |
66 | continue |
67 | else: |
68 | raise |
69 | - |
70 | - def _reconnect(self): |
71 | - logging.info("Connecting to %s." % (self.server_uri or "discovered local port",)) |
72 | - self._server = self._server_class(self.server_uri, |
73 | + raise ValueError("failed to (re-)connect to couchdb server") |
74 | + |
75 | + def _reconnect(self, uri=None): |
76 | + logging.info("Connecting to %s.", self.server_uri or "discovered local port") |
77 | + |
78 | + self._server = self._server_class(uri or self.server_uri, |
79 | **self._server_class_extras) |
80 | if self._database_name not in self._server: |
81 | if self._create: |
82 | self._server.create(self._database_name) |
83 | else: |
84 | raise NoSuchDatabase(self._database_name) |
85 | - self.db = self._server[self._database_name] |
86 | + if self.db is None: |
87 | + self.db = self._server[self._database_name] |
88 | + else: |
89 | + # Monkey-patch the object the user already uses. Oook! |
90 | + new_db = self._server[self._database_name] |
91 | + self.db.resource = new_db.resource |
92 | + |
93 | |
94 | def _temporary_query(self, map_fun, reduce_fun=None, language='javascript', |
95 | wrapper=None, **options): |
96 | @@ -380,9 +390,10 @@ |
97 | |
98 | def record_exists(self, record_id): |
99 | """Check if record with given id exists.""" |
100 | - if record_id not in self.db: |
101 | + try: |
102 | + record = self.with_reconnects(self.db.__getitem__, record_id) |
103 | + except ResourceNotFound: |
104 | return False |
105 | - record = self.with_reconnects(self.db.__getitem__, record_id) |
106 | return not row_is_deleted(record) |
107 | |
108 | def delete_view(self, view_name, design_doc=DEFAULT_DESIGN_DOCUMENT): |
109 | @@ -432,12 +443,36 @@ |
110 | def execute_view(self, view_name, design_doc=DEFAULT_DESIGN_DOCUMENT, |
111 | **params): |
112 | """Execute view and return results.""" |
113 | + |
114 | + class ReconnectingViewWrapper(object): |
115 | + """A view from python-couchdb is an object with attributes that |
116 | + cause HTTP requests to be fired off when accessed. If we wish |
117 | + to be able to reconnect on disappearance of the server, then |
118 | + we must intercept calls from user to python-couchdb.""" |
119 | + |
120 | + def __init__(wrapper, obj, *args, **kwargs): |
121 | + wrapper.obj = self.with_reconnects(obj, *args, **kwargs) |
122 | + |
123 | + def ___call__(wrapper, **options): |
124 | + return self.with_reconnects(wrapper.obj.__call__, **options) |
125 | + |
126 | + def __iter__(wrapper): |
127 | + return self.with_reconnects(wrapper.obj.__iter__) |
128 | + |
129 | + def __len__(wrapper): |
130 | + return self.with_reconnects(wrapper.obj.__len__) |
131 | + |
132 | + def __getitem__(wrapper, key): |
133 | + return ReconnectingViewWrapper(wrapper.obj.__getitem__, key) |
134 | + |
135 | if design_doc is None: |
136 | design_doc = view_name |
137 | |
138 | view_id_fmt = "_design/%(design_doc)s/_view/%(view_name)s" |
139 | - return self.with_reconnects(self.db.view, view_id_fmt % locals(), |
140 | - **params) |
141 | + wrapper = ReconnectingViewWrapper(self.db.view, |
142 | + view_id_fmt % locals(), |
143 | + **params) |
144 | + return wrapper |
145 | |
146 | def add_view(self, view_name, map_js, reduce_js, |
147 | design_doc=DEFAULT_DESIGN_DOCUMENT): |
148 | @@ -447,7 +482,7 @@ |
149 | design_doc = view_name |
150 | |
151 | view = ViewDefinition(design_doc, view_name, map_js, reduce_js) |
152 | - view.sync(self.db) |
153 | + self.with_reconnects(view.sync, self.db) |
154 | assert self.view_exists(view_name, design_doc) |
155 | |
156 | def view_exists(self, view_name, design_doc=DEFAULT_DESIGN_DOCUMENT): |
157 | @@ -573,6 +608,7 @@ |
158 | uri = couchdburi( |
159 | self._server.resource.uri, self.db.name, "_changes", |
160 | since=self._changes_since) |
161 | + ## Assume server has not crashed and URI is the same. FIXME |
162 | resp, data = self._server.resource.http.request(uri, "GET", "", {}) |
163 | if resp["status"] != '200': |
164 | raise IOError( |
165 | |
166 | === modified file 'desktopcouch/records/tests/test_server.py' |
167 | --- desktopcouch/records/tests/test_server.py 2010-07-02 15:42:05 +0000 |
168 | +++ desktopcouch/records/tests/test_server.py 2010-08-25 21:55:59 +0000 |
169 | @@ -19,12 +19,17 @@ |
170 | |
171 | """testing database/contact.py module""" |
172 | import testtools |
173 | +import os |
174 | +import signal |
175 | +import time |
176 | |
177 | import desktopcouch.tests as test_environment |
178 | from desktopcouch.records.server import CouchDatabase |
179 | from desktopcouch.records.server_base import ( |
180 | row_is_deleted, NoSuchDatabase, FieldsConflict, ResourceConflict) |
181 | from desktopcouch.records.record import Record |
182 | +from desktopcouch.stop_local_couchdb import stop_couchdb |
183 | +from desktopcouch import find_pid |
184 | |
185 | # pylint can't deal with failing imports even when they're handled |
186 | # pylint: disable-msg=F0401 |
187 | @@ -53,7 +58,7 @@ |
188 | # Connect to CouchDB server |
189 | self.dbname = self._testMethodName |
190 | self.database = CouchDatabase(self.dbname, create=True, |
191 | - ctx=test_environment.test_context) |
192 | + ctx=self.get_test_context()) |
193 | #create some records to pull out and test |
194 | self.database.put_record(Record({ |
195 | "key1_1": "val1_1", "key1_2": "val1_2", "key1_3": "val1_3", |
196 | @@ -67,8 +72,29 @@ |
197 | |
198 | def tearDown(self): |
199 | """tear down each test""" |
200 | + #del self.database._server[self.dbname] |
201 | + this_context = self.get_test_context() |
202 | + if this_context != test_environment.test_context: |
203 | + stop_couchdb(ctx=this_context) |
204 | super(TestCouchDatabase, self).tearDown() |
205 | - del self.database._server[self.dbname] |
206 | + |
207 | + def get_test_context(self): |
208 | + return test_environment.test_context |
209 | + |
210 | + def maybe_die(self): |
211 | + pass |
212 | + |
213 | + def wait_until_server_dead(self, pid=None): |
214 | + if pid is not None: |
215 | + pid = find_pid(start_if_not_running=False, ctx=self.get_test_context()) |
216 | + if pid is None: |
217 | + return |
218 | + while True: |
219 | + try: |
220 | + os.kill(pid, 0) # Wait until exited |
221 | + time.sleep(0.1) |
222 | + except OSError: |
223 | + break |
224 | |
225 | def test_database_not_exists(self): |
226 | self.assertRaises( |
227 | @@ -78,12 +104,14 @@ |
228 | """Test getting mutliple records by type""" |
229 | records = self.database.get_records( |
230 | record_type="test.com",create_view=True) |
231 | - self.assertEqual(3,len(records)) |
232 | + self.maybe_die() # should be able to survive couchdb death |
233 | + self.assertEqual(3, len(records)) |
234 | |
235 | def test_get_record(self): |
236 | """Test getting a record.""" |
237 | record = Record({'record_number': 0}, record_type="http://example.com/") |
238 | record_id = self.database.put_record(record) |
239 | + self.maybe_die() # should be able to survive couchdb death |
240 | retrieved_record = self.database.get_record(record_id) |
241 | self.assertEqual(0, retrieved_record['record_number']) |
242 | |
243 | @@ -91,6 +119,7 @@ |
244 | """Test putting a record.""" |
245 | record = Record({'record_number': 0}, record_type="http://example.com/") |
246 | record_id = self.database.put_record(record) |
247 | + ###self.maybe_die() # should be able to survive couchdb death |
248 | retrieved_record = self.database._server[self.dbname][record_id] |
249 | self.assertEqual( |
250 | record['record_number'], retrieved_record['record_number']) |
251 | @@ -121,6 +150,7 @@ |
252 | record = Record({'record_number': 0}, record_type="http://example.com/") |
253 | record_id = self.database.put_record(record) |
254 | self.database.delete_record(record_id) |
255 | + ###self.maybe_die() # should be able to survive couchdb death |
256 | deleted_record = self.database._server[self.dbname][record_id] |
257 | self.assert_(deleted_record['application_annotations']['Ubuntu One'][ |
258 | 'private_application_annotations']['deleted']) |
259 | @@ -154,6 +184,7 @@ |
260 | record = Record({'record_number': 0}, record_type="http://example.com/") |
261 | record_id = self.database.put_record(record) |
262 | self.database.delete_record(record_id) |
263 | + self.maybe_die() # should be able to survive couchdb death |
264 | retrieved_record = self.database.get_record(record_id) |
265 | self.assertEqual(None, retrieved_record) |
266 | |
267 | @@ -162,6 +193,7 @@ |
268 | record = Record({'record_number': 0}, record_type="http://example.com/") |
269 | self.assert_(not self.database.record_exists("ThisMustNotExist")) |
270 | record_id = self.database.put_record(record) |
271 | + self.maybe_die() # should be able to survive couchdb death |
272 | self.assert_(self.database.record_exists(record_id)) |
273 | self.database.delete_record(record_id) |
274 | self.assert_(not self.database.record_exists(record_id)) |
275 | @@ -171,12 +203,14 @@ |
276 | dictionary = {'record_number': 0, 'field1': 1, 'field2': 2} |
277 | record = Record(dictionary, record_type="http://example.com/") |
278 | record_id = self.database.put_record(record) |
279 | + self.maybe_die() # should be able to survive couchdb death |
280 | # manipulate the database 'out of view' |
281 | non_working_copy = self.database.get_record(record_id) |
282 | non_working_copy['field2'] = 22 |
283 | non_working_copy['field3'] = 3 |
284 | self.database.put_record(non_working_copy) |
285 | self.database.update_fields(record_id, {'field1': 11}) |
286 | + self.maybe_die() # should be able to survive couchdb death |
287 | working_copy = self.database.get_record(record_id) |
288 | self.assertEqual(0, working_copy['record_number']) |
289 | self.assertEqual(11, working_copy['field1']) |
290 | @@ -200,10 +234,12 @@ |
291 | self.assertRaises( |
292 | KeyError, self.database.delete_view, view2_name, design_doc) |
293 | self.database.add_view(view1_name, map_js, reduce_js, design_doc) |
294 | + self.maybe_die() # should be able to survive couchdb death |
295 | self.database.add_view(view2_name, map_js, reduce_js, design_doc) |
296 | self.database.delete_view(view1_name, design_doc) |
297 | self.assertRaises( |
298 | KeyError, self.database.delete_view, view1_name, design_doc) |
299 | + self.maybe_die() # should be able to survive couchdb death |
300 | self.database.delete_view(view2_name, design_doc) |
301 | self.assertRaises( |
302 | KeyError, self.database.delete_view, view2_name, design_doc) |
303 | @@ -237,6 +273,7 @@ |
304 | record_ids_we_care_about.remove(row.id) |
305 | self.assertFalse(row_is_deleted(row)) |
306 | |
307 | + ###self.maybe_die() # should be able to survive couchdb death |
308 | self.assertTrue(len(record_ids_we_care_about) == 0, "expected zero") |
309 | |
310 | self.assertRaises(KeyError, self.database.get_records, |
311 | @@ -250,6 +287,7 @@ |
312 | map_js = """function(doc) { emit(doc._id, null) }""" |
313 | self.database.add_view(view_name, map_js, None, design_doc) |
314 | |
315 | + ###self.maybe_die() # should be able to survive couchdb death |
316 | self.assertEqual(self.database.list_views(design_doc), [view_name]) |
317 | self.database.delete_view(view_name, design_doc) |
318 | |
319 | @@ -257,11 +295,13 @@ |
320 | |
321 | def test_get_view_by_type_new_but_already(self): |
322 | self.database.get_records(create_view=True) |
323 | + self.maybe_die() # should be able to survive couchdb death |
324 | self.database.get_records(create_view=True) |
325 | # No exceptions on second run? Yay. |
326 | |
327 | def test_get_view_by_type_createxcl_fail(self): |
328 | self.database.get_records(create_view=True) |
329 | + self.maybe_die() # should be able to survive couchdb death |
330 | self.assertRaises(KeyError, self.database.get_records, create_view=None) |
331 | |
332 | def test_get_changes(self): |
333 | @@ -321,6 +361,8 @@ |
334 | # Ensure time is same. |
335 | self.assertEqual(saved_time, self.database._changes_last_used) |
336 | |
337 | + ###self.maybe_die() # should be able to survive couchdb death |
338 | + |
339 | # Next time we run, we get the same event again. |
340 | # Consume queued changes. |
341 | count = self.database.report_changes(rep) |
342 | @@ -370,6 +412,7 @@ |
343 | constructed_record.attach(content, "nu/mbe/rs", "text/plain") |
344 | constructed_record.attach("string", "another document", "text/plain") |
345 | |
346 | + self.maybe_die() # should be able to survive couchdb death |
347 | constructed_record.attach("XXXXXXXXX", "never used", "text/plain") |
348 | constructed_record.detach("never used") # detach works before commit. |
349 | |
350 | @@ -382,6 +425,7 @@ |
351 | self.assertRaises(KeyError, constructed_record.attach, content, |
352 | "another document", "text/x-rst") |
353 | |
354 | + self.maybe_die() # should be able to survive couchdb death |
355 | record_id = self.database.put_record(constructed_record) |
356 | retrieved_record = self.database.get_record(record_id) |
357 | |
358 | @@ -402,6 +446,7 @@ |
359 | # get new |
360 | retrieved_record = self.database.get_record(record_id) |
361 | |
362 | + self.maybe_die() # should be able to survive couchdb death |
363 | # We can get a list of attachments. |
364 | self.assertEqual(set(retrieved_record.list_attachments()), |
365 | set(["nu/mbe/rs", "Document"])) |
366 | @@ -424,6 +469,7 @@ |
367 | self.assertEqual(out_data, content.getvalue()) |
368 | self.assertEqual(out_content_type, "text/plain") |
369 | |
370 | + self.maybe_die() # should be able to survive couchdb death |
371 | # Asking for a named document that does not exist causes KeyError. |
372 | self.assertRaises(KeyError, retrieved_record.attachment_data, |
373 | "NoExist") |
374 | @@ -454,6 +500,7 @@ |
375 | # ordinary requests are in key order |
376 | self.assertEqual(data, sorted(data)) |
377 | |
378 | + ###self.maybe_die() # should be able to survive couchdb death |
379 | # now request descending order and confirm that it *is* descending |
380 | descdata = [i.key for i in |
381 | list(self.database.execute_view(view1_name, design_doc, |
382 | @@ -513,3 +560,36 @@ |
383 | self.fail() |
384 | except FieldsConflict, e: |
385 | self.assertEqual({('field1',): (22, 11)}, e.conflicts) |
386 | + |
387 | + |
388 | +class TestServerDiesSegv(TestCouchDatabase): |
389 | + def get_test_context(self): |
390 | + try: |
391 | + return self.ctx |
392 | + except AttributeError: |
393 | + self.ctx = test_environment.create_new_test_environment() |
394 | + return self.ctx |
395 | + |
396 | + def maybe_die(self): |
397 | + time.sleep(2) |
398 | + pid = find_pid(start_if_not_running=False, ctx=self.get_test_context()) |
399 | + if pid is None: |
400 | + print "couchdb has already quit! That's unexpected." |
401 | + return |
402 | + print "DIE, process", pid, "!" |
403 | + os.kill(pid, signal.SIGSEGV) |
404 | + self.wait_until_server_dead(pid=pid) |
405 | + |
406 | + |
407 | +class TestServerDiesNormal(TestCouchDatabase): |
408 | + def get_test_context(self): |
409 | + try: |
410 | + return self.ctx |
411 | + except AttributeError: |
412 | + self.ctx = test_environment.create_new_test_environment() |
413 | + return self.ctx |
414 | + |
415 | + def maybe_die(self): |
416 | + time.sleep(2) |
417 | + stop_couchdb(ctx=self.get_test_context()) |
418 | + self.wait_until_server_dead() |
419 | |
420 | === modified file 'desktopcouch/stop_local_couchdb.py' |
421 | --- desktopcouch/stop_local_couchdb.py 2010-02-04 22:31:41 +0000 |
422 | +++ desktopcouch/stop_local_couchdb.py 2010-08-25 21:55:59 +0000 |
423 | @@ -25,7 +25,7 @@ |
424 | from desktopcouch import local_files |
425 | |
426 | def stop_couchdb(ctx=local_files.DEFAULT_CONTEXT): |
427 | - local_exec = ctx.couch_exec_command + ["-k"] |
428 | + local_exec = ctx.couch_exec_command + ["-d"] |
429 | try: |
430 | retcode = subprocess.call(local_exec, shell=False) |
431 | if retcode < 0: |
Looks good tests pass