mcpField
A
akka-net-best-practices

akka-net-best-practices

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

akka-net-best-practices について

Akka.NET のアクターシステムを設計・運用する開発者向けのリファレンススキルです。通信、監督、エラー処理、作業分散に加え、ローカルとクラスター間の抽象化を扱い、よくある設計上の誤りも解説します。

主な機能

  1. 01EventStream と DistributedPubSub の使い分け
  2. 02アクターの監督とエラー処理
  3. 03Props と DependencyResolver の選択
  4. 04バックグラウンド作業の分散パターン
  5. 05ローカル・クラスター両モードの抽象化
  6. 06非同期キャンセルとアクターログ

活用シーン

  1. 01クラスター間のパブリッシュ・サブスクライブ方式の選択
  2. 02アクターの障害と再起動への対応
  3. 03複数ノードへのバックグラウンド処理の分散
  4. 04ローカルモードでのクラスター向け設計のテスト

インストール

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

$npx skills add https://github.com/aaronontheweb/dotnet-skills --skill akka-net-best-practices

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

同梱ファイル

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

async-cancellation-patterns.md
cluster-local-abstractions.md
work-distribution-patterns.md

詳細

Akka.NET のベストプラクティス

このスキルを使用する場面

次のような場合にこのスキルを使用します:

  • アクター間の通信パターンを設計する
  • EventStream と DistributedPubSub のどちらを使用するか判断する
  • アクターのエラー処理を実装する
  • 監督戦略を理解する
  • Props パターンと DependencyResolver のどちらを使用するか選択する
  • ノード間の作業分散を設計する
  • クラスターインフラストラクチャの有無にかかわらず実行できる、テスト可能なアクターシステムを作成する
  • ローカルテストのシナリオで Cluster Sharding を抽象化する

参照ファイル


1. EventStream と DistributedPubSub

重要:EventStream はローカルのみ

Context.System.EventStream は、単一の ActorSystem プロセス内でのみ使用できます。クラスター内のノード間では機能しません。

// BAD: This only works on a single server
// When you add a second server, subscribers on server 2 won't receive events from server 1
Context.System.EventStream.Subscribe(Self, typeof(PostCreated));
Context.System.EventStream.Publish(new PostCreated(postId, authorId));

EventStream が適切な場面:

  • 単一プロセス内でのログ記録と診断
  • 真に単一プロセスで動作するアプリケーションのローカルイベントバス
  • 開発/テストのシナリオ
マルチノードでは DistributedPubSub を使用する

複数のクラスター内ノードにまたがってアクターにイベントを届ける必要がある場合は、Akka.Cluster.Tools.PublishSubscribe を使用します:

using Akka.Cluster.Tools.PublishSubscribe;

public class TimelineUpdatePublisher : ReceiveActor
{
    private readonly IActorRef _mediator;

    public TimelineUpdatePublisher()
    {
        // Get the DistributedPubSub mediator
        _mediator = DistributedPubSub.Get(Context.System).Mediator;

        Receive<PublishTimelineUpdate>(msg =>
        {
            // Publish to a topic - reaches all subscribers across all nodes
            _mediator.Tell(new Publish($"timeline:{msg.UserId}", msg.Update));
        });
    }
}
DistributedPubSub の Akka.Hosting 設定
builder.WithDistributedPubSub(role: null); // Available on all roles, or specify a role
トピックの設計パターン
パターントピック形式ユースケース
ユーザーごとtimeline:{userId}タイムラインの更新、通知
エンティティごとpost:{postId}投稿のエンゲージメント更新
ブロードキャストsystem:announcementsシステム全体への通知
ロールベースworkers:rss-poller作業の分散

2. 監督戦略

重要な確認:監督の対象は子アクターです

アクターに定義された監督戦略が決めるのは、そのアクターが子アクターをどのように監督するかであり、そのアクター自身がどのように監督されるかではありません。

public class ParentActor : ReceiveActor
{
    // This strategy applies to children of ParentActor, NOT to ParentActor itself
    protected override SupervisorStrategy SupervisorStrategy()
    {
        return new OneForOneStrategy(
            maxNrOfRetries: 10,
            withinTimeRange: TimeSpan.FromSeconds(30),
            decider: ex => ex switch
            {
                ArithmeticException => Directive.Resume,
                NullReferenceException => Directive.Restart,
                ArgumentException => Directive.Stop,
                _ => Directive.Escalate
            });
    }
}
デフォルトの監督戦略

デフォルトの OneForOneStrategy には、すでにレート制限が含まれています:

  • 1 秒以内に 10 回再起動すると、アクターは完全に停止します
  • これにより、無限の再起動ループを防ぎます

特別な要件がない限り、カスタム戦略が必要になることはほとんどありません。

カスタム監督を定義する場面

適切な理由:

  • アクターが、回復不能な状態の破損を示す例外をスローする -> 再起動
  • アクターが、再起動を引き起こすべきではない例外(想定内の失敗)をスローする -> 再開
  • 子アクターの失敗が兄弟アクターに影響する必要がある -> AllForOneStrategy を使用する
  • デフォルトとは異なる再試行上限が必要

不適切な理由:

  • 「念のため」— デフォルトですでに安全です
  • アクターの動作を理解していない — まず動作を理解しましょう

3. エラー処理:監督と Try-Catch

Try-Catch を使用する場面(ほとんどの場合)

次の場合は try-catch を使用します:

  • 失敗が想定内(ネットワークタイムアウト、無効な入力、外部サービスの停止)
  • 例外が発生した理由を正確に把握している
  • 適切に処理できる(再試行、エラーレスポンスの返却、ログを記録して処理を続行)
  • 再起動しても効果がない(同じエラーが再び発生する)
public class RssFeedPollerActor : ReceiveActor
{
    public RssFeedPollerActor()
    {
        ReceiveAsync<PollFeed>(async msg =>
        {
            try
            {
                var feed = await _httpClient.GetStringAsync(msg.FeedUrl);
                var items = ParseFeed(feed);
                // Process items...
            }
            catch (HttpRequestException ex)
            {
                // Expected failure - log and schedule retry
                _log.Warning("Feed {Url} unavailable: {Error}", msg.FeedUrl, ex.Message);
                Context.System.Scheduler.ScheduleTellOnce(
                    TimeSpan.FromMinutes(5), Self, msg, Self);
            }
            catch (XmlException ex)
            {
                // Invalid feed format - log and mark as bad
                _log.Error("Feed {Url} has invalid format: {Error}", msg.FeedUrl, ex.Message);
                Sender.Tell(new FeedPollResult.InvalidFormat(msg.FeedUrl));
            }
        });
    }
}
監督に処理させる場面

次の場合は例外を伝播させます(監督をトリガーします):

  • 例外が発生した理由がまったくわからない
  • アクターの状態が破損している可能性がある
  • 再起動が有効(状態を初期化する、リソースに再接続する)
  • プログラミングエラー(NullReferenceException、不適切なロジックによる InvalidOperationException)である
アンチパターン:不明な例外を握りつぶす
// BAD: Swallowing exceptions hides problems
catch (Exception ex)
{
    _log.Error(ex, "Error processing work");
    // Actor continues with potentially corrupt state
}

// GOOD: Handle known exceptions, let unknown ones propagate
catch (HttpRequestException ex)
{
    // Known, expected failure - handle gracefully
    _log.Warning("HTTP request failed: {Error}", ex.Message);
    Sender.Tell(new WorkResult.TransientFailure());
}
// Unknown exceptions propagate to supervision

4. Props と DependencyResolver

通常の Props を使用する場面

次の場合は Props.Create() を使用します:

  • アクターが IServiceProvider や IRequiredActor<T> を必要としない
  • すべての依存関係をコンストラクター経由で渡せる
  • アクターがシンプルで自己完結している
// Simple actor with no DI needs
public static Props Props(PostId postId, IPostWriteStore store)
    => Akka.Actor.Props.Create(() => new PostEngagementActor(postId, store));
DependencyResolver を使用する場面

次の場合は resolver.Props<T>() を使用します。

  • アクターが、スコープ付きサービスを作成するために IServiceProvider を必要とする
  • アクターが IRequiredActor<T> を使用して、ほかのアクターへの参照を取得する
  • アクターに、すでに DI コンテナーに登録されている依存関係が多数ある
// Registration with DI
builder.WithActors((system, registry, resolver) =>
{
    var actor = system.ActorOf(resolver.Props<OrderProcessorActor>(), "order-processor");
    registry.Register<OrderProcessorActor>(actor);
});
リモートデプロイに関する考慮事項

リモートデプロイが必要になることは、ほとんどありません。 リモートデプロイを行わない場合(おそらく行わないでしょう)は、次のとおりです。

  • クロージャーを使った Props.Create(() => new Actor(...)) で問題ありません
  • 「シリアル化の問題」に関する警告は当てはまりません

ほとんどのアプリケーションでは、リモートデプロイではなくクラスターシャーディングを使用します。分散を自動的に処理できます。


5. 作業分散のパターン

多数のバックグラウンドジョブ(RSS フィード、メール送信など)がある場合、一度にすべてを処理しないでください。サンダリングハード問題が発生する原因になります。

これを解決する 3 つのパターン:

  1. データベース駆動の作業キュー - FOR UPDATE SKIP LOCKED を使用して、ノード間で自然に分散します
  2. Akka.Streams のレート制限 - 単一ノード内で処理を調整します
  3. 永続キュー(Outbox パターン) - 信頼性の高い処理を実現する、データベースを基盤としたアウトボックスです

コードサンプルの全容は、work-distribution-patterns.md を参照してください。


6. よくある間違いのまとめ

間違い問題となる理由修正方法
ノード間のパブリッシュ/サブスクライブに EventStream を使用するEventStream はローカル専用ですDistributedPubSub を使用する
アクターを「保護」するために監督を定義する監督が保護するのは子アクターです階層構造を理解する
すべての例外をキャッチするバグを隠し、状態を破損させます想定されるエラーのみをキャッチする
常に DependencyResolver を使用する不要な複雑さが加わります可能な場合は通常の Props を使用する
すべてのバックグラウンドジョブを一度に処理するサンダリングハードやリソース枯渇の原因になりますデータベースキューとレート制限を使用する
想定内の失敗に対して例外をスローする不要な再起動が発生します結果型を返し、メッセージングを使用する

7. クイックリファレンス

通信パターンの判断フローチャート
Need to communicate between actors?
├── Same process only? -> EventStream is fine
├── Across cluster nodes?
│   ├── Point-to-point? -> Use ActorSelection or known IActorRef
│   └── Pub/sub? -> Use DistributedPubSub
└── Fire-and-forget to external system? -> Consider outbox pattern
エラー処理の判断フローチャート
Exception occurred in actor?
├── Expected failure (HTTP timeout, invalid input)?
│   └── Try-catch, handle gracefully, continue
├── State might be corrupt?
│   └── Let supervision restart
├── Unknown cause?
│   └── Let supervision restart
└── Programming error (null ref, bad logic)?
    └── Let supervision restart, fix the bug
Props の判断フローチャート
Creating actor Props?
├── Actor needs IServiceProvider?
│   └── Use resolver.Props<T>()
├── Actor needs IRequiredActor<T>?
│   └── Use resolver.Props<T>()
├── Simple actor with constructor params?
│   └── Use Props.Create(() => new Actor(...))
└── Remote deployment needed?
    └── Probably not - use cluster sharding instead

8. クラスター/ローカルモードの抽象化

クラスター化された本番環境とローカル/テスト環境の両方で実行する必要があるアプリケーションでは、実装を切り替える抽象化パターンを使用します。

  • AkkaExecutionMode enum - 使用する実装(LocalTest または Clustered)を制御します
  • GenericChildPerEntityParent - 同じ IMessageExtractor を使用し、ローカルでシャーディングの動作を模倣します
  • IPubSubMediator - DistributedPubSub を抽象化し、ローカル/クラスターの実装を切り替えられるようにします

実装コードの全容は、cluster-local-abstractions.md を参照してください。


9. アクターのロギング

ILogger ではなく ILoggingAdapter を使用する

アクターでは、DI で注入した ILogger<T> ではなく、Context.GetLogger() から取得した ILoggingAdapter を使用します。

public class MyActor : ReceiveActor
{
    private readonly ILoggingAdapter _log = Context.GetLogger();

    public MyActor()
    {
        Receive<MyMessage>(msg =>
        {
            _log.Info("Processing message for user {UserId}", msg.UserId);
            _log.Error(ex, "Failed to process {MessageType}", msg.GetType().Name);
        });
    }
}

ILoggingAdapter を使用する理由:

  • Akka のロギングパイプラインおよび監督と連携します
  • v1.5.57 以降、セマンティック/構造化ロギングをサポートします
  • メソッド名は Info()、Debug()、Warning()、Error() です(Log* 系ではありません)
  • DI は不要です。アクターのコンテキストから直接取得できます

アクターに ILogger を注入しないでください。 Akka のロギング基盤を迂回してしまいます。

セマンティックロギング(v1.5.57 以降)
// Named placeholders for better log aggregation and querying
_log.Info("Order {OrderId} processed for customer {CustomerId}", order.Id, order.CustomerId);

// Prefer named placeholders over positional
// Good: {OrderId}, {CustomerId}
// Avoid: {0}, {1}

10. CancellationToken による非同期操作の管理

アクターが PipeTo 経由で非同期操作を開始した場合、適切に管理しなければ、その操作がアクターの終了後も継続することがあります。重要な実践方法は次のとおりです。

  • PostStop でのアクター CTS - PostStop() で必ずキャンセルし、破棄します
  • 操作ごとに新しい CTS を作成する - 新しい処理を開始する前に、前の処理をキャンセルします
  • トークンをすべての処理に渡す - EF Core クエリ、HTTP 呼び出しなど
  • タイムアウトにはリンクされた CTS を使用する - 外部呼び出しに短いタイムアウトを設定して、停止状態を防ぎます
  • 適切に処理する - catch ブロックでタイムアウトとシャットダウンを区別します

実装コードの全容は、async-cancellation-patterns.md を参照してください。

コメント