diff options
Diffstat (limited to 'common/src/leap/soledad/common/couch.py')
-rw-r--r-- | common/src/leap/soledad/common/couch.py | 119 |
1 files changed, 74 insertions, 45 deletions
diff --git a/common/src/leap/soledad/common/couch.py b/common/src/leap/soledad/common/couch.py index 8e8613a1..0aa84170 100644 --- a/common/src/leap/soledad/common/couch.py +++ b/common/src/leap/soledad/common/couch.py @@ -31,14 +31,16 @@ import threading from StringIO import StringIO from collections import defaultdict +from urlparse import urljoin +from contextlib import contextmanager -from couchdb.client import Server +from couchdb.client import Server, Database from couchdb.http import ( ResourceConflict, ResourceNotFound, ServerError, - Session, + Session as CouchHTTPSession, ) from u1db import query_parser, vectorclock from u1db.errors import ( @@ -331,6 +333,35 @@ class MultipartWriter(object): self.headers[name] = value +class Session(CouchHTTPSession): + """ + An HTTP session that can be closed. + """ + + def close_connections(self): + for key, conns in list(self.conns.items()): + for conn in conns: + conn.close() + + +@contextmanager +def couch_server(url): + """ + Provide a connection to a couch server and cleanup after use. + + For database creation and deletion we use an ephemeral connection to the + couch server. That connection has to be properly closed, so we provide it + as a context manager. + + :param url: The URL of the Couch server. + :type url: str + """ + session = Session(timeout=COUCH_TIMEOUT) + server = Server(url=url, session=session) + yield server + session.close_connections() + + class CouchDatabase(CommonBackend): """ A U1DB implementation that uses CouchDB as its persistence layer. @@ -353,7 +384,7 @@ class CouchDatabase(CommonBackend): release_fun): """ :param db: The database from where to get the document. - :type db: u1db.Database + :type db: CouchDatabase :param doc_id: The doc_id of the document to be retrieved. :type doc_id: str :param check_for_conflicts: Whether the get_doc() method should @@ -380,7 +411,7 @@ class CouchDatabase(CommonBackend): self._release_fun() @classmethod - def open_database(cls, url, create, ensure_ddocs=False): + def open_database(cls, url, create, replica_uid=None, ensure_ddocs=False): """ Open a U1DB database using CouchDB as backend. @@ -388,6 +419,8 @@ class CouchDatabase(CommonBackend): :type url: str :param create: should the replica be created if it does not exist? :type create: bool + :param replica_uid: an optional unique replica identifier + :type replica_uid: str :param ensure_ddocs: Ensure that the design docs exist on server. :type ensure_ddocs: bool @@ -400,16 +433,16 @@ class CouchDatabase(CommonBackend): raise InvalidURLError url = m.group(1) dbname = m.group(2) - server = Server(url=url) - try: - server[dbname] - except ResourceNotFound: - if not create: - raise DatabaseDoesNotExist() - return cls(url, dbname, ensure_ddocs=ensure_ddocs) + with couch_server(url) as server: + try: + server[dbname] + except ResourceNotFound: + if not create: + raise DatabaseDoesNotExist() + server.create(dbname) + return cls(url, dbname, replica_uid=replica_uid, ensure_ddocs=ensure_ddocs) - def __init__(self, url, dbname, replica_uid=None, full_commit=True, - session=None, ensure_ddocs=True): + def __init__(self, url, dbname, replica_uid=None, ensure_ddocs=True): """ Create a new Couch data container. @@ -419,31 +452,19 @@ class CouchDatabase(CommonBackend): :type dbname: str :param replica_uid: an optional unique replica identifier :type replica_uid: str - :param full_commit: turn on the X-Couch-Full-Commit header - :type full_commit: bool - :param session: an http.Session instance or None for a default session - :type session: http.Session :param ensure_ddocs: Ensure that the design docs exist on server. :type ensure_ddocs: bool """ # save params self._url = url - self._full_commit = full_commit - if session is None: - session = Session(timeout=COUCH_TIMEOUT) - self._session = session + self._session = Session(timeout=COUCH_TIMEOUT) self._factory = CouchDocument self._real_replica_uid = None # configure couch - self._server = Server(url=self._url, - full_commit=self._full_commit, - session=self._session) self._dbname = dbname - try: - self._database = self._server[self._dbname] - except ResourceNotFound: - self._server.create(self._dbname) - self._database = self._server[self._dbname] + self._database = Database( + urljoin(self._url, self._dbname), + self._session) if replica_uid is not None: self._set_replica_uid(replica_uid) if ensure_ddocs: @@ -482,7 +503,9 @@ class CouchDatabase(CommonBackend): """ Delete a U1DB CouchDB database. """ - del(self._server[self._dbname]) + with couch_server(self._url) as server: + del(server[self._dbname]) + self.close_connections() def close(self): """ @@ -491,13 +514,26 @@ class CouchDatabase(CommonBackend): :return: True if db was succesfully closed. :rtype: bool """ + self.close_connections() self._url = None self._full_commit = None self._session = None - self._server = None self._database = None return True + def close_connections(self): + """ + Close all open connections to the couch server. + """ + if self._session is not None: + self._session.close_connections() + + def __del__(self): + """ + Close the database upon garbage collection. + """ + self.close() + def _set_replica_uid(self, replica_uid): """ Force the replica uid to be set. @@ -855,7 +891,9 @@ class CouchDatabase(CommonBackend): try: self._database.resource.put_json( doc.doc_id, body=buf.getvalue(), headers=envelope.headers) - self._renew_couch_session() + # What follows is a workaround for an ugly bug. See: + # https://leap.se/code/issues/5448 + self.close_connections() except ResourceConflict: raise RevisionConflict() @@ -1411,7 +1449,7 @@ class CouchDatabase(CommonBackend): # strptime here by evaluating the conversion of an arbitrary date. # This will not be needed when/if we switch from python-couchdb to # paisley. - time.strptime('Mar 4 1917', '%b %d %Y') + time.strptime('Mar 8 1917', '%b %d %Y') # spawn threads to retrieve docs threads = [] for doc_id in doc_ids: @@ -1427,15 +1465,6 @@ class CouchDatabase(CommonBackend): continue yield t._doc - def _renew_couch_session(self): - """ - Create a new couch connection session. - - This is a workaround for #5448. Will not be needed once bigcouch is - merged with couchdb. - """ - self._database.resource.session = Session(timeout=COUCH_TIMEOUT) - class CouchSyncTarget(CommonSyncTarget): """ @@ -1489,9 +1518,9 @@ class CouchServerState(ServerState): :return: The CouchDatabase object. :rtype: CouchDatabase """ - return CouchDatabase.open_database( - self._couch_url + '/' + dbname, - create=False, + return CouchDatabase( + self._couch_url, + dbname, ensure_ddocs=False) def ensure_database(self, dbname): |