У меня есть веб-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>());