C# gRPC チャネルの状態と自動再接続


gRPC 1.16.0 で、Channel クラスに TryWaitForStateChangedAsync メソッドが追加されました。これを機に、チャネルの状態と自動再接続についてまとめてみます。

## チャネルの状態 ##

チャネルの状態は次の 5 つで表され、C# gRPC では Grpc.Core.ChannelState 列挙体として定義されています。

|列挙値|説明|
|:--|:--|
| Idle | 何も行われていない状態。 |
| Connecting | 接続を試みている状態。接続が成功すると Ready に移行し、失敗すると TransientFailure に移行します。 |
| Ready | 接続が確立しており、通信を実行できる状態。 |
| TransientFailure | 接続に失敗している状態。チャネル内部で定期的に接続状態が復旧できるかが試行され、成功するまで Connecting と TransientFailure の状態が繰り返されます。失敗するたびに試行の間隔が長くなっていきます。 |
| Shotdown | シャットダウンされた状態。 |

参考）gRPC の公式サイト
[【GitHub】gRPC Connectivity Semantics and API](https://github.com/grpc/grpc/blob/master/doc/connectivity-semantics-and-api.md)

## チャネルで提供されている機能 ##

Grpc.Core.Channel クラスの接続に関するプロパティとメソッドです。

### State プロパティ ###

* 現在のチャネルの接続状態を取得できます。

### ConnectAsync メソッド ###

* 接続します。
* 明示的にこのメソッドを呼び出さなくても、Idol 状態で RPC 処理が呼び出された場合には接続が行われます。

### WaitForStateChangedAsync メソッド ###

* 接続状態が指定した値から別の値に変更されるまで待機します。
* 待機開始から一定時間接続状態が変更されなかった場合には待機が終了します。

### TryWaitForStateChangedAsync メソッド ###

* 接続状態が指定した値から別の値に変更されるまで待機します。
* 待機開始から一定時間接続状態が変更されなかった場合には待機が終了し、戻り値として false を返します。WaitForStateChangedAsync メソッドとの違いは、接続状態が変更されたかどうかを戻り値で判断できることです。

## チャネルの状態の監視 ##

チャネルの状態を監視し、状態が変更されたときに処理を行う例です。
TryWaitForStateChangedAsync メソッドが true を返したときに状態が変更されたと見なしています。メソッド呼び出し前後の状態は敢えて比較していません。後で触れます。

```C#

private Channel m_Channel;
private void InitChannel()
{
    m_Channel = new Channel("127.0.0.1:50001", ChannelCredentials.Insecure);
    var nowaitTask = Task.Run(() => MonitorChannelStateAsync(m_Channel));
}

/// <summary>
/// チャネルの状態を監視します。
/// </summary>
private async Task MonitorChannelStateAsync(Channel channel)
{
    while (channel.State != ChannelState.Shutdown)
    {
        // 状態が変更されるまで待機する
        ChannelState state = channel.State;
        if (await channel.TryWaitForStateChangedAsync(state).ConfigureAwait(false))
        {
            // 状態が変更されたときの処理を実行する
            OnChannelStateChanged(state, channel.State);
        }
    }
}

/// <summary>
/// チャネルの状態が変更されたときの処理を行います。
/// </summary>
private void OnChannelStateChanged(ChannelState lastState, ChannelState currentState)
{
    ++m_Counter;
    System.Diagnostics.Debug.WriteLine("({0}) {1} {2} --> {3}"
        , m_Counter
        , DateTime.Now.ToString("HH:mm:ss")
        , lastState
        , currentState
    );
}
private int m_Counter = 0;

--- コンソール出力結果（ランタイムが出力するログは除いています） ---
(1) 16:11:32 Idle --> Ready
(2) 16:11:33 Ready --> Idle
(3) 16:11:35 Idle --> Connecting
(4) 16:11:37 Connecting --> Connecting
(5) 16:11:39 Connecting --> Connecting
(6) 16:11:41 Connecting --> TransientFailure
(7) 16:11:42 TransientFailure --> Connecting
(8) 16:11:44 Connecting --> TransientFailure
(9) 16:11:46 TransientFailure --> Connecting
(10) 16:11:48 Connecting --> TransientFailure
(11) 16:11:53 TransientFailure --> Ready
(12) 16:11:58 Ready --> Idle
(13) 16:11:59 Idle --> Connecting
(14) 16:12:01 Connecting --> Connecting
(15) 16:12:12 Connecting --> Connecting
(16) 16:12:26 Connecting --> Connecting
(17) 16:12:56 Connecting --> Ready
(18) 16:13:09 Ready --> Idle
(19) 16:13:12 Idle --> Connecting
(20) 16:13:14 Connecting --> TransientFailure
(21) 16:13:19 TransientFailure --> Connecting
(22) 16:13:21 Connecting --> TransientFailure
(23) 16:13:31 TransientFailure --> Connecting
(24) 16:13:33 Connecting --> TransientFailure
(25) 16:13:49 TransientFailure --> Connecting
(26) 16:13:51 Connecting --> TransientFailure
(27) 16:14:11 TransientFailure --> Connecting
(28) 16:14:13 Connecting --> TransientFailure
(29) 16:15:01 TransientFailure --> Connecting
(30) 16:15:03 Connecting --> TransientFailure
(31) 16:15:04 TransientFailure --> Shutdown

```

(1) 接続または RPC 処理を実行。
(2) サーバーアプリケーションを終了。
(3) サーバーアプリケーションを終了させたままの状態で接続または RPC 処理を実行。
(11) サーバーアプリケーションを再起動。しばらくすると自動的に接続される。
(12) サーバーアプリケーションを終了。
(13) サーバーアプリケーションを終了させたままの状態で接続または RPC 処理を実行。
(17) サーバーアプリケーションを再起動。しばらくすると自動的に接続される。
(18) サーバーアプリケーションを終了。
(19) サーバーアプリケーションを終了させたままの状態で接続または RPC 処理を実行。
(31) シャットダウンを実行。

### ログからわかること ###

* (4)～(5)、(14)～(16) のように、TryWaitForStateChangedAsync メソッドが true を返す場合でも状態の値が変わらないことがある。（待機が終了した直後に再び状態が変わっている？）
* 接続が切断された後、自動的に再接続が行われる。
* 接続に失敗したときの再試行の間隔は徐々に長くなっていく。障害や遅延が瞬間的なものであればすぐに自動的に再接続されるが、しばらくその状態が続くと復旧してもすぐに再接続されるとは限らない。