Arrgh… After talking with some of the IIS team folks yesterday here at PDC about Kernel Caching in IIS, I thought it’d be nice to add this functionality to the Web Connection ISAPI extension. Kernel Caching uses a special ISAPI ‘function’ description HSE_REQ_VECTOR_SEND, which supports setting a few flags that are supposed to have IIS kick in cache the URL internally. What this does is basically cache a dynamic request similar to the way that a static file is cached in IIS.
This is server caching, not to be confused with browser or proxy caching. This means IIS is caching the file internally and when another hit to the same URL occurs the URL is served from the cache and not refreshed so the ISAPI extension is not called again until the Cache entry times out. You can set the duration of the cache via an Expires header that can be passed in optionally.
The benefit of this of course is that it takes the caching load completely out of the hands of the ISAPI extension, which in the case of Web Connection which is a fairly heavy weight COM server. It provides a performance improvement of an order of magnitude for pages that can truly be cached in this way. On my site this immediately brings a few things to mind, especially the various RSS feeds which are now cached application side.
Now, implementing this function is something I looked into a few months ago and gave up because of the horrible, horrible documentation on the Microsoft site. Yes, in hindsight I suppose it makes some sense, but even given that, without the help of a Microsoft dev today at PDC I probably never would have figured this out completely.
First things first. Kernel caching is cool but there are number of rules that need to be applied.
- Requests must be under 255k
Actually the value for this is configurable, but there’s a stock value for the result message size. Kernel caching doesn’t kick in for larger files. The value is configurable in the registry (see KB article reference later).
- Works only with IIS 6
This feature only works with IIS 6.0 (and possibly IIS 5.1 on XP SP2 – not sure though). Not a big deal except it’s hard to test exact behavior in a non-IIS 6 environment like developing in XP.
- Requests require Last-modified header
In order for Kernel Caching to kick in a Last-modified and optionally an Expires header must be provided to IIS. These are used to manage the cache duration.
- ( Requests cannot have a query string )
This is a pretty big limitation for dynamic applications, but any page that contains query strings cannot be cached. This means Kernel caching requires either that you create a vanity URL, or simply don’t require a query string for the page. If a querystring is used there will still be browser caching (same user, same request might not hit the server) but not server side caching.
(the above is according to a KB article that describes limitations. In my own testing it looks like IIS 6 is caching query string requests just fine)
There are a boatload of more rules that a bit more obscure and not as likely to affect you, but you can find them in this KB Article here:
http://support.microsoft.com/?id=817445
To use Kernel Caching you use the ISAPI ServerSupportFunction() callback method with the HSE_REQ_VECTOR_SEND option. This option requires that you set up a request structure that breaks out the header and the body of the request and sends it as a single string to the ServerSupport function.
The following is a C++ function that takes an full HTTP response including HTTP headers as input splits them up, sets up the various structures required and then calls ServerSupportFunction for caching:
/*
CacheOutput
Caches output for a full request string in the IIS Kernel Cache.
Assumes you have already made sure that:
Header is included in the request
includes Last-Modified and Expires headers
*/
BOOL CacheOutput(EXTENSION_CONTROL_BLOCK *pEcb,char *lpOutput,DWORD cbSize)
{
HSE_RESPONSE_VECTOR vec;
HSE_VECTOR_ELEMENT element;
DWORD HeaderSize = 0;
char *strHeader;
// *** find end of header
char *Content = strstr((const char *) lpOutput,"\r\n\r\n");
*Content=0; // null terminate
//// *** if we have no header end just send it as part of request
//// *** Otherwise split it out with
strHeader = new char[ strlen(lpOutput) + 1];
strHeader[0] = 0;
strcpy(strHeader,lpOutput);
strcat(strHeader,"\r\n\r\n");
Content = Content + 4; // Skip over header breaks
HeaderSize = strlen( strHeader ) ;
element.ElementType = HSE_VECTOR_ELEMENT_TYPE_MEMORY_BUFFER;
element.cbOffset = 0;
element.cbSize = cbSize-HeaderSize;
element.pvContext = Content;
vec.pszHeaders = strHeader;
vec.lpElementArray = &element;
vec.nElementCount = 1;
vec.pszStatus = "200 OK";
vec.dwFlags = HSE_IO_FINAL_SEND | HSE_IO_CACHE_RESPONSE | HSE_IO_SEND_HEADERS;
BOOL Result = pEcb->ServerSupportFunction(pEcb,HSE_REQ_VECTOR_SEND,&vec,NULL,NULL);
delete(strHeader);
return TRUE;
}
This function takes a full response string as input and it assumes the content is prefiltered and contains an embedded http header. Typical input looks like this:
HTTP/1.1 200 OK
Content-type: text/html
P3P: CP="ALL CRA TAA OWW IND PHY ONL"
Last-Modified: Fri, 16 Sep 2005 04:13:52 GMT
Expires: Fri, 16 Sep 2005 04:23:52 GMT
… more HTML
which is a typical response that Web Connection might generate to force caching. In this case the cache would be 10 minutes. Notice the Last-Modifed and Expires header, which the Web Connection ISAPI DLL uses as a trigger (in addition to an IIS 6 check) to fire the above method.
Note in the code above that you have to make sure to split the HTTP headers off the main content. Make sure that the Headers contain the two trailing CRLFs or else IIS will not interpret the headers and simply write them out as is. That threw me for a loop as I assumed IIS would accept the headers without the trailing CRLFs. Once I added them in the code started caching properly.
My mainline ISAPI code then checks for a set of conditions (IIS version, Content Length and headers), which if verified cause the code to branch and call the CacheOutput method instead. This is some internal stuff here, but it should give you an idea of what to check for:
// *** If we use Caching - use Vector Send to send the whole file
// Works only IIS 6.0
if( gnServerMode > 11 && ContentLength < 255000 &&
stristr(lpHeader,"last-modified:") &&
stristr(lpHeader,"expires:") )
{
DWORD Result = 0;
try
{
char *OutputBinary = (char *) new char[ ContentLength + 4 ];
ReadFromFile(pParam->szOutputFileName,OutputBinary,&ContentLength);
OutputBinary[ContentLength] = 0; // null terminate
Result = CacheOutput(ecb,OutputBinary,ContentLength);
delete(OutputBinary);
}
catch(...)
{
StandardPage("An error occurred","The application failed to write Cached Output to the Cache Store.",
ecb,"CacheOutputFailure");
Result = FALSE;
}
return Result;
}
This is very cool and I have a number of places where this functionality can be applied right away, so I’m stoked.
Hopefully this will help make life a little easier for anybody else who needs to use HSE_REQ_VECTOR_SEND and Kernel Caching…
Note that for the moment I didn't mess around with Cache invalidation API which is also available.
Other Posts you might also like