-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #550 from sanger/DPL-199-create-feedback-messages
DPL-199: Perform simple message processing from RabbitMQ
- Loading branch information
Showing
26 changed files
with
745 additions
and
67 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
import logging | ||
|
||
from crawler.config.centres import CENTRE_DATA_SOURCE_RABBITMQ, get_centres_config | ||
from crawler.config.defaults import RABBITMQ_FEEDBACK_EXCHANGE | ||
from crawler.constants import ( | ||
CENTRE_KEY_LAB_ID_DEFAULT, | ||
RABBITMQ_CREATE_FEEDBACK_ORIGIN_PARSING, | ||
RABBITMQ_CREATE_FEEDBACK_ORIGIN_PLATE, | ||
RABBITMQ_FIELD_LAB_ID, | ||
RABBITMQ_FIELD_MESSAGE_UUID, | ||
RABBITMQ_FIELD_PLATE, | ||
RABBITMQ_ROUTING_KEY_CREATE_PLATE_FEEDBACK, | ||
RABBITMQ_SUBJECT_CREATE_PLATE_FEEDBACK, | ||
) | ||
from crawler.exceptions import TransientRabbitError | ||
from crawler.rabbit.avro_encoder import AvroEncoder | ||
from crawler.rabbit.messages.create_feedback_message import CreateFeedbackError, CreateFeedbackMessage | ||
|
||
LOGGER = logging.getLogger(__name__) | ||
|
||
|
||
class CreatePlateProcessor: | ||
def __init__(self, schema_registry, basic_publisher, config): | ||
self._encoder = AvroEncoder(schema_registry, RABBITMQ_SUBJECT_CREATE_PLATE_FEEDBACK) | ||
self._basic_publisher = basic_publisher | ||
self._config = config | ||
|
||
self._centres = None | ||
|
||
def process(self, message): | ||
self._centres = None | ||
|
||
try: | ||
self._validate_message(message) | ||
except TransientRabbitError as ex: | ||
LOGGER.error(f"Transient error while processing message: {ex.message}") | ||
raise # Cause the consumer to restart and try this message again. Ideally we will delay the consumer. | ||
except Exception as ex: | ||
LOGGER.error(f"Unhandled error while processing message: {type(ex)} {str(ex)}") | ||
self._publish_feedback( | ||
message, | ||
additional_errors=[ | ||
CreateFeedbackError( | ||
origin=RABBITMQ_CREATE_FEEDBACK_ORIGIN_PARSING, | ||
description="An unhandled error occurred while processing the message.", | ||
) | ||
], | ||
) | ||
return False # Send the message to dead letters | ||
|
||
self._publish_feedback(message) | ||
return len(message.errors) == 0 | ||
|
||
@property | ||
def centres(self): | ||
if self._centres is None: | ||
try: | ||
self._centres = get_centres_config(self._config, CENTRE_DATA_SOURCE_RABBITMQ) | ||
except Exception: | ||
raise TransientRabbitError("Unable to reach MongoDB while getting centres config.") | ||
|
||
return self._centres | ||
|
||
def _publish_feedback(self, message, additional_errors=()): | ||
message_uuid = message.message[RABBITMQ_FIELD_MESSAGE_UUID].decode() | ||
errors = message.errors | ||
errors.extend(additional_errors) | ||
|
||
feedback_message = CreateFeedbackMessage( | ||
sourceMessageUuid=message_uuid, | ||
countOfTotalSamples=0, | ||
countOfValidSamples=0, | ||
operationWasErrorFree=len(errors) == 0, | ||
errors=errors, | ||
) | ||
|
||
encoded_message = self._encoder.encode([feedback_message]) | ||
self._basic_publisher.publish_message( | ||
RABBITMQ_FEEDBACK_EXCHANGE, | ||
RABBITMQ_ROUTING_KEY_CREATE_PLATE_FEEDBACK, | ||
encoded_message.body, | ||
RABBITMQ_SUBJECT_CREATE_PLATE_FEEDBACK, | ||
encoded_message.version, | ||
) | ||
|
||
@staticmethod | ||
def _add_error(message, origin, description, sample_uuid="", field=""): | ||
LOGGER.error( | ||
f"Error found in message with origin '{origin}', sampleUuid '{sample_uuid}', field '{field}': {description}" | ||
) | ||
message.add_error( | ||
CreateFeedbackError( | ||
origin=origin, | ||
sampleUuid=sample_uuid, | ||
field=field, | ||
description=description, | ||
) | ||
) | ||
|
||
def _validate_message(self, message): | ||
body = message.message | ||
|
||
# Check that the message is for a centre we are accepting RabbitMQ messages for. | ||
lab_id = body[RABBITMQ_FIELD_PLATE][RABBITMQ_FIELD_LAB_ID] | ||
if lab_id not in [c[CENTRE_KEY_LAB_ID_DEFAULT] for c in self.centres]: | ||
CreatePlateProcessor._add_error( | ||
message, | ||
RABBITMQ_CREATE_FEEDBACK_ORIGIN_PLATE, | ||
f"The lab ID provided '{lab_id}' is not configured to receive messages via RabbitMQ.", | ||
field=RABBITMQ_FIELD_LAB_ID, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
from crawler.constants import RABBITMQ_HEADER_KEY_SUBJECT, RABBITMQ_HEADER_KEY_VERSION | ||
|
||
|
||
class RabbitMessage: | ||
def __init__(self, headers, encoded_body): | ||
self.headers = headers | ||
self.encoded_body = encoded_body | ||
self.errors = [] | ||
|
||
self._subject = None | ||
self._schema_version = None | ||
self._decoded_list = None | ||
self._message = None | ||
|
||
@property | ||
def subject(self): | ||
if self._subject is None: | ||
self._subject = self.headers[RABBITMQ_HEADER_KEY_SUBJECT] | ||
return self._subject | ||
|
||
@property | ||
def schema_version(self): | ||
if self._schema_version is None: | ||
self._schema_version = self.headers[RABBITMQ_HEADER_KEY_VERSION] | ||
return self._schema_version | ||
|
||
def decode(self, encoder): | ||
self._decoded_list = list(encoder.decode(self.encoded_body, self.schema_version)) | ||
|
||
@property | ||
def contains_single_message(self): | ||
return self._decoded_list and len(self._decoded_list) == 1 | ||
|
||
@property | ||
def message(self): | ||
if self._decoded_list: | ||
return self._decoded_list[0] | ||
|
||
def add_error(self, error): | ||
self.errors.append(error) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import logging | ||
|
||
from crawler.constants import RABBITMQ_SUBJECT_CREATE_PLATE | ||
from crawler.processing.create_plate_processor import CreatePlateProcessor | ||
from crawler.processing.rabbit_message import RabbitMessage | ||
from crawler.rabbit.avro_encoder import AvroEncoder | ||
|
||
LOGGER = logging.getLogger(__name__) | ||
|
||
|
||
class RabbitMessageProcessor: | ||
def __init__(self, schema_registry, basic_publisher, config): | ||
self._schema_registry = schema_registry | ||
self._basic_publisher = basic_publisher | ||
self._config = config | ||
|
||
self._processors = { | ||
RABBITMQ_SUBJECT_CREATE_PLATE: CreatePlateProcessor( | ||
self._schema_registry, self._basic_publisher, self._config | ||
) | ||
} | ||
|
||
def process_message(self, headers, body): | ||
message = RabbitMessage(headers, body) | ||
try: | ||
message.decode(AvroEncoder(self._schema_registry, message.subject)) | ||
except Exception as ex: | ||
LOGGER.error(f"Unrecoverable error while decoding RabbitMQ message: {type(ex)} {str(ex)}") | ||
return False # Send the message to dead letters. | ||
|
||
if not message.contains_single_message: | ||
LOGGER.error("RabbitMQ message received containing multiple AVRO encoded messages.") | ||
return False # Send the message to dead letters. | ||
|
||
try: | ||
return self._processors[message.subject].process(message) | ||
except KeyError: | ||
LOGGER.error( | ||
f"Received message has subject '{message.subject}'" | ||
" but there is no implemented processor for this subject." | ||
) | ||
return False # Send the message to dead letters. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.