'.net core, testing service application. After leaving worker loop, program not stopping until console window closed. Is this normal behaviour?
I'm testing a .net core 6 multi-threaded service worker app that processes batches of data rows and tables on sql server.
I use serilog to log to file only.
When I run it in debug mode, a console window opens but it's empty and remains empty. I use a counter for testing purposes so that after so many loops the ExecuteAsync() function in my main worker exits.
I've noticed that after the main worker returns, the finally block in my program.cs Main method isn't breaking (I've a break point on it), and the program won't 'return' to visual studio until I close the console, there's not even a console message asking me to press a key. Even if I close the console the finally {Log.CloseAndFlush} block isn't breaking. I found my debug output prints many lines afterwards similar to
"The thread 0x7590 has exited with code 0 (0x0).
The thread 0xdcc has exited with code 0 (0x0).
The thread 0x6c68 has exited with code 0 (0x0).
The thread 0x690c has exited with code 0 (0x0).
The thread 0x3ec0 has exited with code 0 (0x0).
The thread 0x7f08 has exited with code 0 (0x0)."
after I closed the console window I got the following output
"The program '[18244] pulse.deletemember.service.exe: Program Trace' has exited with code 0 (0x0).
The program '[18244] pulse.deletemember.service.exe' has exited with code 3221225786 (0xc000013a)."
I've included the main code. Can anybody suggest why this might be happening? In a similar thread somebody suggested it may be caused by things not being disposed of. I usually use BackgroundWorker as I thoroughly understand their behaviour and disposing of them etc, this await task stuff is new to me.
program.cs
using Serilog;
namespace pulse.deletemember.service;
public class Program
{
// With help from https://www.youtube.com/watch?v=_iryZxv8Rxw&t=1055s
public static void Main(string[] args)
{
try
{
Environment.CurrentDirectory = AppDomain.CurrentDomain.BaseDirectory;
// Create temp logger before dependency injection takes over
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(MyConfiguration.ConfigurationSettings)
.CreateLogger();
Log.Logger.Information("Application starting up");
// Start the application and wait for it to complete
CreateHostBuilder(args).Build().Run();
}
catch (Exception err)
{
Log.Fatal(err, "The application failed to start correctly.");
}
finally
{
Log.CloseAndFlush();
}
}
/// <summary>
/// This sets up app
/// </summary>
public static IHostBuilder CreateHostBuilder(string[] args)
{
return Host.CreateDefaultBuilder(args)
.UseWindowsService()
.UseSerilog()
.ConfigureServices(ConfigureDelegate)
;
}
/// <summary>
/// Example of Dependency Injection
/// Injecting singleton instance of MyConfiguration.
/// Tested that config is re-read if changed
/// </summary>
/// <param name="hostContext"></param>
/// <param name="services"></param>
private static void ConfigureDelegate(HostBuilderContext hostContext, IServiceCollection services)
{
// Injects Worker thread. This adds a singleton that lasts length of the application
services.AddHostedService<Worker>();
services.AddSingleton<IMyConfiguration,MyConfiguration>();
services.AddSingleton<IRepository, Repository>();
services.AddSingleton<IMemoryQueryClass, MemoryQueryClass>();
}
}
worker.cs
using System.Data.SqlClient;
namespace pulse.deletemember.service
{
internal class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
private readonly IMyConfiguration _configuration;
private readonly IRepository _repository;
private readonly IMemoryQueryClass _memoryQueryClass;
private readonly List<Task> _taskQueue = new();
private long _counter;
public Worker(ILogger<Worker> logger, IMyConfiguration configuration, IRepository repository,
IMemoryQueryClass memoryQueryClass)
{
_logger = logger;
_configuration = configuration;
_repository = repository;
_memoryQueryClass = memoryQueryClass;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// Initialise the delay from config file
var delay = _configuration.PollDelaySecs;
_logger.LogInformation("Worker running at: {time} ({e} utc)", DateTime.UtcNow.ToString("g"),
Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT"));
while (!stoppingToken.IsCancellationRequested)
{
// are we finished yet?
if (_configuration.MaxCounter >= 0 && _counter >= _configuration.MaxCounter) break;
_counter += _configuration.BatchSize;
try
{
var memoryUsed = _memoryQueryClass.PrivateMemorySize;
_logger.LogInformation("Loop starting at {time} utc. Private memory = {memoryUsed:N1} Mb",
DateTime.UtcNow.ToString("g"),memoryUsed);
await Task.Delay(delay * 1000, stoppingToken);
/* https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/asynchronous-programming */
_repository.ResetMutexes();
stoppingToken.ThrowIfCancellationRequested();
// Delete old records in membership deletions log table
_repository.TruncateDeletionsLogTable();
// Pull in a bunch of member rows for deletion
using var members = _repository.ReadBatch();
if (members.Count == 0)
{
// zero records read, so set delay to 'normal' interval and start loop again
delay = _configuration.PollDelaySecs;
continue;
}
// Set delay to zero while more than zero records read from deletions table
delay = 0;
var taskQueueQuery =
from memberRow in members
select _repository.MainDeleteProcessAsync(memberRow, stoppingToken);
// Convert query to enumerable list
_taskQueue.AddRange(taskQueueQuery.ToList());
// While any task is available in the list, keep waiting.
// Once queue is empty then start overall loop again
while (_taskQueue.Any())
{
var finishedTask = await Task.WhenAny(_taskQueue);
// remove task from queue so taskQueue knows when to exit loop
_taskQueue.Remove(finishedTask);
stoppingToken.ThrowIfCancellationRequested();
}
}
catch (SqlException err)
{
// As errors are handled in their own threads, this handler will only
// catch when reading in batch.
_logger.LogError(err, "Sql Exception in worker");
if (err.Number == 18487 || err.Number == 18488)
_logger.LogError("err.Number == 18487 || err.Number == 18488, password expired or needs to be reset");
// Set delay for a min to allow for reboots or whatever
delay = 60;
}
catch (OperationCanceledException err)
{
_logger.LogError(err, "Stoppingtoken operation was cancelled");
break;
}
catch (Exception err)
{
_logger.LogError(err, "General error caught in worker loop. Ignoring");
}
} // while
if (stoppingToken.IsCancellationRequested)
_logger.LogInformation("stoppingToken.Cancelled set. Exiting application");
_logger.LogInformation("Service exited. Counter = {counter}",_counter);
}// function
}
}
Solution 1:[1]
Tried with several google queries to find answers and luckily I did.
Firstly I found that services don't behave like applications, they continue to run even after the worker thread quits. Tbh, why that's the default behaviour is a mystery, would be interested to know why.
How do I (gracefully) shut down a worker service from within itself?
From there I wondered how to create an instance of a IHostApplicationLifetime that can be used in my app. That's when I found a followup question to the above link. An instance of this is supported already and just needs adding to the constructor parameters of the worker thread.
How to get and inject the IHostApplicationLifetime in my service to the container (Console App)
I tried adding _hostApplicationLifetime.StopApplication(); as the final line in the worker loop and it worked. Main() in program.cs resumed, I received a notification in the console window and log file was flushed and closed.
But I'd still like to understand why the app continues to run despite the main loop exiting if anybody can explain please.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Czeshirecat |