我有一个WCF服务,它从Microsoft消息队列(netMsmqBinding
)接收消息。
我希望我的服务在消息队列不可用时恢复。我的代码应该无法打开服务,但在延迟后重试。
我有代码可以在队列不可用时识别错误:
static bool ExceptionIsBecauseMsmqNotStarted(TypeInitializationException ex)
{
MsmqException msmqException = ex.InnerException as MsmqException;
return ((msmqException != null) && msmqException.HResult == (unchecked((int)(0xc00e000b))));
}
所以这应该很简单:我呼叫ServiceHost.Open()
,捕获此异常,等待一两秒钟,然后重复,直到我的Open
调用成功。
问题是,如果这个异常被抛出一次,它就会继续被抛出。消息队列可能已变为可用,但我正在运行的进程处于错误状态,我继续获取TypeInitializationException
,直到我关闭进程并重新启动它。
有没有办法解决这个问题?我可以让 WCF 原谅队列并真正尝试再次收听它吗?
这是我的服务开放代码:
public async void Start()
{
try
{
_log.Debug("Starting the data warehouse service");
while(!_cancellationTokenSource.IsCancellationRequested)
{
try
{
_serviceHost = new ServiceHost(_dataWarehouseWriter);
_serviceHost.Open();
return;
}
catch (TypeInitializationException ex)
{
_serviceHost.Abort();
if(!ExceptionIsBecauseMsmqNotStarted(ex))
{
throw;
}
}
await Task.Delay(1000, _cancellationTokenSource.Token);
}
}
catch (Exception ex)
{
_log.Error("Failed to start the service host", ex);
}
}
这是堆栈信息。第一次抛出内部异常的堆栈跟踪时是:
at System.ServiceModel.Channels.MsmqQueue.GetMsmqInformation(Version&version, Boolean& activeDirectoryEnabled)
at System.ServiceModel.Channels.Msmq..cctor()
以及外部异常堆栈的顶部条目:
at System.ServiceModel.Channels.MsmqChannelListenerBase'1.get_TransportManagerTable()
at System.ServiceModel.Channels.TransportManagerContainer..ctor(TransportChannelListener listener
)
Microsoft使WCF的源代码可见,所以现在我们可以确切地弄清楚发生了什么。
坏消息:WCF 的实现方式是,如果对ServiceModel.Start()
的初始调用触发排队错误,则无法恢复。
WCF 框架包括一个名为MsmqQueue
的内部类。此类具有静态构造函数。静态构造函数调用 GetMsmqInformation,这可能会引发异常。
阅读有关静态构造函数的 C# 编程指南:
如果静态构造函数引发异常,运行时将不会再次调用它,并且该类型在运行程序的应用程序域的生存期内将保持未初始化状态。
这里有一个编程课程:不要将异常抛出代码放在静态构造函数中!
显而易见的解决方案在代码之外。创建托管服务时,我可以在消息队列服务上添加服务依赖项。但是,我宁愿用代码然后配置来解决此问题。
另一种解决方案是使用非 WCF 代码手动检查队列是否可用。
如果消息队列服务不可用,则 System.Messaging.MessageQueue.Exists 方法返回false
。知道这一点,以下工作:
private const string KNOWN_QUEUE_PATH = @".Private$datawarehouse";
private static string GetMessageQueuePath()
{
// We can improve this by extracting the queue path from the configuration file
return KNOWN_QUEUE_PATH;
}
public async void Start()
{
try
{
_log.Debug("Starting the data warehouse service");
string queuePath = GetMessageQueuePath();
while(!_cancellationTokenSource.IsCancellationRequested)
{
if (!(System.Messaging.MessageQueue.Exists(queuePath)))
{
_log.Warn($"Unable to find the queue {queuePath}. Will try again shortly");
await Task.Delay(60000, _cancellationTokenSource.Token);
}
else
{
_serviceHost = new ServiceHost(_dataWarehouseWriter);
_serviceHost.Open();
return;
}
}
}
catch(System.OperationCanceledException)
{
_log.Debug("The service start operation was cancelled");
}
catch (Exception ex)
{
_log.Error("Failed to start the service host", ex);
}
}