-
Notifications
You must be signed in to change notification settings - Fork 167
Description
Discussed in #417
Originally posted by HannaHromenko March 5, 2025
FusionCache: EagerExpirationTimestamp Becomes Null After Being Checked
Issue Description
We have been using FusionCache for a while, and from time to time, we encounter the following exception:
System.InvalidOperationException: Nullable object must have a value.
at ZiggyCreatures.Caching.Fusion.Internals.FusionCacheInternalUtils.ShouldEagerlyRefresh(IFusionCacheEntry entry)
in /_/src/ZiggyCreatures.FusionCache/Internals/FusionCacheInternalUtils.cs:line 124
From the error message, it is clear that the issue is related to EagerRefresh. Specifically, EagerExpirationTimestamp appears to be null at the time of access, even though there is a check beforehand to verify if it has a value.
Code Analysis
Here’s the FusionCache method where the exception is thrown:
public static bool ShouldEagerlyRefresh(this IFusionCacheEntry? entry)
{
if (entry?.Metadata is null)
return false;
if (entry.Metadata.EagerExpirationTimestamp.HasValue == false)
return false;
if (entry.Metadata.EagerExpirationTimestamp.Value >= DateTimeOffset.UtcNow.UtcTicks) // According to the error log it is thrown here because the value is null even thought we checked it for nullability before
return false;
if (entry.IsLogicallyExpired())
return false;
return true;
}
According to the error log, EagerExpirationTimestamp.HasValue initially returns true, but by the time the next line accesses entry.Metadata.EagerExpirationTimestamp.Value, it becomes null, causing the exception.
Observations
We use EagerRefresh in two different places. Both trigger the error:
1. First place where cache is used
private readonly FusionCacheEntryOptions _slidingEntryOptions = new FusionCacheEntryOptions()
.SetDuration(TimeSpan.FromSeconds(60))
.SetFailSafe(true)
.SetFactoryTimeouts(TimeSpan.FromMilliseconds(50))
.SetEagerRefresh(0.8f);
public async Task<Dictionary<string, Result>> GetCached(string cacheKey, Func<CancellationToken, Task<IEnumerable<Result>>> factory, CancellationToken ct)
{
return await cache.GetOrSetAsync<Dictionary<string, Result>>(cacheKey,
async (context, token) =>
{
return await factory(ct);
}, _slidingEntryOptions, token: ct);
}2. Second place where code is used
private readonly FusionCacheEntryOptions _slidingEntryOptions = new FusionCacheEntryOptions()
.SetDuration(TimeSpan.FromMinutes(5))
.SetFailSafe(true)
.SetFactoryTimeouts(TimeSpan.FromMilliseconds(100))
.SetEagerRefresh(0.8f);
public async Task<Result?> GetCached(VersionRulesCacheKey cacheKey, Func<string, CancellationToken, Task<Result?>> factory, CancellationToken ct)
{
return await cache.GetOrSetAsync<VersionRules?>(cacheKey, async (cancellationToken) =>
{
return await factory(cacheKey.VersionId, cancellationToken);
}, _slidingEntryOptions, token: ct);
}Questions
- What could cause
EagerExpirationTimestampto become null between the HasValue check and its access?
Any insights would be greatly appreciated! 🚀