I have a scenario where I call an api in one of my handlers and that Api can go down for like 6 hours per month. Therefore, I designed a retry logic with 1sec retry, 1 minute retry and a 6 hour retry. This all works fine but then I found that long delay retries are not a good option.Could you please give me your experience about this?
Thank you!
If I were you, I would use Rebus' ability to defer messages to the future to implement this functionality.
You will need to track the number of failed delivery attempts manually though, by attaching and updating headers on the deferred message.
Something like this should do the trick:
public class YourHandler : IHandleMessages<MakeExternalApiCall>
{
const string DeliveryAttemptHeaderKey = "delivery-attempt";
public YourHandler(IMessageContext context, IBus bus)
{
_context = context;
_bus = bus;
}
public async Task Handle(MakeExternalApiCall message)
{
try
{
await MakeCallToExternalWebApi();
}
catch(Exception exception)
{
var deliveryAttempt = GetDeliveryAttempt();
if (deliveryAttempt > 5)
{
await _bus.Advanced.TransportMessage.Forward("error");
}
else
{
var delay = GetNextDelay(deliveryAttempt);
var headers = new Dictionary<string, string> {
{DeliveryAttemptHeaderKey, (deliveryAttempt+1).ToString()}
};
await bus.Defer(delay.Value, message, headers);
}
}
}
int GetDeliveryAttempt() => _context.Headers.TryGetValue(DeliveryAttemptHeaderKey, out var deliveryAttempt)
? deliveryAttempt
: 0;
TimeSpan GetNextDelay() => ...
}
When running in production, please remember to configure some kind of persistent subscription storage – e.g. SQL Server – otherwise, your deferred messages will be lost in the event of a restart.
You can configure it like this (after having installed the Rebus.SqlServer package):
Configure.With(...)
.(...)
.Timeouts(t => t.StoreInSqlServer(...))
.Start();
Related
I am trying out the new MassTransit IJobConsumer implementation, and although I've tried to follow the documentation, the JobConsumer I have written is never being run/hit.
I have:
created the JobConsumer which has a run method that runs the code I need it to
public class CalculationStartRunJobConsumer : IJobConsumer<ICalculationStartRun>
{
private readonly ICalculationRunQueue runQueue;
public CalculationStartRunJobConsumer(ICalculationRunQueue runQueue)
{
this.runQueue = runQueue;
}
public Task Run(JobContext<ICalculationStartRun> context)
{
return Task.Run(
() =>
{
var longRunningJob = new LongRunningJob<ICalculationStartRun>
{
Job = context.Job,
CancellationToken = context.CancellationToken,
JobId = context.JobId,
};
runQueue.StartSpecial(longRunningJob);
},
context.CancellationToken);
}
}
I have registered that consumer trying both ConnectReceiveEndpoint and AddConsumer
Configured the ServiceInstance as shown in the documentation
services.AddMassTransit(busRegistrationConfigurator =>
{
// TODO: Get rid of this ugly if statement.
if (consumerTypes != null)
{
foreach (var consumerType in consumerTypes)
{
busRegistrationConfigurator.AddConsumer(consumerType);
}
}
if(requestClientType != null)
{
busRegistrationConfigurator.AddRequestClient(requestClientType);
}
busRegistrationConfigurator.UsingRabbitMq((context, cfg) =>
{
cfg.UseNewtonsoftJsonSerializer();
cfg.UseNewtonsoftJsonDeserializer();
cfg.ConfigureNewtonsoftJsonSerializer(settings =>
{
// The serializer by default omits fields that are set to their default value, but this causes unintended effects
settings.NullValueHandling = NullValueHandling.Include;
settings.DefaultValueHandling = DefaultValueHandling.Include;
return settings;
});
cfg.Host(
messagingHostInfo.HostAddress,
hostConfigurator =>
{
hostConfigurator.Username(messagingHostInfo.UserName);
hostConfigurator.Password(messagingHostInfo.Password);
});
cfg.ServiceInstance(instance =>
{
instance.ConfigureJobServiceEndpoints(serviceCfg =>
{
serviceCfg.FinalizeCompleted = true;
});
instance.ConfigureEndpoints(context);
});
});
});
Seen that the queue for the job does appear in the queue for RabbitMQ
When I call .Send to send a message to that queue, it does not activate the Run method on the JobConsumer.
public async Task Send<T>(string queueName, T message) where T : class
{
var endpointUri = GetEndpointUri(messagingHostInfo.HostAddress, queueName);
var sendEndpoint = await bus.GetSendEndpoint(endpointUri);
await sendEndpoint.Send(message);
}
Can anyone help?
Software
MassTransit 8.0.2
MassTransit.RabbitMq 8.0.2
MassTransit.NewtonsoftJson 8.0.2
.NET6
Using in-memory for JobConsumer
The setup of any type of repository for long running jobs is missing. We needed to either:
explicitly specify that it was using InMemory (missing from the docs)
Setup saga repositories using e.g. EF Core.
As recommended by MassTransit, we went with the option of setting up saga repositories by implementing databases and interacting with them using EF Core.
I'm registering my FluentValidation validators as follows:
services.AddFluentValidation(fv => fv.RegisterValidatorsFromAssemblyContaining<CustomRequestValidator>());
How can I get a handle of any validation errors to log them via my logging implementation, e.g. Serilog?
For users stumbling into this problem (like me) I'd like to provide the solution I found.
The actual problem is that ASP.NET Core, and APIs in particular, make Model State errors automatically trigger a 400 error response, which doesn't log the validation errors.
Below that same documentation they include instructions on how to enable automatic logging for that feature which I believe should be the correct solution to this problem.
To jump onto what #mdarefull said, I did the following:
public void ConfigureServices(IServiceCollection services)
{
// Possible other service configurations
services.AddMvc()
.ConfigureApiBehaviorOptions(options =>
{
options.InvalidModelStateResponseFactory = context =>
{
if (!context.ModelState.IsValid)
{
LogAutomaticBadRequest(context);
}
return new BadRequestObjectResult(context.ModelState);
};
});
// Possible other service configurations
}
The LogAutomaticBadRequest method is as follows:
private static void LogAutomaticBadRequest(ActionContext context)
{
// Setup logger from DI - as explained in https://github.com/dotnet/AspNetCore.Docs/issues/12157
var loggerFactory = context.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger(context.ActionDescriptor.DisplayName);
// Get error messages
var errorMessages = string.Join(" | ", context.ModelState.Values
.SelectMany(x => x.Errors)
.Select(x => x.ErrorMessage));
var request = context.HttpContext.Request;
// Use whatever logging information you want
logger.LogError("Automatic Bad Request occurred." +
$"{System.Environment.NewLine}Error(s): {errorMessages}" +
$"{System.Environment.NewLine}|{request.Method}| Full URL: {request.Path}{request.QueryString}");
}
I am using MassTransit 3.0.0.0 and I have a hard time understanding how to intercept messages in a Request-Response scenario on their way out and add some information to the headers field that I can read on the receiver's end.
I was looking at the Middleware, as recommended in the MassTransit docs - see Observers warning - but the context you get on the Send is just a Pipe context that doesn't have access to the Headers field so I cannot alter it. I used the sample provided in Middleware page.
I then, looked at IPublishInterceptor
public class X<T> : IPublishInterceptor<T> where T : class, PipeContext
{
public Task PostPublish(PublishContext<T> context)
{
return new Task(() => { });
}
public Task PostSend(PublishContext<T> context, SendContext<T> sendContext)
{
return new Task(() => { });
}
public Task PrePublish(PublishContext<T> context)
{
context.Headers.Set("ID", Guid.NewGuid().ToString());
return new Task(() => { });
}
public Task PreSend(PublishContext<T> context, SendContext<T> sendContext)
{
context.Headers.Set("ID", Guid.NewGuid().ToString());
return new Task(() => { });
}
}
Which is very clear and concise. However, I don't know where it is used and how to link it to the rest of the infrastructure. As it stands, this is just an interface that is not really linked to anything.
If you need to add headers when a message is being sent, you can add middleware components to either the Send or the Publish pipeline as shown below. Note that Send filters will apply to all messages, whereas Publish filters will only apply to messages which are published.
// execute a synchronous delegate on send
cfg.ConfigureSend(x => x.Execute(context => {}));
// execute a synchronous delegate on publish
cfg.ConfigurePublish(x => x.Execute(context => {}));
The middleware can be configured on either the bus or individual receive endpoints, and those configurations are local to where it's configured.
You can also add headers in the consumer class:
public async Task Consume(ConsumeContext<MyMessage> context)
{
....
await context.Publish<MyEvent>(new { Data = data }, c => AddHeaders(c));
}
public static void AddHeaders(PublishContext context)
{
context.Headers.Set("CausationId", context.MessageId);
}
http://masstransit-project.com/MassTransit/advanced/middleware/custom.html
Shows adding an extension method to make it clear what you're setup. That's a big help if it's an interceptor that will be used a lot, so it's clear that purpose. You can skip that step if you want.
Basically, just...
cfg.AddPipeSpecification(new X<MyMessage>());
When configuring the transport.
I was developing a sample application to test the timeout management in saga using NserviceBus.
I am tryin to achieve the following
When a saga started set it's timeout to 1 minute
Before the timeout happens if an update came to the nessage updates the timeout to 5 minutes
My code is like below
public class OrderSaga : Saga<OrderSagaData>,
IAmStartedByMessages<SampleMessage>,
IHandleMessages<UpdateMessage>
{
public override void ConfigureHowToFindSaga()
{
ConfigureMapping<UpdateMessage>(s => s.PurchaseOrderNumber, m => m.Update);
}
public void Handle(SampleMessage message)
{
this.Data.PurchaseOrderNumber = message.Name;
RequestTimeout(DateTime.Now.AddMinutes(1), message.Name);
}
private void Complete()
{
MarkAsComplete();
}
public override void Timeout(object state)
{
Complete();
}
#region IMessageHandler<UpdateMessage> Members
public void Handle(UpdateMessage message)
{
this.Data.PurchaseOrderNumber = message.NewValue;
RequestTimeout(DateTime.Now.AddMinutes(5), message.Update);
}
#endregion
}
}
But here the problem is the timeout is not getting updated to 5 minutes.The timeout still works for 1 minute.
Could you please let me know what is doing wrong here?
Thanks in advance,
Ajai
Saga timeouts can't be updated. They will fire no matter what you do. In your case you will receive both timeouts and given that you call Complete in your timeout handler your saga will end after one minute. You need to add some logic in that takes this into account.
Something like this might do it:
if(!updateReceived or state == ThisTimeoutWasRequestedByMyUpdateHandler)
Complete();
Hope this helps!
Is there a way to schedule a Task for execution in the future using the Task Parallel Library?
I realize I could do this with pre-.NET4 methods such as System.Threading.Timer ... however if there is a TPL way to do this I'd rather stay within the design of the framework. I am not able to find one however.
Thank you.
This feature was introduced in the Async CTP, which has now been rolled into .NET 4.5. Doing it as follows does not block the thread, but returns a Task which will execute in the future.
Task<MyType> new_task = Task.Delay(TimeSpan.FromMinutes(5))
.ContinueWith<MyType>( /*...*/ );
(If using the old Async releases, use the static class TaskEx instead of Task)
You can write your own RunDelayed function. This takes a delay and a function to run after the delay completes.
public static Task<T> RunDelayed<T>(int millisecondsDelay, Func<T> func)
{
if(func == null)
{
throw new ArgumentNullException("func");
}
if (millisecondsDelay < 0)
{
throw new ArgumentOutOfRangeException("millisecondsDelay");
}
var taskCompletionSource = new TaskCompletionSource<T>();
var timer = new Timer(self =>
{
((Timer) self).Dispose();
try
{
var result = func();
taskCompletionSource.SetResult(result);
}
catch (Exception exception)
{
taskCompletionSource.SetException(exception);
}
});
timer.Change(millisecondsDelay, millisecondsDelay);
return taskCompletionSource.Task;
}
Use it like this:
public void UseRunDelayed()
{
var task = RunDelayed(500, () => "Hello");
task.ContinueWith(t => Console.WriteLine(t.Result));
}
Set a one-shot timer that, when fired, starts the task. For example, the code below will wait five minutes before starting the task.
TimeSpan TimeToWait = TimeSpan.FromMinutes(5);
Timer t = new Timer((s) =>
{
// start the task here
}, null, TimeToWait, TimeSpan.FromMilliseconds(-1));
The TimeSpan.FromMilliseconds(-1) makes the timer a one-shot rather than a periodic timer.