Building a Simple Page Caching System
Building a Simple Page Caching System
by John Peterson
Introduction
Not too long ago I wrote an article about the different types of
server-side caching you could use to increase performance of
selected asp pages. (If you haven't read it yet, you might want
to read it now since this
article builds upon it.) At the request of our users, I'm now
going to cover building a simple system to implement the page
caching technique I discussed in that article.
The Concept
The basic premise is to build an include file that you can
insert at the top of any asp page and have it cache a static copy
of the page that automatically gets refreshed after a given period
of time.
In order to accomplish this, we need a few things. The first is a
way to retreive a copy of the page to cache. The next is a place
to store that page. Then we need a mechinism to determine when to
refresh the cache. Finally we need a system to alternate between
showing the cached and non-cached versions.
Note: This system would be quite a bit less
complex if the cache controlling page and the page to be cached
were seperated, but in order to make this operate within the
confines of an easy to use include file, this isn't really
feasible... so bear with me as the code can get a little
confusing.
Before we get to any code... let me walk you through the basic
process. A request for the asp page comes in. The first decision
is whether or not we can use a previously cached version. If we
can then we use ASP 3.0's Server.Transfer command to point the user
to it. If we can't then we use the dynamic version. When we use
the dynamic version, it means the cache has expired so it only
makes sense to update the cache. To do this we need to get a
copy of the page. We do this by requesting the page via
an http component with a special
QueryString that tells the page to give us the dynamic version,
but also tells it not to initiate another update. If we didn't
we'd end up in an infinite loop of updating the cached version
and instead of reducing the load and increasing performance, we'd
increase the load and probably bring the server to it's knees so
I'm warning you now... be very careful when tinkering with the script!
The Code
Okay, so now that I've given you the quick overview, here's the
code. Instead of giving you it piece by piece with a paragraph
thrown in here and there, I'm just posting the one big block of
code and commenting heavily to try and make things easier to
follow and understand.
caching.asp
<%
' Declare our variables
' To try and cut down on possible name collisions
' with existing scripts, I've prefixed them w/ caching
Dim cachingDynamicPageURL
Dim cachingStaticPageURL
Dim cachingForceRefresh
' Get the URLs of the pages.
' I use .htm for the cached version of our .asp files, but
' you could replace it with any extension if you have htm
' files on your server that you're afraid of overwriting.
cachingDynamicPageURL = Request.ServerVariables("URL")
cachingStaticPageURL = Replace(cachingDynamicPageURL, ".asp", ".htm")
' Here's the basic logic... the implementation is
' mainly contained in the functions which follow:
' This first conditional decides whether or not the
' cached version is recent enough to show. This is
' where the timeout (in minutes) should be changed
' if needed.
If PageIsFresh(1) Then
' Simply a wrapper for our Server.Transfer command.
' Transfers control to the cached page and stops
' execution of this one.
ShowCachedPage()
Else
' If we need to do an update then the request to
' update can't also initiate an update... so this
' checks to see if one is already in progress.
If PageIsBeingRefreshed() Then
' Does the actual update to the cache file.
UpdateCachedPage()
End If
' The rest of the remaining script continues to process.
End If
' *** Begin Functions *************************************
Function PageIsFresh(iTimeInMinutes) ' As Boolean
Dim dLastUpdated
Dim bPageIsFresh
' Check and see when we last updated... I use a
' variable based on the page name so we can use
' this for lots of files on the same server.
dLastUpdated = Application(GetAppVarName())
' If the last update variable is blank... set a
' default value of Jan 1, 2000
If dLastUpdated = "" Then
dLastUpdated = CDate("January, 1, 2000")
End If
' Compare Now to the last updated date and set
' the flag indicating if the page has been
' updated within the given timeframe.
If DateDiff("n", dLastUpdated, Now()) < iTimeInMinutes Then
bPageIsFresh = True
Else
bPageIsFresh = False
End If
' Debugging Line
'Response.Write bPageIsFresh & "<br />"
' Override the value we just determined based on
' the Application var if we find the appropriate QS...
' which is: ?nocache=true
If bPageIsFresh = True Then
' A little difficult to read and understand:
'bPageIsFresh = _
' (Not(LCase(Request.QueryString("nocache")) = "true"))
' Longer simpler version.
If LCase(Request.QueryString("nocache")) = "true" Then
bPageIsFresh = False
Else
' Somewhat pointless since we know it's already
' True, but what the hell.
bPageIsFresh = True
End If
End If
' Set the global flag telling us if we need to update
' based on the current "freshness" of the page.
If bPageIsFresh Then
cachingForceRefresh = False
Else
cachingForceRefresh = True
End If
' Ok... the horrible term of "fresh" is done
' being used now.
PageIsFresh = bPageIsFresh
End Function
Function ShowCachedPage() ' None
Server.Transfer(cachingStaticPageURL)
End Function
Function PageIsBeingRefreshed() ' As Boolean
Dim bRefresh
' Default to the value set when we checked if
' the page was "fresh" enough
bRefresh = cachingForceRefresh
' If it's true then we need to check and make
' sure we're not overriding it to false via QS.
' QS command is: refresh="false"
If bRefresh Then
' Again a little difficult to read and understand:
'bRefresh = _
' LCase(Request.QueryString("refresh")) = "true"
' Again a simpler version
If LCase(Request.QueryString("refresh")) = "false" Then
bRefresh = False
Else
' Again somewhat pointless since we know it's
' already True, but again... what the hell.
' Haven't we done this once already? ;)
bRefresh = True
End If
End If
' Set the return value
PageIsBeingRefreshed = bRefresh
End Function
' Finally a simple, straight-forward function... sort of!
Function UpdateCachedPage() ' None
Dim strFullURL, strFullPath
Dim objXmlHttp, strHTML
Dim objFSO, objFile
' Build the URL of our cache refreshing data request.
' Be SURE to include the nocache and refresh line.
strFullURL = "http://" & Request.ServerVariables("SERVER_NAME")
strFullURL = strFullURL & cachingDynamicPageURL
strFullURL = strFullURL & "?nocache=true&refresh=false"
' Get out full file system path.
strFullPath = Server.MapPath(cachingStaticPageURL)
' Send the request and get the result as text.
' I assume it's good... you might want to check for
' something it's supposed to contain and abort
' if it's not there.
Set objXmlHttp = Server.CreateObject("Msxml2.ServerXMLHTTP")
objXmlHttp.open "GET", strFullURL, False
objXmlHttp.send
strHTML = objXmlHttp.responseText
Set objXmlHttp = Nothing
' Debugging line.
'Response.Write strHTML
' Take our recently acquired text and write it to a file.
Set objFSO = Server.CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile(strFullPath, 2, True)
' I do a replace for illustration in the sample code:
objFile.WriteLine Replace(strHTML, "Dynamic", "Static")
' Here's the line without it.
'objFile.WriteLine strHTML
objFile.Close
Set objFile = Nothing
Set objFSO = Nothing
' Update our last updated date!
' Wheh... try saying that 3 times quickly. ;)
Application.Lock
Application(GetAppVarName()) = Now()
Application.Unlock
End Function
' Wrapper for the way we determine our application
' variable name since this might want to be changed.
Function GetAppVarName() ' As String
Dim strTemp
' Read in from global var
strTemp = cachingDynamicPageURL
GetAppVarName = ConvertPageNameToVarName(strTemp)
End Function
' Pages can contain characters that variable names can't so I
' built this little function to weed them out. I can't think
' of any other common file characters that don't work, but if
' you run across any simply update this function.
Function ConvertPageNameToVarName(strPageName) ' As String
Dim strTemp
strTemp = strPageName
strTemp = Replace(strTemp, "/", "~s~")
strTemp = Replace(strTemp, ".", "~p~")
ConvertPageNameToVarName = strTemp
End Function
' This can't be expected to always reverse the above, but for
' simple filenames it should work most of the time. Not that
' I can think of any reason you'd need to do it anyway, but
' just in case it ever comes up... here it is.
Function ConvertVarNameToPageName(strPageName) ' As String
Dim strTemp
strTemp = strPageName
strTemp = Replace(strTemp, "~s~", "/")
strTemp = Replace(strTemp, "~p~", ".")
ConvertVarNameToPageName = strTemp
End Function
' *** End Functions ***************************************
%>
|
Implementation
The code was written to be pretty easy to implement. Simply upload
the include file, change the amount of time the caching lasts to
an appropriate value for your content, and add a line for the include
file to the top of the asp page you want to cache.
Here's a basic sample script that I've included in the zip file.
sample.asp
<%@ Language=VBScript %>
<%
Option Explicit
Response.Buffer = False
%>
<!-- #include file="caching.asp" -->
<html>
<head>
<title>Dynamic Page</title>
</head>
<body>
<h2>Dynamic Page</h2>
<p>Page Rendered: <%= Now() %></p>
</body>
</html>
|
There are a couple potential problems that might arise during
the installation of this code. The first has to do with the NTFS
security settings on the caching file. The anonymous internet user
will need change (RWD) access to it in order to refresh the cache.
The other issue has to do with multiple users refreshing the cache
simultaneously. While this shouldn't be too big a deal, it could
cause some problems on extremely busy sites so if you're expecting
extreme traffic, you'll probably be better off implementing this
in seperate pages and devising a little more sophisticated locking
mechinism.
Possible Problems
The code uses direct access to global varibales from within
functions. I normally frown upon this type of thing, but it was
quick and easy and well... I got lazy. So sue me... it's not as
pretty as usual, but it works. ;)
Another issue is that I'm still having the
same trouble with the ServerXMLHTTP object that I mention in the code
in this sample, so until someone gets me a fix, you might have to
use XMLHTTP, but be forewarned it really isn't server safe so you'd
probably be better off using a 3rd party component or the Internet
Transfer Control for the http work... at least for now.
Conclusion
It may not be the fastest or best caching solution around, but like
I said in the title, it is a simple asp-only solution that enables
you to do page level caching with a minimum of fuss and almost no
setup.
That's all I've got for now folks. Here's the code in a
zip file
so you can download an play with it. Once we solve this
ServerXMLHTTP issue, I'll put up a sample so you can play with it,
but for now that's all she wrote.
Additional Information:
Server Side Caching Options - The article that led to this one
Related Products:
Post Point Software - Maker of XBuilder and XCache commercial caching software
WebGecko - Maker of Active Page Generator (APGen) commercial caching software