FHIR Model and temporarily cached data (#3705)
* Change SqlServerFhirModel to use FhirMemoryCache * Improvements in FhirMemoryCache. * Added support to multi case cache. * Test improvements. * New tests. Removing lock to be aligned with the MemoryCache.
This commit is contained in:
Родитель
d23bf8fd07
Коммит
869dbabbe3
|
@ -0,0 +1,194 @@
|
|||
// -------------------------------------------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Health.Fhir.Core.Features.Storage;
|
||||
using Microsoft.Health.Fhir.Tests.Common;
|
||||
using Microsoft.Health.Test.Utilities;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Health.Fhir.Core.UnitTests.Features.Storage
|
||||
{
|
||||
[Trait(Traits.OwningTeam, OwningTeam.Fhir)]
|
||||
[Trait(Traits.Category, Categories.Security)]
|
||||
public sealed class FhirMemoryCacheTests
|
||||
{
|
||||
private readonly ILogger _logger = Substitute.For<ILogger>();
|
||||
|
||||
private const string DefaultKey = "key";
|
||||
private const string DefaultValue = "value";
|
||||
|
||||
[Theory]
|
||||
[InlineData(01, 01 * 1024 * 1024)]
|
||||
[InlineData(14, 14 * 1024 * 1024)]
|
||||
[InlineData(55, 55 * 1024 * 1024)]
|
||||
public void GivenAnEmptyCache_CheckTheCacheMemoryLimit(int limitSizeInMegabytes, long expectedLimitSizeInBytes)
|
||||
{
|
||||
var cache = new FhirMemoryCache<string>(Guid.NewGuid().ToString(), limitSizeInMegabytes, TimeSpan.FromMinutes(1), _logger);
|
||||
|
||||
Assert.Equal(expectedLimitSizeInBytes, cache.CacheMemoryLimit);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GivenACache_RaiseErrorsIfParametersAreInvalid()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(
|
||||
() => new FhirMemoryCache<string>(
|
||||
null,
|
||||
limitSizeInMegabytes: 0,
|
||||
TimeSpan.FromMinutes(1),
|
||||
_logger));
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(
|
||||
() => new FhirMemoryCache<string>(
|
||||
Guid.NewGuid().ToString(),
|
||||
limitSizeInMegabytes: 0,
|
||||
TimeSpan.FromMinutes(1),
|
||||
_logger));
|
||||
|
||||
Assert.Throws<ArgumentNullException>(
|
||||
() => new FhirMemoryCache<string>(
|
||||
Guid.NewGuid().ToString(),
|
||||
limitSizeInMegabytes: 1,
|
||||
TimeSpan.FromMinutes(1),
|
||||
null));
|
||||
|
||||
var cache = CreateRegularMemoryCache<string>();
|
||||
|
||||
Assert.Throws<ArgumentNullException>(() => cache.GetOrAdd(null, DefaultValue));
|
||||
Assert.Throws<ArgumentNullException>(() => cache.TryAdd(null, DefaultValue));
|
||||
Assert.Throws<ArgumentException>(() => cache.GetOrAdd(string.Empty, DefaultValue));
|
||||
Assert.Throws<ArgumentException>(() => cache.TryAdd(string.Empty, DefaultValue));
|
||||
Assert.Throws<ArgumentNullException>(() => cache.GetOrAdd(DefaultKey, null));
|
||||
Assert.Throws<ArgumentNullException>(() => cache.TryAdd(DefaultKey, null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GivenAnEmptyCache_WhenAddingValue_ThenValueShouldBeAdded()
|
||||
{
|
||||
var cache = CreateRegularMemoryCache<string>();
|
||||
|
||||
var result1 = cache.GetOrAdd(DefaultKey, DefaultValue);
|
||||
Assert.Equal(DefaultValue, result1);
|
||||
|
||||
const string anotherValue = "Another Value";
|
||||
var result2 = cache.GetOrAdd(DefaultKey, anotherValue);
|
||||
Assert.NotEqual(anotherValue, result2);
|
||||
Assert.Equal(DefaultValue, result1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GivenAnEmptyCache_WhenAddingValue_ThenValueShouldBeAddedAndCanBeRetrieved()
|
||||
{
|
||||
var cache = CreateRegularMemoryCache<string>();
|
||||
|
||||
cache.GetOrAdd(DefaultKey, DefaultValue);
|
||||
|
||||
Assert.True(cache.TryGet(DefaultKey, out var result));
|
||||
Assert.Equal(DefaultValue, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GivenAnEmptyCache_WhenAddingValueIfIgnoreCaseEnabled_ThenMultipleSimilarKeysShouldWorkAsExpected()
|
||||
{
|
||||
var cache = new FhirMemoryCache<string>(Guid.NewGuid().ToString(), limitSizeInMegabytes: 1, TimeSpan.FromMinutes(1), _logger, ignoreCase: true);
|
||||
|
||||
cache.GetOrAdd(DefaultKey, DefaultValue);
|
||||
|
||||
Assert.True(cache.TryGet(DefaultKey, out var result));
|
||||
Assert.Equal(DefaultValue, result);
|
||||
|
||||
Assert.True(cache.TryGet(DefaultKey.ToUpper(), out result));
|
||||
Assert.Equal(DefaultValue, result);
|
||||
|
||||
Assert.True(cache.TryGet("Key", out result));
|
||||
Assert.Equal(DefaultValue, result);
|
||||
|
||||
Assert.True(cache.TryGet("kEy", out result));
|
||||
Assert.Equal(DefaultValue, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GivenAnEmptyCache_WhenGettingAnItemThatDoNotExist_ThenReturnFalse()
|
||||
{
|
||||
var cache = CreateRegularMemoryCache<string>();
|
||||
|
||||
Assert.False(cache.TryGet(DefaultKey, out var result));
|
||||
Assert.Equal(default, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GivenAnEmptyCache_WhenAddingValueIfIgnoreCaseDisabled_ThenMultipleSimilarKeysShouldWorkAsExpected()
|
||||
{
|
||||
var cache = CreateRegularMemoryCache<string>();
|
||||
|
||||
cache.GetOrAdd(DefaultKey, DefaultValue);
|
||||
|
||||
Assert.True(cache.TryGet("key", out var result));
|
||||
Assert.Equal(DefaultValue, result);
|
||||
|
||||
Assert.False(cache.TryGet("KEY", out result));
|
||||
|
||||
Assert.False(cache.TryGet("Key", out result));
|
||||
|
||||
Assert.False(cache.TryGet("kEy", out result));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GivenAnEmptyCache_WhenAddingValue_ThenValueShouldBeAddedAndCanBeRetrievedUsingTryGetValue()
|
||||
{
|
||||
var cache = CreateRegularMemoryCache<string>();
|
||||
|
||||
cache.GetOrAdd(DefaultKey, DefaultValue);
|
||||
|
||||
Assert.True(cache.TryGet(DefaultKey, out var result));
|
||||
Assert.Equal(DefaultValue, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GivenAnEmptyCache_WhenAddingValue_ThenValueShouldBeAddedAndCanBeRetrievedUsingTryGetValueWithOut()
|
||||
{
|
||||
var cache = CreateRegularMemoryCache<string>();
|
||||
|
||||
cache.GetOrAdd(DefaultKey, DefaultValue);
|
||||
|
||||
Assert.True(cache.TryGet(DefaultKey, out var result));
|
||||
Assert.Equal(DefaultValue, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GivenAnEmptyCache_WhenAddingValue_ThenValueShouldBeAddedAndCanBeRetrievedUsingTryGetValueWithOutAndValue()
|
||||
{
|
||||
var cache = CreateRegularMemoryCache<string>();
|
||||
|
||||
cache.GetOrAdd(DefaultKey, DefaultValue);
|
||||
|
||||
Assert.True(cache.TryGet(DefaultKey, out var result));
|
||||
Assert.Equal(DefaultValue, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GivenAnEmptyCache_WhenRunningOperations_ThenItemsShouldBeRespected()
|
||||
{
|
||||
var cache = CreateRegularMemoryCache<long>();
|
||||
|
||||
cache.GetOrAdd(DefaultKey, 2112);
|
||||
Assert.True(cache.TryGet(DefaultKey, out var result));
|
||||
Assert.Equal(2112, result);
|
||||
|
||||
Assert.True(cache.Remove(DefaultKey));
|
||||
|
||||
Assert.False(cache.TryGet(DefaultKey, out result));
|
||||
}
|
||||
|
||||
private IMemoryCache<T> CreateRegularMemoryCache<T>()
|
||||
{
|
||||
return new FhirMemoryCache<T>(Guid.NewGuid().ToString(), limitSizeInMegabytes: 1, TimeSpan.FromMinutes(1), _logger);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,166 @@
|
|||
// -------------------------------------------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Runtime.Caching;
|
||||
using EnsureThat;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.Health.Fhir.Core.Features.Storage
|
||||
{
|
||||
public sealed class FhirMemoryCache<T> : IMemoryCache<T>
|
||||
{
|
||||
private const int DefaultLimitSizeInMegabytes = 50;
|
||||
private const int DefaultExpirationTimeInMinutes = 24 * 60;
|
||||
|
||||
private readonly string _cacheName;
|
||||
private readonly ILogger _logger;
|
||||
private readonly ObjectCache _cache;
|
||||
private readonly TimeSpan _expirationTime;
|
||||
private readonly bool _ignoreCase;
|
||||
|
||||
public FhirMemoryCache(string name, ILogger logger, bool ignoreCase = false)
|
||||
: this(
|
||||
name,
|
||||
limitSizeInMegabytes: DefaultLimitSizeInMegabytes,
|
||||
expirationTime: TimeSpan.FromMinutes(DefaultExpirationTimeInMinutes),
|
||||
logger)
|
||||
{
|
||||
}
|
||||
|
||||
public FhirMemoryCache(string name, int limitSizeInMegabytes, TimeSpan expirationTime, ILogger logger, bool ignoreCase = false)
|
||||
{
|
||||
EnsureArg.IsNotNull(name, nameof(name));
|
||||
EnsureArg.IsGt(limitSizeInMegabytes, 0, nameof(name));
|
||||
EnsureArg.IsNotNull(logger, nameof(logger));
|
||||
|
||||
_cacheName = name;
|
||||
_cache = new MemoryCache(
|
||||
_cacheName,
|
||||
new NameValueCollection()
|
||||
{
|
||||
{ "CacheMemoryLimitMegabytes", limitSizeInMegabytes.ToString() },
|
||||
});
|
||||
_expirationTime = expirationTime;
|
||||
_logger = logger;
|
||||
_ignoreCase = ignoreCase;
|
||||
}
|
||||
|
||||
public long CacheMemoryLimit => ((MemoryCache)_cache).CacheMemoryLimit;
|
||||
|
||||
/// <summary>
|
||||
/// Get or add the value to cache.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of the value in cache</typeparam>
|
||||
/// <param name="key">Key</param>
|
||||
/// <param name="value">Value</param>
|
||||
/// <returns>Value in cache</returns>
|
||||
public T GetOrAdd(string key, T value)
|
||||
{
|
||||
EnsureArg.IsNotNullOrWhiteSpace(key, nameof(key));
|
||||
if (value == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
}
|
||||
|
||||
key = FormatKey(key);
|
||||
|
||||
CacheItem newCacheItem = new CacheItem(key, value);
|
||||
|
||||
CacheItem cachedItem = _cache.AddOrGetExisting(
|
||||
newCacheItem,
|
||||
GetDefaultCacheItemPolicy());
|
||||
|
||||
if (cachedItem.Value == null)
|
||||
{
|
||||
// If the item cache item is null, then the item was added to the cache.
|
||||
return (T)newCacheItem.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
return (T)cachedItem.Value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add the value to cache if it does not exist.
|
||||
/// </summary>
|
||||
/// <param name="key">Key</param>
|
||||
/// <param name="value">Value</param>
|
||||
/// <returns>Returns true if the item was added to the cache, returns false if there is an item with the same key in cache.</returns>
|
||||
public bool TryAdd(string key, T value)
|
||||
{
|
||||
EnsureArg.IsNotNullOrWhiteSpace(key, nameof(key));
|
||||
if (value == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
}
|
||||
|
||||
key = FormatKey(key);
|
||||
|
||||
return _cache.Add(key, value, GetDefaultCacheItemPolicy());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get an item from the cache.
|
||||
/// </summary>
|
||||
/// <param name="key">Key</param>
|
||||
/// <returns>Value</returns>
|
||||
public T Get(string key)
|
||||
{
|
||||
key = FormatKey(key);
|
||||
|
||||
return (T)_cache[key];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to retrieve an item from cache, if it does not exist then returns the <see cref="default"/> for that generic type.
|
||||
/// </summary>
|
||||
/// <param name="key">Key</param>
|
||||
/// <param name="value">Value</param>
|
||||
/// <returns>True if the value exists in cache</returns>
|
||||
public bool TryGet(string key, out T value)
|
||||
{
|
||||
key = FormatKey(key);
|
||||
|
||||
CacheItem cachedItem = _cache.GetCacheItem(key);
|
||||
|
||||
if (cachedItem != null)
|
||||
{
|
||||
value = (T)cachedItem.Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
_logger.LogTrace("Item does not exist in '{CacheName}' cache. Returning default value.", _cacheName);
|
||||
value = default;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removed the item indexed by the key.
|
||||
/// </summary>
|
||||
/// <param name="key">Key of the item to be removed from cache.</param>
|
||||
/// <returns>Returns false if the items does not exist in cache.</returns>
|
||||
public bool Remove(string key)
|
||||
{
|
||||
key = FormatKey(key);
|
||||
|
||||
object objectInCache = _cache.Remove(key);
|
||||
|
||||
return objectInCache != null;
|
||||
}
|
||||
|
||||
private string FormatKey(string key) => _ignoreCase ? key.ToLowerInvariant() : key;
|
||||
|
||||
private CacheItemPolicy GetDefaultCacheItemPolicy() => new CacheItemPolicy()
|
||||
{
|
||||
Priority = CacheItemPriority.Default,
|
||||
SlidingExpiration = _expirationTime,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
// -------------------------------------------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.Health.Fhir.Core.Features.Storage
|
||||
{
|
||||
public interface IMemoryCache<T>
|
||||
{
|
||||
long CacheMemoryLimit { get; }
|
||||
|
||||
T GetOrAdd(string key, T value);
|
||||
|
||||
bool TryAdd(string key, T value);
|
||||
|
||||
T Get(string key);
|
||||
|
||||
bool TryGet(string key, out T value);
|
||||
|
||||
bool Remove(string key);
|
||||
}
|
||||
}
|
|
@ -21,6 +21,7 @@ using Microsoft.Health.Fhir.Core.Configs;
|
|||
using Microsoft.Health.Fhir.Core.Features.Definition;
|
||||
using Microsoft.Health.Fhir.Core.Features.Operations;
|
||||
using Microsoft.Health.Fhir.Core.Features.Search.Registry;
|
||||
using Microsoft.Health.Fhir.Core.Features.Storage;
|
||||
using Microsoft.Health.Fhir.Core.Messages.Storage;
|
||||
using Microsoft.Health.Fhir.Core.Models;
|
||||
using Microsoft.Health.Fhir.SqlServer.Features.Schema;
|
||||
|
@ -30,6 +31,7 @@ using Microsoft.Health.SqlServer.Features.Client;
|
|||
using Microsoft.Health.SqlServer.Features.Schema;
|
||||
using Microsoft.Health.SqlServer.Features.Schema.Model;
|
||||
using Microsoft.Health.SqlServer.Features.Storage;
|
||||
using Namotion.Reflection;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Microsoft.Health.Fhir.SqlServer.Features.Storage
|
||||
|
@ -52,8 +54,8 @@ namespace Microsoft.Health.Fhir.SqlServer.Features.Storage
|
|||
private Dictionary<string, short> _resourceTypeToId;
|
||||
private Dictionary<short, string> _resourceTypeIdToTypeName;
|
||||
private Dictionary<Uri, short> _searchParamUriToId;
|
||||
private ConcurrentDictionary<string, int> _systemToId;
|
||||
private ConcurrentDictionary<string, int> _quantityCodeToId;
|
||||
private FhirMemoryCache<int> _systemToId;
|
||||
private FhirMemoryCache<int> _quantityCodeToId;
|
||||
private Dictionary<string, byte> _claimNameToId;
|
||||
private Dictionary<string, byte> _compartmentTypeToId;
|
||||
private int _highestInitializedVersion;
|
||||
|
@ -153,7 +155,7 @@ namespace Microsoft.Health.Fhir.SqlServer.Features.Storage
|
|||
public bool TryGetSystemId(string system, out int systemId)
|
||||
{
|
||||
ThrowIfNotInitialized();
|
||||
return _systemToId.TryGetValue(system, out systemId);
|
||||
return _systemToId.TryGet(system, out systemId);
|
||||
}
|
||||
|
||||
public int GetSystemId(string system)
|
||||
|
@ -175,7 +177,7 @@ namespace Microsoft.Health.Fhir.SqlServer.Features.Storage
|
|||
public bool TryGetQuantityCodeId(string code, out int quantityCodeId)
|
||||
{
|
||||
ThrowIfNotInitialized();
|
||||
return _quantityCodeToId.TryGetValue(code, out quantityCodeId);
|
||||
return _quantityCodeToId.TryGet(code, out quantityCodeId);
|
||||
}
|
||||
|
||||
public async Task EnsureInitialized()
|
||||
|
@ -279,8 +281,6 @@ namespace Microsoft.Health.Fhir.SqlServer.Features.Storage
|
|||
var resourceTypeToId = new Dictionary<string, short>(StringComparer.Ordinal);
|
||||
var resourceTypeIdToTypeName = new Dictionary<short, string>();
|
||||
var searchParamUriToId = new Dictionary<Uri, short>();
|
||||
var systemToId = new ConcurrentDictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
var quantityCodeToId = new ConcurrentDictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
var claimNameToId = new Dictionary<string, byte>(StringComparer.Ordinal);
|
||||
var compartmentTypeToId = new Dictionary<string, byte>();
|
||||
|
||||
|
@ -335,26 +335,26 @@ namespace Microsoft.Health.Fhir.SqlServer.Features.Storage
|
|||
// result set 5
|
||||
await reader.NextResultAsync(cancellationToken);
|
||||
|
||||
_systemToId = new FhirMemoryCache<int>("systemToId", _logger, ignoreCase: true);
|
||||
while (await reader.ReadAsync(cancellationToken))
|
||||
{
|
||||
var (value, systemId) = reader.ReadRow(VLatest.System.Value, VLatest.System.SystemId);
|
||||
systemToId.TryAdd(value, systemId);
|
||||
_systemToId.TryAdd(value, systemId);
|
||||
}
|
||||
|
||||
// result set 6
|
||||
await reader.NextResultAsync(cancellationToken);
|
||||
|
||||
_quantityCodeToId = new FhirMemoryCache<int>("quantityCodeToId", _logger, ignoreCase: true);
|
||||
while (await reader.ReadAsync(cancellationToken))
|
||||
{
|
||||
(string value, int quantityCodeId) = reader.ReadRow(VLatest.QuantityCode.Value, VLatest.QuantityCode.QuantityCodeId);
|
||||
quantityCodeToId.TryAdd(value, quantityCodeId);
|
||||
_quantityCodeToId.TryAdd(value, quantityCodeId);
|
||||
}
|
||||
|
||||
_resourceTypeToId = resourceTypeToId;
|
||||
_resourceTypeIdToTypeName = resourceTypeIdToTypeName;
|
||||
_searchParamUriToId = searchParamUriToId;
|
||||
_systemToId = systemToId;
|
||||
_quantityCodeToId = quantityCodeToId;
|
||||
_claimNameToId = claimNameToId;
|
||||
_compartmentTypeToId = compartmentTypeToId;
|
||||
_resourceTypeIdRange = (lowestResourceTypeId, highestResourceTypeId);
|
||||
|
@ -422,9 +422,9 @@ namespace Microsoft.Health.Fhir.SqlServer.Features.Storage
|
|||
}
|
||||
}
|
||||
|
||||
private int GetStringId(ConcurrentDictionary<string, int> cache, string stringValue, Table table, Column<int> idColumn, Column<string> stringColumn)
|
||||
private int GetStringId(FhirMemoryCache<int> cache, string stringValue, Table table, Column<int> idColumn, Column<string> stringColumn)
|
||||
{
|
||||
if (cache.TryGetValue(stringValue, out int id))
|
||||
if (cache.TryGet(stringValue, out int id))
|
||||
{
|
||||
return id;
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче