/// <summary> /// Actually does the work of processing a single message. /// </summary> /// <param name="queueId"></param> /// <returns></returns> public async Task <bool> DoWork() { const string savepointName = "BeforeMessageHandler"; // get a message. var messageContext = await _queueReader.GetNext(QueueName); // there are no messages, so we are done. Return false so the transaction will roll back, will sleep for a while. if (messageContext == null) { return(false); } // we found a message to process. _log.Debug($"Processing {messageContext.MessageData.MessageId}"); var started = DateTime.UtcNow; try { _counters.StartMessage(); // creat a save point. If anything goes wrong we can roll back to here, // increment the retry count and try again later. _dataAccess.CreateSavepoint(savepointName); // determine what type of message it is. var messageType = Type.GetType(messageContext.Headers.MessageClass); if (messageType == null) { throw new ApplicationException($"Message {messageContext.MessageData.MessageId} as a message class of {messageContext.Headers.MessageClass} which was not a recognized type."); } // Get the message handlers for this message type from the Dependency Injection container. // the list will contain both regular handlers and sagas. // if a message has mulitple handlers, we'll get multiple handlers. var method = typeof(IFindQueueHandlers).GetMethod("FindHandlers"); var genericMethod = method.MakeGenericMethod(messageType); var handlers = genericMethod.Invoke(_findHandlers, null); var castHandlers = (handlers as IEnumerable <object>).ToArray(); // sanity check that the Depenency Injection container found at least one handler. // we shouldn't process a message that has no handlers. if (castHandlers.Length < 1) { throw new ApplicationException($"There were no message handlers for {messageContext.Headers.MessageClass}."); } // invoke each of the handlers. foreach (var handler in castHandlers) { // determine if this handler is a saga. var handlerType = handler.GetType(); var handlerIsSaga = handlerType.IsSubclassOfSaga(); if (handlerIsSaga) { messageContext.SagaKey = _sagaMessageMapManager.GetKey(handler, messageContext.Message); _log.Debug($"Active Saga {messageContext.SagaKey}"); await _queueReader.LoadSaga(handler, messageContext); if (messageContext.SagaData != null && messageContext.SagaData.Blocked) { // the saga is blocked. delay the message and try again later. _log.Info($"The saga {handlerType} for key {messageContext.SagaKey} is blocked. The current message will be delayed and retried."); _dataAccess.RollbackToSavepoint(savepointName); await _queueReader.DelayMessage(messageContext, 250); _counters.SagaBlocked(); return(true); } if (messageContext.SagaData == null && !handlerType.IsSagaStartHandler(messageType)) { // the saga was not locked, and it doesn't exist, and this message doesn't start a saga. // we are processing a saga message but it is not a saga start message and we didnt read previous // saga data from the DB. This means we are processing a non-start messge before the saga is started. // we could continute but that might be bad. Its probably better to stop and draw attention to a probable bug in the saga or message order. throw new ApplicationException($"A Message of Type {messageType} is being processed, but the saga {handlerType} has not been started for key {messageContext.SagaKey}. An IHandleSagaStartMessage<> handler on the saga must be processed first to start the saga."); } } // find the right method on the handler. var parameterTypes = new[] { typeof(QueueContext), messageType }; var handleMethod = handler.GetType().GetMethod("Handle", parameterTypes); // Invoke and await the method. // should it have a seperate try-catch around this and treat it differently? // that would allow us to tell the difference between a problem in a handler, or if the problem was in the bus code. // does that mater for the retry? { var taskObject = handleMethod.Invoke(handler, new object[] { messageContext, messageContext.Message }); var castTask = taskObject as Task; await castTask; } if (handlerIsSaga) { await _queueReader.SaveSaga(handler, messageContext); _log.Debug($"Inactive Saga {messageContext.SagaKey}"); } } // if nothing threw an exception, we can mark the message as processed. await _queueReader.Complete(messageContext); // return true so the transaction commits and the main loop looks for another mesage right away. return(true); } catch (Exception ex) { // there was an exception, Rollback to the save point to undo // any db changes done by the handlers. _log.Warn($"There was an execption processing the message. {ex}"); _dataAccess.RollbackToSavepoint(savepointName); // increment the retry count, (or maybe even fail the message) await _queueReader.Fail(messageContext, ex); // return true so the transaction commits and the main loop looks for another mesage right away. return(true); } finally { _counters.FinishMessage(started); } }