summaryrefslogtreecommitdiff
path: root/common/src/leap/soledad/common/couch.py
diff options
context:
space:
mode:
Diffstat (limited to 'common/src/leap/soledad/common/couch.py')
-rw-r--r--common/src/leap/soledad/common/couch.py119
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):