Unable to start multiple Kafka consumers - c#

I have a console app that has multiple BackgroundServices, each reading from the same Kafka topic using the Confluent.Kafka nuget package (v1.6.2). The topic has 3 partitions.
When the app starts, all the background services have their constructors called, however only one of the ExecuteAsync methods is ever called. If I add a Task.Delay() - the number of milliseconds doesn't seem to matter - at the start of each ExecuteAsync, everything works fine and all the background services run.
No exceptions are raised, as far as I can tell.
Does anyone have an idea of what may be happening, or where to look further?
Here's the code:
using Confluent.Kafka;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace KafkaConsumer
{
class Program
{
static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
private static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
services.AddHostedService<ConsumerA>();
services.AddHostedService<ConsumerB>();
services.AddHostedService<ConsumerC>();
});
}
public class ConsumerA : BackgroundService
{
private readonly ILogger<ConsumerA> _logger;
private readonly IConsumer<Ignore, string> _consumer;
public ConsumerA(ILogger<ConsumerA> logger)
{
_logger = logger;
var config = new ConsumerConfig()
{
BootstrapServers = #"server:port",
GroupId = "Group1",
AutoOffsetReset = AutoOffsetReset.Earliest
};
_consumer = new ConsumerBuilder<Ignore, string>(config).Build();
_logger.LogInformation("ConsumerA constructor");
}
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
// await Task.Delay(10);
_logger.LogInformation("ConsumerA starting");
_consumer.Subscribe(new List<string> { "topic" });
while (!cancellationToken.IsCancellationRequested)
{
_ = _consumer.Consume(cancellationToken);
}
}
}
public class ConsumerB : BackgroundService
{
private readonly ILogger<ConsumerB> _logger;
private readonly IConsumer<Ignore, string> _consumer;
public ConsumerB(ILogger<ConsumerB> logger)
{
_logger = logger;
var config = new ConsumerConfig()
{
BootstrapServers = #"server:port",
GroupId = "Group1",
AutoOffsetReset = AutoOffsetReset.Earliest
};
_consumer = new ConsumerBuilder<Ignore, string>(config).Build();
_logger.LogInformation("ConsumerB constructor");
}
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
// await Task.Delay(10);
_logger.LogInformation("ConsumerB starting");
_consumer.Subscribe(new List<string> { "topic" });
while (!cancellationToken.IsCancellationRequested)
{
_ = _consumer.Consume(cancellationToken);
}
}
}
public class ConsumerC : BackgroundService
{
private readonly ILogger<ConsumerC> _logger;
private readonly IConsumer<Ignore, string> _consumer;
public ConsumerC(ILogger<ConsumerC> logger)
{
_logger = logger;
var config = new ConsumerConfig()
{
BootstrapServers = #"server:port",
GroupId = "Group1",
AutoOffsetReset = AutoOffsetReset.Earliest
};
_consumer = new ConsumerBuilder<Ignore, string>(config).Build();
_logger.LogInformation("ConsumerC constructor");
}
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
// await Task.Delay(10);
_logger.LogInformation("ConsumerC starting");
_consumer.Subscribe(new List<string> { "topic" });
while (!cancellationToken.IsCancellationRequested)
{
_ = _consumer.Consume(cancellationToken);
}
}
}
}
And the output:
(with no delays):
info: KafkaConsumer.ConsumerA[0]
ConsumerA constructor
info: KafkaConsumer.ConsumerB[0]
ConsumerB constructor
info: KafkaConsumer.ConsumerC[0]
ConsumerC constructor
info: KafkaConsumer.ConsumerA[0]
ConsumerA starting
(with delays added):
info: KafkaConsumer.ConsumerA[0]
ConsumerA constructor
info: KafkaConsumer.ConsumerB[0]
ConsumerB constructor
info: KafkaConsumer.ConsumerC[0]
ConsumerC constructor
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
Content root path: c:\users\..\kafkaconsumer\bin\Debug\net5.0
info: KafkaConsumer.ConsumerC[0]
ConsumerC starting
info: KafkaConsumer.ConsumerA[0]
ConsumerA starting
info: KafkaConsumer.ConsumerB[0]
ConsumerB starting

When starting up the BackgroundServices, the framework is evidently doing something like this:
var starting1 = service1.ExecuteAsync(...); //all called in sequence without awaits inbetween
var starting2 = service2.ExecuteAsync(...);
var starting3 = service3.ExecuteAsync(...);
...
//will await the startings all at once later on
Of course, when it does this in one of your services, it immediately gets trapped in a synchronous loop in which it blockingly polls the Kafka consumer. The thread of execution is never yielded back to the framework to continue calling other services.
You can get around this by doing your synchronous looping on separate threads, leaving the framework to happily go about its business:
protected Task ExecuteAsync(...)
{
return Task.Run(() => { //runs the below on a separate thread from the threadpool
_logger.LogInformation("ConsumerC starting");
_consumer.Subscribe(new List<string> { "topic" });
while (!cancellationToken.IsCancellationRequested)
{
_ = _consumer.Consume(cancellationToken);
}
});
}
When dealing with async APIs, there's a general expectation that you don't sit on your given thread, as doing so can cause problems for things above you that are expecting the thread back. When you await things, the point of execution 'stays' in your code but really the thread is given back to the caller while a continuation is queued to carry on doing your stuff at the right time (transparently, mostly).
Unfortunately as far as I know the Kafka libraries don't have APIs for playing along with this, and so they require full threads of their own.

This is because your execute method is async but you don't use await inside that to inform SynchronizationContext.
Write your executeAsync method like this:
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
// await Task.Delay(10);
_logger.LogInformation("ConsumerC starting");
await Task.Run(() => _consumer.Subscribe(new List<string> { "topic" }));
while (!cancellationToken.IsCancellationRequested)
{
_await Task.Run(() => consumer.Consume(cancellationToken));
}
}

Related

Unit test .net 6 console app using IHostApplicationLifeTime

I have a class (CronClass) that inherits from IHostedService with 2 methods which are StartAsync(CancellationToken) and StopAsync(CancellationToken).
How do you go about unit testing the StartAsync method to verify that the code was executed, using Moq Library?
For example:
public class CronClass: IHostedService
{
private readonly IHostedApplicationLifetime applicationLifetime;
private readonly IService service;
// IHostedApplicationLifetime/IService are injected DI to via the constructor
public Task StartAsync(CancellationToken cancellationToken)
{
applicationLifeTime.ApplicationStarted.Register(() =>
{
Task.Run(async () =>
{
log.LogInformation("Cron Started");
await service.Process();
});
});
}
//...
}
I would start with creating a mock of IHostApplicationLifetime
public class MockHostApplicationLifetime : IHostApplicationLifetime, IDisposable
{
internal readonly CancellationTokenSource _ctsStart = new CancellationTokenSource();
internal readonly CancellationTokenSource _ctsStopped = new CancellationTokenSource();
internal readonly CancellationTokenSource _ctsStopping = new CancellationTokenSource();
public MockHostApplicationLifetime()
{
}
public void Started()
{
_ctsStart.Cancel();
}
CancellationToken IHostApplicationLifetime.ApplicationStarted => _ctsStart.Token;
CancellationToken IHostApplicationLifetime.ApplicationStopping => _ctsStopping.Token;
CancellationToken IHostApplicationLifetime.ApplicationStopped => _ctsStopped.Token;
public void Dispose()
{
_ctsStopped.Cancel();
_ctsStart.Dispose();
_ctsStopped.Dispose();
_ctsStopping.Dispose();
}
public void StopApplication()
{
_ctsStopping.Cancel();
}
}
In your unit test create a mock of IService. Create instance of CronClass and call cronClass.StartAsync. Then start MockHostApplicationLifetime. It will trigger registered callback ApplicationStarted.Register. Then verify that Process() was called.
You are starting the task in Register method, so it can happen that the unit test can finish before the task is created and service.Process is called. In that case I would wait some time before verification.
[Test]
public async Task Test1()
{
var hal = new MockHostApplicationLifetime();
var mockService = new Mock<IService>();
var cronClass = new CronClass(hal, mockService.Object);
await cronClass.StartAsync(CancellationToken.None);
hal.Started();
// maybe not needed, test passed without the delay
//await Task.Delay(500);
mockService.Verify(mock => mock.Process());
}

Topshelf start stuck in infinite loop

I'm creating a message processor to take messages of a queue
I have used topshelf for this and justgot some basic code for now. However my message processor is stuck in a loop and causing my topshelf service to not start. I thought if I returned and stored the task, this would not be the case
class Program
{
static void Main(string[] args)
{
HostFactory.Run(configure =>
{
configure.Service<WorkerService>(service =>
{
service.ConstructUsing(() => new WorkerService());
service.WhenStarted(s => s.Start());
service.WhenStopped(s => s.Stop());
});
configure.RunAsLocalSystem();
});
}
}
public class WorkerService
{
private Task _task;
private Processor _processor;
private readonly CancellationTokenSource _cancellation;
public WorkerService()
{
_cancellation = new CancellationTokenSource();
_processor = new Processor();
}
public void Start()
{
Console.WriteLine("Starting");
_task = _processor.Run(_cancellation.Token);
Console.WriteLine("I NEVER GET HERE");
}
public void Stop()
{
_cancellation.Cancel();
_task.Wait();
}
}
public class Processor
{
public async Task Run(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
Console.WriteLine("Running");
}
}
}
So when I look at my windows services I just see this app stuck in "Starting"
Your run function doesn't actually hit an await call (where it will exit and later resume). In fact, it doesn't exit at all. You're stuck in the Run method. Try putting this after your Console.WriteLine:
await Task.Delay(200);
In addition, you might consider not mixing async/await with traditional Task.Wait(), as that's known to cause deadlocks as well. Example: await vs Task.Wait - Deadlock?

Cancellation token processing in multiple customers of Service Bus Queue

I have configurable count of Server Bus queue consumers in a single process. The code uses ReceiveAsync method of QueueClient class and it invokes QueueClient.Close on cancellation.
It works pretty well but it turned out that there is some issue with closing QueueClient - only one client ends immediately, all others hang until serverWaitTime timeout expires.
Look at the code and its output:
using System;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.ServiceBus.Messaging;
public class Program
{
private static void Main()
{
CancellationTokenSource source = new CancellationTokenSource();
var cancellationToken = source.Token;
var logger = new Logger();
Task.Run(() =>
{
Task.Delay(TimeSpan.FromSeconds(10)).Wait();
source.Cancel();
logger.Log("Cancellation requested.");
});
string connectionString = "...";
string queueName = "...";
var workers = Enumerable.Range(1, 3).Select(i => new Worker(connectionString, queueName, logger));
var tasks = workers.Select(worker => Task.Run(() => worker.RunAsync(cancellationToken), cancellationToken)).ToArray();
Task.WaitAll(tasks);
logger.Log("The end.");
}
}
class Worker
{
private readonly Logger _logger;
private readonly QueueClient _queueClient;
public Worker(string connectionString, string queueName, Logger logger)
{
_logger = logger;
_queueClient = QueueClient.CreateFromConnectionString(connectionString, queueName);
}
public async Task RunAsync(CancellationToken cancellationToken)
{
_logger.Log($"Worker {GetHashCode()} started.");
using (cancellationToken.Register(() => _queueClient.Close()))
while (!cancellationToken.IsCancellationRequested)
{
try
{
var message = await _queueClient.ReceiveAsync(TimeSpan.FromSeconds(20));
_logger.Log($"Worker {GetHashCode()}: Process message {message.MessageId}...");
}
catch (OperationCanceledException ex)
{
_logger.Log($"Worker {GetHashCode()}: {ex.Message}");
}
}
_logger.Log($"Worker {GetHashCode()} finished.");
}
}
class Logger
{
private readonly Stopwatch _stopwatch;
public Logger()
{
_stopwatch = new Stopwatch();
_stopwatch.Start();
}
public void Log(string message) => Console.WriteLine($"{_stopwatch.Elapsed}: {message}");
}
Output:
00:00:00.8125644: Worker 12547953 started.
00:00:00.8127684: Worker 45653674 started.
00:00:00.8127314: Worker 59817589 started.
00:00:10.4534961: Cancellation requested.
00:00:11.4912900: Worker 45653674: The operation cannot be performed because the entity has been closed or aborted.
00:00:11.4914054: Worker 45653674 finished.
00:00:22.3242631: Worker 12547953: The operation cannot be performed because the entity has been closed or aborted.
00:00:22.3244501: Worker 12547953 finished.
00:00:22.3243945: Worker 59817589: The operation cannot be performed because the entity has been closed or aborted.
00:00:22.3252456: Worker 59817589 finished.
00:00:22.3253535: The end.
So as you can see the worker 45653674 stopped immediately but two others stopped only 10 seconds later.
I found some helpful information in this article: https://developers.de/blogs/damir_dobric/archive/2013/12/03/service-bus-undocumented-scaling-tips-amp-tricks.aspx. The issue goes away if each queue client works via its own physical connection.
So to fix the issue it's necessary to replace the following code:
_queueClient = QueueClient.CreateFromConnectionString(connectionString, queueName);
with
var factory = MessagingFactory.CreateFromConnectionString(connectionString);
_queueClient = factory.CreateQueueClient(queueName);

Log configuration changes in ASP.NET Core

I want to log when configuration is changed.
I do this in Program.cs or Startup.cs:
ChangeToken.OnChange(
() => configuration.GetReloadToken(),
state => logger.Information("Configuration reloaded"),
(object)null
);
But I get double change reports, so it needs to be debounced. The advice is to do this:
ChangeToken.OnChange(
() => configuration.GetReloadToken(),
state => { Thread.Sleep(2000); logger.Information("Configuration reloaded"); },
(object)null
);
I'm using 2000 here as I'm not sure what's a reasonable value.
I've found that sometimes I still get multiple change detections, separated by 2000 milliseconds. So the debounce doesn't work for me, just causes a delay between reported changes. If I set a high value then I only get one report, but that isn't ideal (and conceals the problem).
So I'd like to know:
Is this really debouncing, or just queueing reported changes?
I've used values from 1000 to 5000 to varying success. What are others using?
Is the sleep issued to the server's main thread? I hope not!
The multiple change detection issue discussed here (and at least a dozen other issues in multiple repos) is something they refuse to address using a built-in mechanism.
The MS docs use a file hashing approach, but I think that debouncing is better.
My solution uses async (avoids async-in-sync which could blow up something accidentally) and a hosted service that debounces change detections.
Debouncer.cs:
public sealed class Debouncer : IDisposable {
public Debouncer(TimeSpan? delay) => _delay = delay ?? TimeSpan.FromSeconds(2);
private readonly TimeSpan _delay;
private CancellationTokenSource? previousCancellationToken = null;
public async Task Debounce(Action action) {
_ = action ?? throw new ArgumentNullException(nameof(action));
Cancel();
previousCancellationToken = new CancellationTokenSource();
try {
await Task.Delay(_delay, previousCancellationToken.Token);
await Task.Run(action, previousCancellationToken.Token);
}
catch (TaskCanceledException) { } // can swallow exception as nothing more to do if task cancelled
}
public void Cancel() {
if (previousCancellationToken != null) {
previousCancellationToken.Cancel();
previousCancellationToken.Dispose();
}
}
public void Dispose() => Cancel();
}
ConfigWatcher.cs:
public sealed class ConfigWatcher : IHostedService, IDisposable {
public ConfigWatcher(IServiceScopeFactory scopeFactory, ILogger<ConfigWatcher> logger) {
_scopeFactory = scopeFactory;
_logger = logger;
}
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<ConfigWatcher> _logger;
private readonly Debouncer _debouncer = new(TimeSpan.FromSeconds(2));
private void OnConfigurationReloaded() {
_logger.LogInformation("Configuration reloaded");
// ... can do more stuff here, e.g. validate config
}
public Task StartAsync(CancellationToken cancellationToken) {
ChangeToken.OnChange(
() => { // resolve config from scope rather than ctor injection, in case it changes (this hosted service is a singleton)
using var scope = _scopeFactory.CreateScope();
var configuration = scope.ServiceProvider.GetRequiredService<IConfiguration>();
return configuration.GetReloadToken();
},
async () => await _debouncer.Debounce(OnConfigurationReloaded)
);
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public void Dispose() => _debouncer.Dispose();
}
Startup.cs:
services.AddHostedService<ConfigWatcher>(); // registered as singleton
Hopefully, someone else can answer your questions, but I did run into this issue and found this Gist by cocowalla.
The code provided by cocowalla debounces instead of just waiting. It successfully deduplicated the change callback for me.
Cocowalla also includes an extension method so you can simply call OnChange on the IConfiguration.
Here's a sample:
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Primitives;
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
class Program
{
public static async Task Main(string[] args)
{
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile(path: "appsettings.json", optional: false, reloadOnChange: true)
.Build();
configuration.OnChange(() => Console.WriteLine("configuration changed"));
while (true)
{
await Task.Delay(1000);
}
}
}
public class Debouncer : IDisposable
{
private readonly CancellationTokenSource cts = new CancellationTokenSource();
private readonly TimeSpan waitTime;
private int counter;
public Debouncer(TimeSpan? waitTime = null)
{
this.waitTime = waitTime ?? TimeSpan.FromSeconds(3);
}
public void Debouce(Action action)
{
var current = Interlocked.Increment(ref this.counter);
Task.Delay(this.waitTime).ContinueWith(task =>
{
// Is this the last task that was queued?
if (current == this.counter && !this.cts.IsCancellationRequested)
action();
task.Dispose();
}, this.cts.Token);
}
public void Dispose()
{
this.cts.Cancel();
}
}
public static class IConfigurationExtensions
{
/// <summary>
/// Perform an action when configuration changes. Note this requires config sources to be added with
/// `reloadOnChange` enabled
/// </summary>
/// <param name="config">Configuration to watch for changes</param>
/// <param name="action">Action to perform when <paramref name="config"/> is changed</param>
public static void OnChange(this IConfiguration config, Action action)
{
// IConfiguration's change detection is based on FileSystemWatcher, which will fire multiple change
// events for each change - Microsoft's code is buggy in that it doesn't bother to debounce/dedupe
// https://github.com/aspnet/AspNetCore/issues/2542
var debouncer = new Debouncer(TimeSpan.FromSeconds(3));
ChangeToken.OnChange<object>(config.GetReloadToken, _ => debouncer.Debouce(action), null);
}
}
In the sample, the debounce delay is 3 seconds, for my small json file, the debounce delay stops deduplicating around 230 milliseconds.

Graceful shutdown with Generic Host in .NET Core 2.1

.NET Core 2.1 introduced new Generic Host, which allows to host non-HTTP workloads with all benefits of Web Host. Currently, there is no much information and recipes with it, but I used following articles as a starting point:
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host?view=aspnetcore-2.1
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-2.1
https://learn.microsoft.com/en-us/dotnet/standard/microservices-architecture/multi-container-microservice-net-applications/background-tasks-with-ihostedservice
My .NET Core application starts, listens for new requests via RabbitMQ message broker and shuts down by user request (usually by Ctrl+C in console). However, shutdown is not graceful - application still have unfinished background threads while it returns control to OS. I see it by console messages - when I press Ctrl+C in console I see few lines of console output from my application, then OS command prompt and then again console output from my application.
Here is my code:
Program.cs
public class Program
{
public static async Task Main(string[] args)
{
var host = new HostBuilder()
.ConfigureHostConfiguration(config =>
{
config.SetBasePath(AppContext.BaseDirectory);
config.AddEnvironmentVariables(prefix: "ASPNETCORE_");
config.AddJsonFile("hostsettings.json", optional: true);
})
.ConfigureAppConfiguration((context, config) =>
{
var env = context.HostingEnvironment;
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);
config.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);
if (env.IsProduction())
config.AddDockerSecrets();
config.AddEnvironmentVariables();
})
.ConfigureServices((context, services) =>
{
services.AddLogging();
services.AddHostedService<WorkerPoolHostedService>();
// ... other services
})
.ConfigureLogging((context, logging) =>
{
if (context.HostingEnvironment.IsDevelopment())
logging.AddDebug();
logging.AddSerilog(dispose: true);
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(context.Configuration)
.CreateLogger();
})
.UseConsoleLifetime()
.Build();
await host.RunAsync();
}
}
WorkerPoolHostedService.cs
internal class WorkerPoolHostedService : IHostedService
{
private IList<VideoProcessingWorker> _workers;
private CancellationTokenSource _stoppingCts = new CancellationTokenSource();
protected WorkerPoolConfiguration WorkerPoolConfiguration { get; }
protected RabbitMqConfiguration RabbitMqConfiguration { get; }
protected IServiceProvider ServiceProvider { get; }
protected ILogger<WorkerPoolHostedService> Logger { get; }
public WorkerPoolHostedService(
IConfiguration configuration,
IServiceProvider serviceProvider,
ILogger<WorkerPoolHostedService> logger)
{
this.WorkerPoolConfiguration = new WorkerPoolConfiguration(configuration);
this.RabbitMqConfiguration = new RabbitMqConfiguration(configuration);
this.ServiceProvider = serviceProvider;
this.Logger = logger;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
var connectionFactory = new ConnectionFactory
{
AutomaticRecoveryEnabled = true,
UserName = this.RabbitMqConfiguration.Username,
Password = this.RabbitMqConfiguration.Password,
HostName = this.RabbitMqConfiguration.Hostname,
Port = this.RabbitMqConfiguration.Port,
VirtualHost = this.RabbitMqConfiguration.VirtualHost
};
_workers = Enumerable.Range(0, this.WorkerPoolConfiguration.WorkerCount)
.Select(i => new VideoProcessingWorker(
connectionFactory: connectionFactory,
serviceScopeFactory: this.ServiceProvider.GetRequiredService<IServiceScopeFactory>(),
logger: this.ServiceProvider.GetRequiredService<ILogger<VideoProcessingWorker>>(),
cancellationToken: _stoppingCts.Token))
.ToList();
this.Logger.LogInformation("Worker pool started with {0} workers.", this.WorkerPoolConfiguration.WorkerCount);
}
public async Task StopAsync(CancellationToken cancellationToken)
{
this.Logger.LogInformation("Stopping working pool...");
try
{
_stoppingCts.Cancel();
await Task.WhenAll(_workers.SelectMany(w => w.ActiveTasks).ToArray());
}
catch (AggregateException ae)
{
ae.Handle((Exception exc) =>
{
this.Logger.LogError(exc, "Error while cancelling workers");
return true;
});
}
finally
{
if (_workers != null)
{
foreach (var worker in _workers)
worker.Dispose();
_workers = null;
}
}
}
}
VideoProcessingWorker.cs
internal class VideoProcessingWorker : IDisposable
{
private readonly Guid _id = Guid.NewGuid();
private bool _disposed = false;
protected IConnection Connection { get; }
protected IModel Channel { get; }
protected IServiceScopeFactory ServiceScopeFactory { get; }
protected ILogger<VideoProcessingWorker> Logger { get; }
protected CancellationToken CancellationToken { get; }
public VideoProcessingWorker(
IConnectionFactory connectionFactory,
IServiceScopeFactory serviceScopeFactory,
ILogger<VideoProcessingWorker> logger,
CancellationToken cancellationToken)
{
this.Connection = connectionFactory.CreateConnection();
this.Channel = this.Connection.CreateModel();
this.Channel.BasicQos(prefetchSize: 0, prefetchCount: 1, global: false);
this.ServiceScopeFactory = serviceScopeFactory;
this.Logger = logger;
this.CancellationToken = cancellationToken;
#region [ Declare ]
// ...
#endregion
#region [ Consume ]
// ...
#endregion
}
// ... worker logic ...
public void Dispose()
{
if (!_disposed)
{
this.Channel.Close(200, "Goodbye");
this.Channel.Dispose();
this.Connection.Close();
this.Connection.Dispose();
this.Logger.LogDebug("Worker {0}: disposed.", _id);
}
_disposed = true;
}
}
So, when I press Ctrl+C I see following output in console (when there is no request processing):
Stopping working pool...
command prompt
Worker id: disposed.
How to shutdown gracefully?
You need IApplicationLifetime. This provides you with all the needed information about application start and shutdown. You can even trigger the shutdown with it via appLifetime.StopApplication();
Look at https://github.com/aspnet/Docs/blob/66916c2ed3874ed9b000dfd1cab53ef68e84a0f7/aspnetcore/fundamentals/host/generic-host/samples/2.x/GenericHostSample/LifetimeEventsHostedService.cs
Snippet(if the link becomes invalid):
public Task StartAsync(CancellationToken cancellationToken)
{
appLifetime.ApplicationStarted.Register(OnStarted);
appLifetime.ApplicationStopping.Register(OnStopping);
appLifetime.ApplicationStopped.Register(OnStopped);
return Task.CompletedTask;
}
I'll share some patterns I think works very well for non-WebHost projects.
namespace MyNamespace
{
public class MyService : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
private readonly IApplicationLifetime _appLifetime;
public MyService(
IServiceProvider serviceProvider,
IApplicationLifetime appLifetime)
{
_serviceProvider = serviceProvider;
_appLifetime = appLifetime;
}
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
_appLifetime.ApplicationStopped.Register(OnStopped);
return RunAsync(stoppingToken);
}
private async Task RunAsync(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
using (var scope = _serviceProvider.CreateScope())
{
var runner = scope.ServiceProvider.GetRequiredService<IMyJobRunner>();
await runner.RunAsync();
}
}
}
public void OnStopped()
{
Log.Information("Window will close automatically in 20 seconds.");
Task.Delay(20000).GetAwaiter().GetResult();
}
}
}
A couple notes about this class:
I'm using the BackgroundService abstract class to represent my service. It's available in the Microsoft.Extensions.Hosting.Abstractions package. I believe this is planned to be in .NET Core 3.0 out of the box.
The ExecuteAsync method needs to return a Task representing the running service. Note: If you have a synchronous service wrap your "Run" method in Task.Run().
If you want to do additional setup or teardown for your service you can inject the app lifetime service and hook into events. I added an event to be fired after the service is fully stopped.
Because you don't have the auto-magic of new scope creation for each web request as you do in MVC projects you have to create your own scope for scoped services. Inject IServiceProvider into the service to do that. All dependencies on the scope should be added to the DI container using AddScoped().
Set up the host in Main( string[] args ) so that it shuts down gracefully when CTRL+C / SIGTERM is called:
IHost host = new HostBuilder()
.ConfigureServices( ( hostContext, services ) =>
{
services.AddHostedService<MyService>();
})
.UseConsoleLifetime()
.Build();
host.Run(); // use RunAsync() if you have access to async Main()
I've found this set of patterns to work very well outside of ASP.NET applications.
Be aware that Microsoft has built against .NET Standard so you don't need to be on .NET Core to take advantage of these new conveniences. If you're working in Framework just add the relevant NuGet packages. The package is built against .NET Standard 2.0 so you need to be on Framework 4.6.1 or above. You can find the code for all of the infrastructure here and feel free to poke around at the implementations for all the abstractions you are working with: https://github.com/aspnet/Extensions
In Startup.cs, you can terminate the application with the Kill() method of the current process:
public void Configure(IHostApplicationLifetime appLifetime)
{
appLifetime.ApplicationStarted.Register(() =>
{
Console.WriteLine("Press Ctrl+C to shut down.");
});
appLifetime.ApplicationStopped.Register(() =>
{
Console.WriteLine("Shutting down...");
System.Diagnostics.Process.GetCurrentProcess().Kill();
});
}
Program.cs
Don't forget to use UseConsoleLifetime() while building the host.
Host.CreateDefaultBuilder(args).UseConsoleLifetime(opts => opts.SuppressStatusMessages = true);

Categories

Resources