worker-services について
.NET で長時間稼働するバックグラウンドサービスや定期実行ワーカーを実装するためのスキルです。ホステッドサービスのライフサイクル、キャンセル、スコープ付き依存関係、キュー処理、ヘルスチェック、正常終了を扱います。
主な機能
- 01BackgroundService によるワーカーライフサイクル管理
- 02スコープ付き依存関係のスコープ管理
- 03キャンセル伝播と正常終了
- 04PeriodicTimer によるタスク実行
- 05ヘルスチェックと構造化ログの追加
活用シーン
- 01定期実行バックグラウンドワーカーの構築
- 02スコープ付きサービスを用いたキュー処理
- 03ワーカーのヘルス監視と終了手順の整備
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ managedcode/ dotnet-skills --skill worker-servicesインストール後、Claude Code で/worker-servicesを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
manifest.jsonreferences/anti-patterns.mdpatterns.md詳細
.NET ワーカーサービス
対象となる状況
- 長時間実行されるバックグラウンドサービスやスケジュールされたワーカーを構築するとき
- アプリにホステッドサービスを追加するとき、またはワーカープロセスとして切り出すとき
- グレースフルシャットダウン、キャンセル、キュー処理、ヘルスチェックの動作を確認するとき
ドキュメント
- .NET のワーカーサービス
- ASP.NET Core のホステッドサービスを使用したバックグラウンドタスク
- BackgroundService を使用した Windows サービスの作成
- .NET のアプリケーションヘルスチェック
- ASP.NET Core のヘルスチェック
リファレンス
- patterns.md - BackgroundService のパターン、グレースフルシャットダウン、ヘルスチェックの実装
- anti-patterns.md - ワーカーサービスでよくある誤りとその回避方法
ワークフロー
-
基本クラスとして BackgroundService を使用します:
- 標準的な
StartAsync/StopAsyncの処理が用意されています ExecuteAsyncの実装だけに集中できます- キャンセレーショントークンは適切に管理されます
- 標準的な
-
スコープ付き依存関係を正しく処理します:
- スコープ付きサービスのためにサービススコープを作成します
- ホステッドサービスでは、既定でスコープは作成されません
-
グレースフルシャットダウンを実装します:
- キャンセレーショントークンを全体に伝播させます
- トークンが通知されたら、速やかに処理を完了します
- タイムアウト時の強制終了を避けます
-
実行ループをシンプルに保ちます:
- ビジネスロジックをテスト可能なサービスに移します
- サービスのクラッシュを防ぐため、例外を処理します
- スケジュールされた処理には
PeriodicTimerを使用します
-
可観測性を追加します:
- レディネス/ライブネスの確認にヘルスチェックを使用します
- メトリクスと構造化ログを公開します
- 複数インスタンスで実行する場合は、分散ロックを検討します
現在の上流情報
.NET runtimev10.0.11はサービス更新版です。ワーカーでは、アップグレード後、既定でアーキテクチャを変更するのではなく、cgroup-v2 のメモリ制限検出、キャンセル、グレースフルシャットダウン、長時間実行されるスレッドローカルおよび GC の経路、該当する場合は NativeAOT/WASM の起動、Windows のディレクトリ列挙を再確認してください。- 新しいワーカーのエントリポイントを追加する際は、最新の Worker Services およびホステッドサービスの Learn ページを参照し、現在のホスティング API とヘルスチェック API を正確に確認してください。
基本的な BackgroundService のパターン
シンプルなワーカー
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 starting");
while (!stoppingToken.IsCancellationRequested)
{
try
{
_logger.LogInformation("Worker running at: {Time}", DateTimeOffset.Now);
await DoWorkAsync(stoppingToken);
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
// Graceful shutdown, not an error
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in worker iteration");
// Continue or break based on error severity
}
}
_logger.LogInformation("Worker stopping");
}
private async Task DoWorkAsync(CancellationToken cancellationToken)
{
// Business logic here
}
}
PeriodicTimer を使用するパターン(推奨)
public class TimedWorker : BackgroundService
{
private readonly ILogger<TimedWorker> _logger;
private readonly IServiceScopeFactory _scopeFactory;
private readonly TimeSpan _period = TimeSpan.FromMinutes(1);
public TimedWorker(ILogger<TimedWorker> logger, IServiceScopeFactory scopeFactory)
{
_logger = logger;
_scopeFactory = scopeFactory;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var timer = new PeriodicTimer(_period);
while (await timer.WaitForNextTickAsync(stoppingToken))
{
try
{
await using var scope = _scopeFactory.CreateAsyncScope();
var processor = scope.ServiceProvider.GetRequiredService<IDataProcessor>();
await processor.ProcessAsync(stoppingToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing scheduled task");
}
}
}
}
スコープ付き依存関係の処理
スコープファクトリーを使用する正しいパターン
public class ScopedWorker : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<ScopedWorker> _logger;
public ScopedWorker(IServiceScopeFactory scopeFactory, ILogger<ScopedWorker> logger)
{
_scopeFactory = scopeFactory;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
// Create scope for each unit of work
await using var scope = _scopeFactory.CreateAsyncScope();
var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var service = scope.ServiceProvider.GetRequiredService<IScopedService>();
await service.ProcessAsync(dbContext, stoppingToken);
await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
}
}
}
キュー処理のパターン
メッセージキューワーカー
public class QueueWorker : BackgroundService
{
private readonly ILogger<QueueWorker> _logger;
private readonly IServiceScopeFactory _scopeFactory;
private readonly IBackgroundTaskQueue _taskQueue;
public QueueWorker(
ILogger<QueueWorker> logger,
IServiceScopeFactory scopeFactory,
IBackgroundTaskQueue taskQueue)
{
_logger = logger;
_scopeFactory = scopeFactory;
_taskQueue = taskQueue;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Queue Worker started");
while (!stoppingToken.IsCancellationRequested)
{
var workItem = await _taskQueue.DequeueAsync(stoppingToken);
try
{
await using var scope = _scopeFactory.CreateAsyncScope();
await workItem(scope.ServiceProvider, stoppingToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing queued work item");
// Handle poison message - retry, dead-letter, etc.
}
}
}
}
// Task queue interface
public interface IBackgroundTaskQueue
{
ValueTask QueueBackgroundWorkItemAsync(
Func<IServiceProvider, CancellationToken, ValueTask> workItem);
ValueTask<Func<IServiceProvider, CancellationToken, ValueTask>> DequeueAsync(
CancellationToken cancellationToken);
}
ワーカーのヘルスチェック
ヘルスチェックエンドポイントの追加
// Program.cs
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<Worker>();
// Add health checks
builder.Services.AddHealthChecks()
.AddCheck<WorkerHealthCheck>("worker_health")
.AddResourceUtilizationHealthCheck();
// Add HTTP endpoint for health checks
builder.Services.AddHealthChecksUI();
// Or use simple TCP listener for Kubernetes
builder.Services.AddSingleton<TcpHealthProbeService>();
builder.Services.AddHostedService(sp => sp.GetRequiredService<TcpHealthProbeService>());
var host = builder.Build();
host.Run();
カスタムヘルスチェック
public class WorkerHealthCheck : IHealthCheck
{
private readonly WorkerState _workerState;
public WorkerHealthCheck(WorkerState workerState)
{
_workerState = workerState;
}
public Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
if (_workerState.LastSuccessfulRun > DateTime.UtcNow.AddMinutes(-5))
{
return Task.FromResult(HealthCheckResult.Healthy(
$"Last successful run: {_workerState.LastSuccessfulRun}"));
}
return Task.FromResult(HealthCheckResult.Unhealthy(
$"No successful run since: {_workerState.LastSuccessfulRun}"));
}
}
// Shared state
public class WorkerState
{
public DateTime LastSuccessfulRun { get; set; } = DateTime.UtcNow;
public bool IsProcessing { get; set; }
}
グレースフルシャットダウンのパターン
適切なシャットダウン処理
public class GracefulWorker : BackgroundService
{
private readonly ILogger<GracefulWorker> _logger;
private int _currentWorkItemId;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Worker starting");
while (!stoppingToken.IsCancellationRequested)
{
_currentWorkItemId = GetNextWorkItemId();
try
{
// Pass cancellation token to all async operations
await ProcessWorkItemAsync(_currentWorkItemId, stoppingToken);
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
_logger.LogInformation(
"Shutdown requested, stopping after work item {Id}", _currentWorkItemId);
break;
}
}
}
public override async Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Worker stopping gracefully");
await base.StopAsync(cancellationToken);
_logger.LogInformation("Worker stopped");
}
}
Windows サービスのデプロイ
Windows サービスとしての構成
// Program.cs
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddWindowsService(options =>
{
options.ServiceName = "My Worker Service";
});
builder.Services.AddHostedService<Worker>();
var host = builder.Build();
host.Run();
プロジェクトファイルの設定
<Project Sdk="Microsoft.NET.Sdk.Worker">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<PublishSingleFile>true</PublishSingleFile>
<SelfContained>true</SelfContained>
</PropertyGroup>
</Project>
ベストプラクティス
- 基本クラスとして BackgroundService を使用する -
StartAsync/StopAsyncの定型処理とキャンセル管理を担います - スコープが必要な依存関係にはスコープを作成する -
IServiceScopeFactoryを使用して、DbContext などのスコープ付きサービスを解決します - キャンセルトークンをすべての処理に伝播する - 応答性の高いシャットダウンを実現するため、すべての非同期メソッドに渡します
- 処理を try-catch で囲む - 未処理の例外が発生すると、サービスが完全に停止します
- 時間指定のタスクには PeriodicTimer を使用する - 適切なキャンセルサポートがあり、
Task.Delayよりすっきり記述できます - ヘルスチェックを追加する - Kubernetes のライブネス/レディネスプローブに不可欠です
- StartAsync のブロックを避ける - 初期化に時間がかかると、ほかのホステッドサービスの起動が遅れます
- オーバーライド時は基本クラスのメソッドを呼び出す - 必ず
await base.StartAsync()とawait base.StopAsync()を呼び出します - Windows Service 用に単一ファイルとして発行する - デプロイの複雑さとエラーを軽減できます
- スケーリング要件を考慮する - 個別のスケーリングが必要な場合は、ワーカープロジェクトを分けます
避けるべきアンチパターン
| アンチパターン | 問題点 | よりよい方法 |
|---|---|---|
場当たり的な while(true) ループ | 正常なシャットダウンができず、ライフサイクル管理も不十分です | BackgroundService を使用します |
| キャンセルトークンを無視する | 正常にシャットダウンできず、リソースがリークします | すべての非同期呼び出しにトークンを伝播します |
| スコープ付きサービスを直接注入する | 依存関係の抱え込みやメモリリークにつながります | IServiceScopeFactory を使用します |
ExecuteAsync 内の未処理例外 | ワーカーが気づかれないまま停止します | try-catch で囲み、ログを記録して処理を続行します |
時間のかかる StartAsync | ほかのサービスの起動を妨げます | 処理を ExecuteAsync に移します |
async void メソッド | 例外発生時にプロセスがクラッシュします | async Task を使用します |
| ヘルスチェックがない | ワーカーの状態を把握できません | IHealthCheck を実装します |
| 短い間隔のループによるポーリング | CPU を浪費し、応答性も低くなります | PeriodicTimer またはイベント駆動方式を使用します |
StopAsync をオーバーライドしない | クリーンアップの機会を逃します | 正常なクリーンアップのためにオーバーライドします |
| シングルトンの DbContext | スレッドセーフではなく、データも古くなります | 操作ごとにスコープを作成します |
提供するもの
- 適切に動作するワーカープロセスとホステッドサービス
- 予測可能な起動・シャットダウン動作
- 適切なスコープ付き依存関係の処理
- 本番環境での可観測性を高めるヘルスチェック
- キューワーク向けの再試行とポイズンメッセージの処理
検証項目
- キャンセルトークンが伝播され、シャットダウンが適切に処理されること
- スコープ付きサービスが適切なスコープ内で解決されること
- 例外処理によってサービスのクラッシュを防げること
- ヘルスチェックがワーカーの状態を正確に報告すること
- ログまたはテレメトリを通じて実行時の動作を確認できること
- 非同期コンテキストでブロッキング呼び出しがないこと
コメント
Markdown 対応