class TestWorkerPool: def setup_method(self, test_method): self.pool = WorkerPool(min_workers=3) def teardown_method(self, test_method): self.pool.stop(signal.SIGTERM) def test_worker(self): self.pool.init_workers(SimpleWorker().work_loop) assert len(self.pool) == 3 for worker in self.pool.workers: assert worker.messages_sent == 0 assert worker.alive is True def test_single_task(self): self.pool.init_workers(SimpleWorker().work_loop) self.pool.write(0, 'xyz') assert self.pool.workers[0].messages_sent == 1 # worker at index 0 handled one task assert self.pool.workers[1].messages_sent == 0 assert self.pool.workers[2].messages_sent == 0 def test_queue_preference(self): self.pool.init_workers(SimpleWorker().work_loop) self.pool.write(2, 'xyz') assert self.pool.workers[0].messages_sent == 0 assert self.pool.workers[1].messages_sent == 0 assert self.pool.workers[2].messages_sent == 1 # worker at index 2 handled one task def test_worker_processing(self): result_queue = multiprocessing.Queue() self.pool.init_workers(ResultWriter().work_loop, result_queue) for i in range(10): self.pool.write( random.choice(range(len(self.pool))), 'Hello, Worker {}'.format(i) ) all_messages = [result_queue.get(timeout=1) for i in range(10)] all_messages.sort() assert all_messages == [ 'Hello, Worker {}!!!'.format(i) for i in range(10) ] total_handled = sum([worker.messages_sent for worker in self.pool.workers]) assert total_handled == 10
class AWXConsumerBase(object): last_stats = time.time() def __init__(self, name, worker, queues=[], pool=None): self.should_stop = False self.name = name self.total_messages = 0 self.queues = queues self.worker = worker self.pool = pool if pool is None: self.pool = WorkerPool() self.pool.init_workers(self.worker.work_loop) self.redis = redis.Redis.from_url(settings.BROKER_URL) @property def listening_on(self): return f'listening on {self.queues}' def control(self, body): logger.warning(f'Received control signal:\n{body}') control = body.get('control') if control in ('status', 'running'): reply_queue = body['reply_to'] if control == 'status': msg = '\n'.join([self.listening_on, self.pool.debug()]) elif control == 'running': msg = [] for worker in self.pool.workers: worker.calculate_managed_tasks() msg.extend(worker.managed_tasks.keys()) with pg_bus_conn() as conn: conn.notify(reply_queue, json.dumps(msg)) elif control == 'reload': for worker in self.pool.workers: worker.quit() else: logger.error('unrecognized control message: {}'.format(control)) def process_task(self, body): if 'control' in body: try: return self.control(body) except Exception: logger.exception(f"Exception handling control message: {body}") return if len(self.pool): if "uuid" in body and body['uuid']: try: queue = UUID(body['uuid']).int % len(self.pool) except Exception: queue = self.total_messages % len(self.pool) else: queue = self.total_messages % len(self.pool) else: queue = 0 self.pool.write(queue, body) self.total_messages += 1 self.record_statistics() def record_statistics(self): if time.time( ) - self.last_stats > 1: # buffer stat recording to once per second try: self.redis.set(f'awx_{self.name}_statistics', self.pool.debug()) self.last_stats = time.time() except Exception: logger.exception( f"encountered an error communicating with redis to store {self.name} statistics" ) self.last_stats = time.time() def run(self, *args, **kwargs): signal.signal(signal.SIGINT, self.stop) signal.signal(signal.SIGTERM, self.stop) # Child should implement other things here def stop(self, signum, frame): self.should_stop = True logger.warning('received {}, stopping'.format(signame(signum))) self.worker.on_stop() raise SystemExit()
class AWXConsumer(ConsumerMixin): def __init__(self, name, connection, worker, queues=[], pool=None): self.connection = connection self.total_messages = 0 self.queues = queues self.worker = worker self.pool = pool if pool is None: self.pool = WorkerPool() self.pool.init_workers(self.worker.work_loop) def get_consumers(self, Consumer, channel): logger.debug(self.listening_on) return [ Consumer(queues=self.queues, accept=['json'], callbacks=[self.process_task]) ] @property def listening_on(self): return 'listening on {}'.format( ['{} [{}]'.format(q.name, q.exchange.type) for q in self.queues]) def control(self, body, message): logger.warn('Consumer received control message {}'.format(body)) control = body.get('control') if control in ('status', 'running'): producer = Producer(channel=self.connection, routing_key=message.properties['reply_to']) if control == 'status': msg = '\n'.join([self.listening_on, self.pool.debug()]) elif control == 'running': msg = [] for worker in self.pool.workers: worker.calculate_managed_tasks() msg.extend(worker.managed_tasks.keys()) producer.publish(msg) elif control == 'reload': for worker in self.pool.workers: worker.quit() else: logger.error('unrecognized control message: {}'.format(control)) message.ack() def process_task(self, body, message): if 'control' in body: try: return self.control(body, message) except Exception: logger.exception("Exception handling control message:") return if len(self.pool): if "uuid" in body and body['uuid']: try: queue = UUID(body['uuid']).int % len(self.pool) except Exception: queue = self.total_messages % len(self.pool) else: queue = self.total_messages % len(self.pool) else: queue = 0 self.pool.write(queue, body) self.total_messages += 1 message.ack() def run(self, *args, **kwargs): signal.signal(signal.SIGINT, self.stop) signal.signal(signal.SIGTERM, self.stop) self.worker.on_start() super(AWXConsumer, self).run(*args, **kwargs) def stop(self, signum, frame): self.should_stop = True # this makes the kombu mixin stop consuming logger.warn('received {}, stopping'.format(signame(signum))) self.worker.on_stop() raise SystemExit()