У меня есть веб-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
Как видите, служба была остановлена, а затем началась новая работа. Как это исправить?





По крайней мере, одна из проблем здесь заключается в следующем:
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>());