I'm working on converting an existing Java/SpringBoot/ActiveMQ console app (which consumes messages from a named ActiveMQ Message Queue and processes the messages) into an analogous C# .NET console app. I've got things running correctly for the success case, but I'm having trouble replicating the Java app's behavior for cases in which the message handler fails to process the message successfully.
The failure case behavior which I'm attempting to replicate is rollback & re-queueing of the message (back into the named queue from which it was received) when message handling fails.
In Java/SpringBoot, I do this via adding transactional configuration to appropriate classes/methods wherein the message processing occurs. For failure cases, I throw a RuntimeException (unchecked) and allow such thrown exceptions to be handled in the Spring transactional framework (using all SpringBoot defaults for such exception handling) and, based on transaction rollback processing by the framework, the message is re-queued. I'm not explicitly doing any rollback processing in my Java application code logic, but, rather, allowing the framework default flow to handle all this.
I'm less familiar with the Spring .NET analog for achieving this auto-rollback/re-queue behavior in my C# application. And, as of yet, I've not been able to replicate this. According to section 31.5.5 of the Spring .NET documentation:
Invoking a message listener within a transaction only requires reconfiguration of the listener container. Local message transactions can be activated by setting the property SessionAcknowledgeMode which for NMS is of the enum type AcknowledgementMode, to AcknowledgementMode.Transactional. Each message listener invocation will then operate within an active messaging transaction, with message reception rolled back in case of listener execution failure.
I have followed the prescription above in the Main method of my C# console app:
static void Main(string[] args)
{
var ctx = ContextRegistry.GetContext();
var msgListenerContainer = ctx.GetObject<SimpleMessageListenerContainer>("MessageListenerContainer");
msgListenerContainer.SessionAcknowledgeMode = AcknowledgementMode.Transactional;
Console.ReadLine();
}
However, it is not clear to me how to trigger listener execution failure in my C# console application. I've tried throwing various types of Exceptions (ApplicationException, Exception, NMSException) and I see no re-queuing of messages.
Any enlightenment would be much appreciated.
FYI, here is additional germane code logic and configuration:
Message handling method:
public void HandleMessage(Hashtable message)
{
Logger.Debug("Entered HandleMessage");
var ticketUuid = message["ti"] as string;
if (ticketUuid == null) throw new ArgumentNullException("ticketUuid");
var gameTitle = message["gt"] as string;
if (gameTitle == null) throw new ArgumentNullException("gameTitle");
var recallData = message["grd"] as string;
if (recallData == null) throw new ArgumentNullException("recallData");
Logger.Debug(string.Format("HandleMessage - ticketUuid={0} gameTitle={1} recallData={2}", ticketUuid,
gameTitle, recallData));
VideoRecordingService.RecordVideo(ticketUuid, gameTitle, recallData);
}
Here is a version of RecordVideo method with much irrelevant code logic elided for clarity:
[Transaction]
public async Task RecordVideo(string ticketId, string gameTitle, string gameRecallData)
{
<elided code>
// start up the video recording app
Recorder.PowerOn();
// start up the Air Game Exe as a separate process
RunAirGameProc(gameTitle);
// for testing failed message processing
throw new ApplicationException("forced exception");
var videoBytes = File.ReadAllBytes(gameShareVideoFilename);
MsgProducer.UpdateJobStatus(ticketId, TicketStatusEnum.Recorded, videoBytes);
MsgProducer.UploadVideo(ticketId, videoBytes);
}
catch (Exception ex)
{
Logger.WarnFormat("Exception caught: {0}" + Environment.NewLine + "{1}", ex.Message, ex.StackTrace);
throw new NMSException(ex.Message);
}
finally
{
// clean up files
<elided>
}
}
And, here is the relevant Spring .NET configuration for the console app:
<spring>
<context>
<resource uri="config://spring/objects" />
</context>
<objects xmlns="http://www.springframework.net">
<description>Game Share Video Recording Service Spring IoC Configuration</description>
<object name="MsgProducer"
type="GameShare.VideoRecorder.MessageProducer, GameShare.VideoRecorder">
<property name="NmsTemplate" ref="NmsTemplate" />
<property name="JobInfoDestination">
<object type="Apache.NMS.ActiveMQ.Commands.ActiveMQQueue, Apache.NMS.ActiveMQ">
<constructor-arg value="gameShareJobInfo" />
</object>
</property>
<property name="VideoUploadDestination">
<object type="Apache.NMS.ActiveMQ.Commands.ActiveMQQueue, Apache.NMS.ActiveMQ">
<constructor-arg value="gameShareVideoUpload" />
</object>
</property>
</object>
<object name="ConnectionFactory"
type="Spring.Messaging.Nms.Connections.CachingConnectionFactory, Spring.Messaging.Nms">
<property name="SessionCacheSize" value="10" />
<property name="TargetConnectionFactory">
<object type="Apache.NMS.ActiveMQ.ConnectionFactory, Apache.NMS.ActiveMQ">
<constructor-arg index="0" value="tcp://localhost:61616" />
</object>
</property>
</object>
<object name="MessageHandler"
type="GameShare.VideoRecorder.MessageHandler, GameShare.VideoRecorder"
autowire="autodetect" />
<object name="MessageListenerAdapter"
type="Spring.Messaging.Nms.Listener.Adapter.MessageListenerAdapter, Spring.Messaging.Nms">
<property name="HandlerObject" ref="MessageHandler" />
</object>
<object name="MessageListenerContainer"
type="Spring.Messaging.Nms.Listener.SimpleMessageListenerContainer, Spring.Messaging.Nms">
<property name="ConnectionFactory" ref="ConnectionFactory" />
<property name="DestinationName" value="gameShareVideoRecording" />
<property name="MessageListener" ref="MessageListenerAdapter" />
</object>
<object name="NmsTemplate" type="Spring.Messaging.Nms.Core.NmsTemplate, Spring.Messaging.Nms">
<property name="ConnectionFactory" ref="ConnectionFactory" />
</object>
</objects>
</spring>
UPDATE: After re-reading chapter 31 of the Spring .Net docs (and tinkering with my console app a bit more), I've arrived at a solution/answer. The default listener-container (Spring.Messaging.Nms.Listener.SimpleMessageListenerContainer) does not provide the behavior I am trying to emulate from my Java/Spring/ActiveMQ model (which, BTW, is the default behavior for message handling in the context of a transaction using the Java/SpringBoot framework). Specifically, as noted in section 31.5.2 Asynchronous Reception:
Exceptions that are thrown during message processing can be passed to an implementation of IExceptionHandler and registered with the container via the property ExceptionListener. The registered IExceptionHandler will be invoked if the exception is of the type NMSException (or the equivalent root exception type for other providers). The SimpleMessageListenerContainer will log the exception at error level and not propagate the exception to the provider. All handling of acknowledgement and/or transactions is done by the listener container. You can override the method HandleListenerException to change this behavior.
So, in order to get the desired behavior, I had to provide my own implementations of IExceptionHandler and IErrorHandler that do propagate the exceptions to the provider. The provider (ActiveMQ in my case) will see the exception and re-queue the message.
A simple implementation for these interfaces is as follows:
public class VideoRecorderApp
{
static void Main(string[] args)
{
var ctx = ContextRegistry.GetContext();
var msgListenerContainer = ctx.GetObject<SimpleMessageListenerContainer>("MessageListenerContainer");
msgListenerContainer.SessionAcknowledgeMode = AcknowledgementMode.Transactional;
msgListenerContainer.ErrorHandler = new MyErrorHandler();
msgListenerContainer.ExceptionListener = new MyExceptionListener();
Console.ReadLine();
}
}
internal class MyErrorHandler : IErrorHandler
{
/// <summary>
/// The logger
/// </summary>
private static readonly ILog Logger = LogManager.GetLogger<MyErrorHandler>();
/// <summary>
/// Handles the error.
/// </summary>
/// <param name="exception">The exception.</param>
public void HandleError(Exception exception)
{
Logger.WarnFormat("HandleError: {0}", exception.Message);
throw exception;
}
}
internal class MyExceptionListener : IExceptionListener
{
/// <summary>
/// The logger
/// </summary>
private static readonly ILog Logger = LogManager.GetLogger<MyExceptionListener>();
/// <summary>
/// Called when there is an exception in message processing.
/// </summary>
/// <param name="exception">The exception.</param>
public void OnException(Exception exception)
{
Logger.WarnFormat("OnException: {0}", exception.Message);
throw exception;
}
}
And, if one wants/needs to distinguish between specific Exception types (e.g., some of which should trigger a re-queue and others of which we wish just to log/swallow), one can certainly add code logic into the handlers to do so.