Skip to content

Instantly share code, notes, and snippets.

@woloski
Created June 27, 2010 00:16
Show Gist options
  • Save woloski/454485 to your computer and use it in GitHub Desktop.
Save woloski/454485 to your computer and use it in GitHub Desktop.
Invalidate ASP.NET Cache using a Windows Azure queue
namespace Southworks.WindowsAzure
{
using System;
using System.Web.Caching;
/// <summary>
/// Represents a cache dependency that uses Windows Azure queues to detect if an item in cache has changed
/// </summary>
/// <remarks>Sending a message a certain queue (specified using the <see cref="CloudQueueCacheDependencyMonitor" />) will invalidate the cache item </remarks>
/// <example>
/// The following code adds a dependency on a cache item in ASP.NET
/// <code>
/// Cache.Insert("test", DateTime.Now, new CloudQueueCacheDependency("test"));
/// </code>
/// The following code that sends a message to the queue "invalidatecache-roleinsanceid" will invalidate the cache item "cache-item-key"
/// <code>
/// var queue = GetQueue("invalidatecache-roleinsanceid");
/// queue.AddMessage(new CloudQueueMessage("test"));
/// </code>
/// </example>
public class CloudQueueCacheDependency : CacheDependency
{
/// <summary>
/// Initializes a new instance of the CloudQueueCacheDependency class
/// </summary>
/// <param name="key">The key that identifies the cached item</param>
public CloudQueueCacheDependency(string key)
{
this.Id = key;
CloudQueueCacheDependencyMonitor.Instance.RegisterDependency(this);
}
/// <summary>
/// Gets or sets the unique identifier of the cache dependency
/// </summary>
public string Id { get; set; }
/// <summary>
/// Notifies the ASP.NET caching infrastructure that the dependency associated has been changed
/// </summary>
public void NotifyQueueDependencyChanged()
{
this.NotifyDependencyChanged(this, EventArgs.Empty);
}
/// <summary>
/// Called by ASP.NET cache when the dependency is being disposed
/// </summary>
protected override void DependencyDispose()
{
CloudQueueCacheDependencyMonitor.Instance.ReleaseDependency(this);
base.DependencyDispose();
}
}
}
namespace Southworks.WindowsAzure
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading;
using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.ServiceRuntime;
using Microsoft.WindowsAzure.StorageClient;
/// <summary>
/// This is a singleton class that will spin up a thread checks a Windows Azure queue named by default "invalidatecache-roleinstanceid".
/// </summary>
/// <remarks>
/// This is intendede to be started once (e.g.: in the Application_Start) in a Windows Azure web role.
/// A queue will be created and listened for each instance of the web role.
/// For instance, if your role name is MyWebApp, the instance id will be MyWebApp_IN_X where X is the instance number.
/// So if you are running two instances of a web role, calling CloudQueueCacheDependencyMonitor.Start will create and listen thse two queues "invalidatecache-mywebapp-in-0" and "invalidatecache-mywebapp-in-1".
/// </remarks>
public class CloudQueueCacheDependencyMonitor
{
private static readonly CloudQueueCacheDependencyMonitor instance = new CloudQueueCacheDependencyMonitor();
private const int DefaultSleepTimeInMilliseconds = 3000;
private const string DefaultStorageConnectionString = "DataConnectionString";
private const string DefaultQueueName = "invalidatecache";
private int intervalInMilliseconds;
private string prefixQueueName;
private CloudQueue queue;
private List<CloudQueueCacheDependency> dependencies = new List<CloudQueueCacheDependency>();
private object syncLock = new object();
/// <summary>
/// Gets the singleton instance that allows using the CloudQueueCacheDependencyMonitor
/// </summary>
public static CloudQueueCacheDependencyMonitor Instance
{
get
{
return instance;
}
}
/// <summary>
/// Starts a background thread that will monitor the queue
/// "invalidatecache-{roleinstanceid}" in a storage account specified
/// in config with the name "DataConnectionString"
/// </summary>
public void Start()
{
this.Start(DefaultQueueName, DefaultStorageConnectionString, DefaultSleepTimeInMilliseconds);
}
/// <summary>
/// Starts a background thread that will monitor the queue in a specific storage account
/// </summary>
/// <param name="prefixQueueName">The queue to monitor (this will be the prefix that will be appended to the role instance id sanitized)</param>
/// <param name="connectionStringName">The connection string of the storage account where the queue will be created</param>
/// <param name="intervalInMilliseconds">The time between each queue peek</param>
public void Start(string prefixQueueName, string connectionStringName, int intervalInMilliseconds)
{
this.intervalInMilliseconds = intervalInMilliseconds;
this.prefixQueueName = prefixQueueName;
CloudStorageAccount.SetConfigurationSettingPublisher((configName, configSetter) => configSetter(RoleEnvironment.GetConfigurationSettingValue(configName)));
this.CreateQueue(prefixQueueName, connectionStringName);
ThreadPool.QueueUserWorkItem((state) => { Work(); });
}
/// <summary>
/// Register the dependency so that it gets notified when a message is dequeued and matches the Id
/// </summary>
/// <param name="dependency">A dependency that needs to be monitored</param>
public void RegisterDependency(CloudQueueCacheDependency dependency)
{
if (!this.dependencies.Exists(dep => dep.Id.Equals(dependency.Id, StringComparison.OrdinalIgnoreCase)))
{
lock (this.syncLock)
{
this.dependencies.Add(dependency);
}
}
}
/// <summary>
/// Deletes the reference to the dependency
/// </summary>
/// <param name="dependency">A dependency previously registered</param>
public void ReleaseDependency(CloudQueueCacheDependency dependency)
{
if (this.dependencies.Exists(dep => dep.Id.Equals(dependency.Id, StringComparison.OrdinalIgnoreCase)))
{
lock (this.syncLock)
{
this.dependencies.Remove(dependency);
}
}
}
private void CreateQueue(string queueName, string connectionStringName)
{
var account = CloudStorageAccount.FromConfigurationSetting(connectionStringName);
// turning nagle off to increase performance
ServicePoint queueServicePoint = ServicePointManager.FindServicePoint(account.QueueEndpoint);
queueServicePoint.UseNagleAlgorithm = false;
var client = account.CreateCloudQueueClient();
string instanceQueueName = string.Format("{0}-{1}", queueName, this.GetRoleInstanceId());
this.queue = client.GetQueueReference(instanceQueueName.ToLowerInvariant());
this.queue.CreateIfNotExist();
}
private string GetRoleInstanceId()
{
string roleInstance = RoleEnvironment.CurrentRoleInstance.Id;
string sanitized;
if (roleInstance.StartsWith("deployment"))
{
sanitized = roleInstance.Substring(roleInstance.IndexOf('.') + 1).Replace(".", "-");
}
else
{
sanitized = roleInstance.Replace(".", "-").Replace("_", "-");
}
if (sanitized.Length > 64)
{
sanitized = sanitized.Remove(0, sanitized.IndexOf("-") + 1);
if (sanitized.Length > 64)
{
sanitized = sanitized.Remove(0, sanitized.IndexOf("-") + 1);
}
}
return sanitized;
}
private void Work()
{
while (true)
{
var messages = this.queue.GetMessages(32);
if (messages != null)
{
foreach (var message in messages)
{
this.ProcessMessage(message);
this.queue.DeleteMessage(message);
}
}
Thread.Sleep(this.intervalInMilliseconds);
}
}
private void ProcessMessage(CloudQueueMessage message)
{
var content = message.AsString;
var dependency = this.dependencies.SingleOrDefault(dep => dep.Id.Equals(content, StringComparison.OrdinalIgnoreCase));
if (dependency != null)
{
dependency.NotifyQueueDependencyChanged();
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment