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
Closed

Update hosted service topic #3352

danroth27 opened this issue May 11, 2017 · 36 comments

Comments

@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
Copy link
Contributor

Rick-Anderson commented May 24, 2017

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

@danroth27
Copy link
Member Author

@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 Pri1 High priority, do before Pri2 and Pri3 label Jun 8, 2017
@Rick-Anderson
Copy link
Contributor

@muratg can we get a sample doing this?

@muratg
Copy link

muratg commented Jun 9, 2017

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

@muratg
Copy link

muratg commented Jun 9, 2017

cc @Tratcher

@Rick-Anderson
Copy link
Contributor

@Tratcher can you come up with a sample app?

@Rick-Anderson Rick-Anderson mentioned this issue Sep 25, 2017
14 tasks
@Tratcher
Copy link
Member

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
Copy link
Contributor

@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
Copy link
Member

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
Copy link
Member

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
Copy link
Member Author

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

@Rick-Anderson
Copy link
Contributor

@danroth27 who are the right folks for the meeting?

@Rick-Anderson Rick-Anderson removed this from the 2017-Q3 milestone Sep 27, 2017
@danroth27
Copy link
Member Author

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

@Tratcher
Copy link
Member

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
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 Pri0 Urgent priority and removed Pri1 High priority, do before Pri2 and Pri3 labels Oct 24, 2017
@Rick-Anderson Rick-Anderson removed their assignment Oct 24, 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
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 Hosting non-server apps with Generic Host #5798. I think we should discuss how to organize all of the hosting content over on Hosting non-server apps with Generic Host #5798.
  2. Before working this issue (3352), it seems like we might want to work issue Hosting non-server apps with Generic Host #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
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
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
Copy link
Member

Tratcher commented May 4, 2018

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

@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
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
Copy link
Member

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

@guardrex
Copy link
Collaborator

@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
Labels
None yet
Projects
No open projects
Development

No branches or pull requests

8 participants