- Notifications
You must be signed in to change notification settings - Fork 20
feat: Added ODPSegmentManager #321
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
Changes from 15 commits
d9db9a5
e5fcd9a
61d3e0f
c4db70a
b79861e
bc60e1f
38281ab
164b9d6
4f62cd7
a614e9f
a42ed21
7efe971
8b4f002
3d27572
1c5a914
9d61e43
40f2fdf
3f91f81
be49cb1
892cd3f
77d56dd
eedf603
ec81354
fe6a6d8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,213 @@ | ||
/* | ||
* Copyright 2022 Optimizely | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
| ||
using Moq; | ||
using NUnit.Framework; | ||
using OptimizelySDK.AudienceConditions; | ||
using OptimizelySDK.ErrorHandler; | ||
using OptimizelySDK.Logger; | ||
using OptimizelySDK.Odp; | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Net; | ||
| ||
namespace OptimizelySDK.Tests.OdpTests | ||
{ | ||
[TestFixture] | ||
public class OdpSegmentManagerTest | ||
{ | ||
private const string API_KEY = "S0m3Ap1KEy4U"; | ||
private const string API_HOST = "https://odp-host.example.com"; | ||
private const string FS_USER_ID = "some_valid_user_id"; | ||
| ||
private static readonly string expectedCacheKey = $"fs_user_id-$-{FS_USER_ID}"; | ||
| ||
private static readonly List<string> segmentsToCheck = new List<string> | ||
{ | ||
"segment1", | ||
"segment2", | ||
}; | ||
| ||
private OdpConfig _odpConfig; | ||
private Mock<IOdpSegmentApiManager> _mockApiManager; | ||
private Mock<ILogger> _mockLogger; | ||
private Mock<ICache<List<string>>> _mockCache; | ||
| ||
[SetUp] | ||
public void Setup() | ||
{ | ||
_odpConfig = new OdpConfig(API_KEY, API_HOST, segmentsToCheck); | ||
| ||
_mockApiManager = new Mock<IOdpSegmentApiManager>(); | ||
| ||
_mockLogger = new Mock<ILogger>(); | ||
_mockLogger.Setup(i => i.Log(It.IsAny<LogLevel>(), It.IsAny<string>())); | ||
| ||
_mockCache = new Mock<ICache<List<string>>>(); | ||
} | ||
| ||
[Test] | ||
public void ShouldFetchSegmentsOnCacheMiss() | ||
{ | ||
var keyCollector = new List<string>(); | ||
_mockCache.Setup(c => c.Lookup(Capture.In(keyCollector))) | ||
.Returns(default(List<string>)); | ||
_mockApiManager.Setup(a => a.FetchSegments(It.IsAny<string>(), It.IsAny<string>(), | ||
It.IsAny<OdpUserKeyType>(), It.IsAny<string>(), It.IsAny<List<string>>())) | ||
.Returns(segmentsToCheck.ToArray()); | ||
var manager = new OdpSegmentManager(_odpConfig, _mockApiManager.Object, | ||
Constants.DEFAULT_MAX_CACHE_SIZE, null, _mockLogger.Object, _mockCache.Object); | ||
| ||
var segments = manager.FetchQualifiedSegments(FS_USER_ID); | ||
| ||
var cacheKey = keyCollector.FirstOrDefault(); | ||
Assert.AreEqual(expectedCacheKey, cacheKey); | ||
_mockCache.Verify(c => c.Reset(), Times.Never); | ||
_mockCache.Verify(c => c.Lookup(cacheKey), Times.Once); | ||
_mockLogger.Verify(l => | ||
l.Log(LogLevel.DEBUG, "ODP Cache Miss. Making a call to ODP Server."), Times.Once); | ||
_mockApiManager.Verify( | ||
a => a.FetchSegments( | ||
API_KEY, | ||
API_HOST, | ||
OdpUserKeyType.FS_USER_ID, | ||
FS_USER_ID, | ||
_odpConfig.SegmentsToCheck), Times.Once); | ||
_mockCache.Verify(c => c.Save(cacheKey, It.IsAny<List<string>>()), Times.Once); | ||
Assert.AreEqual(segmentsToCheck, segments); | ||
} | ||
| ||
[Test] | ||
public void ShouldFetchSegmentsSuccessOnCacheHit() | ||
{ | ||
var keyCollector = new List<string>(); | ||
_mockCache.Setup(c => c.Lookup(Capture.In(keyCollector))) | ||
.Returns(segmentsToCheck); | ||
_mockApiManager.Setup(a => a.FetchSegments(It.IsAny<string>(), It.IsAny<string>(), | ||
It.IsAny<OdpUserKeyType>(), It.IsAny<string>(), It.IsAny<List<string>>())); | ||
var manager = new OdpSegmentManager(_odpConfig, _mockApiManager.Object, | ||
Constants.DEFAULT_MAX_CACHE_SIZE, null, _mockLogger.Object, _mockCache.Object); | ||
| ||
var segments = manager.FetchQualifiedSegments(FS_USER_ID); | ||
| ||
var cacheKey = keyCollector.FirstOrDefault(); | ||
Assert.AreEqual(expectedCacheKey, cacheKey); | ||
_mockCache.Verify(c => c.Reset(), Times.Never); | ||
_mockCache.Verify(c => c.Lookup(cacheKey), Times.Once); | ||
_mockLogger.Verify(l => | ||
l.Log(LogLevel.DEBUG, "ODP Cache Hit. Returning segments from Cache."), Times.Once); | ||
_mockApiManager.Verify( | ||
a => a.FetchSegments(It.IsAny<string>(), It.IsAny<string>(), | ||
It.IsAny<OdpUserKeyType>(), It.IsAny<string>(), It.IsAny<List<string>>()), | ||
Times.Never); | ||
_mockCache.Verify(c => c.Save(expectedCacheKey, It.IsAny<List<string>>()), Times.Never); | ||
Assert.AreEqual(segmentsToCheck, segments); | ||
} | ||
| ||
[Test] | ||
public void ShouldHandleFetchSegmentsWithError() | ||
{ | ||
// OdpSegmentApiManager.FetchSegments() return null on any error | ||
_mockApiManager.Setup(a => a.FetchSegments(It.IsAny<string>(), It.IsAny<string>(), | ||
It.IsAny<OdpUserKeyType>(), It.IsAny<string>(), It.IsAny<List<string>>())) | ||
.Returns(null as string[]); | ||
var manager = new OdpSegmentManager(_odpConfig, _mockApiManager.Object, | ||
Constants.DEFAULT_MAX_CACHE_SIZE, null, _mockLogger.Object, _mockCache.Object); | ||
| ||
var segments = manager.FetchQualifiedSegments(FS_USER_ID); | ||
| ||
_mockCache.Verify(c => c.Reset(), Times.Never); | ||
_mockCache.Verify(c => c.Lookup(expectedCacheKey), Times.Once); | ||
_mockApiManager.Verify( | ||
a => a.FetchSegments(It.IsAny<string>(), It.IsAny<string>(), | ||
It.IsAny<OdpUserKeyType>(), It.IsAny<string>(), It.IsAny<List<string>>()), | ||
Times.Once); | ||
_mockCache.Verify(c => c.Save(expectedCacheKey, It.IsAny<List<string>>()), Times.Once); | ||
Assert.IsNull(segments); | ||
} | ||
| ||
[Test] | ||
public void ShouldLogAndReturnEmptySegmentsListWhenOdpConfigNotReady() | ||
{ | ||
var mockOdpConfig = new Mock<OdpConfig>(API_KEY, API_HOST, new List<string>(0)); | ||
mockOdpConfig.Setup(o => o.IsReady()).Returns(false); | ||
var manager = new OdpSegmentManager(mockOdpConfig.Object, _mockApiManager.Object, | ||
Constants.DEFAULT_MAX_CACHE_SIZE, null, _mockLogger.Object, _mockCache.Object); | ||
| ||
var segments = manager.FetchQualifiedSegments(FS_USER_ID); | ||
| ||
Assert.IsTrue(segments.Count == 0); | ||
_mockLogger.Verify( | ||
l => l.Log(LogLevel.WARN, Constants.ODP_NOT_INTEGRATED_MESSAGE), | ||
Times.Once); | ||
} | ||
| ||
[Test] | ||
public void ShouldIgnoreCache() | ||
{ | ||
var manager = new OdpSegmentManager(_odpConfig, _mockApiManager.Object, | ||
Constants.DEFAULT_MAX_CACHE_SIZE, null, _mockLogger.Object, _mockCache.Object); | ||
| ||
manager.FetchQualifiedSegments(FS_USER_ID, new List<OdpSegmentOption> | ||
{ | ||
OdpSegmentOption.IgnoreCache, | ||
}); | ||
| ||
_mockCache.Verify(c => c.Reset(), Times.Never); | ||
_mockCache.Verify(c => c.Lookup(It.IsAny<string>()), Times.Never); | ||
_mockApiManager.Verify( | ||
a => a.FetchSegments(It.IsAny<string>(), It.IsAny<string>(), | ||
It.IsAny<OdpUserKeyType>(), It.IsAny<string>(), It.IsAny<List<string>>()), | ||
Times.Once); | ||
_mockCache.Verify(c => c.Save(expectedCacheKey, It.IsAny<List<string>>()), Times.Never); | ||
} | ||
| ||
[Test] | ||
public void ShouldResetCache() | ||
{ | ||
var manager = new OdpSegmentManager(_odpConfig, _mockApiManager.Object, | ||
Constants.DEFAULT_MAX_CACHE_SIZE, null, _mockLogger.Object, _mockCache.Object); | ||
| ||
manager.FetchQualifiedSegments(FS_USER_ID, new List<OdpSegmentOption> | ||
{ | ||
OdpSegmentOption.ResetCache, | ||
}); | ||
| ||
_mockCache.Verify(c => c.Reset(), Times.Once); | ||
_mockCache.Verify(c => c.Lookup(It.IsAny<string>()), Times.Never); | ||
_mockApiManager.Verify( | ||
a => a.FetchSegments(It.IsAny<string>(), It.IsAny<string>(), | ||
It.IsAny<OdpUserKeyType>(), It.IsAny<string>(), It.IsAny<List<string>>()), | ||
Times.Once); | ||
_mockCache.Verify(c => c.Save(expectedCacheKey, It.IsAny<List<string>>()), Times.Once); | ||
} | ||
| ||
[Test] | ||
public void ShouldMakeValidCacheKey() | ||
{ | ||
var keyCollector = new List<string>(); | ||
_mockCache.Setup(c => c.Lookup(Capture.In(keyCollector))); | ||
var manager = new OdpSegmentManager(_odpConfig, _mockApiManager.Object, | ||
Constants.DEFAULT_MAX_CACHE_SIZE, null, _mockLogger.Object, _mockCache.Object); | ||
| ||
manager.FetchQualifiedSegments(FS_USER_ID); | ||
| ||
var cacheKey = keyCollector.FirstOrDefault(); | ||
Assert.AreEqual(expectedCacheKey, cacheKey); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
/* | ||
* Copyright 2022 Optimizely | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
| ||
using System.Collections.Generic; | ||
| ||
namespace OptimizelySDK.Odp | ||
{ | ||
/// <summary> | ||
/// Interface to schedule connections to ODP for audience segmentation and caches the results. | ||
/// </summary> | ||
public interface IOdpSegmentManager | ||
{ | ||
/// <summary> | ||
/// Attempts to fetch and return a list of a user's qualified segments from the local segments cache. | ||
/// If no cached data exists for the target user, this fetches and caches data from the ODP server instead. | ||
/// </summary> | ||
/// <param name="fsUserId">The FS User ID identifying the user</param> | ||
/// <param name="options">An array of OptimizelySegmentOption used to ignore and/or reset the cache.</param> | ||
/// <returns>Qualified segments for the user from the cache or the ODP server if the cache is empty.</returns> | ||
List<string> FetchQualifiedSegments(string fsUserId, List<OdpSegmentOption> options = null); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
| @@ -25,11 +25,6 @@ namespace OptimizelySDK.Odp | |
public class LruCache<T> : ICache<T> | ||
where T : class | ||
{ | ||
/// <summary> | ||
/// Default maximum number of elements to store | ||
/// </summary> | ||
private const int DEFAULT_MAX_SIZE = 10000; | ||
| ||
/// <summary> | ||
/// The maximum number of elements that should be stored | ||
/// </summary> | ||
| @@ -66,7 +61,8 @@ public class LruCache<T> : ICache<T> | |
/// <param name="maxSize">Maximum number of elements to allow in the cache</param> | ||
/// <param name="itemTimeout">Timeout or time to live for each item</param> | ||
/// <param name="logger">Implementation used for recording LRU events or errors</param> | ||
public LruCache(int maxSize = DEFAULT_MAX_SIZE, TimeSpan? itemTimeout = default, | ||
public LruCache(int maxSize = Constants.DEFAULT_MAX_CACHE_SIZE, | ||
TimeSpan? itemTimeout = default, | ||
ILogger logger = null | ||
) | ||
{ | ||
| @@ -76,7 +72,7 @@ public LruCache(int maxSize = DEFAULT_MAX_SIZE, TimeSpan? itemTimeout = default, | |
| ||
_logger = logger ?? new DefaultLogger(); | ||
| ||
_timeout = itemTimeout ?? TimeSpan.FromMinutes(10); | ||
_timeout = itemTimeout ?? TimeSpan.FromMinutes(Constants.DEFAULT_CACHE_MINUTES); | ||
if (_timeout < TimeSpan.Zero) | ||
{ | ||
_logger.Log(LogLevel.WARN, | ||
| @@ -118,9 +114,11 @@ public void Save(string key, T value) | |
{ | ||
var leastRecentlyUsedItem = _list.Last; | ||
| ||
var leastRecentlyUsedItemKey = (from cacheItem in _cache | ||
where cacheItem.Value == leastRecentlyUsedItem.Value | ||
select cacheItem.Key).FirstOrDefault(); | ||
var leastRecentlyUsedItemKey = | ||
_cache.Where( | ||
cacheItem => cacheItem.Value == leastRecentlyUsedItem.Value). | ||
| ||
Select(cacheItem => cacheItem.Key). | ||
FirstOrDefault(); | ||
| ||
if (leastRecentlyUsedItemKey != null) | ||
{ | ||
| @@ -227,9 +225,14 @@ public string[] _readCurrentCacheKeys() | |
{ | ||
_logger.Log(LogLevel.WARN, "_readCurrentCacheKeys used for non-testing purpose"); | ||
| ||
return (from listItem in _list | ||
join cacheItem in _cache on listItem equals cacheItem.Value | ||
select cacheItem.Key).ToArray(); | ||
lock (_mutex) | ||
{ | ||
return _list.Join(_cache, | ||
| ||
listItem => listItem, | ||
cacheItem => cacheItem.Value, | ||
(listItem, cacheItem) => cacheItem.Key). | ||
ToArray(); | ||
} | ||
} | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.