時間のかかるクエリでもブラウザをタイムアウトさせない (unibon)

1999年10月10日: 新規作成。
2000年03月05日: Web サーバにアップロードし忘れていたので、アップロードして公開した。
ASP から DBMS に対して時間のかかるクエリを実行してもブラウザをタイムアウトさせないようにします。
ASP の処理に時間がかかり、response が長時間に渡って中断していると、ブラウザがタイムアウトしてしまいます。IE のデフォルトは 5 分です(IE の新しいバージョンでは、レジストリの設定によりこの期間を変更することができますが、すべてのブラウザについておこなう必要があります)。このタイムアウトの期間は、最後に response してから経過した時間であり、最初の response から経過した時間ではないようです。
したがって、定期的に response をおこなえば、何時間でもタイムアウトさせずにブラウザを待たせることができます。

しかし、ASP の中で DBMS にクエリを投げていて、そのクエリの実行に時間にかかる場合は、response を返すことができません。これを解決するには、「非同期クエリ」を使います。「非同期クエリ」を使えば、クエリを実行中も、ASP の VBScript が処理をする余地が生まれますので、その間に Response.Write により、ブラウザに response を返すことができ、ブラウザがタイムアウトすることを抑制できます。

本来、「非同期クエリ」を使う場合は、クエリの完了を「イベント」として捕らえて、即座に処理を継続すべきですが、ASP には「イベント」を捕らえる機能がないため、「ポーリング」により完了かどうかを定期的に検査しています。「ポーリング」の間隔を得るためには、時間待ちをするモジュールを呼ぶ方法もありますが、ASP や VBScript に標準では備わっていないので,ここでは DBMS の機能としての MS SQL Server の WAITFOR を呼び出して代用しています。

以下、DBMS として MS SQL Server を対象とした例です。なお、DBMS によっては、「非同期クエリ」の機能がなかったり、あるいは WAITFOR に相当する機能がなかったりする場合も考えられます。その場合は、前者ならば諦めるしかないのですが、後者ならば上述のようになんらかの時間待ちをするモジュールを使うことで解決できるでしょう。簡単には VBScript で For 〜 Next で無駄にループする方法もありますが、CPU 負荷を無駄に消費してしまうのでその点からは好ましくありません。


使用例:
<%@Language=VBScript EnableSessionState=False%>
<%Option Explicit%><HTML>
<HEAD>
<TITLE>非同期クエリ(MS SQL Server 用)</TITLE>
</HEAD>
<BODY>
<%
Const adOpenForwardOnly = 0
Const adOpenKeyset = 1
Const adOpenDynamic = 2
Const adOpenStatic = 3

Const adLockReadOnly = 1
Const adLockPessimistic = 2
Const adLockOptimistic = 3
Const adLockBatchOptimistic = 4

Const adAsyncExecute = &H10
Const adAsyncFetch = &H20
Const adAsyncFetchNonBlocking = &H40
Const adExecuteNoRecords = &H80

Const adStateClosed = 0
Const adStateOpen = 1
Const adStateConnecting = 2
Const adStateExecuting = 4
Const adStateFetching = 8

Server.ScriptTimeout = 900 ' クエリよりも、そもそも ASP の VBScript がタイムアウトしないための設定として、クエリにかかる時間と WAITFOR の時間の和よりも、さらに余裕を持って設定する。

Dim ConnectionString
ConnectionString = "Provider=SQLOLEDB.1;Persist Security Info=False;Initial Catalog=pubs;Data Source=YOURDBSERVER" ' 接続用文字列(クエリ用とタイマ用に兼用する)。
Dim uid
uid = "sa" ' 接続用ユーザID(クエリ用とタイマ用に兼用する)。
Dim pwd
pwd = "" ' 接続用パスワード(クエリ用とタイマ用に兼用する)。

Dim adoWait
Set adoWait = Server.CreateObject("ADODB.Connection") ' ポーリングのためのタイマ用の接続。
adoWait.CommandTimeout = 100 ' WAITFOR に指定する時間に見合うだけの時間にさらに少し余裕を持って設定する。
Call adoWait.Open(ConnectionString, uid, pwd)

Dim adoConn
Set adoConn = Server.CreateObject("ADODB.Connection") ' 本来のクエリのための接続。
adoConn.CommandTimeout = 900 ' クエリがタイムアウトしないための設定。
Call adoConn.Open(ConnectionString, uid, pwd)

Dim sqlString
sqlString = "WAITFOR DELAY '00:08:00' SELECT * FROM pubs.dbo.authors" ' ここの WAITFOR はテスト用に故意に時間を消費するためのダミーであり、本番時は不要。なおクエリにはたとえば MS SQL Server に付属のサンプルデータベース pubs を使用する。

Dim adoRs
Set adoRs = Server.CreateObject("ADODB.Recordset")
Call adoRs.Open(sqlString, adoConn, adOpenForwardOnly, adLockReadOnly, adAsyncExecute) ' 「非同期クエリ」としてクエリを投げる。

Do While adoRs.State And adStateExecuting
    Call Response.Write("<!-- " & Now() & " -->" & vbNewLine) ' ブラウザをタイムアウトさせないための response として HTML のコメントを送出する。
    Call adoWait.Execute("WAITFOR DELAY '00:01:30'") ' たとえば 90 秒間待機する。この期間は長すぎると反応が鈍くなり、あまり短すぎると無駄にコストがかかるので、適切な値を指定する。
Loop

Call adoWait.Close()
Set adoWait = Nothing

' 以降は通常の処理として、レコードセットの response をおこなう。

Call Response.Write("<TABLE BORDER=1>" & vbNewLine)

Call Response.Write("<TR>" & vbNewLine)
Dim i
For i = 0 To adoRs.Fields.Count - 1
    Call Response.Write("<TH>" & adoRs.Fields(i).Name & "</TH>" & vbNewLine)
Next
Call Response.Write("</TR>" & vbNewLine)

Do Until adoRs.EOF
    Call Response.Write("<TR>" & vbNewLine)
    For i = 0 To adoRs.Fields.Count - 1
        Dim s
        s = adoRs.Fields(i).Value
        If Len(s) > 0 Then
            Call Response.Write("<TD>" & s & "</TD>" & vbNewLine)
        Else
            Call Response.Write("<TD><BR></TD>" & vbNewLine)
        End If
    Next
    Call adoRs.MoveNext()
    Call Response.Write("</TR>" & vbNewLine)
Loop

Call Response.Write("</TABLE>" & vbNewLine)
Call adoRs.Close()
Call adoConn.Close()
Set adoRs = Nothing
Set adoConn = Nothing
%>
</BODY>
</HTML>

ASP の目次
ホーム
(このページ自身の絶対的な URL)