asyncの落とし穴Part3, async voidを避けるべき100億の理由

だいぶ前から時間経ってしまいましたが、非同期の落とし穴シリーズPart3。ちなみにまだ沢山ネタはあるんだから!どこいっても非同期は死にますからね!

async void vs async Task

自分で書く場合は、必ずasync Taskで書くべき、というのは非同期のベストプラクティスで散々言われていることなのですけれど、理由としては、まず、voidだと、終了を待てないから。voidだと、その中の処理が軽かろうと重かろうと、終了を感知できない。例外が発生しても分からない。投げっぱなし。これがTaskになっていれば、awaitで終了待ちできる。例外を受け取ることができる。await Task.WhenAllで複数同時に走らせたのを待つことができる。はい、async Taskで書かない理由のほうがない。

んじゃあ何でasync voidが存在するかというと、イベントがvoidだから。はい。button_clickとか非同期対応させるにはvoidしかない。それだけです。なので、自分で書く時は必ずasync Taskで。async voidにするのはイベントだけ。これ絶対。

ASP.NET + async voidで死ぬ

それでもasync voidをうっかり使っちゃうとどうなるでしょう?終了を待てないだけとか、そんなんならいいんですよ、でも、ASP.NETでasync void使うと死にます。文字通りに死にます。アプリケーションが。じゃあ、ASP.NET MVCで試してみましょうか。WebForms?しらね。

public async void Sinu()
{
    await Task.Delay(TimeSpan.FromSeconds(1));
}

public ActionResult Index()
{
    Sinu();

    return View();
}

死にました!警告一切ないのに!って、ああ、そうですね、async="true"にしないとですね、まぁそれはないのですけれど、はい、Task<ActionResult>を返しましょう。そうすればいいんでしょ?

public async void Sinu()
{
    await Task.Delay(TimeSpan.FromSeconds(1));
}

public async Task<ActionResult> Index()
{
    Sinu();

    return View();
}

はい、死にました!非同期操作が保留中に非同期のモジュールとハンドラーが完了しちゃいましたか、しょーがないですね。しょーがない、のか……?

で、これの性質の悪いところは、メソッド呼び出しの中に一個でもasync voidがあると詰みます。

// こんなクソクラスがあるとして
public class KusoClass
{
    public async void Sinu()
    {
        await Task.Delay(TimeSpan.FromSeconds(1)); // 1じゃなく5ね。
    }
}

// この一見大丈夫そうなメソッドを
public async Task Suteki()
{
    // ここでは大丈夫
    await Task.Delay(TimeSpan.FromSeconds(1));

    // これを実行したことにより……
    new KusoClass().Sinu();

    // ここも実行されるし
    await Task.Delay(TimeSpan.FromSeconds(1));
}

// このアクションから呼び出してみると
public async Task<ActionResult> Index()
{
    // これを呼び出してちゃんと待機して
    await Suteki();

    // ここも実行されるのだけれど
    await Task.Delay(TimeSpan.FromSeconds(1));

    return View();
}

死にます。ただし、上で5秒待機を1秒待機に変えれば、動きます。なぜかというと、KusoClass.Sinuを実行のあとに2秒待機があってViewを返してるので、Viewを返すまでの間にKusoClass.Sinuの実行が完了するから。そう、View返すまでに完了してればセーフ。してなければ死亡。まあ、ようするに、死ぬってことですね結局やっぱり。何故かたまに死ぬ、とかいう状況に陥ると、むしろ検出しづらくて厄介極まりないので、死ぬなら潔く死ねって感じ。検出されずそのまま本番環境に投下されてしまったら……!あ、やった人がいるとかいないとかいるらしい気がしますが気のせい。

呼び出し階層の奥底にasync voidが眠ってたら死ぬとか、どーせいという話です。どーにもならんです。なので、共通ライブラリとか絶対async void使っちゃダメ。あるだけで死んでしまうのですから。

FireAndForget

さて、投げっぱなしの非同期メソッドを使いたい場合、どうすればいいんでしょう?

public async Task ToaruAsyncMethod()
{
    await Task.Delay(TimeSpan.FromSeconds(1));
    Debug.WriteLine("hoge");
}

public async Task<ActionResult> Index()
{
    // 待機することなく投げっぱなしにしたいのだけど警告が出る!
    ToaruAsyncMethod();

    return View();
}

あー、警告ウザす警告ウザす。その場合、しょうがなく変数で受けたりします。

public async Task<ActionResult> Index()
{
    // 警告抑制のため
    var _ = ToaruAsyncMethod();

    return View();
}

はたしてそれでいーのか。いやよくない。それに、やっぱこれだと例外発生した時に捉えられないですしね。TaskScheduler.UnobservedTaskExceptionに登録しておけば大丈夫ですけれど(&これは登録必須ですが!)。というわけで、以下の様なものを用意しましょう。

// こんな拡張メソッドを用意すると
public static class TaskEx
{
    // ロガーはここではNLogを使うとします
    private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();

    /// <summary>
    /// 投げっぱなしにする場合は、これを呼ぶことでコンパイラの警告の抑制と、例外発生時のロギングを行います。
    /// </summary>
    public static void FireAndForget(this Task task)
    {
        task.ContinueWith(x => 
        {
            logger.ErrorException("TaskUnhandled", x.Exception);
        }, TaskContinuationOptions.OnlyOnFaulted);
    }
}

// こんな投げっぱなしにしたい非同期メソッドを呼んでも
public async Task ToaruAsyncMethod()
{
    await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
    Debug.WriteLine("hoge");
    throw new Exception();
}

public ActionResult Index()
{
    ToaruAsyncMethod().FireAndForget(); // こうすれば警告抑制&例外ロギングができる
            
    return View();
}

いいんじゃないでしょうか?

ところで、ToaruAsyncMethodに.ConfigureAwait(false)をつけてるのは理由があって、これつけないと死にます。理由は、覚えてますか?asyncの落とし穴Part2, SynchronizationContextの向こう側に書きましたが、リクエストが終了してHttpContextが消滅した状態でawaitが完了するとNullReferenceExceptionが発生するためです。

そして、これで発生するNullReferenceExceptionは、FireAndForget拡張メソッドを「通りません」。こうなると、例外が発生したことはUnobservedTaskExceptionでしか観測できないし、しかも、そうなるとスタックトレースも死んでいるため、どこで発生したのか全く分かりません。Oh...。

たとえFireAndForgetで警告が抑制できたとしても、非同期の投げっぱなしは細心の注意を払って、呼び出しているメソッド階層の奥底まで大丈夫であるという状態が確認できていて、ようやく使うことができるのです。うげぇぇ。それを考えると、ちゃんと警告してくれるのはありがたいね、って思うのでした。

まとめ

voidはまぢで死ぬ。投げっぱなしも基本死ぬ。

では何故、我々は非同期を多用するのか。それはハイパフォーマンスの実現には欠かせないからです。それだけじゃなく、asyncでしか実現できないイディオムも山のようにあるので。いや、こんなの全然マシですよ、大袈裟に書きましたがasync void使わないとか当たり前なのでそこ守れば大丈夫なんですよ(棒)。じゃあ何でasync voidなんてあるんだよとか言われると、イベント機構があるからしょうがないじゃん(ボソボソ)、とか悲しい顔にならざるを得ない。

というわけで、弊社は非同期でゴリゴリ地雷踏みたいエンジニアを大募集してます。ほんと。

謎社が一周年を迎えました。

まあ、迎えたのは9/19なので、10日以上経っちゃってるんですが。ほげ。というわけかで、謎社あらため株式会社グラニは、設立一年を迎えました。前職を退職したのが10/20なので、私にとっては一年、まだ経ってません。はい。今の役職は取締役CTOなのですが、実は設立時には居なかったんですねえ。ジョインしたのは若干遅れてます。その間シンガポールにいたりほげもげ……。

ともあれ今は、当面は地下に潜伏していますが、必ず浮上しますのでしばしお待ちくだしあ。
gloopsを退職しました。 - 2012/10/20

なんで謎社かというと、退職後から表に出るまでは、ちょっとだけ内緒ということで、その間Twitterでずっと謎社って言ってたのが残ってるだけですね。Twilog @neuecc/謎社 古い順。googleで謎社で検索しても一位がグラニになってたりするので、それはそれで何となく定着してるのでヨシとしませうか。

一年の成果

謎々潜伏期間中に思い描いてたこと、あります。

C#といったら謎社!みたいな、C#を使う人が憧れるぐらいな立ち位置の会社にできればいいなと思っています
2012年を振り返る。 - 2012/12/30

これには、まず、会社が成功してなきゃダメです。その点でいうと、最初のタイトル(1/25に出しました)神獄のヴァルハラゲートは大躍進を遂げ、3度のテレビCM(今も放送してます)、GREE Platform Award2013年上半期総合大賞受賞など、業界では2013年を代表するタイトルとなれたと思います。

というわけで、会社は成功した(勿論、まだまだこれから更に発展していきますよ!)。技術的にはどうでしょう。実は最初のリリース時はPHPだったのですが、これは7月にC#に完全リプレースしていて、今は100% C#, ASP.NET MVCで動いています。技術に関しては、一部はリリース前にBuild Insider Offlineというイベントで.NET最先端技術によるハイパフォーマンスウェブアプリケーションとして発表しましたが(.NET系にしては珍しくはてブ300超えて割とヒット)、使用テクノロジ・アーキテクチャに関しては、間違いなく最先端を走っていると思います。エクストリームWebFormsやエクストリームDataSetに比べると、ちゃんと技術を外に語れるのがいいですにぇ。

また、.NETでのフルAWS環境で超高トラフィックを捌いているのですが、これは結構珍しいところかもです。.NETというだけじゃなく、この業界だと、データベースはFusion-ioのようなハイパーなドライブを詰んだオンプレミス環境であることも多いのですが、Fusion-ioは甘え、クラウドでも十分やれる。むしろこれからはそれがスタンダード。完全クラウドでやれる、という証明をしていく、というわけでAWS ゲーム業界事例 株式会社グラニ様などでも紹介されています。

NewRelicSumo Logicなど、日本では(特に.NETでは)マイナーなサービスでも、良いと思ったら柔軟にガンガン導入していっています。特にSumoLogicはWindows+日本語環境だと文字化けとかもありましたが、弊社からのフィードバックで解消していっているなど(つまりうち以外誰も使ってないのかいな……)我々が次代のスタンダードを作っていく、という気概でやっていってます。

と、たった一年の企業にしては相当やったと思うのですが、しかし、「憧れるぐらいな立ち位置」には、まだまだ全然。土台は出来たと思うので、ここからはしっかり発展させていかなきゃな、と。

We're Hiring

というわけで、何を言いたいかというとコレです(笑)。超採用中です。グラニ/採用情報が、非常に古臭いページで、しかもmailtoでしか応募できないというハードルの高さでアレなのですが、かなり!真面目に!募集してます。ページはそのうちまともになるので、むしろ応募人数が少ないmailtoのうちのほうが採用確立高いかもですよ!?

現在どのぐらい人数がいるかというと、会社全体で既に50人ぐらい、エンジニアも20人弱います。小規模な会社、というフェーズは超えてます。会社自体も↑のように割と成功しているので、色々とは安心してください。

開発環境はかなり充実していて、トリプルディスプレイが出力できない開発PCなんて許さん!とかショボい椅子は嫌だ!とかWindows 8じゃなきゃ嫌だ!とか当然VS2012!Fakesの使えないVisual StudioなんてありえないからPremium以上!とか、こんなにやれてる会社は中々ないでしょう。

コードは、つい7月にリリースしたものがソースコードの全てで過去の遺産が一切ない状態なので、100%、C# 5.0 + .NET 4.5 + ASP.NET MVC 4という、最先端のフレームワークが存分に利用できます。これは、常にアップデートしていく、という意思が固いので、今後も古いもので書かなきゃいけない……みたいな状況は絶対作りません。これはもう宣言。誓って。

技術的にも凄まじいasync祭り(Webでここまでやってるのは世界でも稀でしょう)とか、良くも悪くも先端を突っ走るし地雷は自分で踏んで自分で処理して、「我々が道を作る」覚悟で、技術的に業界をリードする会社であろうとしています。そうじゃなきゃ「C#を使う人が憧れるぐらいな立ち位置」にはなれませんから。なので、技術的な発信に関しては、私に限らず、皆がアクティブに行っていきたいと思っています。なお、私含めてMicrosoft MVPは3人在籍しています。

C#といったら謎社にする。といった気概のある方は、是非とも応募してみて下さい。らんぷの巣窟にC#で殴りこみをかけれるとか謎社にしか出来ない面白いポジションですし、.NET世界に篭もらずに、C#を業界のスタンダードへと導けるのは我々だけ!というぐらいな勢いがありますよ。

(注意:但し、我々はサービスを提供している会社です。技術あってのサービス、サービスあっての技術。両輪なので、多少の偏りはいいんですが、片方がゼロの場合は良い物は作れないので、お断るかもしれません)

とかなんとかだと、ハードル高すぎ、な感がするかもですが、そんなにそんなでもないので、気になるなぁと思った人は現時点での何らかの懸念(技術的に、とかスキルセットが合わないかも、とか)は抜きにして、来てもらえると嬉しいですね。ウェブ系以外でも全然OKですし。C#が全てに通用することを現実世界での成功でもって証明する!ことも掲げているので、ウェブ以外であっても、アリアリなのです。

MySQL + Dapperによる高負荷時のバグで死んだ話

今回はMySQLとDapperを組み合わせると死ぬ、という超極少数にしか該当しない話ですよ!というわけで、まぁ読み物としてどーぞ。ちなみに割とクリティカルなので、その組み合わせで何かやろうという人は気をつけたほうがいいです。

観測

何が起こるかの観測からはじめましょう。まず、NuGetからMySQL.DataとDapperをインストールして、以下の簡単なコードを走らせ、ません。コード書いて待機で。

using Dapper;
using MySql.Data.MySqlClient;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

class Program
{
    static void Main(string[] args)
    {
        // スレッドプールを先に伸びるように
        ThreadPool.SetMinThreads(200, 200);
        var count = 0;

        // とりあえず200並列で実行する
        Parallel.For(0, 10000, new ParallelOptions { MaxDegreeOfParallelism = 200 }, () =>
        {
            var conn = new MySqlConnection("せつぞくもじれーつ,Max Pool Size=1000");
            conn.Open();
            return conn;
        }, (x, state, conn) =>
        {
            System.Console.WriteLine(Interlocked.Increment(ref count));

            // ↑や↓にグチャグチャありますが、実態はこの一行だけです
            conn.Query<long>("select 1", buffered: false).First();

            return conn;
        }, conn =>
        {
            conn.Dispose();
        });
    }
}

Parallelなのは高負荷じゃないとイマイチ分からないので、そのシミュレートね。これも別にそこまで高負荷ってわけでもないですが、まぁこんなんでいいのです(原因分かってて逆算して書いてるので)。接続文字列は、とりあえず200並列に耐えられるようにMaxPoolSizeだけ大きめに設定しておきます。さて、そしてPowerShellを立ち上げ、とりあえず以下のスクリプトを走らせておきます。

 while ($true) {Get-NetTCPConnection | group state -NoElement; sleep 1}

で、上のC#コードを実行。すると、実行は、遅いです。Parallelなので速いのか遅いのかわからないかもですが、まあぶっちけ凄く遅いです。正常状態ならサクッと終わるのですが、↑のはかなりもたついてます。で、運が良ければ以下の例外にも遭遇するでしょう。時には完走するかもしれませんが。

ハンドルされていない例外: System.AggregateException: 1 つ以上のエラーが発生しました。
---> MySql.Data.MySqlClient.MySqlException: Unable to connect to any of the specified MySQL hosts.
---> System.Net.Sockets.SocketException: そのようなホストは不明です。

よくわからないけどSocketExceptionで死にますね!!!さて、PowerShellのほうはどうなっていたか、というと、

ぎゃー、TIME_WAIT祭だー。って、Parallelだからそうなったんじゃないかって?いえいえ、所詮200並列ですし、ある程度はOpen/Close繰り返されるとはいえ、4000個もコネクション作ったりなんてしないです。ていうかそもそもコネクションプールあるんだから、そんなに繋ぐわけないでしょーが。

つまり、どういうことだってばよ?シンプルな例にしましょうか。

using (var conn = new MySqlConnection("せつぞくもじれーつ"))
{
    conn.Open();
    for (int i = 0; i < 100; i++)
    {
        conn.Query<long>("select 1", buffered: false).First();
    }
}

はい。100のループで100のTIME_WAITが発生しています。つまり、1クエリにつき1TIME_WAITです。ほへ……?ということは、低負荷時はそれでも生きてられるかもですが、高負荷時は、最初の例で見たように、山のようなTIME_WAITに見舞われます。そしてsocketは枯渇する。そして死ぬ。

何故こうなるのか

何故、の前にどの組み合わせでなるか、というと、現時点での最新のDapper(1.13)で、Queryをbuffered:falseで、FirstなどReaderの列挙を完了させないもの(ToListとかなら大丈夫)で確定させると詰みます。じゃあまあ、原因にDeepDive。

キーになるコードは、Dapperのこのメソッドです。

private static IEnumerable<T> QueryInternal<T>(this IDbConnection cnn, string sql, object param, IDbTransaction transaction, int? commandTimeout, CommandType? commandType)
{
    var identity = new Identity(sql, commandType, cnn, typeof(T), param == null ? null : param.GetType(), null);
    var info = GetCacheInfo(identity);

    IDbCommand cmd = null;
    IDataReader reader = null;

    bool wasClosed = cnn.State == ConnectionState.Closed;
    try
    {
        cmd = SetupCommand(cnn, transaction, sql, info.ParamReader, param, commandTimeout, commandType);

        if (wasClosed) cnn.Open();
        reader = cmd.ExecuteReader(wasClosed ? CommandBehavior.CloseConnection : CommandBehavior.Default);
        wasClosed = false; // *if* the connection was closed and we got this far, then we now have a reader
        // with the CloseConnection flag, so the reader will deal with the connection; we
        // still need something in the "finally" to ensure that broken SQL still results
        // in the connection closing itself
        var tuple = info.Deserializer;
        int hash = GetColumnHash(reader);
        if (tuple.Func == null || tuple.Hash != hash)
        {
            tuple = info.Deserializer = new DeserializerState(hash, GetDeserializer(typeof(T), reader, 0, -1, false));
            SetQueryCache(identity, info);
        }

        var func = tuple.Func;

        while (reader.Read())
        {
            yield return (T)func(reader);
        }
        // happy path; close the reader cleanly - no
        // need for "Cancel" etc
        reader.Dispose();
        reader = null;
    }
    finally
    {
        if (reader != null)
        {
            if (!reader.IsClosed) try { cmd.Cancel(); }
                catch { /* don't spoil the existing exception */ }
            reader.Dispose();
        }
        if (wasClosed) cnn.Close();
        if (cmd != null) cmd.Dispose();
    }
}

色々ありますが、while(reader.Read())以下の部分だけを見ればOKです。ToListなら大丈夫でFirstだとダメな理由もここにあります。ToListの場合、Readerが全て読むので、コード上にコメントでhappy pathと書いてある、reader.Disposeが呼ばれます、そしてfinallyでは何もしない。逆にFirstの場合は、最初のyield returnで打ち切られてfinallyへ向かうので、cmd.Cancel()が呼ばれた後に、reader.Disposeが呼ばれます。

そう、ここです。reader.Disposeの前にcmd.Cancelが呼ばれる、というのが、非常にマズい。少なくともMySQL Connectorにおいては。MySQL ConnectorのCommmand.Cancelの実装を見てみましょう。あ、MySQL Connectorのソースコードはちゃんと公開されています、Download Connector/NetのSelect PlatformでSource Codeを選べば落とせます。zipで。リポジトリは、多分公開されてない、残念ながら……。

さて、Command.Cancelは以下のメソッドを呼び出します。

public void CancelQuery(int timeout)
{
    MySqlConnectionStringBuilder cb = new MySqlConnectionStringBuilder(Settings.ConnectionString);
    cb.Pooling = false;
    cb.AutoEnlist = false;
    cb.ConnectionTimeout = (uint)timeout;

    using (MySqlConnection c = new MySqlConnection(cb.ConnectionString))
    {
        c.isKillQueryConnection = true;
        c.Open();
        string commandText = "KILL QUERY " + ServerThread;
        MySqlCommand cmd = new MySqlCommand(commandText, c);
        cmd.CommandTimeout = timeout;
        cmd.ExecuteNonQuery();
    }
}

どういうことか。「新しいコネクションをコネクションプーリングなしで新規生成して」「KILL QUERY実行して」「コネクションを閉じる」。KILL QUERYはどうでもいいんですが、新しいコネクションをプーリングなしで作って閉じるということがどういう結果をもたらすか。一回のクエリ毎に↑のが実行されるとどうなるか。まず、遅くなる。そりゃ遅いわな、1クエリ毎に1接続&切断してるんだもの。そして、1クエリ = 1TIME_WAIT。完全にコードは書かれたとおりに動いて書かれるとおりの結果しか出てこない。素晴らしい。泣ける。

どうすればいいのか

これ、1.12 ~ 1.13の間に加えられた変更が原因です。具体的にはIssue 106で、SQL ServerだとTimeoutException出るから、reader閉じる前にcommandのCancel呼んで欲しいんだよねー、というパッチが受け入れられたのでした。その結果、MySQLだと死ぬことに。

なので、1.12を使えば問題は起こりません。もしくは1.13だったら、手動でソースコードに修正を加えればいいでしょう。もともとDapperはソースコードが1ファイルなので、NuGet経由ではなく、最新のコードをファイルで持ってくれば、編集は楽です。

ちなみに、この件はDapperのForumでは報告済みです。一ヶ月前に。そして返事はありません。みょーん。まあ、あと究極的にはSQL ServerをとるかMySQLをとるか、になるので、どうなんでしょうね、あんま期待は持たないほうがいい気もします。

おまけ、MySQL Connectorのコネクションプールについて

MySQL Connectorのコードは結構素朴なので、読みやすいです(MSのSqlServerのに比べると遥かに!)。というわけで、つらつら読んでみるといいんじゃないでしょーか。参考になったりゲッソリしたり色々です。

というわけでコネクションプールとかがどうなってるかの説明しませう。そしてそれぞれが接続文字列のオプションでどう弄れるのかについて。接続文字列でのオプションの設定は結構重要ですからね、失敗してると死にますから。何度か死にましたから……。接続文字列のリファレンスはこちらConnector/Net Connection String Options Reference。古いもの(日本語訳されてる!)だけを参照すると時々痛い目に会うので適度に注意。

さて、まず、MySqlConnectionをnewすること自体は全然軽くて(ただの入れ物なので)じゅーよーなメソッドはOpenとCloseです。OpenするとコネクションプールからDriver(これが本当の実態でプールするもの)を取り出します。CloseするとコネクションプールにDriverを戻します。これが基本的なこと。ちなみにプールはQueueです、なのでDriverは先入れ先出しで循環してます。

プールということは、一度生成したコネクションはいつ消えるの?というと、Closeする時です。Close時にExpireしているかチェックし、してれば消滅、してなければプールへ。基準時間は 「接続が最初に作られた時」です。そのExpireの時間はどこで設定するの?というと接続文字列のConnection Lifetime。ちなみにデフォルトは0で、Close時には消滅しません。 なお、コネクションの最大プール数も接続文字列で指定できて、そのMax Pool Sizeのデフォは100です。

実際のところ、Close時以外にもコネクションが消える時があります。3分に一回、アイドル状態の接続がチェックされて(Timerで別スレッドで常に巡回されてる)、その時に、3分以上、プールに溜まったままのコネクションはお掃除されます。基準時間は「接続がプールに戻された時」です。Connection Lifetimeと基準が違うんですね。

Open/Closeを繰り返すことのロスは、上記のようにプールから取り出したり戻したり程度なので、かなり小さいです(一応、取り出したりする際にプールをlockしますが、全然小さいので無視できるでしょう)。あと、一応Open時にサーバーが生きてるかPingを飛ばします(この動作はビミョーだと思うんですけどねえ)。プールのlockよりも、コスト的にはこっちのほうが大きいかもですね。

あと他に接続文字列だと、Connection TimeoutとDefaultCommandTimeoutを弄っておくと幸せになれるでしょふ。

DefaultCommandTimeoutで設定できるコマンドタイムアウトはExecuteNonQueryやExecuteReaderで最初のレスポンスが戻ってくるまでの時間ではかられます。だから、ExecuteReaderでも、例えば数億件のデータがあって凄く時間がかかるものでも、反応はかえって来てるのでそこでのTimeout判定は入りません。レコード数よりも、純粋なSQLの内容(馬鹿でかいデータにlikeで検索とかは引っかかりますよね)のためのものです。

Connection Timeoutは、実はコネクションをOpenにする時、だけに関連してるわけじゃあなかったりします。streamからReadする際の時間でもConnectionStringsのConnect Timeoutで判定があります。この値を流用されるのイマイチ納得いかない気がしなくもないですが、まあそんなもんですかねえ。なので結果セットを読み込んでる最中でもネットワークの調子が悪くて止まると、タイムアウト判定が来ます。Raedの単位はデータセット全て、ではなく1行分とか、そういう特定バイト数ごとになるので、巨大データを読み取るのに時間がかかるので死ぬ、とかにはなりません。一応参考までにってとこですかね。

まとめ

このDapperで死ぬ問題、プロダクション環境化で発覚したんですよね!!!分かってみると割と単純なのですが、しかし条件が変則的で突き止めるのに手間取って泣きたかった……。低負荷だと割と動いちゃうというか、そこそこの負荷でも割と耐え切れちゃう(IIS偉い)ので、全然気づかず。なんか分からないけど超高負荷の時に落ちる!とか、ね。ワケワカラナカッタ。まず疑うのは自分のコードのほうだしねえ。

まぁ、解決してほんと良かったです、はい。解決しなかったら首吊るしかなかったですもの、思い出すだけでハイパーお通夜。冗談抜きに今までの人生で一番精神的に苦しかった……。そんな感じで若干トラブルもありましたが、それを除けばDapperはパワフルでかなり満足しています。少々、上モノを被せているので、その辺のものはそのうち紹介しましょうかしらん。

Http, SQL, Redisのロギングと分析・可視化について

改善は計測から。何がどれだけの回数通信されているか、どれだけ時間がかかっているのか、というのは言うまでもなく重要な情報です。障害対策でも大事ですしね。が、じゃあどうやって取るの、というとパッとでてくるでしょうか?そして、それ、実際に取っていますか?存外、困った話なのですねー。TraceをONにすると内部情報が沢山出てきますが、それはそれで情報過多すぎるし、欲しいのはそれじゃないんだよ、みたいな。

Grani←「謎社」で検索一位取ったので、ちょっと英語表記の検索ランキングをあげようとしている――では自前で中間を乗っ取ってやる形で統一していて、使用している通信周り、Http, RDBMS, Redisは全てログ取りして分析可能な状態にしています。

HTTP

HttpClient(HttpClientについてはHttpClient詳解を読んでね)には、DelegatingHandlerが用意されているので、その前後でStopwatchを動かしてやるだけで済みます。

public class TraceHandler : DelegatingHandler
{
    static readonly Logger httpLogger = NLog.LogManager.GetLogger("Http");

    public TraceHandler()
        : base(new HttpClientHandler())
    {

    }

    public TraceHandler(HttpMessageHandler innerHandler)
        : base(innerHandler)
    {

    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
    {
        var sw = Stopwatch.StartNew();

        // SendAsyncの前後を挟むだけ
        var result = await base.SendAsync(request, cancellationToken);

        sw.Stop();
        httpLogger.Trace(Newtonsoft.Json.JsonConvert.SerializeObject(new
        {
            date = DateTime.Now,
            command = request.Method,
            key = request.RequestUri,
            ms = sw.ElapsedMilliseconds
        }, Newtonsoft.Json.Formatting.None));

        return result;
    }
}
// 使う時はこんな感じにコンストラクタへ突っ込む
var client = new HttpClient(new TraceHandler());

// {"date":"2013-07-30T21:29:03.2314858+09:00","command":{"Method":"GET"},"key":"http://www.google.co.jp/","ms":129}
client.GetAsync("http://google.co.jp/").Wait();

なお、StreamをReadする時間は含まれていないので、あくまで向こうが反応を返した速度だけの記録になりますが、それでも十分でしょう。 Loggerは別にConsole.WriteLineでもTraceでも何でもいいのですが、弊社では基本的にNLogを使っています。フォーマットは、Http, Sql, Redisと統一するためにdate, command, key, msにしていますが、この辺もお好みで。

なお、DelegatingHandlerは連鎖して多段に組み合わせることが可能です。実際AsyncOAuthと合わせて使うと

var client = new HttpClient(
    new TraceHandler(
        new OAuthMessageHandler("key", "secret")));

といった感じになります。AsyncOAuthはHttpClientの拡張性がそのまま活かせるのが強い。

SQL

全てのデータベース通信は最終的にはADO.NETのDbCommandを通ります、というわけで、そこをフックしてしまえばいいのです。というのがMiniProfilerで、以下のように使います。

var conn = new ProfiledDbConnection(new SqlConnection("connectionString"), MiniProfiler.Current);

MiniProfilerはASP.NET MVCでの開発に超絶必須な拡張なわけで、当然、弊社でも使っています。さて、これはこれでいいのですけれど、MiniProfiler.Currentは割とヘヴィなので、そのまま本番に投入するわけもいかずで、単純にトレースするだけのがあるといいんだよねー。なので、ここは、MiniProfiler.Current = IDbProfilerを作りましょう。

なお、DbCommandをフックするProfiledDbConnectionに関してはそのまま使わせてもらいます。ただたんに移譲してるだけなんですが、DbCommandやDbTransactionや、とか、関連するもの全てを作って回らなければならなくて、自作するのカッタルイですから。ありものがあるならありものを使おう。ちなみに、MiniProfilerにはSimpleProfiledConnectionという、もっとシンプルな、本当に本当に移譲しただけのものもあるのですけれど、これはIDbConnectionがベースになってるので実質使えません。ProfiledDbConnectionのベースはDbConnection。IDbConnectionとDbConnectionの差異はかなり大きいので(*AsyncもDb...のほうだし)、実用的にはDbConnectionが基底と考えてしまっていいかな。

public class TraceDbProfiler : IDbProfiler
{
    static readonly Logger sqlLogger = NLog.LogManager.GetLogger("Sql");

    public bool IsActive
    {
        get { return true; }
    }

    public void OnError(System.Data.IDbCommand profiledDbCommand, ExecuteType executeType, System.Exception exception)
    {
        // 何も記録しない
    }

    // 大事なのは↓の3つ

    Stopwatch stopwatch;
    string commandText;

    // コマンドが開始された時に呼ばれる(ExecuteReaderとかExecuteNonQueryとか)
    public void ExecuteStart(System.Data.IDbCommand profiledDbCommand, ExecuteType executeType)
    {
        stopwatch = Stopwatch.StartNew();
    }

    // コマンドが完了された時に呼ばれる
    public void ExecuteFinish(System.Data.IDbCommand profiledDbCommand, ExecuteType executeType, System.Data.Common.DbDataReader reader)
    {
        commandText = profiledDbCommand.CommandText;
        if (executeType != ExecuteType.Reader)
        {
            stopwatch.Stop();
            sqlLogger.Trace(Newtonsoft.Json.JsonConvert.SerializeObject(new
            {
                date = DateTime.Now,
                command = executeType,
                key = commandText,
                ms = stopwatch.ElapsedMilliseconds
            }, Newtonsoft.Json.Formatting.None));
        }
    }

    // Readerが完了した時に呼ばれる
    public void ReaderFinish(System.Data.IDataReader reader)
    {
        stopwatch.Stop();
        sqlLogger.Trace(Newtonsoft.Json.JsonConvert.SerializeObject(new
        {
            date = DateTime.Now,
            command = ExecuteType.Reader,
            key = commandText,
            ms = stopwatch.ElapsedMilliseconds
        }, Newtonsoft.Json.Formatting.None));
    }
}

これで、

{"date":"2013-07-15T18:24:17.4465207+09:00","command":"Reader","key":"select * from hogemoge where id = @id","ms":6}

のようなデータが取れます。パラメータの値も展開したい!とかいう場合は自由にcommandのとこから引っ張れば良いでしょう。更に、MiniProfiler.Currentと共存したいような場合は、合成するIDbProfilerを用意すればなんとかなる。Time的には若干ずれますが、そこまで問題でもないかしらん。

public class CompositeDbProfiler : IDbProfiler
{
    readonly IDbProfiler[] profilers;

    public CompositeDbProfiler(params IDbProfiler[] dbProfilers)
    {
        this.profilers = dbProfilers;
    }

    public void ExecuteFinish(IDbCommand profiledDbCommand, ExecuteType executeType, DbDataReader reader)
    {
        foreach (var item in profilers)
        {
            if (item != null && item.IsActive)
            {
                item.ExecuteFinish(profiledDbCommand, executeType, reader);
            }
        }
    }

    public void ExecuteStart(IDbCommand profiledDbCommand, ExecuteType executeType)
    {
        foreach (var item in profilers)
        {
            if (item != null && item.IsActive)
            {
                item.ExecuteStart(profiledDbCommand, executeType);
            }
        }
    }

    public bool IsActive
    {
        get
        {
            return true;
        }
    }

    public void OnError(IDbCommand profiledDbCommand, ExecuteType executeType, Exception exception)
    {
        foreach (var item in profilers)
        {
            if (item != null && item.IsActive)
            {
                item.OnError(profiledDbCommand, executeType, exception);
            }
        }
    }

    public void ReaderFinish(IDataReader reader)
    {
        foreach (var item in profilers)
        {
            if (item != null && item.IsActive)
            {
                item.ReaderFinish(reader);
            }
        }
    }
}

といったものを用意しておけば、

var profiler = new CompositeDbProfiler(
    StackExchange.Profiling.MiniProfiler.Current,
    new TraceDbProfiler());

var conn = new ProfiledDbConnection(new SqlConnection("connectionString"), profiler); 

と、書けます。

SumoLogicによる分析

データ取るのはいいんだけど、それどーすんのー?って話なわけですが、以前にASP.NETでの定期的なモニタリング手法に少し出しましたけれど、弊社ではSumo Logicを利用しています。例えば、SQLで採取したログに以下のようクエリが発行できます。

これは10ミリ秒よりかかったDELETE文を集計、ですね。Sumoは結構柔軟なクエリで、ログのパースもできるんですが、最初からJSONで吐き出しておけばjsonコマンドだけでパースできるので非常に楽ちん。で、パース後は10msより上なら ms > 10 といった形でクエリ書けます。

問題があった時の分析に使ってもいいし、別途グラフ化も可能(棒でも円でも色々)されるので、幾つか作成してダッシュボードに置いてもいいし、閾値を設定してアラートメールを飛ばしてもいい。slow_logも良いし当然併用しますが、それとは別に持っておくと、柔軟に処理できて素敵かと思われます。

Redis

弊社ではキャッシュ層もMemcachedではなく、全てRedisを用いています。Redisに関しては、C#のRedisライブラリ「BookSleeve」の利用法を読んでもらいたいのですが、ともあれ、BookSleeveと、その上に被せているお手製ライブラリのCloudStructuresを使用しています。

実質的に開発者が触るのはCloudStructuresだけです。というわけで、CloudStructuresに用意してあるモニター用のものを使いましょう。というかそのために用意しました。まず、ICommandTracerを実装します。

public class RedisProfiler : ICommandTracer
{
    static readonly Logger redisLogger = NLog.LogManager.GetLogger("Redis");

    Stopwatch stopwatch;
    string command;
    string key;

    public void CommandStart(string command, string key)
    {
        this.command = command;
        this.key = key;
        stopwatch = Stopwatch.StartNew();
    }

    public void CommandFinish()
    {
        stopwatch.Stop();

        redisLogger.Trace(Newtonsoft.Json.JsonConvert.SerializeObject(new
        {
            date = DateTime.Now,
            command = command,
            key = key,
            ms = stopwatch.ElapsedMilliseconds
        }, Newtonsoft.Json.Formatting.None));

        // NewRelic使うなら以下のも。後で解説します。
        var ms = (long)System.Math.Round(stopwatch.Elapsed.TotalMilliseconds);
        NewRelic.Api.Agent.NewRelic.RecordResponseTimeMetric("Custom/Redis", ms);
    }
}

何らかのRedisへの通信が走る際にCommandStartとCommandFinishが呼ばれるようになってます。そして、RedisSettingsに渡してあげれば

// tracerFactoryにFuncを渡すか、.configに書くかのどちらかで指定できます
var settings = new RedisSettings("127.0.0.1", tracerFactory: () => new RedisProfiler());
            
// {"date":"2013-07-30T22:41:34.2669518+09:00","command":"RedisString.TryGet","key":"hogekey","ms":18}
var value = await new RedisString<string>(settings, "hogekey").GetValueOrDefault();

みたいになります。

CloudStructuresは、既に実アプリケーションに投下していて、凄まじい数のメッセージを捌いているので、割と安心して使っていいと思いますですよ。ServiceStack.Redisはショッパイけど、BookSleeveはプリミティブすぎて辛ぽよ、な方々にフィットするはずです。実際、C# 5.0と合わせた際のBookSleeveの破壊力は凄まじいので、是非試してみて欲しいですね。

New Relicによるグラフ化

Sumo Logicはいいんですけど、しかし、もう少し身近なところにも観測データを置いておきたい。そして見やすく。弊社ではモニタリングにNew Relicを採用していますが、そこに、そもそもSQLやHttpのカジュアルな監視は置いてあるんですね。なので、Redis情報も統合してあげればいい、というのが↑のNewRelicのAPIを叩いているものです。ただたんにNuGetからNewRelicのライブラリを持ってきて呼ぶだけの簡単さ。それだけで、以下の様なグラフが!

これはCall Countですが、他にAverageのResponse Timeなどもグラフ化してカスタムダッシュボードに置いています。

線が6本ありますが、これは用途によってRedisの台を分けているからです。例えばRedis.Cache, Redis.Session、のように。NewRelicのAPIを叩く際に、Custom/Redis/Cache、Custon/Redis/Sessionのようなキーのつけ方をすることで、個別に記録されます(それぞれのSettingsに個別のICommandTracerを渡しています)。ダッシュボードの表示時にCustom/Redis/*にするだけでひとまとめに表示できるから便利。

今のところ、Redisは全台平等に分散ではなく、グループ分け+負荷の高いものは複数台で構成しています。キャッシュ用途の台はファイルへのセーブなしで、完全インメモリ(Memcachedに近い)にしているなど、個別チューニングも入っています。

一番カジュアルに確認できるNew Relic、詳細な情報や解析クエリはSumo Logic。見る口が複数あるのは全然いいことです。

レスポンスタイム

HttpContextのTimestampに最初の時間が入っているので、Application_EndRequestで捕まえて差分を取ればかかった時間がサクッと。

protected void Application_EndRequest()
{
    var context = HttpContext.Current;
    if (context != null)
    {
        var responseTime = (DateTime.Now - context.Timestamp);
  
        // 解析するにあたってクエリストリングは邪魔なのでkeyには含めずの形で
        logger.Trace(Newtonsoft.Json.JsonConvert.SerializeObject(new
        {
            date = DateTime.Now,
            command = this.Request.Url.GetComponents(UriComponents.Path, UriFormat.Unescaped),
            key = this.Request.Url.GetComponents(UriComponents.Query, UriFormat.Unescaped),
            ms = (long)responseTime.TotalMilliseconds
        }, Newtonsoft.Json.Formatting.None));
    }
}

取れますね。

まとめ

改善は計測から!足元を疎かにして改善もクソもないのです。そして、存外、当たり前のようで、当たり前にはできないのね。また、データは取るだけじゃなく、大事なのは開発メンバーの誰もが見れる場所にあるということ。いつでも。常に。そうじゃないと数字って相対的に比較するものだし、肌感覚が養えないから。

弊社では、簡易なリアルタイムな表示はMiniProfilerとビュー統合のログ表示。実アプリケーションでは片っ端から収集し、NewRelicとSumoLogicに流しこんで簡単に集計・可視化できる体制を整えています。実際、C#移行を果たしてからの弊社のアプリケーションは業界最速、といってよいほどの速度を叩きだしています。基礎設計からガチガチにパフォーマンスを意識しているから、というのはもちろんあるのですが(そしてC# 5.0の非同期がそれを可能にした!)、現在自分が作っているものがどのぐらいのパフォーマンスなのか、を常に意識できる状態に置けたことも一因ではないかな、と考えています。(ただし、.NET最先端技術によるハイパフォーマンスウェブアプリケーションで述べましたが、そのためには開発環境も本番と等しいぐらいのネットワーク環境にしてないとダメですよ!)

私は今年は、言語や設計などの小さな優劣の話よりも、実際に現実に成功させることに重きを置いてます。C#で素晴らしい成果が出せる、その証明を果たしていくフェーズにある。成果は出せるに決まってるでしょ、と、仮に理屈では分かっていても、しかしモデルケースがなければ誰もついてこない。だから、そのための先陣を切っていきたい。勿論、同時に、成果物はどんどん公開していきます。C#が皆さんのこれからの選択肢の一番に上がってくれるといいし、また、C#といったらグラニ、となれるよう頑張ります。

ASP.NETでの定期的なモニタリング手法

cron的な定期実行といったら、タスクスケジューラ使え。完。なわけですが、それとは別にして、アプリケーションサーバー内部からしか分からない情報を定期的に吐き出したいようなシチュエーションにどうしましょうか?例えばスレッドプールの情報!かなり古いのですがHow To 情報: カスタム カウンタを使った ASP.NET スレッド プールの監視方法なんて、まずレジストリに登録して、そこから定期的に無限ループ+Thread.Sleep(ダセぇ)で出力という、なんともトホホな感じ。いや、これトホホでしょう。というわけで、もっとモダンにいきましょう。

IHttpModuleとInit, Dispose

カスタム HTTP モジュールを作成および登録するということで、IHttpModuleを作成することで、ASP.NETパイプライン上での各イベント時に実行されるものを追加していくことができます。Global.asax.csに直書きでもいいですが、こっちのほうが分離されてる感はありますにぇ。さて、通常はapplication.BeginRequest+=とか、イベント登録するんですが、Application_Startイベントに相当するものは……ありません。はい。ただしかわりにInitメソッドがあります。普段はイベント登録しますが、ここでメソッド実行すれば、それApplication_Startに等しいよねー、と思っていた時もありました。

public class CountModule : IHttpModule
{
    public static int Count = 0;

    public void Init(HttpApplication context)
    {
        Interlocked.Increment(ref Count);
    }
}

これ、ブレークポイントを張って様子みたり、Countの値を表示して見たりするとわかりますが、何度も呼ばれます。何度も何度も。どーいうことかというと、Application_StartとInitは等しくないです。ASP.NET Runtimeは複数のアプリケーションプールを作り、それごとにInitは呼ばれてるんですね。じゃあ、どうするか、というと……

public class CallOnceModule : IHttpModule
{
    static int initializedModuleCount;

    public void Init(HttpApplication context)
    {
        var count = Interlocked.Increment(ref initializedModuleCount);
        if (count != 1) return;

        // ここに本体書く
    }

    public void Dispose()
    {
        var count = Interlocked.Decrement(ref initializedModuleCount);
        if (count == 0)
        {
            // ここに本体書く
        }
    }
}

Initが呼ばれた回数を取れば、正しく1回になります。ちなみに属性を張るだけで、Application_Startっぽく呼び出されるメソッドを作れるWebActivatorも、似たような感じの仕組みです。

Timer

IHttpModuleの話はこのぐらいにして、本体の話にいきましょう。定期的に、例えば1分間隔に、とかは、Timer使えばいいんですよ、Timer。あ、Timerは幾つかありますが、System.Threading.Timerのほうね。

/// <summary>
/// 1分間隔でThreadInfoをログ取りするモジュール
/// </summary>
public class ThreadInfoLoggingModule : IHttpModule
{
    static NLog.Logger logger = NLog.LogManager.GetLogger("ThreadInfo");
    static NLog.Logger classLogger = NLog.LogManager.GetCurrentClassLogger();
    static int initializedModuleCount;
    static Timer timer;

    public void Init(HttpApplication context)
    {
        var count = Interlocked.Increment(ref initializedModuleCount);
        if (count != 1) return;

        timer = new Timer(_ =>
        {
            try
            {
                var date = DateTime.Now;

                int availableWorkerThreads, availableCompletionPortThreads;
                ThreadPool.GetAvailableThreads(out availableWorkerThreads, out availableCompletionPortThreads);

                int maxWorkerThreads, maxCompletionPortThreads;
                ThreadPool.GetMaxThreads(out maxWorkerThreads, out maxCompletionPortThreads);

                using (var sw = new System.IO.StringWriter())
                using (var jw = new Newtonsoft.Json.JsonTextWriter(sw))
                {
                    jw.Formatting = Newtonsoft.Json.Formatting.None;

                    jw.WriteStartObject(); // {

                    jw.WritePropertyName("date");
                    jw.WriteValue(date);
                    jw.WritePropertyName("availableWorkerThreads");
                    jw.WriteValue(availableWorkerThreads);
                    jw.WritePropertyName("availableCompletionPortThreads");
                    jw.WriteValue(availableCompletionPortThreads);
                    jw.WritePropertyName("maxWorkerThreads");
                    jw.WriteValue(maxWorkerThreads);
                    jw.WritePropertyName("maxCompletionPortThreads");
                    jw.WriteValue(maxCompletionPortThreads);

                    jw.WriteEndObject(); // }

                    jw.Flush();

                    var message = sw.ToString();
                    logger.Trace(message);
                }
            }
            catch (Exception ex)
            {
                classLogger.ErrorException("ThreadInfoLogging encounts error", ex);
            }
        }, null, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1));
    }


    public void Dispose()
    {
        var count = Interlocked.Decrement(ref initializedModuleCount);
        if (count == 0)
        {
            var target = Interlocked.Exchange(ref timer, null);
            if (target != null)
            {
                target.Dispose();
            }
        }
    }
}

なにをやっているか。よーするに、Timerで1分置きにロガーでJSONを吐き出してます。ロガーは謎社ではNLogを使ってます。というかこのModuleでは謎社のプロダクション環境で動いてるものです。なお、JsonTextWriter使ってるところは、別に普通にSerializeで構いませんですよ、なんとなく手書きしちゃっただけなので。

というわけで、これでThreadPoolの情報が取れました。やったね!あとはJSONなので好きな様にゴリゴリすればいいんですが、賢く解析したいなら、とりあえず謎社ではSumo Logicを使っています。Next Generation Log Management & Analyticsということで、集計ツールと解析ウェブアプリをワンセットで提供してくれてます。

独自のクエリ言語でガッと解析してグラフ化できてる、ってのは伝わるでしょうか?ふいんきね。便利そう、とか思ってもらえれば。クエリと可視化、更に閾値による通知など、色々できちゃいます。こいつぁイイね?←ちなみに、私はまだ全然クエリ書けないので、これは謎社の誇るPowerShellマスターが用意してくれました。

(ここではparseが手書きチックですが、実際にJSONをSumoでパースする際はJsonコマンドが用意されているので、それを使えばもっと綺麗にparseできます)

BookSleeveのMonitor

ついでに、謎社ではRedisを多用しているんですが、RedisライブラリであるBookSleeveは、GetCountersというメソッドで、それぞれのRedisConnectionの情報を吐き出すことができます。これを1分置きに、ThreadInfoと同様に吐き出すようにしてます。

/// <summary>
/// 1分間隔でRedis(BookSleeve)のCounterをログ取りするモジュール
/// </summary>
public class RedisCounterLoggingModule : IHttpModule
{
    static NLog.Logger logger = NLog.LogManager.GetLogger("RedisCounter");
    static NLog.Logger classLogger = NLog.LogManager.GetCurrentClassLogger();
    static int initializedModuleCount;
    static Timer timer;

    public void Init(HttpApplication context)
    {
        var count = Interlocked.Increment(ref initializedModuleCount);
        if (count != 1) return;

        timer = new Timer(_ =>
        {
            try
            {
                var date = DateTime.Now;

                // ここは謎社Internalな部分なのでテキトーにスルーしてくださいな 
                var query = Grani.Core.GlobalConfig.RedisGroupDictionary
                    .SelectMany(x => x.Value.Settings, (x, settings) => new { x.Value.GroupName, settings });
                foreach (var item in query)
                {
                    var connection = item.settings.GetConnection();
                    var counters = connection.GetCounters();

                    using (var sw = new System.IO.StringWriter())
                    using (var jw = new Newtonsoft.Json.JsonTextWriter(sw))
                    {
                        jw.Formatting = Newtonsoft.Json.Formatting.None;

                        jw.WriteStartObject(); // {

                        jw.WritePropertyName("date");
                        jw.WriteValue(date);

                        jw.WritePropertyName("GroupName");
                        jw.WriteValue(item.GroupName);
                        jw.WritePropertyName("Host");
                        jw.WriteValue(item.settings.Host + ":" + item.settings.Port);
                        jw.WritePropertyName("Db");
                        jw.WriteValue(item.settings.Db);

                        jw.WritePropertyName("MessagesSent");
                        jw.WriteValue(counters.MessagesSent);
                        jw.WritePropertyName("MessagesReceived");
                        jw.WriteValue(counters.MessagesReceived);
                        jw.WritePropertyName("MessagesCancelled");
                        jw.WriteValue(counters.MessagesCancelled);
                        jw.WritePropertyName("Timeouts");
                        jw.WriteValue(counters.Timeouts);
                        jw.WritePropertyName("QueueJumpers");
                        jw.WriteValue(counters.QueueJumpers);
                        jw.WritePropertyName("Ping");
                        jw.WriteValue(counters.Ping);
                        jw.WritePropertyName("SentQueue");
                        jw.WriteValue(counters.SentQueue);
                        jw.WritePropertyName("UnsentQueue");
                        jw.WriteValue(counters.UnsentQueue);
                        jw.WritePropertyName("ErrorMessages");
                        jw.WriteValue(counters.ErrorMessages);
                        jw.WritePropertyName("SyncCallbacks");
                        jw.WriteValue(counters.SyncCallbacks);
                        jw.WritePropertyName("AsyncCallbacks");
                        jw.WriteValue(counters.AsyncCallbacks);
                        jw.WritePropertyName("SyncCallbacksInProgress");
                        jw.WriteValue(counters.SyncCallbacksInProgress);
                        jw.WritePropertyName("AsyncCallbacksInProgress");
                        jw.WriteValue(counters.AsyncCallbacksInProgress);
                        jw.WritePropertyName("LastSentMillisecondsAgo");
                        jw.WriteValue(counters.LastSentMillisecondsAgo);
                        jw.WritePropertyName("LastKeepAliveMillisecondsAgo");
                        jw.WriteValue(counters.LastKeepAliveMillisecondsAgo);
                        jw.WritePropertyName("KeepAliveSeconds");
                        jw.WriteValue(counters.KeepAliveSeconds);
                        jw.WritePropertyName("State");
                        jw.WriteValue(counters.State.ToString());

                        jw.WriteEndObject(); // }

                        jw.Flush();

                        var message = sw.ToString();
                        logger.Trace(message);
                    }
                }
            }
            catch (Exception ex)
            {
                classLogger.ErrorException("RedisCounterLogging encounts error", ex);
            }
        }, null, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1));
    }

    public void Dispose()
    {
        var count = Interlocked.Decrement(ref initializedModuleCount);
        if (count == 0)
        {
            var target = Interlocked.Exchange(ref timer, null);
            if (target != null)
            {
                target.Dispose();
            }
        }
    }
}

RedisGroupとかコネクション管理はBookSleeveの上に被せてるCloudStructuresによるものです(プロダクション環境でヘヴィに使ってますよ!)。CloudStructuresの使い方とかもまたそのうち。

まとめ

とまぁ、そんなふうにして色々データ取ってます。改善の基本はデータ取りから。色々なところからデータ取ってチェック取れるような体制を整えています。次回は、SQLやHttp、Redisの実行時間をどう取得するかについてお話しましょふ。たぶんね。きっと。

ところで、プロダクション環境下で――と書いているように、謎社のアプリケーションは完全にC#に移行しました。結果ですが、最先端環境で練り上げたC#によるウェブアプリケーションは、超絶速い。しかも、完全にAWSクラウドに乗っけての話ですからね、オンプレミスでのスペシャルなマシンやFusion-ioなDBでやってるわけじゃなく、成果出せてる。

Sumo LogicやNew Relicなど外部サービスの活用やRedisの使い倒しかた、非同期処理の塊、などなど、次世代のC#ウェブアプリケーションのスタンダードというものを示せたのではないかな、と思っています。詳しい話はそのうちまたどこかで発表したいとは思うので待っててください。

C#で扱うRedisのLuaスクリプティング

Redis 2.6からLuaスクリプティングが使えるようになりました。コマンドはEVALです。というわけでC#のRedisライブラリ、BookSleeveで、試してみましょう。RedisやBookSleeveに関しては、以前に私がBuildInsiderで書いたC#のRedisライブラリ「BookSleeve」の利用法を参照ください。

BookSleeveは当然NuGet経由で入れるとして、Windows版のRedisバイナリもNuGetで配布されています。手軽に試してみるなら、Install-Package Redis-64が良いのではないでしょーか。現在の最新は2.6.12.1ということで、Evalにも対応しています。インストールするとpackages\Redis-64.2.6.12.1\toolsにredis-server.exeが転がっているので、それを起動すれば、とりあえず127.0.0.1:6379で動きます。

多重アクセスの検出

HelloWorld!ということで、多重アクセス検知のスクリプトでも書いてみます。ルールとしては、X秒以内にY回アクセスしてきた人間はZ秒アク禁にする。という感じですね。DOSアタック対策的な。LUAスクリプティングを使わないと、キーを2つ用意したりしなけりゃいけなかったり複数コマンド打ったりしたりとか、若干面倒だったり効率悪いのですが、スクリプティング使えば一発で済ませられます。

    public static class RedisExtensions
    {
        public static async Task<bool> DetectAttack(this RedisConnection redis, int db, string key, int limitCount = 10, int durationSecond = 1, int bannedSecond = 300)
        {
            var result = await redis.Scripting.Eval(db, @"
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local count = redis.call('incr', key)
if(count >= limit) then
    local banSec = tonumber(ARGV[3])
    redis.call('EXPIRE', key, banSec)
    return true
else
    local expireSec = tonumber(ARGV[2])
    redis.call('EXPIRE', key, expireSec)
    return false
end", new[] { key }, new object[] { limitCount, durationSecond, bannedSecond }).ConfigureAwait(false);

            // Lua->Redisはtrueの時に1を、falseの時にnullを返す
            return (result == null) ? false
                : ((long)result == 1) ? true
                : false;
        }
    }

こんな感じですね。基本的にはEvalメソッドでスクリプトを渡すだけです、あとKEYS配列とARGV配列を必要ならば。戻り値の扱いなどに若干のクセがありますので、その辺はRedisのEVALのドキュメントを読んでおくといいでしょう。

スクリプトは、まずincrを呼んでカウントを取る。そのカウントが指定数を超えてたらExpireの時間をBanの時間(デフォは300秒=5分)引き伸ばす。超えてなければ、Expireの時間を指定間隔(デフォは1秒)だけ伸ばす。もし1秒以内に連続でアクセスがあれば、Incrのカウントが増えていく。1秒以上経過すればExpireされているので、countは0スタートになる。といった感じです。

利用する場合はこんな具合。

var redis = new RedisConnection("127.0.0.1");
await redis.Open();

var v = await redis.DetectAttack(0, "hogehoge");
Console.WriteLine(v); // false

for (int i = 0; i < 15; i++)
{
    var v2 = await redis.DetectAttack(0, "hogehoge");
    Console.WriteLine(v2); // false,false,...,true,true
}

いい具合ですにぇ?

EVALSHA

BookSleeveのEvalは、正確にはEVALSHAです(更に正しくはデフォルトの、引数のuseCacheがtrueの場合)。

EVALSHAは、事前にスクリプトのSHA1を算出し、初回に登録しておくことで、コマンドの転送をSHA1の転送だけで済ませます。スクリプトを毎回投げていたらコマンド転送に時間がかかるので、それの節約です。この辺をBookSleeveは何も意識しなくても、やってくれるのが非常に楽ちん。素晴らしい。

Increment/DecrementLimit

せっかくなので、もう一つ例を。RedisのIncrementやDecrementはアトミックな操作で非常に使いやすいのですが、上限や下限を設けたい場合があります。例えば、HPは0以下になって欲しくないし、最大HPを超えて回復されても困る、みたいな。それも当然、Luaスクリプティングを使えば簡単に実現可能です。

        public static async Task<long> IncrementWithLimit(this RedisConnection redis, int db, string key, long value, long maxLimit)
        {
            var result = await redis.Scripting.Eval(db, @"
local inc = tonumber(ARGV[1])
local max = tonumber(ARGV[2])
local x = redis.call('incrby', KEYS[1], inc)
if(x > max) then
    redis.call('set', KEYS[1], max)
    x = max
end
return x", new[] { key }, new object[] { value, maxLimit }).ConfigureAwait(false);
            return (long)result;
        }

incrbyの結果が指定の値を超えていたら、setで固定する、といった感じです、単純単純。使うときはこんな具合。

var redis = new RedisConnection("127.0.0.1");
await redis.Open();

var v1 = await redis.IncrementWithLimit(0, "hoge", 40, maxLimit: 100);
var v2 = await redis.IncrementWithLimit(0, "hoge", 40, maxLimit: 100);
var v3 = await redis.IncrementWithLimit(0, "hoge", 40, maxLimit: 100);

// 40->80->100
Console.WriteLine(v1 + "->" + v2 + "->" + v3);

楽ちん、これは捗る。

まとめ

というわけで、RedisいいよRedis。いやほんと色々な面で使ってて嬉しいことが多いです。RDBMSだけで頑張ると非常に辛ぽよ、Redisがあるだけで何かと楽になれますので、一家に一台は置いておきたい。

Luaスクリプティングは複数コマンド間で戻り値が扱えるため、利用範囲がグッと広がります。そしてスクリプティング中の動作もまたアトミックである、というのが嬉しい点です(C#コード上で複数コマンドを扱うと、そこの保証がないというのが大きな違い)。と同時に注意しなければならないのは、アトミックなので、スクリプト実行中は完全にブロックされてます。ので、あまりヘヴィなことをLuaスクリプティングでやるのは避けたほうがいいのではないかなー、と思われます。

asyncの落とし穴Part2, SynchronizationContextの向こう側

非同期QUIZの時間がやってきました!前回はデッドロックについてでしたが、今回はヌルポについて扱いましょう。まずは以下のコードの何が問題なのかを当ててください。ASP.NET MVCです。あ、.NET 4.5ね。

public class HomeController : Controller
{
    async Task DoAsync()
    {
        await Task.Delay(TimeSpan.FromSeconds(3));
    }

    public ActionResult Index()
    {
        DoAsync();
        return View();
    }
}

どこがダメで、どうすれば改善されるのかはすぐ分かると思います。「なにが起こるのか」「なぜ起こるのか」について、考えてみてください。おわり。

さて、で、Ctrl+F5で実行すると、このコードは何の問題もなくふとぅーに動きます。一見何の問題もない。実際何の問題もない。オシマイ。

というのもアレなので、何が起こっているのか観測します。まず、Global.asax.csに以下のコードを。

protected void Application_Start()
{
    // ルーターの登録とか標準のものがこの辺に

    Trace.Listeners.Add(new TextWriterTraceListener(@"D:\log.txt"));
    Trace.AutoFlush = true;

    System.Threading.Tasks.TaskScheduler.UnobservedTaskException += (sender, e) =>
    {
        Trace.WriteLine(e.Exception);
    };
}

で、本体はこんな風に。

public class HomeController : Controller
{
    async Task DoAsync()
    {
        Trace.WriteLine("start"); // 何か開始処理があるのだとする

        await Task.Delay(TimeSpan.FromSeconds(3)); // 何か非同期処理してるとする

        Trace.WriteLine("end"); // 何か後処理があるのだとする
    }

    public ActionResult Index()
    {
        GC.Collect(); // GC自然発生待ちダルいので発動しちゃう

        var _ = DoAsync(); // 非同期処理を"待たない"
        return View();
    }
}

D:\log.txtは、まぁどこに吐いてもいいんですが、ちゃんと書き込み権限があるところに。んでは、実行しましょう。log.txtは、初回はまず、「start」と書かれます。つまり、endまで到達してないことが確認できます。二回目のアクセスではGC.Collectが走り、それによりUnobservedTaskExceptionが実行されます。で、log.txtには以下のものが書き込まれます。

System.AggregateException: タスクの例外が、タスクの待機によっても、タスクの Exception プロパティへのアクセスによっても監視されませんでした。その結果、監視されていない例外がファイナライザー スレッドによって再スローされました。 ---> System.NullReferenceException: オブジェクト参照がオブジェクト インスタンスに設定されていません。
   場所 System.Web.ThreadContext.AssociateWithCurrentThread(Boolean setImpersonationContext)
   場所 System.Web.HttpApplication.OnThreadEnterPrivate(Boolean setImpersonationContext)
   場所 System.Web.HttpApplication.System.Web.Util.ISyncContext.Enter()
   場所 System.Web.Util.SynchronizationHelper.SafeWrapCallback(Action action)
   場所 System.Web.Util.SynchronizationHelper.<>c__DisplayClass9.<QueueAsynchronous>b__7(Task _)
   場所 System.Threading.Tasks.ContinuationTaskFromTask.InnerInvoke()
   場所 System.Threading.Tasks.Task.Execute()
   --- 内部例外スタック トレースの終わり ---
---> (内部例外 #0) System.NullReferenceException: オブジェクト参照がオブジェクト インスタンスに設定されていません。
   場所 System.Web.ThreadContext.AssociateWithCurrentThread(Boolean setImpersonationContext)
   場所 System.Web.HttpApplication.OnThreadEnterPrivate(Boolean setImpersonationContext)
   場所 System.Web.HttpApplication.System.Web.Util.ISyncContext.Enter()
   場所 System.Web.Util.SynchronizationHelper.SafeWrapCallback(Action action)
   場所 System.Web.Util.SynchronizationHelper.<>c__DisplayClass9.<QueueAsynchronous>b__7(Task _)
   場所 System.Threading.Tasks.ContinuationTaskFromTask.InnerInvoke()
   場所 System.Threading.Tasks.Task.Execute()<---

おぅ!例外が発生していた!ぬるり!ぬるり!

GC.Collectを実行している理由は、Taskに溜まった未処理例外は、GCが走ったタイミングでUnobservedTaskExceptionに渡されるので、それを待つ時間を短縮しているだけです。GC.Collectを明示的に実行しなくても、長く動かしてればそのうち発生します。

というわけで、何が起こるのか、というと、await Task.Delayのところでヌルリが発生します。↑の例外情報からは、どこで発生していたのかの情報が一切出てこないので(さすがにこれなんとかして欲しいですけどねぇ……)、いざ発生するとなると場所を突き止めるのに割と苦労します、というか虱潰ししかないので結構大変です。そもそもUnobservedTaskExceptionをモニタしてなければ、発生していたことにすら気づけません。

なぜ起こるのか、というと、awaitによってPOSTする先のContextが消滅しているからです。非同期処理を待たなかったことによって、Viewの表示まで全て完了してContextが消滅する。その後で、DoAsync内のawaitが完了し、続行しようとPOSTを開始する、と、しかしContextは消滅していてなにもなーい。ので、ぬるり。

では、解決方法は、というと、例によってasyncで統一するか

public class HomeController : Controller
{
    async Task DoAsync()
    {
        Trace.WriteLine("start"); // 何か開始処理があるのだとする

        await Task.Delay(TimeSpan.FromSeconds(3)); // 何か非同期処理してるとする

        Trace.WriteLine("end"); // 何か後処理があるのだとする
    }

    public async Task<ActionResult> Index()
    {
        GC.Collect();

        await DoAsync(); // awaitする
        return View();
    }
}

もしくはConfigureAwait(false)でContextを維持しないこと。

public class HomeController : Controller
{
    async Task DoAsync()
    {
        Trace.WriteLine("start"); // 何か開始処理があるのだとする

        await Task.Delay(TimeSpan.FromSeconds(3)).ConfigureAwait(false); // ConfigureAwait(false)する

        Trace.WriteLine("end"); // 何か後処理があるのだとする
    }

    public ActionResult Index()
    {
        GC.Collect();

        var _ = DoAsync(); // 非同期処理を"待たない"
        return View();
    }
}

です。

そもそも、何故非同期処理を"待たない"のか。例えば、アクセスログを取るために記録するだけだとか、別に完了を待つ必要がないものだったりするなら、待たないことでレスポンスは速くなる。待つ必要ないのなら、待たなくてもいいぢゃない。それはそうです。

なので、待たないなら待たないでいいのですが、中身について用心しないと、ヌルりで死んでしまいます。これは、同期的に待つ時もそうですね。待つなら待つでいいですけれど、中身について用心しないと、デッドロックで死んでしまいます。待っても死亡、待たなくても死亡、ホント非同期は地獄だぜ!

まあ、変数で受けたりしない限りは警告は出してくれますので(ウザいと思っていたアナタ!実に有益な警告ではないですか!)、不注意による死亡はある程度は避けられはします。

.NET 4.0 vs .NET 4.5

.NET 4.5だと、↑のような挙動ですが、.NET 4.0だとちょっと事情が違ったりします。async/awaitは利用したままで、ターゲットフレームワークのバージョンだけ4.0にしましょう。

<system.web>
    <httpRuntime targetFramework="4.0" />
    <compilation debug="true" targetFramework="4.0" />
</system.web>

で、Ctrl+F5で実行して、何度かブラウザをリロードしましょう。死んでます。IIS Expressが。完全に無反応になります。何故?Windowsのイベントビューアーを見ましょう。

ハンドルされない例外のため、プロセスが中止されました。というわけで、未処理例外が突き抜けてアプリケーションエラーとして記録されていくためです。プロダクション環境でもIISのラピッドフェール保護が発動して、デフォルトでは5分以内に5エラーでアプリケーションは停止します。これは実にクリティカル。

なんで.NET 4.0と4.5で挙動が違うのか、というと、Taskの未処理例外の扱いが4.0と4.5で変わったためです。この辺はPfxTeamのTask Exception Handling in .NET 4.5を参照にどーぞ。4.5のほうが安全っちゃー安全ですね。いずれにせよ、UnobservedTaskExceptionの例外ロギングは欠かさずやっておきましょう。

まとめ

非同期もいいんですけど、実際にマジでフルに使い出すと結構なんだかんだでハマりどころは多いですねぇ。幸い、デバッガビリティに関してはWindows 8.1 + Visual Studio 2013である程度改善するようで、待ち遠しいです。とはいえデッドロックだったりコンテキスト場外でヌルりだとかは、注意するしかない。

ASP.NET MVCのフィルターはやく非同期に対応してくださいー。ASP.NET MVC 5でも予定に入ってないようでどうなってんだゴルァ。Resultで待つしかなくて非常にヒヤヒヤします。EF6も非同期対応とか、そもそもMVC 5では.NET 4.5からのみだとか、どんどん非同期使われてくにつれ、死亡率も間違いなく上がってきますにゃ。

Micro-ORMとテーブルのクラス定義自動生成について

謎社のデータアクセスはMicro-ORMでやっています。生SQL書いて、シンプルなPOCOにマッピングするだけの。ですが、そこで困るのはPOCOの作成。データベースの写しなだけのクラスですが、手で作るには、ひじょーに面倒。Entity Frameworkならドラッグアンドドロップで!DataSetですらホイホイと作れるのに、100%手作業とか嫌だよー、200テーブルを延々とクラス作るだけの刺身たんぽぽなんてしてたら死んじゃうよー。

というわけで、Micro-ORM使うなら避けては通れない定義。EFのクラス定義だけ流用しちゃうとか色々と逃げ道も考えられなくもないですが、もしくは数によっては手動で頑張ってしまうのも手ですが、ここは自動生成しましょうの会。

GetSchema

普通にSQLのクエリを書いてデータベースの情報を取ってくることも可能ですが、各データベースでそれぞれバラバラだったりするので、ここはADO.NETで用意されているGetSchemaを使いましょう。情報取得の部分が抽象化されていて、型無しDataTableとして受け取ることが可能です。

using (var conn = new MySqlConnection("接続文字列。MySQLでもなんでもいいよ。"))
{
    conn.Open();

    var schema = conn.GetSchema();
}

さて、schemaとやらをデバッガで見てみるとですね……

うおおお、これがDataTableか!な、なんだ、このデバッガビリティの低さは……。これはヤヴァい。マジキチ。RowsのKeyを辿るのも苦労するうえに、Valueが一覧で見れない。頑張ってもKeyだけ。なんだこりゃ。データの取得もLINQ to DataSet(笑)によって、普通に実に扱いづらい。話にならない。 クソが。というわけで、今時ならば型無しDataTableはdynamicで扱ったほうが楽です。ExpandoObjectに変換しましょう。

public static class DataTableExtensions
{
    /// <summary>DataTableの各RowをExpandoObjectに変換します。</summary>
    public static IEnumerable<dynamic> AsDynamic(this DataTable table)
    {
        return table.AsEnumerable().Select(x =>
        {
            IDictionary<string, object> dict = new ExpandoObject();
            foreach (DataColumn column in x.Table.Columns)
            {
                var value = x[column];
                if (value is System.DBNull) value = null;
                dict.Add(column.ColumnName, value);
            }
            return (dynamic)dict;
        });
    }
}

こんなものを用意すると、 var schema = conn.GetSchema().AsDynamic() とするだけで

うおおおおおおお、超捗る!ちゃんと動的ビューでKeyとValueが見える!ExpandoObjectありがとう。DataTableは死ね。また、DBNullをフツーのnullに変換したりなどもしているので、データを触るのもかなり捗るといったところもあります。item.Field<string>("CollectionName")と書くよりも、item.CollectionNameって書きたいですから。

では、気を取り直してこれで解析していきましょう。まずは、件のCollectionNameを見てみますか。

// MetaDataCollections
// DataSourceInformation
// DataTypes
// Restrictions
// ReservedWords
// Databases
// Tables
// Columns
// Users
// Foreign Keys
// IndexColumns
// Indexes
// Foreign Key Columns
// UDF
// Views
// ViewColumns
// Procedure Parameters
// Procedures
// Triggers
foreach (var item in conn.GetSchema().AsDynamic())
{
    Console.WriteLine(item.CollectionName);
}

MySQLでは以上のデータが取れるようです。この辺は使ってるデータベースによってかなり変わるので、適宜調べながら合わせてみてくださいな。というわけで、それっぽそうなTablesを見てみます。var tables = conn.GetSchema("Tables").AsDynamic(); とすれば

テーブル一覧が取れるようです。で、しかし、今回必要なのはTablesではありません。TablesはほんとーにTableのデータだけなので。今回必要なのは、Columnsです。

var columns = conn.GetSchema("Columns").AsDynamic()
    .GroupBy(x => x.TABLE_NAME) // 全てのカラムが平らに列挙されてくるのでテーブル名でグルーピング
    .Select(g => new
    {
        ClassName = g.Key, // クラス名はテーブル名(= グルーピングのキー)
        Properties = g
            .OrderBy(x => x.ORDINAL_POSITION) // どんな順序で来るか不明なので、カラム定義順にきちんと並び替え
            .Select(x => new
            {
                Name = x.COLUMN_NAME,
                Type = x.DATA_TYPE
            })
            .ToArray()
    });

良い感じに作れてきました。さて、クラスを自動生成するのに必要なのは「クラス名」「プロパティ名」「プロパティの型」です。DATA_TYPEだとDBの生の型名、bigintとかvarcharとかC#のデータ型じゃないよー。なので、このまんまじゃダメです。

というわけで、マッピングを用意してあげます。といっても手動でやる必要はなくて、これはGetSchema("DataTypes")で取れます。

// 型名を決めるのに必要なのは
// TypeName(MySQLの型名), DataType(.NETの型名), IsUnsigned
var typeDictionary = conn.GetSchema("DataTypes").AsDynamic()
    .ToDictionary(x =>
        Tuple.Create((string)x.TypeName.ToLower(), (bool?)x.IsUnsigned ?? false),
        x => (Type)Type.GetType(x.DataType));

// MySQLのtinyintはboolとして使われることが多いので、そちらにマッピングしちゃう(不要ならしなくていいです)
typeDictionary[Tuple.Create("tinyint", true)] = typeof(bool);
typeDictionary[Tuple.Create("tinyint", false)] = typeof(bool);

// nullableの場合を考慮する必要があるのでTypeDictionaryは生では使わない
Func<string, bool, bool, string> getTypeName = (dataType, isUnsigned, isNullable) =>
{
    var type = typeDictionary[Tuple.Create(dataType.ToLower(), isUnsigned)];
    return (isNullable && type.IsValueType)
        ? type.Name + "?" // 値型かつnull許可の時
        : type.Name;
};

TypeName(MySQLの型名), DataType(.NETの型名), IsUnsigned。それにNullableへの対応を組み合わせれば、マッピングできると考えられます。GetSchemaからの情報だけだとNullable対応が苦しくなるので、外にメソッド立てています。

さて、この型定義辞書を使って変換すると、以下のようになります。

var columns = conn.GetSchema("Columns").AsDynamic()
    .GroupBy(x => x.TABLE_NAME) // 全てのカラムが平らに列挙されてくるのでテーブル名でグルーピング
    .Select(g => new
    {
        ClassName = g.Key, // クラス名はテーブル名(= グルーピングのキー)
        Properties = g
            .OrderBy(x => x.ORDINAL_POSITION) // どんな順序で来るか不明なので、カラム定義順にきちんと並び替え
            .Select(x => new
            {
                Name = x.COLUMN_NAME,
                // unsignedの判定はCOLUMN_TYPEから、nullableの判定はYES/NOで行われる
                Type = getTypeName(x.DATA_TYPE, x.COLUMN_TYPE.Contains("unsigned"), x.IS_NULLABLE == "YES")
            })
            .ToArray()
    });

これで完璧!さて、定義の抽出はできたので、次はテンプレート作りにいきましょうか。

T4

(テキストとしての)C#コード生成は、C#コード上で文字列を切った貼ったする、わけは勿論ありません。この手の作業するときはテンプレートエンジンを使うのが良いでしょう。最近だとRazorを使ったRazorEngineなどもあるのですが、RazorはあくまでHTML/XMLを出力するのに向いている構文で、C#コードを出力するような用途で使うのは、あまり向いていません。ここは素直にVisual Studio標準のT4 Templateを使うのが良いでしょう。あえてStringTemplate.NETとか、他のを選ぶ理由は、ないかなぁ。T4でいいですよ。

T4にはVisual Studioと連携して保存時にテンプレートが当てはまったテキストを出力するタイプと、ふつーのクラスとして、実行時に任意の変数をあてて、テキストを生成するもののニタイプが選べます。このブログでも何度か紹介してきたのは、全て前者でしたが、今回は後者のパターンを使います。

昔の名前は「前処理されたテキストテンプレート」でした。VS2012から名前変わって「ランタイムテキストテンプレート」になったようです。見つからなかったら検索ウィンドウにT4と入れると良いですよ。では、まず、TableGeneratorTemplate.ttとしてテンプレートを定義します。

<#@ template language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

public class <#= ClassName #>
{
<# foreach(var x in Properties) {#>
    public <#= x.Type #> <#= x.Name #> { get; set; }
<# } #>

    public override string ToString()
    {
        return ""
<# foreach(var x in Properties) {#>
            + "<#= x.Name #> : " + <#= x.Name #> + "|"
<# } #>
            ;
    }
}

テンプレートの記法としては、#=で囲むだけなので、まぁそう難しいものでもないです、読みづらさはかなりありますが。さて、このままだとClassNameとかPropertiesとかいうのは未定義でコンパイルもできないので、パーシャルクラスを作ります。クラス名はテンプレートと同名で。

public partial class TableGeneratorTemplate
{
    public string ClassName { get; set; }
    public IEnumerable<dynamic> Properties { get; set; }

    public TableGeneratorTemplate(string className, IEnumerable<dynamic> properties)
    {
        this.ClassName = className;
        this.Properties = properties;
    }
}

これで、パラメータを渡せるようになりました。コンパイルエラーも出ません。というわけで実際に出力しましょう。テンプレートをnewして、TransformTextを呼ぶだけです。

// あ、ちなみにここまでのはConsoleApplicationでの話でした、はい。
// テーブル名.csにテンプレートを当てて全部出力
foreach (var item in columns)
{
    var tt = new TableGeneratorTemplate(item.ClassName, item.Properties);
    var text = tt.TransformText();
    File.WriteAllText(item.ClassName + ".cs", text, Encoding.UTF8);
}

これで、以下の様なファイルがテーブル数だけ出力されます。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

public class aiueo_test_table
{
    public Int64 id { get; set; }
    public String name { get; set; }
    public Int32 age { get; set; }
    public DateTime? created { get; set; }
    public DateTime? modified { get; set; }

    public override string ToString()
    {
        return ""
            + "id : " + id + "|"
            + "name : " + name + "|"
            + "age : " + age + "|"
            + "created : " + created + "|"
            + "modified : " + modified + "|"
            ;
    }
}

やったね!刺身たんぽぽさようなら!ちなみにただのプロパティの塊というだけじゃなく、ToStringも生成しておいてやると、実アプリでのデバッグの時に割と便利ですねー。

応用

GetSchemaで得られる情報は他にも沢山ありますので、Diffを取るプログラムを書けたり、あと、Index, IndexColumnsでインデックスの情報が取れます。インデックスで貼られているものはselectクエリーをほぼほぼ発行するはず、とみなせるので、selectクエリを発行するメソッドを自動生成しちゃったりとかは、実際に謎社ではしています。

つまり作業手順としては、どちらかというデータベースファーストになります。結局、コードとデータベースは違うので、データベース優先の定義・作業のほうが、どこまでいっても自然かな、と。(EF)コードファーストは私は幻想だと思っています。別に、SQL Server Management StudioなりHeidiSQLなどのツールでぽちぽち定義作るのは、そう面倒なわけでもない。そこからC#側のクラス定義も自動で生成できるのなら、むしろ、無理のあるコードでのデータベース表現をして回るよりも、結局、楽じゃない?DB定義をC#側から発行されて、どーのこーとか、なんてのに気を使わなくてもいいので、ずっと楽ちんだと思うんだ。原始的で全然スマートじゃないようで、実利はこっちにある。

まとめ

コードの詳細はMySQLなので、SQL Serverとかじゃ100%そのままは動かないかもですが、その辺は適宜調整してください、きっと似たようなのはあるはずなので。

あと、GetSchemaのDataTableで何が取れるのか、どんなフィールドがあるのかって、特にMySQLだとドキュメントゼロなのですが、そこでAsDynamicは本当に死ぬほど役に立ちました。Visual Studio上でのデバッガビリティを高めるの超大事。その辺がクソなのがDataTableの嫌なところですねえ。今時DataSetを使いまくってるレガシー会社とかあると悲しいですねえ。

ともあれ、dynamicはかなりデバッガビリティ高いので、活用してあげると良いです。dynamic、最近だと忘れ去られているC#の機能らしいので(笑)まあ、メインには使いませんけれど、あるとやっぱ便利なので、あって良かったなって、思いますよん。

AsyncOAuth ver.0.7.0 - HttpClient正式版対応とバグ修正

HttpClientがRTMを迎えた、と思ったら、次バージョンのベータが出た、と、展開早くて追いつけないよ~な感じですが、とりあえず正式版のほうを要求する形で、AsyncOAuthも今回からBetaじゃなくなりました。

AsyncOAuthについてはAsyncOAuth - C#用の全プラットフォーム対応の非同期OAuthライブラリを、HttpClientについてはHttpClient詳解、或いはAsyncOAuthのアップデートについてを参照ください。

今回のアップデートなのですが、PCL版HttpClientのRTM対応という他に、バグ修正が二点ほどあります。バグ的には、結構痛いところですね……。

OrderByと文字列について

バグ修正その一として、OAuthの認証シグネチャを作るのにパラメータを並び替える必用があるのですが、その並び順が特定条件の時に狂っていました。狂っている結果、正しく認証できないので、実行が必ず失敗します。特定条件というのは、パラメータ名が大文字始まりと小文字始まりが混在するときです。例えばhogeとHugaとか。本当はHuga-hogeにならなければならないのに、hoge-Hugaの順序になってしまっていたのでした。

なぜそうなったか、というと、OrderByをデフォルトで使っていたからです。

// charの配列をOrderByで並び替えると、ASCIIコード順 = 65:'A', 97:'a'
foreach (var item in new[] { 'a', 'A' }.OrderBy(x => x))
{
    Console.WriteLine((int)item + ":" + (char)item);
}

// stringの配列をOrderByで並び替えると、良い感じ順 = "a", "A"
foreach (var item in new[] { "a", "A" }.OrderBy(x => x))
{
    Console.WriteLine(item);
}

良い感じ順!というのはなにかというと、StringComparison/Comparer.CurrentCultureの順番です。ほぅ……。ありがたいような迷惑のような。で、今回はASCIIコードにきっちり従って欲しいので、

// 実際のコード。
// ところでrealmのところは!x.Key.Equals("realm", StringComparison.OrdinalIgnoreCase) って書くべき、次のバージョンで直します
var stringParameter = parameters
    .Where(x => x.Key.ToLower() != "realm")
    .Concat(queryParams)
    .OrderBy(p => p.Key, StringComparer.Ordinal)
    .ThenBy(p => p.Value, StringComparer.Ordinal)
    .Select(p => p.Key.UrlEncode() + "=" + p.Value.UrlEncode())
    .ToString("&");

といったように、StringComparer.OrdinalをOrderBy/ThenByに渡してあげることで解決しました。メデタシメデタシ。文字列の比較とか、言われてみれば基本中の基本っっっ!なのですけれど、完全に失念してました。テストでも、パラメータ小文字ばっかだったりして問題が中々表面化しないんですね、言い訳ですけれど……。しかし、幾つかのライブラリ見てみると、ほとんどがこれの対応できてなかったので私だけじゃないもん!みたいな、うう、情けないのでやめておきましょう。OAuthBase.csもissueには上がってましたがマージされてないんで、同様の問題抱えてるのですねえ。さすがにDotNetOpenAuthはきっちりできていました。

UriクラスとQueryとエスケープについて

バグ修正その2。GETにパラメータをくっつける、つまりクエリストリングとして並べた時に、日本語などURLエンコードが必要な物が混ざっている時に必ず認証に失敗しました。エンコード周りということで、お察しの通り二重エンコードが原因です。

これに関しては、私のUriクラスへの認識が甘々だったのがマズかったです。ぶっちけ、ただのstringを包んだだけの面倒臭い代物とか思ってたりとかしたりとか……。いや、さすがにそこまでではないんですが、まぁしかし甘々でした。こっちは↑のに比べても本当に初歩ミスすぎて穴掘って埋まりたいですぅー。相当恥ずかしい。

// クエリストリングつけたものを投げるとして
var uri = new Uri("http://google.co.jp/serach?q=つくば");

// QueryはURLエンコードされてる => ?q=%E3%81%A4%E3%81%8F%E3%81%B0
Console.WriteLine(uri.Query);

// URLエンコードされてるのを渡したとして
uri = new Uri("http://google.co.jp/serach?q=%E3%81%A4%E3%81%8F%E3%81%B0");

// ToString結果はURLデコードされたものがでてくる => http://google.co.jp/serach?q=つくば
Console.WriteLine(uri.ToString());

というわけで、Queryは常にURLエンコードされるし、ToStringは常にURLデコードされてる。この辺の認識がフワフワッとしてると、エンコードやデコード結果が非常にアレになっちゃうんですね……。

// 元データが欲しい時はOriginalString
Console.WriteLine(uri.OriginalString);

// エンコードされないで取るにはGetComponentsで指定してあげる
// ちなみにQueryは (UriComponents.Query | UriComponents.KeepDelimiter, UriFormat.Escaped) と等しい
var unescaped = uri.GetComponents(UriComponents.Query, UriFormat.Unescaped);
Console.WriteLine(unescaped); // q=つくば

GetComponents大事大事。

Next

今回からBetaを取ったということで、1.0に上げようかなあ、とか思ったんですが、↑のように手痛いバグが残っていたことが発覚したりなので、まだもう少し様子見といった感。さすがにもう大丈夫なはず!といくら思っても見つかっていくわけで、枯れてるって本当に大事ですねえ。AsyncOAuthも枯れたライブラリになるよう、頑張ります。今回のバグとかもユーザーが伝えてくれるお陰なので足を向けて寝られません。

私自身が使ってるのか?というと、答えは、使っています!こないだに.NET最先端技術によるハイパフォーマンスウェブアプリケーションというセッションを行ない、そこで言及したように、現在、某ソーシャルゲームをC#で再構築中です。で、GREEにせよMobageにせよ、ソーシャルゲームってOpenSocialの仕組みに乗っかっているのですが、その認証がOAuthなのです。というわけで、めっちゃくちゃヘヴィーに使っています。(認証の形態がちょっと違うので幾つかメソッドが足されているのと、Shift-JIS対応とかのためのカスタムバージョンだったりはしますが…… その辺はAsyncOAuth本体にも載せるか検討中)。実戦投下まであとちょっとってところですが、それできっちり動ききれば、十分に枯れた、といえるのではないかと思っています。その時までは0.xで。でも現状でもかなり大丈夫だとは思いますので、是非是非使ってください。

The History of LINQ

という内容で、つくばC#勉強会で話してきました。

The History of LINQ from Yoshifumi Kawai

初心者向け勉強会(?)ということで、歴史ですかにぇー。詳細よりは、全体的にあっさり、みたいな。多分ね!とかもう10年前ですが、これだけ時間のたった今だからこそ、あらためてちょっと見てみると面白いね、みたいな。

第一回つくばC#勉強会 #tkbcsmt 当日の様子ということで実にカオス、よかったね!面白かったです~。

TypeScript 0.9のジェネリクス対応でlinq.jsの型定義作って苦労した話

久しぶりのTypeScriptAnnouncing TypeScript 0.9というわけで、ついに待望のジェネリクスが搭載されました。やったね!というわけで、ジェネリクス対応のlinq.jsの型定義を早速作りました、と。ver 3.0.4-Beta5です。まだまだBeta、すみませんすみません、色々忙しくて……。NuGetからもしくはサイトからのダウンロードで公開してます。

とりあえず例として使ってみた感じ。

// booleanしか受け入れないwhereにnumberを突っ込んだらちゃんと怒ってくれるよ!
// Call signatures of types '(x: any) => number' and '(element: number, index: number) => boolean' are incompatible.
var seq = Enumerable.from([1, 10, 100, 100]).where(x => x * 2);

やったー。これだよこれ!LINQはジェネリクスがあって本当の本当の真価を発揮するんです!!!

面白げな型定義

C#とちょっと違うところとしては、ジェネリクスの型変数のところにもオブジェクトを突っ込めるんですね。だから

// from(obj)の型定義はこんな感じ。
from(obj: any): IEnumerable<{ key: string; value: any }>;

// ちなみにfromのオーバーロードはいっぱいある
// linq.js自体はJScriptのIEnumerableとWinMDのIIterable<T>にも対応してるのですがTSで表現できないので、そこは未定義……。。。
from(): IEnumerable<any>; // empty
from<T>(obj: IEnumerable<T>): IEnumerable<T>;
from(obj: number): IEnumerable<number>;
from(obj: boolean): IEnumerable<boolean>;
from(obj: string): IEnumerable<string>;
from<T>(obj: T[]): IEnumerable<T>;
from<T>(obj: { length: number;[x: number]: T; }): IEnumerable<T>;

このkey,valueはどこから出てきたんだよって感じでわかりづらかったので、こうして見えてくれると嬉しい度高い。

型消去

場合によってはIEnumerable<any>となるため、Tの型をつけてあげたかったり、もしくは強引に変換したかったり(例えば↑のIIterable<T>は{key,value}に解釈されてしまうので、明示的に変換してあげる必要がある)する場合のために、castメソッドを用意しました。

// any[]型で渡ってきたりする場合に
var seq: any[] = [1, 2, 3, 4, 5];

var result = Enumerable.from(seq)
    .cast<number>() // numberに変換
    .select(x => x * x) // x:number
    .toArray(); // result:number[]

さて、ところで、TypeScriptのジェネリクスは型消去(Type Erasure)式です。コンパイル結果のJSは以下のような感じで

var result = Enumerable.from(seq)
    .cast()
    .select(function (x) { return x * x; })
    .toArray();

一切の型は消えています。なので、TypeScriptでTの型を取って実行時に扱ったりはできません。型が欲しければ型を渡せ方式。

linq.jsにはofTypeという、型でフィルタリングするメソッドがあるのですが

// 混在した配列から、型でフィルタして取り出す
var mixed: any[] = [1, "hoge", 100, true, "nano"];

// このままじゃ型が分からないのでresultはany[]
var result1 = Enumerable.from(mixed)
    .ofType(Number) // 数値型のみでフィルタ、つまり[1, 100]
    .toArray();

// ofTypeの後にcastするか、もしくはofTypeで型指定するか
var result2 = Enumerable.from(mixed)
    .ofType<number>(Number)
    .toArray();

というように、<number>(Number)と連続するのが非常に不恰好……。まあ、この辺はそういうものなのでしょうがないと諦めましょう。

地雷ふんだり

さて、そんな素敵なTypeScript 0.9なのですが、残念なお知らせ。現在の、というか、この0.9ですが、完成度はものすごーーーーーーーーく、低いです。はい、超低いです。Visual Studioと組み合わせて使う場合、半端無く動作も補完も遅いです。正直、ベータどころかアルファぐらいのクオリティで、何故に堂々と出してきたのか理解に苦しむ。遅いだけならまだしも、ジェネリクスの解釈が非常に怪しく、地雷を踏むと補完やエラー通知が消え去ります。いや、消え去るだけならまだよくて、Visual Studioと裏で動くTypeScript Compilerが大暴走をはじめてCPUが100%に張り付いたりします。もうヤヴァい。タスクマネージャー開きっぱにして警戒しながらじゃないと書けないです。linq.jsの型定義書くの超絶苦労した……。

interface IEnumerable<T> {
    // 戻り値の型をIEnumerable<IGrouping<TKey, T>>にしたいのですが、
    // interface側で定義しているTをネストした型変数に使うとVSが大暴走して死ぬ
    // ので、今回のバージョンではanyにしてます
    groupBy<TKey>(keySelector: (element: T) => TKey): IEnumerable<IGrouping<TKey, any>>;
}

具体的に踏んだのは↑ですねえ。最初原因が分かってなくてかなり時間食われちゃいました。んもー。最小セットで試してる限りでは、コンパイルエラーにはなるんですが(A generic type may not reference itself with a wrapped form of its own type parameters.)暴走はしない、んですがlinq.d.tsで弄ってると死ぬ。おうふ。もう定義の仕方が悪いのかコンパイラがアレなのか判断つかないのでしんどい。

そんなわけで、0.8の完成度からだいぶ退化して、実用性はゼロになってしまいました。私の環境だけ、じゃあないよねえ…‥?とりあえず0.9.1を待ちましょう。どうやらコンパイラをまるっと書き換えたそうですしねー、初物だからshoganaiと思うことにして。しかし泣きたい。

linq.d.ts

とりあえず、こんな感じになってます。

// Type Definition for linq.js, ver 3.0.4-Beta5

declare module linqjs {
    interface IEnumerator<T> {
        current(): T;
        moveNext(): boolean;
        dispose(): void;
    }

    interface Enumerable {
        Utils: {
            createLambda(expression: any): (...params: any[]) => any;
            createEnumerable<T>(getEnumerator: () => IEnumerator<T>): IEnumerable<T>;
            createEnumerator<T>(initialize: () => void , tryGetNext: () => boolean, dispose: () => void ): IEnumerator<T>;
            extendTo(type: any): void;
        };
        choice<T>(...params: T[]): IEnumerable<T>;
        cycle<T>(...params: T[]): IEnumerable<T>;
        empty<T>(): IEnumerable<T>;
        // from<T>, obj as JScript's IEnumerable or WinMD IIterable<T> is IEnumerable<T> but it can't define.
        from(): IEnumerable<any>; // empty
        from<T>(obj: IEnumerable<T>): IEnumerable<T>;
        from(obj: number): IEnumerable<number>;
        from(obj: boolean): IEnumerable<boolean>;
        from(obj: string): IEnumerable<string>;
        from<T>(obj: T[]): IEnumerable<T>;
        from<T>(obj: { length: number;[x: number]: T; }): IEnumerable<T>;
        from(obj: any): IEnumerable<{ key: string; value: any }>;
        make<T>(element: T): IEnumerable<T>;
        matches<T>(input: string, pattern: RegExp): IEnumerable<T>;
        matches<T>(input: string, pattern: string, flags?: string): IEnumerable<T>;
        range(start: number, count: number, step?: number): IEnumerable<number>;
        rangeDown(start: number, count: number, step?: number): IEnumerable<number>;
        rangeTo(start: number, to: number, step?: number): IEnumerable<number>;
        repeat<T>(element: T, count?: number): IEnumerable<T>;
        repeatWithFinalize<T>(initializer: () => T, finalizer: (element) => void ): IEnumerable<T>;
        generate<T>(func: () => T, count?: number): IEnumerable<T>;
        toInfinity(start?: number, step?: number): IEnumerable<number>;
        toNegativeInfinity(start?: number, step?: number): IEnumerable<number>;
        unfold<T>(seed: T, func: (value: T) => T): IEnumerable<T>;
        defer<T>(enumerableFactory: () => IEnumerable<T>): IEnumerable<T>;
    }

    interface IEnumerable<T> {
        constructor(getEnumerator: () => IEnumerator<T>);
        getEnumerator(): IEnumerator<T>;

        // Extension Methods
        traverseBreadthFirst(func: (element: T) => IEnumerable<T>): IEnumerable<T>;
        traverseBreadthFirst<TResult>(func: (element: T) => IEnumerable<T>, resultSelector: (element: T, nestLevel: number) => TResult): IEnumerable<TResult>;
        traverseDepthFirst<TResult>(func: (element: T) => Enumerable): IEnumerable<T>;
        traverseDepthFirst<TResult>(func: (element: T) => Enumerable, resultSelector?: (element: T, nestLevel: number) => TResult): IEnumerable<TResult>;
        flatten(): IEnumerable<any>;
        pairwise<TResult>(selector: (prev: T, current: T) => TResult): IEnumerable<TResult>;
        scan(func: (prev: T, current: T) => T): IEnumerable<T>;
        scan<TAccumulate>(seed: TAccumulate, func: (prev: TAccumulate, current: T) => TAccumulate): IEnumerable<TAccumulate>;
        select<TResult>(selector: (element: T, index: number) => TResult): IEnumerable<TResult>;
        selectMany<TOther>(collectionSelector: (element: T, index: number) => IEnumerable<TOther>): IEnumerable<TOther>;
        selectMany<TCollection, TResult>(collectionSelector: (element: T, index: number) => IEnumerable<TCollection>, resultSelector: (outer: T, inner: TCollection) => TResult): IEnumerable<TResult>;
        selectMany<TOther>(collectionSelector: (element: T, index: number) => TOther[]): IEnumerable<TOther>;
        selectMany<TCollection, TResult>(collectionSelector: (element: T, index: number) => TCollection[], resultSelector: (outer: T, inner: TCollection) => TResult): IEnumerable<TResult>;
        selectMany<TOther>(collectionSelector: (element: T, index: number) => { length: number;[x: number]: TOther; }): IEnumerable<TOther>;
        selectMany<TCollection, TResult>(collectionSelector: (element: T, index: number) => { length: number;[x: number]: TCollection; }, resultSelector: (outer: T, inner: TCollection) => TResult): IEnumerable<TResult>;
        where(predicate: (element: T, index: number) => boolean): IEnumerable<T>;
        choose(selector: (element: T, index: number) => T): IEnumerable<T>;
        ofType<TResult>(type: any): IEnumerable<TResult>;
        zip<TResult>(second: IEnumerable<T>, resultSelector: (first: T, second: T, index: number) => TResult): IEnumerable<TResult>;
        zip<TResult>(second: { length: number;[x: number]: T; }, resultSelector: (first: T, second: T, index: number) => TResult): IEnumerable<TResult>;
        zip<TResult>(second: T[], resultSelector: (first: T, second: T, index: number) => TResult): IEnumerable<TResult>;
        zip<TResult>(...params: any[]): IEnumerable<TResult>; // last one is selector
        merge<TResult>(...params: IEnumerable<T>[]): IEnumerable<T>;
        merge<TResult>(...params: { length: number;[x: number]: T; }[]): IEnumerable<T>;
        merge<TResult>(...params: T[][]): IEnumerable<T>;
        join<TInner, TKey, TResult>(inner: IEnumerable<TInner>, outerKeySelector: (outer: T) => TKey, innerKeySelector: (inner: TInner) => TKey, resultSelector: (outer: T, inner: TKey) => TResult, compareSelector?: (obj: T) => TKey): IEnumerable<TResult>;
        join<TInner, TKey, TResult>(inner: { length: number;[x: number]: TInner; }, outerKeySelector: (outer: T) => TKey, innerKeySelector: (inner: TInner) => TKey, resultSelector: (outer: T, inner: TKey) => TResult, compareSelector?: (obj: T) => TKey): IEnumerable<TResult>;
        join<TInner, TKey, TResult>(inner: TInner[], outerKeySelector: (outer: T) => TKey, innerKeySelector: (inner: TInner) => TKey, resultSelector: (outer: T, inner: TKey) => TResult, compareSelector?: (obj: T) => TKey): IEnumerable<TResult>;
        groupJoin<TInner, TKey, TResult>(inner: IEnumerable<TInner>, outerKeySelector: (outer: T) => TKey, innerKeySelector: (inner: TInner) => TKey, resultSelector: (outer: T, inner: TKey) => TResult, compareSelector?: (obj: T) => TKey): IEnumerable<TResult>;
        groupJoin<TInner, TKey, TResult>(inner: { length: number;[x: number]: TInner; }, outerKeySelector: (outer: T) => TKey, innerKeySelector: (inner: TInner) => TKey, resultSelector: (outer: T, inner: TKey) => TResult, compareSelector?: (obj: T) => TKey): IEnumerable<TResult>;
        groupJoin<TInner, TKey, TResult>(inner: TInner[], outerKeySelector: (outer: T) => TKey, innerKeySelector: (inner: TInner) => TKey, resultSelector: (outer: T, inner: TKey) => TResult, compareSelector?: (obj: T) => TKey): IEnumerable<TResult>;
        all(predicate: (element: T) => boolean): boolean;
        any(predicate?: (element: T) => boolean): boolean;
        isEmpty(): boolean;
        concat(...sequences: IEnumerable<T>[]): IEnumerable<T>;
        concat(...sequences: { length: number;[x: number]: T; }[]): IEnumerable<T>;
        concat(...sequences: T[]): IEnumerable<T>;
        insert(index: number, second: IEnumerable<T>): IEnumerable<T>;
        insert(index: number, second: { length: number;[x: number]: T; }): IEnumerable<T>;
        alternate(alternateValue: T): IEnumerable<T>;
        alternate(alternateSequence: { length: number;[x: number]: T; }): IEnumerable<T>;
        alternate(alternateSequence: IEnumerable<T>): IEnumerable<T>;
        alternate(alternateSequence: T[]): IEnumerable<T>;
        contains(value: T): boolean;
        contains<TCompare>(value: T, compareSelector?: (element: T) => TCompare): boolean;
        defaultIfEmpty(defaultValue?: T): IEnumerable<T>;
        distinct(): IEnumerable<T>;
        distinct<TCompare>(compareSelector: (element: T) => TCompare): IEnumerable<T>;
        distinctUntilChanged(): IEnumerable<T>;
        distinctUntilChanged<TCompare>(compareSelector: (element: T) => TCompare): IEnumerable<T>;
        except(second: { length: number;[x: number]: T; }): IEnumerable<T>;
        except<TCompare>(second: { length: number;[x: number]: T; }, compareSelector: (element: T) => TCompare): IEnumerable<T>;
        except(second: IEnumerable<T>): IEnumerable<T>;
        except<TCompare>(second: IEnumerable<T>, compareSelector: (element: T) => TCompare): IEnumerable<T>;
        except(second: T[]): IEnumerable<T>;
        except<TCompare>(second: T[], compareSelector: (element: T) => TCompare): IEnumerable<T>;
        intersect(second: { length: number;[x: number]: T; }): IEnumerable<T>;
        intersect<TCompare>(second: { length: number;[x: number]: T; }, compareSelector: (element: T) => TCompare): IEnumerable<T>;
        intersect(second: IEnumerable<T>): IEnumerable<T>;
        intersect<TCompare>(second: IEnumerable<T>, compareSelector: (element: T) => TCompare): IEnumerable<T>;
        intersect(second: T[]): IEnumerable<T>;
        intersect<TCompare>(second: T[], compareSelector: (element: T) => TCompare): IEnumerable<T>;
        union(second: { length: number;[x: number]: T; }): IEnumerable<T>;
        union<TCompare>(second: { length: number;[x: number]: T; }, compareSelector: (element: T) => TCompare): IEnumerable<T>;
        union(second: IEnumerable<T>): IEnumerable<T>;
        union<TCompare>(second: IEnumerable<T>, compareSelector: (element: T) => TCompare): IEnumerable<T>;
        union(second: T[]): IEnumerable<T>;
        union<TCompare>(second: T[], compareSelector: (element: T) => TCompare): IEnumerable<T>;
        sequenceEqual(second: { length: number;[x: number]: T; }): boolean;
        sequenceEqual<TCompare>(second: { length: number;[x: number]: T; }, compareSelector: (element: T) => TCompare): boolean;
        sequenceEqual(second: IEnumerable<T>): boolean;
        sequenceEqual<TCompare>(second: IEnumerable<T>, compareSelector: (element: T) => TCompare): boolean;
        sequenceEqual(second: T[]): boolean;
        sequenceEqual<TCompare>(second: T[], compareSelector: (element: T) => TCompare): boolean;
        orderBy<TKey>(keySelector: (element: T) => TKey): IOrderedEnumerable<T>;
        orderByDescending<TKey>(keySelector: (element: T) => TKey): IOrderedEnumerable<T>;
        reverse(): IEnumerable<T>;
        shuffle(): IEnumerable<T>;
        weightedSample(weightSelector: (element: T) => number): IEnumerable<T>;
        // truly, return type is IEnumerable<IGrouping<TKey, T>> but Visual Studio + TypeScript Compiler can't compile.
        groupBy<TKey>(keySelector: (element: T) => TKey): IEnumerable<IGrouping<TKey, T>>;
        groupBy<TKey, TElement>(keySelector: (element: T) => TKey, elementSelector: (element: T) => TElement): IEnumerable<IGrouping<TKey, TElement>>;
        groupBy<TKey, TElement, TResult>(keySelector: (element: T) => TKey, elementSelector: (element: T) => TElement, resultSelector: (key: TKey, element: IEnumerable<TElement>) => TResult): IEnumerable<TResult>;
        groupBy<TKey, TElement, TResult, TCompare>(keySelector: (element: T) => TKey, elementSelector: (element: T) => TElement, resultSelector: (key: TKey, element: IEnumerable<TElement>) => TResult, compareSelector: (element: T) => TCompare): IEnumerable<TResult>;
        // :IEnumerable<IGrouping<TKey, T>>
        partitionBy<TKey>(keySelector: (element: T) => TKey): IEnumerable<IGrouping<TKey, any>>;
        // :IEnumerable<IGrouping<TKey, TElement>>
        partitionBy<TKey, TElement>(keySelector: (element: T) => TKey, elementSelector: (element: T) => TElement): IEnumerable<IGrouping<TKey, TElement>>;
        partitionBy<TKey, TElement, TResult>(keySelector: (element: T) => TKey, elementSelector: (element: T) => TElement, resultSelector: (key: TKey, element: IEnumerable<TElement>) => TResult): IEnumerable<TResult>;
        partitionBy<TKey, TElement, TResult, TCompare>(keySelector: (element: T) => TKey, elementSelector: (element: T) => TElement, resultSelector: (key: TKey, element: IEnumerable<TElement>) => TResult, compareSelector: (element: T) => TCompare): IEnumerable<TResult>;
        buffer(count: number): IEnumerable<T>;
        aggregate(func: (prev: T, current: T) => T): T;
        aggregate<TAccumulate>(seed: TAccumulate, func: (prev: TAccumulate, current: T) => TAccumulate): TAccumulate;
        aggregate<TAccumulate, TResult>(seed: TAccumulate, func: (prev: TAccumulate, current: T) => TAccumulate, resultSelector: (last: TAccumulate) => TResult): TResult;
        average(selector?: (element: T) => number): number;
        count(predicate?: (element: T, index: number) => boolean): number;
        max(selector?: (element: T) => number): number;
        min(selector?: (element: T) => number): number;
        maxBy<TKey>(keySelector: (element: T) => TKey): T;
        minBy<TKey>(keySelector: (element: T) => TKey): T;
        sum(selector?: (element: T) => number): number;
        elementAt(index: number): T;
        elementAtOrDefault(index: number, defaultValue?: T): T;
        first(predicate?: (element: T, index: number) => boolean): T;
        firstOrDefault(predicate?: (element: T, index: number) => boolean, defaultValue?: T): T;
        last(predicate?: (element: T, index: number) => boolean): T;
        lastOrDefault(predicate?: (element: T, index: number) => boolean, defaultValue?: T): T;
        single(predicate?: (element: T, index: number) => boolean): T;
        singleOrDefault(predicate?: (element: T, index: number) => boolean, defaultValue?: T): T;
        skip(count: number): IEnumerable<T>;
        skipWhile(predicate: (element: T, index: number) => boolean): IEnumerable<T>;
        take(count: number): IEnumerable<T>;
        takeWhile(predicate: (element: T, index: number) => boolean): IEnumerable<T>;
        takeExceptLast(count?: number): IEnumerable<T>;
        takeFromLast(count: number): IEnumerable<T>;
        indexOf(item: T): number;
        indexOf(predicate: (element: T, index: number) => boolean): number;
        lastIndexOf(item: T): number;
        lastIndexOf(predicate: (element: T, index: number) => boolean): number;
        asEnumerable(): IEnumerable<T>;
        cast<TResult>(): IEnumerable<TResult>;
        toArray(): T[];
        // truly, return type is ILookup<TKey, T> but Visual Studio + TypeScript Compiler can't compile. 
        toLookup<TKey>(keySelector: (element: T) => TKey): ILookup<TKey, any>;
        toLookup<TKey, TElement>(keySelector: (element: T) => TKey, elementSelector: (element: T) => TElement): ILookup<TKey, TElement>;
        toLookup<TKey, TElement, TCompare>(keySelector: (element: T) => TKey, elementSelector: (element: T) => TElement, compareSelector: (key: TKey) => TCompare): ILookup<TKey, TElement>;
        toObject(keySelector: (element: T) => any, elementSelector?: (element: T) => any): Object;
        // :IDictionary<TKey, T>
        toDictionary<TKey>(keySelector: (element: T) => TKey): IDictionary<TKey, any>;
        toDictionary<TKey, TValue>(keySelector: (element: T) => TKey, elementSelector: (element: T) => TValue): IDictionary<TKey, TValue>;
        toDictionary<TKey, TValue, TCompare>(keySelector: (element: T) => TKey, elementSelector: (element: T) => TValue, compareSelector: (key: TKey) => TCompare): IDictionary<TKey, TValue>;
        toJSONString(replacer: (key: string, value: any) => any): string;
        toJSONString(replacer: any[]): string;
        toJSONString(replacer: (key: string, value: any) => any, space: any): string;
        toJSONString(replacer: any[], space: any): string;
        toJoinedString(separator?: string): string;
        toJoinedString<TResult>(separator: string, selector: (element: T, index: number) => TResult): string;
        doAction(action: (element: T, index: number) => void ): IEnumerable<T>;
        doAction(action: (element: T, index: number) => boolean): IEnumerable<T>;
        forEach(action: (element: T, index: number) => void ): void;
        forEach(action: (element: T, index: number) => boolean): void;
        write(separator?: string): void;
        write<TResult>(separator: string, selector: (element: T) => TResult): void;
        writeLine(): void;
        writeLine<TResult>(selector: (element: T) => TResult): void;
        force(): void;
        letBind<TResult>(func: (source: IEnumerable<T>) => { length: number;[x: number]: TResult; }): IEnumerable<TResult>;
        letBind<TResult>(func: (source: IEnumerable<T>) => TResult[]): IEnumerable<TResult>;
        letBind<TResult>(func: (source: IEnumerable<T>) => IEnumerable<TResult>): IEnumerable<TResult>;
        share(): IDisposableEnumerable<T>;
        memoize(): IDisposableEnumerable<T>;
        catchError(handler: (exception: any) => void ): IEnumerable<T>;
        finallyAction(finallyAction: () => void ): IEnumerable<T>;
        log(): IEnumerable<T>;
        log<TValue>(selector: (element: T) => TValue ): IEnumerable<T>;
        trace(message?: string): IEnumerable<T>;
        trace<TValue>(message: string, selector: (element: T) => TValue ): IEnumerable<T>;
    }

    interface IOrderedEnumerable<T> extends IEnumerable<T> {
        createOrderedEnumerable<TKey>(keySelector: (element: T) => TKey, descending: boolean): IOrderedEnumerable<T>;
        thenBy<TKey>(keySelector: (element: T) => TKey): IOrderedEnumerable<T>;
        thenByDescending<TKey>(keySelector: (element: T) => TKey): IOrderedEnumerable<T>;
    }

    interface IDisposableEnumerable<T> extends IEnumerable<T> {
        dispose(): void;
    }

    interface IDictionary<TKey, TValue> {
        add(key: TKey, value: TValue): void;
        get(key: TKey): TValue;
        set(key: TKey, value: TValue): boolean;
        contains(key: TKey): boolean;
        clear(): void;
        remove(key: TKey): void;
        count(): number;
        toEnumerable(): IEnumerable<{ key: TKey; value: TValue }>;
    }

    interface ILookup<TKey, TElement> {
        count(): number;
        get(key: TKey): IEnumerable<TElement>;
        contains(key: TKey): boolean;
        toEnumerable(): IEnumerable<IGrouping<TKey, TElement>>;
    }

    interface IGrouping<TKey, TElement> extends IEnumerable<TElement> {
        key(): TKey;
    }
}

// export definition
declare var Enumerable: linqjs.Enumerable;

アップデートを全然追ってないので、初期に作った定義の仕方のまんまなので、大丈夫かな、まあ、大丈夫じゃないかな、きっと。

とりあえずとにかく面倒くさかった。しかし定義する人が苦労すれば、利用者はハッピーになれるから!!!なのではやくVS対応がまともになってください。ぶんぶんぶん回したいのだけれどねえ←その前にlinq.js ver.3がいつまでもBetaなのをなんとかしろ

C#の強み、或いは何故PHPから乗り換えるのか

という内容で、C#ユーザー会で話してきました。

C#の強み、或いは何故PHPから乗り換えるのか from Yoshifumi Kawai

特にPHPディスりたいわけでは、あるのかないのかはともかく、やっぱり実際に使ってきて良いところというのも分からなくもない感じです。会場でも話したのは、短期的な開発速度には有利なのは間違いないのかな、と。デプロイとかも、とりあえずポン置きでいいし、開発も、なんかもう複雑なことやると面倒だし、どうせ文字列だらけになるしで、開き直ってハードコーディングでバカバカ作っていくから速い、とか。ただし勿論あとで苦労するわけですがそれはそれとして。けれどやっぱC#良いよね、って。

言語も色々なトレードオフで成り立つわけですが、その中でもC#は、バランス良くて好きだなーというのが私の個人的なところです。Visual Studio良いよねー、でもいいですしLINQ良いよねー、もいいですし、IntelliSenseがないと生きていけないですし。うん、そう、IntelliSense指向言語が好きなわけです。

ほとんどVisual Studioの話じゃねーか、というのは、まぁそうなのですけれど、大事なのはVisual Studioを前提においた言語構造になってるってとこです。強力すぎる型推論は、100%の入力補完を実現できなかったりする。強力すぎる動的さは100%の入力補完を実現できなかったりする。C#がVisual Studioとともに使って快適なのは、そういう言語設計になっているからです。コンパイルの速さも重要で。C#は他のコンパイル型言語に比べて速い部類に入ります。だから快適だし、エラー通知とかもリアルタイム。目に見えないところ、使ってみないと評価しにくい、ただの○×表だけの性能比較にはない部分、結構多いものです。

.NET最先端技術によるハイパフォーマンスウェブアプリケーション FAQ

Build Insider Offlineにて、「.NET最先端技術によるハイパフォーマンスウェブアプリケーション」と題して、グラニのC#によるウェブアプリケーション作成の仕組みについて話してきました。

.NET最先端技術によるハイパフォーマンスウェブアプリケーション from Yoshifumi Kawai

一日でViewsが1万、はてブが250と、ドトネト系にしては珍しく多くの人に見てもらえたようでなにより。

今までのC#関連って、MS純正ライブラリを使ったどうのこうの、というのはありましたが、外部ライブラリを組み合わせて、実践的にどうしているかっていうような話ってほとんどなかったんですよね。やってるところがない、ことはないのですが、しかし表にない、見えないものはないに等しいです。

別にC#だって自分達の手でライブラリを選び、作り、組み上げていく。それがこれからの時代のスタンダードです。遅れていたのかもしれません。しかし、遅すぎるなんてことはない。素材は良いし、.NETは死んだのでなく、むしろ風が吹いてきている。リアルなモデルケースとして、引っ張っていけたらと思っています。

FAQ

  • 今時Windowsサーバー?

AWSやAzureなどで簡単にWindowsインスタンスの立ち上げが可能なので、今時というか普通に選択肢に入ってくれるといいですねえ、これからは。ウェブ=LAMPとか誰が決めたの?という話で。コスト面ではそんなに高くなるわけでもないし、それで開発効率が上がったり、サーバー台数が削減されるなら、むしろプラスです。開発効率に関しては人員次第ですが、うちのメンバーの習熟度で言ったら間違いなく上がります、圧倒的に。

  • サーバーサイドでトラフィック以外がくってるのおかしい

そうそう、一般的にウェブではネックなのは通信部分だけ、と思っていたことも有りました。でもまぁ、クックパッドのRails アプリケーションのパフォーマンスについて RubyKaigi 2013 で発表しましたのスライドのResponse time breakdownのところ。SQLが20%がRubyが80%とあるんですね。うちのグラフと一緒?CakePHPはとにかくアレなわけですが、重量級フレームワークは割とそうなるもんなのかしらねえ、と少しだけホッとしたような絶望したような。

先入観じゃなくちゃんとモニタリングしてくのが大事、と当たり前の話ですけれど。スライドでも推しましたがNew Relicは本当に最高なので、よほどの事情がない限りは入れるべきかなぁ、と。PHP, Ruby, Java, .NET, Python, Node.js、多くの言語に対応しています。確実にパワーは喰いますが、それでも全然お釣りくる。

  • アプリケーションサーバー:DBマスター:DBスレーブの比率がおかしい

ええと、これ、実質的にはSlaveは0です。色々な経緯があって、アプリ本体からは参照も全てMasterにし か振ってません。Slaveの用途は管理画面からの参照用とか、その程度です(スタンバイという点でも、RDSのMulti-AZを利用しているので意味は無い)。となると比率はますますオカシクなるわけですが、うーん、まあ、今時のDBってなんだかんだで頑丈ですからねえ、そう増えないかなあ。しかしそれにしてもアプリケーションサーバーの台数が多すぎなのは、まぁもう本当にすみませんすみませんって感じなわけなのですが。みんなCakePHP触ってみるといいですよ、XHProfにかけると絶望しますから。

勿論チューニングの余地はあります。ありますし、最後にはCakePHPを捨てるというところになることも見えるし、PHPにこだわる理由がどこにもありません。それならC#移行に、といったところですね。とはいえ実際のとこ増え続けることによる歪みが色々発生してるので、ヒィヒィ言いながら適宜対処してます。あと、最低限というか割とそれなりにはCakePHP本体に手を入れたりもしてはいるんですけど、それでも中々どうにも。

  • PHPのLINQあるよ

ないよ。幾つかのPHP-LINQライブラリを、コードも読んで評価しましたが、ゴミという結論に達しました。私はJavaScriptにLINQ移植したりとかしているので、LINQにはこだわりがありひじょーにうるさいのです。あとCakePHPに含まれてるコレクション系のメソッドもゴミですね。かわりに自作したLINQっぽいコレクション処理用の何かを使っています。PHP 5.4を使っているので、ラムダとかもがしがし使えはするので、メソッドチェーンでフィルターやグルーピング、複数キーのソートとか一通りできるように作りました。これ作ってなかったら死んでたわ……。ただ、簡易的な実装なので遅延実行ではありません。最近発表されたGinqは非常に良いですね!もっと前にあったら、採用していたかもしれません。

  • 「何でもハッシュに詰めるしかない」

ここ説明が足りなくてアレでしたね、申し訳ないです。文脈としてはIntelliSenseが効くか効かないか、の話しかするつもりなかったのと、ハッシュだろうとオブジェクトだろうとDBからの戻り値の場合はどうせ効かないのでどうでもいい、といった感だったので不正確でした、すみません。

あ、DBから以外の部分で、Modelとして作りこむところでは普通にclass立てたり、TypeHinting使ったりPhpDoc書いたりして、補完がなるべく効きやすいように作ってはいますよ。IDE信奉者なので、PhpStormを会社で購入して使っていますので。とはいえ、タイプヒンティングやPHPDocでも、PHP自体がゆるふわなので効きめはイマイチなんですよ。じゃあ、PHP自体がのゆるふわさを捨てて完全にガチガチに書くか?といったら、それはそれでイマイチになるので、まぁ、半分諦めるのがいいかな、とは。

一応誤解なきように弁解すると、無知のC#モノがイヤイヤPHP使ってるだけでPHPについて何も分かっちゃいない、というほどに分かってないわけではないです。一応はPHPの最新言語仕様についてはちゃんと追っかけて差分取っているぐらいには使っています。さすがに仕事の商売道具ですから、嫌いだから何も勉強しない!わけでもないです。traitなども効果的だと思ったところには使ってますし、まぁtrait使うと更にPhpStormの補完が死ぬわけですが。

  • なんで水平分割いやがるの

一応スライド中にも書きましたが、メンドーごとが増えるので。特に嫌なのは、HeidiSQLとか、GUIツール郡の使い勝手が低下して、かわりに自社製のツールセット(コマンドか、しょぼいWebUI)になるか、とかですよねえ、それが一番避けたくて。他社の話を聞いていて驚くのは、phpMyAdminとかコマンド叩いてるとかいうんですよ、DB見たりするのに。私的には、ありえない。みんなもっとちゃんとGUIツールも使いましょう。必要とあらばウェブツールだけじゃなくてGUIツールも自作しましょう(C#なら簡単です!)。

まあ、どうしてもダメになったら水平にします。でもFusion-IOはもとより、AWSでも10万IOPSのインスタンスが出たりとか、ハードウェア性能はどんどん進化しているので、何とかなってくれるんじゃないかなあ、と楽観視はしています。良い時代になったな、と思います。

なぜ移行するの?C#のどこがいいの?

ということを、6/11 第90回codeseek&第30回日本C#ユーザー会 勉強会 「やっぱり.NETだよね」でお話しますので、明日ですが、当日まで申し込みは受け付けてる的なノリがいつもの感じだとあるので、時間あるかたは是非お越しください。そんなにDisってよりは割とSoftな感じです、私の分はいちおー。

珍しく既にスライドは完成していまして、「Static vs Dynamic」「Type for IntelliSense」「Type for Refactoring」「Debugger is Power」「LINQ vs array_xxx」「Razor:Template Engine Revolution」といったようなタイトルが並んでますので、気になった方はどーぞ。

ああ、そう、あとA vs Bにおいて、全てにおけてAのほうがいい、なんてことは絶対にないんですね。どこかしらかはBのほうがよかったり、BならXxxなのに、とかいうようなことも出てくるでしょう。何がどれだけ自分に、自分達に良いのかの選択をしてくのが肝要なので、その選択を考えるための道具になればいいかと思ってます。

HttpClient詳解、或いはAsyncOAuthのアップデートについて

すっかり忘れていたわけではないですが、ちょっとかなり前、3/30のRoom metro #15にて、HttpClient詳解という、HttpClientについてのセッションを行いました。

HttpClient詳解、或いは非同期の落とし穴について from Yoshifumi Kawai

HttpClientは、使えば使うほど、もうWebRequestやWebClientに戻りたくないわー、という非常に秀逸な、完全にこれからのスタンダードになる代物なので、きっちり習得しましょう。

或いは非同期の落とし穴について、ということで、async/awaitでも顕在の、いや、async/awaitだからこそ現れるデッドロックの問題と回避方法についても紹介しています。はまる時ははまっちゃうんですよねー、これ、何気に地味に実は。それなりに痛い目みました、私も。

PCL版のRC

HttpClientは現在.NET 4.5とWindows Store Appsのほうに標準搭載されていますが、それ以外でも使うために、Portable Class Libraryとしての提供がされています。そして、5/22にPortable HttpClient is now available as RCとしてRC版がリリースされました!

こないだまでのBeta版だと、AsyncOAuthを使ってTiwtterのストリーミングAPIを読む時に、awaitすると全く戻ってこなくなるという現象がありました。これはAsyncOAuthが悪いのかHttpClientが悪いのか調べたんですが、結果としてHttpClientのバグでした。HttpClientは内部で通信にWebRequestを使っているのですが、それのAllowReadStreamBufferingとAllowWriteStreamBufferingをfalseにセットしなければならないのに、何もセットしない(ことによって結果的にtrueになっている)状態でした。すると、ストリーミングAPIを読むのにバッファを取ろうとして、当然ストリーミングなのでオワリがないので永遠に帰ってこないという……。

ちゃんとバグ報告したら(偉い!←自分で言う)、今回のRC版で直してくれたようです、多分。とりあえずWP8のEmulatorで試した限りでは、ちゃんとストリーミングAPI動きました。よかったよかった。というわけで、AsyncOAuthもver.0.6.4として、新しいHttpClientに依存するようにアップデートしておきました。なお、AsyncOAuthについてはAsyncOAuth - C#用の全プラットフォーム対応の非同期OAuthライブラリを読んでくださいな。

そういえば同時に、というか4/17にですが、.NET4などでもasync/awaitを使えるようにするMicrosoft.Bcl.AsyncはStableになってました。これで気兼ねなくasync使える!

6月の予定

6月は何故かいっぱいイベントに出ることになっています。6/8のBuild Insider OFFLINE、そこで「.NET最先端技術によるハイパフォーマンスウェブアプリケーション」についてお話します。もう席は満席となってしまいましたが、Ustreamでの中継も行われるようですので、よろしければそちらで見ていただければと思います。

また、6/11~14あたりに、C#ユーザー会で何か話すそうです。何か。何でしょうね。一節によるとPHP被害者友の会(?)だとか・

そして6/22につくばC#勉強会でThe History of LINQと題して、何か話すそうです。はい。つくばいいですね!素晴らしいですぅー。つくば勉強会はまだまだ残席あるようなので、みんな参加しよう!登壇者も募集しているようですので、登壇もしよう!

RxとRedisを用いたリモートPub/Sub通信

今日から始まったBuild Insiderで、RedisとBookSleeveの記事を書きました - C#のRedisライブラリ「BookSleeve」の利用法。Redis、面白いし、Windowsで試すのも想像以上に簡単なので、是非是非試してみて欲しいです。そして何よりもBookSleeve!使うと、強制的に全てがasyncになるので、C# 5.0でのコーディングの仕方のトレーニングになる(笑)。にちじょー的にasync/awaitを使い倒すと、そこから色々アイディアが沸き上がっきます。ただたんにsyncがasyncになった、などというだけじゃなく、アプリケーションの造りが変わります。そういう意味では、Taskは結構過小評価されてるのかもしれないな、なんて最近は思っています。

さて、RedisにはPub/Sub機能がついているわけですが、Pub/Sub→オブザーバーパターン→Rx!これはティンと来た!と、いうわけで、これ、Rxに乗せられました、とても自然に。というわけで、□□を○○と見做すシリーズ、なCloudStructuresにRx対応を載せました。

追加したクラスはRedisSubject<T>。これはSubjectやAsyncSubjectなどと同じく、ISubject<T>になっています。IObservableであり、IObserverでもある代物。とりあえず、コード例を見てください。

var settings = new RedisSettings("127.0.0.1");

var subject = new RedisSubject<string>(settings, "PubSubTest");

// SubscribeはIObservable<T>なのでRxなLINQで書ける
var a = subject
    .Select(x => DateTime.Now.Ticks + " " + x)
    .Subscribe(x => Console.WriteLine(x));

var b = subject
    .Where(x => !x.StartsWith("A"))
    .Subscribe(x => Console.WriteLine(x), () => Console.WriteLine("completed!"));

// IObserverなのでOnNext/OnError/OnCompletedでメッセージ配信
subject.OnNext("ABCDEFGHIJKLM");
subject.OnNext("あいうえお");
subject.OnNext("なにぬねの");

Thread.Sleep(200); // 結果表示を待つ...

a.Dispose(); // UnsubscribeはDisposeで
subject.OnCompleted(); // OnCompletedを受信したSubscriberもUnsubscribeされる

はい、別になんてこともない極々フツーのRxのSubjectです。が、しかし、これはネットワークを通って、Redisを通して、全てのSubscriberへとメッセージを配信しています。おお~。いやまあ、コードの見た目からじゃあそういうの分からないので何も感動するところもないのですが、とにかく、本当に極々自然に普通に、しかし、ネットワークを超えます。この、見た目何も変わらずに、というところがいいところなわけです。

Subscribeする側はIObservableなので別にフツーに合成していけますし、Publishする側もIObserverなので、Subscribeにしれっと潜り込ませたりしても構わない。もう、全然、普通にインメモリなRxでやる時と同じことが、できます。

オワリ。地味だ。

う、うーん。ほ、ほらほら!!

// ネットワーク経由
var settings = new RedisSettings("xx.xxx.xxx.xxx");

var subject = new RedisSubject<DateTime>(settings, "PubSubTest");

// publisherはこちらのコード
while (true)
{
    Console.ReadLine();
    var now = DateTime.Now;
    Console.WriteLine(now.Ticks);
    subject.OnNext(now);
}

// subscriberはこちらのコードを動かす
subject.Subscribe(x =>
{
    var now = DateTime.Now;
    Console.Write(x.Ticks + " => " + now.Ticks);
    Console.WriteLine(" | " + (now - x));
});

というわけで、実際にネットワーク経由(AWS上に立てたRedisサーバーを通してる)で動かしてみた結果がこんな感じです。ネットワークを超えたことで用法は幾らでもある!夢膨らみまくり!

で、↑のコードは遅延時間のチェックを兼ねてるのですが、概ね、0.03秒ぐらい。たまにひっかかって0.5秒超えてるのがあって、ぐぬぬですが。実際のとこRedis/Linuxの設定で結構変わってくるところがあるので、その辺は煮詰めてくださいといったところでしょうか。

ともあれチャットとかなら全然問題なし、ゲームでもアクションとかタイミングにシビアじゃないものなら余裕ですね、ボードゲームぐらいなら全く問題ない。ちょっとしたMMOぐらいならいけるかも。これからはネットワーク対戦はRedisで、Rxで!!!

余談

CloudStructuresですが、.configからの設定読み込み機能も地味につけました。

<configSections>
    <section name="cloudStructures" type="CloudStructures.Redis.CloudStructuresConfigurationSection, CloudStructures" />
</configSections>

<cloudStructures>
    <redis>
        <group name="cache">
            <add host="127.0.0.1" />
            <add host="127.0.0.2" port="1000" />
        </group>
        <group name="session">
            <add host="127.0.0.1" db="2" valueConverter="CloudStructures.Redis.ProtoBufRedisValueConverter, CloudStructures" />
        </group>
    </redis>
</cloudStructures>
// これで設定を読み込める
var groups = CloudStructuresConfigurationSection.GetSection().ToRedisGroups();

色々使いやすくなってきて良い感じじゃあないでしょーか。

コールバック撲滅

そうそう、最後に実装の話を。元々BookSleeveのPub/Sub購読はコールバック形式です。「public Task Subscribe(string key, Action<string, byte[]> handler)」handlerはstringがキー, byte[]が送られてくるオブジェクトを指します。コールバック is ダサい。コールバック is 扱いにくい。ので、コールバックを見かけたらObservableかTaskに変換することを考えましょう!それがC# 5.0世代の常識です!

というわけで、以下のようにして変換しました。

public IDisposable Subscribe(IObserver<T> observer)
{
    var channel = Connection.GetOpenSubscriberChannel();

    var disposable = System.Reactive.Disposables.Disposable.Create(() =>
    {
        channel.Unsubscribe(Key).Wait();
    });

    // ここが元からあるコールバック
    channel.Subscribe(Key, (_, xs) =>
    {
        using (var ms = new MemoryStream(xs))
        {
            var value = RemotableNotification<T>.ReadFrom(ms, valueConverter);
            value.Accept(observer); // この中でobserverのOnNext/OnError/OnCompletedが叩かれる
            if (value.Kind == NotificationKind.OnError || value.Kind == NotificationKind.OnCompleted)
            {
                disposable.Dispose(); // ErrorかCompletedでもUnsubscribeしますん
            }
        }
    }).Wait();

    return disposable; // もしDisposableが呼ばれたらUnsubscribeしますん
}

こんなふぅーにRxで包むことで、相当使いやすさがアップします。感動した。

Prev | | Next

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

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

X:@neuecc GitHub:neuecc

Archive