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

Proposed by Vincenzo Di Somma
Status: Merged
Approved by: Manuel de la Peña
Approved revision: 184
Merged at revision: 175
Proposed branch: lp:~vds/desktopcouch/views-with-reconnector-proxy
Merge into: lp:desktopcouch
Diff against target: 480 lines (+172/-21)
4 files modified
desktopcouch/records/server_base.py (+77/-15)
desktopcouch/records/tests/test_server.py (+86/-4)
desktopcouch/start_local_couchdb.py (+8/-1)
desktopcouch/stop_local_couchdb.py (+1/-1)
To merge this branch: bzr merge lp:~vds/desktopcouch/views-with-reconnector-proxy
Reviewer Review Type Date Requested Status
Manuel de la Peña (community) Approve
Eric Casteleijn (community) Approve
Review via email: mp+35853@code.launchpad.net

Commit message

Now View and ViewResults can reconnect if the couchdb server crashes.

Description of the change

Now ViewResult reconnects when the server crashes.

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

Ooooh, live monkeys!

Code looks good, but the tests take ages now, and it looks like the server is killed and restarted way too often, though most of the tests seem to pass. Adding test output below. If this is all intentional, approved.

One test does fail, but I believe that's because I'm on lucid:

===============================================================================
[ERROR]: desktopcouch.pair.tests.test_ubuntuone_pairing

Traceback (most recent call last):
  File "/usr/lib/python2.6/dist-packages/twisted/trial/runner.py", line 563, in loadPackage
    module = modinfo.load()
  File "/usr/lib/python2.6/dist-packages/twisted/python/modules.py", line 381, in load
    return self.pathEntry.pythonPath.moduleLoader(self.name)
  File "/usr/lib/python2.6/dist-packages/twisted/python/reflect.py", line 464, in namedAny
    topLevelPackage = _importAndCheckStack(trialname)
  File "/home/eric/canonical/desktopcouch/r-vds/desktopcouch/pair/tests/test_ubuntuone_pairing.py", line 19, in <module>
    from mocker import Mocker
exceptions.ImportError: No module named mocker
-------------------------------------------------------------------------------

eric@eric-laptop:~/canonical/desktopcouch/r-vds$ PYTHONPATH=. trial desktopcouch
Apache CouchDB has started, time to relax.
Browse your desktop CouchDB at file:///tmp/tmpEaIpq4/data/couchdb.html
desktopcouch.bookmarks.tests.test_record
  TestBookmarkRecord
    test_bookmark_record ... [OK]
  TestFeedRecord
    test_feed_record ... [OK]
  TestFolderRecord
    test_folder_record ... [OK]
  TestSeparatorRecord
    test_separator_record ... [OK]
desktopcouch.contacts.tests.test_contactspicker
  TestContactsPicker
    test_can_contruct_contactspicker ... [OK]
desktopcouch.contacts.tests.test_create
  TestCreate
    test_create_many_contacts ... [OK]
    test_head_or_tails ... [OK]
    test_random_bools ... [OK]
desktopcouch.contacts.tests.test_record
  TestContactRecord
    test_contact_record ... [OK]
desktopcouch.contacts.tests.test_view
  TestLocalFiles
    test_find_contact_starting ... [OK]
    test_find_contacts_exact ... [OK]
desktopcouch.notes.tests.test_record
  TestNoteRecord
    test_note_record ... [OK]
desktopcouch.pair.tests.test_couchdb_io
  TestCouchdbIo
    test_get_database_names_replicatable ... [OK]
    test_get_database_names_replicatable_bad_server ... [SKIPPED]
    test_get_my_host_unique_id ... [OK]
    test_mkuri ... [OK]
    test_obsfuscation ... [OK]
   ...

review: Approve
Revision history for this message
Vincenzo Di Somma (vds) wrote :

> Ooooh, live monkeys!
>
> Code looks good, but the tests take ages now, and it looks like the server is
> killed and restarted way too often, though most of the tests seem to pass.
> Adding test output below. If this is all intentional, approved.
>
> One test does fail, but I believe that's because I'm on lucid:

About the test failing, mocker is in Lucid and should also be a developer dependency, if you install the package the test will pass, I'll file a bug to update our dependency packages.

The couchdb server is killed and restarted many time on purpose, to check that desktopcouch is able to reconnect to a different instance of couchdb if the previous one crashed.
It slows down the tests a lot I know. But there's not much we can do here apart from moving that logic somewhere else, like in python-couchdb.
the weird text couchdb's fault, I'll try to hide it in the next branch.

Revision history for this message
Manuel de la Peña (mandel) wrote :

All tests pass. I just have a question, dont we have a better name for a method than _is_bug_lp539674. I know we can go to lp and check that out, but it would be nice a more self explanatory name.

Revision history for this message
Manuel de la Peña (mandel) wrote :

Cool +!

review: Approve
Revision history for this message
dobey (dobey) wrote :
Download full text (109.7 KiB)

The attempt to merge lp:~vds/desktopcouch/views-with-reconnector-proxy into lp:desktopcouch failed. Below is the output from the failed tests.

running build
running build_py
creating build
creating build/lib.linux-i686-2.6
creating build/lib.linux-i686-2.6/desktopcouch
copying desktopcouch/stop_local_couchdb.py -> build/lib.linux-i686-2.6/desktopcouch
copying desktopcouch/start_local_couchdb.py -> build/lib.linux-i686-2.6/desktopcouch
copying desktopcouch/replication.py -> build/lib.linux-i686-2.6/desktopcouch
copying desktopcouch/local_files.py -> build/lib.linux-i686-2.6/desktopcouch
copying desktopcouch/__init__.py -> build/lib.linux-i686-2.6/desktopcouch
creating build/lib.linux-i686-2.6/desktopcouch/tests
copying desktopcouch/tests/test_start_local_couchdb.py -> build/lib.linux-i686-2.6/desktopcouch/tests
copying desktopcouch/tests/test_local_files.py -> build/lib.linux-i686-2.6/desktopcouch/tests
copying desktopcouch/tests/__init__.py -> build/lib.linux-i686-2.6/desktopcouch/tests
copying desktopcouch/tests/test_replication.py -> build/lib.linux-i686-2.6/desktopcouch/tests
creating build/lib.linux-i686-2.6/desktopcouch/tasks
copying desktopcouch/tasks/record.py -> build/lib.linux-i686-2.6/desktopcouch/tasks
copying desktopcouch/tasks/__init__.py -> build/lib.linux-i686-2.6/desktopcouch/tasks
creating build/lib.linux-i686-2.6/desktopcouch/pair
copying desktopcouch/pair/__init__.py -> build/lib.linux-i686-2.6/desktopcouch/pair
creating build/lib.linux-i686-2.6/desktopcouch/records
copying desktopcouch/records/field_registry.py -> build/lib.linux-i686-2.6/desktopcouch/records
copying desktopcouch/records/record.py -> build/lib.linux-i686-2.6/desktopcouch/records
copying desktopcouch/records/server.py -> build/lib.linux-i686-2.6/desktopcouch/records
copying desktopcouch/records/couchgrid.py -> build/lib.linux-i686-2.6/desktopcouch/records
copying desktopcouch/records/__init__.py -> build/lib.linux-i686-2.6/desktopcouch/records
copying desktopcouch/records/server_base.py -> build/lib.linux-i686-2.6/desktopcouch/records
creating build/lib.linux-i686-2.6/desktopcouch/replication_services
copying desktopcouch/replication_services/ubuntuone.py -> build/lib.linux-i686-2.6/desktopcouch/replication_services
copying desktopcouch/replication_services/__init__.py -> build/lib.linux-i686-2.6/desktopcouch/replication_services
copying desktopcouch/replication_services/example.py -> build/lib.linux-i686-2.6/desktopcouch/replication_services
creating build/lib.linux-i686-2.6/desktopcouch/notes
copying desktopcouch/notes/record.py -> build/lib.linux-i686-2.6/desktopcouch/notes
copying desktopcouch/notes/__init__.py -> build/lib.linux-i686-2.6/desktopcouch/notes
creating build/lib.linux-i686-2.6/desktopcouch/bookmarks
copying desktopcouch/bookmarks/record.py -> build/lib.linux-i686-2.6/desktopcouch/bookmarks
copying desktopcouch/bookmarks/__init__.py -> build/lib.linux-i686-2.6/desktopcouch/bookmarks
creating build/lib.linux-i686-2.6/desktopcouch/contacts
copying desktopcouch/contacts/record.py -> build/lib.linux-i686-2.6/desktopcouch/contacts
copying desktopcouch/contacts/view.py -> build/lib.linux-i686-2.6/desktopcouch/contacts
copying desktopcouch/contac...

Revision history for this message
dobey (dobey) wrote :
Download full text (112.7 KiB)

The attempt to merge lp:~vds/desktopcouch/views-with-reconnector-proxy into lp:desktopcouch failed. Below is the output from the failed tests.

running build
running build_py
creating build
creating build/lib.linux-i686-2.6
creating build/lib.linux-i686-2.6/desktopcouch
copying desktopcouch/stop_local_couchdb.py -> build/lib.linux-i686-2.6/desktopcouch
copying desktopcouch/start_local_couchdb.py -> build/lib.linux-i686-2.6/desktopcouch
copying desktopcouch/replication.py -> build/lib.linux-i686-2.6/desktopcouch
copying desktopcouch/local_files.py -> build/lib.linux-i686-2.6/desktopcouch
copying desktopcouch/__init__.py -> build/lib.linux-i686-2.6/desktopcouch
creating build/lib.linux-i686-2.6/desktopcouch/tests
copying desktopcouch/tests/test_start_local_couchdb.py -> build/lib.linux-i686-2.6/desktopcouch/tests
copying desktopcouch/tests/test_local_files.py -> build/lib.linux-i686-2.6/desktopcouch/tests
copying desktopcouch/tests/__init__.py -> build/lib.linux-i686-2.6/desktopcouch/tests
copying desktopcouch/tests/test_replication.py -> build/lib.linux-i686-2.6/desktopcouch/tests
creating build/lib.linux-i686-2.6/desktopcouch/tasks
copying desktopcouch/tasks/record.py -> build/lib.linux-i686-2.6/desktopcouch/tasks
copying desktopcouch/tasks/__init__.py -> build/lib.linux-i686-2.6/desktopcouch/tasks
creating build/lib.linux-i686-2.6/desktopcouch/pair
copying desktopcouch/pair/__init__.py -> build/lib.linux-i686-2.6/desktopcouch/pair
creating build/lib.linux-i686-2.6/desktopcouch/records
copying desktopcouch/records/field_registry.py -> build/lib.linux-i686-2.6/desktopcouch/records
copying desktopcouch/records/record.py -> build/lib.linux-i686-2.6/desktopcouch/records
copying desktopcouch/records/server.py -> build/lib.linux-i686-2.6/desktopcouch/records
copying desktopcouch/records/couchgrid.py -> build/lib.linux-i686-2.6/desktopcouch/records
copying desktopcouch/records/__init__.py -> build/lib.linux-i686-2.6/desktopcouch/records
copying desktopcouch/records/server_base.py -> build/lib.linux-i686-2.6/desktopcouch/records
creating build/lib.linux-i686-2.6/desktopcouch/replication_services
copying desktopcouch/replication_services/ubuntuone.py -> build/lib.linux-i686-2.6/desktopcouch/replication_services
copying desktopcouch/replication_services/__init__.py -> build/lib.linux-i686-2.6/desktopcouch/replication_services
copying desktopcouch/replication_services/example.py -> build/lib.linux-i686-2.6/desktopcouch/replication_services
creating build/lib.linux-i686-2.6/desktopcouch/notes
copying desktopcouch/notes/record.py -> build/lib.linux-i686-2.6/desktopcouch/notes
copying desktopcouch/notes/__init__.py -> build/lib.linux-i686-2.6/desktopcouch/notes
creating build/lib.linux-i686-2.6/desktopcouch/bookmarks
copying desktopcouch/bookmarks/record.py -> build/lib.linux-i686-2.6/desktopcouch/bookmarks
copying desktopcouch/bookmarks/__init__.py -> build/lib.linux-i686-2.6/desktopcouch/bookmarks
creating build/lib.linux-i686-2.6/desktopcouch/contacts
copying desktopcouch/contacts/record.py -> build/lib.linux-i686-2.6/desktopcouch/contacts
copying desktopcouch/contacts/view.py -> build/lib.linux-i686-2.6/desktopcouch/contacts
copying desktopcouch/contac...

Revision history for this message
dobey (dobey) wrote :
Download full text (112.3 KiB)

The attempt to merge lp:~vds/desktopcouch/views-with-reconnector-proxy into lp:desktopcouch failed. Below is the output from the failed tests.

running build
running build_py
creating build
creating build/lib.linux-i686-2.6
creating build/lib.linux-i686-2.6/desktopcouch
copying desktopcouch/stop_local_couchdb.py -> build/lib.linux-i686-2.6/desktopcouch
copying desktopcouch/start_local_couchdb.py -> build/lib.linux-i686-2.6/desktopcouch
copying desktopcouch/replication.py -> build/lib.linux-i686-2.6/desktopcouch
copying desktopcouch/local_files.py -> build/lib.linux-i686-2.6/desktopcouch
copying desktopcouch/__init__.py -> build/lib.linux-i686-2.6/desktopcouch
creating build/lib.linux-i686-2.6/desktopcouch/tests
copying desktopcouch/tests/test_start_local_couchdb.py -> build/lib.linux-i686-2.6/desktopcouch/tests
copying desktopcouch/tests/test_local_files.py -> build/lib.linux-i686-2.6/desktopcouch/tests
copying desktopcouch/tests/__init__.py -> build/lib.linux-i686-2.6/desktopcouch/tests
copying desktopcouch/tests/test_replication.py -> build/lib.linux-i686-2.6/desktopcouch/tests
creating build/lib.linux-i686-2.6/desktopcouch/tasks
copying desktopcouch/tasks/record.py -> build/lib.linux-i686-2.6/desktopcouch/tasks
copying desktopcouch/tasks/__init__.py -> build/lib.linux-i686-2.6/desktopcouch/tasks
creating build/lib.linux-i686-2.6/desktopcouch/pair
copying desktopcouch/pair/__init__.py -> build/lib.linux-i686-2.6/desktopcouch/pair
creating build/lib.linux-i686-2.6/desktopcouch/records
copying desktopcouch/records/field_registry.py -> build/lib.linux-i686-2.6/desktopcouch/records
copying desktopcouch/records/record.py -> build/lib.linux-i686-2.6/desktopcouch/records
copying desktopcouch/records/server.py -> build/lib.linux-i686-2.6/desktopcouch/records
copying desktopcouch/records/couchgrid.py -> build/lib.linux-i686-2.6/desktopcouch/records
copying desktopcouch/records/__init__.py -> build/lib.linux-i686-2.6/desktopcouch/records
copying desktopcouch/records/server_base.py -> build/lib.linux-i686-2.6/desktopcouch/records
creating build/lib.linux-i686-2.6/desktopcouch/replication_services
copying desktopcouch/replication_services/ubuntuone.py -> build/lib.linux-i686-2.6/desktopcouch/replication_services
copying desktopcouch/replication_services/__init__.py -> build/lib.linux-i686-2.6/desktopcouch/replication_services
copying desktopcouch/replication_services/example.py -> build/lib.linux-i686-2.6/desktopcouch/replication_services
creating build/lib.linux-i686-2.6/desktopcouch/notes
copying desktopcouch/notes/record.py -> build/lib.linux-i686-2.6/desktopcouch/notes
copying desktopcouch/notes/__init__.py -> build/lib.linux-i686-2.6/desktopcouch/notes
creating build/lib.linux-i686-2.6/desktopcouch/bookmarks
copying desktopcouch/bookmarks/record.py -> build/lib.linux-i686-2.6/desktopcouch/bookmarks
copying desktopcouch/bookmarks/__init__.py -> build/lib.linux-i686-2.6/desktopcouch/bookmarks
creating build/lib.linux-i686-2.6/desktopcouch/contacts
copying desktopcouch/contacts/record.py -> build/lib.linux-i686-2.6/desktopcouch/contacts
copying desktopcouch/contacts/view.py -> build/lib.linux-i686-2.6/desktopcouch/contacts
copying desktopcouch/contac...

183. By Vincenzo Di Somma

fixed issue with keyring

184. By Vincenzo Di Somma

logging when the issue happens

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'desktopcouch/records/server_base.py'
--- desktopcouch/records/server_base.py 2010-08-23 22:51:48 +0000
+++ desktopcouch/records/server_base.py 2010-09-22 14:14:44 +0000
@@ -150,22 +150,24 @@
150 self._server_class_extras = server_class_extras150 self._server_class_extras = server_class_extras
151 self.record_factory = record_factory or Record151 self.record_factory = record_factory or Record
152 self.server_uri = uri152 self.server_uri = uri
153 self._server = None
154 self.db = None
153 self._reconnect()155 self._reconnect()
154 self._changes_since = self.db.info()["update_seq"]156 self._changes_since = self.db.info()["update_seq"]
155 self._changes_last_used = 0 # Immediate run works.157 self._changes_last_used = 0 # Immediate run works.
156158
157 @staticmethod159 @staticmethod
158 def _is_bug_lp539674(ex):160 def _is_reconnection_fail(ex):
159 return isinstance(ex, AttributeError) and \161 return isinstance(ex, AttributeError) and \
160 ex.args == ("'NoneType' object has no attribute 'makefile'",)162 ex.args == ("'NoneType' object has no attribute 'makefile'",)
161163
162 def with_reconnects(self, func, *args, **kwargs):164 def with_reconnects(self, func, *args, **kwargs):
163 for retry in (2, 1, None):165 for retry in (3, 2, 1, None):
164 try:166 try:
165 return func(*args, **kwargs)167 return func(*args, **kwargs)
166 except Exception, e:168 except Exception, e:
167 if self._is_bug_lp539674(e) and retry:169 if self._is_reconnection_fail(e) and retry:
168 logging.warn("DB connection timed out. Reconnecting.")170 logging.warn("DB connection failed. Reconnecting.")
169 self._reconnect()171 self._reconnect()
170 continue172 continue
171 elif isinstance(e, socket.error):173 elif isinstance(e, socket.error):
@@ -175,9 +177,10 @@
175 continue177 continue
176 else:178 else:
177 raise179 raise
180 raise ValueError("failed to (re-)connect to couchdb server")
178181
179 def _reconnect(self, uri=None):182 def _reconnect(self, uri=None):
180 logging.info("Connecting to %s." % (self.server_uri or "discovered local port",))183 logging.info("Connecting to %s.", self.server_uri or "discovered local port")
181184
182 self._server = self._server_class(uri or self.server_uri,185 self._server = self._server_class(uri or self.server_uri,
183 **self._server_class_extras)186 **self._server_class_extras)
@@ -186,7 +189,12 @@
186 self._server.create(self._database_name)189 self._server.create(self._database_name)
187 else:190 else:
188 raise NoSuchDatabase(self._database_name)191 raise NoSuchDatabase(self._database_name)
189 self.db = self._server[self._database_name]192 if self.db is None:
193 self.db = self._server[self._database_name]
194 else:
195 # Monkey-patch the object the user already uses. Oook!
196 new_db = self._server[self._database_name]
197 self.db.resource = new_db.resource
190198
191 def _temporary_query(self, map_fun, reduce_fun=None, language='javascript',199 def _temporary_query(self, map_fun, reduce_fun=None, language='javascript',
192 wrapper=None, **options):200 wrapper=None, **options):
@@ -381,9 +389,10 @@
381389
382 def record_exists(self, record_id):390 def record_exists(self, record_id):
383 """Check if record with given id exists."""391 """Check if record with given id exists."""
384 if record_id not in self.db:392 try:
393 record = self.with_reconnects(self.db.__getitem__, record_id)
394 except ResourceNotFound:
385 return False395 return False
386 record = self.with_reconnects(self.db.__getitem__, record_id)
387 return not row_is_deleted(record)396 return not row_is_deleted(record)
388397
389 def delete_view(self, view_name, design_doc=DEFAULT_DESIGN_DOCUMENT):398 def delete_view(self, view_name, design_doc=DEFAULT_DESIGN_DOCUMENT):
@@ -434,23 +443,28 @@
434 **params):443 **params):
435 """Execute view and return results."""444 """Execute view and return results."""
436445
437 class ReconnectingViewWrapper:446 class ReconnectingViewWrapper(object):
438 """A view from python-couchdb is an object with attributes that447 """A view from python-couchdb is an object with attributes that
439 cause HTTP requests to be fired off when accessed. If we wish448 cause HTTP requests to be fired off when accessed. If we wish
440 to be able to reconnect on disappearance of the server, then449 to be able to reconnect on disappearance of the server, then
441 we must intercept calls from user to python-couchdb."""450 we must intercept calls from user to python-couchdb."""
442451
443 def __init__(wrapper, view, *args, **kwargs):452 def __init__(wrapper, obj, *args, **kwargs):
444 wrapper.view = self.with_reconnects(view, *args, **kwargs)453 wrapper.obj = self.with_reconnects(obj, *args, **kwargs)
445454
446 def ___call__(wrapper, **options):455 def ___call__(wrapper, **options):
447 return self.with_reconnects(wrapper.view.__call__, **options)456 return self.with_reconnects(wrapper.obj.__call__, **options)
448457
449 def __iter__(wrapper):458 def __iter__(wrapper):
450 return self.with_reconnects(wrapper.view.__iter__)459 return self.view_with_reconnects(wrapper.obj.__iter__,
460 wrapper.obj)
461
462 def __len__(wrapper):
463 return self.view_with_reconnects(wrapper.obj.__len__,
464 wrapper.obj)
451465
452 def __getitem__(wrapper, key):466 def __getitem__(wrapper, key):
453 return self.with_reconnects(wrapper.view.__getitem__, key)467 return ReconnectingViewWrapper(wrapper.obj.__getitem__, key)
454468
455 if design_doc is None:469 if design_doc is None:
456 design_doc = view_name470 design_doc = view_name
@@ -461,6 +475,44 @@
461 **params)475 **params)
462 return wrapper476 return wrapper
463477
478 def view_with_reconnects(self, func, view, *args, **kwargs):
479 for retry in (3, 2, 1, None):
480 try:
481 return func(*args, **kwargs)
482 except Exception, e:
483 if self._is_reconnection_fail(e) and retry:
484 logging.warn("DB connection failed. Reconnecting.")
485 self._reconnect_view(view_result=view)
486 continue
487 elif isinstance(e, socket.error):
488 logging.warn("Other socket error %s. Reconnecting.", e)
489 sleep(0.3)
490 self._reconnect()
491 continue
492 else:
493 raise
494 raise ValueError("failed to (re-)connect to couchdb server")
495
496 def _reconnect_view(self, uri=None, view_result=None):
497 logging.info("Connecting to %s.",
498 self.server_uri or "discovered local port")
499 self._server = self._server_class(
500 uri or '/'.join(self.db.resource.uri.split('/')[:-1]),
501 **self._server_class_extras)
502 if self._database_name not in self._server:
503 if self._create:
504 self._server.create(self._database_name)
505 else:
506 raise NoSuchDatabase(self._database_name)
507 if self.db is None:
508 self.db = self._server[self._database_name]
509 else:
510 # Monkey-patch the object the user already uses. Oook!
511 view_url = urlparse.urlparse(view_result.view.resource.uri)
512 db_url = urlparse.urlparse(self.db.resource.uri)
513 view_result.view.resource.uri = urlparse.urlunparse(
514 [view_url[0], db_url[1]] + list(view_url[2:]))
515
464 def add_view(self, view_name, map_js, reduce_js,516 def add_view(self, view_name, map_js, reduce_js,
465 design_doc=DEFAULT_DESIGN_DOCUMENT):517 design_doc=DEFAULT_DESIGN_DOCUMENT):
466 """Create a view, given a name and the two parts (map and reduce).518 """Create a view, given a name and the two parts (map and reduce).
@@ -469,7 +521,7 @@
469 design_doc = view_name521 design_doc = view_name
470522
471 view = ViewDefinition(design_doc, view_name, map_js, reduce_js)523 view = ViewDefinition(design_doc, view_name, map_js, reduce_js)
472 view.sync(self.db)524 self.with_reconnects(view.sync, self.db)
473 assert self.view_exists(view_name, design_doc)525 assert self.view_exists(view_name, design_doc)
474526
475 def view_exists(self, view_name, design_doc=DEFAULT_DESIGN_DOCUMENT):527 def view_exists(self, view_name, design_doc=DEFAULT_DESIGN_DOCUMENT):
@@ -595,6 +647,7 @@
595 uri = couchdburi(647 uri = couchdburi(
596 self._server.resource.uri, self.db.name, "_changes",648 self._server.resource.uri, self.db.name, "_changes",
597 since=self._changes_since)649 since=self._changes_since)
650 ## Assume server has not crashed and URI is the same. FIXME
598 resp, data = self._server.resource.http.request(uri, "GET", "", {})651 resp, data = self._server.resource.http.request(uri, "GET", "", {})
599 if resp["status"] != '200':652 if resp["status"] != '200':
600 raise IOError(653 raise IOError(
@@ -611,3 +664,12 @@
611 # If exception in cb, we never update governor.664 # If exception in cb, we never update governor.
612 self._changes_last_used = now665 self._changes_last_used = now
613 return call_count666 return call_count
667
668 def ensure_full_commit(self):
669 """
670 Make sure that CouchDb flushes all writes to the database,
671 flushing all delayed commits, before going on.
672 """
673 self.db.resource.post(
674 path='_ensure_full_commit',
675 headers={'Content-Type': 'application/json'})
614676
=== modified file 'desktopcouch/records/tests/test_server.py'
--- desktopcouch/records/tests/test_server.py 2010-07-02 15:42:05 +0000
+++ desktopcouch/records/tests/test_server.py 2010-09-22 14:14:44 +0000
@@ -19,12 +19,17 @@
1919
20"""testing database/contact.py module"""20"""testing database/contact.py module"""
21import testtools21import testtools
22import os
23import signal
24import time
2225
23import desktopcouch.tests as test_environment26import desktopcouch.tests as test_environment
24from desktopcouch.records.server import CouchDatabase27from desktopcouch.records.server import CouchDatabase
25from desktopcouch.records.server_base import (28from desktopcouch.records.server_base import (
26 row_is_deleted, NoSuchDatabase, FieldsConflict, ResourceConflict)29 row_is_deleted, NoSuchDatabase, FieldsConflict, ResourceConflict)
27from desktopcouch.records.record import Record30from desktopcouch.records.record import Record
31from desktopcouch.stop_local_couchdb import stop_couchdb
32from desktopcouch import find_pid
2833
29# pylint can't deal with failing imports even when they're handled34# pylint can't deal with failing imports even when they're handled
30# pylint: disable-msg=F040135# pylint: disable-msg=F0401
@@ -53,7 +58,7 @@
53 # Connect to CouchDB server58 # Connect to CouchDB server
54 self.dbname = self._testMethodName59 self.dbname = self._testMethodName
55 self.database = CouchDatabase(self.dbname, create=True,60 self.database = CouchDatabase(self.dbname, create=True,
56 ctx=test_environment.test_context)61 ctx=self.get_test_context())
57 #create some records to pull out and test62 #create some records to pull out and test
58 self.database.put_record(Record({63 self.database.put_record(Record({
59 "key1_1": "val1_1", "key1_2": "val1_2", "key1_3": "val1_3",64 "key1_1": "val1_1", "key1_2": "val1_2", "key1_3": "val1_3",
@@ -67,8 +72,28 @@
6772
68 def tearDown(self):73 def tearDown(self):
69 """tear down each test"""74 """tear down each test"""
75 this_context = self.get_test_context()
76 if this_context != test_environment.test_context:
77 stop_couchdb(ctx=this_context)
70 super(TestCouchDatabase, self).tearDown()78 super(TestCouchDatabase, self).tearDown()
71 del self.database._server[self.dbname]79
80 def get_test_context(self):
81 return test_environment.test_context
82
83 def maybe_die(self):
84 pass
85
86 def wait_until_server_dead(self, pid=None):
87 if pid is not None:
88 pid = find_pid(start_if_not_running=False, ctx=self.get_test_context())
89 if pid is None:
90 return
91 while True:
92 try:
93 os.kill(pid, 0) # Wait until exited
94 time.sleep(0.1)
95 except OSError:
96 break
7297
73 def test_database_not_exists(self):98 def test_database_not_exists(self):
74 self.assertRaises(99 self.assertRaises(
@@ -78,12 +103,14 @@
78 """Test getting mutliple records by type"""103 """Test getting mutliple records by type"""
79 records = self.database.get_records(104 records = self.database.get_records(
80 record_type="test.com",create_view=True)105 record_type="test.com",create_view=True)
81 self.assertEqual(3,len(records))106 self.maybe_die() # should be able to survive couchdb death
107 self.assertEqual(3, len(records))
82108
83 def test_get_record(self):109 def test_get_record(self):
84 """Test getting a record."""110 """Test getting a record."""
85 record = Record({'record_number': 0}, record_type="http://example.com/")111 record = Record({'record_number': 0}, record_type="http://example.com/")
86 record_id = self.database.put_record(record)112 record_id = self.database.put_record(record)
113 self.maybe_die() # should be able to survive couchdb death
87 retrieved_record = self.database.get_record(record_id)114 retrieved_record = self.database.get_record(record_id)
88 self.assertEqual(0, retrieved_record['record_number'])115 self.assertEqual(0, retrieved_record['record_number'])
89116
@@ -91,7 +118,8 @@
91 """Test putting a record."""118 """Test putting a record."""
92 record = Record({'record_number': 0}, record_type="http://example.com/")119 record = Record({'record_number': 0}, record_type="http://example.com/")
93 record_id = self.database.put_record(record)120 record_id = self.database.put_record(record)
94 retrieved_record = self.database._server[self.dbname][record_id]121 self.maybe_die() # should be able to survive couchdb death
122 retrieved_record = self.database.get_record(record_id)
95 self.assertEqual(123 self.assertEqual(
96 record['record_number'], retrieved_record['record_number'])124 record['record_number'], retrieved_record['record_number'])
97125
@@ -121,7 +149,9 @@
121 record = Record({'record_number': 0}, record_type="http://example.com/")149 record = Record({'record_number': 0}, record_type="http://example.com/")
122 record_id = self.database.put_record(record)150 record_id = self.database.put_record(record)
123 self.database.delete_record(record_id)151 self.database.delete_record(record_id)
152 ###self.maybe_die() # should be able to survive couchdb death
124 deleted_record = self.database._server[self.dbname][record_id]153 deleted_record = self.database._server[self.dbname][record_id]
154 #deleted_record = self.database.get_record(record_id)
125 self.assert_(deleted_record['application_annotations']['Ubuntu One'][155 self.assert_(deleted_record['application_annotations']['Ubuntu One'][
126 'private_application_annotations']['deleted'])156 'private_application_annotations']['deleted'])
127157
@@ -154,6 +184,7 @@
154 record = Record({'record_number': 0}, record_type="http://example.com/")184 record = Record({'record_number': 0}, record_type="http://example.com/")
155 record_id = self.database.put_record(record)185 record_id = self.database.put_record(record)
156 self.database.delete_record(record_id)186 self.database.delete_record(record_id)
187 self.maybe_die() # should be able to survive couchdb death
157 retrieved_record = self.database.get_record(record_id)188 retrieved_record = self.database.get_record(record_id)
158 self.assertEqual(None, retrieved_record)189 self.assertEqual(None, retrieved_record)
159190
@@ -162,6 +193,7 @@
162 record = Record({'record_number': 0}, record_type="http://example.com/")193 record = Record({'record_number': 0}, record_type="http://example.com/")
163 self.assert_(not self.database.record_exists("ThisMustNotExist"))194 self.assert_(not self.database.record_exists("ThisMustNotExist"))
164 record_id = self.database.put_record(record)195 record_id = self.database.put_record(record)
196 self.maybe_die() # should be able to survive couchdb death
165 self.assert_(self.database.record_exists(record_id))197 self.assert_(self.database.record_exists(record_id))
166 self.database.delete_record(record_id)198 self.database.delete_record(record_id)
167 self.assert_(not self.database.record_exists(record_id))199 self.assert_(not self.database.record_exists(record_id))
@@ -171,12 +203,14 @@
171 dictionary = {'record_number': 0, 'field1': 1, 'field2': 2}203 dictionary = {'record_number': 0, 'field1': 1, 'field2': 2}
172 record = Record(dictionary, record_type="http://example.com/")204 record = Record(dictionary, record_type="http://example.com/")
173 record_id = self.database.put_record(record)205 record_id = self.database.put_record(record)
206 self.maybe_die() # should be able to survive couchdb death
174 # manipulate the database 'out of view'207 # manipulate the database 'out of view'
175 non_working_copy = self.database.get_record(record_id)208 non_working_copy = self.database.get_record(record_id)
176 non_working_copy['field2'] = 22209 non_working_copy['field2'] = 22
177 non_working_copy['field3'] = 3210 non_working_copy['field3'] = 3
178 self.database.put_record(non_working_copy)211 self.database.put_record(non_working_copy)
179 self.database.update_fields(record_id, {'field1': 11})212 self.database.update_fields(record_id, {'field1': 11})
213 self.maybe_die() # should be able to survive couchdb death
180 working_copy = self.database.get_record(record_id)214 working_copy = self.database.get_record(record_id)
181 self.assertEqual(0, working_copy['record_number'])215 self.assertEqual(0, working_copy['record_number'])
182 self.assertEqual(11, working_copy['field1'])216 self.assertEqual(11, working_copy['field1'])
@@ -200,10 +234,12 @@
200 self.assertRaises(234 self.assertRaises(
201 KeyError, self.database.delete_view, view2_name, design_doc)235 KeyError, self.database.delete_view, view2_name, design_doc)
202 self.database.add_view(view1_name, map_js, reduce_js, design_doc)236 self.database.add_view(view1_name, map_js, reduce_js, design_doc)
237 self.maybe_die() # should be able to survive couchdb death
203 self.database.add_view(view2_name, map_js, reduce_js, design_doc)238 self.database.add_view(view2_name, map_js, reduce_js, design_doc)
204 self.database.delete_view(view1_name, design_doc)239 self.database.delete_view(view1_name, design_doc)
205 self.assertRaises(240 self.assertRaises(
206 KeyError, self.database.delete_view, view1_name, design_doc)241 KeyError, self.database.delete_view, view1_name, design_doc)
242 self.maybe_die() # should be able to survive couchdb death
207 self.database.delete_view(view2_name, design_doc)243 self.database.delete_view(view2_name, design_doc)
208 self.assertRaises(244 self.assertRaises(
209 KeyError, self.database.delete_view, view2_name, design_doc)245 KeyError, self.database.delete_view, view2_name, design_doc)
@@ -237,6 +273,7 @@
237 record_ids_we_care_about.remove(row.id)273 record_ids_we_care_about.remove(row.id)
238 self.assertFalse(row_is_deleted(row))274 self.assertFalse(row_is_deleted(row))
239275
276 self.maybe_die() # should be able to survive couchdb death
240 self.assertTrue(len(record_ids_we_care_about) == 0, "expected zero")277 self.assertTrue(len(record_ids_we_care_about) == 0, "expected zero")
241278
242 self.assertRaises(KeyError, self.database.get_records,279 self.assertRaises(KeyError, self.database.get_records,
@@ -250,6 +287,7 @@
250 map_js = """function(doc) { emit(doc._id, null) }"""287 map_js = """function(doc) { emit(doc._id, null) }"""
251 self.database.add_view(view_name, map_js, None, design_doc)288 self.database.add_view(view_name, map_js, None, design_doc)
252289
290 self.maybe_die() # should be able to survive couchdb death
253 self.assertEqual(self.database.list_views(design_doc), [view_name])291 self.assertEqual(self.database.list_views(design_doc), [view_name])
254 self.database.delete_view(view_name, design_doc)292 self.database.delete_view(view_name, design_doc)
255293
@@ -257,11 +295,13 @@
257295
258 def test_get_view_by_type_new_but_already(self):296 def test_get_view_by_type_new_but_already(self):
259 self.database.get_records(create_view=True)297 self.database.get_records(create_view=True)
298 self.maybe_die() # should be able to survive couchdb death
260 self.database.get_records(create_view=True)299 self.database.get_records(create_view=True)
261 # No exceptions on second run? Yay.300 # No exceptions on second run? Yay.
262301
263 def test_get_view_by_type_createxcl_fail(self):302 def test_get_view_by_type_createxcl_fail(self):
264 self.database.get_records(create_view=True)303 self.database.get_records(create_view=True)
304 self.maybe_die() # should be able to survive couchdb death
265 self.assertRaises(KeyError, self.database.get_records, create_view=None)305 self.assertRaises(KeyError, self.database.get_records, create_view=None)
266306
267 def test_get_changes(self):307 def test_get_changes(self):
@@ -321,6 +361,8 @@
321 # Ensure time is same.361 # Ensure time is same.
322 self.assertEqual(saved_time, self.database._changes_last_used)362 self.assertEqual(saved_time, self.database._changes_last_used)
323363
364 ###self.maybe_die() # should be able to survive couchdb death
365
324 # Next time we run, we get the same event again.366 # Next time we run, we get the same event again.
325 # Consume queued changes.367 # Consume queued changes.
326 count = self.database.report_changes(rep)368 count = self.database.report_changes(rep)
@@ -370,6 +412,7 @@
370 constructed_record.attach(content, "nu/mbe/rs", "text/plain")412 constructed_record.attach(content, "nu/mbe/rs", "text/plain")
371 constructed_record.attach("string", "another document", "text/plain")413 constructed_record.attach("string", "another document", "text/plain")
372414
415 ###self.maybe_die() # should be able to survive couchdb death
373 constructed_record.attach("XXXXXXXXX", "never used", "text/plain")416 constructed_record.attach("XXXXXXXXX", "never used", "text/plain")
374 constructed_record.detach("never used") # detach works before commit.417 constructed_record.detach("never used") # detach works before commit.
375418
@@ -382,6 +425,7 @@
382 self.assertRaises(KeyError, constructed_record.attach, content,425 self.assertRaises(KeyError, constructed_record.attach, content,
383 "another document", "text/x-rst")426 "another document", "text/x-rst")
384427
428 ###self.maybe_die() # should be able to survive couchdb death
385 record_id = self.database.put_record(constructed_record)429 record_id = self.database.put_record(constructed_record)
386 retrieved_record = self.database.get_record(record_id)430 retrieved_record = self.database.get_record(record_id)
387431
@@ -402,6 +446,7 @@
402 # get new446 # get new
403 retrieved_record = self.database.get_record(record_id)447 retrieved_record = self.database.get_record(record_id)
404448
449 ###self.maybe_die() # should be able to survive couchdb death
405 # We can get a list of attachments.450 # We can get a list of attachments.
406 self.assertEqual(set(retrieved_record.list_attachments()),451 self.assertEqual(set(retrieved_record.list_attachments()),
407 set(["nu/mbe/rs", "Document"]))452 set(["nu/mbe/rs", "Document"]))
@@ -424,6 +469,7 @@
424 self.assertEqual(out_data, content.getvalue())469 self.assertEqual(out_data, content.getvalue())
425 self.assertEqual(out_content_type, "text/plain")470 self.assertEqual(out_content_type, "text/plain")
426471
472 ###self.maybe_die() # should be able to survive couchdb death
427 # Asking for a named document that does not exist causes KeyError.473 # Asking for a named document that does not exist causes KeyError.
428 self.assertRaises(KeyError, retrieved_record.attachment_data,474 self.assertRaises(KeyError, retrieved_record.attachment_data,
429 "NoExist")475 "NoExist")
@@ -454,6 +500,7 @@
454 # ordinary requests are in key order500 # ordinary requests are in key order
455 self.assertEqual(data, sorted(data))501 self.assertEqual(data, sorted(data))
456502
503 self.maybe_die() # should be able to survive couchdb death
457 # now request descending order and confirm that it *is* descending504 # now request descending order and confirm that it *is* descending
458 descdata = [i.key for i in505 descdata = [i.key for i in
459 list(self.database.execute_view(view1_name, design_doc,506 list(self.database.execute_view(view1_name, design_doc,
@@ -513,3 +560,38 @@
513 self.fail()560 self.fail()
514 except FieldsConflict, e:561 except FieldsConflict, e:
515 self.assertEqual({('field1',): (22, 11)}, e.conflicts)562 self.assertEqual({('field1',): (22, 11)}, e.conflicts)
563
564
565class TestServerDiesSegv(TestCouchDatabase):
566 def get_test_context(self):
567 try:
568 return self.ctx
569 except AttributeError:
570 self.ctx = test_environment.create_new_test_environment()
571 return self.ctx
572
573 def maybe_die(self):
574 self.database.ensure_full_commit()
575 time.sleep(2)
576 pid = find_pid(start_if_not_running=False, ctx=self.get_test_context())
577 if pid is None:
578 print "couchdb has already quit! That's unexpected."
579 return
580 print "DIE, process", pid, "!"
581 os.kill(pid, signal.SIGSEGV)
582 self.wait_until_server_dead(pid=pid)
583
584
585class TestServerDiesNormal(TestCouchDatabase):
586 def get_test_context(self):
587 try:
588 return self.ctx
589 except AttributeError:
590 self.ctx = test_environment.create_new_test_environment()
591 return self.ctx
592
593 def maybe_die(self):
594 self.database.ensure_full_commit()
595 time.sleep(2)
596 stop_couchdb(ctx=self.get_test_context())
597 self.wait_until_server_dead()
516598
=== modified file 'desktopcouch/start_local_couchdb.py'
--- desktopcouch/start_local_couchdb.py 2010-09-17 15:24:19 +0000
+++ desktopcouch/start_local_couchdb.py 2010-09-22 14:14:44 +0000
@@ -47,6 +47,8 @@
47import sys47import sys
48import time48import time
4949
50from gnomekeyring import NoKeyringDaemonError
51
50import desktopcouch52import desktopcouch
51from desktopcouch import local_files, read_pidfile, process_is_couchdb53from desktopcouch import local_files, read_pidfile, process_is_couchdb
52from desktopcouch.records.server import CouchDatabase54from desktopcouch.records.server import CouchDatabase
@@ -216,7 +218,12 @@
216 logging.info("Ubuntu SSO dbus service could not be used.")218 logging.info("Ubuntu SSO dbus service could not be used.")
217219
218 if sso: 220 if sso:
219 credentials = sso.find_credentials('Ubuntu One')221 try:
222 credentials = sso.find_credentials('Ubuntu One')
223 except NoKeyringDaemonError:
224 logging.info(
225 "NoKeyringDaemonError exception raised.")
226 credentials = None
220 if credentials and len(credentials) > 0:227 if credentials and len(credentials) > 0:
221 pair_with_ubuntuone()228 pair_with_ubuntuone()
222 229
223230
=== modified file 'desktopcouch/stop_local_couchdb.py'
--- desktopcouch/stop_local_couchdb.py 2010-02-04 22:31:41 +0000
+++ desktopcouch/stop_local_couchdb.py 2010-09-22 14:14:44 +0000
@@ -25,7 +25,7 @@
25from desktopcouch import local_files25from desktopcouch import local_files
2626
27def stop_couchdb(ctx=local_files.DEFAULT_CONTEXT):27def stop_couchdb(ctx=local_files.DEFAULT_CONTEXT):
28 local_exec = ctx.couch_exec_command + ["-k"]28 local_exec = ctx.couch_exec_command + ["-d"]
29 try:29 try:
30 retcode = subprocess.call(local_exec, shell=False)30 retcode = subprocess.call(local_exec, shell=False)
31 if retcode < 0:31 if retcode < 0:

Subscribers

People subscribed via source and target branches