// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
namespace Microsoft.Agents.AI.Hosting.OpenAI;
///
/// Extension methods for that provide atomic operations.
///
///
/// The standard GetOrCreate method has a race condition where multiple threads can simultaneously
/// detect that a key doesn't exist and create different instances, with only one being cached.
/// See: https://github.com/dotnet/runtime/issues/36499
///
internal static class MemoryCacheExtensions
{
private static readonly ConcurrentDictionary<(IMemoryCache, object), SemaphoreSlim> s_semaphores = new();
///
/// Atomically gets the value associated with this key if it exists, or generates a new entry
/// using the provided key and a value from the given factory if the key is not found.
///
/// The type of the object to get.
/// The instance this method extends.
/// The key of the entry to look for or create.
/// The factory that creates the value associated with this key if the key does not exist in the cache.
/// The cancellation token.
/// A tuple containing the value and a flag indicating whether it was created (true) or retrieved from cache (false).
public static async Task GetOrCreateAtomicAsync(
this IMemoryCache memoryCache,
object key,
Func factory,
CancellationToken cancellationToken = default)
{
// Fast path: check if the value already exists
if (memoryCache.TryGetValue(key, out object? value))
{
Debug.Assert(value is not null);
return (T)value;
}
// Get or create a semaphore for this cache key
bool isOwner = false;
var semaphoreKey = (memoryCache, key);
if (!s_semaphores.TryGetValue(semaphoreKey, out SemaphoreSlim? semaphore))
{
SemaphoreSlim? createdSemaphore = null;
semaphore = s_semaphores.GetOrAdd(semaphoreKey, _ => createdSemaphore = new SemaphoreSlim(1));
// If we created the semaphore that made it into the dictionary, we're the owner
if (ReferenceEquals(createdSemaphore, semaphore))
{
isOwner = true;
}
else
{
// Our semaphore wasn't the one stored, so dispose it
createdSemaphore?.Dispose();
}
}
await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
// Double-check: another thread might have created the value while we were waiting
if (!memoryCache.TryGetValue(key, out value))
{
ICacheEntry entry = memoryCache.CreateEntry(key);
entry.SetValue(value = factory(entry));
entry.Dispose();
Debug.Assert(value is not null);
return (T)value;
}
Debug.Assert(value is not null);
return (T)value;
}
finally
{
// If we were the owner of the semaphore, remove it from the dictionary
// This prevents memory leaks from accumulating semaphores for evicted cache entries
if (isOwner)
{
s_semaphores.TryRemove(semaphoreKey, out _);
}
semaphore.Release();
}
}
}