How to implement a state machine with Automatonymous in C# - c#

I am trying to implement a simple example/demo for a state machine using Automatonymous with RabbitMQ. Unfortunately I could not find one to rebuild / learn from (I found the ShoppingWeb, but in my eyes it's anything but simple). Also in my opinion the documentation is lacking information.
This is the state machine example I thought of (sorry, it's pretty ugly):
Please note that this example is completely made up and it's not important if it makes sense or not. This project's purpose is to get "warm" with Automatonymous.
What I want to do / to have is:
Four applications running:
The state machine itself
The "requester" sending requests to be interpreted
The "validator" or "parser" checking if the provided request is valid
The "interpreter" interpreting the given request
An example of this could be:
Requester sends "x=5"
Validator checks if a "=" is contained
Intepreter says "5"
My implementation of the state machine looks like this:
public class InterpreterStateMachine : MassTransitStateMachine<InterpreterInstance>
{
public InterpreterStateMachine()
{
InstanceState(x => x.CurrentState);
Event(() => Requesting, x => x.CorrelateBy(request => request.Request.RequestString, context => context.Message.Request.RequestString)
.SelectId(context => Guid.NewGuid()));
Event(() => Validating, x => x.CorrelateBy(request => request.Request.RequestString, context => context.Message.Request.RequestString));
Event(() => Interpreting, x => x.CorrelateBy(request => request.Request.RequestString, context => context.Message.Request.RequestString));
Initially(
When(Requesting)
.Then(context =>
{
context.Instance.Request = new Request(context.Data.Request.RequestString);
})
.ThenAsync(context => Console.Out.WriteLineAsync($"Request received: {context.Data.Request.RequestString}"))
.Publish(context => new ValidationNeededEvent(context.Instance))
.TransitionTo(Requested)
);
During(Requested,
When(Validating)
.Then(context =>
{
context.Instance.Request.IsValid = context.Data.Request.IsValid;
if (!context.Data.Request.IsValid)
{
this.TransitionToState(context.Instance, Error);
}
else
{
this.TransitionToState(context.Instance, RequestValid);
}
})
.ThenAsync(context => Console.Out.WriteLineAsync($"Request '{context.Data.Request.RequestString}' validated with {context.Instance.Request.IsValid}"))
.Publish(context => new InterpretationNeededEvent(context.Instance))
,
Ignore(Requesting),
Ignore(Interpreting)
);
During(RequestValid,
When(Interpreting)
.Then((context) =>
{
//do something
})
.ThenAsync(context => Console.Out.WriteLineAsync($"Request '{context.Data.Request.RequestString}' interpreted with {context.Data.Answer}"))
.Publish(context => new AnswerReadyEvent(context.Instance))
.TransitionTo(AnswerReady)
.Finalize(),
Ignore(Requesting),
Ignore(Validating)
);
SetCompletedWhenFinalized();
}
public State Requested { get; private set; }
public State RequestValid { get; private set; }
public State AnswerReady { get; private set; }
public State Error { get; private set; }
//Someone is sending a request to interprete
public Event<IRequesting> Requesting { get; private set; }
//Request is validated
public Event<IValidating> Validating { get; private set; }
//Request is interpreted
public Event<IInterpreting> Interpreting { get; private set; }
class ValidationNeededEvent : IValidationNeeded
{
readonly InterpreterInstance _instance;
public ValidationNeededEvent(InterpreterInstance instance)
{
_instance = instance;
}
public Guid RequestId => _instance.CorrelationId;
public Request Request => _instance.Request;
}
class InterpretationNeededEvent : IInterpretationNeeded
{
readonly InterpreterInstance _instance;
public InterpretationNeededEvent(InterpreterInstance instance)
{
_instance = instance;
}
public Guid RequestId => _instance.CorrelationId;
}
class AnswerReadyEvent : IAnswerReady
{
readonly InterpreterInstance _instance;
public AnswerReadyEvent(InterpreterInstance instance)
{
_instance = instance;
}
public Guid RequestId => _instance.CorrelationId;
}
}
Then I have services like this:
public class RequestService : ServiceControl
{
readonly IScheduler scheduler;
IBusControl busControl;
BusHandle busHandle;
InterpreterStateMachine machine;
InMemorySagaRepository<InterpreterInstance> repository;
public RequestService()
{
scheduler = CreateScheduler();
}
public bool Start(HostControl hostControl)
{
Console.WriteLine("Creating bus...");
machine = new InterpreterStateMachine();
repository = new InMemorySagaRepository<InterpreterInstance>();
busControl = Bus.Factory.CreateUsingRabbitMq(x =>
{
IRabbitMqHost host = x.Host(new Uri(/*rabbitMQ server*/), h =>
{
/*credentials*/
});
x.UseInMemoryScheduler();
x.ReceiveEndpoint(host, "interpreting_answer", e =>
{
e.PrefetchCount = 5; //?
e.StateMachineSaga(machine, repository);
});
x.ReceiveEndpoint(host, "2", e =>
{
e.PrefetchCount = 1;
x.UseMessageScheduler(e.InputAddress);
//Scheduling !?
e.Consumer(() => new ScheduleMessageConsumer(scheduler));
e.Consumer(() => new CancelScheduledMessageConsumer(scheduler));
});
});
Console.WriteLine("Starting bus...");
try
{
busHandle = MassTransit.Util.TaskUtil.Await<BusHandle>(() => busControl.StartAsync());
scheduler.JobFactory = new MassTransitJobFactory(busControl);
scheduler.Start();
}
catch (Exception)
{
scheduler.Shutdown();
throw;
}
return true;
}
public bool Stop(HostControl hostControl)
{
Console.WriteLine("Stopping bus...");
scheduler.Standby();
if (busHandle != null) busHandle.Stop();
scheduler.Shutdown();
return true;
}
static IScheduler CreateScheduler()
{
ISchedulerFactory schedulerFactory = new StdSchedulerFactory();
IScheduler scheduler = MassTransit.Util.TaskUtil.Await<IScheduler>(() => schedulerFactory.GetScheduler()); ;
return scheduler;
}
}
My questions are:
How do I send the "intial" request, so that the state machine will transition to my initial state
How do I "react" within the consumers to check the data that were sent and then send new data like in 1?

Okay I figured it out. I probably had problems because I'm not only new to Masstransit/Automatonymous and RabbitMQ, but also don't have much experience with C# yet.
So if anyone ever will have the same problem, here is what you need:
Given the above example there are three different types plus some small interfaces needed:
A sender (in this case the "requester") including a specific consumer
A service that consumes specific message types (the "validator" and "interpreter")
A service that holds the state machine without a specific consumer
Some "contracts", which are interfaces defining the type of message that's sent/consumed
1) This is the sender:
using InterpreterStateMachine.Contracts;
using MassTransit;
using System;
using System.Threading.Tasks;
namespace InterpreterStateMachine.Requester
{
class Program
{
private static IBusControl _busControl;
static void Main(string[] args)
{
var busControl = ConfigureBus();
busControl.Start();
Console.WriteLine("Enter request or quit to exit: ");
while (true)
{
Console.Write("> ");
String value = Console.ReadLine();
if ("quit".Equals(value,StringComparison.OrdinalIgnoreCase))
break;
if (value != null)
{
String[] values = value.Split(';');
foreach (String v in values)
{
busControl.Publish<IRequesting>(new
{
Request = new Request(v),
TimeStamp = DateTime.UtcNow
});
}
}
}
busControl.Stop();
}
static IBusControl ConfigureBus()
{
if (null == _busControl)
{
_busControl = Bus.Factory.CreateUsingRabbitMq(cfg =>
{
var host = cfg.Host(new Uri(/*rabbitMQ server*/), h =>
{
/*credentials*/
});
cfg.ReceiveEndpoint(host, "answer_ready", e =>
{
e.Durable = true;
//here the consumer is registered
e.Consumer<AnswerConsumer>();
});
});
_busControl.Start();
}
return _busControl;
}
//here comes the actual logic of the consumer, which consumes a "contract"
class AnswerConsumer : IConsumer<IAnswerReady>
{
public async Task Consume(ConsumeContext<IAnswerReady> context)
{
await Console.Out.WriteLineAsync($"\nReceived Answer for \"{context.Message.Request.RequestString}\": {context.Message.Answer}.");
await Console.Out.WriteAsync(">");
}
}
}
}
2) This is the service (here it is the validation sercive)
using InterpreterStateMachine.Contracts;
using MassTransit;
using MassTransit.QuartzIntegration;
using MassTransit.RabbitMqTransport;
using Quartz;
using Quartz.Impl;
using System;
using System.Threading.Tasks;
using Topshelf;
namespace InterpreterStateMachine.Validator
{
public class ValidationService : ServiceControl
{
readonly IScheduler _scheduler;
static IBusControl _busControl;
BusHandle _busHandle;
public static IBus Bus => _busControl;
public ValidationService()
{
_scheduler = CreateScheduler();
}
public bool Start(HostControl hostControl)
{
Console.WriteLine("Creating bus...");
_busControl = MassTransit.Bus.Factory.CreateUsingRabbitMq(x =>
{
IRabbitMqHost host = x.Host(new Uri(/*rabbitMQ server*/), h =>
{
/*credentials*/
});
x.UseInMemoryScheduler();
x.UseMessageScheduler(new Uri(RabbitMqServerAddress));
x.ReceiveEndpoint(host, "validation_needed", e =>
{
e.PrefetchCount = 1;
e.Durable = true;
//again this is how the consumer is registered
e.Consumer<RequestConsumer>();
});
});
Console.WriteLine("Starting bus...");
try
{
_busHandle = MassTransit.Util.TaskUtil.Await<BusHandle>(() => _busControl.StartAsync());
_scheduler.JobFactory = new MassTransitJobFactory(_busControl);
_scheduler.Start();
}
catch (Exception)
{
_scheduler.Shutdown();
throw;
}
return true;
}
public bool Stop(HostControl hostControl)
{
Console.WriteLine("Stopping bus...");
_scheduler.Standby();
_busHandle?.Stop();
_scheduler.Shutdown();
return true;
}
static IScheduler CreateScheduler()
{
ISchedulerFactory schedulerFactory = new StdSchedulerFactory();
IScheduler scheduler = MassTransit.Util.TaskUtil.Await<IScheduler>(() => schedulerFactory.GetScheduler());
return scheduler;
}
}
//again here comes the actual consumer logic, look how the message is re-published after it was checked
class RequestConsumer : IConsumer<IValidationNeeded>
{
public async Task Consume(ConsumeContext<IValidationNeeded> context)
{
await Console.Out.WriteLineAsync($"(c) Received {context.Message.Request.RequestString} for validation (Id: {context.Message.RequestId}).");
context.Message.Request.IsValid = context.Message.Request.RequestString.Contains("=");
//send the new message on the "old" context
await context.Publish<IValidating>(new
{
Request = context.Message.Request,
IsValid = context.Message.Request.IsValid,
TimeStamp = DateTime.UtcNow,
RequestId = context.Message.RequestId
});
}
}
}
The validator consumes the contract "IValidationNeeded" and then publishes the contract "IValidating", which then will be consumed by the state machine itself (the "Validating" event).
3) The difference between a consumer service and the sate machine service lies withing the "ReceiveEndpoint". Here is no consumer registered, but the state machine is set:
...
InterpreterStateMachine _machine = new InterpreterStateMachine();
InMemorySagaRepository<InterpreterInstance> _repository = new InMemorySagaRepository<InterpreterInstance>();
...
x.ReceiveEndpoint(host, "state_machine", e =>
{
e.PrefetchCount = 1;
//here the state machine is set
e.StateMachineSaga(_machine, _repository);
e.Durable = false;
});
4) Last but not least, the contracts are pretty small and look like this:
using System;
namespace InterpreterStateMachine.Contracts
{
public interface IValidationNeeded
{
Guid RequestId { get; }
Request Request { get; }
}
}
So overall it's pretty straightforward, I just had to use my brain :D
I hope this will help someone.

Related

Make method more Rx like by replacing List<T> and TaskCompletionSource<T>

This is a request-response model (like HTTP) but over sockets/websockets. We know which response corresponds to which request by comparing the request IDs.
The workflow is as following:
Subscribe to the observables Error, ContractDetails (_itemObservable) and ContractDetailsEnd (_itemEndObservable)
Match request id to response id and push the corresponding messages via .OnNext(...) to ContractDetails. When there is nothing else left to push, push a final message to ContractDetailsEnd which essentially does .OnCompleted.
Cleanup – unsubscribe, i.e. dispose the observables
In the first snippet below I use a List<TItemArgs>, CancellationTokenSource and a TaskCompletionSource<TItemArgs>. That's completely unnecessary if everything was Rx style. It would have definitely been less lines of code too.
The second snippet is my personal attempt to make it look more Rx like. It has some issues that I want to resolve:
I don't think the try/catch block is necessary since .Subscribe could handle errors
.Timeout didn't work out for me – if the request is not matched within a few seconds, it should return a Result<IEnumerable<TItemArgs>>.FromError(new TimeoutError(...))
In addition to the errors, a message pushed to _errorSubject should also return an error such as Result<IEnumerable<TItemArgs>>.FromError(new RemoteError(...)) but .Merge(errorMessages.Any(_ => false)) is not working.
In my attempt I use ReplaySubject in opposed to AsyncSubject because I believe AsyncSubject is only useful when I'm only interested in the last value of the sequence and want to avoid getting all previous values, which is not in my case. In my case I want to return all values, so ReplaySubject would be more suitable as it keeps track of all the previous values and wants all subscribers to receive the same values, regardless of when they subscribed.
public async ValueTask<Result<IEnumerable<TItemArgs>>> ExecuteAsync(Action<int> action)
{
var requestId = _client.GetNextRequestId();
var data = new List<TItemArgs>();
var cts = new CancellationTokenSource(_timeout);
var tcs = new TaskCompletionSource<IEnumerable<TItemArgs>>();
cts.Token.Register(() =>
{
tcs.TrySetCanceled();
}, false);
void OnError(ErrorData msg)
{
tcs.SetException(new IBClientException(msg.RequestId, msg.Code, msg.Message, msg.AdvancedOrderRejectJson));
}
void OnDetails(TItemArgs item)
{
data.Add(item);
}
void OnDetailsEnd(TItemListEndArgs item)
{
tcs.TrySetResult(data);
}
var disposable = new CompositeDisposable();
_client.Error
.Where(e => HasRequestId && e.RequestId == requestId)
.Subscribe(OnError)
.DisposeWith(disposable);
_itemObservable
.Where(item => MatchRequest(item, _itemRequestIdExtractor, requestId))
.Subscribe(OnDetails)
.DisposeWith(disposable);
_itemEndObservable
.Where(item => MatchRequest(item, _itemListEndRequestIdExtractor, requestId))
.Subscribe(OnDetailsEnd)
.DisposeWith(disposable);
action(requestId);
try
{
await tcs.Task.ContinueWith(x =>
{
disposable.Dispose();
cts.Dispose();
}, TaskContinuationOptions.RunContinuationsAsynchronously);
return Result<IEnumerable<TItemArgs>>.FromSuccess(tcs.Task.Result);
}
catch (Exception e)
{
return Result<IEnumerable<TItemArgs>>.FromError(new RemoteError(e.Message, null));
}
}
My attempt
using System.Reactive.Concurrency;
using System.Reactive.Linq;
using System.Reactive.Subjects;
var client = new IBClient();
var result = await client.GetContractDetailsAsync();
if (result.Success)
{
foreach (var item in result.Data!)
{
Console.WriteLine($"RequestId: {item.RequestId} | Data: {item.ContractDetails}");
}
}
public sealed class IBClient
{
private int _nextValidId;
private readonly Subject<ErrorData> _errorSubject = new();
public IObservable<ErrorData> Error => _errorSubject.AsObservable();
private readonly Subject<ContractDetailsData> _contractDetailsSubject = new();
public IObservable<ContractDetailsData> ContractDetails => _contractDetailsSubject.AsObservable();
private readonly Subject<RequestEndData> _contractDetailsEndSubject = new();
public IObservable<RequestEndData> ContractDetailsEnd => _contractDetailsEndSubject.AsObservable();
public ValueTask<Result<IEnumerable<ContractDetailsData>>> GetContractDetailsAsync()
{
return new PendingRequest<ContractDetailsData, RequestEndData>(
this,
ContractDetails,
ContractDetailsEnd,
e => e.RequestId,
e => e.RequestId)
.ExecuteAsync(reqId => TestCall(reqId));
}
private void TestCall(int requestId)
{
_contractDetailsSubject.OnNext(new ContractDetailsData(requestId, "hey from test call"));
_contractDetailsSubject.OnNext(new ContractDetailsData(requestId, "hey two"));
// TODO: Errors doesn't seem to work
// _errorSubject.OnNext(new ErrorData(requestId, 123, "Error happened", ""));
_contractDetailsEndSubject.OnNext(new RequestEndData(requestId));
// There shouldn't be matched.
_contractDetailsSubject.OnNext(new ContractDetailsData(123, "fake ones, so we know it works"));
_contractDetailsEndSubject.OnNext(new RequestEndData(123));
}
public int GetNextRequestId()
{
return Interlocked.Increment(ref _nextValidId);
}
}
public sealed class PendingRequest<TItemArgs, TItemListEndArgs>
{
private readonly TimeSpan _timeout = TimeSpan.FromSeconds(2);
private readonly IBClient _client;
private readonly IObservable<TItemArgs> _itemObservable;
private readonly IObservable<TItemListEndArgs> _itemEndObservable;
private readonly Func<TItemArgs, int>? _itemRequestIdExtractor;
private readonly Func<TItemListEndArgs, int>? _itemListEndRequestIdExtractor;
public PendingRequest(
IBClient client,
IObservable<TItemArgs> itemObservable,
IObservable<TItemListEndArgs> itemEndObservable,
Func<TItemArgs, int>? itemRequestIdExtractor = null,
Func<TItemListEndArgs, int>? itemListEndRequestIdExtractor = null)
{
_client = client;
_itemObservable = itemObservable;
_itemEndObservable = itemEndObservable;
_itemRequestIdExtractor = itemRequestIdExtractor;
_itemListEndRequestIdExtractor = itemListEndRequestIdExtractor;
}
private bool HasRequestId => _itemRequestIdExtractor != null && _itemListEndRequestIdExtractor != null;
public async ValueTask<Result<IEnumerable<TItemArgs>>> ExecuteAsync(Action<int> action, IScheduler? scheduler = null)
{
scheduler ??= ImmediateScheduler.Instance;
var requestId = _client.GetNextRequestId();
var results = new ReplaySubject<TItemArgs>();
try
{
var errorMessages = _client.Error
.Where(e => HasRequestId && e.RequestId == requestId);
using (_itemObservable
.Where(item => MatchRequest(item, _itemRequestIdExtractor, requestId))
.ObserveOn(scheduler)
.Subscribe(results))
using (_itemEndObservable
.Any(item => MatchRequest(item, _itemListEndRequestIdExtractor, requestId))
.Merge(errorMessages.Any(_ => false)) // TODO: ???
.ObserveOn(scheduler)
.Subscribe(_ => results.OnCompleted()))
{
action(requestId);
// Don't want an Exception thrown if there result list is empty
await results.DefaultIfEmpty();
return Result<IEnumerable<TItemArgs>>.FromSuccess(results.ToEnumerable());
}
}
catch (Exception ex)
{
return Result<IEnumerable<TItemArgs>>.FromError(new RemoteError(ex.Message, null));
}
}
private bool MatchRequest<T>(T item, Func<T, int>? idExtractor, int id)
{
return !HasRequestId || (idExtractor != null && idExtractor(item) == id);
}
}
public sealed class ContractDetailsData
{
public ContractDetailsData(int requestId, string contractDetails)
{
RequestId = requestId;
ContractDetails = contractDetails;
}
public int RequestId { get; }
public string ContractDetails { get; }
}
public sealed class ErrorData
{
public ErrorData(int requestId, int code, string message, string advancedOrderRejectJson)
{
RequestId = requestId;
Code = code;
Message = message;
AdvancedOrderRejectJson = advancedOrderRejectJson;
}
public int RequestId { get; }
public int Code { get; }
public string Message { get; }
public string AdvancedOrderRejectJson { get; }
}
public sealed class RequestEndData
{
public RequestEndData(int requestId)
{
RequestId = requestId;
}
public int RequestId { get; }
}
public class IBClientException : Exception
{
public IBClientException(int requestId, int errorCode, string message, string advancedOrderRejectJson)
: base(message)
{
RequestId = requestId;
ErrorCode = errorCode;
AdvancedOrderRejectJson = advancedOrderRejectJson;
}
public IBClientException(string err)
: base(err)
{
}
public IBClientException(Exception e)
{
Exception = e;
}
public int RequestId { get; }
public int ErrorCode { get; }
public string? AdvancedOrderRejectJson { get; }
public Exception? Exception { get; }
}
public abstract record Error(int? Code, string Message, object? Data);
public record RemoteError : Error
{
public RemoteError(string message, object? data) : base(null, message, data)
{
}
public RemoteError(int? code, string message, object? data) : base(code, message, data)
{
}
}
public record Result<T>(bool Success, T? Data, Error? Error)
{
public Result(T data) : this(true, data, default)
{
}
public Result(Error error) : this(false, default, error)
{
}
public static Result<T> FromSuccess(T data)
{
return new Result<T>(data);
}
public static Result<T> FromError<TError>(TError error) where TError : Error
{
return new Result<T>(error);
}
}
public static class DisposableExtensions
{
public static T DisposeWith<T>(this T disposable, ICollection<IDisposable> collection)
where T : IDisposable
{
ArgumentNullException.ThrowIfNull(disposable);
ArgumentNullException.ThrowIfNull(collection);
collection.Add(disposable);
return disposable;
}
}
Here's a good start:
IObservable<TItemArgs> observable =
Observable
.Merge(
_itemObservable
.Where(item => MatchRequest(item, _itemRequestIdExtractor, requestId))
.Select(item => Notification.CreateOnNext(item))
.Take(1),
_itemEndObservable
.Any(item => MatchRequest(item, _itemListEndRequestIdExtractor, requestId))
.Select(item => Notification.CreateOnCompleted<TItemArgs>()))
.Dematerialize();
And here's a final observable without the ReplaySubject.
private static async Task<Result<IEnumerable<Details>>> Test(IScheduler scheduler) =>
await
Observable
.Defer(() =>
{
var client = new IBClient();
var requestId = 1;
return Observable.Create<Result<IEnumerable<Details>>>(o =>
{
IDisposable subscription =
Observable
.Merge(
client.Details.Where(x => x.RequestId == requestId).Select(x => Notification.CreateOnNext(x)),
client.DetailsEnd.Any(x => x.RequestId == requestId).Select(x => Notification.CreateOnCompleted<Details>()),
client.Error.Where(x => x.RequestId == requestId).Select(x => Notification.CreateOnError<Details>(new Exception($"Code: {x.Code}, Message: {x.Message}"))))
.Dematerialize()
.Synchronize()
.ToArray()
.Select(items => Result<IEnumerable<Details>>.FromSuccess(items))
.Catch<Result<IEnumerable<Details>>, Exception>(ex => Observable.Return(Result<IEnumerable<Details>>.FromError(new Error(null, ex.Message, null))))
.Timeout(TimeSpan.FromSeconds(2))
.ObserveOn(scheduler)
.Subscribe(o);
client.Emit(2, 4);
client.Emit(requestId, 42);
client.EmitEnd(2);
client.Emit(requestId, 62);
client.Emit(requestId, 123);
//client.EmitError(requestId, 123, "Something bad happened");
client.EmitEnd(requestId);
client.Emit(requestId, 1);
return subscription;
});
});

How to Schedule in the Initial state of MassTransit saga

I created a state machine saga that will receive multiple messages and only after a given time period elapses, I want it to continue its work. I figured the only way to do it with mass transit is to go with the scheduling capabilities of the framework.
The saga code (shortened for brevity) is given below:
public class CheckFeedSubmissionStateMachine : MassTransitStateMachine<CheckFeedSubmissionState>
{
public State? WaitingForTimeoutExpiration { get; private set; }
public State? FetchingSubmissionData { get; private set; }
public Event<CheckFeedSubmissionCommand> CheckFeedSubmissionCommandReceived { get; private set; }
public Event<FeedSubmissionListReceivedEvent> FeedSubmissionListReceived { get; private set; }
public Event<FeedSubmissionListErrorReceivedEvent> FeedSubmissionListErrorReceived { get; private set; }
public Event<FeedSubmissionResultReceivedEvent> FeedSubmissionResultReceived { get; private set; }
public Event<FeedSubmissionResultErrorReceivedEvent> FeedSubmissionResultErrorReceived { get; private set; }
public Schedule<CheckFeedSubmissionState, SchedulingCompletionTimeoutExpired> ScheduleCompletionTimeout { get; private set; }
private readonly int _scheduleDelay;
public CheckFeedSubmissionStateMachine(IOptions<SagasOptions> options)
{
_scheduleDelay = int.Parse(options.Value.CheckFeedSubmissionStateMachine["ScheduleDelay"]);
Configure();
BuildProcess();
}
private void Configure()
{
Event(
() => CheckFeedSubmissionCommandReceived,
e => e.CorrelateById(x => x.Message.PartnerGuid));
Schedule(() => ScheduleCompletionTimeout, instance => instance.SchedulingCompletionTimeoutTokenId, s =>
{
s.Delay = TimeSpan.FromSeconds(_scheduleDelay);
s.Received = r => r.CorrelateById(context => context.Message.CorrelationId);
});
InstanceState(state => state.CurrentState);
}
private void BuildProcess()
{
Initially(
When(CheckFeedSubmissionCommandReceived)
.Then(InitializeState)
.Then(StoreSubmissionIds)
.Schedule(ScheduleCompletionTimeout, ScheduleEvent)
.TransitionTo(WaitingForTimeoutExpiration));
During(WaitingForTimeoutExpiration,
When(CheckFeedSubmissionCommandReceived)
.Then(StoreSubmissionIds),
When(ScheduleCompletionTimeout.Received)
.Activity(QueueGetFeedSubmissionListRequest)
.TransitionTo(FetchingSubmissionData));
// the rest ommited for brevity
}
private void InitializeState(BehaviorContext<CheckFeedSubmissionState, CheckFeedSubmissionCommand> ctx) =>
ctx.Instance.PartnerId = ctx.Data.PartnerId;
private void StoreSubmissionIds(BehaviorContext<CheckFeedSubmissionState, CheckFeedSubmissionCommand> ctx)
{
ctx.Instance.SubmissionIdToStatusMap[ctx.Data.FeedSubmissionId] = FeedProcessingStatus.Submitted;
ctx.Instance.SubmissionIdsToCorrelationIdsMap[ctx.Data.FeedSubmissionId] = ctx.Data.CorrelationId;
}
private Task<SchedulingCompletionTimeoutExpired> ScheduleEvent<TEvent>(
ConsumeEventContext<CheckFeedSubmissionState, TEvent> ctx) where TEvent : class =>
ctx.Init<SchedulingCompletionTimeoutExpired>(new { ctx.Instance.CorrelationId });
private EventActivityBinder<CheckFeedSubmissionState, SchedulingCompletionTimeoutExpired> QueueGetFeedSubmissionListRequest(
IStateMachineActivitySelector<CheckFeedSubmissionState, SchedulingCompletionTimeoutExpired> sel) =>
sel.OfType<QueueGetFeedSubmissionListActivity>();
}
The one test that I created for it aims at checking if both published messages have been preserved in the saga, the code below:
[Fact]
public async Task GivenCheckFeedSubmissionCommand_WhenAnotherCheckFeedSubmissionCommandIsReceived_ThenTheSagaStoresBothSubmissionIds()
{
var (harness, sagaHarness) = GetTestComponents();
var partnerGuid = Guid.NewGuid();
await harness.Start();
try
{
await harness.Bus.Publish(GetInitiatingEvent("1", partnerGuid));
await Consumption<CheckFeedSubmissionCommand>(harness, sagaHarness, 1);
await harness.Bus.Publish(GetInitiatingEvent("2", partnerGuid));
await Consumption<CheckFeedSubmissionCommand>(harness, sagaHarness, 2);
var state = sagaHarness.Sagas.Contains(partnerGuid);
state.CurrentState.Should().Be("WaitingForTimeoutExpiration");
state.SubmissionIdsToCorrelationIdsMap.Should().ContainKeys("1", "2");
}
finally
{
await harness.Stop();
}
}
private static (InMemoryTestHarness, IStateMachineSagaTestHarness<CheckFeedSubmissionState, CheckFeedSubmissionStateMachine>) GetTestComponents() =>
TestHarnessFactory.Create<CheckFeedSubmissionState, CheckFeedSubmissionStateMachine>(
sp => sp
.AddSingleton(Options.Create(new SagasOptions
{
CheckFeedSubmissionStateMachine = new Dictionary<string, string>
{
["ScheduleDelay"] = "0"
}
})));
private static CheckFeedSubmissionCommand GetInitiatingEvent(string feedSubmissionId, Guid partnerGuid) =>
new(Guid.NewGuid(), "1", partnerGuid, feedSubmissionId);
private static async Task Consumption<TEvent>(
InMemoryTestHarness harness,
IStateMachineSagaTestHarness<CheckFeedSubmissionState, CheckFeedSubmissionStateMachine> sagaHarness,
int expectedCount)
where TEvent : class
{
if (expectedCount == 1)
{
var harnessConsumed = harness.Consumed.SelectAsync<TEvent>().Any();
var sagaConsumed = sagaHarness.Consumed.SelectAsync<TEvent>().Any();
await Task.WhenAll(harnessConsumed, sagaConsumed);
}
else
{
int harnessConsumedCount;
int sagaConsumedCount;
do
{
var harnessConsumedTask = harness.Consumed.SelectAsync<TEvent>().Count();
var sagaConsumedTask = sagaHarness.Consumed.SelectAsync<TEvent>().Count();
harnessConsumedCount = await harnessConsumedTask;
sagaConsumedCount = await sagaConsumedTask;
await Task.Delay(1000);
} while (harnessConsumedCount < expectedCount && sagaConsumedCount < expectedCount);
}
}
The problem is that when I invoke this line .Schedule(ScheduleCompletionTimeout, ScheduleEvent) in the Initially/When phase, it somehow interferes with state switching and the saga does not switch to the next state - it stays in the Initial state indefinitely. I confirmed it both by inspecting the state variable in the test and by setting a breakpoint in the InitializeState method - it gets hit twice. When I remove that line doing the scheduling, the test passes, though I can't do that, because I need it. Any help?
It's likely you don't have a scheduler configured for the bus with the test harness. If you had logging enabled for the test, you'd see the error in the logs.
The bus configuration for the test harness should let you add the scheduler:
configurator.UseDelayedMessageScheduler();
There is a configuration event on the test harness, OnConfigureInMemoryBus or something like that, which you can use to configure the bus.

Signal R connection state is 0 after sometime

I am using #aspnet/signalr to connect to a hub and from the hub i am calling the method to return me some data.
When i first initialize the server the connection is established normally, but if i refresh the window 3 or 4 times the client stop sending connection request to the server.
I tried logging the HubConnection, the connectionState at first is equal to 1 but after the problem accrues the connectionState is alwase equal 0
Here is how i am building the hubConnection:
buildConnection() {
this.hubConnection = new HubConnectionBuilder()
.withUrl(this.tradesService.getStockQuotationsHubUrl())
.build();
this.hubConnection.serverTimeoutInMilliseconds = 1000 * 10;
this.hubConnection.onclose(() => setTimeout(() => this.startSignalRConnection(), 2000));}
Here is how i am starting the hubConnection:
startConnection() {
this.hubConnection
.start()
.then(() => {
this.hubConnection.on('updateMethod', (data: any) => {
this.store.push([
{ type: 'update', key: data.stockID, data },
]);
});
this.dataSource = new DataSource({
store: this.store,
reshapeOnPush: true,
});
});}
Note: I am listing to 5 hubs at once in my page but the only one having problems is this one.
[Update]
The problem is from the server because when i restart the server the connection is reestablished between the client and the server, but if the client refresh or quit the page for multiple times the hub does not even try to connect to the client
public class StockQuotationsHub : Microsoft.AspNetCore.SignalR.Hub
{
private string SectorID { get; set; }
private int TradingSession { get; set; }
private int MarketID { get; set; }
public static StockQuotationExt stockQuotationExt { get; set; }
private readonly StockQuotationTicker _stockTicker;
public StockQuotationsHub(StockQuotationTicker stockTicker){
_stockTicker = stockTicker;
}
public override Task OnConnectedAsync(){
return base.OnConnectedAsync();
}
public IEnumerable<StockQuotation> GetAllStockQuotations(string[] stockID, string sectorID, int tradingSession, int marketType){
return _stockTicker.
GetAllStocks(Context.ConnectionId, stockID, sectorID, tradingSession, marketType);
}
public override async Task OnDisconnectedAsync(Exception exception){
await base.OnDisconnectedAsync(exception);
}
and here is my stock ticker class:
public IEnumerable<StockQuotation> GetAllStocks(string connectionId, string[] stockID, string sectorID, int tradingSession, int marketType)
{
_stocks = new List<StockQuotation>();
_stocks = Task.Run(async () => await GetStockQuotationModelAsync("", 0, 1, 0)).Result.ToList();
this.MaxTimeStamp = _stocks.Max(s => s.TStamp);
this.SectorID = sectorID;
this.TradingSession = tradingSession;
this.MarketID = marketType;
AddToGroups(connectionId, stockID);
if (_timer==null)
_timer = new Timer(UpdateStockPrices, null, _updateInterval, _updateInterval);
if (stockID.Length == 0)
{
return _stocks;
}
else
{
var stocksList = new List<StockQuotation>();
foreach (var stock in stockID)
{
stocksList.AddRange(_stocks.Where(s => s.StockID == stock).ToList());
}
return stocksList;
}
}
private void AddToGroups(string connectionId, string[] stockID)
{
if (_stocks.Count > 0)
{
if (stockID.Length == 0)
{
Hub.Groups.AddToGroupAsync(connectionId, "ALL");
}
else
{
foreach (var stock in stockID)
{
Hub.Groups.AddToGroupAsync(connectionId, stock);
var s = _stocks.FirstOrDefault(s => s.StockID == stock);
if(s != null)
{
s.Snapshots = new List<double>(GetStockQuotationSnapshots(stock));
}
}
}
}
}
private void AddToGroups(string connectionId, string[] stockID)
{
if (_stocks.Count > 0)
{
if (stockID.Length == 0)
{
Hub.Groups.AddToGroupAsync(connectionId, "ALL");
}
else
{
foreach (var stock in stockID)
{
Hub.Groups.AddToGroupAsync(connectionId, stock);
var s = _stocks.FirstOrDefault(s => s.StockID == stock);
if(s != null)
{
s.Snapshots = new List<double>(GetStockQuotationSnapshots(stock));
}
}
}
}
}
I really appreciated the help.
Eventually the problem was that the project type was MVC, so I changed it to be webApi
and everything worked successfully
Even if you fix it I would like to recommend you to add the following code in order to know exactly the root cause of disconnection.
First of all enable the extended debug info in the service configuration:
services.AddSignalR(o =>
{
o.EnableDetailedErrors = true;
});
Furthermore, you need to attach to the event Closed in HubConnection like that:
hubConnection.Closed += (exception) =>
{
if (exception == null)
{
Console.WriteLine("Connection closed without error.");
}
else
{
Console.WriteLine($"Connection closed due to an error: {exception}");
}
return null;
};

Consumer not getting called when unit testing MassTransit

I'm trying to write unit tests using MassTransit. When looking online, I found the best way to access the Bus would be creating it using an InMemoryTestHarness. I add my consumers and a PublishObserver to get the resulting behavior.
In the example below, I send a TestRequest message to the bus, then my consumer reads the request and puts a TestResponse message back on the bus. Finally, an observer gets the response.
I don't know if the problem is with some configuration I'm missing, or if there is some task I'm not waiting, but the request message never even arrives at the consumer.
What am I missing?
The test
[TestMethod]
public void RequestResponseBusTest()
{
var harness = new InMemoryTestHarness();
var consumer = new TestConsumer();
harness.OnConfigureInMemoryBus += c =>
{
c.ReceiveEndpoint("testqueue", e =>
e.Consumer(() => consumer));
};
var observer = new TestPublishObserver();
harness.OnConnectObservers += c =>
{
c.ConnectPublishObserver(observer);
};
harness.Start().Wait();
var bus = harness.Bus;
bus.Publish(new TestRequest() { X = 99 }).Wait();
Assert.AreEqual(1, consumer.ConsumedMessages.Count, "consumed");
Assert.AreEqual(1, observer.PublishedRequests.Count, "requests");
Assert.AreEqual(1, observer.PublishedResponses.Count, "responses");
}
And supporting classes
[Serializable]
public class TestRequest
{
public int X { get; set; }
}
[Serializable]
public class TestResponse
{
public int Y { get; set; }
}
public class TestConsumer : IConsumer<TestRequest>
{
public List<TestRequest> ConsumedMessages { get; } = new List<TestRequest>();
public Task Consume(ConsumeContext<TestRequest> context)
{
ConsumedMessages.Add(context.Message);
context.Publish(new TestResponse() { Y = 123 }).Wait();
return Task.CompletedTask;
}
}
private class TestPublishObserver : IPublishObserver
{
public List<TestRequest> PublishedRequests { get; } = new List<TestRequest>();
public List<TestResponse> PublishedResponses { get; } = new List<TestResponse>();
public Task PrePublish<T>(PublishContext<T> context) where T : class
{
return Task.CompletedTask;
}
public Task PostPublish<T>(PublishContext<T> context) where T : class
{
var msg = context.Message;
if (msg is TestRequest)
PublishedRequests.Add((TestRequest)(object)msg);
if (msg is TestResponse)
PublishedResponses.Add((TestResponse)(object)msg);
return Task.CompletedTask;
}
public Task PublishFault<T>(PublishContext<T> context, Exception exception) where T : class
{
return Task.CompletedTask;
}
}
you need to add Thread.Sleep(2000) after bus.Publish(new TestRequest() { X = 99 }).Wait();
bus.Publish does not guarantee message delivery. When you call the Wait() method, you simply wait for it to be sent, not processed
OR!!!
[TestMethod]
public void RequestResponseBusTest()
{
var harness = new InMemoryTestHarness();
var consumer = new TestConsumer();
harness.OnConfigureInMemoryBus += c =>
{
c.ReceiveEndpoint("testqueue", e =>
e.Consumer(() => consumer));
};
var observer = new TestPublishObserver();
harness.OnConnectObservers += c =>
{
c.ConnectPublishObserver(observer);
};
harness.Start().Wait();
var bus = harness.Bus;
bus.Publish(new TestRequest() { X = 99 }).Wait();
//add this line
var receivedMessage = harness.Consumed.Select<TestRequest>().FirstOrDefault();
Assert.AreEqual(1, consumer.ConsumedMessages.Count, "consumed");
Assert.AreEqual(1, observer.PublishedRequests.Count, "requests");
Assert.AreEqual(1, observer.PublishedResponses.Count, "responses");
}

ChangeToken.OnChange not fire on custom configuration provider

I'll try to create custom config provider that will take keys from database. As written in manuals, I created this provider and it's work fine. All keys are loaded on start and all works fine.
But now I'm trying to use IOptionsSnapshot and reload keys from db as they change. But nothing happens.
Can anyone tell me what's going wrong? Here is my code:
public class EFConfigProvider : ConfigurationProvider
{
private DateTime lastLoaded;
public EFConfigProvider(Action<DbContextOptionsBuilder> optionsAction)
{
OptionsAction = optionsAction;
lastLoaded = DateTime.Now;
ChangeToken.OnChange(
() => Watch(),
() => {
Thread.Sleep(250);
this.Load();
});
}
public new IChangeToken GetReloadToken()
{
return Watch();
}
Action<DbContextOptionsBuilder> OptionsAction { get; }
// Load config data from EF DB.
public override void Load()
{
this.Data.Clear();
var builder = new DbContextOptionsBuilder<ConfigContext>();
OptionsAction(builder);
using (var dbContext = new ConfigContext(builder.Options))
{
// Save Load Fact
dbContext.SaveLoadFact();
// Load Partners Settings
GetPartners(dbContext);
}
}
private IChangeToken Watch()
{
return new DatabaseChangeToken();
}
}
public class DatabaseChangeToken : IChangeToken
{
public bool HasChanged
{
get
{
return true;
}
}
public bool ActiveChangeCallbacks => false;
public IDisposable RegisterChangeCallback(Action<object> callback, object state) => EmptyDisposable.Instance;
internal class EmptyDisposable : IDisposable
{
public static EmptyDisposable Instance { get; } = new EmptyDisposable();
private EmptyDisposable() { }
public void Dispose() { }
}
}
What I did to start to worked it:
I add in class EFConfigProvider variable
private ConfigurationReloadToken _reloadToken = new ConfigurationReloadToken();
I add in constructor
// Start Periodic task to refresh the DB
PeriodicTask.Run(() =>
{
//Refresh();
OnReload();
}, TimeSpan.FromSeconds(reload));
I Add class for periodTask
public class PeriodicTask
{
public static async Task Run(Action action, TimeSpan period, CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
await Task.Delay(period, cancellationToken);
if (!cancellationToken.IsCancellationRequested)
action();
}
}
public static Task Run(Action action, TimeSpan period)
{
return Run(action, period, CancellationToken.None);
}
}
Add method on Reload
protected new void OnReload()
{
var previousToken = Interlocked.Exchange(ref _reloadToken, new ConfigurationReloadToken());
previousToken.OnReload();
}
Add change of using change token
ChangeToken.OnChange(
() => { return this._reloadToken; },
() => {
Thread.Sleep(250);
this.Load();
});
Updated: I found that overriding OnReload() broke the notification of changes (I was using OptionsMonitor, which didn't see the changes). So I changed my implementation to not do that. It's calling the default OnReload() which handles the notification of changes to OptionsMonitor.
Your updated implementation really helped me out, so thank you!
But rather than using your custom PeriodicTask, you could just use a Timer. The effect is just the same though.
Here is my implementation, which reloads the data every 5 minutes:
using System;
using System.Linq;
using System.Threading;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Primitives;
using Timer = System.Timers.Timer;
namespace MyProject.Classes.Configuration {
public class MyConfigProvider : ConfigurationProvider {
private readonly DbContextOptions<MyDbContext> _dbOptions;
private readonly Timer _reloadTimer = new Timer();
private ConfigurationReloadToken _reloadToken = new ConfigurationReloadToken();
public MyConfigProvider(Action<DbContextOptionsBuilder> dbOptionsAction) {
var builder = new DbContextOptionsBuilder<MyDbContext>();
dbOptionsAction(builder);
_dbOptions = builder.Options;
_reloadTimer.AutoReset = false;
_reloadTimer.Interval = TimeSpan.FromMinutes(5).TotalMilliseconds;
_reloadTimer.Elapsed += (s, e) => { Load(); };
}
public override void Load() {
try {
using (var db = new MyDbContext(_dbOptions)) {
var settings = db.Settings.AsNoTracking().ToList();
Data.Clear();
foreach (var s in settings) {
Data.Add(s.Name, s.Value);
}
}
OnReload();
} finally {
_reloadTimer.Start();
}
}
}
}
I am having same requirement and I reached to you code and I tried the same thing and not working.
But your code and some other investigation lead me to the hint.
public class CustomConfigurationProvider : ConfigurationProvider
{
private readonly string applicationName;
private readonly bool reloadOnChange;
private readonly IConfiguration configuration;
public CustomConfigurationProvider(string applicationName, bool reloadOnChange)
{
this.applicationName = applicationName;
this.reloadOnChange = reloadOnChange;
if(reloadOnChange)
{
ChangeToken.OnChange(
() => GetReloadToken(), // listener to token change
() =>
{
Thread.Sleep(250);
this.Load();
});
}
}
public override async void Load()
{
Data.Clear();
Data = read data from database;
if (Condition to check if data in database changed)
{
OnReload(); // This will create new token and trigger change so what is register in OnChange above will be called again which is this.Load()
}
}
}
I also refereed https://www.mikesdotnetting.com/article/301/loading-asp-net-core-mvc-views-from-a-database-or-other-location
and
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/primitives/change-tokens
Hope this helps.

Categories

Resources