From 4e32b1b0cfeb0349dae80b5c8228f9836ebb7172 Mon Sep 17 00:00:00 2001 From: IvarHenckel Date: Sun, 2 Nov 2025 20:39:14 +0100 Subject: [PATCH] Keep track of pending Redis get calls in client side caching to avoid race with invalidations. #3481 --- .../core/support/caching/CacheAccessor.java | 16 ++++++- .../support/caching/ClientSideCaching.java | 2 + .../support/caching/MapCacheAccessor.java | 36 -------------- .../caching/PendingAwareCacheAccessor.java | 47 +++++++++++++++++++ 4 files changed, 63 insertions(+), 38 deletions(-) delete mode 100644 src/main/java/io/lettuce/core/support/caching/MapCacheAccessor.java create mode 100644 src/main/java/io/lettuce/core/support/caching/PendingAwareCacheAccessor.java diff --git a/src/main/java/io/lettuce/core/support/caching/CacheAccessor.java b/src/main/java/io/lettuce/core/support/caching/CacheAccessor.java index 0e9f789c51..7b5f955c9d 100644 --- a/src/main/java/io/lettuce/core/support/caching/CacheAccessor.java +++ b/src/main/java/io/lettuce/core/support/caching/CacheAccessor.java @@ -8,7 +8,7 @@ * * @param Key type. * @param Value type. - * @author Mark Paluch + * @author Mark Paluch, Ivar Henckel * @since 6.0 */ public interface CacheAccessor { @@ -22,7 +22,7 @@ public interface CacheAccessor { * @return a {@link CacheAccessor} backed by a {@link Map} implementation. */ static CacheAccessor forMap(Map map) { - return new MapCacheAccessor<>(map); + return new PendingAwareCacheAccessor<>(map); } /** @@ -59,4 +59,16 @@ static CacheAccessor forMap(Map map) { */ void evict(K key); + /** + * Mark a key as pending. + *

+ * By marking keys as pending, i.e. Redis get operation in progress, it's possible to block cache updates that would + * otherwise overwrite simultaneous invalidates. + * + * @param key the key to be marked as pending. + */ + default void setPending(K key) { + // Do nothing. + } + } diff --git a/src/main/java/io/lettuce/core/support/caching/ClientSideCaching.java b/src/main/java/io/lettuce/core/support/caching/ClientSideCaching.java index e512c88f7f..34faa3262d 100644 --- a/src/main/java/io/lettuce/core/support/caching/ClientSideCaching.java +++ b/src/main/java/io/lettuce/core/support/caching/ClientSideCaching.java @@ -124,6 +124,7 @@ public V get(K key) { V value = cacheAccessor.get(key); if (value == null) { + cacheAccessor.setPending(key); value = redisCache.get(key); if (value != null) { @@ -140,6 +141,7 @@ public V get(K key, Callable valueLoader) { V value = cacheAccessor.get(key); if (value == null) { + cacheAccessor.setPending(key); value = redisCache.get(key); if (value == null) { diff --git a/src/main/java/io/lettuce/core/support/caching/MapCacheAccessor.java b/src/main/java/io/lettuce/core/support/caching/MapCacheAccessor.java deleted file mode 100644 index ac9f21f109..0000000000 --- a/src/main/java/io/lettuce/core/support/caching/MapCacheAccessor.java +++ /dev/null @@ -1,36 +0,0 @@ -package io.lettuce.core.support.caching; - -import java.util.Map; - -/** - * {@link CacheAccessor} implementation for {@link Map}-based cache implementations. - * - * @param Key type. - * @param Value type. - * @author Mark Paluch - * @since 6.0 - */ -class MapCacheAccessor implements CacheAccessor { - - private final Map map; - - MapCacheAccessor(Map map) { - this.map = map; - } - - @Override - public V get(K key) { - return map.get(key); - } - - @Override - public void put(K key, V value) { - map.put(key, value); - } - - @Override - public void evict(K key) { - map.remove(key); - } - -} diff --git a/src/main/java/io/lettuce/core/support/caching/PendingAwareCacheAccessor.java b/src/main/java/io/lettuce/core/support/caching/PendingAwareCacheAccessor.java new file mode 100644 index 0000000000..0fd3796873 --- /dev/null +++ b/src/main/java/io/lettuce/core/support/caching/PendingAwareCacheAccessor.java @@ -0,0 +1,47 @@ +package io.lettuce.core.support.caching; + +import java.util.Map; + +/** + * {@link CacheAccessor} implementation for {@link Map}-based cache implementations. + * + * @param Key type. + * @param Value type. + * @author Mark Paluch, Ivar Henckel + * @since 6.0 + */ +class PendingAwareCacheAccessor implements CacheAccessor { + + private static final Object REDIS_IN_PROGRESS = new Object(); + + private final Map map; + + PendingAwareCacheAccessor(Map map) { + this.map = map; + } + + @Override + public V get(K key) { + V value = map.get(key); + if (value == REDIS_IN_PROGRESS) { + return null; + } + return value; + } + + @Override + public void put(K key, V value) { + map.replace(key, (V) REDIS_IN_PROGRESS, value); + } + + @Override + public void evict(K key) { + map.remove(key); + } + + @Override + public void setPending(K key) { + map.put(key, (V) REDIS_IN_PROGRESS); + } + +}