Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions src/DynamicData/Cache/Internal/ExpireAfter.ForSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -161,11 +161,18 @@ private void OnEditingSource(ISourceUpdater<TObject, TKey> updater)
{
_expirationDueTimesByKey.Remove(proposedExpiration.Key);

_removedItemsBuffer.Add(new(
key: proposedExpiration.Key,
value: updater.Lookup(proposedExpiration.Key).Value));

updater.RemoveKey(proposedExpiration.Key);
// The item may have been removed or updated by another thread between when
// this expiration was scheduled and when it fired. Check that the item is
// still present and still has an expiration before removing it.
var lookup = updater.Lookup(proposedExpiration.Key);
if (lookup.HasValue && _timeSelector.Invoke(lookup.Value) is not null)
{
_removedItemsBuffer.Add(new(
key: proposedExpiration.Key,
value: lookup.Value));

updater.RemoveKey(proposedExpiration.Key);
}
Comment on lines +168 to +175
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

_timeSelector is user-provided and can throw. Calling _timeSelector.Invoke(lookup.Value) inside the scheduled expiration-management path (OnEditingSource via ManageExpirations) is currently unguarded (no try/catch here or around _source.Edit(_onEditingSource)), so an exception would escape the scheduler callback instead of being propagated via _observer.OnError (as OnSourceNext does). Consider either avoiding timeSelector evaluation here (rely on internal expiration state), or wrapping the invocation/removal block to cancel further scheduling and forward the error to _observer.OnError consistently.

Suggested change
if (lookup.HasValue && _timeSelector.Invoke(lookup.Value) is not null)
{
_removedItemsBuffer.Add(new(
key: proposedExpiration.Key,
value: lookup.Value));
updater.RemoveKey(proposedExpiration.Key);
}
try
{
if (lookup.HasValue && _timeSelector.Invoke(lookup.Value) is not null)
{
_removedItemsBuffer.Add(new(
key: proposedExpiration.Key,
value: lookup.Value));
updater.RemoveKey(proposedExpiration.Key);
}
}
catch (Exception error)
{
_observer.OnError(error);
return;
}

Copilot uses AI. Check for mistakes.
}
}
_proposedExpirationsQueue.RemoveRange(0, proposedExpirationIndex);
Expand Down Expand Up @@ -273,7 +280,7 @@ private void OnSourceNext(IChangeSet<TObject, TKey> changes)
{
if (_timeSelector.Invoke(change.Current) is { } expireAfter)
{
haveExpirationsChanged = TrySetExpiration(
haveExpirationsChanged |= TrySetExpiration(
key: change.Key,
dueTime: now + expireAfter);
}
Expand Down
Loading