Refresh cache in CacheItemRemovedCallback can cause StackOverFlow in ASP.NET 2.0

If you have code that refresh ASP.NET Cache using Cache.Insert in CacheItemRemovedCallback, it works under ASP.NET 1.1 but it may cause infinite loop under ASP.NET 2.0.

Create a ASP.NET 2.0 web form project using this code.

using

System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Web;
using System.Web.SessionState;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
using System.Threading;
using System.Web.Caching;
namespace AspCacheTest
{

public partial class WebForm1 : System.Web.UI.Page
{
    // we use this isToSleep to create the race condition for first call ItemRemovedCallback
// if recursive calls happens, all the subsequent calls will not be delayed the the sleep call.
    private static bool isToSleep = true;
    protected void Page_Load(object sender, EventArgs e)
{
    // This is request thread adding the item to the cache
    if (Cache.Get("Item01") == null)
AddToCache("Item01", "Item01 Value");
}

private void AddToCache(string key, object value)
{
WriteLog("a.PageLoad find : "+key+" is not in cache. Adding Item to Cache: " + key);
    DateTime exp = DateTime.Now.AddSeconds(10);
    // Using insert or add here shouldn't matter.
    Cache.Add(key, value, null, exp, Cache.NoSlidingExpiration, CacheItemPriority.Default,
    new CacheItemRemovedCallback(ItemRemovedCallback));
}

#region Web Form Designer generated code
override protected void OnInit(EventArgs e)
{
    //
// CODEGEN: This call is required by the ASP.NET Web Form Designer.
//
    InitializeComponent();
    base.OnInit(e);
}

/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
}
#endregion

private void ItemRemovedCallback(string key, Object value, CacheItemRemovedReason reason)
{
    //
// Here we are trying to simulate the race condition. The callback thread is put to sleep to give us
// time to refresh the page and thus, add the same item from request thread. The static isToSleep is used
// to turn off the Thread.Sleep() for subsequent callback executions in order to create the stack overflow quickly.
//
    WriteLog("b.ItemRemovedCallback is called because Item: "+key+" " + reason.ToString());
    if (isToSleep)
{
isToSleep = false;
WriteLog("c.ItemRemovedCallback is putting callback to sleep...");
System.Threading.Thread.Sleep(10 * 1000);
}
    // This is callback thread "refreshing" the item in Cache.
// Bug Repro:
    WriteLog("d.ItemRemovedCallback is inserting item : " + key);
    // Bug Fix: WriteLog("d.ItemRemovedCallback is adding item : " + key);
    DateTime exp = DateTime.Now.AddSeconds(10);
    // Bug Repro:
    Cache.Insert(key, value, null, exp, Cache.NoSlidingExpiration, CacheItemPriority.Default,
    new CacheItemRemovedCallback(ItemRemovedCallback));
    // Bug Fix: Use Cache.Add instead of Cache.Insert
//Cache.Add(key, value, null, exp, Cache.NoSlidingExpiration, CacheItemPriority.Default,
// new CacheItemRemovedCallback(ItemRemovedCallback));
}
private void WriteLog(string message)
{
System.Diagnostics.Debug.WriteLine(message);
System.Diagnostics.EventLog log = new System.Diagnostics.EventLog("Application");
log.Source = "Application";
log.WriteEntry(message, System.Diagnostics.EventLogEntryType.Warning, 1);
}
}
}
Steps to repro the loop:
1. Run the code in Visual Studio 2005 debugger.
2. Bring up the "Output Windows" to monitor the debugging info.
3. The Output Windows will output: a.PageLoad find : Item01 is not in cache. Adding Item to Cache: Item01 When the page is load.
4. Wait for about 30 seconds when next lines appear in the output window, click the refresh button of the page immediately.
b.ItemRemovedCallback is called because Item: Item01 Expired
c.ItemRemovedCallback is putting callback to sleep...
5. Notice the Output Windows will dump the debug info caused by the loop conditions.
a.PageLoad find : Item01 is not in cache. Adding Item to Cache: Item01
d.ItemRemovedCallback is inserting item : Item01
b.ItemRemovedCallback is called because Item: Item01 Removed
d.ItemRemovedCallback is inserting item : Item01
b.ItemRemovedCallback is called because Item: Item01 Removed
d.ItemRemovedCallback is inserting item : Item01
b.ItemRemovedCallback is called because Item: Item01 Removed
d.ItemRemovedCallback is inserting item : Item01
b.ItemRemovedCallback is called because Item: Item01 Removed
d.ItemRemovedCallback is inserting item : Item01
b.ItemRemovedCallback is called because Item: Item01 Removed
d.ItemRemovedCallback is inserting item : Item01
b.ItemRemovedCallback is called because Item: Item01 Removed
d.ItemRemovedCallback is inserting item : Item01
b.ItemRemovedCallback is called because Item: Item01 Removed
d.ItemRemovedCallback is inserting item : Item01
b.ItemRemovedCallback is called because Item: Item01 Removed
d.ItemRemovedCallback is inserting item : Item01 .................................................

Analysis:
1. Cached Item is added to the cache when the page is loaded.
2. Cache Item is expired after 10 seconds.
3. Item is removed from cache and CacheItemRemoved event is fired.
4. CacheItemRemovedCallBack is called.
5. CacheItemRemovedCallBack is put to sleep for 10 seconds to simulate the race condition.
6. Page is refreshed and Item is added back to cache at this time. (This is a simulation of race condition under multiple users environment).
7. Cache.Insert is called to add the item back to cache. What happens next is different under ASP.NET 1.1 and ASP.NET 2.0

Under 1.1:
8. Cache.Insert finds the item is already in the cache. It will remove the item, fire the CacheItemRemoved event and then insert the item.
 

Under 2.0:
8. Cache.Insert finds the item is already in the cache. It will fire the CacheItemRemoved event and CacheItemRemovedCallBack is called before the item is removed.
9. CacheItemRemovedCallBack will invoke the Cache.Insert again and Insert sees the item is still in the cache and jump back to step 8.
 

Resolution and Conclusion:
1. Use Cache.Add instead of Cache.Insert in the call back method will fix the problem.
2. Refresh cache in the CacheItemRemovedCallBack is not a good practice. Try to refresh the cache when the page is load.