-
Notifications
You must be signed in to change notification settings - Fork 0
Open
Description
Summary
A race condition in GetItemAsyncCore can cause it to return cache items with empty or incorrect dependencies when dependencies are enabled and concurrent writes occur.
Root Cause
GetItemAsyncCore performs multiple non-atomic reads:
- Read version
- Read value
- Read forward dependencies
During this multi-step read, a concurrent SetItem or RemoveItem can modify or delete the item, causing inconsistency between the value read in step 2 and the forward dependencies read in step 3.
Symptoms
Under high load with concurrent read/write operations:
GetItemreturns items with emptyDependenciesarray when they should have dependencies- The serialized payload contains the expected dependencies, but the forward dependencies list was deleted/modified concurrently
- Error: "Corrupted dependencies - different number of expected and actual dependencies"
Fix
Add consistency verification to GetItemAsyncCore when includeDependencies is true:
- After reading forward dependencies, verify the value still exists
- Verify the version hasn't changed since we started reading
- If version changed, retry the entire read using
TransactionRetryPolicy - If retry policy exhausted, return null
protected override async ValueTask<CacheItem?> GetItemAsyncCore( string key, bool includeDependencies, CancellationToken cancellationToken )
{
var shortKey = GetShortKeyFromUserKey( key );
var versionKey = this.KeyBuilder.GetVersionKey( shortKey );
object? retryState = null;
for ( var attempt = 0;
await this.Configuration.TransactionRetryPolicy.TryAsync( OperationKind.GetItem, attempt, null, ref retryState, cancellationToken );
attempt++ )
{
var version = await this.GetVersionOrNullAsync( shortKey );
if ( version == null ) return null;
var keyAndVersion = new ShortKeyAndVersion( shortKey, version );
var valueKey = this.KeyBuilder.GetVersionedValueKey( keyAndVersion );
var serializedValue = await this.Database.StringGetAsync( valueKey, ... );
if ( !serializedValue.HasValue ) return null;
ImmutableArray<string> dependencies = default;
if ( includeDependencies )
{
var forwardDeps = await this.GetForwardDependenciesAsync( keyAndVersion );
// Verify consistency after multi-step read
if ( !await this.Database.KeyExistsAsync( valueKey, ... ) )
{
return null; // Value deleted
}
var currentVersion = await this.GetVersionOrNullAsync( shortKey );
if ( currentVersion != version )
{
continue; // Version changed - retry
}
// ... convert forwardDeps to dependencies
}
return this.ItemSerializer.Deserialize( serializedValue, dependencies ).Item;
}
return null; // Retry policy exhausted
}Affected Code
Metalama.Patterns.Caching.Backends.Redis/DependenciesRedisCachingBackend.cs-GetItemAsyncCoremethod
Filed by Claude
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels