Skip to content

Fix RedissonCache.retrieve(key, valueLoader) returning the internal NullValue sentinel instead of null - #7327

Merged
mrniko merged 2 commits into
redisson:masterfrom
stlahxm:fix/cache-retrieve-nullvalue
Aug 27, 2026
Merged

mrniko merged 2 commits into
redisson:masterfrom
stlahxm:fix/cache-retrieve-nullvalue

Conversation

@stlahxm

@stlahxm stlahxm commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Closes #7326

RedissonCache.retrieve(Object key, Supplier<CompletableFuture<T>> valueLoader) unwraps the negative-cache sentinel (NullValue) back to null on the outer lookup, but not on the inner re-check that runs after acquiring the per-key lock. Once a null value is cached for a key, every call after the first one returns the raw internal NullValue object instead of null.

Changes

  • RedissonCache.java: apply fromStoreValue(r) to the re-checked value inside retrieve(Object, Supplier)'s lock branch, matching what retrieve(Object) already does and what the analogous branch in get(Object, Callable) already does.
  • Added RedissonCacheRetrieveNullValueTest, covering the exact reported scenario: cache a null via retrieve(), call retrieve() again for the same key, assert the loader does not re-run and the result is null rather than the sentinel.

Testing

Ran the new test against a real Redis instance (Docker redis:latest, via the module's RedisDockerTest base):

Before the fix:
org.opentest4j.AssertionFailedError: BUG: retrieve() returned org.redisson.spring.cache.NullValue@16da1abc instead of null for an already-cached negative entry
Tests run: 1, Failures: 1, Errors: 0, Skipped: 0

After the fix:
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

Confirmed the failure was real (not a test bug) by running the test with the fix reverted first (git stash on the one-line change, test kept in place), seeing the exact failure above, then restoring the fix and confirming it passes.

…ullValue sentinel instead of null

retrieve(Object, Supplier) unwraps the negative-cache sentinel on the
outer lookup, but not on the inner re-check that runs after acquiring
the per-key lock. Once a null value is cached for a key, every call
after the first one returned the raw internal NullValue object
instead of null.

Applies fromStoreValue(r) to the re-checked value, matching what
retrieve(Object) and get(Object, Callable) already do.

Fixes redisson#7326

Signed-off-by: 심현민 <[email protected]>
* re-check under the per-key lock reads the raw stored value without calling
* fromStoreValue().
*/
public class RedissonCacheRetrieveNullValueTest extends RedisDockerTest {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add test to RedissonSpringCacheTest class

return CompletableFuture.completedFuture(null);
}).get();
assertNull(first, "first call should yield null");
assertEquals(1, loaderCalls.get());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use AssertJ

@mrniko mrniko added this to the 4.8.0 milestone Aug 27, 2026
@mrniko mrniko added the bug label Aug 27, 2026
…eTest

Address review feedback on redisson#7327: consolidate the standalone
RedissonCacheRetrieveNullValueTest into the existing
RedissonSpringCacheTest class instead of adding a new file, and
switch assertions from JUnit to AssertJ to match the rest of the
class.

Signed-off-by: 심현민 <[email protected]>
@mrniko
mrniko merged commit dbe4d6b into redisson:master Aug 27, 2026
4 checks passed
@mrniko

mrniko commented Aug 27, 2026

Copy link
Copy Markdown
Member

Thanks for contribution

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Development

Successfully merging this pull request may close these issues.

RedissonCache.retrieve(key, valueLoader) returns the internal NullValue sentinel instead of null for an already-cached negative entry

2 participants