Skip to content

Commit 3cd6745

Browse files
feat: Added ODPSegmentManager (#321)
* WIP Initial SegmentManager commit * WIP Initial commit fixes * Finish OdpSegmentManager & interface * WIP unit tests starts * Unit tests & Segment Manager edits to satisfy the unit tests. * Fix merge issues; Add unit test * Lint fixes * Remove re-added IOdpConfig.cs * Add internal doc * PR code review revisions * Update unit test * Update OptimizelySDK/Odp/OdpSegmentManager.cs Co-authored-by: Jae Kim <45045038+jaeopt@users.noreply.github.com> * Pull request code revisions * Remove time complexity looping/Linq * Small refactor * Use OrderedDictionary Co-authored-by: Jae Kim <45045038+jaeopt@users.noreply.github.com>
1 parent 3eb53bd commit 3cd6745

File tree

9 files changed

+472
-51
lines changed

9 files changed

+472
-51
lines changed
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
/*
2+
* Copyright 2022 Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
using Moq;
18+
using NUnit.Framework;
19+
using OptimizelySDK.AudienceConditions;
20+
using OptimizelySDK.ErrorHandler;
21+
using OptimizelySDK.Logger;
22+
using OptimizelySDK.Odp;
23+
using System;
24+
using System.Collections.Generic;
25+
using System.Linq;
26+
using System.Net;
27+
28+
namespace OptimizelySDK.Tests.OdpTests
29+
{
30+
[TestFixture]
31+
public class OdpSegmentManagerTest
32+
{
33+
private const string API_KEY = "S0m3Ap1KEy4U";
34+
private const string API_HOST = "https://odp-host.example.com";
35+
private const string FS_USER_ID = "some_valid_user_id";
36+
37+
private static readonly string expectedCacheKey = $"fs_user_id-$-{FS_USER_ID}";
38+
39+
private static readonly List<string> segmentsToCheck = new List<string>
40+
{
41+
"segment1",
42+
"segment2",
43+
};
44+
45+
private OdpConfig _odpConfig;
46+
private Mock<IOdpSegmentApiManager> _mockApiManager;
47+
private Mock<ILogger> _mockLogger;
48+
private Mock<ICache<List<string>>> _mockCache;
49+
50+
[SetUp]
51+
public void Setup()
52+
{
53+
_odpConfig = new OdpConfig(API_KEY, API_HOST, segmentsToCheck);
54+
55+
_mockApiManager = new Mock<IOdpSegmentApiManager>();
56+
57+
_mockLogger = new Mock<ILogger>();
58+
_mockLogger.Setup(i => i.Log(It.IsAny<LogLevel>(), It.IsAny<string>()));
59+
60+
_mockCache = new Mock<ICache<List<string>>>();
61+
}
62+
63+
[Test]
64+
public void ShouldFetchSegmentsOnCacheMiss()
65+
{
66+
var keyCollector = new List<string>();
67+
_mockCache.Setup(c => c.Lookup(Capture.In(keyCollector))).
68+
Returns(default(List<string>));
69+
_mockApiManager.Setup(a => a.FetchSegments(It.IsAny<string>(), It.IsAny<string>(),
70+
It.IsAny<OdpUserKeyType>(), It.IsAny<string>(), It.IsAny<List<string>>())).
71+
Returns(segmentsToCheck.ToArray());
72+
var manager = new OdpSegmentManager(_odpConfig, _mockApiManager.Object,
73+
Constants.DEFAULT_MAX_CACHE_SIZE, null, _mockLogger.Object, _mockCache.Object);
74+
75+
var segments = manager.FetchQualifiedSegments(FS_USER_ID);
76+
77+
var cacheKey = keyCollector.FirstOrDefault();
78+
Assert.AreEqual(expectedCacheKey, cacheKey);
79+
_mockCache.Verify(c => c.Reset(), Times.Never);
80+
_mockCache.Verify(c => c.Lookup(cacheKey), Times.Once);
81+
_mockLogger.Verify(l =>
82+
l.Log(LogLevel.DEBUG, "ODP Cache Miss. Making a call to ODP Server."), Times.Once);
83+
_mockApiManager.Verify(
84+
a => a.FetchSegments(
85+
API_KEY,
86+
API_HOST,
87+
OdpUserKeyType.FS_USER_ID,
88+
FS_USER_ID,
89+
_odpConfig.SegmentsToCheck), Times.Once);
90+
_mockCache.Verify(c => c.Save(cacheKey, It.IsAny<List<string>>()), Times.Once);
91+
Assert.AreEqual(segmentsToCheck, segments);
92+
}
93+
94+
[Test]
95+
public void ShouldFetchSegmentsSuccessOnCacheHit()
96+
{
97+
var keyCollector = new List<string>();
98+
_mockCache.Setup(c => c.Lookup(Capture.In(keyCollector))).Returns(segmentsToCheck);
99+
_mockApiManager.Setup(a => a.FetchSegments(It.IsAny<string>(), It.IsAny<string>(),
100+
It.IsAny<OdpUserKeyType>(), It.IsAny<string>(), It.IsAny<List<string>>()));
101+
var manager = new OdpSegmentManager(_odpConfig, _mockApiManager.Object,
102+
Constants.DEFAULT_MAX_CACHE_SIZE, null, _mockLogger.Object, _mockCache.Object);
103+
104+
var segments = manager.FetchQualifiedSegments(FS_USER_ID);
105+
106+
var cacheKey = keyCollector.FirstOrDefault();
107+
Assert.AreEqual(expectedCacheKey, cacheKey);
108+
_mockCache.Verify(c => c.Reset(), Times.Never);
109+
_mockCache.Verify(c => c.Lookup(cacheKey), Times.Once);
110+
_mockLogger.Verify(l =>
111+
l.Log(LogLevel.DEBUG, "ODP Cache Hit. Returning segments from Cache."), Times.Once);
112+
_mockApiManager.Verify(
113+
a => a.FetchSegments(It.IsAny<string>(), It.IsAny<string>(),
114+
It.IsAny<OdpUserKeyType>(), It.IsAny<string>(), It.IsAny<List<string>>()),
115+
Times.Never);
116+
_mockCache.Verify(c => c.Save(expectedCacheKey, It.IsAny<List<string>>()), Times.Never);
117+
Assert.AreEqual(segmentsToCheck, segments);
118+
}
119+
120+
[Test]
121+
public void ShouldHandleFetchSegmentsWithError()
122+
{
123+
// OdpSegmentApiManager.FetchSegments() return null on any error
124+
_mockApiManager.Setup(a => a.FetchSegments(It.IsAny<string>(), It.IsAny<string>(),
125+
It.IsAny<OdpUserKeyType>(), It.IsAny<string>(), It.IsAny<List<string>>())).
126+
Returns(null as string[]);
127+
var manager = new OdpSegmentManager(_odpConfig, _mockApiManager.Object,
128+
Constants.DEFAULT_MAX_CACHE_SIZE, null, _mockLogger.Object, _mockCache.Object);
129+
130+
var segments = manager.FetchQualifiedSegments(FS_USER_ID);
131+
132+
_mockCache.Verify(c => c.Reset(), Times.Never);
133+
_mockCache.Verify(c => c.Lookup(expectedCacheKey), Times.Once);
134+
_mockApiManager.Verify(
135+
a => a.FetchSegments(It.IsAny<string>(), It.IsAny<string>(),
136+
It.IsAny<OdpUserKeyType>(), It.IsAny<string>(), It.IsAny<List<string>>()),
137+
Times.Once);
138+
_mockCache.Verify(c => c.Save(expectedCacheKey, It.IsAny<List<string>>()), Times.Never);
139+
Assert.IsNull(segments);
140+
}
141+
142+
[Test]
143+
public void ShouldLogAndReturnAnEmptySetWhenNoSegmentsToCheck()
144+
{
145+
var odpConfig = new OdpConfig(API_KEY, API_HOST, new List<string>(0));
146+
var manager = new OdpSegmentManager(odpConfig, _mockApiManager.Object,
147+
Constants.DEFAULT_MAX_CACHE_SIZE, null, _mockLogger.Object, _mockCache.Object);
148+
149+
var segments = manager.FetchQualifiedSegments(FS_USER_ID);
150+
151+
Assert.IsTrue(segments.Count == 0);
152+
_mockLogger.Verify(
153+
l => l.Log(LogLevel.DEBUG,
154+
"No Segments are used in the project, Not Fetching segments. Returning empty list."),
155+
Times.Once);
156+
}
157+
158+
[Test]
159+
public void ShouldLogAndReturnNullWhenOdpConfigNotReady()
160+
{
161+
var mockOdpConfig = new Mock<OdpConfig>(API_KEY, API_HOST, new List<string>(0));
162+
mockOdpConfig.Setup(o => o.IsReady()).Returns(false);
163+
var manager = new OdpSegmentManager(mockOdpConfig.Object, _mockApiManager.Object,
164+
Constants.DEFAULT_MAX_CACHE_SIZE, null, _mockLogger.Object, _mockCache.Object);
165+
166+
var segments = manager.FetchQualifiedSegments(FS_USER_ID);
167+
168+
Assert.IsNull(segments);
169+
_mockLogger.Verify(
170+
l => l.Log(LogLevel.WARN, Constants.ODP_NOT_INTEGRATED_MESSAGE),
171+
Times.Once);
172+
}
173+
174+
[Test]
175+
public void ShouldIgnoreCache()
176+
{
177+
var manager = new OdpSegmentManager(_odpConfig, _mockApiManager.Object,
178+
Constants.DEFAULT_MAX_CACHE_SIZE, null, _mockLogger.Object, _mockCache.Object);
179+
180+
manager.FetchQualifiedSegments(FS_USER_ID, new List<OdpSegmentOption>
181+
{
182+
OdpSegmentOption.IgnoreCache,
183+
});
184+
185+
_mockCache.Verify(c => c.Reset(), Times.Never);
186+
_mockCache.Verify(c => c.Lookup(It.IsAny<string>()), Times.Never);
187+
_mockApiManager.Verify(
188+
a => a.FetchSegments(It.IsAny<string>(), It.IsAny<string>(),
189+
It.IsAny<OdpUserKeyType>(), It.IsAny<string>(), It.IsAny<List<string>>()),
190+
Times.Once);
191+
_mockCache.Verify(c => c.Save(expectedCacheKey, It.IsAny<List<string>>()), Times.Never);
192+
}
193+
194+
[Test]
195+
public void ShouldResetCache()
196+
{
197+
var manager = new OdpSegmentManager(_odpConfig, _mockApiManager.Object,
198+
Constants.DEFAULT_MAX_CACHE_SIZE, null, _mockLogger.Object, _mockCache.Object);
199+
200+
manager.FetchQualifiedSegments(FS_USER_ID, new List<OdpSegmentOption>
201+
{
202+
OdpSegmentOption.ResetCache,
203+
});
204+
205+
_mockCache.Verify(c => c.Reset(), Times.Once);
206+
_mockCache.Verify(c => c.Lookup(It.IsAny<string>()), Times.Once);
207+
_mockApiManager.Verify(
208+
a => a.FetchSegments(It.IsAny<string>(), It.IsAny<string>(),
209+
It.IsAny<OdpUserKeyType>(), It.IsAny<string>(), It.IsAny<List<string>>()),
210+
Times.Once);
211+
_mockCache.Verify(c => c.Save(expectedCacheKey, It.IsAny<List<string>>()), Times.Once);
212+
}
213+
214+
[Test]
215+
public void ShouldMakeValidCacheKey()
216+
{
217+
var keyCollector = new List<string>();
218+
_mockCache.Setup(c => c.Lookup(Capture.In(keyCollector)));
219+
var manager = new OdpSegmentManager(_odpConfig, _mockApiManager.Object,
220+
Constants.DEFAULT_MAX_CACHE_SIZE, null, _mockLogger.Object, _mockCache.Object);
221+
222+
manager.FetchQualifiedSegments(FS_USER_ID);
223+
224+
var cacheKey = keyCollector.FirstOrDefault();
225+
Assert.AreEqual(expectedCacheKey, cacheKey);
226+
}
227+
}
228+
}

OptimizelySDK.Tests/OptimizelySDK.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
<Compile Include="OdpTests\LruCacheTest.cs" />
8686
<Compile Include="OdpTests\OdpEventManagerTests.cs" />
8787
<Compile Include="OdpTests\OdpEventApiManagerTest.cs" />
88+
<Compile Include="OdpTests\OdpSegmentManagerTest.cs" />
8889
<Compile Include="OptimizelyConfigTests\OptimizelyConfigTest.cs" />
8990
<Compile Include="OptimizelyDecisions\OptimizelyDecisionTest.cs" />
9091
<Compile Include="OptimizelyJSONTest.cs" />

OptimizelySDK/Odp/Constants.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,5 +101,15 @@ public static class Constants
101101
/// Default amount of time to wait for ODP response
102102
/// </summary>
103103
public static readonly TimeSpan DEFAULT_TIMEOUT_INTERVAL = TimeSpan.FromSeconds(10);
104+
105+
/// <summary>
106+
/// Default maximum number of elements to cache
107+
/// </summary>
108+
public const int DEFAULT_MAX_CACHE_SIZE = 10000;
109+
110+
/// <summary>
111+
/// Default number of seconds to cache
112+
/// </summary>
113+
public const int DEFAULT_CACHE_SECONDS = 600;
104114
}
105115
}

OptimizelySDK/Odp/Enums.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,23 @@
1616

1717
namespace OptimizelySDK.Odp
1818
{
19+
/// <summary>
20+
/// Type of ODP key used for fetching segments & sending events
21+
/// </summary>
1922
public enum OdpUserKeyType
2023
{
2124
// ReSharper disable InconsistentNaming
22-
// ODP expects these names; .ToString() used
23-
VUID = 0,
25+
// ODP expects these names in UPPERCASE; .ToString() used
26+
VUID = 0, // kept for SDK consistency and awareness
2427
FS_USER_ID = 1,
2528
}
29+
30+
/// <summary>
31+
/// Options used during segment cache handling
32+
/// </summary>
33+
public enum OdpSegmentOption
34+
{
35+
IgnoreCache = 0,
36+
ResetCache = 1,
37+
}
2638
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright 2022 Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
using System.Collections.Generic;
18+
19+
namespace OptimizelySDK.Odp
20+
{
21+
/// <summary>
22+
/// Interface to schedule connections to ODP for audience segmentation and caches the results.
23+
/// </summary>
24+
public interface IOdpSegmentManager
25+
{
26+
/// <summary>
27+
/// Attempts to fetch and return a list of a user's qualified segments from the local segments cache.
28+
/// If no cached data exists for the target user, this fetches and caches data from the ODP server instead.
29+
/// </summary>
30+
/// <param name="fsUserId">The FS User ID identifying the user</param>
31+
/// <param name="options">An array of OptimizelySegmentOption used to ignore and/or reset the cache.</param>
32+
/// <returns>Qualified segments for the user from the cache or the ODP server if the cache is empty.</returns>
33+
List<string> FetchQualifiedSegments(string fsUserId, List<OdpSegmentOption> options = null);
34+
}
35+
}

0 commit comments

Comments
 (0)