Skip to content

Commit 869dbab

Browse files
authored
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.
1 parent d23bf8f commit 869dbab

File tree

4 files changed

+396
-12
lines changed

4 files changed

+396
-12
lines changed
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
// -------------------------------------------------------------------------------------------------
2+
// Copyright (c) Microsoft Corporation. All rights reserved.
3+
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
4+
// -------------------------------------------------------------------------------------------------
5+
6+
using System;
7+
using System.Collections.Generic;
8+
using Microsoft.Extensions.Logging;
9+
using Microsoft.Health.Fhir.Core.Features.Storage;
10+
using Microsoft.Health.Fhir.Tests.Common;
11+
using Microsoft.Health.Test.Utilities;
12+
using NSubstitute;
13+
using Xunit;
14+
15+
namespace Microsoft.Health.Fhir.Core.UnitTests.Features.Storage
16+
{
17+
[Trait(Traits.OwningTeam, OwningTeam.Fhir)]
18+
[Trait(Traits.Category, Categories.Security)]
19+
public sealed class FhirMemoryCacheTests
20+
{
21+
private readonly ILogger _logger = Substitute.For<ILogger>();
22+
23+
private const string DefaultKey = "key";
24+
private const string DefaultValue = "value";
25+
26+
[Theory]
27+
[InlineData(01, 01 * 1024 * 1024)]
28+
[InlineData(14, 14 * 1024 * 1024)]
29+
[InlineData(55, 55 * 1024 * 1024)]
30+
public void GivenAnEmptyCache_CheckTheCacheMemoryLimit(int limitSizeInMegabytes, long expectedLimitSizeInBytes)
31+
{
32+
var cache = new FhirMemoryCache<string>(Guid.NewGuid().ToString(), limitSizeInMegabytes, TimeSpan.FromMinutes(1), _logger);
33+
34+
Assert.Equal(expectedLimitSizeInBytes, cache.CacheMemoryLimit);
35+
}
36+
37+
[Fact]
38+
public void GivenACache_RaiseErrorsIfParametersAreInvalid()
39+
{
40+
Assert.Throws<ArgumentNullException>(
41+
() => new FhirMemoryCache<string>(
42+
null,
43+
limitSizeInMegabytes: 0,
44+
TimeSpan.FromMinutes(1),
45+
_logger));
46+
47+
Assert.Throws<ArgumentOutOfRangeException>(
48+
() => new FhirMemoryCache<string>(
49+
Guid.NewGuid().ToString(),
50+
limitSizeInMegabytes: 0,
51+
TimeSpan.FromMinutes(1),
52+
_logger));
53+
54+
Assert.Throws<ArgumentNullException>(
55+
() => new FhirMemoryCache<string>(
56+
Guid.NewGuid().ToString(),
57+
limitSizeInMegabytes: 1,
58+
TimeSpan.FromMinutes(1),
59+
null));
60+
61+
var cache = CreateRegularMemoryCache<string>();
62+
63+
Assert.Throws<ArgumentNullException>(() => cache.GetOrAdd(null, DefaultValue));
64+
Assert.Throws<ArgumentNullException>(() => cache.TryAdd(null, DefaultValue));
65+
Assert.Throws<ArgumentException>(() => cache.GetOrAdd(string.Empty, DefaultValue));
66+
Assert.Throws<ArgumentException>(() => cache.TryAdd(string.Empty, DefaultValue));
67+
Assert.Throws<ArgumentNullException>(() => cache.GetOrAdd(DefaultKey, null));
68+
Assert.Throws<ArgumentNullException>(() => cache.TryAdd(DefaultKey, null));
69+
}
70+
71+
[Fact]
72+
public void GivenAnEmptyCache_WhenAddingValue_ThenValueShouldBeAdded()
73+
{
74+
var cache = CreateRegularMemoryCache<string>();
75+
76+
var result1 = cache.GetOrAdd(DefaultKey, DefaultValue);
77+
Assert.Equal(DefaultValue, result1);
78+
79+
const string anotherValue = "Another Value";
80+
var result2 = cache.GetOrAdd(DefaultKey, anotherValue);
81+
Assert.NotEqual(anotherValue, result2);
82+
Assert.Equal(DefaultValue, result1);
83+
}
84+
85+
[Fact]
86+
public void GivenAnEmptyCache_WhenAddingValue_ThenValueShouldBeAddedAndCanBeRetrieved()
87+
{
88+
var cache = CreateRegularMemoryCache<string>();
89+
90+
cache.GetOrAdd(DefaultKey, DefaultValue);
91+
92+
Assert.True(cache.TryGet(DefaultKey, out var result));
93+
Assert.Equal(DefaultValue, result);
94+
}
95+
96+
[Fact]
97+
public void GivenAnEmptyCache_WhenAddingValueIfIgnoreCaseEnabled_ThenMultipleSimilarKeysShouldWorkAsExpected()
98+
{
99+
var cache = new FhirMemoryCache<string>(Guid.NewGuid().ToString(), limitSizeInMegabytes: 1, TimeSpan.FromMinutes(1), _logger, ignoreCase: true);
100+
101+
cache.GetOrAdd(DefaultKey, DefaultValue);
102+
103+
Assert.True(cache.TryGet(DefaultKey, out var result));
104+
Assert.Equal(DefaultValue, result);
105+
106+
Assert.True(cache.TryGet(DefaultKey.ToUpper(), out result));
107+
Assert.Equal(DefaultValue, result);
108+
109+
Assert.True(cache.TryGet("Key", out result));
110+
Assert.Equal(DefaultValue, result);
111+
112+
Assert.True(cache.TryGet("kEy", out result));
113+
Assert.Equal(DefaultValue, result);
114+
}
115+
116+
[Fact]
117+
public void GivenAnEmptyCache_WhenGettingAnItemThatDoNotExist_ThenReturnFalse()
118+
{
119+
var cache = CreateRegularMemoryCache<string>();
120+
121+
Assert.False(cache.TryGet(DefaultKey, out var result));
122+
Assert.Equal(default, result);
123+
}
124+
125+
[Fact]
126+
public void GivenAnEmptyCache_WhenAddingValueIfIgnoreCaseDisabled_ThenMultipleSimilarKeysShouldWorkAsExpected()
127+
{
128+
var cache = CreateRegularMemoryCache<string>();
129+
130+
cache.GetOrAdd(DefaultKey, DefaultValue);
131+
132+
Assert.True(cache.TryGet("key", out var result));
133+
Assert.Equal(DefaultValue, result);
134+
135+
Assert.False(cache.TryGet("KEY", out result));
136+
137+
Assert.False(cache.TryGet("Key", out result));
138+
139+
Assert.False(cache.TryGet("kEy", out result));
140+
}
141+
142+
[Fact]
143+
public void GivenAnEmptyCache_WhenAddingValue_ThenValueShouldBeAddedAndCanBeRetrievedUsingTryGetValue()
144+
{
145+
var cache = CreateRegularMemoryCache<string>();
146+
147+
cache.GetOrAdd(DefaultKey, DefaultValue);
148+
149+
Assert.True(cache.TryGet(DefaultKey, out var result));
150+
Assert.Equal(DefaultValue, result);
151+
}
152+
153+
[Fact]
154+
public void GivenAnEmptyCache_WhenAddingValue_ThenValueShouldBeAddedAndCanBeRetrievedUsingTryGetValueWithOut()
155+
{
156+
var cache = CreateRegularMemoryCache<string>();
157+
158+
cache.GetOrAdd(DefaultKey, DefaultValue);
159+
160+
Assert.True(cache.TryGet(DefaultKey, out var result));
161+
Assert.Equal(DefaultValue, result);
162+
}
163+
164+
[Fact]
165+
public void GivenAnEmptyCache_WhenAddingValue_ThenValueShouldBeAddedAndCanBeRetrievedUsingTryGetValueWithOutAndValue()
166+
{
167+
var cache = CreateRegularMemoryCache<string>();
168+
169+
cache.GetOrAdd(DefaultKey, DefaultValue);
170+
171+
Assert.True(cache.TryGet(DefaultKey, out var result));
172+
Assert.Equal(DefaultValue, result);
173+
}
174+
175+
[Fact]
176+
public void GivenAnEmptyCache_WhenRunningOperations_ThenItemsShouldBeRespected()
177+
{
178+
var cache = CreateRegularMemoryCache<long>();
179+
180+
cache.GetOrAdd(DefaultKey, 2112);
181+
Assert.True(cache.TryGet(DefaultKey, out var result));
182+
Assert.Equal(2112, result);
183+
184+
Assert.True(cache.Remove(DefaultKey));
185+
186+
Assert.False(cache.TryGet(DefaultKey, out result));
187+
}
188+
189+
private IMemoryCache<T> CreateRegularMemoryCache<T>()
190+
{
191+
return new FhirMemoryCache<T>(Guid.NewGuid().ToString(), limitSizeInMegabytes: 1, TimeSpan.FromMinutes(1), _logger);
192+
}
193+
}
194+
}
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
// -------------------------------------------------------------------------------------------------
2+
// Copyright (c) Microsoft Corporation. All rights reserved.
3+
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
4+
// -------------------------------------------------------------------------------------------------
5+
6+
using System;
7+
using System.Collections.Generic;
8+
using System.Collections.Specialized;
9+
using System.Runtime.Caching;
10+
using EnsureThat;
11+
using Microsoft.Extensions.Logging;
12+
13+
namespace Microsoft.Health.Fhir.Core.Features.Storage
14+
{
15+
public sealed class FhirMemoryCache<T> : IMemoryCache<T>
16+
{
17+
private const int DefaultLimitSizeInMegabytes = 50;
18+
private const int DefaultExpirationTimeInMinutes = 24 * 60;
19+
20+
private readonly string _cacheName;
21+
private readonly ILogger _logger;
22+
private readonly ObjectCache _cache;
23+
private readonly TimeSpan _expirationTime;
24+
private readonly bool _ignoreCase;
25+
26+
public FhirMemoryCache(string name, ILogger logger, bool ignoreCase = false)
27+
: this(
28+
name,
29+
limitSizeInMegabytes: DefaultLimitSizeInMegabytes,
30+
expirationTime: TimeSpan.FromMinutes(DefaultExpirationTimeInMinutes),
31+
logger)
32+
{
33+
}
34+
35+
public FhirMemoryCache(string name, int limitSizeInMegabytes, TimeSpan expirationTime, ILogger logger, bool ignoreCase = false)
36+
{
37+
EnsureArg.IsNotNull(name, nameof(name));
38+
EnsureArg.IsGt(limitSizeInMegabytes, 0, nameof(name));
39+
EnsureArg.IsNotNull(logger, nameof(logger));
40+
41+
_cacheName = name;
42+
_cache = new MemoryCache(
43+
_cacheName,
44+
new NameValueCollection()
45+
{
46+
{ "CacheMemoryLimitMegabytes", limitSizeInMegabytes.ToString() },
47+
});
48+
_expirationTime = expirationTime;
49+
_logger = logger;
50+
_ignoreCase = ignoreCase;
51+
}
52+
53+
public long CacheMemoryLimit => ((MemoryCache)_cache).CacheMemoryLimit;
54+
55+
/// <summary>
56+
/// Get or add the value to cache.
57+
/// </summary>
58+
/// <typeparam name="T">Type of the value in cache</typeparam>
59+
/// <param name="key">Key</param>
60+
/// <param name="value">Value</param>
61+
/// <returns>Value in cache</returns>
62+
public T GetOrAdd(string key, T value)
63+
{
64+
EnsureArg.IsNotNullOrWhiteSpace(key, nameof(key));
65+
if (value == null)
66+
{
67+
throw new ArgumentNullException(nameof(value));
68+
}
69+
70+
key = FormatKey(key);
71+
72+
CacheItem newCacheItem = new CacheItem(key, value);
73+
74+
CacheItem cachedItem = _cache.AddOrGetExisting(
75+
newCacheItem,
76+
GetDefaultCacheItemPolicy());
77+
78+
if (cachedItem.Value == null)
79+
{
80+
// If the item cache item is null, then the item was added to the cache.
81+
return (T)newCacheItem.Value;
82+
}
83+
else
84+
{
85+
return (T)cachedItem.Value;
86+
}
87+
}
88+
89+
/// <summary>
90+
/// Add the value to cache if it does not exist.
91+
/// </summary>
92+
/// <param name="key">Key</param>
93+
/// <param name="value">Value</param>
94+
/// <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>
95+
public bool TryAdd(string key, T value)
96+
{
97+
EnsureArg.IsNotNullOrWhiteSpace(key, nameof(key));
98+
if (value == null)
99+
{
100+
throw new ArgumentNullException(nameof(value));
101+
}
102+
103+
key = FormatKey(key);
104+
105+
return _cache.Add(key, value, GetDefaultCacheItemPolicy());
106+
}
107+
108+
/// <summary>
109+
/// Get an item from the cache.
110+
/// </summary>
111+
/// <param name="key">Key</param>
112+
/// <returns>Value</returns>
113+
public T Get(string key)
114+
{
115+
key = FormatKey(key);
116+
117+
return (T)_cache[key];
118+
}
119+
120+
/// <summary>
121+
/// Try to retrieve an item from cache, if it does not exist then returns the <see cref="default"/> for that generic type.
122+
/// </summary>
123+
/// <param name="key">Key</param>
124+
/// <param name="value">Value</param>
125+
/// <returns>True if the value exists in cache</returns>
126+
public bool TryGet(string key, out T value)
127+
{
128+
key = FormatKey(key);
129+
130+
CacheItem cachedItem = _cache.GetCacheItem(key);
131+
132+
if (cachedItem != null)
133+
{
134+
value = (T)cachedItem.Value;
135+
return true;
136+
}
137+
138+
_logger.LogTrace("Item does not exist in '{CacheName}' cache. Returning default value.", _cacheName);
139+
value = default;
140+
141+
return false;
142+
}
143+
144+
/// <summary>
145+
/// Removed the item indexed by the key.
146+
/// </summary>
147+
/// <param name="key">Key of the item to be removed from cache.</param>
148+
/// <returns>Returns false if the items does not exist in cache.</returns>
149+
public bool Remove(string key)
150+
{
151+
key = FormatKey(key);
152+
153+
object objectInCache = _cache.Remove(key);
154+
155+
return objectInCache != null;
156+
}
157+
158+
private string FormatKey(string key) => _ignoreCase ? key.ToLowerInvariant() : key;
159+
160+
private CacheItemPolicy GetDefaultCacheItemPolicy() => new CacheItemPolicy()
161+
{
162+
Priority = CacheItemPriority.Default,
163+
SlidingExpiration = _expirationTime,
164+
};
165+
}
166+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// -------------------------------------------------------------------------------------------------
2+
// Copyright (c) Microsoft Corporation. All rights reserved.
3+
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
4+
// -------------------------------------------------------------------------------------------------
5+
6+
using System.Collections.Generic;
7+
8+
namespace Microsoft.Health.Fhir.Core.Features.Storage
9+
{
10+
public interface IMemoryCache<T>
11+
{
12+
long CacheMemoryLimit { get; }
13+
14+
T GetOrAdd(string key, T value);
15+
16+
bool TryAdd(string key, T value);
17+
18+
T Get(string key);
19+
20+
bool TryGet(string key, out T value);
21+
22+
bool Remove(string key);
23+
}
24+
}

0 commit comments

Comments
 (0)