1
0
mirror of synced 2024-11-22 01:13:49 +00:00

Added request/response validation for non-yieldpoint methods

Added ExceptionStrategy
Removed PublishExchange, fixed default ExchangeStrategy
This commit is contained in:
Mark van Renswoude 2017-02-07 16:13:33 +01:00
parent c04f7bd0fe
commit b980e308d1
16 changed files with 190 additions and 121 deletions

View File

@ -7,7 +7,6 @@ namespace Tapeti.Config
{ {
public interface IConfig public interface IConfig
{ {
string SubscribeExchange { get; }
IDependencyResolver DependencyResolver { get; } IDependencyResolver DependencyResolver { get; }
IReadOnlyList<IMessageMiddleware> MessageMiddleware { get; } IReadOnlyList<IMessageMiddleware> MessageMiddleware { get; }
IEnumerable<IQueue> Queues { get; } IEnumerable<IQueue> Queues { get; }

View File

@ -14,6 +14,7 @@ namespace Tapeti.Connection
private readonly IDependencyResolver dependencyResolver; private readonly IDependencyResolver dependencyResolver;
private readonly IReadOnlyList<IMessageMiddleware> messageMiddleware; private readonly IReadOnlyList<IMessageMiddleware> messageMiddleware;
private readonly List<IBinding> bindings; private readonly List<IBinding> bindings;
private readonly IExceptionStrategy exceptionStrategy;
public TapetiConsumer(TapetiWorker worker, string queueName, IDependencyResolver dependencyResolver, IEnumerable<IBinding> bindings, IReadOnlyList<IMessageMiddleware> messageMiddleware) public TapetiConsumer(TapetiWorker worker, string queueName, IDependencyResolver dependencyResolver, IEnumerable<IBinding> bindings, IReadOnlyList<IMessageMiddleware> messageMiddleware)
@ -23,6 +24,8 @@ namespace Tapeti.Connection
this.dependencyResolver = dependencyResolver; this.dependencyResolver = dependencyResolver;
this.messageMiddleware = messageMiddleware; this.messageMiddleware = messageMiddleware;
this.bindings = bindings.ToList(); this.bindings = bindings.ToList();
exceptionStrategy = dependencyResolver.Resolve<IExceptionStrategy>();
} }
@ -46,47 +49,61 @@ namespace Tapeti.Connection
Properties = properties Properties = properties
}) })
{ {
foreach (var binding in bindings) try
{ {
if (!binding.Accept(context, message).Result) foreach (var binding in bindings)
continue; {
if (!binding.Accept(context, message).Result)
continue;
context.Controller = dependencyResolver.Resolve(binding.Controller); context.Controller = dependencyResolver.Resolve(binding.Controller);
context.Binding = binding; context.Binding = binding;
// ReSharper disable AccessToDisposedClosure - MiddlewareHelper will not keep a reference to the lambdas // ReSharper disable AccessToDisposedClosure - MiddlewareHelper will not keep a reference to the lambdas
MiddlewareHelper.GoAsync( MiddlewareHelper.GoAsync(
binding.MessageMiddleware != null binding.MessageMiddleware != null
? messageMiddleware.Concat(binding.MessageMiddleware).ToList() ? messageMiddleware.Concat(binding.MessageMiddleware).ToList()
: messageMiddleware, : messageMiddleware,
async (handler, next) => await handler.Handle(context, next), async (handler, next) => await handler.Handle(context, next),
() => binding.Invoke(context, message) () => binding.Invoke(context, message)
).Wait(); ).Wait();
// ReSharper restore AccessToDisposedClosure // ReSharper restore AccessToDisposedClosure
validMessageType = true; validMessageType = true;
}
if (!validMessageType)
throw new ArgumentException($"Unsupported message type: {message.GetType().FullName}");
}
catch (Exception e)
{
worker.Respond(deliveryTag, exceptionStrategy.HandleException(context, UnwrapException(e)));
} }
} }
if (!validMessageType)
throw new ArgumentException($"Unsupported message type: {message.GetType().FullName}");
worker.Respond(deliveryTag, ConsumeResponse.Ack); worker.Respond(deliveryTag, ConsumeResponse.Ack);
} }
catch (Exception e) catch (Exception e)
{ {
// TODO allow different exception handling depending on exception type worker.Respond(deliveryTag, exceptionStrategy.HandleException(null, UnwrapException(e)));
worker.Respond(deliveryTag, ConsumeResponse.Requeue);
var aggregateException = e as AggregateException;
if (aggregateException != null && aggregateException.InnerExceptions.Count == 1)
throw aggregateException.InnerExceptions[0];
throw;
} }
} }
private static Exception UnwrapException(Exception exception)
{
// In async/await style code this is handled similarly. For synchronous
// code using Tasks we have to unwrap these ourselves to get the proper
// exception directly instead of "Errors occured". We might lose
// some stack traces in the process though.
var aggregateException = exception as AggregateException;
if (aggregateException != null && aggregateException.InnerExceptions.Count == 1)
throw aggregateException.InnerExceptions[0];
return UnwrapException(exception);
}
protected class MessageContext : IMessageContext protected class MessageContext : IMessageContext
{ {
public IDependencyResolver DependencyResolver { get; set; } public IDependencyResolver DependencyResolver { get; set; }

View File

@ -12,12 +12,12 @@ namespace Tapeti.Connection
public class TapetiWorker public class TapetiWorker
{ {
public TapetiConnectionParams ConnectionParams { get; set; } public TapetiConnectionParams ConnectionParams { get; set; }
public string SubscribeExchange { get; set; }
private readonly IDependencyResolver dependencyResolver; private readonly IDependencyResolver dependencyResolver;
private readonly IReadOnlyList<IMessageMiddleware> messageMiddleware; private readonly IReadOnlyList<IMessageMiddleware> messageMiddleware;
private readonly IMessageSerializer messageSerializer; private readonly IMessageSerializer messageSerializer;
private readonly IRoutingKeyStrategy routingKeyStrategy; private readonly IRoutingKeyStrategy routingKeyStrategy;
private readonly IExchangeStrategy exchangeStrategy;
private readonly Lazy<SingleThreadTaskQueue> taskQueue = new Lazy<SingleThreadTaskQueue>(); private readonly Lazy<SingleThreadTaskQueue> taskQueue = new Lazy<SingleThreadTaskQueue>();
private RabbitMQ.Client.IConnection connection; private RabbitMQ.Client.IConnection connection;
private IModel channelInstance; private IModel channelInstance;
@ -27,15 +27,16 @@ namespace Tapeti.Connection
{ {
this.dependencyResolver = dependencyResolver; this.dependencyResolver = dependencyResolver;
this.messageMiddleware = messageMiddleware; this.messageMiddleware = messageMiddleware;
messageSerializer = dependencyResolver.Resolve<IMessageSerializer>(); messageSerializer = dependencyResolver.Resolve<IMessageSerializer>();
routingKeyStrategy = dependencyResolver.Resolve<IRoutingKeyStrategy>(); routingKeyStrategy = dependencyResolver.Resolve<IRoutingKeyStrategy>();
exchangeStrategy = dependencyResolver.Resolve<IExchangeStrategy>();
} }
public Task Publish(object message, IBasicProperties properties) public Task Publish(object message, IBasicProperties properties)
{ {
// TODO use exchange strategy! return Publish(message, properties, exchangeStrategy.GetExchange(message.GetType()), routingKeyStrategy.GetRoutingKey(message.GetType()));
return Publish(message, properties, SubscribeExchange, routingKeyStrategy.GetRoutingKey(message.GetType()));
} }
@ -67,7 +68,7 @@ namespace Tapeti.Connection
foreach (var binding in queue.Bindings) foreach (var binding in queue.Bindings)
{ {
var routingKey = routingKeyStrategy.GetRoutingKey(binding.MessageClass); var routingKey = routingKeyStrategy.GetRoutingKey(binding.MessageClass);
channel.QueueBind(dynamicQueue.QueueName, SubscribeExchange, routingKey); channel.QueueBind(dynamicQueue.QueueName, exchangeStrategy.GetExchange(binding.MessageClass), routingKey);
(binding as IDynamicQueueBinding)?.SetQueueName(dynamicQueue.QueueName); (binding as IDynamicQueueBinding)?.SetQueueName(dynamicQueue.QueueName);
} }

View File

@ -5,15 +5,9 @@ namespace Tapeti.Default
{ {
public class NamespaceMatchExchangeStrategy : IExchangeStrategy public class NamespaceMatchExchangeStrategy : IExchangeStrategy
{ {
public const string DefaultFormat = "^Messaging\\.(.[^\\.]+)"; // If the namespace starts with "Messaging.Service[.Optional.Further.Parts]", the exchange will be "Service".
// If no Messaging prefix is present, the first part of the namespace will be used instead.
private readonly Regex namespaceRegEx; private static readonly Regex NamespaceRegex = new Regex("^(Messaging\\.)?(?<exchange>[^\\.]+)", RegexOptions.Compiled | RegexOptions.Singleline);
public NamespaceMatchExchangeStrategy()
{
namespaceRegEx = new Regex(DefaultFormat, RegexOptions.Compiled | RegexOptions.Singleline);
}
public string GetExchange(Type messageType) public string GetExchange(Type messageType)
@ -21,11 +15,11 @@ namespace Tapeti.Default
if (messageType.Namespace == null) if (messageType.Namespace == null)
throw new ArgumentException($"{messageType.FullName} does not have a namespace"); throw new ArgumentException($"{messageType.FullName} does not have a namespace");
var match = namespaceRegEx.Match(messageType.Namespace); var match = NamespaceRegex.Match(messageType.Namespace);
if (!match.Success) if (!match.Success)
throw new ArgumentException($"Namespace for {messageType.FullName} does not match the specified format"); throw new ArgumentException($"Namespace for {messageType.FullName} does not match the specified format");
return match.Groups[1].Value.ToLower(); return match.Groups["exchange"].Value.ToLower();
} }
} }
} }

View File

@ -0,0 +1,14 @@
using System;
using Tapeti.Config;
namespace Tapeti.Default
{
public class RequeueExceptionStrategy : IExceptionStrategy
{
public ConsumeResponse HandleException(IMessageContext context, Exception exception)
{
// TODO log exception
return ConsumeResponse.Requeue;
}
}
}

16
IExceptionStrategy.cs Normal file
View File

@ -0,0 +1,16 @@
using System;
using Tapeti.Config;
namespace Tapeti
{
public interface IExceptionStrategy
{
/// <summary>
/// Called when an exception occurs while handling a message.
/// </summary>
/// <param name="context">The message context if available. May be null!</param>
/// <param name="exception">The exception instance</param>
/// <returns>The ConsumeResponse to determine whether to requeue, dead-letter (nack) or simply ack the message.</returns>
ConsumeResponse HandleException(IMessageContext context, Exception exception);
}
}

View File

@ -0,0 +1,73 @@
using System;
using System.Reflection;
using System.Threading.Tasks;
using Tapeti.Config;
using Tapeti.Flow.Annotations;
using Tapeti.Helpers;
namespace Tapeti.Flow.Default
{
// TODO figure out a way to prevent binding on Continuation methods (which are always the target of a direct response)
internal class FlowBindingMiddleware : IBindingMiddleware
{
public void Handle(IBindingContext context, Action next)
{
RegisterContinuationFilter(context);
RegisterYieldPointResult(context);
next();
ValidateRequestResponse(context);
}
private static void RegisterContinuationFilter(IBindingContext context)
{
var continuationAttribute = context.Method.GetCustomAttribute<ContinuationAttribute>();
if (continuationAttribute == null)
return;
context.Use(new FlowBindingFilter());
context.Use(new FlowMessageMiddleware());
}
private static void RegisterYieldPointResult(IBindingContext context)
{
bool isTask;
if (!context.Result.Info.ParameterType.IsTypeOrTaskOf(typeof(IYieldPoint), out isTask))
return;
if (isTask)
{
context.Result.SetHandler(async (messageContext, value) =>
{
var yieldPoint = await (Task<IYieldPoint>)value;
if (yieldPoint != null)
await HandleYieldPoint(messageContext, yieldPoint);
});
}
else
context.Result.SetHandler((messageContext, value) => HandleYieldPoint(messageContext, (IYieldPoint)value));
}
private static Task HandleYieldPoint(IMessageContext context, IYieldPoint yieldPoint)
{
var flowHandler = context.DependencyResolver.Resolve<IFlowHandler>();
return flowHandler.Execute(context, yieldPoint);
}
private static void ValidateRequestResponse(IBindingContext context)
{
var request = context.MessageClass?.GetCustomAttribute<RequestAttribute>();
if (request?.Response == null)
return;
bool isTask;
if (!context.Result.Info.ParameterType.IsTypeOrTaskOf(t => t == request.Response || t == typeof(IYieldPoint), out isTask))
throw new ResponseExpectedException($"Response of class {request.Response.FullName} expected in controller {context.Method.DeclaringType?.FullName}, method {context.Method.Name}");
}
}
}

View File

@ -1,11 +1,6 @@
using System; using System.Collections.Generic;
using System.Collections.Generic;
using System.Reflection;
using System.Threading.Tasks;
using Tapeti.Config; using Tapeti.Config;
using Tapeti.Flow.Annotations;
using Tapeti.Flow.Default; using Tapeti.Flow.Default;
using Tapeti.Helpers;
namespace Tapeti.Flow namespace Tapeti.Flow
{ {
@ -26,55 +21,5 @@ namespace Tapeti.Flow
return new[] { new FlowBindingMiddleware() }; return new[] { new FlowBindingMiddleware() };
} }
internal class FlowBindingMiddleware : IBindingMiddleware
{
public void Handle(IBindingContext context, Action next)
{
RegisterContinuationFilter(context);
RegisterYieldPointResult(context);
next();
}
private static void RegisterContinuationFilter(IBindingContext context)
{
var continuationAttribute = context.Method.GetCustomAttribute<ContinuationAttribute>();
if (continuationAttribute == null)
return;
context.Use(new FlowBindingFilter());
context.Use(new FlowMessageMiddleware());
}
private static void RegisterYieldPointResult(IBindingContext context)
{
bool isTask;
if (!context.Result.Info.ParameterType.IsTypeOrTaskOf(typeof(IYieldPoint), out isTask))
return;
if (isTask)
{
context.Result.SetHandler(async (messageContext, value) =>
{
var yieldPoint = await (Task<IYieldPoint>)value;
if (yieldPoint != null)
await HandleYieldPoint(messageContext, yieldPoint);
});
}
else
context.Result.SetHandler((messageContext, value) => HandleYieldPoint(messageContext, (IYieldPoint)value));
}
private static Task HandleYieldPoint(IMessageContext context, IYieldPoint yieldPoint)
{
var flowHandler = context.DependencyResolver.Resolve<IFlowHandler>();
return flowHandler.Execute(context, yieldPoint);
}
}
} }
} }

View File

@ -0,0 +1,9 @@
using System;
namespace Tapeti.Flow
{
public class ResponseExpectedException : Exception
{
public ResponseExpectedException(string message) : base(message) { }
}
}

View File

@ -53,6 +53,7 @@
<Compile Include="Annotations\RequestAttribute.cs" /> <Compile Include="Annotations\RequestAttribute.cs" />
<Compile Include="ContextItems.cs" /> <Compile Include="ContextItems.cs" />
<Compile Include="Default\FlowBindingFilter.cs" /> <Compile Include="Default\FlowBindingFilter.cs" />
<Compile Include="Default\FlowBindingMiddleware.cs" />
<Compile Include="Default\FlowContext.cs" /> <Compile Include="Default\FlowContext.cs" />
<Compile Include="Default\FlowMessageMiddleware.cs" /> <Compile Include="Default\FlowMessageMiddleware.cs" />
<Compile Include="Default\FlowState.cs" /> <Compile Include="Default\FlowState.cs" />
@ -67,6 +68,7 @@
<Compile Include="IFlowStore.cs" /> <Compile Include="IFlowStore.cs" />
<Compile Include="IFlowProvider.cs" /> <Compile Include="IFlowProvider.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="ResponseExpectedException.cs" />
<Compile Include="YieldPointException.cs" /> <Compile Include="YieldPointException.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -63,11 +63,13 @@
<Compile Include="Default\JsonMessageSerializer.cs" /> <Compile Include="Default\JsonMessageSerializer.cs" />
<Compile Include="Default\PublishResultBinding.cs" /> <Compile Include="Default\PublishResultBinding.cs" />
<Compile Include="Default\NamespaceMatchExchangeStrategy.cs" /> <Compile Include="Default\NamespaceMatchExchangeStrategy.cs" />
<Compile Include="Default\RequeueExceptionStrategy.cs" />
<Compile Include="Default\TypeNameRoutingKeyStrategy.cs" /> <Compile Include="Default\TypeNameRoutingKeyStrategy.cs" />
<Compile Include="Helpers\ConsoleHelper.cs" /> <Compile Include="Helpers\ConsoleHelper.cs" />
<Compile Include="Helpers\MiddlewareHelper.cs" /> <Compile Include="Helpers\MiddlewareHelper.cs" />
<Compile Include="Helpers\TaskTypeHelper.cs" /> <Compile Include="Helpers\TaskTypeHelper.cs" />
<Compile Include="IConnection.cs" /> <Compile Include="IConnection.cs" />
<Compile Include="IExceptionStrategy.cs" />
<Compile Include="IExchangeStrategy.cs" /> <Compile Include="IExchangeStrategy.cs" />
<Compile Include="ILogger.cs" /> <Compile Include="ILogger.cs" />
<Compile Include="Config\IMessageContext.cs" /> <Compile Include="Config\IMessageContext.cs" />
@ -78,7 +80,7 @@
<Compile Include="Config\IBindingMiddleware.cs" /> <Compile Include="Config\IBindingMiddleware.cs" />
<Compile Include="TapetiConnectionParams.cs" /> <Compile Include="TapetiConnectionParams.cs" />
<Compile Include="TapetiConfig.cs" /> <Compile Include="TapetiConfig.cs" />
<Compile Include="TapetiTypes.cs" /> <Compile Include="ConsumeResponse.cs" />
<Compile Include="Tasks\SingleThreadTaskQueue.cs" /> <Compile Include="Tasks\SingleThreadTaskQueue.cs" />
<Compile Include="IDependencyResolver.cs" /> <Compile Include="IDependencyResolver.cs" />
<Compile Include="IMessageSerializer.cs" /> <Compile Include="IMessageSerializer.cs" />

View File

@ -27,13 +27,11 @@ namespace Tapeti
private readonly List<IBindingMiddleware> bindingMiddleware = new List<IBindingMiddleware>(); private readonly List<IBindingMiddleware> bindingMiddleware = new List<IBindingMiddleware>();
private readonly List<IMessageMiddleware> messageMiddleware = new List<IMessageMiddleware>(); private readonly List<IMessageMiddleware> messageMiddleware = new List<IMessageMiddleware>();
private readonly string subscribeExchange;
private readonly IDependencyResolver dependencyResolver; private readonly IDependencyResolver dependencyResolver;
public TapetiConfig(string subscribeExchange, IDependencyResolver dependencyResolver) public TapetiConfig(IDependencyResolver dependencyResolver)
{ {
this.subscribeExchange = subscribeExchange;
this.dependencyResolver = dependencyResolver; this.dependencyResolver = dependencyResolver;
Use(new DependencyResolverBinding()); Use(new DependencyResolverBinding());
@ -63,7 +61,7 @@ namespace Tapeti
queues.AddRange(dynamicBindings.Select(bl => new Queue(new QueueInfo { Dynamic = true }, bl))); queues.AddRange(dynamicBindings.Select(bl => new Queue(new QueueInfo { Dynamic = true }, bl)));
var config = new Config(subscribeExchange, dependencyResolver, messageMiddleware, queues); var config = new Config(dependencyResolver, messageMiddleware, queues);
(dependencyResolver as IDependencyContainer)?.RegisterDefaultSingleton<IConfig>(config); (dependencyResolver as IDependencyContainer)?.RegisterDefaultSingleton<IConfig>(config);
return config; return config;
@ -104,17 +102,18 @@ namespace Tapeti
public void RegisterDefaults() public void RegisterDefaults()
{ {
var container = dependencyResolver as IDependencyContainer; var container = dependencyResolver as IDependencyContainer;
if (container != null) if (container == null)
{ return;
if (ConsoleHelper.IsAvailable())
container.RegisterDefault<ILogger, ConsoleLogger>();
else
container.RegisterDefault<ILogger, DevNullLogger>();
container.RegisterDefault<IMessageSerializer, JsonMessageSerializer>(); if (ConsoleHelper.IsAvailable())
container.RegisterDefault<IExchangeStrategy, NamespaceMatchExchangeStrategy>(); container.RegisterDefault<ILogger, ConsoleLogger>();
container.RegisterDefault<IRoutingKeyStrategy, TypeNameRoutingKeyStrategy>(); else
} container.RegisterDefault<ILogger, DevNullLogger>();
container.RegisterDefault<IMessageSerializer, JsonMessageSerializer>();
container.RegisterDefault<IExchangeStrategy, NamespaceMatchExchangeStrategy>();
container.RegisterDefault<IRoutingKeyStrategy, TypeNameRoutingKeyStrategy>();
container.RegisterDefault<IExceptionStrategy, RequeueExceptionStrategy>();
} }
@ -310,7 +309,6 @@ namespace Tapeti
protected class Config : IConfig protected class Config : IConfig
{ {
public string SubscribeExchange { get; }
public IDependencyResolver DependencyResolver { get; } public IDependencyResolver DependencyResolver { get; }
public IReadOnlyList<IMessageMiddleware> MessageMiddleware { get; } public IReadOnlyList<IMessageMiddleware> MessageMiddleware { get; }
public IEnumerable<IQueue> Queues { get; } public IEnumerable<IQueue> Queues { get; }
@ -318,9 +316,8 @@ namespace Tapeti
private readonly Dictionary<MethodInfo, IBinding> bindingMethodLookup; private readonly Dictionary<MethodInfo, IBinding> bindingMethodLookup;
public Config(string subscribeExchange, IDependencyResolver dependencyResolver, IReadOnlyList<IMessageMiddleware> messageMiddleware, IEnumerable<IQueue> queues) public Config(IDependencyResolver dependencyResolver, IReadOnlyList<IMessageMiddleware> messageMiddleware, IEnumerable<IQueue> queues)
{ {
SubscribeExchange = subscribeExchange;
DependencyResolver = dependencyResolver; DependencyResolver = dependencyResolver;
MessageMiddleware = messageMiddleware; MessageMiddleware = messageMiddleware;
Queues = queues.ToList(); Queues = queues.ToList();

View File

@ -20,8 +20,7 @@ namespace Tapeti
worker = new Lazy<TapetiWorker>(() => new TapetiWorker(config.DependencyResolver, config.MessageMiddleware) worker = new Lazy<TapetiWorker>(() => new TapetiWorker(config.DependencyResolver, config.MessageMiddleware)
{ {
ConnectionParams = Params ?? new TapetiConnectionParams(), ConnectionParams = Params ?? new TapetiConnectionParams()
SubscribeExchange = config.SubscribeExchange
}); });
} }

View File

@ -62,8 +62,6 @@ namespace Test
* This will automatically include the correlationId in the response and * This will automatically include the correlationId in the response and
* use the replyTo header of the request if provided. * use the replyTo header of the request if provided.
*/ */
// TODO validation middleware to ensure a request message returns the specified response (already done for IYieldPoint methods)
public PoloConfirmationResponseMessage PoloConfirmation(PoloConfirmationRequestMessage message) public PoloConfirmationResponseMessage PoloConfirmation(PoloConfirmationRequestMessage message)
{ {
Console.WriteLine(">> PoloConfirmation (returning confirmation)"); Console.WriteLine(">> PoloConfirmation (returning confirmation)");

View File

@ -10,11 +10,14 @@ namespace Test
{ {
private static void Main() private static void Main()
{ {
// TODO SQL based flow store
// TODO logging
var container = new Container(); var container = new Container();
container.Register<MarcoEmitter>(); container.Register<MarcoEmitter>();
container.Register<Visualizer>(); container.Register<Visualizer>();
var config = new TapetiConfig("test", new SimpleInjectorDependencyResolver(container)) var config = new TapetiConfig(new SimpleInjectorDependencyResolver(container))
.WithFlow() .WithFlow()
.RegisterAllControllers() .RegisterAllControllers()
.Build(); .Build();