Closed danroth27 closed 6 years ago
@danroth27 which dev should we work with?
Is this similar to HostingEnvironment.QueueBackgroundWorkItem
?
@muratg Who can help us out for this doc?
@muratg can we get a sample doing this?
@Rick-Anderson Chris can help with it after Preview2 work winds down.
cc @Tratcher
@Tratcher can you come up with a sample app?
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace WebApplication87
{
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IHostedService, MyBackgroundServiceA>();
services.AddSingleton<IHostedService, MyBackgroundServiceB>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.Run(async (context) =>
{
await context.Response.WriteAsync("Hello World!");
});
}
}
internal class MyBackgroundServiceA : IHostedService
{
private Timer _timer;
public Task StartAsync(CancellationToken cancellationToken)
{
_timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(2));
return Task.CompletedTask;
}
private void DoWork(object state)
{
Console.WriteLine("My background service A is doing work.");
}
public Task StopAsync(CancellationToken cancellationToken)
{
_timer.Dispose();
return Task.CompletedTask;
}
}
internal class MyBackgroundServiceB : IHostedService
{
private Timer _timer;
public Task StartAsync(CancellationToken cancellationToken)
{
_timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(4));
return Task.CompletedTask;
}
private void DoWork(object state)
{
Console.WriteLine("My background service B is doing work.");
}
public Task StopAsync(CancellationToken cancellationToken)
{
_timer.Dispose();
return Task.CompletedTask;
}
}
}
@Tratcher Thanks. I plugged your code into the Razor Pages project. I'd like to use the CancellationToken
from the About page to cancel the work. Fowler has a sample here
My sample is here - can you show me how to cancel?
Should I set up a meeting with you or @muratg or both on the outline of the article?
Fowler abandoned that approach, it has too many threading and cancellation issues.
I don't know that cancellation from the About page makes sense. These services have the same lifetime as your application, you don't spin them up and down dynamically.
You could communicate with the background service from the About page though to do things like change the output message. Inject a shared singleton dependency like MyBackgroundServiceData that holds the message to write into both the background service and the about page.
We should discuss what we want to document. Right now in the box is an extremely low level building block API. 2.1 will have more goodies that make it easier to implement common scenarios but showing something like a timer would be good.
We should also discuss some issues that will be extremely common like doing DI in one of these services. One big issues people will end up having to deal with with is scoped objects (like a db context). When you using this API we'll activate your service once at startup and never again. You'll need to manually create scopes during execution to do anything with say your db context in the timer callback.
@Rick-Anderson let's get some time on the calendar then with the right folks
@danroth27 who are the right folks for the meeting?
Probably @davidfowl and @Tratcher and whoever else they think should be there.
Here's a sample for consuming scoped services in a hosted service:
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace WebApplication88
{
public class Program
{
public static void Main(string[] args)
{
BuildWebHost(args).Run();
}
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.ConfigureServices(services =>
{
services.AddSingleton<IHostedService, HostedServiceScopingSample>();
services.AddScoped<IFoo, MyFoo>();
})
.Build();
}
internal interface IFoo
{
void DoWork();
}
internal class MyFoo : IFoo
{
public void DoWork()
{
Console.WriteLine("Doing scoped work");
}
}
internal class HostedServiceScopingSample : IHostedService
{
public HostedServiceScopingSample(IServiceProvider services)
{
Services = services ?? throw new ArgumentNullException(nameof(services));
}
public IServiceProvider Services { get; }
public Task StartAsync(CancellationToken cancellationToken)
{
using (var scope = Services.CreateScope())
{
var foo = scope.ServiceProvider.GetRequiredService<IFoo>();
foo.DoWork();
}
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}
}
I owe you one more sample for queuing background tasks.
@davidfowl how's this?
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace WebApplication1
{
public class Program
{
public static void Main(string[] args)
{
BuildWebHost(args).Run();
}
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.ConfigureServices(services =>
{
services.AddSingleton<IHostedService, BackgroundTaskService>();
services.AddSingleton<IBackgroundTaskQueue, BackgroundTaskQueue>();
})
.Build();
}
public interface IBackgroundTaskQueue
{
/// <summary>
/// https://msdn.microsoft.com/en-us/library/dn636893(v=vs.110).aspx
/// Differs from a normal ThreadPool work item in that ASP.NET can keep track of how many work items registered through
/// this API are currently running, and the ASP.NET runtime will try to delay AppDomain shutdown until these work items
/// have finished executing. This API cannot be called outside of an ASP.NET-managed AppDomain. The provided CancellationToken
/// will be signaled when the application is shutting down.
/// </summary>
/// <param name="workItem"></param>
void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem);
Task<Func<CancellationToken, Task>> DequeueAsync(CancellationToken cancellationToken);
}
public class BackgroundTaskQueue : IBackgroundTaskQueue
{
private ConcurrentQueue<Func<CancellationToken, Task>> _workItems = new ConcurrentQueue<Func<CancellationToken, Task>>();
private SemaphoreSlim _signal = new SemaphoreSlim(0);
public void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem)
{
if (workItem == null)
{
throw new ArgumentNullException(nameof(workItem));
}
_workItems.Enqueue(workItem);
_signal.Release();
}
public async Task<Func<CancellationToken, Task>> DequeueAsync(CancellationToken cancellationToken)
{
await _signal.WaitAsync(cancellationToken);
_workItems.TryDequeue(out var workItem);
return workItem;
}
}
public class BackgroundTaskService : IHostedService
{
private CancellationTokenSource _shutdown = new CancellationTokenSource();
private Task _backgroundTask;
public BackgroundTaskService(IBackgroundTaskQueue taskQueue)
{
TaskQueue = taskQueue ?? throw new ArgumentNullException(nameof(taskQueue));
}
public IBackgroundTaskQueue TaskQueue { get; }
public Task StartAsync(CancellationToken cancellationToken)
{
_backgroundTask = Task.Run(BackgroundProceessing);
return Task.CompletedTask;
}
private async Task BackgroundProceessing()
{
while (!_shutdown.IsCancellationRequested)
{
var workItem = await TaskQueue.DequeueAsync(_shutdown.Token);
try
{
await workItem(_shutdown.Token);
}
catch (Exception) { } // TODO: Log
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
_shutdown.Cancel();
return Task.WhenAny(_backgroundTask, Task.Delay(Timeout.Infinite, cancellationToken));
}
}
}
// HomeController.cs
public IBackgroundTaskQueue Queue { get; }
public HomeController(IBackgroundTaskQueue queue)
{
Queue = queue ?? throw new ArgumentNullException(nameof(queue));
}
public IActionResult Index()
{
return View();
}
public IActionResult About()
{
ViewData["Message"] = "Adding a task to the queue";
Queue.QueueBackgroundWorkItem(async token =>
{
for (int i = 0; i < 5; i++)
{
Console.WriteLine($"A background Task is running {i}/5");
await Task.Delay(TimeSpan.FromSeconds(5), token);
}
Console.WriteLine($"A background Task is complete.");
});
return View();
}
@Tratcher your BackgroundTaskService
does not really touch the passed in CancellationToken
except in StopAsync(), whereas @davidfowl used
CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)
in his implementation.
Is this an example of one of those threading/cancellation issues you mentioned upthread?
What other token am I missing? The one from StartAsync? That one is not relevant in this scenario, the service starts instantly.
@Tratcher I was messing around with this today :point_right: https://github.com/guardrex/HostedServiceSample
That's an RP app. It does deal with scopes and the dB context to allow the background tasks to interact with the dB. It does offer the ability to shutdown background tasks from a page model method. It uses BackgroundService
from 2.1 and registers a couple of IHostedService
instances.
Anywho .... Question: Is there a better way to get a hold of the background task service to stop it than this horribly inelegant approach? ...
public async Task<IActionResult> OnPostStopMessageProcessingServiceAsync()
{
var hostedServices = HttpContext.RequestServices.GetServices<IHostedService>();
foreach (var service in hostedServices)
{
if (service.GetType() == typeof(MessageProcessingService))
{
var castedService = (MessageProcessingService)service;
await service.StopAsync(castedService.StoppingToken);
break;
}
}
return RedirectToPage();
}
public async Task<IActionResult> OnPostStopWordCountingServiceAsync()
{
var hostedServices = HttpContext.RequestServices.GetServices<IHostedService>();
foreach (var service in hostedServices)
{
if (service.GetType() == typeof(WordCountingService))
{
var castedService = (WordCountingService)service;
await service.StopAsync(castedService.StoppingToken);
break;
}
}
return RedirectToPage();
}
You see ... I have two background task services running. That approach seemed to be the only way to figure out which one to hit on a UI button click to just shut down the correct one.
You can use a separate control service for each like IBackgroundTaskQueue above.
I see. I broke some of the rules here. :grinning:
In addition to stopping (and possibly starting) services the way I am, I'm also concerned about scoping the dB as a singleton like that. Idk if that's a :bomb: or not.
I'm aware of QBWI, but QBWI feels so much more focused on individual tasks that one creates to process specific operations at specific times; whereas for BackgroundService
/IHostedService
, it truly feels more like a constantly running background service on any schedule you like processing anything you want. I speculate there's a great usefulness for this in web apps; and except for the two problem areas I came across, this is a snap to understand and use. I guess it just was never intended to be used this way.
@Rick-Anderson @scottaddie @Tratcher
Additional reference material: https://docs.microsoft.com/dotnet/standard/microservices-architecture/multi-container-microservice-net-applications/background-tasks-with-ihostedservice
@Rick-Anderson @scottaddie While working through the Antares-IIS PR reviews over the next few days (the work is done; just passing the PRs in one-at-a-time), I'll start working on this issue.
Unless you want to go a different way, I plan to package the three @Tratcher-supplied samples :point_up: into the topic. I'll try a single RP app approach first that incorporates all three sample demos, two of which won't interact with the user.
Title: Background tasks with IHostedService in ASP.NET Core
TOC: fundamentals/ihostedservice.md
UID: fundamentals/ihostedservice
Outline
Right now in the box is an extremely low level building block API. 2.1 will have more goodies that make it easier to implement common scenarios but showing something like a timer would be good.
I'll keep that in mind. I won't spend a massive amount of time here if "goodies" are on the way to a framework near you that will require a significant update.
TOC: performance/ihostedservice.md
I wouldn't look in perf. Wouldn't fundamentals be a better fit?
I wondered ... but do you want to clutter fundamentals with low-traffic topics? If it's a "primitive," it could go there next to the Change Tokens topic. I thought "performance," since that's ~what~ [EDIT] one of the important things from the user's perspective background tasks help solve.
Plan on fundamentals and we can ask @Tratcher
and dan.roth when it's in review.
I'm ok with putting this in the fundamentals section
@Tratcher I left this open following the topic going live on #5440 because there was a remark that 2.1 would (or might) change some (or a lot of) IHostedService
scenarios.
Here's the topic: https://docs.microsoft.com/aspnet/core/fundamentals/hosted-services
Here's a list of issues that reference the interface: https://github.com/aspnet/Hosting/issues?utf8=%E2%9C%93&q=is%3Aissue+IHostedService
Do any of those pertain to 2.1 updates for the topic? If not, we can close this issue. If so, I should get to this shortly (Thursday perhaps).
The only change here is the introduction of the new generic Host API where IHostedService becomes the primary unit of work. See https://github.com/aspnet/Hosting/blob/dev/samples/GenericHostSample/ProgramHelloWorld.cs
@Tratcher I hacked up a version of the Hosted Services topic sample (HostedServicesTopicSample) that runs on the Generic Host (the usual 🔪 Dino Hacks:tm: 🔪 ... u know the drill! 😀) . A few questions ...
AddHostedService
registers Transient. What's important to understand about that difference? Should HostedServicesTopicSample be left as I have it, or should it be using AddHostedService
and Transient registrations?AddHostedService
in my Generic Host version ... it claims it can't find it on the service collection. I might be missing an assembly.~ U only added it a few days ago. ~I prob just need to update my bits.~ Yep ... got it with 2.1.0-rtm-30722
.BackgroundTaskQueue
. Any idears??This last bit pertains to the overall planning for this topic and the docs work on Generic Host in general:
I agree that we should get the basic docs up for Generic Host and then come back and sort out the samples.
Nevermind ... "Until 3.0 we'll need separate samples." ... interpreted as 'yes' ... we'll add the sample now.
Yes, do add the sample, but get the basic docs done first.
The new (draft) sample has been updated for use when we get back to this issue: HostedServicesTopicSample. Console bits added to deal with enqueuing background work items can be seen in the Program.cs file ...
https://github.com/guardrex/HostedServicesTopicSample/blob/master/Program.cs
We'll need to address the terminal choice for running the sample because the keystroke capture fails in a redirected console (e.g., such as with VSC's internalConsole
... the dev must use either the externalTerminal
or integratedTerminal
... a similar situation is probably true for VS as well).
@guardrex did we add the call to the new AddHostedService
method?
@davidfowl It's in the Generic Host topic: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host?view=aspnetcore-2.1#configureservices
... and it appears in the Generic Host sample of this topic (the background services topic). We planned to have the background services topic text call it out when the Generic Host takes over.
See Document IHostStartup (host lightup) #4438
To do background tasks in ASP.NET Core you use IHostedService. We should document that. meeting with chris/fowler
Notes; Low level component for running long running tasks. Runs a jobs once at startup and runs for the life of the app. If you want to get scoped dependencies - you need to create a scope per invocation. Not something you create/destroy while app is running. It's not QBWI - although it is designed for long running processes. Sample goes with controller, not RP. RP are for UI. 14:30 IHostedService jobs don't run in parallel with application.
StartAsync
should return quickly so it doesn't block the web app from starting.return Task.CompletedTask;
needs to return when the task is started, not when the task is completed. So you can't return a task of a long running operation inStartAsync
.