Beispiel #1
0
 def setUp(self):
     db_h, self.db_path = tempfile.mkstemp(suffix=".db")
     os.close(db_h)
     self.db = DbRetry(self.db_path, 'tmp_table')
     # le initdb est déjà fait en __init__ mais ça permet de s'assurer
     # qu'on est bien initialisés
     d = self.db.initdb()
     return d
Beispiel #2
0
    def test_flush_double(self):
        """
        Un double flush doit être mis en file d'attente
        """
        db = DbRetry(self.db_path, 'tmp_table')
        db._flush = Mock()
        self.assertTrue(db._is_flushing_d is None)

        d = db.flush()
        self.assertTrue(db._is_flushing_d is not None)
        def check(r):
            self.assertTrue(db._flush.called)
            self.assertEqual(len(db._flush.call_args_list), 2)
            # le 2e appel a été déclenché avec le 1er
            self.assertTrue(d2.called)
        d2 = db.flush()
        d.addCallback(check)
        return d
Beispiel #3
0
    def test_vacuum(self):
        """
        Teste le nettoyage de la base
        """
        db = DbRetry(self.db_path, 'tmp_table')
        stub = ConnectionPoolStub(db._db)
        db._db = stub

        xml = '<abc foo="bar">def</abc>'
        yield db.put(xml)
        yield db.flush()

        # On récupère 2 fois un élément: une fois pour vider la base, et la
        # seconde fois déclenche un VACUUM
        yield db.get()
        yield db.get()

        # On attend un peu, le VACUUM est décalé
        yield wait(1)
        print stub.requests
        self.assertEqual( (("VACUUM", ), {}), stub.requests.pop() )
Beispiel #4
0
 def __init__(self, dbfilename=None, dbtable=None, max_queue_size=None):
     self.producer = None
     self.consumer = None
     self.paused = True
     # File d'attente mémoire
     self.max_queue_size = max_queue_size
     self.queue = None
     self._build_queue()
     self._processing_queue = False
     # Base de backup
     if dbfilename is None or dbtable is None:
         self.retry = None
     else:
         self.retry = DbRetry(dbfilename, dbtable)
     # Stats
     self.stat_names = {
             "queue": "queue",
             "backup_in_buf": "backup_in_buf",
             "backup_out_buf": "backup_out_buf",
             "backup": "backup",
             }
Beispiel #5
0
class TestDbRetry(unittest.TestCase):
    """
    Teste la classe DbRetry.
    """

    @deferred(timeout=30)
    def setUp(self):
        db_h, self.db_path = tempfile.mkstemp(suffix=".db")
        os.close(db_h)
        self.db = DbRetry(self.db_path, 'tmp_table')
        # le initdb est déjà fait en __init__ mais ça permet de s'assurer
        # qu'on est bien initialisés
        d = self.db.initdb()
        return d

    def tearDown(self):
        del self.db
        os.remove(self.db_path)


    @deferred(timeout=30)
    def test_retrieval(self):
        """
        Teste l'enregistrement et la récupération d'un message avec DbRetry.
        """
        xmls = [
            u'<abc foo="bar">def</abc>',
            u'<root />',
            u'<toto><tutu/><titi><tata/></titi></toto>',
        ]

        # On stocke un certain nombre de messages.
        puts = []
        for xml in xmls:
            d = self.db.put(xml)
            puts.append(d)
        main_d = defer.DeferredList(puts)

        # On vérifie qu'on peut récupérer les messages stockés
        # et qu'ils nous sont transmis dans le même ordre que
        # celui dans lequel on les a stocké, comme une FIFO.
        def try_get(r, xml):
            d = self.db.get()
            d.addCallback(self.assertEquals, xml)
            return d
        for xml in xmls:
            main_d.addCallback(try_get, xml)

        # Arrivé ici, la base doit être vide, donc unstore()
        # renvoie None pour indiquer la fin des messages.
        def try_final_get(r):
            d = self.db.get()
            d.addCallback(self.assertEquals, None)
            return d
        main_d.addCallback(try_final_get)

        return main_d

    @deferred(timeout=30)
    @defer.inlineCallbacks
    def test_put_buffer(self):
        """
        Teste le buffer d'entrée
        """
        xml = '<abc foo="bar">def</abc>'
        yield self.db.put(xml)
        self.assertEqual(len(self.db.buffer_in), 1)
        for _i in range(self.db._buffer_in_max):
            yield self.db.put(xml)
        self.assertEqual(len(self.db.buffer_in), 0)
        backup_size = yield self.db.qsize()
        self.assertEqual(backup_size, self.db._buffer_in_max + 1)

    @deferred(timeout=30)
    @defer.inlineCallbacks
    def test_get_buffer(self):
        """
        Teste le buffer de sortie
        """
        xml = '<abc foo="bar">def</abc>'
        msg_count = (self.db._buffer_in_max + 1) * 2
        for _i in range(msg_count):
            yield self.db.put(xml)
        self.assertEqual(len(self.db.buffer_out), 0)
        yield self.db.get()
        self.assertEqual(len(self.db.buffer_out), msg_count - 1) # il y a un message en moins, c'est 'got_xml'
        backup_size = yield self.db.qsize()
        self.assertEqual(backup_size, len(self.db.buffer_out))

    @deferred(timeout=30)
    @defer.inlineCallbacks
    def test_qsize(self):
        """
        Teste le buffer de sortie
        """
        xml = '<abc foo="bar">def</abc>'
        msg_count = (self.db._buffer_in_max + 1)
        for _i in range(msg_count):
            self.db.buffer_in.append(xml)
            self.db.buffer_out.append((None, xml))
        yield self.db.flush()
        self.assertEqual(len(self.db.buffer_in), 0)
        self.assertEqual(len(self.db.buffer_out), 0)
        backup_size = yield self.db.qsize()
        self.assertEqual(backup_size, msg_count * 2)

    @deferred(timeout=30)
    @defer.inlineCallbacks
    def test_vacuum(self):
        """
        Teste le nettoyage de la base
        """
        db = DbRetry(self.db_path, 'tmp_table')
        stub = ConnectionPoolStub(db._db)
        db._db = stub

        xml = '<abc foo="bar">def</abc>'
        yield db.put(xml)
        yield db.flush()

        # On récupère 2 fois un élément: une fois pour vider la base, et la
        # seconde fois déclenche un VACUUM
        yield db.get()
        yield db.get()

        # On attend un peu, le VACUUM est décalé
        yield wait(1)
        print stub.requests
        self.assertEqual( (("VACUUM", ), {}), stub.requests.pop() )

    @deferred(timeout=30)
    def test_flush_double(self):
        """
        Un double flush doit être mis en file d'attente
        """
        db = DbRetry(self.db_path, 'tmp_table')
        db._flush = Mock()
        self.assertTrue(db._is_flushing_d is None)

        d = db.flush()
        self.assertTrue(db._is_flushing_d is not None)
        def check(r):
            self.assertTrue(db._flush.called)
            self.assertEqual(len(db._flush.call_args_list), 2)
            # le 2e appel a été déclenché avec le 1er
            self.assertTrue(d2.called)
        d2 = db.flush()
        d.addCallback(check)
        return d
Beispiel #6
0
class BackupProvider(Service):
    """
    Ajoute à un PushProducer la possibilité d'être mis en pause. Les données
    vont alors dans une file d'attente mémoire qui est sauvegardée sur le
    disque dans une base.

    @ivar producer: source de messages
    @ivar consumer: destination des messages
    @ivar paused: etat de la production
    @type paused: C{bool}
    @ivar queue: file d'attente mémoire
    @type queue: C{deque}
    @ivar max_queue_size: taille maximum de la file d'attente mémoire
    @type max_queue_size: C{int}
    @ivar retry: base de données de stockage
    @type retry: L{DbRetry}
    @ivar stat_names: nom des données de performances produites à destination de
        Vigilo
    @type stat_names: C{dict}
    """

    implements(IPushProducer, IConsumer)


    def __init__(self, dbfilename=None, dbtable=None, max_queue_size=None):
        self.producer = None
        self.consumer = None
        self.paused = True
        # File d'attente mémoire
        self.max_queue_size = max_queue_size
        self.queue = None
        self._build_queue()
        self._processing_queue = False
        # Base de backup
        if dbfilename is None or dbtable is None:
            self.retry = None
        else:
            self.retry = DbRetry(dbfilename, dbtable)
        # Stats
        self.stat_names = {
                "queue": "queue",
                "backup_in_buf": "backup_in_buf",
                "backup_out_buf": "backup_out_buf",
                "backup": "backup",
                }


    def _build_queue(self):
        if self.max_queue_size is not None:
            self.queue = deque(maxlen=self.max_queue_size)
        else:
            # sur python < 2.6, il n'y a pas de maxlen
            self.queue = deque()


    def startService(self):
        """Executé au démarrage du connecteur"""
        d = self.retry.initdb()
        if self.producer is not None:
            d.addCallback(lambda _x: self.producer.startService())
        return d


    def stopService(self):
        """Executé à l'arrêt du connecteur"""
        if self.producer is not None:
            d = self.producer.stopService()
        else:
            d = defer.succeed(None)
        d.addCallback(lambda _x: self._saveToDb())
        d.addCallback(lambda _x: self.retry.flush())
        return d


    def registerProducer(self, producer, streaming):
        """
        Enregistre le producteur des messages, qui doit être un PushProducer.
        Si c'était un PullProducer, cette classe ne serait pas nécessaire,
        puisqu'il suffirait de ne pas appeler C{resumeProducing}.
        """
        assert streaming == True # Ça n'a pas de sens avec un PullProducer
        self.producer = producer
        self.producer.consumer = self

    def unregisterProducer(self):
        """Supprime le producteur"""
        #self.producer.pauseProducing() # A priori il sait pas faire
        self.producer = None


    def getStats(self):
        """Récupère des métriques de fonctionnement"""
        stats = {
            self.stat_names["queue"]: len(self.queue),
            self.stat_names["backup_in_buf"]: len(self.retry.buffer_in),
            self.stat_names["backup_out_buf"]: len(self.retry.buffer_out),
            }
        backup_size_d = self.retry.qsize()
        def add_backup_size(backup_size):
            stats[ self.stat_names["backup"] ] = backup_size
            return stats
        backup_size_d.addCallbacks(add_backup_size,
                                   lambda e: add_backup_size("U"))
        return backup_size_d


    def write(self, data):
        """Méthode appelée par le producteur pour transférer un message."""
        self.queue.append(data)
        self.processQueue()


    def pauseProducing(self):
        """
        Met en pause la production vers le consommateur. Les messages en entrée
        seront stockés en base de données à partir de maintenant.
        """
        self.paused = True
        d = self._saveToDb()
        d.addCallback(lambda _x: self.retry.flush())
        return d


    def resumeProducing(self):
        """
        Débute ou reprend la production vers le consommateur. Les messages
        seront pris en priorité depuis le backup.
        """
        self.paused = False
        if self.retry.initialized.called:
            d = self.processQueue()
        else:
            d = self.retry.initialized
            d.addCallback(lambda _x: self.processQueue())
        return d


    def stopProducing(self):
        pass


    @defer.inlineCallbacks
    def processQueue(self):
        """
        Traite les messages en attente, en donnant la priorité à la base de
        backup (L{retry}).
        """
        if self._processing_queue:
            return
        if self.paused:
            yield self._saveToDb()
            return

        self._processing_queue = True
        while True:
            msg = yield self._getNextMsg()
            if msg is None:
                break
            try:
                yield self.consumer.write(msg)
            except Exception, e: # pylint: disable-msg=W0703
                # W0703: Catch "Exception"
                self._send_failed(e, msg)
        self._processing_queue = False