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 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
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() )
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", }
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
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