2010年4月10日土曜日

GAE/J + BlazeDS 環境の DuplicateSessionDetected 問題

GAE/J + BlazeDS 環境では、以下のようなエラーメッセージが出ることがあります。
Detected duplicate HTTP-based FlexSessions, generally due to the remote host disabling session cookies. Session cookies must be enabled to manage the client connection correctly." faultCode="Server.Processing.DuplicateSessionDetected

この問題について、AppEngine & Adobe BlazeDS (fix) では flex.messaging.endpoints.BaseHTTPEndpoint クラスのソースにパッチを当てる方法でエラーを回避しています。

今回は、この問題を少し掘り下げて考えてみようと思います。


確かにエラーは出なくなるけど、、、。いいのか?

問題箇所をコメントアウトすればエラーは出なくなります。しかし、コメントアウトされた箇所のコードにも意味があるわけで、とりあえずコメントアウトというのはどうも気持ちがわるいです。

ということで、BaseHTTPEndpoint.java を見てみました。もし GAE 側で session の cookie の扱いに不備があるのであれば、確かに下記のコードで duplicateSessionDetected が true になりそうです。
boolean duplicateSessionDetected = (FlexContext.getHttpRequest().getAttribute(REQUEST_ATTR_DUPLICATE_SESSION_FLAG) != null) ? true : false;

ということで、ログを仕込んで確認してみたところ、結果は false でした。

ということは、怪しい箇所は以下ということになりますね。
if (!duplicateSessionDetected)
{
    List sessions = flexClient.getFlexSessions();
    int n = sessions.size(); // <- suspicious!
    if (n > 1)
    {
        int count = 0;
        for (int i = 0; i < n; i++)
        {
            if (sessions.get(i) instanceof HttpFlexSession)
                count++;
            if (count > 1)
            {
                FlexContext.getHttpRequest().setAttribute(REQUEST_ATTR_DUPLICATE_SESSION_FLAG, Boolean.TRUE);
                duplicateSessionDetected = true;
                break;
            }
        }
    }
}

ログを仕込んで確認したところ、flexClient.getFlexSessions().size() の値はローカルだと常に 1 ですが、GAE 環境だと 2 度目のアクセスで 2 になることを確認しました。しかし 3 にはなりませんでした。

これは、2 になった場合に duplicateSessionDetected = true になり、session の invalidate を行う if ブロックが実行されることが影響していると思われます。ということは、その if ブロックをコメントアウトしてしまうと、アクセスされるたびに flexClient.getFlexSessions() が1つずつ増えていくということが考えられます。

実際にログを仕込んで確認したところ、やはりアクセスのたびに flexClient.getFlexSessions() が 1 つずつ増える現象が確認できました。

となると、FlexClient に FlexSession を格納するところが怪しいですよね。ということで、session オブジェクトの格納箇所を見てみました。
public void registerFlexClient(FlexClient flexClient)
{
    if (flexClients.addIfAbsent(flexClient))
    {
        flexClient.addClientDestroyedListener(this);
        flexClient.registerFlexSession(this);
    }
}

flexClients は java.util.concurrent.CopyOnWriteArrayList 型です。

flexClients.addIfAbsent(flexClient) によって、既に flexClient が flexClients に含まれている場合は無視するようなプログラムになっています。

しかし、同一性の判断はどうしているのか気になります。

予想通り、equals メソッドを適切に実装していませんでした。そのため、同一セッションをあらわすオブジェクトでも、デシリアライズされたインスタンス同士が別物として扱われてしまい、GAE のローカル環境ではうまく動くのに本番環境ではうまく動かなかったというわけです。

ということで、HttpFlexSession クラスに以下のメソッドを追加して問題解決です。(たぶん)
@Override
public boolean equals(Object obj) {
 if (!FlexSession.class.isInstance(obj)) return false;
 return FlexSession.class.cast(obj).getId().equals(getId());
}

@Override
public int hashCode() {
 return httpSession != null ? getId().hashCode() : super.hashCode();
}


もしくは、FlexSession クラス以下に以下のメソッドとか。
@Override
public boolean equals(Object obj) {
 if (!FlexSession.class.isInstance(obj)) return false;
 return FlexSession.class.cast(obj).getId().equals(getId());
}

@Override
public int hashCode() {
 if (HttpFlexSession.class.isInstance(this) && HttpFlexSession.class.cast(this).httpSession != null) { // to avoid NPE.
  return getId().hashCode();
 } else {
  return super.hashCode();
 }
}

0 件のコメント:

コメントを投稿