I'm trying to use a MemoryCache in .net 4.5 to keep track of and automatically update various items, but it seems like no matter what I set as an AbsoluteExpiration it will always only expire in 15 seconds or more.
I want the cache items to expire every 5 seconds, but it always expires in at least 15 seconds, and if I move the expiration time out, it will end up being something like 15 seconds + my refresh interval, but never less than 15 seconds.
Is there some internal timer resolution that I'm not seeing? I looked through a bit of the reflected System.Runtime.Caching.MemoryCache code and nothing stood out to me, and I haven't been able to find anybody else who has this issue out on the internet.
I have a very basic example below that illustrates the problem.
What I want is for CacheEntryUpdate to be hit every 5 seconds or so and update with new data, but, as I've said, it only ever gets hit in 15+ seconds.
static MemoryCache MemCache;
static int RefreshInterval = 5000;
protected void Page_Load(object sender, EventArgs e)
{
if (MemCache == null)
MemCache = new MemoryCache("MemCache");
if (!MemCache.Contains("cacheItem"))
{
var cacheObj = new object();
var policy = new CacheItemPolicy
{
UpdateCallback = new CacheEntryUpdateCallback(CacheEntryUpdate),
AbsoluteExpiration = DateTimeOffset.UtcNow.AddMilliseconds(RefreshInterval)
};
var cacheItem = new CacheItem("cacheItem", cacheObj);
MemCache.Set("cacheItem", cacheItem, policy);
}
}
private void CacheEntryUpdate(CacheEntryUpdateArguments args)
{
var cacheItem = MemCache.GetCacheItem(args.Key);
var cacheObj = cacheItem.Value;
cacheItem.Value = cacheObj;
args.UpdatedCacheItem = cacheItem;
var policy = new CacheItemPolicy
{
UpdateCallback = new CacheEntryUpdateCallback(CacheEntryUpdate),
AbsoluteExpiration = DateTimeOffset.UtcNow.AddMilliseconds(RefreshInterval)
};
args.UpdatedCacheItemPolicy = policy;
}
I've figured it out. There's an internal static readonly TimeSpan on System.Runtime.Caching.CacheExpires called _tsPerBucket that is hardcoded at 20 seconds.
Apparently, this field is what's used on the internal timers that run and check to see if cache items are expired.
I'm working around this by overwriting the value using reflection and clearing the default MemoryCache instance to reset everything. It seems to work, even if it is a giant hack.
Here's the updated code:
static MemoryCache MemCache;
static int RefreshInterval = 1000;
protected void Page_Load(object sender, EventArgs e)
{
if (MemCache == null)
{
const string assembly = "System.Runtime.Caching, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a";
var type = Type.GetType("System.Runtime.Caching.CacheExpires, " + assembly, true, true);
var field = type.GetField("_tsPerBucket", BindingFlags.Static | BindingFlags.NonPublic);
field.SetValue(null, TimeSpan.FromSeconds(1));
type = typeof(MemoryCache);
field = type.GetField("s_defaultCache", BindingFlags.Static | BindingFlags.NonPublic);
field.SetValue(null, null);
MemCache = new MemoryCache("MemCache");
}
if (!MemCache.Contains("cacheItem"))
{
var cacheObj = new object();
var policy = new CacheItemPolicy
{
UpdateCallback = new CacheEntryUpdateCallback(CacheEntryUpdate),
AbsoluteExpiration = DateTimeOffset.UtcNow.AddMilliseconds(RefreshInterval)
};
var cacheItem = new CacheItem("cacheItem", cacheObj);
MemCache.Set("cacheItem", cacheItem, policy);
}
}
private void CacheEntryUpdate(CacheEntryUpdateArguments args)
{
var cacheItem = MemCache.GetCacheItem(args.Key);
var cacheObj = cacheItem.Value;
cacheItem.Value = cacheObj;
args.UpdatedCacheItem = cacheItem;
var policy = new CacheItemPolicy
{
UpdateCallback = new CacheEntryUpdateCallback(CacheEntryUpdate),
AbsoluteExpiration = DateTimeOffset.UtcNow.AddMilliseconds(RefreshInterval)
};
args.UpdatedCacheItemPolicy = policy;
}
Would you be willing/able to change from the older System.Runtime.Caching to the new Microsft.Extensions.Caching? version 1.x supports netstandard 1.3 and net451. If so then the improved API would support the usage you describe without hackery with reflection.
The MemoryCacheOptions object has a property ExpirationScanFrequency to allow you to control the scan frequency of the cache cleanup, see https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.caching.memory.memorycacheoptions.expirationscanfrequency?view=aspnetcore-2.0
Be aware that there is no longer expiration based on timers (this is a performance design decision), and so now memory pressure or calling one of the Get() based methods for the cached items are now the triggers for expiration. However you can force time based expiration using cancellation tokens, see this SO answer for an example https://stackoverflow.com/a/47949111/3140853.
To MatteoSp - the pollingInterval in the configuration or the NameValueCollection in the constructor is a different timer. It is an interval that when called will use the two other config properties to determine if memory is at a level that requires entries to be removed using the Trim method.
An updated version basing on #Jared's answer. Insread of modify the default MemoryCache instance, here creates a new one.
class FastExpiringCache
{
public static MemoryCache Default { get; } = Create();
private static MemoryCache Create()
{
MemoryCache instance = null;
Assembly assembly = typeof(CacheItemPolicy).Assembly;
Type type = assembly.GetType("System.Runtime.Caching.CacheExpires");
if( type != null)
{
FieldInfo field = type.GetField("_tsPerBucket", BindingFlags.Static | BindingFlags.NonPublic);
if(field != null && field.FieldType == typeof(TimeSpan))
{
TimeSpan originalValue = (TimeSpan)field.GetValue(null);
field.SetValue(null, TimeSpan.FromSeconds(3));
instance = new MemoryCache("FastExpiringCache");
field.SetValue(null, originalValue); // reset to original value
}
}
return instance ?? new MemoryCache("FastExpiringCache");
}
}
Related
I am using MemoryCache in one of my project to cache some keys and value. I have attached a listener on my MemoryCache that whenever it is expiring any items then that listener should get called so that I can remove those keys from CacheKeys HashSet as well. Basically I want to be consistent with what I have in CacheKeys and MemoryCache.
I read about MemoryCache and looks like I can use RegisterPostEvictionCallback as shown below:
private static readonly HashSet<string> CacheKeys = new HashSet<string>();
private bool CacheEntries<T>(MemoryCache memoryCache, string cacheKey, T value, Configuration config, Action<MemoryCacheEntryOptions> otherOptions = null)
{
int minutes = randomGenerator.Next(config.LowTime, config.HighTime);
MemoryCacheEntryOptions options = new MemoryCacheEntryOptions()
{
Size = config.Size,
Priority = config.Priority,
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(minutes)
};
// attaching listener
options.RegisterPostEvictionCallback(callback: EvictionCallback, state: this);
if (otherOptions != null) otherOptions(options);
CacheKeys.Add(cacheKey);
memoryCache.Set<T>(cacheKey, value, options);
return true;
}
private void EvictionCallback(object key, object value, EvictionReason reason, object state)
{
CacheKeys.Remove(key);
var message = $"Entry was evicted. Reason: {reason}.";
Console.WriteLine(message);
}
It looks like there is some issue where items doesn't expire automatically as per this this thread.
So to avoid the issue mentioned in that thread. Do I need to remove AbsoluteExpirationRelativeToNow and use CancellationTokenSource here?
If yes, how should I go ahead and make the change since I don't want to change the current functionality that I have already in my original code so if I remove AbsoluteExpirationRelativeToNow and use CancellationTokenSource here will it behave differently than what my original code is doing?
Referencing this answer regarding regenerating new SessionID
I created this code in my Global.asax.cs:
protected void Application_Start(object sender, EventArgs e)
{
Bootstrapper.Initialized += new EventHandler<ExecutedEventArgs>(Bootstrapper_Initialized);
}
void Bootstrapper_Initialized(object sender, Telerik.Sitefinity.Data.ExecutedEventArgs e)
{
if (e.CommandName == "Bootstrapped")
{
EventHub.Subscribe<ILoginCompletedEvent>(LoginCompletedEventVerification);
}
}
private void LoginCompletedEventVerification(ILoginCompletedEvent evt)
{
if (evt.LoginResult == UserLoggingReason.Success)
{
var manager = new SessionIDManager();
var oldId = manager.GetSessionID(Context);
var newId = manager.CreateSessionID(Context);
bool isAdd = false, isRedir = false;
manager.SaveSessionID(Context, newId, out isRedir, out isAdd);
var ctx = HttpContext.Current.ApplicationInstance;
var mods = ctx.Modules;
var ssm = (SessionStateModule)mods.Get("Session");
var fields = ssm.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance);
SessionStateStoreData rqItem = null;
SessionStateStoreProviderBase store = null;
FieldInfo rqIdField = null, rqLockIdField = null, rqStateNotFoundField = null;
foreach (var field in fields)
{
if (field.Name.Equals("_store")) store = (SessionStateStoreProviderBase)field.GetValue(ssm);
if (field.Name.Equals("_rqId")) rqIdField = field;
if (field.Name.Equals("_rqLockId")) rqLockIdField = field;
if (field.Name.Equals("_rqSessionStateNotFound")) rqStateNotFoundField = field;
if ((field.Name.Equals("_rqItem")))
{
rqItem = (SessionStateStoreData)field.GetValue(ssm);
}
}
var lockId = rqLockIdField.GetValue(ssm);
if ((lockId != null) && (oldId != null))
{
store.ReleaseItemExclusive(Context, oldId, lockId);
store.RemoveItem(Context, oldId, lockId, rqItem);
}
rqStateNotFoundField.SetValue(ssm, true);
rqIdField.SetValue(ssm, newId);
}
}
Please keep in mind that I am developing in a Sitefinity web application.
Every time my application hits LoginCompletedEventVerification during a successful login, Context comes up as null. Now, I initially wanted to add this snippet to the Sitefinity LoginWidget, but making that happen is a whole other story.
I did not include it in the code sample, but I do have Session_Start firing to create my application's "shopping cart." I am just trying to create a new SessionID for the cart after authentication.
Is there a reason I cannot get a value for Context during this event?
Thanks in advance. I appreciate any suggestions or criticism!
EDIT: Sitefinity knowledge base article where I got my Bootstrapper_Initialized code
I did not include it in the code sample, but I do have Session_Start
firing to create my application's "shopping cart." I am just trying to
create a new SessionID for the cart after authentication.
Nooooo. Forget about accessing HttpContext in the Application_Start event.
Alternatively you could do that in Application_BeginRequest:
private static object syncRoot = new object();
private static bool initialized = false;
public void Application_BeginRequest(object sender, EventArgs e)
{
if (!initialized)
{
lock (syncRoot)
{
if (!initialized)
{
// Do your stuff here with HttpContext
initialized = true;
}
}
}
}
Another thing you should be aware of is that HttpContext will not be available in any background threads that you might have spawned and in which the HTTP request has already finished executing. So you should be extremely careful where you are trying to access this HttpContext.
The LoginCompletedEvent event is synchronous - it is not fired in a background thread, it is rather part of the authentication request. You can access the current context by either directly calling HttpContect.Current or since you are in the context of a Sitefinity application you can use the Sitefinity wrapper for the current context:
var currentContext = SystemManager.CurrentHttpContext;
The following test fails intermittently. It caches an item in MemoryCache with an absolute expiration time, and an update callback that should be called before the item is removed. However sometimes the callback is invoked before the test finishes, and sometimes not at all.
With a large enough buffer time it will always be invoked at least once. But that does not serve my purposes, since I require that the cache always attempts to update the data before it expired.
Now in my real world scenario I will not have 10 second expiration time and granularity, but it still bothers me that this test fails intermittently.
Anyone have thoughts on why this is happening?
Note: Also intermittently fails with 60 second expiry and 5 second buffer.
using System;
using System.Runtime.Caching;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
[TestClass]
public class MemoryCacheTest
{
private const double ExpiryInSeconds = 10;
private const double ExpiryBufferInSeconds = 5;
private readonly object updateItemCounterLock = new object();
private int updateItemCounter = 0;
[TestMethod]
public async Task MemoryCacheUpdateTest()
{
// Set item in cache with absolute expiration defined above
MemoryCache cache = MemoryCache.Default;
CacheItem cacheItem = new CacheItem("key", "value");
CacheItemPolicy cacheItemPolicy = new CacheItemPolicy
{
AbsoluteExpiration = DateTimeOffset.Now + TimeSpan.FromSeconds(ExpiryInSeconds),
UpdateCallback = new CacheEntryUpdateCallback(this.UpdateItem)
};
cache.Set(cacheItem, cacheItemPolicy);
// Delay for absolute expiration time + buffer
await Task.Delay(TimeSpan.FromSeconds(ExpiryInSeconds) + TimeSpan.FromSeconds(ExpiryBufferInSeconds));
// Test that the update callback was invoked once
Assert.AreEqual(1, updateItemCounter);
}
// Incrememnts the updateItemCounter
private void UpdateItem(CacheEntryUpdateArguments args)
{
lock (updateItemCounterLock)
{
updateItemCounter++;
}
}
}
I suppose calling new CacheEntryUpdateCallback is redundant. You can call:
UpdateCallback = new CacheEntryUpdateCallback(this.UpdateItem) instead
Since there was no solution to this question, I abstracted the MemoryCache methods that I needed into an interface and tested against that. At that point the test became invalid because I would have just been testing my own implementation of the interface.
I've got a simple object being cached like this:
_myCache.Add(someKey, someObj, policy);
Where _myCache is declared as ObjectCache (but injected via DI as MemoryCache.Default), someObj is the object i'm adding, and policy is a CacheItemPolicy.
If i have a CacheItemPolicy like this:
var policy = new CacheItemPolicy
{
Priority = CacheItemPriority.Default,
SlidingExpiration = TimeSpan.FromHours(1)
};
It means it will expire in 1 hour. Cool.
But what will happen is that unlucky first user after the hour will have to wait for the hit.
Is there any way i can hook into an "expired" event/delegate and manually refresh the cache?
I see there is a mention of CacheEntryChangeMonitor but can't find any meaninful doco/examples on how to utilize it in my example.
PS. I know i can use CacheItemPriority.NotRemovable and expire it manually, but i can't do that in my current example because the cached data is a bit too complicated (e.g i would need to "invalidate" in like 10 different places in my code).
Any ideas?
There's a property on the CacheItemPolicy called RemovedCallback which is of type: CacheEntryRemovedCallback. Not sure why they didn't go the standard event route, but that should do what you need.
http://msdn.microsoft.com/en-us/library/system.runtime.caching.cacheitempolicy.removedcallback.aspx
Late to the party with this one but I've just noticed an interesting difference between CacheItemUpdate and CacheItemRemove callbacks.
http://msdn.microsoft.com/en-us/library/system.web.caching.cacheitemupdatereason.aspx
In particular this comment:
Unlike the CacheItemRemovedReason enumeration, this enumeration does
not include the Removed or Underused values. Updateable cache items
are not removable and can thus never be automatically removed by
ASP.NET even if there is a need to free memory.
This is my way to use CacheRemovedCallback event when cache expired.
I share for whom concern.
public static void SetObjectToCache<T>(string cacheItemName, T obj, long expireTime)
{
ObjectCache cache = MemoryCache.Default;
var cachedObject = (T)cache[cacheItemName];
if (cachedObject != null)
{
// remove it
cache.Remove(cacheItemName);
}
CacheItemPolicy policy = new CacheItemPolicy()
{
AbsoluteExpiration = DateTimeOffset.Now.AddMilliseconds(expireTime),
RemovedCallback = new CacheEntryRemovedCallback(CacheRemovedCallback)
};
cachedObject = obj;
cache.Set(cacheItemName, cachedObject, policy);
}
public static void CacheRemovedCallback(CacheEntryRemovedArguments arguments)
{
var configServerIpAddress = Thread.CurrentPrincipal.ConfigurationServerIpAddress();
long configId = Thread.CurrentPrincipal.ConfigurationId();
int userId = Thread.CurrentPrincipal.UserId();
var tagInfoService = new TagInfoService();
string returnCode = string.Empty;
if (arguments.CacheItem.Key.Contains("DatatableTags_"))
{
// do what's needed
Task.Run(() =>
{
});
}
}
I have a class that contains the following property:
public Dictionary<string, int> CommentCounts {
get {
string cacheKey = "CommentCounts";
HttpContext c = HttpContext.Current;
if (c.Cache[cacheKey] == null) {
c.Cache.Insert(cacheKey, new Dictionary<string, int>(), null, DateTime.UtcNow.AddSeconds(30), System.Web.Caching.Cache.NoSlidingExpiration, CacheItemPriority.High, null);
c.Trace.Warn("New cached item: " + cacheKey);
}
return (Dictionary<string, int>)c.Cache[cacheKey];
}
set {
HttpContext.Current.Cache["CommentCounts"] = value;
}
}
It seems that the Trace statement only runs once, and not every 30 seconds after the Cache item has expired. The only way I can get it to refresh the Cached item is to make a code chance and rebuild the project, which is obviously less than ideal.
What am I missing? Thanks in advance...
The set part of the property is probably the cause - Cache["key"] = value is equivalent to calling Cache.Insert with NoAbsoluteExpiration, NoSlidingExpiration which means it never expires. The correct solution would look like this:
public Dictionary<string, int> CommentCounts {
get {
const string cacheKey = "CommentCounts";
HttpContext c = HttpContext.Current;
if (c.Cache[cacheKey] == null) CommentCounts = new Dictionary<string, int>();
return (Dictionary<string, int>)c.Cache[cacheKey];
}
set {
const string cacheKey = "CommentCounts";
c.Cache.Insert(cacheKey, value, null, DateTime.UtcNow.AddSeconds(30), System.Web.Caching.Cache.NoSlidingExpiration, CacheItemPriority.High, null);
c.Trace.Warn("New cached item: " + cacheKey);
}
}
I had this problem before and asked a question here; it was never really answered, but there might be some helpful debugging tips in the answers.