.NET核心,测试服务应用程序。离开工人循环后,直到控制台窗口关闭之前,程序才停止。这是正常的行为吗?

发布于 2025-01-28 05:05:34 字数 6721 浏览 1 评论 0原文

我正在测试一个.NET Core 6多线程服务工作者应用程序,该应用程序处理SQL Server上的数据行和表批次。

我使用serilog进行日志以归档。

当我以调试模式运行它时,请打开控制台窗口,但它是空的,并且保持空。 我将计数器用于测试目的,以便在我的主要工人退出中的executeasync()函数之后。

我注意到,在主要工人返回后,我的程序中的最终障碍。CSMain方法不会破坏(我已经有一个断点),并且该程序在我之前不会“返回” Visual Studio关闭控制台,甚至没有控制台消息要求我按键。即使我关闭控制台,最后{log.closeandflush}块也不会破裂。 发现我的调试输出打印了许多线,类似于

“线程0x7590已使用代码0(0x0)

退出

我 。

0x690c已使用代码0(0x0)

退出

关闭控制台窗口后,我将获得以下输出

“程序'[18244] pulse.deletemember.service.exe:程序跟踪'已使用代码0(0x0)退出

。 EXE'已卸下代码3221225786(0xc000013a)。”

我包含了主要代码。有人可以建议为什么会发生这种情况吗?在类似的线程中,有人建议它可能是由于未处理的东西而引起的。我通常会使用背景工作者,因为我完全了解他们的行为和处理它们等,这对我来说是新的。

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://learn.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
  }
}

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://learn.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
  }
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

独自←快乐 2025-02-04 05:05:34

尝试了一些Google查询以找到答案,幸运的是我做到了。

首先,我发现服务的行为不像应用程序,即使工人线程退出后,它们仍会继续运行。 TBH,为什么这是默认行为是一个谜,很感兴趣知道为什么。

< 我想从那里进行的工人服务?

我想知道如何创建可以在我的应用程序中使用的iHostapplicationlifetime的实例。那时我找到了上述链接的后续问题。已经支持了一个实例,只需要添加到工作线程的构造函数参数中即可。

如何获得和注入在我对容器(Console App)的服务中,iHostApplicationLifetime

我尝试添加_hostapplicationlifetime.stopapplication();作为工人循环中的最后一行,它起作用了。 program.cs中的main()恢复了,我在控制台窗口中收到了一条通知,并将日志文件刷新并关闭。

但是,我仍然想理解,尽管有人可以解释,但尽管主要循环退出,但该应用程序仍继续运行。

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文