-
Notifications
You must be signed in to change notification settings - Fork 560
FHIR Model and temporarily cached data #3705
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 4 commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
3cd9e99
Change SqlServerFhirModel to use FhirMemoryCache
fhibf 0b13202
Improvements in FhirMemoryCache.
fhibf c1d5e6a
Added support to multi case cache.
fhibf 81e2fcf
Test improvements.
fhibf 83e96cf
New tests. Removing lock to be alligned with the MemoryCache.
fhibf File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
191 changes: 191 additions & 0 deletions
191
src/Microsoft.Health.Fhir.Core.UnitTests/Features/Storage/FhirMemoryCacheTests.cs
This file contains hidden or 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,191 @@ | ||
// ------------------------------------------------------------------------------------------------- | ||
// 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)); | ||
} | ||
|
||
[Fact] | ||
public void GivenAnEmptyCache_WhenAddingValue_ThenValueShouldBeAdded() | ||
{ | ||
var cache = CreateRegularMemoryCache<string>(); | ||
|
||
var result = cache.GetOrAdd(DefaultKey, DefaultValue); | ||
|
||
Assert.Equal(DefaultValue, result); | ||
} | ||
|
||
[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_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("key", 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("key", 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)); | ||
} | ||
|
||
[Fact] | ||
public void GivenAnEmptyCache_WhenAddingARange_AllValuesShouldBeIngested() | ||
{ | ||
int anchor = 'a'; | ||
var originalValues = new Dictionary<string, int>(); | ||
for (int i = 0; i < 20; i++) | ||
{ | ||
originalValues.Add(((char)(anchor + i)).ToString(), i); | ||
} | ||
|
||
var cache = CreateRegularMemoryCache<int>(); | ||
cache.AddRange(originalValues); | ||
|
||
foreach (var item in originalValues) | ||
{ | ||
Assert.Equal(item.Value, cache.Get(item.Key)); | ||
} | ||
} | ||
|
||
private IMemoryCache<T> CreateRegularMemoryCache<T>() | ||
{ | ||
return new FhirMemoryCache<T>(Guid.NewGuid().ToString(), limitSizeInMegabytes: 1, TimeSpan.FromMinutes(1), _logger); | ||
} | ||
} | ||
} |
186 changes: 186 additions & 0 deletions
186
src/Microsoft.Health.Fhir.Core/Features/Storage/FhirMemoryCache.cs
This file contains hidden or 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,186 @@ | ||
// ------------------------------------------------------------------------------------------------- | ||
// 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; | ||
private readonly object _lock; | ||
|
||
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; | ||
|
||
_lock = new object(); | ||
} | ||
|
||
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) | ||
{ | ||
key = FormatKey(key); | ||
|
||
lock (_lock) | ||
{ | ||
if (_cache.Contains(key)) | ||
{ | ||
return (T)_cache[key]; | ||
} | ||
|
||
AddInternal(key, value); | ||
} | ||
fhibf marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
||
return 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> | ||
public bool TryAdd(string key, T value) | ||
{ | ||
key = FormatKey(key); | ||
|
||
lock (_lock) | ||
{ | ||
if (_cache.Contains(key)) | ||
{ | ||
return false; | ||
} | ||
|
||
AddInternal(key, value); | ||
} | ||
|
||
return true; | ||
} | ||
|
||
/// <summary> | ||
/// Add a range of values to the cache. | ||
/// </summary> | ||
/// <param name="keyValuePairs">Range of values</param> | ||
public void AddRange(IReadOnlyDictionary<string, T> keyValuePairs) | ||
{ | ||
foreach (KeyValuePair<string, T> item in keyValuePairs) | ||
{ | ||
string key = FormatKey(item.Key); | ||
AddInternal(key, item.Value); | ||
} | ||
} | ||
|
||
/// <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); | ||
|
||
lock (_lock) | ||
{ | ||
if (_cache.Contains(key)) | ||
{ | ||
value = (T)_cache[key]; | ||
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 bool AddInternal(string key, T value) | ||
{ | ||
CacheItemPolicy cachePolicy = new CacheItemPolicy() | ||
{ | ||
Priority = CacheItemPriority.Default, | ||
SlidingExpiration = _expirationTime, | ||
}; | ||
|
||
return _cache.Add(key, value, cachePolicy); | ||
} | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.