Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update hosted service topic #3352

Closed
danroth27 opened this Issue May 11, 2017 · 36 comments

Comments

Projects
8 participants
@danroth27
Copy link
Member

danroth27 commented May 11, 2017

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

This comment has been minimized.

Copy link
Contributor

Rick-Anderson commented May 24, 2017

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

@danroth27

This comment has been minimized.

Copy link
Member Author

danroth27 commented May 24, 2017

@muratg Who can help us out for this doc?

@Rick-Anderson Rick-Anderson added this to the 2017-Q3 milestone Jun 8, 2017

@Rick-Anderson Rick-Anderson added the P1 label Jun 8, 2017

@Rick-Anderson

This comment has been minimized.

Copy link
Contributor

Rick-Anderson commented Jun 8, 2017

@muratg can we get a sample doing this?

@muratg

This comment has been minimized.

Copy link
Member

muratg commented Jun 9, 2017

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

@muratg

This comment has been minimized.

Copy link
Member

muratg commented Jun 9, 2017

@Rick-Anderson

This comment has been minimized.

Copy link
Contributor

Rick-Anderson commented Sep 25, 2017

@Tratcher can you come up with a sample app?

@Rick-Anderson Rick-Anderson referenced this issue Sep 25, 2017

Closed

Lukes top 15 #4385

14 of 14 tasks complete
@Tratcher

This comment has been minimized.

Copy link
Member

Tratcher commented Sep 26, 2017

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

This comment has been minimized.

Copy link
Contributor

Rick-Anderson commented Sep 27, 2017

@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

This comment has been minimized.

Copy link
Member

Tratcher commented Sep 27, 2017

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

This comment has been minimized.

Copy link
Member

davidfowl commented Sep 27, 2017

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

This comment has been minimized.

Copy link
Member Author

danroth27 commented Sep 27, 2017

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

@Rick-Anderson

This comment has been minimized.

Copy link
Contributor

Rick-Anderson commented Sep 27, 2017

@danroth27 who are the right folks for the meeting?

@Rick-Anderson Rick-Anderson removed this from the 2017-Q3 milestone Sep 27, 2017

@danroth27

This comment has been minimized.

Copy link
Member Author

danroth27 commented Sep 27, 2017

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

@Tratcher

This comment has been minimized.

Copy link
Member

Tratcher commented Sep 29, 2017

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

This comment has been minimized.

Copy link
Member

Tratcher commented Oct 2, 2017

@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();
        }

@Rick-Anderson Rick-Anderson added this to the Sprint 125(09/30/17 - 10/2017) milestone Oct 5, 2017

@Rick-Anderson Rick-Anderson modified the milestones: Sprint 125 (09/30/17 - 10/20/17), Sprint 127 (11/11/17 - 12/01/17) Oct 24, 2017

@Rick-Anderson Rick-Anderson self-assigned this Oct 24, 2017

@Rick-Anderson Rick-Anderson added P0 and removed P1 labels Oct 24, 2017

@Rick-Anderson Rick-Anderson removed their assignment Oct 24, 2017

Daniel15 added a commit to Daniel15/Website that referenced this issue Oct 25, 2017

@guardrex guardrex added this to To do in Sprint 135 (4/28/18 - 5/18/18) via automation May 3, 2018

@guardrex guardrex moved this from To do to Next up in Sprint 135 (4/28/18 - 5/18/18) May 3, 2018

@guardrex

This comment has been minimized.

Copy link
Collaborator

guardrex commented May 4, 2018

@Tratcher I hacked up a version of the Hosted Services topic sample (HostedServicesTopicSample) that runs on the Generic Host (the usual 🔪 Dino Hacks™️ 🔪 ... 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 #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

This comment has been minimized.

Copy link
Member

Tratcher commented May 4, 2018

  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

This comment has been minimized.

Copy link
Collaborator

guardrex commented May 4, 2018

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

@Tratcher

This comment has been minimized.

Copy link
Member

Tratcher commented May 4, 2018

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

@guardrex guardrex added the blocked label May 4, 2018

@guardrex guardrex moved this from Next up to To do in Sprint 135 (4/28/18 - 5/18/18) May 4, 2018

@guardrex guardrex moved this from Next up to Backlog in 2.1 scenarios May 4, 2018

@guardrex

This comment has been minimized.

Copy link
Collaborator

guardrex commented May 5, 2018

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 guardrex moved this from Backlog to Next up in 2.1 scenarios May 15, 2018

@guardrex guardrex removed the blocked label May 16, 2018

@guardrex guardrex moved this from Next up to Working in 2.1 scenarios May 16, 2018

@guardrex guardrex moved this from To do to In progress in Sprint 135 (4/28/18 - 5/18/18) May 16, 2018

@guardrex guardrex added this to To do in Sprint 136 (5/19/18 - 6/8/18) via automation May 17, 2018

@guardrex guardrex removed this from In progress in Sprint 135 (4/28/18 - 5/18/18) May 17, 2018

@guardrex guardrex moved this from To do to In progress in Sprint 136 (5/19/18 - 6/8/18) May 17, 2018

2.1 scenarios automation moved this from Working to Done May 25, 2018

Sprint 136 (5/19/18 - 6/8/18) automation moved this from In progress to Done May 25, 2018

@davidfowl

This comment has been minimized.

Copy link
Member

davidfowl commented May 29, 2018

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

@guardrex

This comment has been minimized.

Copy link
Collaborator

guardrex commented May 29, 2018

@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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.