First working prototype of the flow port
This commit is contained in:
parent
a2970b6893
commit
c04f7bd0fe
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,3 +2,4 @@
|
|||||||
bin/
|
bin/
|
||||||
obj/
|
obj/
|
||||||
packages/
|
packages/
|
||||||
|
*.user
|
||||||
|
@ -7,7 +7,7 @@ namespace Tapeti.Config
|
|||||||
{
|
{
|
||||||
public interface IConfig
|
public interface IConfig
|
||||||
{
|
{
|
||||||
string Exchange { get; }
|
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; }
|
||||||
@ -33,9 +33,10 @@ namespace Tapeti.Config
|
|||||||
string QueueName { get; }
|
string QueueName { get; }
|
||||||
|
|
||||||
IReadOnlyList<IMessageMiddleware> MessageMiddleware { get; }
|
IReadOnlyList<IMessageMiddleware> MessageMiddleware { get; }
|
||||||
|
IReadOnlyList<IBindingFilter> BindingFilters { get; }
|
||||||
|
|
||||||
bool Accept(object message);
|
Task<bool> Accept(IMessageContext context, object message);
|
||||||
Task<object> Invoke(IMessageContext context, object message);
|
Task Invoke(IMessageContext context, object message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Reflection;
|
||||||
using RabbitMQ.Client;
|
using RabbitMQ.Client;
|
||||||
|
|
||||||
namespace Tapeti.Config
|
namespace Tapeti.Config
|
||||||
@ -19,5 +20,10 @@ namespace Tapeti.Config
|
|||||||
/// Controller will be null when passed to an IBindingFilter
|
/// Controller will be null when passed to an IBindingFilter
|
||||||
/// </summary>
|
/// </summary>
|
||||||
object Controller { get; }
|
object Controller { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Binding will be null when passed to an IBindingFilter
|
||||||
|
/// </summary>
|
||||||
|
IBinding Binding { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,37 +36,36 @@ namespace Tapeti.Connection
|
|||||||
throw new ArgumentException("Empty message");
|
throw new ArgumentException("Empty message");
|
||||||
|
|
||||||
var validMessageType = false;
|
var validMessageType = false;
|
||||||
foreach (var binding in bindings.Where(b => b.Accept(message)))
|
|
||||||
|
using (var context = new MessageContext
|
||||||
{
|
{
|
||||||
using (var context = new MessageContext
|
DependencyResolver = dependencyResolver,
|
||||||
{
|
Queue = queueName,
|
||||||
DependencyResolver = dependencyResolver,
|
RoutingKey = routingKey,
|
||||||
Controller = dependencyResolver.Resolve(binding.Controller),
|
Message = message,
|
||||||
Queue = queueName,
|
Properties = properties
|
||||||
RoutingKey = routingKey,
|
})
|
||||||
Message = message,
|
{
|
||||||
Properties = properties
|
foreach (var binding in bindings)
|
||||||
})
|
|
||||||
{
|
{
|
||||||
|
if (!binding.Accept(context, message).Result)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
context.Controller = dependencyResolver.Resolve(binding.Controller);
|
||||||
|
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),
|
||||||
async () =>
|
() => binding.Invoke(context, message)
|
||||||
{
|
|
||||||
var result = binding.Invoke(context, message).Result;
|
|
||||||
|
|
||||||
// TODO change to result handler
|
|
||||||
if (result != null)
|
|
||||||
await worker.Publish(result, null);
|
|
||||||
}
|
|
||||||
).Wait();
|
).Wait();
|
||||||
// ReSharper restore AccessToDisposedClosure
|
// ReSharper restore AccessToDisposedClosure
|
||||||
}
|
|
||||||
|
|
||||||
validMessageType = true;
|
validMessageType = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!validMessageType)
|
if (!validMessageType)
|
||||||
@ -74,9 +73,15 @@ namespace Tapeti.Connection
|
|||||||
|
|
||||||
worker.Respond(deliveryTag, ConsumeResponse.Ack);
|
worker.Respond(deliveryTag, ConsumeResponse.Ack);
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
// TODO allow different exception handling depending on exception type
|
||||||
worker.Respond(deliveryTag, ConsumeResponse.Requeue);
|
worker.Respond(deliveryTag, ConsumeResponse.Requeue);
|
||||||
|
|
||||||
|
var aggregateException = e as AggregateException;
|
||||||
|
if (aggregateException != null && aggregateException.InnerExceptions.Count == 1)
|
||||||
|
throw aggregateException.InnerExceptions[0];
|
||||||
|
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -87,6 +92,7 @@ namespace Tapeti.Connection
|
|||||||
public IDependencyResolver DependencyResolver { get; set; }
|
public IDependencyResolver DependencyResolver { get; set; }
|
||||||
|
|
||||||
public object Controller { get; set; }
|
public object Controller { get; set; }
|
||||||
|
public IBinding Binding { get; set; }
|
||||||
|
|
||||||
public string Queue { get; set; }
|
public string Queue { get; set; }
|
||||||
public string RoutingKey { get; set; }
|
public string RoutingKey { get; set; }
|
||||||
|
@ -4,7 +4,7 @@ using RabbitMQ.Client;
|
|||||||
|
|
||||||
namespace Tapeti.Connection
|
namespace Tapeti.Connection
|
||||||
{
|
{
|
||||||
public class TapetiPublisher : IAdvancedPublisher
|
public class TapetiPublisher : IInternalPublisher
|
||||||
{
|
{
|
||||||
private readonly Func<TapetiWorker> workerFactory;
|
private readonly Func<TapetiWorker> workerFactory;
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ namespace Tapeti.Connection
|
|||||||
public class TapetiWorker
|
public class TapetiWorker
|
||||||
{
|
{
|
||||||
public TapetiConnectionParams ConnectionParams { get; set; }
|
public TapetiConnectionParams ConnectionParams { get; set; }
|
||||||
public string Exchange { 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;
|
||||||
@ -34,7 +34,8 @@ namespace Tapeti.Connection
|
|||||||
|
|
||||||
public Task Publish(object message, IBasicProperties properties)
|
public Task Publish(object message, IBasicProperties properties)
|
||||||
{
|
{
|
||||||
return Publish(message, properties, Exchange, routingKeyStrategy.GetRoutingKey(message.GetType()));
|
// TODO use exchange strategy!
|
||||||
|
return Publish(message, properties, SubscribeExchange, routingKeyStrategy.GetRoutingKey(message.GetType()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -66,7 +67,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, Exchange, routingKey);
|
channel.QueueBind(dynamicQueue.QueueName, SubscribeExchange, routingKey);
|
||||||
|
|
||||||
(binding as IDynamicQueueBinding)?.SetQueueName(dynamicQueue.QueueName);
|
(binding as IDynamicQueueBinding)?.SetQueueName(dynamicQueue.QueueName);
|
||||||
}
|
}
|
||||||
@ -173,6 +174,9 @@ namespace Tapeti.Connection
|
|||||||
connection = connectionFactory.CreateConnection();
|
connection = connectionFactory.CreateConnection();
|
||||||
channelInstance = connection.CreateModel();
|
channelInstance = connection.CreateModel();
|
||||||
|
|
||||||
|
if (ConnectionParams.PrefetchCount > 0)
|
||||||
|
channelInstance.BasicQos(0, ConnectionParams.PrefetchCount, false);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
catch (BrokerUnreachableException)
|
catch (BrokerUnreachableException)
|
||||||
|
@ -10,9 +10,9 @@ namespace Tapeti.Default
|
|||||||
private readonly Regex namespaceRegEx;
|
private readonly Regex namespaceRegEx;
|
||||||
|
|
||||||
|
|
||||||
public NamespaceMatchExchangeStrategy(string namespaceFormat = DefaultFormat)
|
public NamespaceMatchExchangeStrategy()
|
||||||
{
|
{
|
||||||
namespaceRegEx = new Regex(namespaceFormat, RegexOptions.Compiled | RegexOptions.Singleline);
|
namespaceRegEx = new Regex(DefaultFormat, RegexOptions.Compiled | RegexOptions.Singleline);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
53
Default/PublishResultBinding.cs
Normal file
53
Default/PublishResultBinding.cs
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using RabbitMQ.Client.Framing;
|
||||||
|
using Tapeti.Config;
|
||||||
|
using Tapeti.Helpers;
|
||||||
|
|
||||||
|
namespace Tapeti.Default
|
||||||
|
{
|
||||||
|
public class PublishResultBinding : IBindingMiddleware
|
||||||
|
{
|
||||||
|
public void Handle(IBindingContext context, Action next)
|
||||||
|
{
|
||||||
|
next();
|
||||||
|
|
||||||
|
if (context.Result.HasHandler)
|
||||||
|
return;
|
||||||
|
|
||||||
|
bool isTask;
|
||||||
|
if (context.Result.Info.ParameterType.IsTypeOrTaskOf(t => t.IsClass, out isTask))
|
||||||
|
{
|
||||||
|
if (isTask)
|
||||||
|
{
|
||||||
|
context.Result.SetHandler(async (messageContext, value) =>
|
||||||
|
{
|
||||||
|
var message = await (Task<object>)value;
|
||||||
|
if (message != null)
|
||||||
|
await Reply(message, messageContext);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
context.Result.SetHandler((messageContext, value) =>
|
||||||
|
value == null ? null : Reply(value, messageContext));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private Task Reply(object message, IMessageContext messageContext)
|
||||||
|
{
|
||||||
|
var publisher = (IInternalPublisher)messageContext.DependencyResolver.Resolve<IPublisher>();
|
||||||
|
var properties = new BasicProperties();
|
||||||
|
|
||||||
|
// Only set the property if it's not null, otherwise a string reference exception can occur:
|
||||||
|
// http://rabbitmq.1065348.n5.nabble.com/SocketException-when-invoking-model-BasicPublish-td36330.html
|
||||||
|
if (messageContext.Properties.IsCorrelationIdPresent())
|
||||||
|
properties.CorrelationId = messageContext.Properties.CorrelationId;
|
||||||
|
|
||||||
|
if (messageContext.Properties.IsReplyToPresent())
|
||||||
|
return publisher.PublishDirect(message, messageContext.Properties.ReplyTo, properties);
|
||||||
|
|
||||||
|
return publisher.Publish(message, properties);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics.Eventing.Reader;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Tapeti.Helpers
|
namespace Tapeti.Helpers
|
||||||
|
28
Helpers/TaskTypeHelper.cs
Normal file
28
Helpers/TaskTypeHelper.cs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Tapeti.Helpers
|
||||||
|
{
|
||||||
|
public static class TaskTypeHelper
|
||||||
|
{
|
||||||
|
public static bool IsTypeOrTaskOf(this Type type, Func<Type, bool> predicate, out bool isTask)
|
||||||
|
{
|
||||||
|
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Task<>))
|
||||||
|
{
|
||||||
|
isTask = true;
|
||||||
|
|
||||||
|
var genericArguments = type.GetGenericArguments();
|
||||||
|
return genericArguments.Length == 1 && predicate(genericArguments[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
isTask = false;
|
||||||
|
return predicate(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static bool IsTypeOrTaskOf(this Type type, Type compareTo, out bool isTask)
|
||||||
|
{
|
||||||
|
return IsTypeOrTaskOf(type, t => t == compareTo, out isTask);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using Tapeti.Config;
|
|
||||||
|
|
||||||
namespace Tapeti
|
namespace Tapeti
|
||||||
{
|
{
|
||||||
@ -13,8 +12,12 @@ namespace Tapeti
|
|||||||
public interface IDependencyContainer : IDependencyResolver
|
public interface IDependencyContainer : IDependencyResolver
|
||||||
{
|
{
|
||||||
void RegisterDefault<TService, TImplementation>() where TService : class where TImplementation : class, TService;
|
void RegisterDefault<TService, TImplementation>() where TService : class where TImplementation : class, TService;
|
||||||
void RegisterPublisher(Func<IPublisher> publisher);
|
void RegisterDefault<TService>(Func<TService> factory) where TService : class;
|
||||||
void RegisterConfig(IConfig config);
|
|
||||||
|
void RegisterDefaultSingleton<TService, TImplementation>() where TService : class where TImplementation : class, TService;
|
||||||
|
void RegisterDefaultSingleton<TService>(TService instance) where TService : class;
|
||||||
|
void RegisterDefaultSingleton<TService>(Func<TService> factory) where TService : class;
|
||||||
|
|
||||||
void RegisterController(Type type);
|
void RegisterController(Type type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,13 +3,15 @@ using RabbitMQ.Client;
|
|||||||
|
|
||||||
namespace Tapeti
|
namespace Tapeti
|
||||||
{
|
{
|
||||||
|
// Note: Tapeti assumes every implementation of IPublisher can also be cast to an IInternalPublisher.
|
||||||
|
// The distinction is made on purpose to trigger code-smells in non-Tapeti code when casting.
|
||||||
public interface IPublisher
|
public interface IPublisher
|
||||||
{
|
{
|
||||||
Task Publish(object message);
|
Task Publish(object message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public interface IAdvancedPublisher : IPublisher
|
public interface IInternalPublisher : IPublisher
|
||||||
{
|
{
|
||||||
Task Publish(object message, IBasicProperties properties);
|
Task Publish(object message, IBasicProperties properties);
|
||||||
Task PublishDirect(object message, string queueName, IBasicProperties properties);
|
Task PublishDirect(object message, string queueName, IBasicProperties properties);
|
||||||
|
@ -7,9 +7,9 @@ using System.Runtime.InteropServices;
|
|||||||
[assembly: AssemblyTitle("Tapeti")]
|
[assembly: AssemblyTitle("Tapeti")]
|
||||||
[assembly: AssemblyDescription("")]
|
[assembly: AssemblyDescription("")]
|
||||||
[assembly: AssemblyConfiguration("")]
|
[assembly: AssemblyConfiguration("")]
|
||||||
[assembly: AssemblyCompany("Hewlett-Packard Company")]
|
[assembly: AssemblyCompany("")]
|
||||||
[assembly: AssemblyProduct("Tapeti")]
|
[assembly: AssemblyProduct("Tapeti")]
|
||||||
[assembly: AssemblyCopyright("Copyright © Hewlett-Packard Company 2016")]
|
[assembly: AssemblyCopyright("")]
|
||||||
[assembly: AssemblyTrademark("")]
|
[assembly: AssemblyTrademark("")]
|
||||||
[assembly: AssemblyCulture("")]
|
[assembly: AssemblyCulture("")]
|
||||||
|
|
||||||
|
11
Tapeti.Flow/ConfigExtensions.cs
Normal file
11
Tapeti.Flow/ConfigExtensions.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
namespace Tapeti.Flow
|
||||||
|
{
|
||||||
|
public static class ConfigExtensions
|
||||||
|
{
|
||||||
|
public static TapetiConfig WithFlow(this TapetiConfig config)
|
||||||
|
{
|
||||||
|
config.Use(new FlowMiddleware());
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -10,14 +10,10 @@ namespace Tapeti.Flow.Default
|
|||||||
public async Task<bool> Accept(IMessageContext context, IBinding binding)
|
public async Task<bool> Accept(IMessageContext context, IBinding binding)
|
||||||
{
|
{
|
||||||
var flowContext = await GetFlowContext(context);
|
var flowContext = await GetFlowContext(context);
|
||||||
if (flowContext?.FlowState == null)
|
if (flowContext?.ContinuationMetadata == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
string continuation;
|
return flowContext.ContinuationMetadata.MethodName == MethodSerializer.Serialize(binding.Method);
|
||||||
if (!flowContext.FlowState.Continuations.TryGetValue(flowContext.ContinuationID, out continuation))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return continuation == MethodSerializer.Serialize(binding.Method);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -35,27 +31,29 @@ namespace Tapeti.Flow.Default
|
|||||||
|
|
||||||
var flowStore = context.DependencyResolver.Resolve<IFlowStore>();
|
var flowStore = context.DependencyResolver.Resolve<IFlowStore>();
|
||||||
|
|
||||||
var flowStateID = await flowStore.FindFlowStateID(continuationID);
|
var flowID = await flowStore.FindFlowID(continuationID);
|
||||||
if (!flowStateID.HasValue)
|
if (!flowID.HasValue)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var flowStateLock = await flowStore.LockFlowState(flowStateID.Value);
|
var flowStateLock = await flowStore.LockFlowState(flowID.Value);
|
||||||
if (flowStateLock == null)
|
if (flowStateLock == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var flowState = await flowStateLock.GetFlowState();
|
var flowState = await flowStateLock.GetFlowState();
|
||||||
|
if (flowState == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
ContinuationMetadata continuation;
|
||||||
var flowMetadata = flowState != null ? Newtonsoft.Json.JsonConvert.DeserializeObject<FlowMetadata>(flowState.Metadata) : null;
|
|
||||||
//var continuationMetaData = Newtonsoft.Json.JsonConvert.DeserializeObject<ContinuationMetadata>(continuation.MetaData);
|
|
||||||
|
|
||||||
var flowContext = new FlowContext
|
var flowContext = new FlowContext
|
||||||
{
|
{
|
||||||
MessageContext = context,
|
MessageContext = context,
|
||||||
ContinuationID = continuationID,
|
|
||||||
FlowStateLock = flowStateLock,
|
FlowStateLock = flowStateLock,
|
||||||
FlowState = flowState,
|
FlowState = flowState,
|
||||||
Reply = flowMetadata?.Reply
|
|
||||||
|
ContinuationID = continuationID,
|
||||||
|
ContinuationMetadata = flowState.Continuations.TryGetValue(continuationID, out continuation) ? continuation : null
|
||||||
};
|
};
|
||||||
|
|
||||||
// IDisposable items in the IMessageContext are automatically disposed
|
// IDisposable items in the IMessageContext are automatically disposed
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using Tapeti.Config;
|
using Tapeti.Config;
|
||||||
|
|
||||||
namespace Tapeti.Flow.Default
|
namespace Tapeti.Flow.Default
|
||||||
@ -8,37 +9,13 @@ namespace Tapeti.Flow.Default
|
|||||||
public IMessageContext MessageContext { get; set; }
|
public IMessageContext MessageContext { get; set; }
|
||||||
public IFlowStateLock FlowStateLock { get; set; }
|
public IFlowStateLock FlowStateLock { get; set; }
|
||||||
public FlowState FlowState { get; set; }
|
public FlowState FlowState { get; set; }
|
||||||
|
|
||||||
public Guid ContinuationID { get; set; }
|
public Guid ContinuationID { get; set; }
|
||||||
|
public ContinuationMetadata ContinuationMetadata { get; set; }
|
||||||
public FlowReplyMetadata Reply { get; set; }
|
|
||||||
|
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
MessageContext?.Dispose();
|
|
||||||
FlowStateLock?.Dispose();
|
FlowStateLock?.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
internal class FlowReplyMetadata
|
|
||||||
{
|
|
||||||
public string ReplyTo { get; set; }
|
|
||||||
public string CorrelationId { get; set; }
|
|
||||||
public string ResponseTypeName { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
internal class FlowMetadata
|
|
||||||
{
|
|
||||||
public string ControllerTypeName { get; set; }
|
|
||||||
public FlowReplyMetadata Reply { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
internal class ContinuationMetadata
|
|
||||||
{
|
|
||||||
public string MethodName { get; set; }
|
|
||||||
public string ConvergeMethodName { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Tapeti.Config;
|
using Tapeti.Config;
|
||||||
using Tapeti.Flow.FlowHelpers;
|
|
||||||
|
|
||||||
namespace Tapeti.Flow.Default
|
namespace Tapeti.Flow.Default
|
||||||
{
|
{
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using RabbitMQ.Client.Framing;
|
using RabbitMQ.Client.Framing;
|
||||||
@ -11,13 +12,13 @@ namespace Tapeti.Flow.Default
|
|||||||
public class FlowProvider : IFlowProvider, IFlowHandler
|
public class FlowProvider : IFlowProvider, IFlowHandler
|
||||||
{
|
{
|
||||||
private readonly IConfig config;
|
private readonly IConfig config;
|
||||||
private readonly IAdvancedPublisher publisher;
|
private readonly IInternalPublisher publisher;
|
||||||
|
|
||||||
|
|
||||||
public FlowProvider(IConfig config, IPublisher publisher)
|
public FlowProvider(IConfig config, IPublisher publisher)
|
||||||
{
|
{
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.publisher = (IAdvancedPublisher)publisher;
|
this.publisher = (IInternalPublisher)publisher;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -55,11 +56,11 @@ namespace Tapeti.Flow.Default
|
|||||||
var continuationID = Guid.NewGuid();
|
var continuationID = Guid.NewGuid();
|
||||||
|
|
||||||
context.FlowState.Continuations.Add(continuationID,
|
context.FlowState.Continuations.Add(continuationID,
|
||||||
Newtonsoft.Json.JsonConvert.SerializeObject(new ContinuationMetadata
|
new ContinuationMetadata
|
||||||
{
|
{
|
||||||
MethodName = responseHandlerInfo.MethodName,
|
MethodName = responseHandlerInfo.MethodName,
|
||||||
ConvergeMethodName = null
|
ConvergeMethodName = null
|
||||||
}));
|
});
|
||||||
|
|
||||||
var properties = new BasicProperties
|
var properties = new BasicProperties
|
||||||
{
|
{
|
||||||
@ -73,25 +74,32 @@ namespace Tapeti.Flow.Default
|
|||||||
|
|
||||||
private async Task SendResponse(FlowContext context, object message)
|
private async Task SendResponse(FlowContext context, object message)
|
||||||
{
|
{
|
||||||
if (context.Reply == null)
|
var reply = context.FlowState.Metadata.Reply;
|
||||||
throw new InvalidOperationException("No response is required");
|
if (reply == null)
|
||||||
|
throw new YieldPointException("No response is required");
|
||||||
|
|
||||||
if (message.GetType().FullName != context.Reply.ResponseTypeName)
|
if (message.GetType().FullName != reply.ResponseTypeName)
|
||||||
throw new InvalidOperationException($"Flow must end with a response message of type {context.Reply.ResponseTypeName}, {message.GetType().FullName} was returned instead");
|
throw new YieldPointException($"Flow must end with a response message of type {reply.ResponseTypeName}, {message.GetType().FullName} was returned instead");
|
||||||
|
|
||||||
var properties = new BasicProperties
|
var properties = new BasicProperties();
|
||||||
{
|
|
||||||
CorrelationId = context.Reply.CorrelationId
|
|
||||||
};
|
|
||||||
|
|
||||||
await publisher.PublishDirect(message, context.Reply.ReplyTo, properties);
|
// Only set the property if it's not null, otherwise a string reference exception can occur:
|
||||||
|
// http://rabbitmq.1065348.n5.nabble.com/SocketException-when-invoking-model-BasicPublish-td36330.html
|
||||||
|
if (reply.CorrelationId != null)
|
||||||
|
properties.CorrelationId = reply.CorrelationId;
|
||||||
|
|
||||||
|
// TODO disallow if replyto is not specified?
|
||||||
|
if (context.FlowState.Metadata.Reply.ReplyTo != null)
|
||||||
|
await publisher.PublishDirect(message, reply.ReplyTo, properties);
|
||||||
|
else
|
||||||
|
await publisher.Publish(message, properties);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private static Task EndFlow(FlowContext context)
|
private static Task EndFlow(FlowContext context)
|
||||||
{
|
{
|
||||||
if (context.Reply != null)
|
if (context.FlowState.Metadata.Reply != null)
|
||||||
throw new InvalidOperationException($"Flow must end with a response message of type {context.Reply.ResponseTypeName}");
|
throw new YieldPointException($"Flow must end with a response message of type {context.FlowState.Metadata.Reply.ResponseTypeName}");
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
@ -107,6 +115,10 @@ namespace Tapeti.Flow.Default
|
|||||||
if (requestAttribute?.Response != null && requestAttribute.Response != binding.MessageClass)
|
if (requestAttribute?.Response != null && requestAttribute.Response != binding.MessageClass)
|
||||||
throw new ArgumentException($"responseHandler must accept message of type {binding.MessageClass}", nameof(responseHandler));
|
throw new ArgumentException($"responseHandler must accept message of type {binding.MessageClass}", nameof(responseHandler));
|
||||||
|
|
||||||
|
var continuationAttribute = binding.Method.GetCustomAttribute<ContinuationAttribute>();
|
||||||
|
if (continuationAttribute == null)
|
||||||
|
throw new ArgumentException($"responseHandler must be marked with the Continuation attribute", nameof(responseHandler));
|
||||||
|
|
||||||
return new ResponseHandlerInfo
|
return new ResponseHandlerInfo
|
||||||
{
|
{
|
||||||
MethodName = MethodSerializer.Serialize(responseHandler.Method),
|
MethodName = MethodSerializer.Serialize(responseHandler.Method),
|
||||||
@ -115,16 +127,71 @@ namespace Tapeti.Flow.Default
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static ReplyMetadata GetReply(IMessageContext context)
|
||||||
|
{
|
||||||
|
var requestAttribute = context.Message.GetType().GetCustomAttribute<RequestAttribute>();
|
||||||
|
if (requestAttribute?.Response == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return new ReplyMetadata
|
||||||
|
{
|
||||||
|
CorrelationId = context.Properties.CorrelationId,
|
||||||
|
ReplyTo = context.Properties.ReplyTo,
|
||||||
|
ResponseTypeName = requestAttribute.Response.FullName
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task Execute(IMessageContext context, IYieldPoint yieldPoint)
|
public async Task Execute(IMessageContext context, IYieldPoint yieldPoint)
|
||||||
{
|
{
|
||||||
var flowContext = (FlowContext)context.Items[ContextItems.FlowContext];
|
|
||||||
if (flowContext == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var delegateYieldPoint = (DelegateYieldPoint)yieldPoint;
|
var delegateYieldPoint = (DelegateYieldPoint)yieldPoint;
|
||||||
await delegateYieldPoint.Execute(flowContext);
|
var storeState = delegateYieldPoint.StoreState;
|
||||||
|
|
||||||
if (delegateYieldPoint.StoreState)
|
FlowContext flowContext;
|
||||||
|
object flowContextItem;
|
||||||
|
|
||||||
|
if (!context.Items.TryGetValue(ContextItems.FlowContext, out flowContextItem))
|
||||||
|
{
|
||||||
|
flowContext = new FlowContext
|
||||||
|
{
|
||||||
|
MessageContext = context
|
||||||
|
};
|
||||||
|
|
||||||
|
if (storeState)
|
||||||
|
{
|
||||||
|
// Initiate the flow
|
||||||
|
var flowStore = context.DependencyResolver.Resolve<IFlowStore>();
|
||||||
|
|
||||||
|
var flowID = Guid.NewGuid();
|
||||||
|
flowContext.FlowStateLock = await flowStore.LockFlowState(flowID);
|
||||||
|
|
||||||
|
if (flowContext.FlowStateLock == null)
|
||||||
|
throw new InvalidOperationException("Unable to lock a new flow");
|
||||||
|
|
||||||
|
flowContext.FlowState = await flowContext.FlowStateLock.GetFlowState();
|
||||||
|
if (flowContext.FlowState == null)
|
||||||
|
throw new InvalidOperationException("Unable to get state for new flow");
|
||||||
|
|
||||||
|
flowContext.FlowState.Metadata.Reply = GetReply(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
flowContext = (FlowContext) flowContextItem;
|
||||||
|
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await delegateYieldPoint.Execute(flowContext);
|
||||||
|
}
|
||||||
|
catch (YieldPointException e)
|
||||||
|
{
|
||||||
|
var controllerName = flowContext.MessageContext.Controller.GetType().FullName;
|
||||||
|
var methodName = flowContext.MessageContext.Binding.Method.Name;
|
||||||
|
|
||||||
|
throw new YieldPointException($"{e.Message} in controller {controllerName}, method {methodName}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (storeState)
|
||||||
{
|
{
|
||||||
flowContext.FlowState.Data = Newtonsoft.Json.JsonConvert.SerializeObject(context.Controller);
|
flowContext.FlowState.Data = Newtonsoft.Json.JsonConvert.SerializeObject(context.Controller);
|
||||||
await flowContext.FlowStateLock.StoreFlowState(flowContext.FlowState);
|
await flowContext.FlowStateLock.StoreFlowState(flowContext.FlowState);
|
||||||
@ -202,61 +269,5 @@ namespace Tapeti.Flow.Default
|
|||||||
public string MethodName { get; set; }
|
public string MethodName { get; set; }
|
||||||
public string ReplyToQueue { get; set; }
|
public string ReplyToQueue { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Handle response (correlationId known)
|
|
||||||
internal async Task HandleMessage(object message, string correlationID)
|
|
||||||
{
|
|
||||||
var continuationID = Guid.Parse(correlationID);
|
|
||||||
var flowStateID = await owner.flowStore.FindFlowStateID(continuationID);
|
|
||||||
|
|
||||||
if (!flowStateID.HasValue)
|
|
||||||
return;
|
|
||||||
|
|
||||||
using (flowStateLock = await owner.flowStore.LockFlowState(flowStateID.Value))
|
|
||||||
{
|
|
||||||
flowState = await flowStateLock.GetFlowState();
|
|
||||||
|
|
||||||
continuation = flowState.Continuations[continuationID];
|
|
||||||
if (continuation != null)
|
|
||||||
await HandleContinuation(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task HandleContinuation(object message)
|
|
||||||
{
|
|
||||||
var flowMetaData = Newtonsoft.Json.JsonConvert.DeserializeObject<FlowMetaData>(flowState.MetaData);
|
|
||||||
var continuationMetaData =
|
|
||||||
Newtonsoft.Json.JsonConvert.DeserializeObject<ContinuationMetaData>(continuation.MetaData);
|
|
||||||
|
|
||||||
reply = flowMetaData.Reply;
|
|
||||||
controllerType = owner.GetControllerType(flowMetaData.ControllerTypeName);
|
|
||||||
method = controllerType.GetMethod(continuationMetaData.MethodName);
|
|
||||||
|
|
||||||
controller = owner.container.GetInstance(controllerType);
|
|
||||||
|
|
||||||
Newtonsoft.Json.JsonConvert.PopulateObject(flowState.Data, controller);
|
|
||||||
|
|
||||||
var yieldPoint = (AbstractYieldPoint) await owner.CallFlowController(controller, method, message);
|
|
||||||
|
|
||||||
await yieldPoint.Execute(this);
|
|
||||||
|
|
||||||
if (yieldPoint.Store)
|
|
||||||
{
|
|
||||||
flowState.Data = Newtonsoft.Json.JsonConvert.SerializeObject(controller);
|
|
||||||
flowState.Continuations.Remove(continuation);
|
|
||||||
|
|
||||||
await flowStateLock.StoreFlowState(flowState);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await flowStateLock.DeleteFlowState();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
95
Tapeti.Flow/Default/FlowState.cs
Normal file
95
Tapeti.Flow/Default/FlowState.cs
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Tapeti.Flow.Default
|
||||||
|
{
|
||||||
|
public class FlowState
|
||||||
|
{
|
||||||
|
private FlowMetadata metadata;
|
||||||
|
private Dictionary<Guid, ContinuationMetadata> continuations;
|
||||||
|
|
||||||
|
|
||||||
|
public FlowMetadata Metadata
|
||||||
|
{
|
||||||
|
get { return metadata ?? (metadata = new FlowMetadata()); }
|
||||||
|
set { metadata = value; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Data { get; set; }
|
||||||
|
|
||||||
|
public Dictionary<Guid, ContinuationMetadata> Continuations
|
||||||
|
{
|
||||||
|
get { return continuations ?? (continuations = new Dictionary<Guid, ContinuationMetadata>()); }
|
||||||
|
set { continuations = value; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void Assign(FlowState value)
|
||||||
|
{
|
||||||
|
Metadata = value.Metadata.Clone();
|
||||||
|
Data = value.Data;
|
||||||
|
Continuations = value.Continuations.ToDictionary(kv => kv.Key, kv => kv.Value.Clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public FlowState Clone()
|
||||||
|
{
|
||||||
|
var result = new FlowState();
|
||||||
|
result.Assign(this);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class FlowMetadata
|
||||||
|
{
|
||||||
|
public ReplyMetadata Reply { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
public FlowMetadata Clone()
|
||||||
|
{
|
||||||
|
return new FlowMetadata
|
||||||
|
{
|
||||||
|
Reply = Reply?.Clone()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class ReplyMetadata
|
||||||
|
{
|
||||||
|
public string ReplyTo { get; set; }
|
||||||
|
public string CorrelationId { get; set; }
|
||||||
|
public string ResponseTypeName { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
public ReplyMetadata Clone()
|
||||||
|
{
|
||||||
|
return new ReplyMetadata
|
||||||
|
{
|
||||||
|
ReplyTo = ReplyTo,
|
||||||
|
CorrelationId = CorrelationId,
|
||||||
|
ResponseTypeName = ResponseTypeName
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class ContinuationMetadata
|
||||||
|
{
|
||||||
|
public string MethodName { get; set; }
|
||||||
|
public string ConvergeMethodName { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
public ContinuationMetadata Clone()
|
||||||
|
{
|
||||||
|
return new ContinuationMetadata
|
||||||
|
{
|
||||||
|
MethodName = MethodName,
|
||||||
|
ConvergeMethodName = ConvergeMethodName
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,17 +1,19 @@
|
|||||||
using System;
|
using Newtonsoft.Json;
|
||||||
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Tapeti.Flow
|
namespace Tapeti.Flow.Default
|
||||||
{
|
{
|
||||||
public class FlowStore : IFlowStore
|
public class FlowStore : IFlowStore
|
||||||
{
|
{
|
||||||
|
private static readonly ConcurrentDictionary<Guid, FlowState> FlowStates = new ConcurrentDictionary<Guid, FlowState>();
|
||||||
|
private static readonly ConcurrentDictionary<Guid, Guid> ContinuationLookup = new ConcurrentDictionary<Guid, Guid>();
|
||||||
|
|
||||||
private readonly IFlowRepository repository;
|
private readonly IFlowRepository repository;
|
||||||
private readonly ConcurrentDictionary<Guid, FlowState> flowStates = new ConcurrentDictionary<Guid, FlowState>();
|
|
||||||
private readonly ConcurrentDictionary<Guid, Guid> continuationLookup = new ConcurrentDictionary<Guid, Guid>();
|
|
||||||
|
|
||||||
|
|
||||||
public FlowStore(IFlowRepository repository)
|
public FlowStore(IFlowRepository repository)
|
||||||
@ -22,41 +24,37 @@ namespace Tapeti.Flow
|
|||||||
|
|
||||||
public async Task Load()
|
public async Task Load()
|
||||||
{
|
{
|
||||||
flowStates.Clear();
|
FlowStates.Clear();
|
||||||
continuationLookup.Clear();
|
ContinuationLookup.Clear();
|
||||||
|
|
||||||
foreach (var state in await repository.GetAllStates())
|
foreach (var flowStateRecord in await repository.GetStates())
|
||||||
{
|
{
|
||||||
flowStates.GetOrAdd(state.FlowID, new FlowState
|
var flowState = ToFlowState(flowStateRecord);
|
||||||
{
|
FlowStates.GetOrAdd(flowStateRecord.FlowID, flowState);
|
||||||
Metadata = state.Metadata,
|
|
||||||
Data = state.Data,
|
|
||||||
Continuations = state.Continuations
|
|
||||||
});
|
|
||||||
|
|
||||||
foreach (var continuation in state.Continuations)
|
foreach (var continuation in flowStateRecord.ContinuationMetadata)
|
||||||
continuationLookup.GetOrAdd(continuation.Key, state.FlowID);
|
ContinuationLookup.GetOrAdd(continuation.Key, flowStateRecord.FlowID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public Task<Guid?> FindFlowStateID(Guid continuationID)
|
public Task<Guid?> FindFlowID(Guid continuationID)
|
||||||
{
|
{
|
||||||
Guid result;
|
Guid result;
|
||||||
return Task.FromResult(continuationLookup.TryGetValue(continuationID, out result) ? result : (Guid?)null);
|
return Task.FromResult(ContinuationLookup.TryGetValue(continuationID, out result) ? result : (Guid?)null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task<IFlowStateLock> LockFlowState(Guid flowStateID)
|
public async Task<IFlowStateLock> LockFlowState(Guid flowID)
|
||||||
{
|
{
|
||||||
var isNew = false;
|
var isNew = false;
|
||||||
var flowState = flowStates.GetOrAdd(flowStateID, id =>
|
var flowState = FlowStates.GetOrAdd(flowID, id =>
|
||||||
{
|
{
|
||||||
isNew = true;
|
isNew = true;
|
||||||
return new FlowState();
|
return new FlowState();
|
||||||
});
|
});
|
||||||
|
|
||||||
var result = new FlowStateLock(this, flowState, flowStateID, isNew);
|
var result = new FlowStateLock(this, flowState, flowID, isNew);
|
||||||
await result.Lock();
|
await result.Lock();
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@ -103,7 +101,7 @@ namespace Tapeti.Flow
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Guid FlowStateID => flowID;
|
public Guid FlowID => flowID;
|
||||||
|
|
||||||
public Task<FlowState> GetFlowState()
|
public Task<FlowState> GetFlowState()
|
||||||
{
|
{
|
||||||
@ -112,12 +110,7 @@ namespace Tapeti.Flow
|
|||||||
if (isDisposed)
|
if (isDisposed)
|
||||||
throw new ObjectDisposedException("FlowStateLock");
|
throw new ObjectDisposedException("FlowStateLock");
|
||||||
|
|
||||||
return Task.FromResult(new FlowState
|
return Task.FromResult(flowState.Clone());
|
||||||
{
|
|
||||||
Data = flowState.Data,
|
|
||||||
Metadata = flowState.Metadata,
|
|
||||||
Continuations = flowState.Continuations.ToDictionary(kv => kv.Key, kv => kv.Value)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -128,37 +121,29 @@ namespace Tapeti.Flow
|
|||||||
if (isDisposed)
|
if (isDisposed)
|
||||||
throw new ObjectDisposedException("FlowStateLock");
|
throw new ObjectDisposedException("FlowStateLock");
|
||||||
|
|
||||||
foreach (
|
foreach (var removedContinuation in flowState.Continuations.Keys.Where(k => !newFlowState.Continuations.ContainsKey(k)))
|
||||||
var removedContinuation in
|
|
||||||
flowState.Continuations.Keys.Where(
|
|
||||||
k => !newFlowState.Continuations.ContainsKey(k)))
|
|
||||||
{
|
{
|
||||||
Guid removedValue;
|
Guid removedValue;
|
||||||
owner.continuationLookup.TryRemove(removedContinuation, out removedValue);
|
ContinuationLookup.TryRemove(removedContinuation, out removedValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (
|
foreach (var addedContinuation in newFlowState.Continuations.Where(c => !flowState.Continuations.ContainsKey(c.Key)))
|
||||||
var addedContinuation in
|
|
||||||
newFlowState.Continuations.Where(
|
|
||||||
c => !flowState.Continuations.ContainsKey(c.Key)))
|
|
||||||
{
|
{
|
||||||
owner.continuationLookup.TryAdd(addedContinuation.Key, flowID);
|
ContinuationLookup.TryAdd(addedContinuation.Key, flowID);
|
||||||
}
|
}
|
||||||
|
|
||||||
flowState.Metadata = newFlowState.Metadata;
|
flowState.Assign(newFlowState);
|
||||||
flowState.Data = newFlowState.Data;
|
|
||||||
flowState.Continuations = newFlowState.Continuations.ToDictionary(kv => kv.Key, kv => kv.Value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isNew)
|
if (isNew)
|
||||||
{
|
{
|
||||||
isNew = false;
|
isNew = false;
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
await
|
await owner.repository.CreateState(ToFlowStateRecord(flowID, flowState), now);
|
||||||
owner.repository.CreateState(flowID, now, flowState.Metadata, flowState.Data, flowState.Continuations);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await owner.repository.UpdateState(flowID, flowState.Metadata, flowState.Data, flowState.Continuations);
|
await owner.repository.UpdateState(ToFlowStateRecord(flowID, flowState));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -172,15 +157,42 @@ namespace Tapeti.Flow
|
|||||||
foreach (var removedContinuation in flowState.Continuations.Keys)
|
foreach (var removedContinuation in flowState.Continuations.Keys)
|
||||||
{
|
{
|
||||||
Guid removedValue;
|
Guid removedValue;
|
||||||
owner.continuationLookup.TryRemove(removedContinuation, out removedValue);
|
ContinuationLookup.TryRemove(removedContinuation, out removedValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
FlowState removedFlow;
|
FlowState removedFlow;
|
||||||
owner.flowStates.TryRemove(flowID, out removedFlow);
|
FlowStates.TryRemove(flowID, out removedFlow);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isNew)
|
if (!isNew)
|
||||||
await owner.repository.DeleteState(flowID);
|
await owner.repository.DeleteState(flowID);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static FlowStateRecord ToFlowStateRecord(Guid flowID, FlowState flowState)
|
||||||
|
{
|
||||||
|
return new FlowStateRecord
|
||||||
|
{
|
||||||
|
FlowID = flowID,
|
||||||
|
Metadata = JsonConvert.SerializeObject(flowState.Metadata),
|
||||||
|
Data = flowState.Data,
|
||||||
|
ContinuationMetadata = flowState.Continuations.ToDictionary(
|
||||||
|
kv => kv.Key,
|
||||||
|
kv => JsonConvert.SerializeObject(kv.Value))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FlowState ToFlowState(FlowStateRecord flowStateRecord)
|
||||||
|
{
|
||||||
|
return new FlowState
|
||||||
|
{
|
||||||
|
Metadata = JsonConvert.DeserializeObject<FlowMetadata>(flowStateRecord.Metadata),
|
||||||
|
Data = flowStateRecord.Data,
|
||||||
|
Continuations = flowStateRecord.ContinuationMetadata.ToDictionary(
|
||||||
|
kv => kv.Key,
|
||||||
|
kv => JsonConvert.DeserializeObject<ContinuationMetadata>(kv.Value))
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,17 +7,17 @@ namespace Tapeti.Flow.Default
|
|||||||
{
|
{
|
||||||
public class NonPersistentFlowRepository : IFlowRepository
|
public class NonPersistentFlowRepository : IFlowRepository
|
||||||
{
|
{
|
||||||
public Task<IQueryable<FlowStateRecord>> GetAllStates()
|
public Task<IQueryable<FlowStateRecord>> GetStates()
|
||||||
{
|
{
|
||||||
return Task.FromResult(new List<FlowStateRecord>().AsQueryable());
|
return Task.FromResult(new List<FlowStateRecord>().AsQueryable());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task CreateState(Guid flowID, DateTime timestamp, string metadata, string data, IDictionary<Guid, string> continuations)
|
public Task CreateState(FlowStateRecord stateRecord, DateTime timestamp)
|
||||||
{
|
{
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task UpdateState(Guid flowID, string metadata, string data, IDictionary<Guid, string> continuations)
|
public Task UpdateState(FlowStateRecord stateRecord)
|
||||||
{
|
{
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ using System.Threading.Tasks;
|
|||||||
using Tapeti.Config;
|
using Tapeti.Config;
|
||||||
using Tapeti.Flow.Annotations;
|
using Tapeti.Flow.Annotations;
|
||||||
using Tapeti.Flow.Default;
|
using Tapeti.Flow.Default;
|
||||||
|
using Tapeti.Helpers;
|
||||||
|
|
||||||
namespace Tapeti.Flow
|
namespace Tapeti.Flow
|
||||||
{
|
{
|
||||||
@ -13,13 +14,14 @@ namespace Tapeti.Flow
|
|||||||
public IEnumerable<object> GetContents(IDependencyResolver dependencyResolver)
|
public IEnumerable<object> GetContents(IDependencyResolver dependencyResolver)
|
||||||
{
|
{
|
||||||
var container = dependencyResolver as IDependencyContainer;
|
var container = dependencyResolver as IDependencyContainer;
|
||||||
|
|
||||||
|
// ReSharper disable once InvertIf
|
||||||
if (container != null)
|
if (container != null)
|
||||||
{
|
{
|
||||||
container.RegisterDefault<IFlowProvider, FlowProvider>();
|
container.RegisterDefault<IFlowProvider, FlowProvider>();
|
||||||
container.RegisterDefault<IFlowHandler, FlowProvider>();
|
container.RegisterDefault<IFlowHandler, FlowProvider>();
|
||||||
// TODO singleton
|
|
||||||
container.RegisterDefault<IFlowStore, FlowStore>();
|
|
||||||
container.RegisterDefault<IFlowRepository, NonPersistentFlowRepository>();
|
container.RegisterDefault<IFlowRepository, NonPersistentFlowRepository>();
|
||||||
|
container.RegisterDefault<IFlowStore, FlowStore>();
|
||||||
}
|
}
|
||||||
|
|
||||||
return new[] { new FlowBindingMiddleware() };
|
return new[] { new FlowBindingMiddleware() };
|
||||||
@ -30,40 +32,41 @@ namespace Tapeti.Flow
|
|||||||
{
|
{
|
||||||
public void Handle(IBindingContext context, Action next)
|
public void Handle(IBindingContext context, Action next)
|
||||||
{
|
{
|
||||||
HandleContinuationFilter(context);
|
RegisterContinuationFilter(context);
|
||||||
HandleYieldPointResult(context);
|
RegisterYieldPointResult(context);
|
||||||
|
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private static void HandleContinuationFilter(IBindingContext context)
|
private static void RegisterContinuationFilter(IBindingContext context)
|
||||||
{
|
{
|
||||||
var continuationAttribute = context.Method.GetCustomAttribute<ContinuationAttribute>();
|
var continuationAttribute = context.Method.GetCustomAttribute<ContinuationAttribute>();
|
||||||
if (continuationAttribute != null)
|
if (continuationAttribute == null)
|
||||||
{
|
return;
|
||||||
context.Use(new FlowBindingFilter());
|
|
||||||
context.Use(new FlowMessageMiddleware());
|
context.Use(new FlowBindingFilter());
|
||||||
}
|
context.Use(new FlowMessageMiddleware());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private static void HandleYieldPointResult(IBindingContext context)
|
private static void RegisterYieldPointResult(IBindingContext context)
|
||||||
{
|
{
|
||||||
if (context.Result.Info.ParameterType == typeof(IYieldPoint))
|
bool isTask;
|
||||||
context.Result.SetHandler((messageContext, value) => HandleYieldPoint(messageContext, (IYieldPoint)value));
|
if (!context.Result.Info.ParameterType.IsTypeOrTaskOf(typeof(IYieldPoint), out isTask))
|
||||||
|
return;
|
||||||
|
|
||||||
else if (context.Result.Info.ParameterType == typeof(Task<>))
|
if (isTask)
|
||||||
{
|
{
|
||||||
var genericArguments = context.Result.Info.ParameterType.GetGenericArguments();
|
context.Result.SetHandler(async (messageContext, value) =>
|
||||||
if (genericArguments.Length == 1 && genericArguments[0] == typeof(IYieldPoint))
|
{
|
||||||
context.Result.SetHandler(async (messageContext, value) =>
|
var yieldPoint = await (Task<IYieldPoint>)value;
|
||||||
{
|
if (yieldPoint != null)
|
||||||
var yieldPoint = await (Task<IYieldPoint>)value;
|
await HandleYieldPoint(messageContext, yieldPoint);
|
||||||
if (yieldPoint != null)
|
});
|
||||||
await HandleYieldPoint(messageContext, yieldPoint);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
context.Result.SetHandler((messageContext, value) => HandleYieldPoint(messageContext, (IYieldPoint)value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -7,9 +7,9 @@ namespace Tapeti.Flow
|
|||||||
{
|
{
|
||||||
public interface IFlowRepository
|
public interface IFlowRepository
|
||||||
{
|
{
|
||||||
Task<IQueryable<FlowStateRecord>> GetAllStates();
|
Task<IQueryable<FlowStateRecord>> GetStates();
|
||||||
Task CreateState(Guid flowID, DateTime timestamp, string metadata, string data, IDictionary<Guid, string> continuations);
|
Task CreateState(FlowStateRecord stateRecord, DateTime timestamp);
|
||||||
Task UpdateState(Guid flowID, string metadata, string data, IDictionary<Guid, string> continuations);
|
Task UpdateState(FlowStateRecord stateRecord);
|
||||||
Task DeleteState(Guid flowID);
|
Task DeleteState(Guid flowID);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -19,6 +19,6 @@ namespace Tapeti.Flow
|
|||||||
public Guid FlowID;
|
public Guid FlowID;
|
||||||
public string Metadata;
|
public string Metadata;
|
||||||
public string Data;
|
public string Data;
|
||||||
public Dictionary<Guid, string> Continuations;
|
public Dictionary<Guid, string> ContinuationMetadata;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,28 +1,23 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Tapeti.Flow.Default;
|
||||||
|
|
||||||
namespace Tapeti.Flow
|
namespace Tapeti.Flow
|
||||||
{
|
{
|
||||||
public interface IFlowStore
|
public interface IFlowStore
|
||||||
{
|
{
|
||||||
Task Load();
|
Task Load();
|
||||||
Task<Guid?> FindFlowStateID(Guid continuationID);
|
Task<Guid?> FindFlowID(Guid continuationID);
|
||||||
Task<IFlowStateLock> LockFlowState(Guid flowStateID);
|
Task<IFlowStateLock> LockFlowState(Guid flowID);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IFlowStateLock : IDisposable
|
public interface IFlowStateLock : IDisposable
|
||||||
{
|
{
|
||||||
Guid FlowStateID { get; }
|
Guid FlowID { get; }
|
||||||
|
|
||||||
Task<FlowState> GetFlowState();
|
Task<FlowState> GetFlowState();
|
||||||
Task StoreFlowState(FlowState flowState);
|
Task StoreFlowState(FlowState flowState);
|
||||||
Task DeleteFlowState();
|
Task DeleteFlowState();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class FlowState
|
|
||||||
{
|
|
||||||
public string Metadata { get; set; }
|
|
||||||
public string Data { get; set; }
|
|
||||||
public Dictionary<Guid, string> Continuations { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
// General Information about an assembly is controlled through the following
|
// General Information about an assembly is controlled through the following
|
||||||
|
@ -55,8 +55,10 @@
|
|||||||
<Compile Include="Default\FlowBindingFilter.cs" />
|
<Compile Include="Default\FlowBindingFilter.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\NonPersistentFlowRepository.cs" />
|
<Compile Include="Default\NonPersistentFlowRepository.cs" />
|
||||||
<Compile Include="Default\DelegateYieldPoint.cs" />
|
<Compile Include="Default\DelegateYieldPoint.cs" />
|
||||||
|
<Compile Include="ConfigExtensions.cs" />
|
||||||
<Compile Include="FlowHelpers\MethodSerializer.cs" />
|
<Compile Include="FlowHelpers\MethodSerializer.cs" />
|
||||||
<Compile Include="FlowMiddleware.cs" />
|
<Compile Include="FlowMiddleware.cs" />
|
||||||
<Compile Include="Default\FlowStore.cs" />
|
<Compile Include="Default\FlowStore.cs" />
|
||||||
@ -65,6 +67,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="YieldPointException.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Tapeti.csproj">
|
<ProjectReference Include="..\Tapeti.csproj">
|
||||||
|
10
Tapeti.Flow/YieldPointException.cs
Normal file
10
Tapeti.Flow/YieldPointException.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Tapeti.Flow
|
||||||
|
{
|
||||||
|
public class YieldPointException : Exception
|
||||||
|
{
|
||||||
|
public YieldPointException(string message) : base(message) { }
|
||||||
|
public YieldPointException(string message, Exception innerException) : base(message, innerException) { }
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using SimpleInjector;
|
using SimpleInjector;
|
||||||
using Tapeti.Config;
|
|
||||||
|
|
||||||
namespace Tapeti.SimpleInjector
|
namespace Tapeti.SimpleInjector
|
||||||
{
|
{
|
||||||
@ -27,29 +26,44 @@ namespace Tapeti.SimpleInjector
|
|||||||
|
|
||||||
public void RegisterDefault<TService, TImplementation>() where TService : class where TImplementation : class, TService
|
public void RegisterDefault<TService, TImplementation>() where TService : class where TImplementation : class, TService
|
||||||
{
|
{
|
||||||
// ReSharper disable once SimplifyLinqExpression - not a fan of negative predicates
|
if (CanRegisterDefault<TService>())
|
||||||
if (!container.GetCurrentRegistrations().Any(ip => ip.ServiceType == typeof(TService)))
|
|
||||||
container.Register<TService, TImplementation>();
|
container.Register<TService, TImplementation>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void RegisterDefault<TService>(Func<TService> factory) where TService : class
|
||||||
public void RegisterConfig(IConfig config)
|
|
||||||
{
|
{
|
||||||
container.RegisterSingleton(config);
|
if (CanRegisterDefault<TService>())
|
||||||
|
container.Register(factory);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void RegisterDefaultSingleton<TService, TImplementation>() where TService : class where TImplementation : class, TService
|
||||||
public void RegisterPublisher(Func<IPublisher> publisher)
|
|
||||||
{
|
{
|
||||||
// ReSharper disable once SimplifyLinqExpression - still not a fan of negative predicates
|
if (CanRegisterDefault<TService>())
|
||||||
if (!container.GetCurrentRegistrations().Any(ip => ip.ServiceType == typeof(IPublisher)))
|
container.RegisterSingleton<TService, TImplementation>();
|
||||||
container.Register(publisher);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void RegisterDefaultSingleton<TService>(TService instance) where TService : class
|
||||||
|
{
|
||||||
|
if (CanRegisterDefault<TService>())
|
||||||
|
container.RegisterSingleton(instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterDefaultSingleton<TService>(Func<TService> factory) where TService : class
|
||||||
|
{
|
||||||
|
if (CanRegisterDefault<TService>())
|
||||||
|
container.RegisterSingleton(factory);
|
||||||
|
}
|
||||||
|
|
||||||
public void RegisterController(Type type)
|
public void RegisterController(Type type)
|
||||||
{
|
{
|
||||||
container.Register(type);
|
container.Register(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private bool CanRegisterDefault<TService>() where TService : class
|
||||||
|
{
|
||||||
|
// ReSharper disable once SimplifyLinqExpression - not a fan of negative predicates
|
||||||
|
return !container.GetCurrentRegistrations().Any(ip => ip.ServiceType == typeof(TService));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -61,10 +61,12 @@
|
|||||||
<Compile Include="Default\ConsoleLogger.cs" />
|
<Compile Include="Default\ConsoleLogger.cs" />
|
||||||
<Compile Include="Default\DevNullLogger.cs" />
|
<Compile Include="Default\DevNullLogger.cs" />
|
||||||
<Compile Include="Default\JsonMessageSerializer.cs" />
|
<Compile Include="Default\JsonMessageSerializer.cs" />
|
||||||
|
<Compile Include="Default\PublishResultBinding.cs" />
|
||||||
<Compile Include="Default\NamespaceMatchExchangeStrategy.cs" />
|
<Compile Include="Default\NamespaceMatchExchangeStrategy.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="IConnection.cs" />
|
<Compile Include="IConnection.cs" />
|
||||||
<Compile Include="IExchangeStrategy.cs" />
|
<Compile Include="IExchangeStrategy.cs" />
|
||||||
<Compile Include="ILogger.cs" />
|
<Compile Include="ILogger.cs" />
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
|
||||||
<s:Boolean x:Key="/Default/Housekeeping/Bookmarks/NumberedBookmarks/=Bookmark9/@KeyIndexDefined">True</s:Boolean>
|
|
||||||
<s:String x:Key="/Default/Housekeeping/Bookmarks/NumberedBookmarks/=Bookmark9/Coords/@EntryValue">(Doc Ln 29 Col 51)</s:String>
|
|
||||||
<s:String x:Key="/Default/Housekeeping/Bookmarks/NumberedBookmarks/=Bookmark9/FileId/@EntryValue">F84AD920-D5A1-455D-AED5-2542B3A47B85/d:Default/f:FlowProvider.cs</s:String>
|
|
||||||
<s:String x:Key="/Default/Housekeeping/Bookmarks/NumberedBookmarks/=Bookmark9/Owner/@EntryValue">NumberedBookmarkManager</s:String></wpf:ResourceDictionary>
|
|
@ -16,7 +16,7 @@ namespace Tapeti
|
|||||||
public TopologyConfigurationException(string message) : base(message) { }
|
public TopologyConfigurationException(string message) : base(message) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
public delegate Task<object> MessageHandlerFunc(IMessageContext context, object message);
|
public delegate Task MessageHandlerFunc(IMessageContext context, object message);
|
||||||
|
|
||||||
|
|
||||||
public class TapetiConfig
|
public class TapetiConfig
|
||||||
@ -27,17 +27,18 @@ 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 exchange;
|
private readonly string subscribeExchange;
|
||||||
private readonly IDependencyResolver dependencyResolver;
|
private readonly IDependencyResolver dependencyResolver;
|
||||||
|
|
||||||
|
|
||||||
public TapetiConfig(string exchange, IDependencyResolver dependencyResolver)
|
public TapetiConfig(string subscribeExchange, IDependencyResolver dependencyResolver)
|
||||||
{
|
{
|
||||||
this.exchange = exchange;
|
this.subscribeExchange = subscribeExchange;
|
||||||
this.dependencyResolver = dependencyResolver;
|
this.dependencyResolver = dependencyResolver;
|
||||||
|
|
||||||
Use(new DependencyResolverBinding());
|
Use(new DependencyResolverBinding());
|
||||||
Use(new MessageBinding());
|
Use(new MessageBinding());
|
||||||
|
Use(new PublishResultBinding());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -62,8 +63,8 @@ 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(exchange, dependencyResolver, messageMiddleware, queues);
|
var config = new Config(subscribeExchange, dependencyResolver, messageMiddleware, queues);
|
||||||
(dependencyResolver as IDependencyContainer)?.RegisterConfig(config);
|
(dependencyResolver as IDependencyContainer)?.RegisterDefaultSingleton<IConfig>(config);
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
@ -139,7 +140,8 @@ namespace Tapeti
|
|||||||
QueueInfo = methodQueueInfo,
|
QueueInfo = methodQueueInfo,
|
||||||
MessageClass = context.MessageClass,
|
MessageClass = context.MessageClass,
|
||||||
MessageHandler = messageHandler,
|
MessageHandler = messageHandler,
|
||||||
MessageMiddleware = context.MessageMiddleware
|
MessageMiddleware = context.MessageMiddleware,
|
||||||
|
BindingFilters = context.BindingFilters
|
||||||
};
|
};
|
||||||
|
|
||||||
if (methodQueueInfo.Dynamic.GetValueOrDefault())
|
if (methodQueueInfo.Dynamic.GetValueOrDefault())
|
||||||
@ -177,61 +179,59 @@ namespace Tapeti
|
|||||||
|
|
||||||
var invalidBindings = context.Parameters.Where(p => !p.HasBinding).ToList();
|
var invalidBindings = context.Parameters.Where(p => !p.HasBinding).ToList();
|
||||||
|
|
||||||
// ReSharper disable once InvertIf - doesn't make the flow clearer imo
|
// ReSharper disable once InvertIf
|
||||||
if (invalidBindings.Count > 0)
|
if (invalidBindings.Count > 0)
|
||||||
{
|
{
|
||||||
var parameterNames = string.Join(", ", invalidBindings.Select(p => p.Info.Name));
|
var parameterNames = string.Join(", ", invalidBindings.Select(p => p.Info.Name));
|
||||||
throw new TopologyConfigurationException($"Method {method.Name} in controller {method.DeclaringType?.Name} has unknown parameters: {parameterNames}");
|
throw new TopologyConfigurationException($"Method {method.Name} in controller {method.DeclaringType?.Name} has unknown parameters: {parameterNames}");
|
||||||
}
|
}
|
||||||
|
|
||||||
return WrapMethod(method, context.Parameters.Select(p => ((IBindingParameterAccess)p).GetBinding()));
|
var resultHandler = ((IBindingResultAccess) context.Result).GetHandler();
|
||||||
|
|
||||||
|
return WrapMethod(method, context.Parameters.Select(p => ((IBindingParameterAccess)p).GetBinding()), resultHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected MessageHandlerFunc WrapMethod(MethodInfo method, IEnumerable<ValueFactory> parameters)
|
protected MessageHandlerFunc WrapMethod(MethodInfo method, IEnumerable<ValueFactory> parameters, ResultHandler resultHandler)
|
||||||
{
|
{
|
||||||
|
if (resultHandler != null)
|
||||||
|
return WrapResultHandlerMethod(method, parameters, resultHandler);
|
||||||
|
|
||||||
if (method.ReturnType == typeof(void))
|
if (method.ReturnType == typeof(void))
|
||||||
return WrapNullMethod(method, parameters);
|
return WrapNullMethod(method, parameters);
|
||||||
|
|
||||||
if (method.ReturnType == typeof(Task))
|
if (method.ReturnType == typeof(Task))
|
||||||
return WrapTaskMethod(method, parameters);
|
return WrapTaskMethod(method, parameters);
|
||||||
|
|
||||||
if (method.ReturnType == typeof(Task<>))
|
if (method.ReturnType.IsGenericType && method.ReturnType.GetGenericTypeDefinition() == typeof(Task<>))
|
||||||
{
|
|
||||||
var genericArguments = method.GetGenericArguments();
|
|
||||||
if (genericArguments.Length != 1)
|
|
||||||
throw new ArgumentException($"Method {method.Name} in controller {method.DeclaringType?.Name} must have exactly one generic argument to Task<>");
|
|
||||||
|
|
||||||
if (!genericArguments[0].IsClass)
|
|
||||||
throw new ArgumentException($"Method {method.Name} in controller {method.DeclaringType?.Name} must have an object generic argument to Task<>");
|
|
||||||
|
|
||||||
return WrapGenericTaskMethod(method, parameters);
|
return WrapGenericTaskMethod(method, parameters);
|
||||||
}
|
|
||||||
|
|
||||||
if (method.ReturnType.IsClass)
|
return WrapObjectMethod(method, parameters);
|
||||||
return WrapObjectMethod(method, parameters);
|
|
||||||
|
|
||||||
throw new ArgumentException($"Method {method.Name} in controller {method.DeclaringType?.Name} has an invalid return type");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected MessageHandlerFunc WrapResultHandlerMethod(MethodInfo method, IEnumerable<ValueFactory> parameters, ResultHandler resultHandler)
|
||||||
|
{
|
||||||
|
return (context, message) =>
|
||||||
|
{
|
||||||
|
var result = method.Invoke(context.Controller, parameters.Select(p => p(context)).ToArray());
|
||||||
|
return resultHandler(context, result);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
protected MessageHandlerFunc WrapNullMethod(MethodInfo method, IEnumerable<ValueFactory> parameters)
|
protected MessageHandlerFunc WrapNullMethod(MethodInfo method, IEnumerable<ValueFactory> parameters)
|
||||||
{
|
{
|
||||||
return (context, message) =>
|
return (context, message) =>
|
||||||
{
|
{
|
||||||
method.Invoke(context.Controller, parameters.Select(p => p(context)).ToArray());
|
method.Invoke(context.Controller, parameters.Select(p => p(context)).ToArray());
|
||||||
return Task.FromResult<object>(null);
|
return Task.CompletedTask;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected MessageHandlerFunc WrapTaskMethod(MethodInfo method, IEnumerable<ValueFactory> parameters)
|
protected MessageHandlerFunc WrapTaskMethod(MethodInfo method, IEnumerable<ValueFactory> parameters)
|
||||||
{
|
{
|
||||||
return async (context, message) =>
|
return (context, message) => (Task)method.Invoke(context.Controller, parameters.Select(p => p(context)).ToArray());
|
||||||
{
|
|
||||||
await (Task)method.Invoke(context.Controller, parameters.Select(p => p(context)).ToArray());
|
|
||||||
return Task.FromResult<object>(null);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -310,7 +310,7 @@ namespace Tapeti
|
|||||||
|
|
||||||
protected class Config : IConfig
|
protected class Config : IConfig
|
||||||
{
|
{
|
||||||
public string Exchange { get; }
|
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 +318,9 @@ namespace Tapeti
|
|||||||
private readonly Dictionary<MethodInfo, IBinding> bindingMethodLookup;
|
private readonly Dictionary<MethodInfo, IBinding> bindingMethodLookup;
|
||||||
|
|
||||||
|
|
||||||
public Config(string exchange, IDependencyResolver dependencyResolver, IReadOnlyList<IMessageMiddleware> messageMiddleware, IEnumerable<IQueue> queues)
|
public Config(string subscribeExchange, IDependencyResolver dependencyResolver, IReadOnlyList<IMessageMiddleware> messageMiddleware, IEnumerable<IQueue> queues)
|
||||||
{
|
{
|
||||||
Exchange = exchange;
|
SubscribeExchange = subscribeExchange;
|
||||||
DependencyResolver = dependencyResolver;
|
DependencyResolver = dependencyResolver;
|
||||||
MessageMiddleware = messageMiddleware;
|
MessageMiddleware = messageMiddleware;
|
||||||
Queues = queues.ToList();
|
Queues = queues.ToList();
|
||||||
@ -361,6 +361,7 @@ namespace Tapeti
|
|||||||
public string QueueName { get; set; }
|
public string QueueName { get; set; }
|
||||||
|
|
||||||
public IReadOnlyList<IMessageMiddleware> MessageMiddleware { get; set; }
|
public IReadOnlyList<IMessageMiddleware> MessageMiddleware { get; set; }
|
||||||
|
public IReadOnlyList<IBindingFilter> BindingFilters { get; set; }
|
||||||
|
|
||||||
private QueueInfo queueInfo;
|
private QueueInfo queueInfo;
|
||||||
public QueueInfo QueueInfo
|
public QueueInfo QueueInfo
|
||||||
@ -382,13 +383,25 @@ namespace Tapeti
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public bool Accept(object message)
|
public async Task<bool> Accept(IMessageContext context, object message)
|
||||||
{
|
{
|
||||||
return message.GetType() == MessageClass;
|
if (message.GetType() != MessageClass)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (BindingFilters == null)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
foreach (var filter in BindingFilters)
|
||||||
|
{
|
||||||
|
if (!await filter.Accept(context, this))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public Task<object> Invoke(IMessageContext context, object message)
|
public Task Invoke(IMessageContext context, object message)
|
||||||
{
|
{
|
||||||
return MessageHandler(context, message);
|
return MessageHandler(context, message);
|
||||||
}
|
}
|
||||||
|
@ -16,12 +16,12 @@ namespace Tapeti
|
|||||||
public TapetiConnection(IConfig config)
|
public TapetiConnection(IConfig config)
|
||||||
{
|
{
|
||||||
this.config = config;
|
this.config = config;
|
||||||
(config.DependencyResolver as IDependencyContainer)?.RegisterPublisher(GetPublisher);
|
(config.DependencyResolver as IDependencyContainer)?.RegisterDefault(GetPublisher);
|
||||||
|
|
||||||
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(),
|
||||||
Exchange = config.Exchange
|
SubscribeExchange = config.SubscribeExchange
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,6 +10,13 @@ namespace Tapeti
|
|||||||
public string Username { get; set; } = "guest";
|
public string Username { get; set; } = "guest";
|
||||||
public string Password { get; set; } = "guest";
|
public string Password { get; set; } = "guest";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The amount of message to prefetch. See http://www.rabbitmq.com/consumer-prefetch.html for more information.
|
||||||
|
///
|
||||||
|
/// If set to 0, no limit will be applied.
|
||||||
|
/// </summary>
|
||||||
|
public ushort PrefetchCount { get; set; } = 50;
|
||||||
|
|
||||||
|
|
||||||
public TapetiConnectionParams()
|
public TapetiConnectionParams()
|
||||||
{
|
{
|
||||||
|
@ -30,30 +30,30 @@ namespace Test
|
|||||||
* The Visualizer could've been injected through the constructor, which is
|
* The Visualizer could've been injected through the constructor, which is
|
||||||
* the recommended way. Just testing the injection middleware here.
|
* the recommended way. Just testing the injection middleware here.
|
||||||
*/
|
*/
|
||||||
public async Task Marco(MarcoMessage message, Visualizer myVisualizer)
|
public async Task<IYieldPoint> Marco(MarcoMessage message, Visualizer myVisualizer)
|
||||||
{
|
{
|
||||||
|
Console.WriteLine(">> Marco (yielding with request)");
|
||||||
|
|
||||||
await myVisualizer.VisualizeMarco();
|
await myVisualizer.VisualizeMarco();
|
||||||
await publisher.Publish(new PoloMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
return flowProvider.YieldWithRequestSync<PoloConfirmationRequestMessage, PoloConfirmationResponseMessage>(
|
||||||
public IYieldPoint Polo(PoloMessage message)
|
|
||||||
{
|
|
||||||
StateTestGuid = Guid.NewGuid();
|
|
||||||
|
|
||||||
return flowProvider.YieldWithRequest<PoloConfirmationRequestMessage, PoloConfirmationResponseMessage>(
|
|
||||||
new PoloConfirmationRequestMessage()
|
new PoloConfirmationRequestMessage()
|
||||||
{
|
{
|
||||||
StoredInState = StateTestGuid
|
StoredInState = StateTestGuid
|
||||||
},
|
},
|
||||||
HandlePoloConfirmationResponse);
|
HandlePoloConfirmationResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task<IYieldPoint> HandlePoloConfirmationResponse(PoloConfirmationResponseMessage message)
|
[Continuation]
|
||||||
|
public IYieldPoint HandlePoloConfirmationResponse(PoloConfirmationResponseMessage message)
|
||||||
{
|
{
|
||||||
await visualizer.VisualizePolo(message.ShouldMatchState.Equals(StateTestGuid));
|
Console.WriteLine(">> HandlePoloConfirmationResponse (ending flow)");
|
||||||
return flowProvider.End();
|
|
||||||
|
Console.WriteLine(message.ShouldMatchState.Equals(StateTestGuid) ? "Confirmed!" : "Oops! Mismatch!");
|
||||||
|
|
||||||
|
// This should error, as MarcoMessage expects a PoloMessage as a response
|
||||||
|
return flowProvider.EndWithResponse(new PoloMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -63,16 +63,28 @@ namespace Test
|
|||||||
* 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)");
|
||||||
|
|
||||||
return new PoloConfirmationResponseMessage
|
return new PoloConfirmationResponseMessage
|
||||||
{
|
{
|
||||||
ShouldMatchState = message.StoredInState
|
ShouldMatchState = message.StoredInState
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public void Polo(PoloMessage message)
|
||||||
|
{
|
||||||
|
Console.WriteLine(">> Polo");
|
||||||
|
StateTestGuid = Guid.NewGuid();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Request(Response = typeof(PoloMessage))]
|
||||||
public class MarcoMessage
|
public class MarcoMessage
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
@ -17,20 +17,33 @@ namespace Test
|
|||||||
|
|
||||||
public async Task Run()
|
public async Task Run()
|
||||||
{
|
{
|
||||||
|
await publisher.Publish(new MarcoMessage());
|
||||||
|
|
||||||
|
/*
|
||||||
var concurrent = new SemaphoreSlim(20);
|
var concurrent = new SemaphoreSlim(20);
|
||||||
|
|
||||||
//for (var x = 0; x < 5000; x++)
|
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
await concurrent.WaitAsync();
|
for (var x = 0; x < 200; x++)
|
||||||
try
|
|
||||||
{
|
{
|
||||||
await publisher.Publish(new MarcoMessage());
|
await concurrent.WaitAsync();
|
||||||
}
|
try
|
||||||
finally
|
{
|
||||||
{
|
await publisher.Publish(new MarcoMessage());
|
||||||
concurrent.Release();
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
concurrent.Release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await Task.Delay(1000);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
await Task.Delay(1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,15 +13,21 @@ namespace Test
|
|||||||
var container = new Container();
|
var container = new Container();
|
||||||
container.Register<MarcoEmitter>();
|
container.Register<MarcoEmitter>();
|
||||||
container.Register<Visualizer>();
|
container.Register<Visualizer>();
|
||||||
//container.RegisterSingleton<ISagaStore, SagaMemoryStore>();
|
|
||||||
|
|
||||||
var config = new TapetiConfig("test", new SimpleInjectorDependencyResolver(container))
|
var config = new TapetiConfig("test", new SimpleInjectorDependencyResolver(container))
|
||||||
.Use(new FlowMiddleware())
|
.WithFlow()
|
||||||
.RegisterAllControllers()
|
.RegisterAllControllers()
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
using (var connection = new TapetiConnection(config))
|
using (var connection = new TapetiConnection(config)
|
||||||
{
|
{
|
||||||
|
Params = new TapetiConnectionParams
|
||||||
|
{
|
||||||
|
HostName = "localhost",
|
||||||
|
PrefetchCount = 200
|
||||||
|
}
|
||||||
|
})
|
||||||
|
{
|
||||||
Console.WriteLine("Subscribing...");
|
Console.WriteLine("Subscribing...");
|
||||||
connection.Subscribe().Wait();
|
connection.Subscribe().Wait();
|
||||||
Console.WriteLine("Done!");
|
Console.WriteLine("Done!");
|
||||||
|
@ -7,9 +7,9 @@ using System.Runtime.InteropServices;
|
|||||||
[assembly: AssemblyTitle("Test")]
|
[assembly: AssemblyTitle("Test")]
|
||||||
[assembly: AssemblyDescription("")]
|
[assembly: AssemblyDescription("")]
|
||||||
[assembly: AssemblyConfiguration("")]
|
[assembly: AssemblyConfiguration("")]
|
||||||
[assembly: AssemblyCompany("Hewlett-Packard Company")]
|
[assembly: AssemblyCompany("")]
|
||||||
[assembly: AssemblyProduct("Test")]
|
[assembly: AssemblyProduct("Test")]
|
||||||
[assembly: AssemblyCopyright("Copyright © Hewlett-Packard Company 2016")]
|
[assembly: AssemblyCopyright("")]
|
||||||
[assembly: AssemblyTrademark("")]
|
[assembly: AssemblyTrademark("")]
|
||||||
[assembly: AssemblyCulture("")]
|
[assembly: AssemblyCulture("")]
|
||||||
|
|
||||||
|
@ -11,9 +11,9 @@ namespace Test
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task VisualizePolo(bool matches)
|
public Task VisualizePolo()
|
||||||
{
|
{
|
||||||
Console.WriteLine(matches ? "Polo!" : "Oops! Mismatch!");
|
Console.WriteLine("Polo!");
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user