CancellationToken не работает в ASP.NET Core Web API HostedService

У меня есть веб-API ASP.NET Core 8 с HostedService и контроллером. Контроллер использует интерфейс, который также реализует HostedService, чтобы службу можно было запустить, остановить или перезапустить. По умолчанию предполагается отсутствие вмешательства.

Метод StartAsync использует PeriodicTimer, поэтому попытки выполнения работы выполняются каждые 15 секунд.

У меня проблема в том, что при попытке остановить службу она не останавливается. Я перепробовал все, что мог придумать, и надеялся увидеть, сможет ли кто-нибудь указать, что я делаю неправильно. Когда я говорю, что он не останавливается, после истечения тактов периодического таймера запускается другой цикл работы. CancellationToken должен быть потокобезопасным, но при этом не распознавать отмену работы.

Код

Program.cs:

using HostedService.Services;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddSingleton<IControllableBackgroundService, UserOfficeHostedService>();
builder.Services.AddHostedService<UserOfficeHostedService>();

builder.Services.Configure<HostOptions>(x =>
{
    x.ServicesStartConcurrently = true;
    x.ServicesStopConcurrently = false;
});

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseRouting();
app.UseCors(x => x
    .AllowAnyOrigin()
    .AllowAnyMethod()
    .AllowAnyHeader()
);

app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();

app.Run();

IControllableBackgroundService:

namespace HostedService.Services;

public interface IControllableBackgroundService
{
    Task StartServiceAsync();
    Task StopServiceAsync();
    Task RestartServiceAsync();
}

BackgroundServiceController:

using HostedService.Services;
using Microsoft.AspNetCore.Mvc;

namespace HostedService.Controllers;
[ApiController]
[Route("[controller]")]
public class BackgroundServiceController(IControllableBackgroundService backgroundService) : ControllerBase
{
    [HttpGet]
    [Route("start")]
    public async Task<IActionResult> StartService()
    {
        await backgroundService.StartServiceAsync();
        return Ok();
    }

    [HttpGet]
    [Route("stop")]
    public async Task<IActionResult> StopService()
    {
        await backgroundService.StopServiceAsync();
        return Ok();
    }

    [HttpGet]
    [Route("restart")]
    public async Task<IActionResult> RestartService()
    {
        await backgroundService.RestartServiceAsync();
        return Ok();
    }
}

UserOfficeHostedService:

namespace HostedService.Services;

public class UserOfficeHostedService(
    ILogger<UserOfficeHostedService> logger) : IHostedService, IControllableBackgroundService, IDisposable
{
    private CancellationTokenSource cts = new();
    private PeriodicTimer timer = new (TimeSpan.FromSeconds(15)));

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        logger.LogInformation("starting service");
        cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);

        while (await timer.WaitForNextTickAsync(cts.Token))
        {
            if (cts.Token.IsCancellationRequested)
                break;

            await UpdateUserOfficeCacheAsync(cts.Token);
        }
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        logger.LogInformation("stopping service");

        await cts.CancelAsync();
        timer.Dispose();
    }

    public async Task StartServiceAsync()
    {
        if (!cts.IsCancellationRequested) 
            return;

        timer = new PeriodicTimer(TimeSpan.FromSeconds(serviceInterval));
        await StartAsync(cts.Token);
    }

    public async Task StopServiceAsync()
    {
        await cts.CancelAsync();
        await StopAsync(CancellationToken.None);
    }

    public async Task RestartServiceAsync()
    {
        await StopAsync(cts.Token);
        timer = new PeriodicTimer(TimeSpan.FromSeconds(serviceInterval));
        await StartAsync(cts.Token);
    }

    private async Task UpdateUserOfficeCacheAsync(CancellationToken cancellationToken)
    {
        logger.LogInformation($"Performing update on UserOffice Cache. Time: {DateTimeOffset.Now}");

        try
        {
            if (cancellationToken.IsCancellationRequested)
            {
                logger.LogInformation($"Cancellation received before work attempted. Time: {DateTimeOffset.Now}");
                return;
            }

            // Simulate task
            await Task.Delay(16000, cancellationToken); // Replace this with actual long-running task logic

            if (cancellationToken.IsCancellationRequested)
            {
                logger.LogInformation($"Cancellation received after work started. Time: {DateTimeOffset.Now}");
            }
        }
        catch (TaskCanceledException)
        {
            logger.LogInformation("UserOffice Cache update was canceled.");
        }
        finally
        {
            logger.LogInformation("UserOffice Background Service released semaphore.");
        }
    }

    public void Dispose()
    {
        timer?.Dispose();
        cts?.Dispose();
    }
}

Полученные результаты

info: HostedService.Services.UserOfficeHostedService[0]
      starting service
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: https://localhost:7016
info: HostedService.Services.UserOfficeHostedService[0]
      Performing update on UserOffice Cache. Time: 7/16/2024 4:41:51 PM -04:00
info: HostedService.Services.UserOfficeHostedService[0]
      stopping service
info: HostedService.Services.UserOfficeHostedService[0]
      UserOffice Background Service released semaphore.
info: HostedService.Services.UserOfficeHostedService[0]
      Performing update on UserOffice Cache. Time: 7/16/2024 4:42:07 PM -04:00

Как видите, служба была остановлена, а затем началась новая работа. Как это исправить?

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
0
55
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

По крайней мере, одна из проблем здесь заключается в следующем:

builder.Services.AddSingleton<IControllableBackgroundService, UserOfficeHostedService>();
builder.Services.AddHostedService<UserOfficeHostedService>();

Будет зарегистрировано два разных экземпляра UserOfficeHostedService, один как IControllableBackgroundService, а другой как IHostedService, следовательно, вы будете управлять другим экземпляром, а не тем, который работает как размещенная служба.

Одним из обходных путей может быть сначала регистрация UserOfficeHostedService, а затем использование регистрации фабрики реализации, которая разрешит зарегистрированный единственный экземпляр:

builder.Services.AddSingleton<UserOfficeHostedService>();
builder.Services.AddSingleton<IControllableBackgroundService>(sp => sp.GetRequiredService<UserOfficeHostedService>());
builder.Services.AddHostedService(sp => sp.GetRequiredService<UserOfficeHostedService>());

Другие вопросы по теме