alastairtree / LazyCache

An easy to use thread safe in-memory caching service with a simple developer friendly API for c#
https://nuget.org/packages/LazyCache
MIT License
1.72k stars 159 forks source link

AddOrUpdate and AddOrUpdateAsync methods [2] #150

Open Temtaime opened 3 years ago

Temtaime commented 3 years ago

Hello. To continue closed #95 issue. Your point that one can manually call Remove/Add is wrong. The whole point of this library is to provide thread-safe and atomic way to cache something evaluating a callback only once. AddOrUpdate cannot be emulated with custom Remove/Add because it requires additional synchronization. It can be useful when one know that element must be updated and such a request must be processed atomically. Update callback must be provided with previous value and evaluated once.

jackfox10 commented 3 years ago

This would be very helpful. Sometimes you need to update part of a cache. I don't think it is possible to do this in a thread safe way without implementing your own locking/synchronisation. In which case you would not use LazyCache.

For example if I had a cache of "orders" which stores a list of orders ordered today. If I receive a new order, I want to add it to the list:

var orderList= cache.Get<List<int>>("orders");
orderList.Add(someId);
cache.Add("orders", orderList);

This not safe. What if another thread calls cache.Get() before this thread calls cache.Add()

It would be a great addition if the cache could handle the necessary locking and the developer could do something like: cache.AddOrUpdate("orders", currentOrders => currentOrders.Add(someId))

Even if the developer was responsible for cloning or providing the new cache, it would still be a huge help:

cache.AddOrUpdate("orders", currentOrders => {
    var copyOfList = currentorders.Select(a =>a);  //shallow
    copyOfList.Add(someId);
    return copyOfList;
})

I would be interested to hear what people have done to work around the problem so far...

alastairtree commented 3 years ago

I think there is a few scenarios to unpack here, and how to handle them below, which might help explain why there is no AddOrUpdate.

Say for some reasons I have a cached order record V1 and I know I want to update the cached instance of it and replace with V2. Say it is cached with key "order-123":

A - I already have the new version V2 of the cached order in memory and all threads should start using it now Then just call cache.Add("order-123", orderV2); and this will replace the currently cached instance.

B - I want to discard the current order and the next request should use the latest version of the order Then just call cache.Remove("order-123"); and let the next request generate the new version when it calls GetOrAdd.

C - I want to generate the new version of the order, and any requests for the cached order that come in while I am generating must wait while generating v2 and must use the new version This is the same as just removing the order from the cache, except once removed you trigger the cache population again. LazyCache wil ensure all threads wait on the same cache key so this will be an atomic operation and only generate once. So just call cache.Remove("order-123"); and then var orderV2 = cache.GetOrAdd("order-123", () => GetOrderV2());

D - I want to generate the new version of the order in the background, and any requests for the cached order that come in while I am generating must use the existing V1. Once V2 is finished generated then use it This is the same as A, except you are actively generate the instance var orderV2 = GetOrderV2(); and then cache.Add("order-123", orderV2);. However it is up to the developer to ensure the background thread is only running once but that usually requires wider knowledge of the platform, such as IHostedService on aspnet.

So depending on the use case different behaviours can be achieved without the need for AddOrUpdate, and while being atomic, and Lazy (for A-C at least).

There is one variant, in example D where the cached item expires during generation, and so you might then generate orderV2 more than once. Usually the best way to solve this is to be sure to refresh before cache expiry, such as on a timer. If you really wanted to handle D in a guaranteed atomic way then yes you would need a number of enhancements to LazyCache but in my experience the methods listed here have been sufficient for me.

jackfox10 commented 3 years ago

Thanks @alastairtree for this information. I think in cases C and D it would be useful to update. Imagine if you only want to increment a value in the cache without calling the expensive full load factory method. To update, you would need to call cache.Get() before calling cache.Add() or cache.Remove() which introduces the race condition. Do you agree?

jesuissur commented 2 years ago

@jackfox10 That's exactly the scenario we got. We want to update the value in cache based on the previous value or add the new value if there's no previous value. Without AddOrUpdate method, we got to implement our own lock mechanism around this 2-steps operation to avoid any race condition.