[ApiController]
[Route("[controller]")]
public class JobController : ControllerBase
{
private readonly IEventBus _bus;
public JobController(IUnitOfWork unitOfWork, IEventBus bus)
{
_bus = bus;
}
...
[HttpPost]
public async Task<IActionResult> Post([FromBody]JobRequest request)
{
try
{
var command = new JobCommand{ Id = 1, Name = "abc"};
await _bus.SendCommand(command);
}
catch (OperationCanceledException)
{
_logger.LogInformation("Task was cancelled!");
}
return CreatedAtAction(nameof(GetById), new { id = 0 }, null);
}
}
public class JobCommandHandler : IRequestHandler<JobCommand, bool>
{
private readonly ILogger<JobCommandHandler> _logger;
public JobCommandHandler(ILogger<JobCommandHandler> logger)
{
_logger = logger;
}
public async Task<bool> Handle(JobCommand request, CancellationToken cancellationToken)
{
//
// I was able to reproduce manual cancellation by using this code below
var cts = new CancellationTokenSource();
cts.Cancel();
cancellationToken = cts.Token;
// how this can be populated sent from the place where I'm issuing command?
cancellationToken.ThrowIfCancellationRequested();
...
// long running task
}
}
My question is:
Do I need to send the CancellationTokenSource together with command? If so, how to trigger that from the swagger Cancel button and is it good practice to include CancellationTokenSource
to be the property of CommandBase class which every Command will extend?
If you want to cancel all IO and processing if the Http Request which initiated everything got canceled, then, yes. You have to make everything in its path cancellation aware and pass the token through all layers.
You can get a request cancellation token from the HttpContext in the controller from HttpContext.RequestAborted.
Now, in your example, I'm not sure what IEventBus is. If that is some kind of distributed messaging backend, simply passing a CancellationToken will not work, in that case you could send a cancel event to cancel the action I guess.
If it is in-memory eventing only, then it should probably work.
Related
I set up a CancellationTokenSource with handler
public class AppTimeout
{
public async Task Invoke(HttpContext httpContext)
{
var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(httpContext.RequestAborted);
cancellationTokenSource.CancelAfter(myTimestamp);
cancellationTokenSource.Token.Register(() =>
{
log.info("...");
});
await _next(httpContext);
}
}
My problem is if I have only one request in timeout , the callback of cancellationTokenSource.Token is called for all request that have been processed by Invoke methode, even request that already finished in correct time
Do you know why I encounter this behaviour and how to fix it please?
using var registration = timeoutCancellationTokenSource.Token.Register(() => {
log.info($"timeout path is {path}");
});
// your other code here...
Now it will unregister correctly when complete, i.e. when leaving the scope of the using.
This is my codebase. I need to stop running job. I've tried BackgroundJob.Delete method and send current jobId but it didn't help.It just deletes the job but not cancelling it. I can run multiple jobs and should be able to stop each of them from UI. I tried to use CancellationToken but on the UI I am using AJAX to send request and it takes some milliseconds so I can't even abort this request. Can somebody suggest something please? Thanks.
public class JobController : Controller {
public void InternalLogic(int id, CancellationToken cancellationToken)
{
foreach (var item in collection)
{
if (someCondition)
{
if (cancellationToken.IsCancellationRequested)
{
//some logic
break;
}
//continue working
}
}
}
public void RunLogic(int id, CancellationToken cancellationToken)
{
//some logic
InternalLogic(id,cancellationToken);
}
public void Run(int id, CancellationToken cancellationToken)
{
var jobId = BackgroundJob.Enqueue(() => this.RunLogic(id, cancellationToken));
}
}
I'm not sure if I understand your question exactly, but I believe that the CancellationToken you are using is short lived. The request to start the job executes and completes at which point the CancellationToken you are using is useless and will never be cancelled since the request that created it has long since completed successfully and discarded its CancellationTokenSource.
If you want to use CancellationToken, you will need to create your own CancellationTokenSource, keep track of it and cancel the job yourself when you need to cancel it.
Perhaps something similar in concept to:
public class JobController : Controller {
//Not safe, for demonstration purposes only
private static Dictionary<int, CancellationTokenSource> _dictionary = new Dictionary<int, CancellationTokenSource>();
private void InternalLogic(int id, CancellationToken cancellationToken)
{
foreach (var item in collection)
{
if (someCondition)
{
if (cancellationToken.IsCancellationRequested)
{
//some logic
break;
}
//continue working
}
}
}
private void RunLogic(int id, CancellationToken cancellationToken)
{
//some logic
InternalLogic(id,cancellationToken);
}
//Client requests that a job is started
public void Run(int id, CancellationToken cancellationToken)
{
var cts = new CancellationTokenSource()
var jobId = BackgroundJob.Enqueue(() => this.RunLogic(id, cts.Token));
}
//Client requests that a job is cancelled.
public void Cancel(int id, CancellationToken cancellationToken)
{
_dictionary[id].Cancel(); //or perhaps track using jobId?
}
}
This is not a production ready solution - you will want to make sure only the correct user can cancel the correct jobs, and will probably want to find something more elegant and thread safe than a static dictionary.
This solution is not specific to Hangfire - it's just the general idea of how you might signal cancellation of a long running, asynchronous background task.
My Mediatr is using the SyncContinueOnException publish strategy, is there any way to run some validation before start the propagation?
Example:
_mediatr.Publish(new MyNotification());
public class MyValidationHandler :
INotificationHandler<MyValidationHandler>
{
Task INotificationHandler<MyValidationHandler>.Handle(MyValidationHandler notification, CancellationToken cancellationToken)
{
// STOP propagation if some condition is false
}
}
public class FirstHandlers :
INotificationHandler<MyNotification>
{
Task INotificationHandler<MyNotification>.Handle(MyNotification notification, CancellationToken cancellationToken)
{
Console.WriteLine("x");
return Task.CompletedTask;
}
}
public class SecondHandlers :
INotificationHandler<MyNotification>
{
Task INotificationHandler<MyNotification>.Handle(MyNotification notification, CancellationToken cancellationToken)
{
Console.WriteLine("x");
return Task.CompletedTask;
}
}
Update Sorry, misread this originally!
One way to wrap the MediatR Publish behavior is to decorate the IMediator instance itself, either manually or with a library like Scrutor. You can add a marker interface to identify notifications that should pass through your validation logic, with any other events just flowing through to MediatR.
public class MediatorDecorator : IMediator
{
private readonly IMediator _mediator;
public MediatorDecorator(IMediator mediator)
{
_mediator = mediator;
}
public Task Publish<TNotification>(TNotification notification, CancellationToken cancellationToken = default) where TNotification : INotification
{
if (notification is IValidatableNotification)
{
// Your validation behavior here
return Task.CompletedTask; // if the validation fails, respond as you'd like ...
}
return _mediator.Publish(notification, cancellationToken);
}
// ...
}
And in Startup, after the MediatR registration, using Scrutor's Decorate:
services.AddMediatR(typeof(Startup));
services.Decorate<IMediator, MediatorDecorator>();
I have to write unit test to verify the code which was run by Task.Run(),which is wrapped inside an async action as shown below.Since I am not awaiting for the task to complete I am not able to achieve this as Task.Run() runs separately.
The requirement is that The call this.KeepAlive(accountNumber, token) should not be waited to complete to call the next statement.But if that KeepAlive service call fails or any validation fails then it should be logged.
{
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
readonly IService service;
readonly IService2 service2;
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger, IService service, IService2 service2)
{
_logger = logger;
this.service = service;
this.service2 = service2;
}
[HttpGet]
public async Task<bool> Testmethod(string accountNumber, string viewName, CancellationToken token)
{
_ = Task.Run(() => this.KeepAlive(accountNumber, token));
var accountStatus = await this.service2.GetValidName(viewName, token);
return accountStatus=="Myname";
}
private async Task KeepAlive(string name, CancellationToken token)
{
if (string.IsNullOrEmpty(name))
{
_logger.LogError("name is empty or null");
return;
}
try
{
var isAlive = await this.service.ChekStatusAsyc(name, token);
if (!isAlive)
{
_logger.LogError("Unable to process the request");
}
}
catch
{
_logger.LogError("Service ChekStatusAsyc Failed");
}
}
}
}
I need to verify below in my unit tests
whether service was called
validation logging happened
Exception logging
The Test which was written as below will not work, since I am not awaiting for task to complete.
[Fact]
public async void KeepAliveAsyncShouldBeCalledErrorShouldBeLoggedIfServiceFails()
{
var mockLogger = new Mock<ILogger<WeatherForecastController>>();
var mockservice = new Mock<service>();
mockservice.Setup(x => x.ChekStatusAsyc(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(false);
var mockservice2 = new Mock<service2>();
var controller = new WeatherForecastController(mockLogger.Object, mockservice.Object, mockservice2.Object);
var result = await controller.Testmethod("account0", "test");
mockservice.Verify(x => x.ChekStatusAsyc(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Once);
}
As I am not awaiting, I can't verify anything which was run by Task.Run().
If I use
await this.KeepAlive(accountNumber, token) then the test case which I wrote will work as expected,but it will wait for the task to complete.which is not as per the requirement.
Any suggestions?
A user can trigger a long-running job by sending a request to an ASP.NET Core controller. Currently, the controller executes the job and then sends a 200 OK response. The problem is that the client has to wait rather long for the response.
This is why I am currently trying to process the job in a background task. I am using an IBackgroundTaskQueue where all jobs are stored and an IHostedService that processes the jobs whenever a new one is enqueued. It is similar to the code in the Microsoft documentation.
But the job does need access to the database and therefore the user has to authenticate using Active Directory. Hence, I need access to the HttpContext.User property in the background task. Unfortunately, the HttpContext is disposed when the response is sent and before the processing of the job begins.
Demonstration
public class Job
{
public Job(string message)
{
Message = message;
}
public string Message { get; }
}
The controller enqueues a new job in the task queue.
[HttpGet]
public IActionResult EnqueueJob()
{
var job = new Job("Hello World");
this.taskQueue.QueueBackgroundWorkItem(job);
return Accepted();
}
public class BackgroundTaskQueue : IBackgroundTaskQueue
{
private ConcurrentQueue<Job> jobs = new ConcurrentQueue<Job>();
private SemaphoreSlim signal = new SemaphoreSlim(0);
public void QueueBackgroundWorkItem(Job job)
{
jobs.Enqueue(job);
signal.Release();
}
public async Task<Job> DequeueAsync(CancellationToken cancellationToken)
{
await signal.WaitAsync(cancellationToken);
jobs.TryDequeue(out var job);
return job;
}
}
The IHostedService creates a new JobRunner for each job it dequeues. I'm using a IServiceScopeFactory here to have dependency injection available. JobRunner also has a lot more dependencies in the real code.
public class JobRunnerService : BackgroundService
{
private readonly IServiceScopeFactory serviceScopeFactory;
private readonly IBackgroundTaskQueue taskQueue;
public JobRunnerService(IServiceScopeFactory serviceScopeFactory, IBackgroundTaskQueue taskQueue)
{
this.serviceScopeFactory = serviceScopeFactory;
this.taskQueue = taskQueue;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (stoppingToken.IsCancellationRequested == false)
{
var job = await taskQueue.DequeueAsync(stoppingToken);
using (var scope = serviceScopeFactory.CreateScope())
{
var serviceProvider = scope.ServiceProvider;
var runner = serviceProvider.GetRequiredService<JobRunner>();
runner.Run(job);
}
}
}
}
public class JobRunner
{
private readonly ILogger<JobRunner> logger;
private readonly IIdentityProvider identityProvider;
public JobRunner(ILogger<JobRunner> logger, IIdentityProvider identityProvider)
{
this.logger = logger;
this.identityProvider= identityProvider;
}
public void Run(Job job)
{
var principal = identityProvider.GetUserName();
logger.LogInformation($"{principal} started a new job. Message: {job.Message}");
}
}
public class IdentityProvider : IIdentityProvider
{
private readonly IHttpContextAccessor httpContextAccessor;
public IdentityProvider(IHttpContextAccessor httpContextAccessor)
{
this.httpContextAccessor = httpContextAccessor;
}
public string GetUserName()
=> httpContextAccessor.HttpContext.User.Identity.Name; // throws NullReferenceException
}
Now, when sending a request, a NullReferenceException is thrown in JobRunner.Run() because httpContextAccessor.HttpContext is null.
What I've tried
I haven't had a good idea yet how to approach this problem. I know that it would be possible to copy the necessary information from the HttpContext, but don't know how to make them available to dependency injection services.
I thought that maybe I could create a new IServiceProvider that uses the services of an old one, but replaces the implementation for IHttpContextAccesor, but it does not seem to be possible.
How can I use the HttpContext in the background task although the response has been completed?