-
Notifications
You must be signed in to change notification settings - Fork 523
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
Showing
4 changed files
with
396 additions
and
12 deletions.
There are no files selected for viewing
194 changes: 194 additions & 0 deletions
194
src/Microsoft.Health.Fhir.Core.UnitTests/Features/Storage/FhirMemoryCacheTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} | ||
} |
166 changes: 166 additions & 0 deletions
166
src/Microsoft.Health.Fhir.Core/Features/Storage/FhirMemoryCache.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
}; | ||
} | ||
} |
24 changes: 24 additions & 0 deletions
24
src/Microsoft.Health.Fhir.Core/Features/Storage/IMemoryCache.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} |
Oops, something went wrong.