Added NoRequestsBehaviour to ParallelFlow.Yield

This commit is contained in:
Mark van Renswoude 2021-12-10 11:45:09 +01:00
parent bc00d476bd
commit 58d1908047
6 changed files with 146 additions and 68 deletions

View File

@ -85,6 +85,15 @@ namespace _03_FlowRequestResponse
Console.WriteLine("[ParallelFlowController] Second quote: " + SecondQuote);
Console.WriteLine("[ParallelFlowController] Third quote: " + ThirdQuote);
return flowProvider.YieldWithParallelRequest()
.YieldSync(ImmediateConvergeTest, FlowNoRequestsBehaviour.Converge);
}
private IYieldPoint ImmediateConvergeTest()
{
Console.WriteLine("[ParallelFlowController] Second parallel flow immediately converged");
exampleState.Done();
return flowProvider.End();
}

View File

@ -1,4 +1,5 @@
using System;
using System.Diagnostics;
using System.Threading.Tasks;
namespace Tapeti.Flow.Default
@ -12,13 +13,13 @@ namespace Tapeti.Flow.Default
public Guid ContinuationID { get; set; }
public ContinuationMetadata ContinuationMetadata { get; set; }
private bool storeCalled;
private bool deleteCalled;
private int storeCalled;
private int deleteCalled;
public async Task Store(bool persistent)
{
storeCalled = true;
storeCalled++;
if (HandlerContext == null) throw new ArgumentNullException(nameof(HandlerContext));
if (FlowState == null) throw new ArgumentNullException(nameof(FlowState));
@ -30,7 +31,7 @@ namespace Tapeti.Flow.Default
public async Task Delete()
{
deleteCalled = true;
deleteCalled++;
if (FlowStateLock != null)
await FlowStateLock.DeleteFlowState();
@ -38,13 +39,16 @@ namespace Tapeti.Flow.Default
public bool IsStoredOrDeleted()
{
return storeCalled || deleteCalled;
return storeCalled > 0 || deleteCalled > 0;
}
public void EnsureStoreOrDeleteIsCalled()
{
if (!IsStoredOrDeleted())
throw new InvalidProgramException("Neither Store nor Delete are called for the state of the current flow. FlowID = " + FlowStateLock?.FlowID);
Debug.Assert(storeCalled <= 1, "Store called more than once!");
Debug.Assert(deleteCalled <= 1, "Delete called more than once!");
}
public void Dispose()

View File

@ -44,9 +44,10 @@ namespace Tapeti.Flow.Default
await next();
if (flowPayload.FlowIsConverging)
await CallConvergeMethod(context, controllerPayload,
flowContext.ContinuationMetadata.ConvergeMethodName,
flowContext.ContinuationMetadata.ConvergeMethodSync);
{
var flowHandler = flowContext.HandlerContext.Config.DependencyResolver.Resolve<IFlowHandler>();
await flowHandler.Converge(new FlowHandlerContext(context));
}
}
else
await next();
@ -121,28 +122,5 @@ namespace Tapeti.Flow.Default
context.Store(new FlowMessageContextPayload(flowContext));
return flowContext;
}
private static async Task CallConvergeMethod(IMessageContext context, ControllerMessageContextPayload controllerPayload, string methodName, bool sync)
{
IYieldPoint yieldPoint;
var method = controllerPayload.Controller.GetType().GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance);
if (method == null)
throw new ArgumentException($"Unknown converge method in controller {controllerPayload.Controller.GetType().Name}: {methodName}");
if (sync)
yieldPoint = (IYieldPoint)method.Invoke(controllerPayload.Controller, new object[] {});
else
yieldPoint = await (Task<IYieldPoint>)method.Invoke(controllerPayload.Controller, new object[] { });
if (yieldPoint == null)
throw new YieldPointException($"Yield point is required in controller {controllerPayload.Controller.GetType().Name} for converge method {methodName}");
var flowHandler = context.Config.DependencyResolver.Resolve<IFlowHandler>();
await flowHandler.Execute(new FlowHandlerContext(context), yieldPoint);
}
}
}

View File

@ -48,7 +48,7 @@ namespace Tapeti.Flow.Default
/// <inheritdoc />
public IFlowParallelRequestBuilder YieldWithParallelRequest()
{
return new ParallelRequestBuilder(config, SendRequest);
return new ParallelRequestBuilder(config, this);
}
/// <inheritdoc />
@ -64,8 +64,8 @@ namespace Tapeti.Flow.Default
}
private async Task SendRequest(FlowContext context, object message, ResponseHandlerInfo responseHandlerInfo,
string convergeMethodName = null, bool convergeMethodTaskSync = false)
internal async Task SendRequest(FlowContext context, object message, ResponseHandlerInfo responseHandlerInfo,
string convergeMethodName = null, bool convergeMethodTaskSync = false, bool store = true)
{
if (context.FlowState == null)
{
@ -89,7 +89,8 @@ namespace Tapeti.Flow.Default
ReplyTo = responseHandlerInfo.ReplyToQueue
};
await context.Store(responseHandlerInfo.IsDurableQueue);
if (store)
await context.Store(responseHandlerInfo.IsDurableQueue);
await publisher.Publish(message, properties, true);
}
@ -122,7 +123,7 @@ namespace Tapeti.Flow.Default
}
private static async Task EndFlow(FlowContext context)
internal static async Task EndFlow(FlowContext context)
{
await context.Delete();
@ -246,18 +247,43 @@ namespace Tapeti.Flow.Default
/// <inheritdoc />
public IFlowParallelRequest GetParallelRequest(IFlowHandlerContext context)
{
if (!context.MessageContext.TryGet<FlowMessageContextPayload>(out var flowPayload))
return null;
return new ParallelRequest(config, SendRequest, flowPayload.FlowContext);
return context.MessageContext.TryGet<FlowMessageContextPayload>(out var flowPayload)
? new ParallelRequest(config, this, flowPayload.FlowContext)
: null;
}
private delegate Task SendRequestFunc(FlowContext context,
object message,
ResponseHandlerInfo responseHandlerInfo,
string convergeMethodName,
bool convergeMethodSync);
/// <inheritdoc />
public Task Converge(IFlowHandlerContext context)
{
return Execute(context, new DelegateYieldPoint(flowContext =>
Converge(flowContext, flowContext.ContinuationMetadata.ConvergeMethodName, flowContext.ContinuationMetadata.ConvergeMethodSync)));
}
internal async Task Converge(FlowContext flowContext, string convergeMethodName, bool convergeMethodSync)
{
IYieldPoint yieldPoint;
if (!flowContext.HandlerContext.MessageContext.TryGet<ControllerMessageContextPayload>(out var controllerPayload))
throw new ArgumentException("Context does not contain a controller payload", nameof(flowContext));
var method = controllerPayload.Controller.GetType().GetMethod(convergeMethodName, BindingFlags.NonPublic | BindingFlags.Instance);
if (method == null)
throw new ArgumentException($"Unknown converge method in controller {controllerPayload.Controller.GetType().Name}: {convergeMethodName}");
if (convergeMethodSync)
yieldPoint = (IYieldPoint)method.Invoke(controllerPayload.Controller, new object[] { });
else
yieldPoint = await(Task<IYieldPoint>)method.Invoke(controllerPayload.Controller, new object[] { });
if (yieldPoint == null)
throw new YieldPointException($"Yield point is required in controller {controllerPayload.Controller.GetType().Name} for converge method {convergeMethodName}");
await Execute(flowContext.HandlerContext, yieldPoint);
}
private class ParallelRequestBuilder : IFlowParallelRequestBuilder
@ -270,14 +296,14 @@ namespace Tapeti.Flow.Default
private readonly ITapetiConfig config;
private readonly SendRequestFunc sendRequest;
private readonly FlowProvider flowProvider;
private readonly List<RequestInfo> requests = new List<RequestInfo>();
public ParallelRequestBuilder(ITapetiConfig config, SendRequestFunc sendRequest)
public ParallelRequestBuilder(ITapetiConfig config, FlowProvider flowProvider)
{
this.config = config;
this.sendRequest = sendRequest;
this.flowProvider = flowProvider;
}
@ -311,36 +337,57 @@ namespace Tapeti.Flow.Default
}
public IYieldPoint Yield(Func<Task<IYieldPoint>> continuation)
public IYieldPoint Yield(Func<Task<IYieldPoint>> continuation, FlowNoRequestsBehaviour noRequestsBehaviour = FlowNoRequestsBehaviour.Exception)
{
return BuildYieldPoint(continuation, false);
return BuildYieldPoint(continuation, false, noRequestsBehaviour);
}
public IYieldPoint YieldSync(Func<IYieldPoint> continuation)
public IYieldPoint YieldSync(Func<IYieldPoint> continuation, FlowNoRequestsBehaviour noRequestsBehaviour = FlowNoRequestsBehaviour.Exception)
{
return BuildYieldPoint(continuation, true);
return BuildYieldPoint(continuation, true, noRequestsBehaviour);
}
private IYieldPoint BuildYieldPoint(Delegate convergeMethod, bool convergeMethodSync)
private IYieldPoint BuildYieldPoint(Delegate convergeMethod, bool convergeMethodSync, FlowNoRequestsBehaviour noRequestsBehaviour = FlowNoRequestsBehaviour.Exception)
{
if (requests.Count == 0)
throw new YieldPointException("At least one request must be added before yielding a parallel request");
{
switch (noRequestsBehaviour)
{
case FlowNoRequestsBehaviour.Exception:
throw new YieldPointException("At least one request must be added before yielding a parallel request");
case FlowNoRequestsBehaviour.Converge:
return new DelegateYieldPoint(context =>
flowProvider.Converge(context, convergeMethod.Method.Name, convergeMethodSync));
case FlowNoRequestsBehaviour.EndFlow:
return new DelegateYieldPoint(EndFlow);
default:
throw new ArgumentOutOfRangeException(nameof(noRequestsBehaviour), noRequestsBehaviour, null);
}
}
if (convergeMethod?.Method == null)
throw new ArgumentNullException(nameof(convergeMethod));
return new DelegateYieldPoint(context =>
return new DelegateYieldPoint(async context =>
{
if (convergeMethod.Method.DeclaringType != context.HandlerContext.Controller.GetType())
throw new YieldPointException("Converge method must be in the same controller class");
return Task.WhenAll(requests.Select(requestInfo =>
sendRequest(context, requestInfo.Message,
await Task.WhenAll(requests.Select(requestInfo =>
flowProvider.SendRequest(
context,
requestInfo.Message,
requestInfo.ResponseHandlerInfo,
convergeMethod.Method.Name,
convergeMethodSync)));
convergeMethodSync,
false)));
await context.Store(requests.Any(i => i.ResponseHandlerInfo.IsDurableQueue));
});
}
}
@ -349,14 +396,14 @@ namespace Tapeti.Flow.Default
private class ParallelRequest : IFlowParallelRequest
{
private readonly ITapetiConfig config;
private readonly SendRequestFunc sendRequest;
private readonly FlowProvider flowProvider;
private readonly FlowContext flowContext;
public ParallelRequest(ITapetiConfig config, SendRequestFunc sendRequest, FlowContext flowContext)
public ParallelRequest(ITapetiConfig config, FlowProvider flowProvider, FlowContext flowContext)
{
this.config = config;
this.sendRequest = sendRequest;
this.flowProvider = flowProvider;
this.flowContext = flowContext;
}
@ -382,7 +429,14 @@ namespace Tapeti.Flow.Default
private Task InternalAddRequest(object message, Delegate responseHandler)
{
var responseHandlerInfo = GetResponseHandlerInfo(config, message, responseHandler);
return sendRequest(flowContext, message, responseHandlerInfo, flowContext.ContinuationMetadata.ConvergeMethodName, flowContext.ContinuationMetadata.ConvergeMethodSync);
return flowProvider.SendRequest(
flowContext,
message,
responseHandlerInfo,
flowContext.ContinuationMetadata.ConvergeMethodName,
flowContext.ContinuationMetadata.ConvergeMethodSync,
false);
}
}

View File

@ -115,9 +115,40 @@ namespace Tapeti.Flow
/// Returns the parallel request for the given message context.
/// </summary>
IFlowParallelRequest GetParallelRequest(IFlowHandlerContext context);
/// <summary>
/// Calls the converge method for a parallel flow.
/// </summary>
Task Converge(IFlowHandlerContext context);
}
/// <summary>
/// Determines how the Yield method of a parallel request behaves when no requests have been added.
/// Useful in cases where requests are sent conditionally.
/// </summary>
public enum FlowNoRequestsBehaviour
{
/// <summary>
/// Throw an exception. This is the default behaviour to prevent subtle bugs when not specifying the behaviour explicitly,
/// as well as for backwards compatibility.
/// </summary>
Exception,
/// <summary>
/// Immediately call the continuation method.
/// </summary>
Converge,
/// <summary>
/// End the flow without calling the converge method.
/// </summary>
EndFlow
}
/// <summary>
/// Builder to publish one or more request messages and continuing the flow when the responses arrive.
/// </summary>
@ -152,7 +183,6 @@ namespace Tapeti.Flow
/// There is no Sync overload with an IFlowParallelRequest parameter, as the AddRequest methods for that are
/// async, so you should always await them.
/// <summary>
/// Constructs an IYieldPoint to continue the flow when responses arrive.
/// The continuation method is called when all responses have arrived.
@ -160,8 +190,9 @@ namespace Tapeti.Flow
/// controller and can store state.
/// Used for asynchronous continuation methods.
/// </summary>
/// <param name="continuation"></param>
IYieldPoint Yield(Func<Task<IYieldPoint>> continuation);
/// <param name="continuation">The converge continuation method to be called when all responses have been handled.</param>
/// <param name="noRequestsBehaviour">How the Yield method should behave when no requests have been added to the parallel request builder.</param>
IYieldPoint Yield(Func<Task<IYieldPoint>> continuation, FlowNoRequestsBehaviour noRequestsBehaviour = FlowNoRequestsBehaviour.Exception);
/// <summary>
/// Constructs an IYieldPoint to continue the flow when responses arrive.
@ -170,8 +201,9 @@ namespace Tapeti.Flow
/// controller and can store state.
/// Used for synchronous continuation methods.
/// </summary>
/// <param name="continuation"></param>
IYieldPoint YieldSync(Func<IYieldPoint> continuation);
/// <param name="continuation">The converge continuation method to be called when all responses have been handled.</param>
/// <param name="noRequestsBehaviour">How the Yield method should behave when no requests have been added to the parallel request builder.</param>
IYieldPoint YieldSync(Func<IYieldPoint> continuation, FlowNoRequestsBehaviour noRequestsBehaviour = FlowNoRequestsBehaviour.Exception);
}

View File

@ -945,8 +945,9 @@ namespace Tapeti.Connection
if (returnInfo.RefCount == 0)
returnRoutingKeys.Remove(messageInfo.ReturnKey);
}
else
messageInfo.CompletionSource.SetResult(0);
messageInfo.CompletionSource.SetResult(0);
confirmMessages.Remove(deliveryTag);
}
}