dotnet / AspNetCore.Docs

Documentation for ASP.NET Core
https://docs.microsoft.com/aspnet/core
Creative Commons Attribution 4.0 International
12.63k stars 25.3k forks source link

Update hosted service topic #3352

Closed danroth27 closed 6 years ago

danroth27 commented 7 years ago

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 in StartAsync .

Rick-Anderson commented 7 years ago

@danroth27 which dev should we work with? Is this similar to HostingEnvironment.QueueBackgroundWorkItem ?

danroth27 commented 7 years ago

@muratg Who can help us out for this doc?

Rick-Anderson commented 7 years ago

@muratg can we get a sample doing this?

muratg commented 7 years ago

@Rick-Anderson Chris can help with it after Preview2 work winds down.

muratg commented 7 years ago

cc @Tratcher

Rick-Anderson commented 7 years ago

@Tratcher can you come up with a sample app?

Tratcher commented 7 years ago
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;
        }
    }
}
Rick-Anderson commented 7 years ago

@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?

Tratcher commented 7 years ago

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.

davidfowl commented 7 years ago

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.

danroth27 commented 7 years ago

@Rick-Anderson let's get some time on the calendar then with the right folks

Rick-Anderson commented 7 years ago

@danroth27 who are the right folks for the meeting?

danroth27 commented 7 years ago

Probably @davidfowl and @Tratcher and whoever else they think should be there.

Tratcher commented 7 years ago

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.

Tratcher commented 7 years ago

@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();
        }
Gavvers commented 7 years ago

@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?

Tratcher commented 7 years ago

What other token am I missing? The one from StartAsync? That one is not relevant in this scenario, the service starts instantly.

guardrex commented 6 years ago

@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.

Tratcher commented 6 years ago

You can use a separate control service for each like IBackgroundTaskQueue above.

guardrex commented 6 years ago

I see. I broke some of the rules here. :grinning:

  1. "Low level component for running long running tasks." I show timed, intermittent, fast-running tasks are possible on any schedule.
  2. "Runs a job once at startup and runs for the life of the app." -AND- "Not something you create/destroy while app is running." I show it runs a job if conditions are met and that you could start and stop it on-demand ... well ... so far, stop it anyway.
  3. "Sample goes with controller, not RP. RP are for UI." I use it in an RP app, and I don't see why this wouldn't be useful to devs using RP apps to run a few low-demand background services in their web apps.

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.

Tratcher commented 6 years ago

https://blogs.msdn.microsoft.com/cesardelatorre/2017/11/18/implementing-background-tasks-in-microservices-with-ihostedservice-and-the-backgroundservice-class-net-core-2-x/

guardrex commented 6 years ago

@Rick-Anderson @scottaddie @Tratcher

Additional reference material: https://docs.microsoft.com/dotnet/standard/microservices-architecture/multi-container-microservice-net-applications/background-tasks-with-ihostedservice

guardrex commented 6 years ago

@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.

Rick-Anderson commented 6 years ago

TOC: performance/ihostedservice.md

I wouldn't look in perf. Wouldn't fundamentals be a better fit?

guardrex commented 6 years ago

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.

Rick-Anderson commented 6 years ago

Plan on fundamentals and we can ask @Tratcher and dan.roth when it's in review.

danroth27 commented 6 years ago

I'm ok with putting this in the fundamentals section

guardrex commented 6 years ago

@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).

Tratcher commented 6 years ago

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

guardrex commented 6 years ago

@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 ...

  1. The original app registers the services Singleton and Scoped, but 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?
  2. ~The sample runs fine registering the services as I do, but I can't seem to invoke 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.
  3. The Generic Host version of the sample isn't interactive, so we need to do something other than a user tapping a button on a page to put tasks in the BackgroundTaskQueue. Any idears??
  4. Is the plan going to be to offer two samples here (at least until WebHostBuilder goes away at some point in the future)? We'd have the current sample (WebHostBuilder-based; RP app) SxS with a new sample (Generic Host-based; non-interactive/non-web). I imagine that this topic won't really get into the nuts and bolts of the Generic Host too much given that it's so focused on the background tasks scenario. It can link out to content on the Generic Host after the Generic Host issue is addressed.

This last bit pertains to the overall planning for this topic and the docs work on Generic Host in general:

  1. This issue (3352) pertains to 2.1 updates for the current Hosted Services topic, which focuses on background tasks. The issue pertaining to the subject of Generic Hosts in general is https://github.com/aspnet/Docs/issues/5798. I think we should discuss how to organize all of the hosting content over on #5798.
  2. Before working this issue (3352), it seems like we might want to work issue #5798 first. Then, we come back over here, drop in the new samp, and link out to that new content. For one thing, we may end up with a Hosting node in the TOC that can hold several Generic Host-related topics, this one (background tasks) being just one of those.
Tratcher commented 6 years ago
  1. The lifetime of IHostedService registrations doesn't really matter, they're only ever resolved once at startup. Using AddHostedService would be better for the sample.
  2. 😁
  3. Add some command line UI? E.g. "Press 'A' to add a work item."
  4. Until 3.0 we'll need separate samples. Assume that's at least a year. That said, we don't need as much coverage for generic host, it's more of a forward looking prototype, not something most of our customers will adopt anytime soon.

I agree that we should get the basic docs up for Generic Host and then come back and sort out the samples.

guardrex commented 6 years ago

Nevermind ... "Until 3.0 we'll need separate samples." ... interpreted as 'yes' ... we'll add the sample now.

Tratcher commented 6 years ago

Yes, do add the sample, but get the basic docs done first.

guardrex commented 6 years ago

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).

davidfowl commented 6 years ago

@guardrex did we add the call to the new AddHostedService method?

guardrex commented 6 years ago

@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.

https://github.com/aspnet/Docs/blob/master/aspnetcore/fundamentals/host/hosted-services/samples/2.x/BackgroundTasksSample-GenericHost/Program.cs