def _configure(self, host, port, name): self._cancel_reconnector() self.host, self.port = host, port self.paisley = CouchDB(host, port) self.db_name = name self.notifier = ChangeNotifier(self.paisley, self.db_name) self.notifier.addListener(self) # ping database to figure trigger changing state to connected d = self._paisley_call(self.paisley.listDB) d.addErrback(failure.Failure.trap, NotConnectedError)
class Notifier(object): def __init__(self, db, filter_): self._db = db self._filter = filter_ self.name = self._filter.name self._params = None self.reconfigure() def reconfigure(self): # called after changing the database self._changes = ChangeNotifier(self._db.paisley, self._db.db_name) self._changes.addListener(self) def setup(self): new_params = self._filter.extract_params() if self._params is not None and \ new_params == self._params and \ self._changes.isRunning(): return defer.succeed(None) self._params = new_params if self._changes.isRunning(): self._changes.stop() d = defer.succeed(None) if new_params is not None: d.addCallback(defer.drop_param, self._db.wait_connected) d.addCallback(defer.drop_param, self._changes.start, heartbeat=1000, **new_params) else: self._db.log("Stopping notifier: %r", self.name) d.addErrback(self.connectionLost) d.addErrback(failure.Failure.trap, NotConnectedError) return d ### paisleys ChangeListener interface def changed(self, change): # The change parameter is just an ugly effect of json unserialization # of the couchdb output. It can be many different things, hence the # strange logic above. if "changes" in change: doc_id = change['id'] deleted = change.get('deleted', False) for line in change['changes']: # The changes are analized when there is not http request # pending. Otherwise it can result in race condition problem. self._db.process_notifications( self._filter, doc_id, line['rev'], deleted) else: self.info('Bizare notification received from CouchDB: %r', change) def connectionLost(self, reason): self._db.connectionLost(reason)
class Database(common.ConnectionManager, log.LogProxy, ChangeListener): implements(IDbConnectionFactory, IDatabaseDriver) log_category = "database" def __init__(self, host, port, db_name): common.ConnectionManager.__init__(self) log.LogProxy.__init__(self, log.FluLogKeeper()) ChangeListener.__init__(self, self) self.semaphore = defer.DeferredSemaphore(1) self.paisley = None self.db_name = None self.host = None self.port = None self.notifier = None self.retry = 0 self.reconnector = None self._configure(host, port, db_name) def reconfigure(self, host, port, name): if self.notifier.isRunning(): self.notifier.stop() self._configure(host, port, name) self._setup_notifier() def show_status(self): eta = self.reconnector and self.reconnector.active() and \ time.left(self.reconnector.getTime()) return "Database", self.is_connected(), self.host, self.port, eta ### IDbConnectionFactory def get_connection(self): return Connection(self) ### IDatabaseDriver def open_doc(self, doc_id): return self._paisley_call(self.paisley.openDoc, self.db_name, doc_id) def save_doc(self, doc, doc_id=None): return self._paisley_call(self.paisley.saveDoc, self.db_name, doc, doc_id) def delete_doc(self, doc_id, revision): return self._paisley_call(self.paisley.deleteDoc, self.db_name, doc_id, revision) def create_db(self): return self._paisley_call(self.paisley.createDB, self.db_name) def listen_changes(self, doc_ids, callback): d = ChangeListener.listen_changes(self, doc_ids, callback) d.addCallback(defer.bridge_param, self._setup_notifier) return d def cancel_listener(self, listener_id): ChangeListener.cancel_listener(self, listener_id) return self._setup_notifier() def query_view(self, factory, **options): factory = IViewFactory(factory) d = self._paisley_call(self.paisley.openView, self.db_name, DESIGN_DOC_ID, factory.name, **options) d.addCallback(self._parse_view_result) return d ### paisleys ChangeListener interface def changed(self, change): # The change parameter is just an ugly effect of json unserialization # of the couchdb output. It can be many different things, hence the # strange logic above. if "changes" in change: doc_id = change['id'] for line in change['changes']: # The changes are analized when there is not http request # pending. Otherwise it can result in race condition problem. self.semaphore.run(self._trigger_change, doc_id, line['rev']) else: self.info('Bizare notification received from CouchDB: %r', change) def connectionLost(self, reason): if reason.check(error.ConnectionDone): # expected just pass return elif reason.check(ResponseDone): self.debug("CouchDB closed the notification listener. This might " "indicate missconfiguration. Take look at it") return elif reason.check(error.ConnectionRefusedError): self.retry += 1 wait = min(2**(self.retry - 1), 300) self.debug('CouchDB refused connection for %d time. ' 'This indicates missconfiguration or temporary ' 'network problem. Will try to reconnect in %d seconds.', self.retry, wait) self.reconnector = time.callLater(wait, self._setup_notifier) self._on_disconnected() return else: # FIXME handle disconnection when network is down self._on_disconnected() self.warning('Connection to db lost with reason: %r', reason) return self._setup_notifier() ### private def _configure(self, host, port, name): self._cancel_reconnector() self.host, self.port = host, port self.paisley = CouchDB(host, port) self.db_name = name self.notifier = ChangeNotifier(self.paisley, self.db_name) self.notifier.addListener(self) # ping database to figure trigger changing state to connected d = self._paisley_call(self.paisley.listDB) d.addErrback(failure.Failure.trap, NotConnectedError) def _parse_view_result(self, resp): assert "rows" in resp for row in resp["rows"]: yield row["key"], row["value"] def _setup_notifier(self): doc_ids = self._extract_doc_ids() self.log('Setting up the notifier passing. Doc_ids: %r.', doc_ids) if self.notifier.isRunning(): self.notifier.stop() if len(doc_ids) == 0: # Don't run listner if it is not needed, # cancel reconnector if one is running. if self.reconnector and self.reconnector.active(): self.reconnector.cancel() self.reconnector = None return d = self.notifier.start( heartbeat=1000) d.addCallback(self._connected) d.addErrback(self.connectionLost) d.addErrback(failure.Failure.trap, NotConnectedError) return d def _connected(self, _): self.debug('Established persistent connection for receiving ' 'notifications.') self._on_connected() self._cancel_reconnector() def _cancel_reconnector(self): if self.reconnector: if self.reconnector.active(): self.reconnector.cancel() self.reconnector = None self.retry = 0 def _paisley_call(self, method, *args, **kwargs): # It is necessarry to acquire the lock to perform the http request # because we need to be sure that we are not in the middle of sth # while analizing the change notification d = self.semaphore.run(method, *args, **kwargs) d.addCallback(defer.bridge_param, self._on_connected) d.addErrback(self._error_handler) return d def _error_handler(self, failure): exception = failure.value msg = failure.getErrorMessage() if isinstance(exception, web_error.Error): status = int(exception.status) if status == 409: raise ConflictError(msg) elif status == 404: raise NotFoundError(msg) else: self.info(exception.response) raise NotImplementedError( 'Behaviour for response code %d not define yet, FIXME!' % status) elif failure.check(error.ConnectionRefusedError): self._on_disconnected() raise NotConnectedError("Database connection refused.") else: failure.raiseException()
def reconfigure(self): # called after changing the database self._changes = ChangeNotifier(self._db.paisley, self._db.db_name) self._changes.addListener(self)