WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

SessionStateModule によるロック

by WebSurfer 2018年4月6日 18:21

ASP.NET Web アプリで Session を利用する場合、同一ユーザー(= SessionID が同じユーザー)が同時にアクセスすると、最初の要求に受けた時点で Session へのアクセスがロックされるので、最初の要求に対する応答が返されるまで次の要求は待たされるという話を書きます。

(元の話は MSDN フォーラムのスレッド「非同期ポストバック中のキャンセルとその後のポストバックについて」です)

非同期ポストバック中のキャンセル

SessionStateModule によるロックメカニズムはマイクロソフト公式解説書「プログラミング Microsoft ASP.NET 4」という本の 17.2.1 章「セッション状態 HTTP モジュール」の「セッション状態へのアクセスの同期」というセクションに詳しく書いてあります。その一部を抜粋すると:

"セッション状態モジュールはリーダー / ライター方式のロックメカニズムを実装し、状態値へのアクセスをキューで管理します。セッション状態への書き込みアクセスを許可されたページは、リクエストの処理が終了するまで、そのセッションのライターロックを保持します。 ・・・(中略)・・・ セッション状態への読み取りアクセスを許可されたページは、リクエストの処理が終了するまでセッションのリーダーロックを保持します"

上記の通り、Session を使うケースでは、応答が返されるまで Session へのアクセスはロックされ、その間次の要求は処理されないのですが、普通に同期ポストバックを行っている限り、それにユーザーが気が付くことはなさそうです。

ところが、ScriptManager + UpdataPanel を利用して非同期ポストバックで要求を出すようにし、それにキャンセル機能を実装した場合は話が違ってきます。

ユーザーが、処理に長い時間がかかる「要求 A」を出したが、応答が返ってくるのを待ちきれないので、一旦その要求をキャンセルして、処理に時間のかからない(=すぐ応答が返ってくる)「要求 B」を出すというケースを考えてみてください。

ユーザーは、「要求 A」はキャンセルしたのだから、「要求 B」はすぐ処理されて応答が返ってくると期待するはずです。

ところが Session を利用しているアプリの場合はユーザーの期待通りにはなりません。「要求 A」をキャンセルする / しないにかかわらず、「要求 B」の応答が返ってくるのは、普通に「要求 A」を処理する時間だけ待たされた後になります。

ユーザーが「要求 A」をキャンセルしてもサーバー側での実行が中断されるわけではなく、サーバーは「要求 A」を処理して応答を返すところまで行うということがポイントです。

Session を利用していない場合、「要求 B」をサーバーが受けると、先に受けた「要求 A」の処理と並行して、直ちに「要求 B」の処理が始まって応答が返されます。結果、ユーザーの期待通り、処理に時間のかからない「要求 B」の応答はすぐ返ってきます。

ところが、Session を利用している場合、Session ロックによって、「要求 A」の処理が終わって応答が返されるまで、「要求 B」の処理は待たされます。結果、「要求 A」の処理のキャンセルが効いてないように見えます。

下の画像を見てください。この記事の下の方に示したサンプルコードを実行し、Fiddler で要求・応答をキャプチャしたものです。

Fiddler によるキャプチャ

実行手順は以下の通りです。

  1. [非同期]ボタンをクリックして非同期ポストバックによる「要求 A」をかける。(この記事の一番上の画像がその時のもの)
  2. [Cancel]ボタンをクリックして「要求 A」をキャンセル。
  3. [同期]ボタンをクリックして「要求 B」をかける。

上の Fiddler のキャプチャ画像で、キャンセルした上記 1 の要求もサーバー側では正しく処理され、完全な応答が返ってきているのが分かるでしょうか? (ただし、ブラウザで abort されています)

問題のページで Session に書き込んでいる場合は何ともならないですが、そうでない場合は以下の解決策で対応できるはずです。

  1. 他のページで Session を使っているが、問題のページでは使ってなければ EnableSessionState を False に設定する。
  2. 問題のページも Session を使っているが、読み取りのみの場合は EnableSessionState を ReadOnly に設定する。

なお、自分では Session は使っていないつもり(コードで明示的に Session["Data"] = xxx のようなことはしていない)でも、Global.asax に Session_Start ハンドラがあるとすべてのページで Session を使っているのと同じことになります。

Visual Studio のテンプレートを使ってプロジェクトを作ると、Global.asax に空の Session_Start ハンドラが生成されることがありますので注意してください。

最後に、この記事を書くときに検証用に使ったサンプルコードを以下に書いておきます。

<%@ Page Language="C#" EnableSessionState="ReadOnly" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<script runat="server">
    protected void Button_Click(object sender, EventArgs e)
    {
        System.Threading.Thread.Sleep(5000);
    }

    protected void Button2_Click(object sender, EventArgs e)
    {
        
    }
</script>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
  <script type="text/javascript">
  //<![CDATA[
  var manager;

  function pageLoad(sender, args) {
    if (args.get_isPartialLoad() === false) {
      manager = Sys.WebForms.PageRequestManager.getInstance();
      manager.add_initializeRequest(OnInitializeRequest);
      manager.add_endRequest(OnEndRequest);
    }
  }

  function OnInitializeRequest(sender, args) {
    $get("<%=UpdatePanel1.ClientID%>").style.display = "none";
  }

  function OnEndRequest(sender, args) {
    $get("<%=UpdatePanel1.ClientID%>").style.display = "block";
  }

  function AbortPostBack() {
    if (manager.get_isInAsyncPostBack()) {
      manager.abortPostBack();
    }
  }
  //]]>
  </script>

    <style type="text/css">
    #UpdatePanel1 { 
      width: 200px; 
      height: 200px; 
      border: gray 2px solid;
      position: relative;
      float: left; 
      margin-left: 10px; 
      margin-top: 10px;
    }

    #UpdateProgress1 {
      width: 400px; 
      background-color: #FFC080;
      border: gray 1px solid;
      /*bottom: 0%; 
      left: 0px; 
      position: absolute;*/
    }

  </style>

</head>
<body>
    <form id="form1" runat="server">
    <asp:ScriptManager ID="ScriptManager1" runat="server">
    </asp:ScriptManager>

    <asp:Button ID="Button1" runat="server" 
        Text="非同期" OnClick="Button_Click" />  
    <asp:Button ID="Button2" runat="server" 
        Text="同期" OnClick="Button2_Click" />
    <br />
    <asp:UpdatePanel ID="UpdatePanel1" runat="server">
        <ContentTemplate>
            UpdatePanel
            <hr />            
            <%=DateTime.Now.ToString() %> <br />
            
            <br /><br />
            [非同期]ボタンをクリックすると、
            5 秒後にこのパネル内が更新されます。
            その間 UpdateProgress が表示されます。
        </ContentTemplate>
        <Triggers>
            <asp:AsyncPostBackTrigger ControlID="Button1" 
                EventName="Click">
            </asp:AsyncPostBackTrigger>
        </Triggers>
    </asp:UpdatePanel>

    <asp:UpdateProgress ID="UpdateProgress1" runat="server">
        <ProgressTemplate>
            非同期ポストバックで更新中です・・・  
            <input type="button" value="Cancel" 
                onclick="AbortPostBack()" />            
        </ProgressTemplate>
    </asp:UpdateProgress>
    </form>
</body>
</html>

Tags: ,

ASP.NET

ReportViewer と Session

by WebSurfer 2015年9月7日 15:22

ReportViewer は Session を使うという話を書きます。

IE で ReportViewer を表示

上の画像は、ReportViewer を使用してパラメーターを含む詳細 (RDLC) レポートを作成する というチュートリアルに従って作ったものですが、どこにも明示的に Session を使うようなコードは含まれていません。

ところが、このページを要求すると、下の画像(Fiddler2 でキャプチャしたもの)のように応答ヘッダにセッション Cookie が設定され(画像の赤枠で囲った部分)、Session の使用が開始されます。

応答ヘッダのセッション Cookie

知ってました? 実は自分は知らなかったです。(汗) 最近まで ReportViewer と Session は何の関係もないと思っていましたが、MSDN フォーラム でのやり取りを通じてこのことを知りました。

MSDN Blog の記事 Did Your Session Really Expire? によると、ReportViewer は再生するのが簡単ではないデータを保存するのに Session を使うとのことです。

そして、ポストバックの際などに期待したデータが Session にないと AspNetSessionExpiredException をスローするとのことです。

AspNetSessionExpiredException 例外がスローされる原因として次のことが書かれています:

  1. 本当にセッションがタイムアウトした。(例:ping をかけてタイムアウトにならないようにしているが、サーバーが忙しいとか接続が不安定で失敗した)
  2. 要求を処理しているサーバーにセッションがない。(例:セッションのモードが InProc で、Web Garden / Farm 構成になっているとか、ワーカープロセスがリサイクルされた)
  3. ブラウザがクッキーを受け付けない設定になっている。

上記 1 のことは MSDN ライブラリの ReportViewer.KeepSessionAlive プロパティ に書かれています。デフォルトは true で、セッションがタイムアウトしないように自動的に ping がかかります。

上記 2 のワーカープロセスのリサイクルによる Session の消失を防ぐためには、Session の格納場所を InProc ではなく、StateServer または SQLServer にするのがよさそうです。商用に使うのであれば、InProc モードは論外という意見もあるようですし。設定方法の詳細は MSDN ライブラリの記事 セッション状態モード を見てください。

Tags: ,

ASP.NET

Session_Start ハンドラの影響

by WebSurfer 2014年7月26日 17:03

Visual Studio 2010 のテンプレートを使って ASP.NET Web Forms アプリケーション(MVC ではなくて)を新規に作成すると、以下の画像のように、デフォルトで Global.asax ファイルの中に空の Session_Start ハンドラが生成されます。Session State を使わないのであれば、このコードは削除しておいた方がよさそうという話を書きます。

なお、以下の話で Session Cookie という言葉が出てきますが、ブラウザ / サーバー間のセッション ID の授受にクッキーを利用する(即ち、sessionState 要素 の cookieless 属性がデフォルトの UseCookies)ということが前提ですのでご注意ください。

Session_Start ハンドラ

つい先日、MSDN Forum でのやりとりを通じて、Global.asax に Session_Start ハンドラを追加するだけで、サーバーは Session State 情報用のストレージを割り当てて Session Cookie を発行することを知りました。

実は、明示的に Session Sate 情報を使用しない限り(即ち、 Session["Data"] = xxxx; のようなコードを書かない限り)Session Cookie が発行されることはないと思い込んでました。

自分では Session["Data"] = xxxx; のようなコードは書いた覚えはないのに、Fiddler2 などで HTTP ヘッダを見ると Session Cookie がやりとりされていたのが不思議だったのですが、Visual Studio が勝手に追加した Session_Start ハンドラが原因だと知ってやっと謎が解けました。(笑)

調べてみると、そのことは自分が持っている「プログラミング ASP.NET 4」という本の 17.3.1 章に書いてありました。せっかく 9,000 円も出して買った本なのに、読んでなかったです。(汗)

その本によると、"Session_Start ハンドラを使用する場合は、セッション状態は空であっても常に(セッション状態プロバイダーに)保存されます" とのことです。さらに、"Session_Start ハンドラはどうしても必要な場合にのみ細心の注意を払って定義してください" とも書いてありました。

というわけで、Visual Studio のテンプレートで自動生成される空の Session_Start ハンドラは、Session State を使わなければ、例え中身が空であってもアプリケーションに不要な負担をかけるだけなので、削除した方がよさそうです。

自分的には、Session Sate 情報を使用することはあっても、Session_Start ハンドラで何か処置を行う必要があるケースは考え付かないのです。なので、何故テンプレートでこのようなコードが自動的に追加されるのか謎です。


以下は余談ですが、今回の件で Session State に関して調べた情報を書いておきます。

サーバーに接続してくるクライアントごとに Session State をセットアップするのは、ASP.NET の既定の HTTP モジュール SessionStateModule です。

SessionStateModule は、クライアントからの要求を処置する HttpApplication オブジェクトのセットアップ時に呼び出され、一意な セッション ID 文字列の生成または取得と、セッション状態プロバイダ(Session State 情報のストレージ)に対するデータの格納と取得を請け負います。

セッションが開始される時、即ち、要求ヘッダに Session Cookie が含まれない状態でサーバーが初回の要求を受けると、SessionStateModule がセッション ID を生成します。

なお、2 回目の要求以降でも、ブラウザからの要求に Session Cookie が含まれてない場合は、SessionStateModule は要求のたびに新しいセッション ID を生成しますので注意してください。

ちなみに、セッション ID の文字列は、SessionID プロパティ から取得して調べることができます。ブラウザから Session Cookie が送られてこなければ、SessionID の値は要求のたび異なります。

Web アプリケーションの Global.asax に Session_Start ハンドラがあるか、Session State 情報を利用するコード(例えば、Session["Data"] = xxxx;)があると、サーバーは "ASP.NET_SessionId" という名前の Session Cookie を生成し、応答ヘッダに含めてブラウザに送信します。以下のような感じです。

Set-Cookie: ASP.NET_SessionId=d4rbf5nhk1xpo0qouuciwxgy; path=/; HttpOnly

上記の通り期限は設定されてないので、一旦クッキーを受け取るとブラウザを閉じない限りクッキーが消滅することはなく、ユーザーが同じドメイン内で閲覧するときには、ブラウザーはクッキーをサーバーに送信し続けます。

それはセッションがタイムアウト(sessionState 構成要素の timeout 属性に設定した時間が過ぎたことを意味します。デフォルトで 20 分)しても同じです。タイムアウトするとストレージに保存されていたセッションデータは破棄されますが、セッション ID は書き換えられず、同じ ID が使用され続けます。即ち、タイムアウトしてもブラウザとサーバ間のセッションは維持されます。

従って、2 回目以降の要求では毎回 Session Cookie がブラウザからサーバーに送られるので、それに含まれるセッション ID をベースにストレージからデータを取得するなどして HttpSessionState オブジェクトを生成することができます。HttpSessionState オブジェクトには Page の Session プロパティを通じてアクセスできます。

なお、2 回目以降の要求に Session Cookie が送られて来る場合は新しいセッションではないと判断され、Session_Start イベントは発生しないというのがデフォルトの動きになるようです。

なお、上にも書きましたが、Session_Start ハンドラがないもしくは Session State 情報を利用するコードがない場合は、サーバーからは Session Cookie は発行されず、上記とは異なった動きとなりますので注意してください。

以下は上にリンクを張った以外に参考にしたページの URL です。後で探さなくて済むよう貼っておきました。

Tags:

ASP.NET

About this blog

2010年5月にこのブログを立ち上げました。その後 ブログ2 を追加し、ここは ASP.NET 関係のトピックス、ブログ2はそれ以外のトピックスに分けました。

Calendar

<<  2018年8月  >>
2930311234
567891011
12131415161718
19202122232425
2627282930311
2345678

View posts in large calendar