Exemplo n.º 1
0
    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)
Exemplo n.º 2
0
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)
Exemplo n.º 3
0
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()
Exemplo n.º 4
0
 def reconfigure(self):
     # called after changing the database
     self._changes = ChangeNotifier(self._db.paisley, self._db.db_name)
     self._changes.addListener(self)