-
Notifications
You must be signed in to change notification settings - Fork 11k
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
Concurrency issue between get(K key, Callable<? extends V> valueLoader) and invalidate(Object key) #1881
Comments
Hi, Do you plan to fix this issue for the next release? Thanks |
My first reaction is that the current semantics make some sense: an entry still being computed isn't really in the cache; it's not really there to be invalidated? |
Hi, Speaking of semantics, it's not value that is being invalidated, it's the key. The key is certainly there - and the value is being calculated. Given that value V(K) is calculated from some other value X(K) (a row in DB, in this sample case), invalidate(K) should mean that any values based on values of X(K) retrieved up to the moment of invalidation are subject for recalculation. Otherwise, the cache is pretty much useless in a simple case of caching a database row. |
Let's assume that we did what you're saying: we invalidated the pending fetch. What exactly should the ongoing get() call from the other thread return? Restarting the load() call seems awkward, if it's even possible. |
The problem does not come from the current pending fetch. |
Okay. If I understand correctly, that stale value will never be stored, but will be returned from the get? |
I'd say this would be the expected behaviour. |
I don't think that's 100% obvious -- I suspect there are cases where you don't want the value-in-flight to be invalidated -- and I'd also expect this to require a fairly significant rewrite to the internals of Cache, but we can research this further. |
I don't really have anything concrete to say here, but I remember that, when it came to |
@lowasser I'm trying out the implementation in my private fork. I had to revisit some tests, and from what I see, there is no good reason for the current behaviour. Some tests actually assume that the value does not change when the invalidation happens, which contradicts the purpose of invalidation. All existing tests are currently green, and ones I added too; but I need to add more tests to check subtler consequences of my changes. I can't guarantee that I will do it fast, but if you have time, let me have a try on this one. |
Please do; I think we'd welcome contributions on this. |
This is causing some very serious issues for us. I would argue that anyone using a lazy-loading cache would expect a happens-before relationship between invalidate and a following call to get, but since it can piggy-back on an existing load operation that fetched data before invalidate was called, that relationship does not exist. I started poking around in here to try to fix it, but as @lowasser hinted, it isn't trivial. The main problems are
This is sufficiently painful that at this point I'm inclined to switch to another library, instead. |
@txshtkckr Please see https://github.com/baltiyskiy/guava -- I've went with the "remove LoadingValueReference" approach, firing removal notification for the value when it is loaded. It's generally done, but I'd like to add some tests cases there. There's one known problem (regaring I've put it on a pause now because I don't have much time due to other commitments, but I intend to finish it. |
I second @sereda expectation of causality, in that a loaded value V(K) must be updated when the key is invalidated. |
The asMap().compute implementation did not take into account that the present value may be loading. A load does not block other writes to that entry and takes into account that it may be clobbered, causing it to automatically discard itself. This is a known design choice that breaks linearizability assumptions (google#1881). The compute should check if a load is in progress and call the appropriate internal removal method. Because a zombie entry remained in the cache and still is marked as loading, the loader could discover entry and try to wait for it to materialize. When the computation is a removal, indicated by a null value, the loader would see this as the zombie's result. Since a cache loader may not return null it would throw an exception to indicate a user bug. A new ComputingValueReference resolves both issues by indicating that the load has completed. The compute's removeEntry will then actually remove this entry and the loader will not wait on the zombie. Instead if it observes the entry, it will neither receive a non-null value or wait for it to load, but rather try to load anew under the lock. This piggybacks on the reference collection support where an entry is present but its value was garbage collected, causing the load to proceed. By the time the lock is obtained the compute method's entry was removed and the load proceeds as normal (so no unnecessary notification is produced). fixes google#5342 fixes google#2827 resolves underlying cause of google#2108
@cpovirk sorry for refreshing an old issue, it seems still relevant. For me, " |
Caffeine resolves this for explicit keyed operations, but not for clearing out the whole cache. That is because it decorates, rather than forks, ConcurrentHashMap which suppresses in-flight loads from appearing in its iterators. A trivial workaround to that is to append a generational id to the key so that existing entries cannot be retrieved and a stale in-flight load detected by the reader to possibly retry. |
@ben-manes thanks for the update about Caffeine. I am still looking for the Guava team's perspective on the problem. @lowasser 's comment from 2014 (#1881 (comment)) suggests a patch would be welcome, but there was also a concern about value-in-flight invalidation (#1881 (comment)). Of course, both are couple years old, so they not necessarily describe current state. Edit: a Guava Cache wrapper that doesn't suffer from invalidation race was extracted from Trino project as a standalone library: https://github.com/findepi/evictable-cache |
Due to google/guava#1881, invalidation does not invalidate any loading that is in progress. In order to overcome this a generationId field was added to the cache keys. However it is partial fix, the issue still may exists when entries are invalidated per key.
The `ContentCache` uses Caffeine. Caffeine offers partial solution to google/guava#1881 race condition problem present in Guava Cache implementation, but not when `invalidateAll` is used. To avoid accidental incorrect use of ContentCache, remove the `invalidateAll` method, which can be deceptive for the caller.
This method does only best-effort invalidation and is susceptible to a race condition. If the caller changed the state that could be cached (perhaps files on the storage) and calls this method, there is no guarantee that the cache will not contain stale entries some time after this method returns. This is a similar problem as the one described at google/guava#1881. `ContentCache` doesn't use a Guava Cache, it uses Caffeine. Caffeine offers partial solution to this issue, but not for `invalidateAll` call. To avoid accidental incorrect use of ContentCache, deprecate the `invalidateAll` method, which can be deceptive for the caller and remove it later.
This method does only best-effort invalidation and is susceptible to a race condition. If the caller changed the state that could be cached (perhaps files on the storage) and calls this method, there is no guarantee that the cache will not contain stale entries some time after this method returns. This is a similar problem as the one described at google/guava#1881. `ContentCache` doesn't use a Guava Cache, it uses Caffeine. Caffeine offers partial solution to this issue, but not for `invalidateAll` call. To avoid accidental incorrect use of ContentCache, deprecate the `invalidateAll` method, which can be deceptive for the caller and remove it later.
This method does only best-effort invalidation and is susceptible to a race condition. If the caller changed the state that could be cached (perhaps files on the storage) and calls this method, there is no guarantee that the cache will not contain stale entries some time after this method returns. This is a similar problem as the one described at google/guava#1881. `ContentCache` doesn't use a Guava Cache, it uses Caffeine. Caffeine offers partial solution to this issue, but not for `invalidateAll` call. To avoid accidental incorrect use of ContentCache, deprecate the `invalidateAll` method, which can be deceptive for the caller and remove it later.
This method does only best-effort invalidation and is susceptible to a race condition. If the caller changed the state that could be cached (perhaps files on the storage) and calls this method, there is no guarantee that the cache will not contain stale entries some time after this method returns. This is a similar problem as the one described at google/guava#1881. `ContentCache` doesn't use a Guava Cache, it uses Caffeine. Caffeine offers partial solution to this issue, but not for `invalidateAll` call. To avoid accidental incorrect use of ContentCache, deprecate the `invalidateAll` method, which can be deceptive for the caller and remove it later.
* Deprecate ContentCache.invalidateAll This method does only best-effort invalidation and is susceptible to a race condition. If the caller changed the state that could be cached (perhaps files on the storage) and calls this method, there is no guarantee that the cache will not contain stale entries some time after this method returns. This is a similar problem as the one described at google/guava#1881. `ContentCache` doesn't use a Guava Cache, it uses Caffeine. Caffeine offers partial solution to this issue, but not for `invalidateAll` call. To avoid accidental incorrect use of ContentCache, deprecate the `invalidateAll` method, which can be deceptive for the caller and remove it later. * Document ContentCache.invalidate blocking nature * empty
* Deprecate ContentCache.invalidateAll This method does only best-effort invalidation and is susceptible to a race condition. If the caller changed the state that could be cached (perhaps files on the storage) and calls this method, there is no guarantee that the cache will not contain stale entries some time after this method returns. This is a similar problem as the one described at google/guava#1881. `ContentCache` doesn't use a Guava Cache, it uses Caffeine. Caffeine offers partial solution to this issue, but not for `invalidateAll` call. To avoid accidental incorrect use of ContentCache, deprecate the `invalidateAll` method, which can be deceptive for the caller and remove it later. * Document ContentCache.invalidate blocking nature * empty
Hi,
I encountered a concurrency issue between the "get(K key, Callable<? extends V> valueLoader)" and "invalidate(Object key)" methods with a basic (and i suppose a common) usage of the cache which let a stale value in the cache.
Here is the use case:
Thread 1
Thread 2
The execution sequence order is marked with // (number)
After the point // 4, I have the old object in myBean variable which is fine.
However, If i try to get again the bean with the identifier ID, I expect to have no value in the cache but a fresh bean reloaded from the DB.
Unfortunately, that is not the case and a stale value is kept in the cache.
Is that a wanted behavior?
In LocalCache$Segment#remove(Object key, int hash) line 3081 to 3087 (guava 17.0):
Shouldn't a particular process be done before return null ?
Thanks
The text was updated successfully, but these errors were encountered: