~mkw

Average guy, above average luck...the blog of M. Keith Warren

ASP.NET: Application level data caching with callbacks

In my current application, the vast majority of the web site is broken into content ‘parts’ that can all be edited through a built in content manager. Pages consist of one or multiple parts which are elements of HTML persisted to the SQL database. In order to improve performance I wanted to look at some techniques for caching these content elements and coupled with my desire to learn new things I decided to use the Cache class directly instead of the more common methods of Output Caching.

My plan was to load all of the content elements into cache during the application startup and then wire up a dependency to a file that I would ‘touch’ any time some content was updated. This was largely experimental at this stage so I chose to load all of the content into a string dictionary, cache the collection and wire the dependency to one file. (In the near term I plan to test this against the perf of caching each content element separately and wiring dependencies to an individual file for each element; but that is later)

Simple enough so far…I want to call my method that will fill the cache during Application_Startup event in the Global ASA

protected void Application_Start(Object sender, EventArgs e)
{
     FillContentCache();
}

private void FillContentCache()
{
    
HttpContext.Current.Cache.Insert(
            "CONTENT", 
            new Content(true).GetActiveElements(), 
            new CacheDependency(cfg.AppSettings["ContentDependencyFile"]), 
            Cache.NoAbsoluteExpiration,
            Cache.NoSlidingExpiration,
            CacheItemPriority.Default,
            new CacheItemRemovedCallback (this.CacheItemRemoved)
            );
}

In the above code you will notice that I am wiring up the callback to CacheItemRemoved which is the name of my method that I want called when the dependency file is touched or the item is removed from cache for some other reason. In my case I am making this really simple, if it gets removed then call put it back in the cache…

private void CacheItemRemoved(string key, object val, CacheItemRemovedReason reason)
{
      FillContentCache();
}

Pretty simple, only problem is that this does not work. The dependency wires up fine and the callback works when you change the file but the code in FillContentCache does not work when it is invoked from CacheItemRemoved.

OK, so I do a little searching on google and fear washes over me. There are a lot of people who have had this same problem, and no one was offering any answers. I could go into all the things that were suggested and things I tried but I will skip all that and tell you why this does not work (it made me feel stupid) and how to make it work.

This code will not work because when CacheItemRemoved is called, it does not necessarily happen inside the context of a request. This fact means that HttpContext.Current.Cache will cause a NullReferenceException to be thrown; but I failed (and I assume most others who took this road) because I did not detect this Exception right away. I was testing with a page that enumerated the cache for me and a button that would invalidate it; the cache enumerates itself before the click event of the button would fire thus visually tricking me into believing the problem was somewhere other than right in front of me…DOH!

Well, beyond my brain fart there is still the problem of the fact that when the callback occurs, we lack the Context necessary to tap the Cache object; luckily though it is a problem easily fixed.

In the Global ASA we create a static instance of the Cache class and then set it to the Cache object during Application_Startup.

private static Cache _cache = null;

protected void Application_Start(Object sender, EventArgs e)
{
      _cache = System.Web.HttpContext.Current.Cache;
      FillContentCache();
}

Since Cache is a reference type, when we assign our _cache instance to System.Web.HttpContext.Current.Cache we are effectively creating a pointer to that spot in memory. We then change our code in FillContentCache to insert into _cache which refers to the Cache while we own some context to it…

private void FillContentCache()
{
    
_cache.Insert(
            "CONTENT", 
            new Content(true).GetActiveElements(), 
            new CacheDependency(cfg.AppSettings["ContentDependencyFile"]), 
            Cache.NoAbsoluteExpiration,
            Cache.NoSlidingExpiration,
            CacheItemPriority.Default,
            new CacheItemRemovedCallback (this.CacheItemRemoved)
            );
}

This will then properly update the application cache while being fired outside the scope of a request which holds application context.

Keep in mind this was largely experimental tinkering and I have not done any serious perf testing on this to determine if it is road worthy.

 

Comments

Chad Grant said:

Dont use the HttpContext in your global.asa.cs use: this.Context.Cache
# May 20, 2004 4:15 PM

M. Keith Warren said:

Chad,

That causes the NullRef as well, the problem is there is NO context when the CacheItemRemoved event is called.
# May 20, 2004 4:17 PM

David Crowell said:

Keith,
Why are you keeping all of the content items in one cache key? What happens if a page request comes through when you are filling the cache?

Wouldn't it be better to store each content item seperately, and load on demand if not in the cache?

You can still invalidate all cached content when updating.
# May 20, 2004 5:28 PM

M. Keith Warren said:

One concern I had was that with hundreds of content elements I would burden the system with too many file watchers when the dependencies wire up. You are right that separate ones might be better but I was trying to put something quick and dirty together and have done no perf testing one way or another.

As far as requests coming in while the cache is invalid, this is handled by my display logic which first tries to fetch the content element from cache and if no luck will tap the database for it; I have played with writing it so that if the cache is dead to spawn a thread to fill it while my main request thread goes out to the db to fill the specific content request.
# May 20, 2004 5:34 PM

David Crowell said:

If you use the same file for cache invalidation, does it create a new filewatcher for each cache entry? I haven't tested.

I generally don't use a file for invalidation, and instead use a relatively short cache time.
# May 20, 2004 5:37 PM

M. Keith Warren said:

I don't really know and It is certainly something I want to find out. I might play with it this evening to try and determine how that happens under the hood.
# May 20, 2004 5:40 PM

Chad Grant said:

Why would you want to use the Cache object and then refill the data when it gets unloaded? Use a Singleton object... You're defeating the purpose of a cache, its not supposed to be in the cache if not needed, hence, the unload...

You should write a wrapper to see if the object is in the cache when you need it, if not then put it into the cache. Not refill the cache when it gets unloaded.
# May 20, 2004 6:00 PM

M. Keith Warren said:

I understand your premise and the loader for the content does check and if the cache is invalidated will force the repopulation but in my case I want to instantly rebuild the cache in the case the content is updated. Admin user updates a page and then I update a log file which in turn invalidates the cache set, firing the event which then talks to SQL to get the updated content.

Either way of doing it works, I was just experimenting with something new.
# May 20, 2004 6:04 PM

Jason G said:

One thing I use (sometimes from window forms applications) is the Cache that you can get access to from the HttpRuntime class.

I'm fairly sure that it's the same as the context cache, but shouldn't have the same problems of not always being there.

(Might want to check that it's the same cache instance, I'd be interested in knowing if it isn't.)

I'd never heard of this problem before, glad it was you who found it and not me :)
# May 20, 2004 7:14 PM

T H said:

Did you check the memory usage? Did it increase over time after several invalidations?
# July 14, 2004 8:58 AM

lynn@soundworks.com said:

HTTPRuntime is the way to go.

Make sure you callbacks are anchored to the Global.asax or an HTTP module so they are always in scope.
# July 14, 2004 6:56 PM

Carl Abramsson said:

Hmm. This is quite a "wrong" way of using the cache-features. Imagine if you have 100,000 pages. The application starts and loads all objects into the cache, but after they are loaded the cache thinks this object is to big big to keep in the cache because of server-limitaions. This will produce a not so pleasant senario, all pages will be loaded for each request.....

It's better to check if the the page-part is in cache when you wan't it. If not, load only that part from the db and insert it into the cache.
Remove it from the cache "manually" after a new version is saved to db.

If you have to use dependency you can write your own dependency-object.
# July 15, 2004 12:24 PM

M. Keith Warren said:

Carl,

You are talking about lazy loading of the content and storing it in the cache. Been there, done that - wanted to try something new. This is a custom solution and I know ahead of time it will never see more than a few thousand content parts which will never total more than 100 megs of memory; I appreciate your comment.
# July 15, 2004 12:38 PM

Mohamed Raafat said:

I have a problem which I think is related:

I retrieve data from a database table using a query, eventhough the resulting data file is only 10 kb when I use dataset.writexml
the recieved bytes on my internet connection is 865kb, can anyone shed some light on this problem.

I would appreciate any help on this

from
Mohamed Raafat
# July 18, 2004 10:28 AM

Cedric B said:

Mohamed, the extra size is being allocated because dataset.writexml wraps all of the data into XML nodes and elements, so therefore you just increased the amount of bytes required to store the data, by choosing XML format.

# July 19, 2004 1:08 PM

bill xie said:

For web applications, because of its disconnected characteristics, generally your method will work. However, note that web application is mutithreaded. So race might cause page error.
# July 19, 2004 2:25 PM

Muzzammil said:

Hi,

I have a relatively unrelated question, In my application Design, we have some referrence tables, for instance tax percentage.

and it happens many times that we have to access one tax percentage value from table.

I used cache to store the whole table ( 10-15 rows) , in the object in my business logic layer. When the function is called , the cache is checked, if its loaded, the value is returned from cache, otherwise cache is populated from DB, and then value is returned.

but the situation is the Cache context is only available in PAGE, and not available in my Business logic layer, and for that purpose I am passing the reference of page in the parameter of function.


i want to know, wheatehr its a good aproach, and if there are other alternate ways of acheiving this goal ?

Thanks
# July 20, 2004 11:38 AM

Senthil said:

Muzzammil - I would say that you then you are creating a dependency on with your presentation layer.

Better design approach would be to have a seperate cache for middle-tier as tomorrow if you decide to move the middle-tier to another box or another service then you will end up in trouble.

You need not spend too much time in writing your own cache framework. You could use the Microsoft application block for Caching which provides almost the same functionalities as ASP.NET cache
# July 27, 2004 5:06 PM

Niyazi Gül said:

You can use HttpRuntime.Cache instead

# August 8, 2008 4:32 PM