def test_get_produced_message_routing_key_requeue(settings): settings.CQRS['queue'] = 'replica' payload = TransportPayload(SignalType.SAVE, 'CQRS_ID', {}, None) payload.is_requeue = True routing_key = PublicRabbitMQTransport.get_produced_message_routing_key( payload) assert routing_key == 'cqrs.replica.CQRS_ID'
def test_get_produced_message_routing_key_dead_letter(settings): settings.CQRS['replica']['dead_letter_queue'] = 'dead_letter_replica' payload = TransportPayload(SignalType.SYNC, 'CQRS_ID', {}, None) payload.is_dead_letter = True routing_key = PublicRabbitMQTransport.get_produced_message_routing_key( payload) assert routing_key == 'cqrs.dead_letter_replica.CQRS_ID'
def _consume_message(cls, body, message): try: dct = ujson.loads(body) for key in ('signal_type', 'cqrs_id', 'instance_data'): if key not in dct: raise ValueError if 'instance_pk' not in dct: logger.warning('CQRS deprecated package structure.') except ValueError: logger.error("CQRS couldn't be parsed: {}.".format(body)) message.reject() return payload = TransportPayload( dct['signal_type'], dct['cqrs_id'], dct['instance_data'], dct.get('instance_pk'), previous_data=dct.get('previous_data'), ) cls.log_consumed(payload) instance = consumer.consume(payload) if instance: message.ack() cls.log_consumed_accepted(payload) else: message.reject() cls.log_consumed_denied(payload)
def test_message_without_retry_dead_letter(settings, mocker, caplog): settings.CQRS['replica']['CQRS_MAX_RETRIES'] = 1 produce_message = mocker.patch( 'dj_cqrs.transport.rabbit_mq.RabbitMQTransport._produce_message', ) channel = mocker.MagicMock() payload = TransportPayload( SignalType.SAVE, 'basic', {'id': 1}, 1, correlation_id='abc', retries=2, ) delay_queue = DelayQueue() PublicRabbitMQTransport.fail_message(channel, 1, payload, None, delay_queue) assert delay_queue.qsize() == 0 assert channel.basic_nack.call_count == 1 assert produce_message.call_count == 1 produce_payload = produce_message.call_args[0][2] assert produce_payload is payload assert getattr(produce_message, 'is_dead_letter', False) assert 'CQRS is failed: pk = 1 (basic), correlation_id = abc, retries = 2.' in caplog.text assert ( 'CQRS is added to dead letter queue: pk = 1 (basic), correlation_id = abc' in caplog.text)
def test_produce_message_ok(mocker): channel = mocker.MagicMock() payload = TransportPayload( SignalType.SAVE, 'cqrs_id', {}, 'id', previous_data={'e': 'f'}, ) exchange = PublicKombuTransport.create_exchange('exchange') PublicKombuTransport.produce_message(channel, exchange, payload) assert channel.basic_publish.call_count == 1 prepare_message_args = channel.prepare_message.call_args[0] basic_publish_kwargs = channel.basic_publish.call_args[1] assert ujson.loads(prepare_message_args[0]) == \ { 'signal_type': SignalType.SAVE, 'cqrs_id': 'cqrs_id', 'instance_data': {}, 'instance_pk': 'id', 'previous_data': {'e': 'f'}, } assert prepare_message_args[2] == 'text/plain' assert prepare_message_args[5]['delivery_mode'] == 2 assert basic_publish_kwargs['exchange'] == 'exchange' assert basic_publish_kwargs['mandatory'] assert basic_publish_kwargs['routing_key'] == 'cqrs_id'
def _consume_message(cls, body, message): try: dct = ujson.loads(body) except ValueError: logger.error("CQRS couldn't be parsed: {0}.".format(body)) message.reject() return required_keys = {'instance_pk', 'signal_type', 'cqrs_id', 'instance_data'} for key in required_keys: if key not in dct: msg = "CQRS couldn't proceed, %s isn't found in body: %s." logger.error(msg, key, body) message.reject() return payload = TransportPayload( dct['signal_type'], dct['cqrs_id'], dct['instance_data'], dct.get('instance_pk'), previous_data=dct.get('previous_data'), correlation_id=dct.get('correlation_id'), ) cls.log_consumed(payload) instance = consumer.consume(payload) if instance: message.ack() cls.log_consumed_accepted(payload) else: message.reject() cls.log_consumed_denied(payload)
def _consume_message(cls, ch, method, properties, body): try: dct = ujson.loads(body) for key in ('signal_type', 'cqrs_id', 'instance_data'): if key not in dct: raise ValueError if 'instance_pk' not in dct: logger.warning('CQRS deprecated package structure.') except ValueError: logger.error("CQRS couldn't be parsed: {}.".format(body)) ch.basic_reject(delivery_tag=method.delivery_tag, requeue=False) return payload = TransportPayload( dct['signal_type'], dct['cqrs_id'], dct['instance_data'], dct.get('instance_pk'), previous_data=dct.get('previous_data'), ) cls.log_consumed(payload) instance = None try: instance = consumer.consume(payload) except Exception: logger.error('CQRS service exception', exc_info=True) if instance: ch.basic_ack(delivery_tag=method.delivery_tag) cls.log_consumed_accepted(payload) else: ch.basic_nack(delivery_tag=method.delivery_tag) cls.log_consumed_denied(payload)
def post_delete(cls, sender, **kwargs): """ :param dj_cqrs.mixins.MasterMixin sender: Class or instance inherited from CQRS MasterMixin. """ if not sender.CQRS_PRODUCE: return instance = kwargs['instance'] if not instance.is_sync_instance(): return instance_data = { 'id': instance.pk, 'cqrs_revision': instance.cqrs_revision + 1, 'cqrs_updated': str(now()), } data = instance.get_custom_cqrs_delete_data() if data: instance_data['custom'] = data signal_type = SignalType.DELETE payload = TransportPayload(signal_type, sender.CQRS_ID, instance_data, instance.pk) # Delete is always in transaction! transaction.on_commit(lambda: producer.produce(payload))
def test_consumer(mocker): factory_mock = mocker.patch( 'dj_cqrs.controller.consumer.route_signal_to_replica_model') consume(TransportPayload('a', 'b', {}, 'c', previous_data={'e': 'f'})) factory_mock.assert_called_once_with('a', 'b', {}, previous_data={'e': 'f'})
def test_produce_connection_error(rabbit_transport, mocker, caplog): mocker.patch.object(RabbitMQTransport, '_get_producer_rmq_objects', side_effect=amqp_error) rabbit_transport.produce( TransportPayload( SignalType.SAVE, 'CQRS_ID', {'id': 1}, 1, ), ) assert "CQRS couldn't be published: pk = 1 (CQRS_ID)." in caplog.text
def test_transport_payload_infinite_expires(): payload = TransportPayload.from_message({ 'signal_type': SignalType.SYNC, 'cqrs_id': 'cqrs_id', 'instance_data': {}, 'instance_pk': 'id', 'expires': None, }) assert payload.expires is None
def test_delay_message_with_requeue(mocker, caplog): channel = mocker.MagicMock() requeue_message = mocker.patch( 'dj_cqrs.transport.rabbit_mq.RabbitMQTransport._requeue_message', ) delay_messages = [] for delay in (2, 1, 3): payload = TransportPayload(SignalType.SAVE, 'CQRS_ID', {'id': delay}, delay) eta = datetime.now(tz=timezone.utc) + timedelta(hours=delay) delay_message = DelayMessage(delivery_tag=delay, payload=payload, eta=eta) delay_messages.append(delay_message) delay_queue = DelayQueue(max_size=3) for delay_message in delay_messages: delay_queue.put(delay_message) exceeding_delay = 0 exceeding_payload = TransportPayload(SignalType.SAVE, 'CQRS_ID', {'id': 4}, 4) PublicRabbitMQTransport.delay_message( channel, 4, exceeding_payload, exceeding_delay, delay_queue, ) assert delay_queue.qsize() == 3 assert delay_queue.get().payload is exceeding_payload assert ( 'CQRS is delayed: pk = 4 (CQRS_ID), correlation_id = None, delay = 0 sec' in caplog.text) assert requeue_message.call_count == 1 requeue_payload = requeue_message.call_args[0][2] min_eta_delay_message = sorted(delay_messages, key=lambda x: x.eta)[0] assert requeue_payload is min_eta_delay_message.payload
def test_fail_message_with_retry(mocker): payload = TransportPayload(SignalType.SAVE, 'basic', {'id': 1}, 1) delay_queue = DelayQueue() PublicRabbitMQTransport.fail_message(mocker.MagicMock(), 100, payload, None, delay_queue) assert delay_queue.qsize() == 1 delay_message = delay_queue.get() assert delay_message.delivery_tag == 100 assert delay_message.payload is payload
def test_produce_publish_error(rabbit_transport, mocker, caplog): mocker.patch.object( RabbitMQTransport, '_get_producer_rmq_objects', return_value=(mocker.MagicMock(), None), ) mocker.patch.object(RabbitMQTransport, '_produce_message', side_effect=amqp_error) rabbit_transport.produce( TransportPayload( SignalType.SAVE, 'CQRS_ID', {'id': 1}, 1, ), ) assert "CQRS couldn't be published: pk = 1 (CQRS_ID)." in caplog.text
def test_produce_ok(rabbit_transport, mocker, caplog): caplog.set_level(logging.INFO) mocker.patch.object( RabbitMQTransport, '_get_producer_rmq_objects', return_value=(mocker.MagicMock(), None), ) mocker.patch.object(RabbitMQTransport, '_produce_message', return_value=True) rabbit_transport.produce( TransportPayload( SignalType.SAVE, 'CQRS_ID', {'id': 1}, 1, ), ) assert 'CQRS is published: pk = 1 (CQRS_ID).' in caplog.text
def test_transport_payload_without_expires(mocker, settings): fake_now = datetime(2020, 1, 1, second=0, tzinfo=timezone.utc) mocker.patch('django.utils.timezone.now', return_value=fake_now) settings.CQRS['master']['CQRS_MESSAGE_TTL'] = 10 expected_expires = datetime(2020, 1, 1, second=10, tzinfo=timezone.utc) payload = TransportPayload.from_message({ 'signal_type': SignalType.SYNC, 'cqrs_id': 'cqrs_id', 'instance_data': {}, 'instance_pk': 'id', }) assert payload.expires == expected_expires
def test_produce_sync_message_queue(mocker): channel = mocker.MagicMock() payload = TransportPayload(SignalType.SYNC, 'cqrs_id', {}, 'id', 'queue') PublicRabbitMQTransport.produce_message(channel, 'exchange', payload) basic_publish_kwargs = channel.basic_publish.call_args[1] assert ujson.loads(basic_publish_kwargs['body']) == \ { 'signal_type': SignalType.SYNC, 'cqrs_id': 'cqrs_id', 'instance_data': {}, 'instance_pk': 'id', 'previous_data': None, } assert basic_publish_kwargs['routing_key'] == 'cqrs.queue.cqrs_id'
def test_producer(mocker): transport_mock = mocker.patch('tests.dj.transport.TransportStub.produce') produce(TransportPayload('a', 'b', {}, 'c', previous_data={'e': 'f'})) assert transport_mock.call_count == 1 assert transport_mock.call_args[0][0].to_dict() == { 'signal_type': 'a', 'cqrs_id': 'b', 'instance_data': {}, 'instance_pk': 'c', 'previous_data': { 'e': 'f' }, 'correlation_id': None, 'expires': None, 'retries': 0, }
def post_save(cls, sender, **kwargs): """ :param dj_cqrs.mixins.MasterMixin sender: Class or instance inherited from CQRS MasterMixin. """ if not sender.CQRS_PRODUCE: return update_fields = kwargs.get('update_fields') if update_fields and ('cqrs_revision' not in update_fields): return instance = kwargs['instance'] if not instance.is_sync_instance(): return using = kwargs['using'] sync = kwargs.get('sync', False) queue = kwargs.get('queue', None) connection = transaction.get_connection(using) if not connection.in_atomic_block: instance.reset_cqrs_saves_count() instance_data = instance.to_cqrs_dict(using, sync=sync) previous_data = instance.get_tracked_fields_data() signal_type = SignalType.SYNC if sync else SignalType.SAVE payload = TransportPayload( signal_type, sender.CQRS_ID, instance_data, instance.pk, queue, previous_data, expires=get_expires_datetime(), ) producer.produce(payload) elif instance.is_initial_cqrs_save: transaction.on_commit( lambda: MasterSignals.post_save( sender, instance=instance, using=using, sync=sync, queue=queue, ), )
def _consume_message(cls, ch, method, properties, body, delay_queue): try: dct = ujson.loads(body) except ValueError: logger.error("CQRS couldn't be parsed: {0}.".format(body)) ch.basic_reject(delivery_tag=method.delivery_tag, requeue=False) return required_keys = { 'instance_pk', 'signal_type', 'cqrs_id', 'instance_data' } for key in required_keys: if key not in dct: msg = "CQRS couldn't proceed, %s isn't found in body: %s." logger.error(msg, key, body) ch.basic_reject(delivery_tag=method.delivery_tag, requeue=False) return payload = TransportPayload.from_message(dct) cls.log_consumed(payload) delivery_tag = method.delivery_tag if payload.is_expired(): cls._add_to_dead_letter_queue(ch, payload) cls._nack(ch, delivery_tag) return instance, exception = None, None try: instance = consumer.consume(payload) except Exception as e: exception = e logger.error("CQRS service exception", exc_info=True) if instance and exception is None: cls._ack(ch, delivery_tag, payload) else: cls._fail_message( ch, delivery_tag, payload, exception, delay_queue, )
def test_produce_sync_message_no_queue(mocker): channel = mocker.MagicMock() payload = TransportPayload(SignalType.SYNC, 'cqrs_id', {}, None) exchange = PublicKombuTransport.create_exchange('exchange') PublicKombuTransport.produce_message(channel, exchange, payload) prepare_message_args = channel.prepare_message.call_args[0] basic_publish_kwargs = channel.basic_publish.call_args[1] assert ujson.loads(prepare_message_args[0]) == \ { 'signal_type': SignalType.SYNC, 'cqrs_id': 'cqrs_id', 'instance_data': {}, 'instance_pk': None, 'previous_data': None, } assert basic_publish_kwargs['routing_key'] == 'cqrs_id'
def handle_retry(self, channel, consumer_generator, dead_letters_count): self.stdout.write("Total dead letters: {}".format(dead_letters_count)) for i in range(1, dead_letters_count + 1): self.stdout.write("Retrying: {}/{}".format(i, dead_letters_count)) method_frame, properties, body = next(consumer_generator) dct = ujson.loads(body) dct['retries'] = 0 if dct.get('expires'): # Message could expire already expires = get_expires_datetime() dct['expires'] = expires.replace(microsecond=0).isoformat() payload = TransportPayload.from_message(dct) payload.is_requeue = True RabbitMQTransportService.produce(payload) message = ujson.dumps(dct) self.stdout.write(message) RabbitMQTransportService.nack(channel, method_frame.delivery_tag)
def test_fail_message_invalid_model(mocker, caplog): nack = mocker.patch( 'dj_cqrs.transport.rabbit_mq.RabbitMQTransport._nack', ) payload = TransportPayload(SignalType.SAVE, 'not_existing', {'id': 1}, 1) delay_queue = DelayQueue() delivery_tag = 101 PublicRabbitMQTransport.fail_message( mocker.MagicMock(), delivery_tag, payload, None, delay_queue, ) assert delay_queue.qsize() == 0 assert nack.call_count == 1 assert nack.call_args[0][1] == delivery_tag assert 'Model for cqrs_id not_existing is not found.' in caplog.text
def test_changed_payload_data_during_consume(mocker): def change_data(*args, **kwargs): instance_data = args[2] instance_data['instance_key'] = 'changed instance' kwargs['previous_data']['previous_key'] = 'changed previous' factory_mock = mocker.patch( 'dj_cqrs.controller.consumer.route_signal_to_replica_model', side_effect=change_data, ) payload = TransportPayload( SignalType.SAVE, cqrs_id='b', instance_data={'instance_key': 'initial instance'}, instance_pk='c', previous_data={'previous_key': 'initial previous'}, ) consume(payload) assert factory_mock.call_count == 1 assert payload.instance_data == {'instance_key': 'initial instance'} assert payload.previous_data == {'previous_key': 'initial previous'}
def test_produce_message_ok(mocker): expires = datetime(2100, 1, 1, tzinfo=timezone.utc) expected_expires = '2100-01-01T00:00:00+00:00' channel = mocker.MagicMock() payload = TransportPayload( SignalType.SAVE, cqrs_id='cqrs_id', instance_data={}, instance_pk='id', previous_data={'e': 'f'}, expires=expires, retries=2, ) PublicRabbitMQTransport.produce_message(channel, 'exchange', payload) assert channel.basic_publish.call_count == 1 basic_publish_kwargs = channel.basic_publish.call_args[1] assert ujson.loads(basic_publish_kwargs['body']) == { 'signal_type': SignalType.SAVE, 'cqrs_id': 'cqrs_id', 'instance_data': {}, 'instance_pk': 'id', 'previous_data': { 'e': 'f' }, 'correlation_id': None, 'expires': expected_expires, 'retries': 2, } assert basic_publish_kwargs['exchange'] == 'exchange' assert basic_publish_kwargs['mandatory'] assert basic_publish_kwargs['routing_key'] == 'cqrs_id' assert basic_publish_kwargs['properties'].content_type == 'text/plain' assert basic_publish_kwargs['properties'].delivery_mode == 2
def test_produce_message_ok(mocker): channel = mocker.MagicMock() payload = TransportPayload( SignalType.SAVE, 'cqrs_id', {}, 'id', previous_data={'e': 'f'}, ) PublicRabbitMQTransport.produce_message(channel, 'exchange', payload) assert channel.basic_publish.call_count == 1 basic_publish_kwargs = channel.basic_publish.call_args[1] assert ujson.loads(basic_publish_kwargs['body']) == \ { 'signal_type': SignalType.SAVE, 'cqrs_id': 'cqrs_id', 'instance_data': {}, 'instance_pk': 'id', 'previous_data': {'e': 'f'}, } assert basic_publish_kwargs['exchange'] == 'exchange' assert basic_publish_kwargs['mandatory'] assert basic_publish_kwargs['routing_key'] == 'cqrs_id' assert basic_publish_kwargs['properties'].content_type == 'text/plain' assert basic_publish_kwargs['properties'].delivery_mode == 2
def test_process_delay_messages(mocker, caplog): channel = mocker.MagicMock() produce = mocker.patch( 'dj_cqrs.transport.rabbit_mq.RabbitMQTransport.produce') payload = TransportPayload(SignalType.SAVE, 'CQRS_ID', {'id': 1}, 1) delay_queue = DelayQueue() delay_queue.put( DelayMessage(delivery_tag=1, payload=payload, eta=datetime.now(tz=timezone.utc)), ) PublicRabbitMQTransport.process_delay_messages(channel, delay_queue) assert delay_queue.qsize() == 0 assert channel.basic_nack.call_count == 1 assert produce.call_count == 1 produce_payload = produce.call_args[0][0] assert produce_payload is payload assert produce_payload.retries == 1 assert getattr(produce_payload, 'is_requeue', False) assert 'CQRS is requeued: pk = 1 (CQRS_ID)' in caplog.text