-
Notifications
You must be signed in to change notification settings - Fork 4.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
New memory cache implementation #48567
Comments
Tagging subscribers to this area: @eerhardt, @maryamariyan, @michaelgsharp Issue DetailsBackground and MotivationThe MemoryCache implementation and interface leaves much to be desired. At the core of it, we ideally want to expose something more akin to
Proposed APIThe APIs are still TBD but I'm thinking a generic memory cache. namespace Microsoft.Extensons.Caching
{
public class IMemoryCache<TKey, TValue>
{
// TBD
}
} Usage ExamplesTBD Alternative DesignsTBD RisksHaving 2 implementations.
|
How is it being typically used by the end-users: to cache multiple instances of the same type or different types? If we make it generic but users end up having more cache instances we might get a memory overhead bigger from just the boxing in the non-generic version. |
DateTimeOffset? AbsoluteExpiration { get; set; }
TimeSpan? AbsoluteExpirationRelativeToNow { get; set; }
TimeSpan? SlidingExpiration { get; set; } Would it be possible to make these properties have setter only, to keep the internal representation of these fields an implementation detail? So we could for example translate private long _absoluteExpirationTicks;
private readonly uint _slidingSeconds; We could also take @edevoogd experiment under the consideration: #45842 (comment) |
The StackOverflow implementation also introduced an |
Libraries end up with their own private caches. Apps usually have a single cache. |
Wish: Pre- vs. PostEvictionCallbacks (instead of or additional) Short: Called before eviction with an option to skip eviction / keep the cache entry. Long: Skip could be:
And all topic main 👍 !.. That could reduce my code removing reflection to get the main collection etc. + make cache a lot faster with this suggestion. Think the hardest part of cache is estimating the memory. Came here to clone - hope above will be a thing ~:) |
Can you file an issue for this? With details and usage patterns etc. |
There is a long running discussion on why I have not implemented Clear() in LazyCache over at alastairtree/LazyCache#67 which largely is due to the use of MemoryCache and it's current API. If this went ahead I suspect I would rewrite LazyCache to use MemoryCache<TKey, TValue> as David's proposal would make that much easier. Worth considering usage scenarios - are you suggesting one cache per item-type, or one cache per app? Having TValue in the definition would encourage me to have one cache per item-type, which makes doing Clear on all cached data (a common use case) more difficult as there are many caches. Typically up to now apps usually have one cache, with the TValue know at time of Get, but that does cause the boxing issue. |
I think having lots of caches is fine (we basically have this today all over the framework). It's should be treated like a dictionary with expiration. |
I would like to see cache hit and cache miss EventCounters per cache. I want to be able to capture metrics in something like InfluxDb. And filter the cache name as a Tag and the cache-hit/cache-miss as a field. Today I am experimenting with LazyCache and adding metrics. I found with my strategy of collecting counters by cache name it becomes a dynamic process of adding counters as they are called rather than knowing the cache names ahead of time and then having to create a specific EventSource for every application. At the moment I have not reached my destination with LazyCache and many named caches, but the point is I would hope some thought would be put towards metrics or at least hooks to allow metrics to be plugged in. Looking around the .NET ecosystem in relation to resiliency frameworks, hooks for metrics don't seem to be a consideration. Like looking at Polly, metrics are absent and not easy to add. |
My 2 cents:
One question: what would be the rationale behind the implementation of the GetOrSet/Add via an ext method instead of being a functionality baked in? |
There are ample cases where people cache objects with specific key/value types (similarly to Dictionary). EF Core uses this internally in multiple places (see #48455). |
I see what you are saying, but I think there is a big difference between what could be described as "a dictionary with an optional expiration logic" and "an application cache", and maybe we are trying to squeeze 2 different concepts into the same abstraction, which may be problematic. I can see a partial convergence in the feature set, but imho they should remain separate. Maybe the right thing would be to have 2 different types? |
Seems fine.
We may remove it on this generic cache and instead expose a specialized cache for strings and bytes where the count actually works.
GetOrAdd can be built in but right now our implementation doesn't work well because we're missing the right primitives on the IMemoryCache to implement it. |
Would we consider an Overall, we found that there is a huge barrier to taking and analyzing a memory dump at scale. However, if we can expose this information in the app with a few tweaks like this it becomes immensely more useful and actionable by far more people. Fram a practical standpoint, we went from doing memory dumps and using ClrMd in LINQPad or other bits to having views like this live in production any time: (apologies for the bad developer UX, but you can see the info!) If anyone's curious about the last one - it's the death by a thousand papercuts, we've found it very useful to just crawl the cache and dump some random entries to see any surprises. The Ideally, whatever primitives we move to here would allow such enumeration without that high "take a memory dump" bar. At current, it doesn't seem possible because:
|
How would users make sense of which of the many memory caches they should use? Are neither of the existing cache APIs redeemable? |
I don't think this matters. The whole point of this implementation is to be a lightweight concurrent dictionary with expiration that doesn't try to respond to memory pressure and start kicking out entries. That's the only reason I see to be concerned about having a single cache. The for it to have a global process wide view of the state. That is already not the case today with .NET Core. Applications have different caches with different policies. I think this is no different.
I don't think so, but I haven't given it a fair chance. |
That is what I was talking about: being for a single The use case is surely interesting, and I for one would like to have that at my disposal, but calling it the "new memory cache implementation" would be potentially misleading: maybe putting it in the |
FYI the current IMemoryCache does the same. To be clear, we already end up with multiple in the app that each have they own policies for eviction. I hear you in the size limit and generics though. The generics especially encourage more caches in the app itself and might cause confusion in some cases. |
FWIW in the EF Core usage, query compilation artifacts are being cached (rather than e.g. remote data). So expiration definitely isn't what we're looking for (artifacts remain valid forever), but rather a decent LRU which guarantees that memory consumption is capped in case there's an explosion of queries. Not saying this has to be the same type/problem being discussed here, though. |
It's mostly not about the length of the strings but the number of times you have to compute them and allocate them. That's CPU comsumtion to compute the key and memory and GC work to allocate/deallocate them.
Beware that different strings might have the same hash code. |
Given that memory pressure based eviction seems to be an explicit non-goal here, I guess unloading is also a non-goal. But I wanted to make sure it's considered. It's another example of an eviction policy, but one which should be typically combined with some other eviction policy and almost never used on its own. What I mean by unloading is making sure that all entries which have references to an I'm sure it's possible to build this on top, but then it becomes a question of how to make all the libraries in the app implement these additional eviction policies so that the app works as a whole (and for example can successfully unload a plugin). |
Yep, for lack of a better option generally. It's also very fast though, for example you could cache based on the hash of a tuple or something, but since that would be on fetch there is a computational cost to it. Is that cheaper than the GC cost? I'm not sure, but it's an interesting experiment to try at scale. Most alternatives I'm aware of have similar risks of hash collision if that's your lookup model, though. I'm all ears for better options - strings are simple, easy to understand, and fairly cheap to compute (though you deal with cost later) - that's not an argument for them over other things, it just places them high on the usability tradeoff scale today. |
For what it's worth, I always thought a model that allow caching via a tuple with minimum allocations might be interesting, but I'm not sure how we address the collision issue or exactly what the interface for such could look like in the current type system. If you had If we could have a cache that internally was exposed like (these arguments being a completely arbitrary example): public MyCachedType Get(int userId, int categoryId)
{
var key = ("SomeUniqueKey", userId, category); // Tuple here for cache key
// ...something...
} Today, we'd do something like: public MyCachedType Get(int userId, int categoryId)
{
var key = $"SomeUniquePattern:{userId.ToString()}:{categoryId.ToString()}";
// ...something...
} (so that it's using ...anyway, I think that'd potentially be useful, but would want a shared cache for such a variant key pattern. The return type from cache would likely be |
It seems to me like there are two quite different usecases that people are looking for, and that a) trying to address both in one design might lead to a compromise, and b) the current discussion/proposal seems to lean more heavily towards the "ConcurrentDictionary with expiration, caching objects with key/value types, N caches" scenario. Should there be a separate proposal/interface for the latter application-cache usecase? To prevent the design from getting muddled between the two different needs. I just feel like the two usecases are sufficiently different that trying to address both in one API might make things messy. Cheers! |
One thing to consider is how much should be in the runtime as default. There are several caching libraries in the .NET ecosystem (FusionCache, CacheManager, Cache Tower, EasyCaching, LazyCache, MonkeyCache, and probably a bunch of others) which can handle the more complicated and feature rich scenarios. I am biased as the creator of one of those caching libraries but my view is that it is the simple/common scenario that the MemoryCache implementation should aim for - the |
@ericsampson I thnik you nailed here the core of the problem While from my standpoint, 99% of the cases would be solved by ConcurrentDictionary with expiration as they mainly revolve around small services where I simply don't want to hammer one particular resource too much with as small overhead as it can be. |
That's a fair point :) If the .NET/ASP docs for caching can list these community packages, that would go a long way.
"straight forward" is an important qualifier :) |
+1 |
Its possible to work around manually. |
If you'd like to avoid the cache stampede problem you can take a look at some alternatives (in alphabetical order):
Hope this helps. |
sure, it can work around by lazy and lock etc, but not everyone can realize that it may have the problem of cache stampede , which will lead to serious consequences. |
I know it's a fairly broad question where implementation may depend on uses cases, but what solution would you suggest when the cache key is dynamic ? (for example when you want to cache fetched user permissions so cache key could be user-17, user-18, ...). Having a |
One of these #48567 (comment) ? |
Thanks @jodydonetti, since our need is simply an IMemoryCache without cache stampede issue, nothing more, nothing less. We went with @StephenCleary solution in the end: https://gist.github.com/StephenCleary/39a2cd0aa3c705a984a4dbbea8275fe9 I like this solution, it's a slim wrapper on top of IMemoryCache and you can easily follow the code. |
Consider something that can be extensible to support Azure Caching Guidance
|
There's a proposal for a new cache implementation here dotnet/extensions#4766. Can those interested review it and leave comments? |
Thanks David, will do! |
Interesting that there is no mention of managing cache dependencies in this thread. Not that it would have to be baked into a new cache class, but still seems like a relevant design consideration. Is nobody really using a consistent pattern for this that they want their cache class to handle? Is everyone just opting for inline code in each application component requiring this, and managing its specific set of dependent caches? |
I was bitten by this just recently. I did not see any documentation in CreateEntry() nor on ICacheEntry that stated that the object needed to be disposed to commit it to the cache. As a result, the code I initially wrote didn't have working caching! I would like to see this documentation clarified for the current version of the cache at least. |
Background and Motivation
The MemoryCache implementation and interface leaves much to be desired. At the core of it, we ideally want to expose something more akin to
ConcurrentDictionary<TKey, TValue>
that supports expiration and can handle memory pressure. What we have right now has issues:Proposed API
The APIs are still TBD but I'm thinking a generic memory cache.
I'm convinced now that this shouldn't be an interface or an abstract class but I'm open to discussion.
Usage Examples
TBD
Alternative Designs
TBD
Risks
Having 3 implementations.
cc @Tratcher @JunTaoLuo @maryamariyan @eerhardt
The text was updated successfully, but these errors were encountered: