问题
I'm learning about what can be done with the WCF Routing Service. Still at the 'screwing around with it to see what it can do' phase.
My understanding of the Routing Service is that when a message comes through, the service will try to pass it on to whichever endpoint appears first in the backup list. If that fails, it will then go on to try the next, and then the next, until either something works or there's nothing left to try.
What I would like to do is get access to that failure event so that I can:
- Log the failure
- Send a notification via email that the endpoint is failing
- Optionally remove the endpoint from the backup list so that it won't slow down future messages from flowing through the system
Having trouble finding how to extend the WCF framework to get at this particular event.
Is this a thing that the WCF Routing Service can do? Any nudge in the right direction would be greatly appreciated.
At the moment I have 30-ish dynamically generated routing services hosted under IIS (or to be more precise, the ASP.NET Development Server for Visual Studio 2010). I'm setting up the routes to the services in Global.asax as below.
protected void Application_Start(object sender, EventArgs e)
{
List<Type> serviceTypes = ServiceUtility.GetServiceTypes();
foreach (Type st in serviceTypes)
{
string route = String.Format("Services/{0}.svc", ServiceUtility.GetServiceName(st));
RouteTable.Routes.Add(new ServiceRoute(route, new RoutingServiceHostFactory(st), typeof(System.ServiceModel.Routing.RoutingService)));
}
}
ServiceUtility and RoutingServiceHostFactory are custom classes. Note that IPolicyService is a WCF service contract interface in the assembly that I'm interested in.
public static class ServiceUtility
{
public static List<Type> GetServiceTypes()
{
Type policyInterfaceType = typeof(IPolicyService);
Assembly serviceContractsAssembly = Assembly.GetAssembly(policyInterfaceType);
Type[] serviceContractsAssemblyTypes = serviceContractsAssembly.GetTypes();
List<Type> serviceTypes = new List<Type>();
foreach (Type t in serviceContractsAssemblyTypes)
{
if (!t.IsInterface)
continue;
object[] attrib = t.GetCustomAttributes(typeof(ServiceContractAttribute), false);
if (attrib == null || attrib.Length <= 0)
continue;
serviceTypes.Add(t);
}
return serviceTypes;
}
// Other stuff
}
I'm generating my ServiceHosts as follows. I've omitted some of my helper methods for brevity.
public class RoutingServiceHostFactory : ServiceHostFactory
{
private Type BackendServiceType { get; set; }
private Binding BackendServiceBinding { get; set; }
public RoutingServiceHostFactory(Type backendServiceType)
{
this.BackendServiceType = backendServiceType;
this.BackendServiceBinding = ServiceUtility.GetBinding(this.BackendServiceType);
}
private const string DOMAIN_LIVE = "http://localhost:2521/";
private const string DOMAIN_DEAD_1 = "http://localhost:2522/";
private const string DOMAIN_DEAD_2 = "http://localhost:2524/";
private const string DOMAIN_DEAD_3 = "http://localhost:2525/";
private const string DOMAIN_DEAD_4 = "http://localhost:2526/";
private const string DOMAIN_DEAD_5 = "http://localhost:2527/";
protected override ServiceHost CreateServiceHost(Type serviceType, Uri[] baseAddresses)
{
ServiceHost host = base.CreateServiceHost(serviceType, baseAddresses);
this.BindEndpoints(host, baseAddresses);
this.ConfigureRoutingBehavior(host);
this.ConfigureServiceMetadataBehavior(host);
this.ConfigureDebugBehavior(host);
host.Description.Behaviors.Add(new RoutingServiceErrorHandlerInjector());
return host;
}
// Other Stuff
private void ConfigureRoutingBehavior(ServiceHost host)
{
string deadAddress1 = ServiceUtility.GetServiceUrl(DOMAIN_DEAD_1, this.BackendServiceType);
string deadAddress2 = ServiceUtility.GetServiceUrl(DOMAIN_DEAD_2, this.BackendServiceType);
string deadAddress3 = ServiceUtility.GetServiceUrl(DOMAIN_DEAD_3, this.BackendServiceType);
string deadAddress4 = ServiceUtility.GetServiceUrl(DOMAIN_DEAD_4, this.BackendServiceType);
string deadAddress5 = ServiceUtility.GetServiceUrl(DOMAIN_DEAD_5, this.BackendServiceType);
string realAddress = ServiceUtility.GetServiceUrl(DOMAIN_LIVE, this.BackendServiceType);
RoutingConfiguration rc = new RoutingConfiguration();
ContractDescription contract = new ContractDescription("IRequestReplyRouter");
ServiceEndpoint deadDestination1 = new ServiceEndpoint(contract, this.BackendServiceBinding, new EndpointAddress(deadAddress1));
ServiceEndpoint deadDestination2 = new ServiceEndpoint(contract, this.BackendServiceBinding, new EndpointAddress(deadAddress2));
ServiceEndpoint deadDestination3 = new ServiceEndpoint(contract, this.BackendServiceBinding, new EndpointAddress(deadAddress3));
ServiceEndpoint deadDestination4 = new ServiceEndpoint(contract, this.BackendServiceBinding, new EndpointAddress(deadAddress4));
ServiceEndpoint deadDestination5 = new ServiceEndpoint(contract, this.BackendServiceBinding, new EndpointAddress(deadAddress5));
ServiceEndpoint realDestination = new ServiceEndpoint(contract, this.BackendServiceBinding, new EndpointAddress(realAddress));
List<ServiceEndpoint> backupList = new List<ServiceEndpoint>();
backupList.Add(deadDestination1);
backupList.Add(deadDestination2);
backupList.Add(deadDestination3);
backupList.Add(deadDestination4);
backupList.Add(deadDestination5);
backupList.Add(realDestination);
rc.FilterTable.Add(new MatchAllMessageFilter(), backupList);
RoutingBehavior rb = new RoutingBehavior(rc);
host.Description.Behaviors.Add(rb);
}
// Other Stuff
}
Port 2521 has an actual website on the other end that is hosting some WCF services. The other ports referenced above have nothing listening.
For context, here's my Web.config for the routing site. Note that the time-outs and such are just the result of my screwing around, don't take them too seriously.
<?xml version="1.0"?>
<configuration>
<system.web>
<compilation debug="true" targetFramework="4.0" />
</system.web>
<system.serviceModel>
<serviceHostingEnvironment aspNetCompatibilityEnabled="true" />
<bindings>
<wsHttpBinding>
<binding
name="TestBinding"
allowCookies="True"
closeTimeout="00:04:00"
openTimeout="00:00:10"
receiveTimeout="00:05:00"
sendTimeout="00:05:00"
maxReceivedMessageSize="15728640">
<security>
<message establishSecurityContext="true" />
</security>
</binding>
</wsHttpBinding>
</bindings>
</system.serviceModel>
</configuration>
EDIT
In response to TheDoctor's answer below, I thought I should expand on what I've been doing with this attempted solution since I originally posted. I've tried implementing the IErrorHandler interface. However, I haven't had much luck with it.
Note that in the example above, my RoutingServiceHostFactory has changed slightly. I am now adding on the RoutingServiceErrorHandlerInjector behaviour to the service description. Note that I have also added extra dead endpoints to my backup list for the purpose of illustration.
public class RoutingServiceErrorHandlerInjector : IServiceBehavior
{
#region IServiceBehavior Members
public void AddBindingParameters(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase, System.Collections.ObjectModel.Collection<ServiceEndpoint> endpoints, System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
{
}
public void ApplyDispatchBehavior(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase)
{
foreach (ChannelDispatcher chanDisp in serviceHostBase.ChannelDispatchers)
{
chanDisp.ErrorHandlers.Add(new RoutingServiceErrorHandler());
}
}
public void Validate(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase)
{
}
#endregion
}
public class RoutingServiceErrorHandler : IErrorHandler
{
#region IErrorHandler Members
public bool HandleError(Exception error)
{
throw new NotImplementedException(error.Message, error);
}
public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
{
throw new NotImplementedException(error.Message, error);
}
#endregion
}
My expectation was that I should trigger a ProvideFault or a HandleError event for deadDestination1 through to deadDestination5. I have breakpoints in place on the NotImplementedExceptions above in my debugger. But that code is never activated. The calls eventually make it through to the real address at the end of the backup list, and the client/server application I'm using to test this RoutingService runs fine. Communication is slower but still well within the timeout limits.
However, if I comment out the line backupList.Add(realDestination);
from the ConfigureRoutingBehavior method above, then the RoutingServiceErrorHandler.ProvideFault method IS excercised... But it only contains the information relating to deadDestination5. Any exceptions or errors that may have been generated for deadDestination1 through deadDestination4 just disappear on me.
Furthermore, I've had a go with the RedGate debugger stepping through the reflected code for RoutingService. It was tricky for me as I'm not used to debugging optimized code, so there were hardly any variables available for me to actually read. But from eyeballing the stepthrough of the logic below:
// This has been taken from System.ServiceModel.Routing.RoutingService
// via the RedGate decompiler - unsure about it's ultimate accuracy.
[AspNetCompatibilityRequirements(RequirementsMode=AspNetCompatibilityRequirementsMode.Allowed), ServiceBehavior(AddressFilterMode=AddressFilterMode.Any, InstanceContextMode=InstanceContextMode.PerSession, UseSynchronizationContext=false, ValidateMustUnderstand=false)]
public sealed class RoutingService : ISimplexDatagramRouter, ISimplexSessionRouter, IRequestReplyRouter, IDuplexSessionRouter, IDisposable
{
[OperationBehavior(Impersonation=ImpersonationOption.Allowed), TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")]
IAsyncResult IRequestReplyRouter.BeginProcessRequest(Message message, AsyncCallback callback, object state)
{
return this.BeginProcessRequest<IRequestReplyRouter>(message, callback, state);
}
private IAsyncResult BeginProcessRequest<TContract>(Message message, AsyncCallback callback, object state)
{
IAsyncResult result;
try
{
System.ServiceModel.Routing.FxTrace.Trace.SetAndTraceTransfer(this.ChannelExtension.ActivityID, true);
result = new ProcessRequestAsyncResult<TContract>(this, message, callback, state);
}
catch (Exception exception)
{
if (TD.RoutingServiceProcessingFailureIsEnabled())
{
TD.RoutingServiceProcessingFailure(this.eventTraceActivity, OperationContext.Current.Channel.LocalAddress.ToString(), exception);
}
throw;
}
return result;
}
}
Relevant sections from System.ServiceModel.Routing.ProcessRequestAsyncResult shown below. These are also from debugging via RedGate, so cannot modify. I'm trusting that RedGate and the released source from Microsoft are accurate. #hesaiddubiously
internal class ProcessRequestAsyncResult<TContract> : TransactedAsyncResult
{
public ProcessRequestAsyncResult(RoutingService service, Message message, AsyncCallback callback, object state) : base(callback, state)
{
this.allCompletedSync = true;
this.service = service;
this.messageRpc = new System.ServiceModel.Routing.MessageRpc(message, OperationContext.Current, service.ChannelExtension.ImpersonationRequired);
if (TD.RoutingServiceProcessingMessageIsEnabled())
{
TD.RoutingServiceProcessingMessage(this.messageRpc.EventTraceActivity, this.messageRpc.UniqueID, message.Headers.Action, this.messageRpc.OperationContext.EndpointDispatcher.EndpointAddress.Uri.ToString(), (this.messageRpc.Transaction != null) ? "True" : "False");
}
try
{
EndpointNameMessageFilter.Set(this.messageRpc.Message.Properties, service.ChannelExtension.EndpointName);
this.messageRpc.RouteToSingleEndpoint<TContract>(this.service.RoutingConfig);
}
catch (MultipleFilterMatchesException exception)
{
throw System.ServiceModel.Routing.FxTrace.Exception.AsError(new ConfigurationErrorsException(System.ServiceModel.Routing.SR.ReqReplyMulticastNotSupported(this.messageRpc.OperationContext.Channel.LocalAddress), exception));
}
while (this.StartProcessing())
{
}
}
private bool StartProcessing()
{
bool flag = false;
SendOperation operation = this.messageRpc.Operations[0];
this.currentClient = this.service.GetOrCreateClient<TContract>(operation.CurrentEndpoint, this.messageRpc.Impersonating);
if (TD.RoutingServiceTransmittingMessageIsEnabled())
{
TD.RoutingServiceTransmittingMessage(this.messageRpc.EventTraceActivity, this.messageRpc.UniqueID, "0", this.currentClient.Key.ToString());
}
try
{
Message message;
if ((this.messageRpc.Transaction != null) && operation.HasAlternate)
{
throw System.ServiceModel.Routing.FxTrace.Exception.AsError(new ConfigurationErrorsException(System.ServiceModel.Routing.SR.ErrorHandlingNotSupportedReqReplyTxn(this.messageRpc.OperationContext.Channel.LocalAddress)));
}
if (operation.AlternateEndpointCount > 0)
{
message = this.messageRpc.CreateBuffer().CreateMessage();
}
else
{
message = this.messageRpc.Message;
}
operation.PrepareMessage(message);
IAsyncResult result = null;
using (base.PrepareTransactionalCall(this.messageRpc.Transaction))
{
using (IDisposable disposable = null)
{
try
{
}
finally
{
disposable = this.messageRpc.PrepareCall();
}
result = this.currentClient.BeginOperation(message, this.messageRpc.Transaction, base.PrepareAsyncCompletion(ProcessRequestAsyncResult<TContract>.operationCallback), this);
}
}
if (!base.CheckSyncContinue(result))
{
return flag;
}
if (this.OperationComplete(result))
{
base.Complete(this.allCompletedSync);
return flag;
}
return true;
}
catch (Exception exception)
{
if (Fx.IsFatal(exception))
{
throw;
}
if (!this.HandleClientOperationFailure(exception))
{
throw;
}
return true;
}
}
}
To my superficial reading, it appears to me that the ProcessRequestAsyncResult is doing the work of stepping through the backup list via the ProcessRequestAsyncResult.StartProcessing method. However, StartProcess() doesn't appear to be throwing every single exception, but rather it selectively chooses whether or not to throw exceptions.
It seems that only the exception for the final dead address is actually being thrown by StartProcess(), then passed up by the RoutingService.BeginProcessRequest catch clause, only to then finally make it all the way to an activation in my IErrorHandler implementation.
This strongly suggests to me that what I'm trying to do here cannot be done with the current implementation of the System.ServiceModel.Routing namespace. Note that RoutingService is a sealed class, so I cannot extend it with a base class of my own to change this behaviour even if I thought that was a good idea (which I don't).
But then again, note that this is a superficial reading. I could easily be wrong. In fact, I would very much like to be proven wrong. I'd vastly prefer to find a way to get the RoutingService to do what I want it to do rather than having to roll my own.
回答1:
WCF provides error handling (http://msdn.microsoft.com/en-us/library/ee517422.aspx), so you could create a function that activates on a CommunicationException (http://msdn.microsoft.com/en-us/library/system.servicemodel.communicationexception.aspx) and logs the error codes from the data passed to the function. You could the go from there to a mail rooting service and whatever else you need.
来源:https://stackoverflow.com/questions/17738305/wcf-routing-service-dynamic-error-handling