mcpField
W
worker-services

worker-services

github.com/managedcode開発49639 回インストール今日更新
リポジトリ

worker-services について

.NET で長時間稼働するバックグラウンドサービスや定期実行ワーカーを実装するためのスキルです。ホステッドサービスのライフサイクル、キャンセル、スコープ付き依存関係、キュー処理、ヘルスチェック、正常終了を扱います。

主な機能

  1. 01BackgroundService によるワーカーライフサイクル管理
  2. 02スコープ付き依存関係のスコープ管理
  3. 03キャンセル伝播と正常終了
  4. 04PeriodicTimer によるタスク実行
  5. 05ヘルスチェックと構造化ログの追加

活用シーン

  1. 01定期実行バックグラウンドワーカーの構築
  2. 02スコープ付きサービスを用いたキュー処理
  3. 03ワーカーのヘルス監視と終了手順の整備

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/managedcode/dotnet-skills --skill worker-services

インストール後、Claude Code で/worker-servicesを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

manifest.json
references/
anti-patterns.md
patterns.md

詳細

.NET ワーカーサービス

対象となる状況

  • 長時間実行されるバックグラウンドサービスやスケジュールされたワーカーを構築するとき
  • アプリにホステッドサービスを追加するとき、またはワーカープロセスとして切り出すとき
  • グレースフルシャットダウン、キャンセル、キュー処理、ヘルスチェックの動作を確認するとき

ドキュメント

リファレンス
  • patterns.md - BackgroundService のパターン、グレースフルシャットダウン、ヘルスチェックの実装
  • anti-patterns.md - ワーカーサービスでよくある誤りとその回避方法

ワークフロー

  1. 基本クラスとして BackgroundService を使用します:

    • 標準的な StartAsync/StopAsync の処理が用意されています
    • ExecuteAsync の実装だけに集中できます
    • キャンセレーショントークンは適切に管理されます
  2. スコープ付き依存関係を正しく処理します:

    • スコープ付きサービスのためにサービススコープを作成します
    • ホステッドサービスでは、既定でスコープは作成されません
  3. グレースフルシャットダウンを実装します:

    • キャンセレーショントークンを全体に伝播させます
    • トークンが通知されたら、速やかに処理を完了します
    • タイムアウト時の強制終了を避けます
  4. 実行ループをシンプルに保ちます:

    • ビジネスロジックをテスト可能なサービスに移します
    • サービスのクラッシュを防ぐため、例外を処理します
    • スケジュールされた処理には PeriodicTimer を使用します
  5. 可観測性を追加します:

    • レディネス/ライブネスの確認にヘルスチェックを使用します
    • メトリクスと構造化ログを公開します
    • 複数インスタンスで実行する場合は、分散ロックを検討します

現在の上流情報

  • .NET runtime v10.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>

ベストプラクティス

  1. 基本クラスとして BackgroundService を使用する - StartAsync/StopAsync の定型処理とキャンセル管理を担います
  2. スコープが必要な依存関係にはスコープを作成する - IServiceScopeFactory を使用して、DbContext などのスコープ付きサービスを解決します
  3. キャンセルトークンをすべての処理に伝播する - 応答性の高いシャットダウンを実現するため、すべての非同期メソッドに渡します
  4. 処理を try-catch で囲む - 未処理の例外が発生すると、サービスが完全に停止します
  5. 時間指定のタスクには PeriodicTimer を使用する - 適切なキャンセルサポートがあり、Task.Delay よりすっきり記述できます
  6. ヘルスチェックを追加する - Kubernetes のライブネス/レディネスプローブに不可欠です
  7. StartAsync のブロックを避ける - 初期化に時間がかかると、ほかのホステッドサービスの起動が遅れます
  8. オーバーライド時は基本クラスのメソッドを呼び出す - 必ず await base.StartAsync() と await base.StopAsync() を呼び出します
  9. Windows Service 用に単一ファイルとして発行する - デプロイの複雑さとエラーを軽減できます
  10. スケーリング要件を考慮する - 個別のスケーリングが必要な場合は、ワーカープロジェクトを分けます

避けるべきアンチパターン

アンチパターン問題点よりよい方法
場当たり的な while(true) ループ正常なシャットダウンができず、ライフサイクル管理も不十分ですBackgroundService を使用します
キャンセルトークンを無視する正常にシャットダウンできず、リソースがリークしますすべての非同期呼び出しにトークンを伝播します
スコープ付きサービスを直接注入する依存関係の抱え込みやメモリリークにつながりますIServiceScopeFactory を使用します
ExecuteAsync 内の未処理例外ワーカーが気づかれないまま停止しますtry-catch で囲み、ログを記録して処理を続行します
時間のかかる StartAsyncほかのサービスの起動を妨げます処理を ExecuteAsync に移します
async void メソッド例外発生時にプロセスがクラッシュしますasync Task を使用します
ヘルスチェックがないワーカーの状態を把握できませんIHealthCheck を実装します
短い間隔のループによるポーリングCPU を浪費し、応答性も低くなりますPeriodicTimer またはイベント駆動方式を使用します
StopAsync をオーバーライドしないクリーンアップの機会を逃します正常なクリーンアップのためにオーバーライドします
シングルトンの DbContextスレッドセーフではなく、データも古くなります操作ごとにスコープを作成します

提供するもの

  • 適切に動作するワーカープロセスとホステッドサービス
  • 予測可能な起動・シャットダウン動作
  • 適切なスコープ付き依存関係の処理
  • 本番環境での可観測性を高めるヘルスチェック
  • キューワーク向けの再試行とポイズンメッセージの処理

検証項目

  • キャンセルトークンが伝播され、シャットダウンが適切に処理されること
  • スコープ付きサービスが適切なスコープ内で解決されること
  • 例外処理によってサービスのクラッシュを防げること
  • ヘルスチェックがワーカーの状態を正確に報告すること
  • ログまたはテレメトリを通じて実行時の動作を確認できること
  • 非同期コンテキストでブロッキング呼び出しがないこと

コメント