MagicOnion Ver 2.1.0

MagicOnionのVer 2.1.0を出しました。前回が2月28日なので、3ヶ月ぶりで少し間が空いてしまった感じもありますが、色々良くなったので紹介していきまうまう。

StramingHubClientでメッセージが詰まるバグの修正

いきなり致命的な話なんですが、StreamingHubClientが1フレにつき1メッセージしか送信されないという、しょうもないバグが存在していました。このバグの原因が面白くて(?)、元はこんな感じのコードだったんですよ。

// readerはIAsyncEnumeratorというMoveNext, Currentでデータを取ってくる非同期イテレーター
while (await reader.MoveNext())
{
    var message = reader.Current; // byte[]
    OnBroadcastEvent(message);    // messageは実際にはヘッダ解析したり色々してます
}

gRPCはIAsyncEnumeratorというかっこつけたインターフェイスを採用しているので、awaitでサーバーからデータが届くのを非同期で待機できる。

で、このawaitが問題で、UnityだとUnitySynchronizationContext経由してawaitの先が実行されます。なので安全にメインスレッドでOnBroadcastEvent(これは最終的にユーザーが実装したインターフェイス定義のメソッドが呼ばれる)が呼ばれて嬉しい。のですが、reader自体は別スレッドで動いているので、awaitの度にメインスレッドへの同期を待っているのです。

正確にはUnitySynchronizationContextがawaitの度にメインスレッド上だろうがなんだろうが問答無用で次フレームに叩き込む仕様だから、なのですけれど。

何れにせよ、そんなわけで、サーバーから同一フレームで沢山のデータが送られてきたとしても、クライアント側は1フレームに1メッセージしか捌けないので、どんどん詰まっていくわけです。もちろん、バグです。仕様じゃなく。普通に。バグ。

var syncContext = SynchronizationContext.Current;

// ConfigureAwait(false)でSyncContextを外して、このループはずっと別スレッドで動かす
while (await reader.MoveNext().ConfigureAwait(false))
{
    var message = reader.Current;
    if (syncContext != null)
    {
        // 手動でPostする(待たない)
        syncContext.Post(() => OnBroadcastEvent(message));
    }
    else
    {
        OnBroadcastEvent(message);
    }
}

と、いうわけで、こんな具合に半手動でPostするコードに書き換えました(Postでラムダ式のキャプチャが発生する問題がありますがshoganai。正確にはobject stateが渡せるのですが、実際のデータでは複数の値が必要になるのでTupleを作る必要があって、余計なオブジェクトが必要という点で変わらない)。ConfigureAwait(false)をつけないことは意識して、意図してやったこと(同期コンテキストを維持してメインスレッド上でコールバックを飛ばす)だったんですが、そこまで意識しといてこういうバグにつなげちゃうのは完全に甘かった、ということで反省しきりです。

ともあれこれで詰まり問題は大解決です。

MagicOnion.Hosting

最初のサンプルがConsole.ReadLineで待っているコードなのでアレなのですが、普通に実開発ではMagicOnion.Hostingというプロジェクトを使って欲しいと思っています。

// using MagicOnion.Hosting
static async Task Main(string[] args)
{
    await MagicOnionHost.CreateDefaultBuilder()
        .UseMagicOnion(
            new MagicOnionOptions(isReturnExceptionStackTraceInErrorDetail: true),
            new ServerPort("localhost", 12345, ServerCredentials.Insecure))
        .RunConsoleAsync();
}

Hostingとは何かと言うと、Genric Hostという.NET Core時代の基盤フレームワークの上に乗っかっています。これは、こないだ作った MicroBatchFramework – クラウドネイティブ時代のC#バッチフレームワーク と同じ仕組みです。

.NET Generic Hostは、標準的な仕組みとしてロギング/コンフィグ読み込み/DIをサポートしています。これによりコンフィグのマッピング、ロギングなどを標準的な作法でフルサポートしています。

というわけで、何が嬉しいかと言うと、↑の件をフルサポートしてくれていることです。コンフィグとか何をどう読み込めばいいんですかー?という話は、Generic Hostの仕組みを使ってください、というのが答えになります。ドキュメントもMicrosoftのドキュメントサイトで沢山解説されていて、それがそっくりそのまま使えるので、良いことしかない!

また、これによってコンストラクタインジェクションでのDIも使えるようになりました。

static async Task Main(string[] args)
{
    await MagicOnionHost.CreateDefaultBuilder()
        .ConfigureServices((hostContext, services) =>
        {
            // DI, you can register types on this section.

            // mapping config json to IOption<MyConfig>
            // requires "Microsoft.Extensions.Options.ConfigurationExtensions" package
            services.Configure<MyConfig>(hostContext.Configuration);
        })
        .RunConsoleAsync();
}

public class MyFirstService : ServiceBase<IMyFirstService>, IMyFirstService
{
    IOptions<MyConfig> config;
    ILogger<MyFirstService> logger;

    public MyFirstService(IOptions<MyConfig> config, ILogger<MyFirstService> logger)
    {
        this.config = config;
        this.logger = logger;
    }

    // ...
}

好きな型を、ConfigureServicesのとこで追加してもらえれば、コンストラクタで設定されたのが入ってきます。

今後

v2のリリース告知から半年経って、かなり注目度が上がっているというのが肌感としてあります。GitHub Starも962まで来ていますし、海外からの問い合わせも国内からも来ていて、盛り上がりありますよ!時代はC#!かもしれない!

というわけかで、来月の6月4日に初のMagicOnion勉強会が開催されます。私も登壇しますので、ぜひぜひ来てください(今はもうキャンセル待ちですが……!)

開発的には、サーバーサイドゲームループ(まだ未サポート)などの追加を挟みつつ、もう少し野心的なものも狙っていますので、是非是非楽しみにしていただければと思います。コードジェネレーターの使い勝手が悪いのも、(MessagePack-CSharpともども)改善の最優先タスクの一つになってますので、なんとかします。

また、フィードバック超大事!なので、ぜひ使ってみて、Twitterでつぶやくなり(捕捉してます)、Qiitaに書いてくれるなり(やったー!)、Issueで報告してもらったりなどなどしてくれると嬉しいです。

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

Microsoft MVP for Developer Technologies(.NET)
April 2011
|
July 2025

X:@neuecc GitHub:neuecc

Archive