接続の回復性と再試行ロジック

Note

EF6 以降のみ - このページで説明する機能、API などは、Entity Framework 6 で導入されました。 以前のバージョンを使用している場合、一部またはすべての情報は適用されません。

データベース サーバーに接続するアプリケーションは必ず、バックエンドの障害や不安定なネットワークによる接続の中断の影響を被ります。 しかし専用のデータベース サーバーを背景に動作する LAN ベースの環境では、こうしたエラーはまれであり、そうした障害に対処するための特別なロジックが要求されることは通常ありません。 Windows Azure SQL データベースなどクラウド ベースのデータベース サーバーが台頭し、また信頼性の低いネットワークでの接続が増えるにつれ、接続の中断が発生する件数は以前よりも多くなってきています。 これは、サービスの公平性を確保するためにクラウド データベースが採用する防御手法 (接続のスロットリングなど) によるものである場合もあれば、断続的なタイムアウトなど一時的なエラーにつながるネットワークの不安定さに起因する場合もあります。

接続の回復性とは、こうした接続の中断が原因で失敗したコマンドを自動的に再試行する EF の機能をいいます。

実行戦略

接続の再試行への対応は、IDbExecutionStrategy インターフェイスの実装によって行います。 操作を受け付け、例外が発生した場合は再試行が適切であるかどうかを判断し、もし適切であれば再試行するという処理を IDbExecutionStrategy の実装が担うことになります。 EF には次の 4 つの実行戦略が備わっています。

  1. DefaultExecutionStrategy: この実行戦略では、いずれの操作も再試行は実行されません。SQL Server 以外のデータベースでは、これが既定となります。
  2. DefaultSqlExecutionStrategy: 既定で使用される内部的な実行戦略です。 再試行は一切実行されませんが、接続の回復性を有効にした方がよいということをユーザーに伝えるために一時的と考えられる例外がラップされます。
  3. DbExecutionStrategy: このクラスは、独自の戦略を含め、他の実行戦略の基底クラスとして適しています。 初回再試行は待ち時間ゼロで実行され、その後は最大再試行回数に到達するまで待ち時間が指数関数的に増えていく、指数関数的な再試行ポリシーが実装されます。 このクラスには抽象 ShouldRetryOn メソッドがあり、派生した実行戦略でこのメソッドを実装することにより、再試行すべき例外を制御できます。
  4. SqlAzureExecutionStrategy: この実行戦略は DbExecutionStrategy を継承し、Azure SQL Database の使用中、おそらく一時的であるとわかっている例外が発生したときに再試行が実行されます。

Note

実行戦略 2 と 4 は、EF に同梱される Sql Server プロバイダー (EntityFramework.SqlServer アセンブリ内) に含まれるもので、SQL Server と連動するように設計されています。

実行戦略を有効にする

特定の実行戦略を使用するよう EF に伝える方法としては、DbConfiguration クラスの SetExecutionStrategy メソッドを使うのが最も簡単です。

public class MyConfiguration : DbConfiguration
{
    public MyConfiguration()
    {
        SetExecutionStrategy("System.Data.SqlClient", () => new SqlAzureExecutionStrategy());
    }
}

このコードは、SQL Server への接続時に SqlAzureExecutionStrategy を使用するよう EF に命令しています。

実行戦略を構成する

SqlAzureExecutionStrategy のコンストラクターには、MaxRetryCount と MaxDelay という 2 つのパラメーターを指定できます。 MaxRetry カウントは、その戦略で再試行する最大回数です。 MaxDelay は、その実行戦略で使用される、再試行間の最大待ち時間を表す TimeSpan です。

最大再試行回数を 1 に、最大待ち時間を 30 秒に設定するには、次のコードを実行します。

public class MyConfiguration : DbConfiguration
{
    public MyConfiguration()
    {
        SetExecutionStrategy(
            "System.Data.SqlClient",
            () => new SqlAzureExecutionStrategy(1, TimeSpan.FromSeconds(30)));
    }
}

SqlAzureExecutionStrategy では、一時的な障害が初めて発生したときには即座に再試行が実行されますが、それ以降は、最大再試行の上限を超えるか、合計時間が最大待ち時間に達するまで、再試行の間隔が長くなっていきます。

この実行戦略で再試行が行われるのは、一時的であることが多い、限られた数の例外だけです。その他のエラーについては自分で処理すると共に、エラーが一時的でない場合や自己解決に時間がかかりすぎる場合には、RetryLimitExceeded 例外をキャッチする必要があります。

再試行の実行戦略を使用する際は、既知の制限がいくつかあります。

ストリーミング クエリはサポートされない

EF6 以降のバージョンでは、クエリの結果がストリーミングされるのではなくバッファー処理されます。これが既定の動作になります。 結果をストリーミングさせたい場合、AsStreaming メソッドを使用すれば、LINQ to Entities クエリをストリーミングに変更できます。

using (var db = new BloggingContext())
{
    var query = (from b in db.Blogs
                orderby b.Url
                select b).AsStreaming();
    }
}

再試行の実行戦略が登録されている場合は、ストリーミングはサポートされません。 この制限が存在する理由は、結果が返されている途中で接続が切断される可能性があるためです。 この場合、EF はクエリ全体を再実行する必要がありますが、既に返されている結果を把握する信頼性の高い方法がありません (クエリが最初に送信されてからデータが変更されている可能性がある、異なる順序で結果が返される可能性がある、結果に一意識別子がない場合がある、など)。

ユーザーによって開始されたトランザクションはサポートされない

再試行を伴う実行戦略を構成した場合、トランザクションの使用に関していくつかの制限があります。

トランザクション内にデータベースの更新があれば、EF が既定で実行します。 この動作を有効にするために、皆さんが何かを行う必要はありません。常に EF によって自動的に行われます。

たとえば次のコードでは、SaveChanges がトランザクション内で自動的に実行されます。 万一どちらかの新しいサイトを挿入した後に SaveChanges が失敗した場合、トランザクションはロールバックされ、データベースに変更は適用されません。 またコンテキストも、SaveChanges を再度呼び出して変更の適用を再試行できる状態のままになります。

using (var db = new BloggingContext())
{
    db.Blogs.Add(new Site { Url = "http://msdn.com/data/ef" });
    db.Blogs.Add(new Site { Url = "http://blogs.msdn.com/adonet" });
    db.SaveChanges();
}

再試行の実行戦略を使用しない場合は、複数の操作を 1 つのトランザクションにラップすることができます。 たとえば、次のコードは、2 回の SaveChanges 呼び出しを 1 つのトランザクションにラップしています。 どちらか一方の操作の途中でエラーが発生した場合は、どちらの変更も適用されません。

using (var db = new BloggingContext())
{
    using (var trn = db.Database.BeginTransaction())
    {
        db.Blogs.Add(new Site { Url = "http://msdn.com/data/ef" });
        db.Blogs.Add(new Site { Url = "http://blogs.msdn.com/adonet" });
        db.SaveChanges();

        db.Blogs.Add(new Site { Url = "http://twitter.com/efmagicunicorns" });
        db.SaveChanges();

        trn.Commit();
    }
}

再試行の実行戦略を使用している場合、これはサポートされません。先行する操作やその再試行方法が、EF によって認識されないためです。 たとえば、2 つ目の SaveChanges でエラーが発生した場合、その時点ではもう、1 つ目の SaveChanges 呼び出しを再試行するために必要な情報が EF にはありません。

解決策: 実行戦略を手動で呼び出す

これを解決するには、実行戦略を手動で使用し、実行すべき一連のロジック全体を与えます。そうすることで、いずれかの操作に失敗した場合でも、すべての処理を再試行することができます。 DbExecutionStrategy から派生した実行戦略が実行されているときは、SaveChanges で使用される暗黙的な実行戦略が中断されます。

再試行すべきコンテキストはすべて、このコード ブロック内に記述されている必要があることに注意してください。 これにより、再試行のたびにクリーンな状態で開始されます。

var executionStrategy = new SqlAzureExecutionStrategy();

executionStrategy.Execute(
    () =>
    {
        using (var db = new BloggingContext())
        {
            using (var trn = db.Database.BeginTransaction())
            {
                db.Blogs.Add(new Blog { Url = "http://msdn.com/data/ef" });
                db.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/adonet" });
                db.SaveChanges();

                db.Blogs.Add(new Blog { Url = "http://twitter.com/efmagicunicorns" });
                db.SaveChanges();

                trn.Commit();
            }
        }
    });