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:
Fernando Henrique Inocêncio Borba Ferreira 2024-02-09 17:06:53 -08:00 коммит произвёл GitHub
Родитель d23bf8fd07
Коммит 869dbabbe3
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
4 изменённых файлов: 396 добавлений и 12 удалений

Просмотреть файл

@ -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;
}