Skip to content
Merged
Show file tree
Hide file tree
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
108 changes: 75 additions & 33 deletions src/Hl7.Fhir.Base/Specification/Source/CachedResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ public class CachedResolver : IResourceResolver, IAsyncResourceResolver
/// <summary>Default expiration time for cached entries.</summary>
public const int DEFAULT_CACHE_DURATION = 4 * 3600; // 4 hours

readonly Cache<Resource> _resourcesByUri;
readonly Cache<Resource> _resourcesByCanonical;
private readonly ResolverCache _resourcesByUri;
private readonly ResolverCache _resourcesByCanonical;

/// <summary>Creates a new artifact resolver that caches loaded resources in memory.</summary>
/// <param name="source">Resolver from which artifacts are initially resolved on a cache miss.</param>
Expand All @@ -46,8 +46,8 @@ public CachedResolver(ISyncOrAsyncResourceResolver source, int cacheDuration = D
AsyncResolver = source.AsAsync();
CacheDuration = cacheDuration;

_resourcesByUri = new Cache<Resource>(id => InternalResolveByUri(id), CacheDuration);
_resourcesByCanonical = new Cache<Resource>(id => InternalResolveByCanonicalUri(id), CacheDuration);
_resourcesByUri = new(InternalResolveByUri, CacheDuration);
_resourcesByCanonical = new(InternalResolveByCanonicalUri, CacheDuration);
}

/// <summary>
Expand All @@ -66,21 +66,21 @@ public CachedResolver(ISyncOrAsyncResourceResolver source, int cacheDuration = D
public int CacheDuration { get; }

/// <inheritdoc cref="ResolveByUriAsync(string)"/>
[Obsolete("CachedResolver now works best with asynchronous resolvers. Use ResolveByUriAsync() instead.")]
public Resource ResolveByUri(string url) => TaskHelper.Await(() => ResolveByUriAsync(url));
[Obsolete("CachedResolver now works best with asynchronous resolvers. Use TryResolveByUriAsync() instead.")]
public Resource ResolveByUri(string url) => TryResolveByUri(url).Value;

/// <summary>Retrieve the artifact with the specified url.</summary>
/// <param name="url">The url of the target artifact.</param>
/// <returns>A <see cref="Resource"/> instance, or <c>null</c> if unavailable.</returns>
/// <remarks>Return data from memory cache if available, otherwise load on demand from the internal artifact source.</remarks>
public async Task<Resource> ResolveByUriAsync(string url)
{
if (url == null) throw Error.ArgumentNull(nameof(url));
return await _resourcesByUri.Get(url, CachedResolverLoadingStrategy.LoadOnDemand).ConfigureAwait(false);
var result = await TryResolveByUriAsync(url).ConfigureAwait(false);
return result.Value;
}

/// <inheritdoc cref="ResolveByUriAsync(string, CachedResolverLoadingStrategy)"/>
[Obsolete("CachedResolver now works best with asynchronous resolvers. Use ResolveByUriAsync() instead.")]
[Obsolete("CachedResolver now works best with asynchronous resolvers. Use TryResolveByUriAsync() instead.")]
public Resource ResolveByUri(string url, CachedResolverLoadingStrategy strategy) =>
TaskHelper.Await(() => ResolveByUriAsync(url, strategy));

Expand All @@ -90,27 +90,58 @@ public Resource ResolveByUri(string url, CachedResolverLoadingStrategy strategy)
/// <returns>A <see cref="Resource"/> instance, or <c>null</c> if unavailable.</returns>
/// <remarks>Return data from memory cache if available, otherwise load on demand from the internal artifact source.</remarks>
public async Task<Resource> ResolveByUriAsync(string url, CachedResolverLoadingStrategy strategy)
{
var result = await TryResolveByUriAsync(url, strategy).ConfigureAwait(false);
return result.Value;
}

/// <summary>Retrieve the artifact with the specified url.</summary>
/// <param name="url">The url of the target artifact.</param>
/// <param name="strategy">Option flag to control the loading strategy.</param>
/// <returns>A <see cref="ResolverResult"/> instance, with either <see cref="ResolverResult.Value"/> or <see cref="ResolverResult.Error"/>.</returns>
/// <remarks>Return data from memory cache if available, otherwise load on demand from the internal artifact source.</remarks>
public async Task<ResolverResult> TryResolveByUriAsync(string url, CachedResolverLoadingStrategy strategy)
{
if (url == null) throw Error.ArgumentNull(nameof(url));
return await _resourcesByUri.Get(url, strategy).ConfigureAwait(false);
}

/// <inheritdoc cref="ResolveByCanonicalUriAsync(string)" />
[Obsolete("CachedResolver now works best with asynchronous resolvers. Use ResolveByCanonicalUriAsync() instead.")]
public Resource ResolveByCanonicalUri(string url) => TaskHelper.Await(() => ResolveByCanonicalUriAsync(url));
[Obsolete("CachedResolver now works best with asynchronous resolvers. Use TryResolveByCanonicalUriAsync() instead.")]
public Resource ResolveByCanonicalUri(string url) => TryResolveByCanonicalUri(url).Value;

/// <inheritdoc cref="TryResolveByUriAsync(string)" />
[Obsolete("CachedResolver now works best with asynchronous resolvers. Use TryResolveByUriAsync() instead.")]
public ResolverResult TryResolveByUri(string uri) => TaskHelper.Await(() => TryResolveByUriAsync(uri));

/// <inheritdoc cref="TryResolveByCanonicalUriAsync(string)" />
[Obsolete("CachedResolver now works best with asynchronous resolvers. Use TryResolveByCanonicalUriAsync() instead.")]
public ResolverResult TryResolveByCanonicalUri(string uri) => TaskHelper.Await(() => TryResolveByCanonicalUriAsync(uri));

/// <summary>Retrieve the conformance resource with the specified canonical url.</summary>
/// <param name="url">The canonical url of the target conformance resource.</param>
/// <returns>A conformance <see cref="Resource"/> instance, or <c>null</c> if unavailable.</returns>
/// <remarks>Return data from memory cache if available, otherwise load on demand from the internal artifact source.</remarks>
public async Task<Resource> ResolveByCanonicalUriAsync(string url)
{
if (url == null) throw Error.ArgumentNull(nameof(url));
return await _resourcesByCanonical.Get(url, CachedResolverLoadingStrategy.LoadOnDemand).ConfigureAwait(false);
var result = await TryResolveByCanonicalUriAsync(url).ConfigureAwait(false);
return result.Value;
}

/// <summary>Retrieve the artifact with the specified url.</summary>
/// <param name="uri">The url of the target artifact.</param>
/// <returns>A <see cref="ResolverResult"/> instance, with either <see cref="ResolverResult.Value"/> or <see cref="ResolverResult.Error"/>.</returns>
/// <remarks>Return data from memory cache if available, otherwise load on demand from the internal artifact source.</remarks>
public async Task<ResolverResult> TryResolveByUriAsync(string uri) => await TryResolveByUriAsync(uri, CachedResolverLoadingStrategy.LoadOnDemand).ConfigureAwait(false);

/// <summary>Retrieve the conformance resource with the specified canonical url.</summary>
/// <param name="uri">The canonical url of the target conformance resource.</param>
/// <returns>A <see cref="ResolverResult"/> instance, with either <see cref="ResolverResult.Value"/> or <see cref="ResolverResult.Error"/>.</returns>
/// <remarks>Return data from memory cache if available, otherwise load on demand from the internal artifact source.</remarks>
public async Task<ResolverResult> TryResolveByCanonicalUriAsync(string uri) => await TryResolveByCanonicalUriAsync(uri, CachedResolverLoadingStrategy.LoadOnDemand).ConfigureAwait(false);

/// <inheritdoc cref="ResolveByCanonicalUriAsync(string, CachedResolverLoadingStrategy)" />
[Obsolete("CachedResolver now works best with asynchronous resolvers. Use ResolveByCanonicalUriAsync() instead.")]
[Obsolete("CachedResolver now works best with asynchronous resolvers. Use TryResolveByCanonicalUriAsync() instead.")]
public Resource ResolveByCanonicalUri(string url, CachedResolverLoadingStrategy strategy) =>
TaskHelper.Await(() => ResolveByCanonicalUriAsync(url, strategy));

Expand All @@ -121,8 +152,19 @@ public Resource ResolveByCanonicalUri(string url, CachedResolverLoadingStrategy
/// <remarks>Return data from memory cache if available, otherwise load on demand from the internal artifact source.</remarks>
public async Task<Resource> ResolveByCanonicalUriAsync(string url, CachedResolverLoadingStrategy strategy)
{
if (url == null) throw Error.ArgumentNull(nameof(url));
return await _resourcesByCanonical.Get(url, strategy).ConfigureAwait(false);
var result = await TryResolveByCanonicalUriAsync(url, strategy).ConfigureAwait(false);
return result.Value;
}

/// <summary>Retrieve the conformance resource with the specified canonical url.</summary>
/// <param name="uri">The canonical url of the target conformance resource.</param>
/// <param name="strategy">Option flag to control the loading strategy.</param>
/// <returns>A <see cref="ResolverResult"/> instance, with either <see cref="ResolverResult.Value"/> or <see cref="ResolverResult.Error"/>.</returns>
/// <remarks>Return data from memory cache if available, otherwise load on demand from the internal artifact source.</remarks>
public async Task<ResolverResult> TryResolveByCanonicalUriAsync(string uri, CachedResolverLoadingStrategy strategy)
{
if (uri == null) throw Error.ArgumentNull(nameof(uri));
return await _resourcesByCanonical.Get(uri, strategy).ConfigureAwait(false);
}

/// <summary>Clear the cache entry for the artifact with the specified url, if it exists.</summary>
Expand Down Expand Up @@ -183,17 +225,17 @@ public LoadResourceEventArgs(string url, Resource resource) : base()
/// <summary>Called when an artifact is loaded into the cache.</summary>
protected virtual void OnLoad(string url, Resource resource) => Load?.Invoke(this, new LoadResourceEventArgs(url, resource));

internal async Task<Resource> InternalResolveByUri(string url)
internal async Task<ResolverResult> InternalResolveByUri(string url)
{
var resource = await AsyncResolver.ResolveByUriAsync(url).ConfigureAwait(false);
OnLoad(url, resource);
var resource = await AsyncResolver.TryResolveByUriAsync(url).ConfigureAwait(false);
OnLoad(url, resource.Value);
return resource;
}

internal async Task<Resource> InternalResolveByCanonicalUri(string url)
internal async Task<ResolverResult> InternalResolveByCanonicalUri(string url)
{
var resource = await AsyncResolver.ResolveByCanonicalUriAsync(url).ConfigureAwait(false);
OnLoad(url, resource);
var resource = await AsyncResolver.TryResolveByCanonicalUriAsync(url).ConfigureAwait(false);
OnLoad(url, resource.Value);
return resource;
}

Expand All @@ -203,32 +245,32 @@ internal async Task<Resource> InternalResolveByCanonicalUri(string url)
internal protected virtual string DebuggerDisplay
=> $"{GetType().Name} for {AsyncResolver.DebuggerDisplayString()}";

private class Cache<T>
private class ResolverCache
{
readonly Func<string, Task<T>> _onCacheMiss;
readonly Func<string, Task<ResolverResult>> _onCacheMiss;
readonly int _duration;

readonly Object _getLock = new Object();
readonly Dictionary<string, CacheEntry<T>> _cache = new Dictionary<string, CacheEntry<T>>();
readonly Dictionary<string, CacheEntry<ResolverResult>> _cache = new Dictionary<string, CacheEntry<ResolverResult>>();

public Cache(Func<string, Task<T>> onCacheMiss, int duration)
public ResolverCache(Func<string, Task<ResolverResult>> onCacheMiss, int duration)
{
_onCacheMiss = onCacheMiss;
_duration = duration;
}


public bool Contains(string identifier) =>
_cache.TryGetValue(identifier, out var entry) && !entry.IsExpired && entry.Data != null;
_cache.TryGetValue(identifier, out var entry) && !entry.IsExpired && entry.Data.Success;

public async Task<T> Get(string identifier, CachedResolverLoadingStrategy strategy)
public async Task<ResolverResult> Get(string identifier, CachedResolverLoadingStrategy strategy)
{
lock (_getLock)
{
// Check the cache
if (strategy != CachedResolverLoadingStrategy.LoadFromSource)
{
if (_cache.TryGetValue(identifier, out CacheEntry<T> entry))
if (_cache.TryGetValue(identifier, out CacheEntry<ResolverResult> entry))
{
// If we still have a fresh entry, return it
if (!entry.IsExpired)
Expand All @@ -246,22 +288,22 @@ public async Task<T> Get(string identifier, CachedResolverLoadingStrategy strate
if (strategy != CachedResolverLoadingStrategy.LoadFromCache)
{
// Otherwise, fetch it and cache it.
T newData = await _onCacheMiss(identifier).ConfigureAwait(false);
ResolverResult newData = await _onCacheMiss(identifier).ConfigureAwait(false);

lock (_getLock)
{
// finally double check whether some other thread has not created and added it by now,
// since we had to release the lock to run the async onCacheMiss.
if (strategy != CachedResolverLoadingStrategy.LoadFromSource &&
_cache.TryGetValue(identifier, out CacheEntry<T> existingEntry))
_cache.TryGetValue(identifier, out CacheEntry<ResolverResult> existingEntry))
return existingEntry.Data;
else
{
// Add new entry or update existing entry.
// Note that an entry is created, even if the newData is null.
// This ensures we don't keep trying to fetch the same url over and over again,
// even if the source cannot resolve it.
_cache[identifier] = new CacheEntry<T>(newData, identifier, DateTimeOffset.UtcNow.AddSeconds(_duration));
_cache[identifier] = new CacheEntry<ResolverResult>(newData, identifier, DateTimeOffset.UtcNow.AddSeconds(_duration));
return newData;
}
}
Expand Down
Loading