[Java] Is WebSocket Session really thread safe?

原文はこちら。
https://blogs.oracle.com/PavelBucek/entry/is_websocket_session_really_thread

今日のエントリは、JSR-356(Java API for WebSocket)の一つのシンプルなユースケースに関するものです。
JSR 356: JavaTM API for WebSocket
https://jcp.org/en/jsr/detail?id=356
Mark Thomasが jsr356-expertsメーリングリストで開始した比較的長いスレッドに注目しました。
[jsr356-experts] Clarification required on sending messages concurrently
https://java.net/projects/websocket-spec/lists/jsr356-experts/archive/2015-01/message/0
ここで提起されている問題は、以下のようなコードで単純化できます。
session.getAsyncRemote().sendText(message);
session.getBasicRemote().sendText(message);
最初のメッセージを非同期に送信し、2番目のメッセージを同期的に送信する、これのどこに問題なのかと不思議に思われるかもしれません。(クラスレベルのJavadocの最後の段落にあるように)Sessionオブジェクト自体はスレッドセーフですが、問題はコンテナ自身にあります。もっと正確に言えば、RemoteEndpoint.Basicインターフェイスに問題があるのです。以下はドキュメントの引用です。
Session (WebSocket server API 1.1 API)
https://tyrus.java.net/apidocs-javax.websocket/1.1/javax/websocket/Session.html
RemoteEndpoint.Basic (WebSocket server API 1.1 API)
https://tyrus.java.net/apidocs-javax.websocket/1.1/javax/websocket/RemoteEndpoint.Basic.html
If the websocket connection underlying this RemoteEndpoint is busy sending a message when a call is made to send another one, for example if two threads attempt to call a send method concurrently, or if a developer attempts to send a new message while in the middle of sending an existing one, the send method called while the connection is already busy may throw an IllegalStateException.
(このRemoteEndpointをベースとするWebSocket接続がメッセージを送信でビジー状態にある場合、別のメッセージを送信するよう呼び出しを実行すると、具体的には2個のスレッドが送信メソッドを同時に呼び出そうとしたり、既存のメッセージを送信する途中で開発者が新たなメッセージを送信しようとしたりすると、既に接続がビジーな状態である最中に呼び出された送信メソッドは、IllegalStateExceptionを送出することがあります。) 
IllegalStateException (Java Platform SE 6)
http://docs.oracle.com/javase/6/docs/api/java/lang/IllegalStateException.html?is-external=true
この段落からわかることは、前のメッセージが送信されていない場合、(上記サンプルの)2行目は、IllegalStateExceptionがスローされる可能性がある、ということです。正直に言うと、リファレンス実装では、(これまで)その例外は送出されず、どのサンプルアプリケーションやWebSocketアプリケーションではそのような状態をチェックしていません。とにかく、javadocは、例外を送出する必要がある、とは言っていません。そのため、上記サンプルについては、両メッセージを送信するまで2個目のブロックすることでOKです。

残念ながら、これで終わりというわけではありません。WebSocket APIはパーシャルメッセージの送信が可能であり、PartialメッセージやWholeメッセージをこのコンテキスト内で混在させる場合、少々難儀なことになります。以下の例を考えてみましょう。
session.getBasicRemote().sendText(message, false); // partial msg
session.getBasicRemote().sendBinary(data);
session.getBasicRemote().sendText(message, true); // partial msg
この例では、2番目のメッセージは3行目でのメッセージの前に送るべきではありません。シングルスレッドでこの順番で実行すると問題が発生します。具体的には、2行目のコードがTextメッセージの完了までブロックすると、問題が発生します。実装では何が起こるか想定出来ませんし、一般的に、スレッドを長時間にわたってブロックしたくないので、IllegalStateExceptionをスローすることは比較的よいことのように思えます。

(追記:リファレンス実装であるTyrusは、このシナリオを正しく処理していませんでした。この機能に関連する問題は、TYRUS-393として登録され、修正が次回のリリースバージョンである1.10に含まれる予定です)
Project Tyrus : JSR 356: Java API for WebSocket - Reference Implementation
https://tyrus.java.net/
[TYRUS-393] Incorrect handling of sending whole message during (unfinished) partial message.
https://java.net/jira/browse/TYRUS-393
これは仕様のグレーな部分(起こるかもしれないのであって、必ず発生するとか、発生しないというわけではない)であるため、実装では、好きなほうを選択できます。実際、スレッドのブロックとメッセージを送信するまで待機することが妥当と信じていますし、それが仕様に準拠したアプローチと考えています。Tyrusのために、少々守備的なソリューションを実装しました。具体的には、実装が短時間待機(スレッドをブロック)し、メッセージ送信の条件が許さない場合には、IllegalStateExceptionを送出する、というような具合です。

結論としては、あまり明確ではありませんが、WebSocketのSessionオブジェクトは、確かにスレッドセーフです。しかし、このAPIの一般的なユーザーが驚いたり、混乱を招いたりするかもしれないいくつかの条件と状態があります。とはいえ、WebSocket Sessionを使う際には、ルールを守れば、こうしたことは全て防ぐことができます。例えば、wholeメッセージを同期送信だけしている場合には何も気にする必要はありあせん。非同期だけで送信している場合も同様ではありますが、実装の中でいくつかの矛盾が発生する場合があります。partialメッセージを送信したい、もしくは送信の必要がある場合、問題の解決が難しくなる可能性があります。というのは、WebSocket Sessionが現在の状況をチェックする方法を提供していないため、どこかに状態を保存し、send*メソッドを呼び出す前にチェックする必要があるからです。現時点では、作成されたアプリケーションはRemoteEndpoint.Basic.send* メソッドから送出されるIllegalStateExceptionの処理を追加しておくべき、とアドバイスいたします。特に、別のJSR-356実装上で実行することができることを考えている場合には追加しておきましょう。

0 件のコメント:

コメントを投稿