Title: Adding request Queuing and Thread Pooling to your ISAPI .DLL
Question: My ASP performs better (under load) than my ISAPI DLL written in Delphi 5 using ISAPIApp. They both perform the same tasks i.e. execute stored procedures (using ADO 2.5) and serve the resulting data back to the client.
We used Microsoft's Web Stress Tool (http://webtool.rte.microsoft.com) to load the ASP and then the DLL with 100 simultaneous requests and Mr. DLL lost the race.
Answer:
We knew that ASP used thread pooling and, without really
understanding why, we added thread pooling to the DLL. And what do
you know, Mr. ASP never won again.
SIDE AFFECT:
===========
A benefit actually. Queuing of ISAPI request is implicitly added to
your solution. As you know (or will realize in a production
environment) WebBroker/ISAPIApp does NOT queue requests. What I mean
is if you have set your Application.MaxConnections to x and you
receive x+1 simultaneous hits WebBroker generates a "500 Internal
Server Error" - a big time no-no in a production environment.
Anyway - attached is the unit we created which sits on top of
ISAPIApp to auto-magically transforms your app into a thread pooling,
request queuing - machine.
Before deciding to use this please use the Microsoft Web Stress Tool
to test you app - once without pooling and again with pooling. You
may not require it.
====
unit uISAPIThreadPool;
(*
SUMMARY
=======
This unit implements a thread pool for a Delphi 5 ISAPI Web
Application.
If all threads are busy in the thread pool the request are
queued, unlike
TISAPIApplication that just generates a "500 Internal Server
Error"
ENVIRONMENT
===========
Tested on Win2k Professional/Server and IIS 5.
USE:
====
Put this unit in the uses clause (in the main web project
source) after the ISAPIApp enty
as this unit overrided the 3 exports (HttpExtensionProc,
GetExtensionProc & TerminateExtension).
Currently an instance of TThreadPool is created with a pool size
of 25. This seems to be
optimum. The Microsoft MFC examples use a pool of 10. Play
around with the pool size to
achieve maximum performance.
Author: Quinton Bernhardt
email: quinton@frooky.co.za
*)
interface
uses ISAPI2,Windows;
function GetExtensionVersion(var Ver: THSE_VERSION_INFO): BOOL;
stdcall;
function HttpExtensionProc(var ECB: TEXTENSION_CONTROL_BLOCK): DWORD;
stdcall;
function TerminateExtension(dwFlags: DWORD): BOOL; stdcall;
implementation
uses ISAPIApp, SysUtils, Classes;
type
TThreadWorkData = Record
ECB: PEXTENSION_CONTROL_BLOCK;
SecurityToken: THandle;
end;
PThreadWorkData = ^TThreadWorkData;
TThreadPool = class
private
FPool: Array of THandle;
FCompletionPortID: THandle;
public
function newInstanceData(ECB: PEXTENSION_CONTROL_BLOCK;
SecToken: THandle): PThreadWorkData;
constructor Create(InitThreads: Integer);
function postWorkItem(ECB: PEXTENSION_CONTROL_BLOCK):
LongBool;
destructor Destroy; override;
property CompletionPortID: THandle read FCompletionPortID;
end;
var ThreadPool: TThreadPool;
function WorkerFunc(Context: TThreadPool): Integer;
var SLen: Cardinal;
SData: PThreadWorkData;
OL: POverLapped;
CompletionPortID: THandle;
begin
CompletionPortID:= Context.CompletionPortID;
While GetQueuedCompletionStatus(CompletionPortID, SLen,
Cardinal(SData), OL, INFINITE) do
try
try
if (OL = Pointer($FFFFFFFF)) then break;
ImpersonateLoggedOnUser(SData^.SecurityToken);
ISAPIApp.HttpExtensionProc(SData^.ECB^);
With SData^.ECB^ do
ServerSupportFunction(ConnID, HSE_REQ_DONE_WITH_SESSION,
nil , nil, nil);
finally
CloseHandle(SData^.SecurityToken);
(* BUGFIX (1-dec-2000) only Dispose of SData if OL
$ffffffff *)
if SData Nil then Dispose(SData);
end;
except // Unhandled Exception, do not exit the worker thread.
(*
SHOULD NEVER HAPPEN.
ISAPIApp.HttpExtensionProc already catches all exceptions
AND
the Win32 functions used here return error codes i.e.
they
do not raise exceptions.
*)
end;
Result:= 0;
end;
function TThreadPool.newInstanceData(ECB: PEXTENSION_CONTROL_BLOCK;
SecToken: THandle): PThreadWorkData;
begin
New(Result);
Result^.ECB:= ECB;
Result^.SecurityToken:= SecToken;
end;
function TThreadPool.postWorkItem(ECB: PEXTENSION_CONTROL_BLOCK):
LongBool;
var SecToken: THandle;
begin
// Open Security token of calling thread this function
OpenThreadToken(GetCurrentThread(), TOKEN_ALL_ACCESS, False,
SecToken);
Result:= PostQueuedCompletionStatus(FCompletionPortID, 0,
Cardinal(newInstanceData(ECB,
SecToken)),
Nil);
end;
constructor TThreadPool.Create(InitThreads: Integer);
var i: integer; FThreadID: LongWord;
begin
FCompletionPortID:=
CreateIoCompletionPort(INVALID_HANDLE_VALUE, 0, 0, 0);
SetLength(FPool, InitThreads);
for i:= 0 to InitThreads - 1 do
FPool[i]:= BeginThread(nil, 0, @WorkerFunc, Self, 0,
FThreadID);
end;
destructor TThreadPool.Destroy;
var i: integer;
begin
for i:= 0 to Length(FPool)-1 do
PostQueuedCompletionStatus(FCompletionPortID, 0, 0,
Pointer($FFFFFFFF));
WaitForMultipleObjects(Length(FPool), @FPool, TRUE, 120000);
for i:= 0 to Length(FPool)-1 do
CloseHandle(FPool[i]);
CloseHandle(FCompletionPortID);
FPool:= Nil;
end;
function HttpExtensionProc(var ECB: TEXTENSION_CONTROL_BLOCK):
DWORD;
var IsPosted: BOOL;
begin
try
// Post Work Item to completions port and store the thread's
security context
IsPosted:= ThreadPool.postWorkItem(@ECB);
if not IsPosted then
Result:= HSE_STATUS_ERROR
else
Result:= HSE_STATUS_PENDING;
except
Result:= HSE_STATUS_ERROR;
end;
end;
function GetExtensionVersion(var Ver: THSE_VERSION_INFO): BOOL;
begin
ThreadPool:= TThreadPool.Create(25);
Result:= ISAPIApp.GetExtensionVersion(Ver);
end;
function TerminateExtension(dwFlags: DWORD): BOOL;
begin
ThreadPool.Free;
Result:= ISAPIApp.TerminateExtension(dwFlags);
end;
end.
===