LibrarySites.Banner

Limit Output Cache Clearing Frequency with the Sitecore ASP.NET CMS

This blog post provides a prototype solution that you can use to prevent excessively frequent clearing of the output cache due to publication in the Sitecore ASP.NET CMS. For more information about this solution, see the blog posts linked at the end of this page.

You can implement a solution based on this untested prototype to prevent the event handler for the publish:end and publish:end:remote from clearing the output caches for each of the managed web site in excess of some period that you define, even if Sitecore publishes during that interval.

To specify a minimum interval for all managed sites, you can use the configuration factory to set the MinimumInterval property of the event handler for the publish:end and publish:end:remote events. You can specify the outputCacheMinimimInterval attribute to override this value for each managed site. The following Web.config include file (/App_Config/Sitecore.Sharedsource.HtmlCacheClearer.config in my case) shows how you can register the event handlers and set MinimumInterval and outputCacheMinimimInterval.

<configuration xmlns:patch="https://www.sitecore.com/xmlconfig/">
  <sitecore>
    <sites>
      <site name="website">
        <patch:attribute name="outputCacheMinimimInterval">00:02:00</patch:attribute>
      </site>
    </sites>
    <events>
      <event name="publish:end">
        <handler type="Sitecore.Sharedsource.Publishing.HtmlCacheClearer, Sitecore.Sharedsource"
                 patch:instead="handler[@type='Sitecore.Publishing.HtmlCacheClearer, Sitecore.Kernel']">
          <patch:attribute name="method">ClearCaches</patch:attribute>
          <sites>
            <patch:delete />
          </sites>
          <minimumInterval>00:05:00</minimumInterval>
        </handler>
      </event>
      <event name="publish:end:remote">
        <handler type="Sitecore.Sharedsource.Publishing.HtmlCacheClearer, Sitecore.Sharedsource"
                 patch:instead="handler[@type='Sitecore.Publishing.HtmlCacheClearer, Sitecore.Kernel']">
          <patch:attribute name="method">ClearCaches</patch:attribute>
          <sites>
            <patch:delete />
          </sites>
          <minimumInterval>00:05:00</minimumInterval>
        </handler>
      </event>
    </events>
  </sitecore>
</configuration>

This solution includes an extension method for the Sitecore.Sites.SiteContext class to expose any value in the outputCacheMinimimInterval attribute for the managed site.

namespace Sitecore.Sharedsource.Sites
{
  using System;
  
  using SC = Sitecore;
  
  public static class SiteContext
  {
    public static TimeSpan GetMinimumOutputCacheClearingFrequency(
      this SC.Sites.SiteContext siteContext)
    {
      SC.Diagnostics.Assert.ArgumentNotNull(siteContext, "siteContext");
      string toParse = siteContext.Properties["outputCacheMinimimInterval"];
  
      if (string.IsNullOrEmpty(toParse))
      {
        return TimeSpan.Zero;
      }
  
      return TimeSpan.Parse(toParse);
    }
  }
}

The main update in this version is that the event handler adds an entry to the cache after clearing it to indicate the date and time of that event. For my convenience, I removed the logic that prevented clearing output caches that contain no entries.

The custom event handler provides the following properties:

  • LastClearedKey: String key into output cache used to store date of last cache clearing
  • MinimumInterval: TimeSpan indicating minimum interval between cache clearing events
  • Sites: The list of sites to process (empty to process all sites)

This custom event handler contains the following public methods:

  • ClearCache(): Default Sitecore functionality for publish:end and publish:end:remote events (requires the Sites property)
  • ClearCaches(): Custom event handler iterates relevant sites and clears output caches, respecting MinimumInterval if defined

This custom event handler contains the following private methods:

  • GetSites(): Determines the managed sites to iterate
  • HandleSite(): Handles output caching for a single managed site
  • GetTargetDatabase(): Determine the target database associated with the publishing operation (possibly null)
  • TargetDatabaseNotRelevant(): Returns true if the target database is irrelevant to the site
  • MinimumIntervalNotElapsed(): Returns false if the minimum interval is defined and has elapsed
  • OutputCachingDisabled(): Returns true if output caching is disabled for the site
namespace Sitecore.Sharedsource.Publishing
{
  using System;
  using System.Collections.Generic;
  
  using SC = Sitecore;
  
  public class HtmlCacheClearer : SC.Publishing.HtmlCacheClearer
  {
    public string LastClearedKey
    {
      get
      {
        return SC.Configuration.Settings.InstanceName 
          + "." 
          + this + ".LastPublished";
      }
    }
  
    public TimeSpan MinimumInterval { get; set; }
  
    public void ClearCaches(object sender, EventArgs args)
    {
      SC.Diagnostics.Assert.ArgumentNotNull(sender, "sender");
      SC.Diagnostics.Assert.ArgumentNotNull(args, "args");
      string targetDb = this.GetTargetDatabase(args);
      string dbString = targetDb == null 
        ? string.Empty 
        : " for sites associated with " + targetDb;
      SC.Diagnostics.Log.Info(
          this + " : clearing HTML caches" + dbString,
          this);
  
      foreach (SC.Sites.SiteContext site in this.GetSites())
      {
        this.HandleSite(site, targetDb);
      }
  
      SC.Diagnostics.Log.Info(this + " done.", this);
    }
  
    private bool MinimumIntervalNotElapsed(
      SC.Caching.HtmlCache htmlCache,
      SC.Sites.SiteContext site, 
      TimeSpan interval)
    {
      SC.Diagnostics.Assert.ArgumentNotNull(htmlCache, "htmlCache");
  
      if (interval == TimeSpan.Zero)
      {
        return false;
      }
  
      string lastPublished = htmlCache.GetHtml(this.LastClearedKey);
  
      if (string.IsNullOrEmpty(lastPublished))
      {
        return false;
      }
  
      DateTime when = SC.DateUtil.IsoDateToDateTime(lastPublished);
  
      if (when.Add(interval).CompareTo(DateTime.Now) > 0)
      {
        SC.Diagnostics.Log.Info(
          this + " : " + site.Name + " last cleared at " + when, 
          this);
        return true;
      }
  
      return false;
    }
  
    private bool TargetDatabaseNotRelevant(
      string targetDb,
      SC.Sites.SiteContext site)
    {
      SC.Diagnostics.Assert.ArgumentNotNull(site, "site");
  
      if (targetDb != null
          && site.Database != null
          && targetDb != site.Database.Name)
      {
        SC.Diagnostics.Log.Info(
          this + " : " + targetDb + " not relevenat to " + site.Name,
          this);
        return true;
      }
  
      return false;
    }
  
    private IEnumerable<SC.Sites.SiteContext> GetSites()
    {
      if (this.Sites != null && this.Sites.Count > 0)
      {
        foreach (string siteName in this.Sites)
        {
          yield return SC.Configuration.Factory.GetSite(siteName);
        }
      }
      else
      {
        foreach (string siteName in SC.Configuration.Factory.GetSiteNames())
        {
          yield return SC.Configuration.Factory.GetSite(siteName);
        }
      }
    }
  
    private bool OutputCachingDisabled(SC.Sites.SiteContext site)
    {
      SC.Diagnostics.Assert.ArgumentNotNull(site, "site");
  
      if (!site.CacheHtml)
      {
        SC.Diagnostics.Log.Info(
          this + " : output caching disabled for " + site.Name, 
          this);
        return true;
      }
  
      return false;
    }
  
    private void HandleSite(
      SC.Sites.SiteContext site, 
      string targetDb)
    {
      if (this.OutputCachingDisabled(site)
        || this.TargetDatabaseNotRelevant(targetDb, site))
      {
        return;
      }
  
      SC.Caching.HtmlCache htmlCache = SC.Caching.CacheManager.GetHtmlCache(
        site);
      SC.Diagnostics.Assert.IsNotNull(
        htmlCache,
        "htmlCache for " + site.Name);
      TimeSpan interval = 
        SC.Sharedsource.Sites.SiteContext.GetMinimumOutputCacheClearingFrequency(site);
  
      if (interval == TimeSpan.Zero)
      {
        interval = this.MinimumInterval;
      }
  
      if (this.MinimumIntervalNotElapsed(htmlCache, site, interval))
      {
        return;
      }
  
      SC.Diagnostics.Log.Info(
        this + " clearing output cache for " + site.Name, 
        this);
      htmlCache.Clear();
  
      if (interval > TimeSpan.Zero)
      {
        htmlCache.SetHtml(this.LastClearedKey, SC.DateUtil.IsoNow);
      }
    }
  
    private string GetTargetDatabase(EventArgs args)
    {
      SC.Diagnostics.Assert.IsNotNull(args, "args");
      SC.Events.SitecoreEventArgs scArgs =
        args as SC.Events.SitecoreEventArgs;
  
      if (scArgs != null)
      {
        SC.Publishing.Publisher publisher = 
          scArgs.Parameters[0] as SC.Publishing.Publisher;
  
        if (publisher != null
          && publisher.Options != null
          && publisher.Options.TargetDatabase != null
          && !string.IsNullOrEmpty(publisher.Options.TargetDatabase.Name))
        {
          return publisher.Options.TargetDatabase.Name;
        }
      }
      else
      {
        SC.Data.Events.PublishEndRemoteEventArgs pubArgs =
          args as SC.Data.Events.PublishEndRemoteEventArgs;
  
        if (pubArgs != null
          && !string.IsNullOrEmpty(pubArgs.TargetDatabaseName))
        {
          return pubArgs.TargetDatabaseName;
        }
      }
  
      return null;
    }
  }
}

Resources