DeepSeek, Task, Complete, BackGroundService, UseFullCode, CompleteCode
https://chat.deepseek.com/share/xy5j143awbsekgjt8g
Часто при завершении Приложения мы ловим
исключения вот на этой конструкции await Task.Delay(1000, stoppingToken); Как
сделать чтобы завершать Приложение нормально? Может быть в какую-то функцию
спрятать и там исключение получить. Лучшим вариантом мог бы быть gracefull
complete
Мы хотим избежать необработанных
исключений при завершении приложения, когда используется Task.Delay с токеном отмены.
Исключение возникает, потому что при завершении приложения токен отмены (stoppingToken) может быть отменен, и тогда Task.Delay выбрасывает OperationCanceledException.
Мы можем обработать это исключение, чтобы завершить приложение нормально.
Один из способов — обернуть вызов Task.Delay в try-catch и обработать OperationCanceledException (или более конкретно TaskCanceledException).
Однако, если мы не хотим видеть исключение в логах, мы можем просто проверить,
отменен ли токен, перед вызовом Task.Delay или
использовать другой метод.
Но важно отметить, что Task.Delay с токеном отмены бросает
исключение, когда токен отменяется. Это ожидаемое поведение.
Вместо того чтобы ловить исключение в
каждом месте, мы можем создать вспомогательный метод, который будет выполнять
задержку, но не будет бросать исключение при отмене.
Предлагаю два варианта:
- Использовать
try-catch вокруг Task.Delay и
игнорировать исключение отмены (только для случая, когда задержка прервана
токеном отмены).
- Использовать
метод, который завершается, когда токен отменен, но не бросает исключение.
Однако, если мы хотим graceful shutdown,
то, возможно, мы хотим дать приложению время завершить работу, и прерывание
задержки — это часть graceful shutdown.
Давайте рассмотрим пример вспомогательного
метода, который не бросает исключение при отмене токена:
csharp
public static async Task DelayWithoutException(TimeSpan delay, CancellationToken cancellationToken)
{
try
{
await Task.Delay(delay,
cancellationToken);
}
catch (OperationCanceledException)
{
// Игнорируем отмену
}
}
Тогда мы можем использовать его так:
csharp
await DelayWithoutException(TimeSpan.FromMilliseconds(1000),
stoppingToken);
Но, возможно, мы не хотим игнорировать
все OperationCanceledException, а
только те, которые связаны с нашим токеном. В данном случае, так как мы
передаем тот же токен, то это именно то, что нужно.
Однако, если мы хотим, чтобы задержка
прерывалась при отмене токена, но без исключения, то этот подход подходит.
Другой вариант — использовать Task.Delay без токена и затем
использовать CancellationToken для
отмены через ThrowIfCancellationRequested или
проверять токен вручную, но это менее эффективно.
Но учтем, что Task.Delay с токеном отмены более эффективен,
потому что он не создает таймер, если токен уже отменен.
Мы можем комбинировать: сначала
проверить, не отменен ли токен, и если отменен, то просто вернуть завершенную
задачу, иначе использовать Task.Delay с
токеном.
Однако, даже если токен отменен, Task.Delay сразу бросит исключение. Поэтому,
чтобы избежать исключения, мы можем сделать так:
csharp
public static async Task DelayWithoutException(TimeSpan delay, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return;
}
try
{
await Task.Delay(delay,
cancellationToken);
}
catch (OperationCanceledException)
{
// Игнорируем отмену
}
}
Но это не совсем правильно, потому что
токен может быть отменен во время задержки, и тогда мы все равно попадем в
catch. Однако, если мы хотим игнорировать отмену в любом случае, то этого
достаточно.
Однако, если мы хотим, чтобы задача
завершалась сразу при отмене токена (без исключения), то это работает.
Но в некоторых сценариях мы можем
захотеть, чтобы задача завершалась только после фактической задержки, если
токен не отменен. Тогда мы можем использовать Task.Delay без токена и проверять токен
вручную, но это не эффективно.
Вместо этого, мы можем
использовать Task.WhenAny для
ожидания либо задержки, либо отмены токена, но без исключения.
Вот еще один вариант:
csharp
public static async Task DelayWithoutException(TimeSpan delay, CancellationToken cancellationToken)
{
var delayTask = Task.Delay(delay);
var cancellationTask = Task.Delay(Timeout.Infinite,
cancellationToken);
var completedTask = await
Task.WhenAny(delayTask,
cancellationTask);
if (completedTask == cancellationTask)
{
// Токен отменен, выходим без исключения
return;
}
// Иначе задержка завершена
await delayTask;
}
Но этот вариант менее эффективен, потому
что создает две задачи, одна из которых — бесконечная задержка с токеном.
Однако, если мы хотим избежать
исключения, это один из способов.
Но, возможно, проще всего использовать
первый вариант с try-catch, потому что он простой и эффективный.
Давайте остановимся на первом варианте с
try-catch, но добавим проверку на уже отмененный токен, чтобы избежать
ненужного вызова Task.Delay:
csharp
public static async Task DelayWithoutException(TimeSpan delay, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return;
}
try
{
await Task.Delay(delay,
cancellationToken);
}
catch (OperationCanceledException)
{
// Игнорируем отмену
}
}
Тогда, если токен уже отменен, мы сразу
возвращаемся, иначе пытаемся выполнить задержку, и если токен отменяется во
время задержки, ловим исключение.
Это должно решить проблему.
Однако, если у нас много таких мест, то
мы можем использовать эту вспомогательную функцию.
Но также можно рассмотреть возможность
использования Task.Delay без
токена и проверять токен вручную, но это не очень эффективно, потому что мы не
отменяем задержку сразу при отмене токена.
Поэтому я предлагаю использовать
вышеуказанную функцию.
Пример использования в приложении:
csharp
await Helper.DelayWithoutException(TimeSpan.FromMilliseconds(1000),
stoppingToken);
Или, если мы находимся в классе, который
имеет доступ к токену, то можно сделать метод, который использует токен по
умолчанию.
Но если мы хотим, чтобы приложение
завершалось нормально, то мы должны обрабатывать отмену токена везде, где это
возможно, и не бросать исключения, которые могут привести к аварийному
завершению.
В контексте .NET Core приложения с
HostedService, мы можем использовать такой подход в цикле, пока приложение не
будет остановлено.
Однако, если мы используем Task.Delay с токеном в других местах, то тоже
следует обрабатывать исключение отмены.
В целом, идея graceful shutdown в том,
чтобы не бросать исключения при отмене операций, которые были отменены по
причине остановки приложения.
Поэтому мы можем использовать такой
вспомогательный метод для задержек.
Но также стоит отметить, что в некоторых
случаях мы можем захотеть, чтобы исключение отмены пробрасывалось, если это не
остановка приложения, а какая-то другая отмена. Поэтому, возможно, мы хотим
игнорировать только определенные типы отмен (например, только когда токен
остановки приложения).
Тогда мы можем проверить, какой именно
токен отменен, но в нашем случае мы передаем stoppingToken, который отменяется при остановке
приложения.
Если мы хотим игнорировать только отмену
по stoppingToken, то мы можем передавать его
и проверять в catch, но в нашем методе мы ловим все OperationCanceledException.
Чтобы быть более точными, мы можем
проверить, что пойманное исключение связано с нашим токеном:
csharp
public static async Task DelayWithoutException(TimeSpan delay, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return;
}
try
{
await Task.Delay(delay,
cancellationToken);
}
catch (OperationCanceledException ex) when (ex.CancellationToken ==
cancellationToken)
{
// Игнорируем отмену только для нашего токена
}
}
Но это не обязательно, если мы уверены,
что токен именно тот, который мы передали.
Однако, если есть вложенные токены, то
может быть и другое исключение. Но в нашем случае, вероятно, это не нужно.
Поэтому, для простоты, можно
использовать первый вариант без when.
Или даже без проверки токена в catch,
потому что мы передаем тот же токен, и если исключение возникло, то оно именно
от этого токена.
Но если есть вероятность, что Task.Delay может выбросить OperationCanceledException по другой причине (хотя это маловероятно),
то мы можем поймать только те, которые связаны с нашим токеном.
Однако, в документации Task.Delay сказано, что он бросает OperationCanceledException при отмене переданного токена.
Поэтому, если мы передаем наш токен, то
исключение будет связано с ним.
Так что можно обойтись без when.
В итоге, я предлагаю использовать
вспомогательный метод с try-catch и, возможно, с проверкой на уже отмененный
токен в начале.
Давайте напишем такой метод и будем
использовать его везде, где мы используем Task.Delay с
токеном отмены, который может быть отменен при остановке приложения.
Для graceful shutdown при
использовании Task.Delay с CancellationToken можно использовать несколько
подходов:
1. Оборачивание
в try-catch (наиболее простой)
csharp
private async Task DelaySafeAsync(TimeSpan delay, CancellationToken stoppingToken)
{
try
{
await Task.Delay(delay,
stoppingToken);
}
catch (OperationCanceledException)
{
// Игнорируем отмену - это нормальное завершение
// Можно добавить логирование, если нужно
}
}
// Использование
await DelaySafeAsync(TimeSpan.FromSeconds(1),
stoppingToken);
2. Использование
Task.WhenAny для graceful отмены
csharp
private async Task DelayWithGracefulCancelAsync(TimeSpan delay, CancellationToken stoppingToken)
{
var delayTask = Task.Delay(delay);
var cancellationTask = Task.Delay(Timeout.Infinite, stoppingToken);
var completedTask = await
Task.WhenAny(delayTask,
cancellationTask);
if (completedTask ==
cancellationTask)
{
// Токен был отменен - нормальный выход
return;
}
// Задержка завершена нормально
await delayTask;
}
3. Проверка токена перед задержкой
csharp
private async Task DelayWithCheckAsync(TimeSpan delay, CancellationToken stoppingToken)
{
// Создаем linked token source для возможности мягкой отмены
using var cts = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);
try
{
// Проверяем, не запрошена ли отмена
if (stoppingToken.IsCancellationRequested)
{
return;
}
await Task.Delay(delay,
cts.Token);
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
// Отмена через stoppingToken - нормальное завершение
}
catch (OperationCanceledException)
{
// Другая отмена - пробрасываем дальше
throw;
}
}
4. Универсальное
решение для IHostedService
Для фоновых служб в .NET Core/5/6/7/8:
csharp
public class MyBackgroundService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
// Выполняем работу
try
{
await Task.Delay(TimeSpan.FromSeconds(1),
stoppingToken);
}
catch (TaskCanceledException)
{
// Нормальное завершение работы
break;
}
}
// Опционально: выполнение cleanup операций
await StopAsyncInternal();
}
private async Task StopAsyncInternal()
{
// Cleanup логика
await Task.CompletedTask;
}
}
5. Расширяющий метод для удобства
csharp
public static class TaskExtensions
{
public static async Task DelaySafeAsync(
this TimeSpan delay,
CancellationToken cancellationToken,
bool throwOnCancel = false)
{
try
{
await Task.Delay(delay,
cancellationToken);
}
catch (OperationCanceledException) when (!throwOnCancel)
{
// Игнорируем отмену, если не нужно бросать исключение
}
}
public static async Task DelayWithGracefulShutdownAsync(
this TimeSpan delay,
CancellationToken cancellationToken)
{
using var
delayCts = new CancellationTokenSource();
using var
linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
cancellationToken,
delayCts.Token);
try
{
await Task.Delay(delay,
linkedCts.Token);
}
catch (OperationCanceledException ex)
{
if (cancellationToken.IsCancellationRequested)
{
//
Это graceful shutdown - логируем и выходим
Log.ShutdownInformation($"Graceful
shutdown during delay: {ex.Message}");
return;
}
throw;
}
}
}
// Использование
await TimeSpan.FromSeconds(1).DelaySafeAsync(stoppingToken);
6. Полный пример для Worker Service
csharp
public class Worker : BackgroundService
{
private readonly ILogger<Worker>
_logger;
public Worker(ILogger<Worker> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Worker
started");
try
{
while (!stoppingToken.IsCancellationRequested)
{
// Основная работа
await DoWorkAsync(stoppingToken);
// Безопасная задержка
await SafeDelayAsync(TimeSpan.FromSeconds(5),
stoppingToken);
}
}
catch (OperationCanceledException)
{
_logger.LogInformation("Worker
stopping gracefully");
}
finally
{
// Cleanup ресурсов
await CleanupAsync();
}
_logger.LogInformation("Worker
stopped");
}
private async Task SafeDelayAsync(TimeSpan delay, CancellationToken stoppingToken)
{
try
{
await Task.Delay(delay, stoppingToken);
}
catch (OperationCanceledException)
{
// Это ожидаемое поведение
при остановке
throw;
}
}
private async Task DoWorkAsync(CancellationToken stoppingToken)
{
// Ваша бизнес-логика
_logger.LogInformation("Working
at: {time}", DateTimeOffset.Now);
// Проверяем отмену в процессе работы
stoppingToken.ThrowIfCancellationRequested();
}
private async Task CleanupAsync()
{
// Очистка ресурсов
await Task.Delay(100); // Пример cleanup
}
}
Рекомендации:
- Используйте try-catch для OperationCanceledException вокруг Task.Delay
- Логируйте
graceful shutdown, чтобы отличать от ошибок
- Добавьте cleanup
логику в finally блок или метод StopAsync
- Проверяйте IsCancellationRequested перед длительными операциями
- Используйте
CancellationTokenSource.CreateLinkedTokenSource для сложных
сценариев
Самый простой и распространенный подход
— первый вариант с try-catch, который надежно обрабатывает отмену без выброса
исключений в лог.
Код завершения получается больше чем
полезный код. Тогда уж лучше делать это с шаблонами T Полезного кода и
завершающего кода. TMayCode и варианты завершения - TGraceFullComlition -
TOptimalCompletion - TUniversalCompletion - TExtendedCompletion -
TSimpleCompletion
Мы хотим абстрагировать код завершения
(graceful shutdown) от полезной работы.
Идея: использовать шаблоны, где TMayCode - полезный код, а остальные - варианты
завершения.
Однако в C# нет таких шаблонов в прямом смысле, но мы можем использовать
делегаты и методы.
Давайте создадим классы или методы,
которые инкапсулируют логику graceful shutdown.
Предположим, у нас есть некоторая
операция, которая выполняется в цикле до отмены.
Мы хотим, чтобы задержка между итерациями обрабатывалась корректно при отмене.
Вот один из вариантов: создать
класс-обертку для фоновой задачи, который будет управлять ее выполнением.
Однако, если мы хотим более
универсальное решение, то можно создать набор методов расширения или хелперов.
Рассмотрим несколько вариантов:
- TGracefulCompletion - просто игнорирует исключение отмены в
задержке.
- TOptimalCompletion - проверяет токен перед задержкой и не
начинает задержку, если уже отменено.
- TUniversalCompletion - комбинированный подход с логированием.
- TExtendedCompletion - с дополнительными действиями при отмене.
- TSimpleCompletion - простой try-catch.
Но лучше создать один метод, который
будет принимать делегат для полезной работы и делегат для задержки, а также
токен.
Однако, если мы хотим именно разделить
полезный код и код завершения, то можно использовать паттерн, например,
шаблонный метод.
Давайте создадим базовый класс для
фоновой службы, который будет иметь абстрактный метод для полезной работы и
реализацию graceful shutdown.
Пример:
csharp
public abstract class GracefulBackgroundService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await DoWorkAsync(stoppingToken);
// Используем безопасную задержку
await SafeDelayAsync(TimeSpan.FromSeconds(1),
stoppingToken);
}
// При необходимости cleanup
await CleanupAsync();
}
protected abstract Task DoWorkAsync(CancellationToken stoppingToken);
protected virtual Task CleanupAsync()
{
return Task.CompletedTask;
}
protected virtual async Task SafeDelayAsync(TimeSpan delay, CancellationToken stoppingToken)
{
try
{
await Task.Delay(delay,
stoppingToken);
}
catch (OperationCanceledException)
{
// Игнорируем, так как это
ожидаемо при остановке
}
}
}
Тогда в конкретной службе мы наследуемся
от GracefulBackgroundService и
реализуем только DoWorkAsync.
Но если нужно больше вариативности в
обработке задержки, то можно сделать SafeDelayAsync переопределяемым.
Другой вариант: использовать композицию
и делегаты.
Создадим класс, который будет запускать
переданную полезную работу с заданной периодичностью и обрабатывать отмену.
csharp
public class PeriodicTaskRunner
{
private readonly Func<CancellationToken,
Task> _work;
private readonly TimeSpan _interval;
private readonly ILogger<PeriodicTaskRunner>
_logger;
public PeriodicTaskRunner(Func<CancellationToken,
Task> work, TimeSpan interval, ILogger<PeriodicTaskRunner> logger = null)
{
_work = work;
_interval = interval;
_logger = logger;
}
public async Task RunAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await _work(stoppingToken);
try
{
await Task.Delay(_interval,
stoppingToken);
}
catch (OperationCanceledException)
{
_logger?.LogInformation("Delay
cancelled, stopping gracefully.");
break;
}
}
}
}
Тогда в BackgroundService мы можем
использовать этот runner.
Но если мы хотим именно шаблоны
(TMayCode, TGracefulCompletion и т.д.), то можно попробовать использовать
generics, но это не совсем подходит.
Давайте рассмотрим возможность создания
набора методов расширения для Task или CancellationToken, которые бы инкапсулировали логику
задержки с graceful shutdown.
Например:
csharp
public static class TaskExtensions
{
public static async Task DelayWithGracefulShutdown(this TimeSpan delay, CancellationToken stoppingToken, ILogger
logger = null)
{
try
{
await Task.Delay(delay,
stoppingToken);
}
catch (OperationCanceledException)
{
logger?.LogInformation("Graceful
shutdown during delay.");
throw; //
или не бросать, в зависимости от желаемого поведения
}
}
}
Однако, если мы не хотим бросать
исключение, то можно просто выйти из метода.
Но в контексте BackgroundService, когда
мы ловим исключение в методе ExecuteAsync, мы
можем просто завершить цикл.
Поэтому, возможно, лучше не бросать
исключение, а просто выйти из метода задержки, а в цикле проверить stoppingToken.IsCancellationRequested.
Перепишем SafeDelayAsync так, чтобы оно
не бросало исключение, а возвращало управление, когда токен отменен.
csharp
protected virtual async Task SafeDelayAsync(TimeSpan delay, CancellationToken stoppingToken)
{
try
{
await Task.Delay(delay,
stoppingToken);
}
catch (OperationCanceledException)
{
// Просто выходим, не бросая исключение дальше.
}
}
Тогда цикл в ExecuteAsync должен проверять stoppingToken.IsCancellationRequested после задержки, но если задержка
была отменена, то мы выходим из задержки и снова проверяем условие цикла.
Однако, если мы хотим, чтобы задержка
могла быть прервана, и мы сразу выходили из цикла, то такой подход подходит.
Итог: выберите подход, который лучше
всего подходит для вашего приложения.
Если нужно несколько вариантов
поведения, можно создать несколько классов-стратегий для обработки задержки.
Но, на мой взгляд, для большинства
случаев достаточно простого игнорирования OperationCanceledException в
задержке.
Поэтому, если вы хотите минимальный код,
то используйте базовый класс GracefulBackgroundService и
переопределяйте DoWorkAsync.
Если вам нужна более гибкая настройка,
то используйте композицию с PeriodicTaskRunner или
подобным.
Если же вы хотите использовать именно
шаблоны, то можно создать generic-классы, но это может быть излишним.
Вот пример с generic-классом, но в C#
нет TMayCode, поэтому мы используем делегаты:
csharp
public class TaskRunner<TMayCode> where TMayCode : class
{
// Это пример, но он не очень практичный.
}
Более практично было бы использовать
делегаты или интерфейсы.
Интерфейс для полезной работы:
csharp
public interface IWork
{
Task DoWorkAsync(CancellationToken
stoppingToken);
}
И затем класс, который запускает работу
с задержкой:
csharp
public class WorkRunner
{
private readonly IWork _work;
private readonly TimeSpan _interval;
private readonly IDelayStrategy _delayStrategy;
public WorkRunner(IWork work, TimeSpan interval, IDelayStrategy delayStrategy = null)
{
_work = work;
_interval = interval;
_delayStrategy = delayStrategy ?? new DefaultDelayStrategy();
}
public async Task RunAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await _work.DoWorkAsync(stoppingToken);
await _delayStrategy.DelayAsync(_interval,
stoppingToken);
}
}
}
public interface IDelayStrategy
{
Task DelayAsync(TimeSpan delay, CancellationToken stoppingToken);
}
public class DefaultDelayStrategy : IDelayStrategy
{
public async Task DelayAsync(TimeSpan delay, CancellationToken stoppingToken)
{
try
{
await Task.Delay(delay,
stoppingToken);
}
catch (OperationCanceledException)
{
// Игнорируем
}
}
}
public class ThrowOnCancelDelayStrategy : IDelayStrategy
{
public async Task DelayAsync(TimeSpan delay, CancellationToken stoppingToken)
{
await Task.Delay(delay,
stoppingToken);
}
}
Тогда вы можете выбирать стратегию
задержки.
Но это уже более сложная архитектура.
В зависимости от требований, выберите
подходящий вариант.
Если нужно простое решение, то
используйте первый вариант с базовым классом и переопределением метода
задержки.
Если нужно более гибкое решение, то
используйте композицию со стратегией.
Если вы хотите минимально изменять
существующий код, то используйте метод расширения для безопасной задержки.
Надеюсь, это поможет!
Вы абсолютно правы! Давайте реализуем
паттерны завершения с использованием обобщенных подходов. Вот элегантное
решение:
1. Базовые
интерфейсы и абстракции
csharp
public interface ICompletionStrategy
{
Task DelayAsync(TimeSpan delay, CancellationToken stoppingToken);
Task OnShutdownAsync(CancellationToken stoppingToken);
}
public abstract class MayCode<TCompletion> where TCompletion : ICompletionStrategy, new()
{
protected readonly TCompletion Completion;
protected MayCode()
{
Completion = new TCompletion();
}
public async Task ExecuteAsync(CancellationToken stoppingToken)
{
await BeforeExecutionAsync(stoppingToken);
try
{
while (!stoppingToken.IsCancellationRequested)
{
await DoWorkAsync(stoppingToken);
// Используем стратегию для задержки
await Completion.DelayAsync(TimeSpan.FromSeconds(1),
stoppingToken);
}
}
finally
{
await Completion.OnShutdownAsync(stoppingToken);
await AfterExecutionAsync(stoppingToken);
}
}
protected abstract Task DoWorkAsync(CancellationToken stoppingToken);
protected virtual Task BeforeExecutionAsync(CancellationToken stoppingToken) =>
Task.CompletedTask;
protected virtual Task AfterExecutionAsync(CancellationToken stoppingToken) =>
Task.CompletedTask;
}
2. Реализация стратегий завершения
TGraceFullCompletion
csharp
public class TGraceFullCompletion : ICompletionStrategy
{
public async Task DelayAsync(TimeSpan delay, CancellationToken stoppingToken)
{
try
{
await Task.Delay(delay,
stoppingToken);
}
catch (OperationCanceledException)
{
// Тихий graceful shutdown
Log.Verbose("Graceful
shutdown requested");
}
}
public Task OnShutdownAsync(CancellationToken stoppingToken)
{
Log.Information("Application
shutting down gracefully...");
return Task.CompletedTask;
}
}
// Использование
public class MyService : MayCode<TGraceFullCompletion>
{
protected override async Task DoWorkAsync(CancellationToken stoppingToken)
{
Console.WriteLine($"Working
at {DateTime.Now}");
await Task.Delay(100);
}
}
TOptimalCompletion
csharp
public class TOptimalCompletion : ICompletionStrategy
{
private readonly ILogger _logger;
public TOptimalCompletion(ILogger logger = null)
{
_logger = logger;
}
public async Task DelayAsync(TimeSpan delay, CancellationToken stoppingToken)
{
// Проверяем перед началом задержки
if (stoppingToken.IsCancellationRequested)
{
_logger?.LogDebug("Skipping
delay due to cancellation");
return;
}
using var
delayCts = new CancellationTokenSource();
using var
linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
stoppingToken,
delayCts.Token);
try
{
await Task.Delay(delay,
linkedCts.Token);
}
catch (OperationCanceledException ex) when (stoppingToken.IsCancellationRequested)
{
_logger?.LogInformation("Optimal
shutdown during delay");
}
catch (OperationCanceledException)
{
throw;
}
}
public async Task OnShutdownAsync(CancellationToken stoppingToken)
{
_logger?.LogInformation("Performing
optimal cleanup...");
await Task.Delay(50); // Имитация cleanup операций
_logger?.LogInformation("Cleanup
completed");
}
}
TUniversalCompletion
csharp
public class TUniversalCompletion : ICompletionStrategy
{
private readonly Action<Exception>
_onCancel;
private readonly Func<CancellationToken,
Task> _onShutdown;
public TUniversalCompletion(
Action<Exception> onCancel = null,
Func<CancellationToken, Task>
onShutdown = null)
{
_onCancel = onCancel;
_onShutdown = onShutdown;
}
public async Task DelayAsync(TimeSpan delay, CancellationToken stoppingToken)
{
var
delayTask = Task.Delay(delay);
var
cancellationTask = Task.Delay(Timeout.Infinite, stoppingToken);
var completedTask = await
Task.WhenAny(delayTask,
cancellationTask);
if (completedTask
== cancellationTask)
{
_onCancel?.Invoke(new OperationCanceledException(stoppingToken));
return;
}
await delayTask;
}
public async Task OnShutdownAsync(CancellationToken stoppingToken)
{
if (_onShutdown
!= null)
{
await _onShutdown(stoppingToken);
}
}
}
TExtendedCompletion
csharp
public class TExtendedCompletion : ICompletionStrategy
{
private readonly IServiceProvider _serviceProvider;
private readonly TimeSpan _shutdownTimeout;
public TExtendedCompletion(
IServiceProvider serviceProvider = null,
TimeSpan? shutdownTimeout = null)
{
_serviceProvider = serviceProvider;
_shutdownTimeout =
shutdownTimeout ?? TimeSpan.FromSeconds(30);
}
public async Task DelayAsync(TimeSpan delay, CancellationToken stoppingToken)
{
using var
perDelayCts = new CancellationTokenSource();
using var
linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
stoppingToken,
perDelayCts.Token);
try
{
// Мониторинг
дополнительных условий для отмены
MonitorExternalConditions(perDelayCts);
await Task.Delay(delay,
linkedCts.Token);
}
catch (OperationCanceledException ex)
{
if (stoppingToken.IsCancellationRequested)
{
await HandleGracefulShutdown(ex);
}
else if (perDelayCts.IsCancellationRequested)
{
throw new ApplicationException("External
condition triggered shutdown", ex);
}
throw;
}
}
private void MonitorExternalConditions(CancellationTokenSource cts)
{
// Здесь можно мониторить внешние условия для shutdown
Task.Run(async () =>
{
// Пример: проверка
наличия файла для экстренного завершения
if (File.Exists("shutdown.trigger"))
{
await Task.Delay(100);
cts.Cancel();
}
});
}
private async Task HandleGracefulShutdown(Exception ex)
{
Log.Warning($"Extended
shutdown triggered: {ex.Message}");
// Выполнение расширенного cleanup через DI
if (_serviceProvider
!= null)
{
using var
scope = _serviceProvider.CreateScope();
var cleanupServices
=
scope.ServiceProvider.GetServices<IExtendedCleanup>();
foreach (var service in cleanupServices)
{
await service.CleanupAsync(_shutdownTimeout);
}
}
}
public async Task OnShutdownAsync(CancellationToken stoppingToken)
{
using var
timeoutCts = new CancellationTokenSource(_shutdownTimeout);
using var
linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
stoppingToken,
timeoutCts.Token);
try
{
await PerformExtendedCleanupAsync(linkedCts.Token);
}
catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested)
{
Log.Error("Shutdown
timeout exceeded!");
}
}
private async Task PerformExtendedCleanupAsync(CancellationToken cancellationToken)
{
// Расширенные операции cleanup
Log.Information("Starting
extended cleanup...");
var cleanupTasks = new List<Task>
{
FlushBuffersAsync(cancellationToken),
CloseConnectionsAsync(cancellationToken),
SaveStateAsync(cancellationToken)
};
await Task.WhenAll(cleanupTasks);
}
}
TSimpleCompletion
csharp
public class TSimpleCompletion : ICompletionStrategy
{
public async Task DelayAsync(TimeSpan delay, CancellationToken stoppingToken)
{
try
{
await Task.Delay(delay,
stoppingToken);
}
catch (TaskCanceledException)
{
// Просто игнорируем
}
}
public Task OnShutdownAsync(CancellationToken stoppingToken)
{
// Ничего не делаем
return Task.CompletedTask;
}
}
3. Фабрика
для создания стратегий
csharp
public static class CompletionFactory
{
public static ICompletionStrategy Create<T>() where T : ICompletionStrategy, new()
=> new T();
public static ICompletionStrategy Create(
CompletionType type,
IServiceProvider serviceProvider = null,
ILogger
logger = null)
{
return
type switch
{
CompletionType.Graceful
=> new TGraceFullCompletion(),
CompletionType.Optimal
=> new TOptimalCompletion(logger),
CompletionType.Universal
=> new TUniversalCompletion(),
CompletionType.Extended
=> new TExtendedCompletion(serviceProvider),
CompletionType.Simple
=> new TSimpleCompletion(),
_ => throw new ArgumentOutOfRangeException(nameof(type),
type, null)
};
}
}
public enum CompletionType
{
Graceful,
Optimal,
Universal,
Extended,
Simple
}
4. Использование с Dependency Injection
csharp
// Регистрация в DI
services.AddSingleton<ICompletionStrategy>(provider
=>
CompletionFactory.Create(CompletionType.Graceful));
// Или с настройками
services.AddSingleton<ICompletionStrategy>(provider
=>
new TExtendedCompletion(
provider,
TimeSpan.FromSeconds(45)));
// Использование в сервисе
public class ProcessingService
{
private readonly ICompletionStrategy _completion;
public ProcessingService(ICompletionStrategy completion)
{
_completion = completion;
}
public async Task RunAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
// Полезная работа
await ProcessDataAsync();
// Задержка через стратегию
await _completion.DelayAsync(TimeSpan.FromSeconds(1),
stoppingToken);
}
await _completion.OnShutdownAsync(stoppingToken);
}
}
5. Декоратор для существующего кода
csharp
public static class TaskCompletionDecorator
{
public static Task<T> ExecuteWithCompletion<T>(
this Func<CancellationToken, Task<T>> operation,
ICompletionStrategy completion,
CancellationToken cancellationToken)
{
return completion.ExecuteAsync(async ct =>
{
// Задержки и другие
асинхронные точки будут обработаны стратегией
return await operation(ct);
},
cancellationToken);
}
}
// Расширение для стратегий
public static class CompletionExtensions
{
public static async Task<T> ExecuteAsync<T>(
this ICompletionStrategy completion,
Func<CancellationToken, Task<T>> operation,
CancellationToken cancellationToken)
{
try
{
return await operation(cancellationToken);
}
finally
{
await completion.OnShutdownAsync(cancellationToken);
}
}
}
6. Пример использования
csharp
// 1. Simple использование
var processor = new MayCode<TSimpleCompletion>();
await processor.ExecuteAsync(cancellationToken);
// 2. DI использование
var completion = new TExtendedCompletion(serviceProvider);
var worker = new BackgroundWorker(completion);
// 3. Функциональный подход
var result = await ((CancellationToken ct) =>
{
// Полезный код
return ProcessData(ct);
})
.ExecuteWithCompletion(
new TOptimalCompletion(logger),
cancellationToken);
Преимущества
подхода:
- Разделение
ответственности - полезный код отделен от логики завершения
- Гибкость - можно
легко менять стратегии
- Тестируемость - стратегии
легко тестируются изолированно
- Переиспользуемость - одна
стратегия для многих сервисов
- Конфигурируемость - настройка
через параметры и DI
Выбирайте стратегию в зависимости от
требований приложения!
Комментариев нет:
Отправить комментарий