【イベントハンドラについて】

はじめに

このフレームワークでは通信状態のステータス管理を可能にするため、イベントハンドラをキューとステータスUNIT(単にUNITとも言います)という単位に分けて最適化を行います。
また、各UNITは非同期で動作しますので、他のクライアントの通信を妨げる事なく複雑で柔軟なシーケンスに対応できるようになっていて、ここではその概念的なものを押さえておきます。

キューとUNITの事をざっくり説明すると、以下のような関係になります。
■UNIT(ステータスUNIT)

    イベント処理の最小単位

■キュー

    1つのイベントを処理するためのUNITの集合(言わば静的に配置されたタスクの待ち行列のようなもの)
                    

このアーキテクチャは、後に出てくるプロトコルやサーバーコンテンツを実装する際の共通インタフェースとして機能し、その組み合わせ次第で様々なプロトコルやコンテンツに対応できる設計になっています。

以降では、チャットサーバーの実装を仮定して、イベントが発生してからキューとUNITを使ってどのように処理されるのか、その内容を見ていきます。

イベント処理の基本構成

チャットサーバーと言えば、クライアントからメッセージを受信して全員に配信するという実装が一番シンプルですが、ここでは特定の相手に対するプライベートメッセージの扱いも兼ねて、以下のようなイベント処理を基本構成として見ていきます。
■チャットメッセージの受信

    【処理】受信したメッセージを全員に配信する

■プライベートメッセージの受信

    【処理】配信先の相手を検索して、受信したメッセージをその相手に配信する
                    

これをキューとUNITで構成すると以下のようになります。
■キュー名:CHAT_MESSAGE

    ①ステータス名:start

        【処理】受信したメッセージを全員に配信する
        【戻り値】nullを返して処理終了

■キュー名:PRIVATE_MESSAGE

    ①ステータス名:start

        【処理】配信先の相手を検索して、受信したメッセージをその相手に配信する
        【戻り値】nullを返して処理終了
                    

上記の例ではチャットメッセージとプライベートメッセージの各イベントにキュー名を割り当て、個々の処理単位にUNITとしてのステータス名を1つだけ割り当てています。
(最初に実行されるステータス名はstartと決まっています)
そしてUNITの戻り値で次に呼び出すUNITを決定し、nullを返すまでUNIT処理を繰り返すルールとなっています。

以降では、これを基本構成としてUNITの代表的な使用例を見ていきます。

再利用UNITとして使う

チャットコンテンツはサーバー側でログを記録しているケースが多いと思います。

そこで、受信したメッセージをログに記録する部分を冒頭の基本構成に組み込んでUNITを再構成してみます。
■キュー名:CHAT_MESSAGE

    ①ステータス名:start

        【処理】受信したメッセージをログに記録して次のステータス(send)へ遷移する
        【戻り値】send

    ②ステータス名:send

        【処理】受信したメッセージを全員に配信する
        【戻り値】nullを返して処理終了

■キュー名:PRIVATE_MESSAGE

    ①ステータス名:start

        【処理】受信したメッセージをログに記録して次のステータス(send)へ遷移する
        【戻り値】send

    ②ステータス名:send

        【処理】配信先の相手を検索して、受信したメッセージをその相手に配信する
        【戻り値】nullを返して処理終了
                    

上記のように、startステータスで受信したメッセージをログに記録する処理を行い、sendステータスでメッセージを配信するようにUNITを構成しました。
御覧の通り、startステータスはチャットメッセージとプライベートメッセージの両方で同じ内容になるので、一つの共通処理UNITとして利用する事が可能です。
UNITの使い方をこのように工夫する事で、イベント処理の構造化プログラミングが可能になります。

※UNIT内ではどのキューで呼ばれたのかを判断できるメソッドがあるので、キューの種類に応じてログの内容を仕分ける事が可能です。

ポーリングUNITとして使う

通常のチャットメッセージは基本的に同プロセス内での配信を想定していますが、プライベートメッセージを送信する場合は相手が常に同じプロセス内に居るとは限りません。
そのためマルチサーバー構成の場合は、相手を検索するためにサーバー間通信(IPC)を使うケースが出てきます。

そこで、サーバー間通信部分を冒頭の基本構成にあるプライベートメッセージの処理に組み込んでUNITを再構成してみます。
■キュー名:PRIVATE_MESSAGE

    ①ステータス名:start

        【処理】
            ・配信先の相手が同プロセス内で検索して見つかった場合、受信したメッセージをその相手に配信して終了する
            ・配信先の相手が同プロセス内で検索して見つからなかった場合、次のステータス(search_request)へ遷移する
        【戻り値】search_request or null(処理終了)

    ②ステータス名:search_request

        【処理】他のプロセスに配信先相手の検索とメッセージ配信を依頼して次のステータス(search_response)へ遷移する
        【戻り値】search_response

    ③ステータス名:search_response

        【処理】
            ・他のプロセスからのレスポンスを受信するまで、自身のステータス(search_response)を返してポーリングを継続する
            →メッセージが無事に送信できた場合、送信結果成功として送信元へレスポンスを返す
            →メッセージが送信できなかった場合、送信結果失敗として送信元へレスポンスを返す
        【戻り値】search_response or nullを返して処理終了
                    

上記のように、search_requestステータスで配信先相手の検索とメッセージ配信を依頼して、search_responseステータスで他のプロセスからのレスポンスを受信するようにUNITを構成しました。
御覧の通り、search_requestsearch_responseステータスに分ける事で、非同期処理を維持したままサーバー間通信を行い、search_responseUNIT内で自身のステータスを返し続ける事でポーリングを行う事ができます。

リトライUNITとして使う

無線通信を含む通信データのやりとりでは、ノイズや電波障害などの影響で通信が途切れたり、データが欠損して通信に失敗する事があります。
それを補うためにリトライ処理を施して通信の安定性を向上させる事でリカバリーできる事があります。

そこで、リトライ処理を先ほどのポーリングUNITの処理に組み込んで再構成してみます。
■キュー名:PRIVATE_MESSAGE

    ①ステータス名:start

        【処理】
            ・配信先の相手が同プロセス内で検索して見つかった場合、受信したメッセージをその相手に配信して終了する
            ・配信先の相手が同プロセス内で検索して見つからなかった場合、次のステータス(search_request)へ遷移する
        【戻り値】search_request or null(処理終了)

    ②ステータス名:search_request

        【処理】他のプロセスに配信先相手の検索とメッセージ配信を依頼して次のステータス(search_response)へ遷移する
        【戻り値】search_response

    ③ステータス名:search_response

        【処理】
            ・他のプロセスからのレスポンスを受信するまで、自身のステータス(search_response)を返してポーリングを継続する
            →メッセージが無事に送信できた場合、送信結果成功として次のステータス(source_response)へ遷移する
            →メッセージ送信が失敗、かつリトライ回数が3回に到達していない場合、前のステータス(search_request)へ遷移してリトライする
            →メッセージ送信が失敗、かつリトライ回数が3回に到達した場合、送信結果失敗として次のステータス(source_response)へ遷移する
        【戻り値】search_request or source_response

    ④ステータス名:source_response

        【処理】前のステータスから受け取った送信結果を添えて送信元へレスポンスを返す
        【戻り値】nullを返して処理終了
                    

上記のように、search_responseステータスでリトライ回数の判定を行い、リトライ時にはsearch_requestステータスへ遷移させる事で繰り返し処理が行えるようUNITを構成しました。

※UNIT内ではリトライ回数などの任意のデータをディスクリプタ(クライアント接続子)に保持できるメソッドがあります。

UNIT集合の種類

これまでの説明で使ってきたキューやUNIT群は、サーバーのコンテンツ部分にあたるコマンドUNIT(あるいはコマンド部)というカテゴリに分類されますが、これに台頭するプロトコルUNIT(あるいはプロトコル部)というものも存在します。

これら二つのカテゴリの概要は次の通りです。
■コマンドUNIT(コマンド部)

    プロトコルUNITで受信したデータを元にディスパッチされるUNITの集合。
    チャットサーバーで言えばチャットメッセージやプライベートメッセージの配信などのコンテンツ部分に相当します。

■プロトコルUNIT(プロトコル部)

    クライアントと直接通信する時に呼び出されるUNITの集合。
    有名なプロトコルで言えばHTTPやWebsocket等。オリジナルプロトコルも含みます。
                    

これを踏まえると、以下の関係が成り立ちます。
■UNIT(ステータスUNIT)

    イベント処理の最小単位

■キュー

    一つのイベントを処理するためのUNITの集合(言わば静的に配置されたタスクの待ち行列のようなもの)

■コマンドUNIT(コマンド部)

    サーバーのコンテンツ部分にあたるキューの集合

■プロトコルUNIT(プロトコル部)

    クライアントとの通信部分にあたるキューの集合
                    

キューの集合が2種類に分かれているのは、同じサーバーコンテンツ(コマンドUNIT)であってもプロトコルを変更したり、同じプロトコルであってもサーバーコンテンツを入れ替えたりする事が可能になるからです。
このようにそれぞれを独立して実装する事で、サーバーの組み合わせのバリエーションが選べるようになっています。

このフレームワーク環境ではコマンドUNITやプロトコルUNITのそれぞれを独自のクラスで管理しています。
メイン処理クラスを生成する場合と同じように、これらのクラスもコマンドでひな形を生成できるので、デベロッパーはクラス内のキューとUNITの開発に集中して取り組む事ができます。

おわりに

今回は触れていませんが、例えばチャットメッセージのログをデータベースへ記録するケースも考えられます。
特にトランザクションが発生する書き込みの場合は少なからず処理のブロッキングが発生します。

データ量が少ない時にはあまり問題にならないかもしれませんが、トラフィックのボトルネックとして影響が出てくるようであれば、データベースオペレーション専用のサーバーを起てておき、ポーリングUNITの項でご紹介したようなサーバー間通信を使う方法で対処した方がいいでしょう。

このフレームワークは非同期処理を基本としているので、動的なプロセスのフォークやスレッドの生成は必要ありません。
そのためマルチサーバー環境を構築する際は、スケーラビリティと安定したリソースを確保するため、プロセスの絶対数による管理を推奨しています。