Let me just preface this post with a declaration : I'm talking about caching objects in code, not module or page level caching for outputs. Module level caching is where the contents of a module are cached so that DNN can speedily emit them back out instead of constructing the details from database records. Page level caching is pretty much synonymous with the Snapsis Pageblaster module : which takes the contents of the entire page and caches them to reduce the amount of database calls. A fine module and a staple for DNN sites everywhere : except mine, oddly, I've never taken the time to implement it.
No, what I wish to discuss here is the ability of the DotNetNuke framework to cache all sorts of objects and to handle all the messy plumbing for you.
A quick scan around documentation and let-me-google-that-for-you will show up an easy example like this :
DotNetNuke.Common.Utilities.DataCache.SetCache("myObject", myObject);
to store your object. And then, somewhere else, this code to extract it again:
object myObject = DotNetNuke.Common.Utilities.GetCache("myObject")
if (myObject != null)
return (myType)myObject;
else
myObject = new myType();
DotNetNuke.Common.Utilities.DataCache.SetCache("myObject", myObject);
return myObject;
So far so good : and this is what was being used in the Url Master module, for the purposes of storing the custom Urls and 301 redirects that the module stores.
But recently, on a couple of separate occasions, I had people report back to me that there were excessive database connections being opened, all using the 'ifty_GetTabsAndRedirects' stored procedure. This procedure returns a union of the tabs in the system, along with any custom redirects that belong to those tabs. One thing I've learnt in my all too long career of creating and solving bugs (otherwise known as programming) is that problems always have a reason, and sooner or later they'll show up again, usually at the most inconvenient time. I had another person report a slow site, so I thought I had better look deeper into the problem. In the past, I hadn't been able to reproduce the problem.
Looking through the code, it was doing everything that I expected; building up the object by calling the stored procedure, storing it in the cache, and moving on. Or was it? When I did a Sql Trace, I found to my horror this telling set of entries:
RPC:Completed 0 exec dbo.ifty_GetTabsAndRedirects @portalId=-1,@includeAdminPages=0,@includeHostPages=0
RPC:Completed 0 exec dbo.ifty_GetTabsAndRedirects @portalId=-1,@includeAdminPages=0,@includeHostPages=0
RPC:Completed 4 exec dbo.ifty_GetTabsAndRedirects @portalId=-1,@includeAdminPages=0,@includeHostPages=0
RPC:Completed 5 exec dbo.ifty_GetTabsAndRedirects @portalId=-1,@includeAdminPages=0,@includeHostPages=0
I checked around a couple of my test sites : it seemed to be DNN version independent, and the problem was the 2nd column : this numeric number is the 'duration' column in the Sql Trace. It is increasing with each call : a performance error which is log(n) - not good. The reason is presumably because each subsequent call has to wait for the previous read on the tables to finish.
Puzzled, I delved further and put a couple of trace statements into the code to follow what was happening. And I got quite a shock, because a trace statement right after the GetCache() call return a null object. Because of this, the stored procedure was called - repeatedly as each time the object was stored in the cache, and then promptly disappeared. Note that this is in the Friendly Url generation side of the module : the holdup comes when working out which urls to use for things like the menu and other page links. The bigger the number of tabs, the worse the problem.
Now this didn't look like some sort of resource problem (the webserver will flush the cache when the memory allocation reaches a pre-set point) because there are two cached objects in the Url Master module : one for the rewrite dictionary, and one for the custom urls and redirects. The other, cached object (the rewrite dictionary) was being cached just fine, despite being 4x the memory footprint.
Solving the Caching Problem
Here's where I'd love to share the brilliant debugging and problem solving skills I used to fix the problem. Only I can't, because I could never work out what was wrong. To me, it would seem that using the simple overload [setCache(key,object)] just doesn't work properly.
What I did in the end was to use the same code I used for the Url Dictionary cache. This was a different overload because it used a callback and a fixed expiration time.
Here's the code:
DateTime absoluteExpiration = DateTime.Now.Add(settings.CacheTime);
DataCache.SetCache(UrlDictKey, urlDict, null, absoluteExpiration, Cache.NoSlidingExpiration, CacheItemPriority.AboveNormal, onRemove, settings.CachePersistRestart);
This code uses a different overload, and for some reason, works properly. Testing confirmed it : previously, when hammering the test server with requests, eventually it would choke up as the database loaded up on queries to the same table over and over again. Now, there would be one call to the sp, the item would be cached, and the requests could go through without choking.
So why the blog post? Well, I'm hoping someone will be able to explain to me what I might have missed in my debugging.
The End Result
Because of all this : the Url Master version 1.13.02 will have the shortest release time of any release yet : it's now being replaced by the 1.13.03 module after only 1 day in circulation. I would recommend anyone running the Url Master module to upgrade to 1.13.03 (or better, once this post ages and new releases are made) : I don't know exactly when this problem first appeared, but I would wager that it goes back to the 1.11 versions. The 1.13.03 release contains the changes to the caching code, which in turn behaves as it should : quickly.
The new release is available here : iFinity Url Master Products Page