Update to Sitecore Stager

May 03, 2011

If you're on a Sitecore environment pre-6.3 and you're using the Sitecore Stager you may have noticed that each publish will clear the entire HTML for a site. This may be fine for you but I like to have more granular control over the cache so I've updated the source code a bit to clear only entries related to the published items. I'm not going to knock the Sitecore Stager, it's an excellent utility. Plus this why they release source code in the first place.

This idea may appeal to you and if it does you should be aware of the pro's and con's of this approach. Let's first go over how cache entries are stored. Each entry is just a string that starts with a path to the sublayout it is storing. There's also language and cache parameter information appended to make the entry key unique. Let's say I have a sublayout using "vary by data". The cache entry would look like this: /sublayouts/PageContent.ascx_#lang:EN_#data:/sitecore/content/Home/SomeSubPage. You'll notice that the data parameter is the path to the page item. This is really the only information about the page that's stored. What you won't see is information about subitems or items in link fields. This means that for you to clear that page's cache entry you'll have to publish that page not just the items releated to it, which is probably why the Sitecore Stager defaults just clearing all cache. You may now be questioning whether or not you're ready to go this route but if you are you can help yourself with some additional tools like the cache manager, which is what I do.

So if you've got the source code and you're still ready to take the plunge let's get into the "how" of getting this working. First browse to /SitecoreStager/Trunk/slave/Code/CacheManager.cs and replace the code with this:


using System;using System.Collections;using System.Collections.Generic;using System.Linq;using System.Web;using System.Web.Hosting;using Sitecore.Caching;using Sitecore.Collections;using Sitecore.Configuration;using Sitecore.Data;using Sitecore.Data.Engines;using Sitecore.Web;using Sitecore.Data.Items;namespace Sitecore.Stager{	public static class CacheManager	{		static CacheManager() {			ConfigWatcher.ConfigChanged += ConfigWatcher_ConfigChanged;		}		private static void ConfigWatcher_ConfigChanged(object sender, ConfigChangedEventArgs e) {			Log.Info("STAGER Shutting down the application due to configuration change");			HostingEnvironment.InitiateShutdown();		}		private static readonly Dictionary<string, PivotCache> pivotCaches = new Dictionary<string, PivotCache>();		public static Cache GetSqlPrefetchCache(Database database) {			return Caching.CacheManager.FindCacheByName("SqlDataProvider - Prefetch data(" + database.Name + ")");		}		public static PivotCache GetPivotCache(Database database) {			if (!pivotCaches.ContainsKey(database.Name)) {				lock (pivotCaches) {					if (!pivotCaches.ContainsKey(database.Name)) {						pivotCaches.Add(database.Name, new PivotCache(database, Settings.PivotCacheSize));					}				}			}			return pivotCaches[database.Name];		}		public static void ClearPathCache(Database database, Set<ID> toClear) {			Log.Debug("STAGER Clearing path cache. Database: " + database.Name);			foreach (ID pubID in toClear) {				Item i = database.GetItem(pubID);				if (i != null) {					string path = i.Paths.Path;					Log.Info("STAGER Clearing " + path + " from AccessResultCache");					Caching.CacheManager.GetPathCache(database).RemoveKeysContaining(path);					Caching.CacheManager.GetPathCache(database).RemoveKeysContaining(i.ID.ToString());				}			}					}		public static void ClearAccessResultCache(Database database, Set<ID> toClear) {			Log.Debug("STAGER Clearing access result cache. Database: " + database.Name);			foreach (ID pubID in toClear) {				Item i = database.GetItem(pubID);				if (i != null) {					string path = i.Paths.Path;					Log.Info("STAGER Clearing " + path + " from AccessResultCache");					Caching.CacheManager.GetAccessResultCache().RemoveKeysContaining(path);					Caching.CacheManager.GetAccessResultCache().RemoveKeysContaining(i.ID.ToString());				}			}		}		public static void ClearHtmlCaches(Database database, Set<ID> toClear) {			Log.Debug("STAGER Clearing HTML caches");			//get the unique list of site infos so you only clear cache on those sites			foreach (ID pubID in toClear) {				//find the site info for this site				SiteInfo si = GetSiteInfoByItemID(database, pubID);				if (si != null) {					Item i = database.GetItem(pubID);					if(i != null){						string path = i.Paths.Path;						Log.Info("STAGER Clearing " + path + " from " + si.Name);						si.HtmlCache.RemoveKeysContaining(path);						si.HtmlCache.RemoveKeysContaining(pubID.ToString());					}				}			}		}		public static void ClearDataItemPrefetch(Database database, IEnumerable<ID> ids) {			Log.Debug("STAGER Clearing item, data, prefetch, std values caches. Database: " + database.Name);			PivotCache cache = GetPivotCache(database);			foreach (ID id in ids) {				cache.RemoveID(id);			}		}		public static void IssueCacheCleanup(DateTime lastPublishDate, Database database) {			Log.Info("STAGER Cache cleanup issued. Last publish date: {0}, database: {1}", lastPublishDate, database.Name);			bool clearAll;			Set<ID> ids = ProcessHistoryStorage(database, lastPublishDate, out clearAll);			if (clearAll) {				database.Engines.TemplateEngine.Reset();				Caching.CacheManager.ClearAllCaches();			} else {				if (ids.Count != 0) {					ClearHtmlCaches(database, ids);					ClearAccessResultCache(database, ids);					ClearPathCache(database, ids);					ClearDataItemPrefetch(database, ids);				}			}		}		private static SiteInfo GetSiteInfoByItemID(Database db, ID itemID) {			Item i = db.GetItem(itemID);			if (i != null) {				List<SiteInfo> si = Factory.GetSiteInfoList().Where(a => a.StartItem.Length > 0 && i.Paths.Path.Contains(a.StartItem)).ToList();				if (si.Any()) {					return si[0];				}			}			return null;		}		private static Set<ID> ProcessHistoryStorage(Database database, DateTime lastPublishDate, out bool clearAll) {			if (database.Engines.HistoryEngine.Storage == null) {				clearAll = true;				Log.Error(string.Format("STAGER History engine unavailable for '{0}' database. Full cleanup issued", database.Name));				return null;			}			HistoryEntryCollection history = database.Engines.HistoryEngine.GetHistory(lastPublishDate, DateTime.UtcNow.AddYears(1).Date);			Log.Debug("STAGER History storage entries: " + history.Count);			clearAll = false;			Set<ID> set = new Set<ID>();			foreach (HistoryEntry entry in history) {				set.Add(entry.ItemId);				if (entry.AdditionalInfo.StartsWith("#")) {					var ids = entry.AdditionalInfo.Split(new[] { '#' }, StringSplitOptions.RemoveEmptyEntries);					foreach (string id in ids) {						if (id.Equals("*")) {							clearAll = true;							break;						}						set.Add(ID.Parse(id));					}				}				if (clearAll) break;			}			if (clearAll) {				Log.Debug("STAGER History storage contains 'clear all' entries. Full cleanup issued");			} else {				Log.Debug("STAGER History storage entries grouped: " + set.Count);			}			return set;		}	}}


What you should notice is the "IssueCacheCleanup" method. This is where all the subsidiary methods are called from. What I've changed is that each method like "ClearHtmlCaches" now takes in the Set of ID's of items that were published. Then if you jump to the "ClearHtmlCaches" method you'll see a foreach loop that iterates through those ID's, retrieves the items and calls the "RemoveKeysContaining" method with the path and ID of that item.

So that's the meat of it and again I highly recommend the Sitecore Stager, but if you're looking to get in there and tweak a few things now you know where to look.