Merge lp:~cmiller/desktopcouch/views-with-reconnector-proxy into lp:desktopcouch

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
Reviewer Review Type Date Requested Status
Eric Casteleijn (community) Approve
Review via email: mp+33471@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Eric Casteleijn (thisfred) wrote :

Looks good tests pass

review: Approve
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://pastebin.canonical.com/36781/

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:

Subscribers

People subscribed via source and target branches