Skip to content

feat: Added a generic LRUCache interface and a default implementation #311

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 21 commits into from
Aug 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
671e159
Skeleton
mikechu-optimizely Aug 5, 2022
3d1ae15
LruCache initial implementation
mikechu-optimizely Aug 5, 2022
b987d9d
Add helper extensions
mikechu-optimizely Aug 5, 2022
295fdd2
WIP Fill unit tests
mikechu-optimizely Aug 5, 2022
50fd4e1
WIP corrections based on tests 2 more failing
mikechu-optimizely Aug 5, 2022
22acf8e
Remove SetTimeout, fix for non-passing test
mikechu-optimizely Aug 8, 2022
d2e3373
Add copyright notices
mikechu-optimizely Aug 8, 2022
d758efb
Remove InternalsVisibleTo for testing
mikechu-optimizely Aug 8, 2022
da33c99
Add new line at end of files via .editorconfig
mikechu-optimizely Aug 8, 2022
9fe246e
Readonly and remove excess methods/constructor
mikechu-optimizely Aug 8, 2022
33dc181
Code review corrections
mikechu-optimizely Aug 10, 2022
2f35de3
WIP code review changes
mikechu-optimizely Aug 10, 2022
24737fe
Possibly better solution to cast
mikechu-optimizely Aug 10, 2022
e042d99
Move readonlys into constructor
mikechu-optimizely Aug 10, 2022
6749182
Switch to using TimeSpan + refactors
mikechu-optimizely Aug 11, 2022
2bac003
A few more refactors
mikechu-optimizely Aug 11, 2022
03c9abf
Change underlying implementation of LRU
mikechu-optimizely Aug 12, 2022
209c3bd
Attempt to fulfill a default Timespan
mikechu-optimizely Aug 16, 2022
42a735d
Add logging; use 0s instead of descriptive consts
mikechu-optimizely Aug 16, 2022
722780c
Remove Tuple from LRU Cache
mikechu-optimizely Aug 16, 2022
ea2f32a
Remove System.ValueTuple NuGet package
mikechu-optimizely Aug 16, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ tab_width = 4

# New line preferences
end_of_line = crlf
insert_final_newline = false
insert_final_newline = true

#### .NET Coding Conventions ####

Expand Down
213 changes: 213 additions & 0 deletions OptimizelySDK.Tests/OdpTests/LruCacheTest.cs
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 NUnit.Framework;
using OptimizelySDK.Odp;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;

namespace OptimizelySDK.Tests.OdpTests
{
public class LruCacheTest
{
private List<string> _segments1And2;
private List<string> _segments3And4;
private List<string> _segments5And6;

[SetUp]
public void SetUp()
{
_segments1And2 = new List<string>
{
"segment1",
"segment2",
};
_segments3And4 = new List<string>
{
"segment3",
"segment4",
};
_segments5And6 = new List<string>
{
"segment5",
"segment6",
};
}

[Test]
public void ShouldCreateSaveAndLookupOneItem()
{
var cache = new LruCache<string>();
Assert.IsNull(cache.Lookup("key1"));

cache.Save("key1", "value1");
Assert.AreEqual("value1", cache.Lookup("key1"));
}

[Test]
public void ShouldSaveAndLookupMultipleItems()
{
var cache = new LruCache<List<string>>();

cache.Save("user1", _segments1And2);
cache.Save("user2", _segments3And4);
cache.Save("user3", _segments5And6);

var cacheKeys = cache._readCurrentCacheKeys();

Assert.AreEqual("user3", cacheKeys[0]);
Assert.AreEqual("user2", cacheKeys[1]);
Assert.AreEqual("user1", cacheKeys[2]);

// Lookup should move user1 to top of the list and push down others.
Assert.AreEqual(_segments1And2, cache.Lookup("user1"));

cacheKeys = cache._readCurrentCacheKeys();
Assert.AreEqual("user1", cacheKeys[0]);
Assert.AreEqual("user3", cacheKeys[1]);
Assert.AreEqual("user2", cacheKeys[2]);

// Lookup should move user2 to the beginning of the list and push others.
Assert.AreEqual(_segments3And4, cache.Lookup("user2"));

cacheKeys = cache._readCurrentCacheKeys();
Assert.AreEqual("user2", cacheKeys[0]);
Assert.AreEqual("user1", cacheKeys[1]);
Assert.AreEqual("user3", cacheKeys[2]);

// Lookup moves user3 to top and pushes others down.
Assert.AreEqual(_segments5And6, cache.Lookup("user3"));

cacheKeys = cache._readCurrentCacheKeys();
Assert.AreEqual("user3", cacheKeys[0]);
Assert.AreEqual("user2", cacheKeys[1]);
Assert.AreEqual("user1", cacheKeys[2]);
}

[Test]
public void ShouldReorderListOnSave()
{
var cache = new LruCache<List<string>>();

cache.Save("user1", _segments1And2);
cache.Save("user2", _segments3And4);
cache.Save("user3", _segments5And6);

var cacheKeys = cache._readCurrentCacheKeys();

// last added should be at the top of the list
Assert.AreEqual("user3",cacheKeys[0]);
Assert.AreEqual("user2",cacheKeys[1]);
Assert.AreEqual("user1" ,cacheKeys[2]);

// save should move user1 to the top of the list and push down others.
cache.Save("user1", _segments1And2);

cacheKeys = cache._readCurrentCacheKeys();
Assert.AreEqual("user1",cacheKeys[0]);
Assert.AreEqual("user3",cacheKeys[1]);
Assert.AreEqual("user2",cacheKeys[2]);

// save user2 should bring it to the top and push down others.
cache.Save("user2", _segments3And4);

cacheKeys = cache._readCurrentCacheKeys();
Assert.AreEqual("user2",cacheKeys[0]);
Assert.AreEqual("user1",cacheKeys[1]);
Assert.AreEqual("user3",cacheKeys[2]);

// saving user3 again should return to the original insertion order.
cache.Save("user3", _segments5And6);

cacheKeys = cache._readCurrentCacheKeys();
Assert.AreEqual("user3",cacheKeys[0]);
Assert.AreEqual("user2",cacheKeys[1]);
Assert.AreEqual("user1" ,cacheKeys[2]);
}

[Test]
public void ShouldHandleWhenCacheIsDisabled()
{
var cache = new LruCache<List<string>>(maxSize: 0);

cache.Save("user1", _segments1And2);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this possible if we can return true or false. @jaeopt a question for you?

cache.Save("user2", _segments3And4);
cache.Save("user3", _segments5And6);

Assert.IsNull(cache.Lookup("user1"));
Assert.IsNull(cache.Lookup("user2"));
Assert.IsNull(cache.Lookup("user3"));
}

[Test]
public void ShouldHandleWhenItemsExpire()
{
var cache = new LruCache<List<string>>(itemTimeout: TimeSpan.FromSeconds(1));

cache.Save("user1", _segments1And2);

Assert.AreEqual(_segments1And2, cache.Lookup("user1"));
Assert.AreEqual(1, cache._readCurrentCacheKeys().Length);

Thread.Sleep(1200);

Assert.IsNull(cache.Lookup("user1"));
Assert.AreEqual(0, cache._readCurrentCacheKeys().Length);
}

[Test]
public void ShouldHandleWhenCacheReachesMaxSize()
{
var cache = new LruCache<List<string>>(maxSize: 2);

cache.Save("user1", _segments1And2);
cache.Save("user2", _segments3And4);
cache.Save("user3", _segments5And6);

Assert.AreEqual(2, cache._readCurrentCacheKeys().Length);

Assert.AreEqual(_segments5And6, cache.Lookup("user3"));
Assert.AreEqual(_segments3And4, cache.Lookup("user2"));
Assert.IsNull(cache.Lookup("user1"));
}

[Test]
public void ShouldHandleWhenCacheIsReset()
{
var cache = new LruCache<List<string>>();

cache.Save("user1", _segments1And2);
cache.Save("user2", _segments3And4);
cache.Save("user3", _segments5And6);

Assert.AreEqual(_segments1And2, cache.Lookup("user1"));
Assert.AreEqual(_segments3And4, cache.Lookup("user2"));
Assert.AreEqual(_segments5And6, cache.Lookup("user3"));

Assert.AreEqual(3, cache._readCurrentCacheKeys().Length);

cache.Reset();

Assert.IsNull(cache.Lookup("user1"));
Assert.IsNull(cache.Lookup("user2"));
Assert.IsNull(cache.Lookup("user3"));

Assert.AreEqual(0, cache._readCurrentCacheKeys().Length);
}
}
}
2 changes: 1 addition & 1 deletion OptimizelySDK.Tests/OptimizelySDK.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
<Compile Include="DefaultErrorHandlerTest.cs" />
<Compile Include="EntityTests\IntegrationTest.cs" />
<Compile Include="EventTests\EventProcessorProps.cs" />
<Compile Include="OdpTests\LruCacheTest.cs" />
<Compile Include="OptimizelyConfigTests\OptimizelyConfigTest.cs" />
<Compile Include="OptimizelyDecisions\OptimizelyDecisionTest.cs" />
<Compile Include="OptimizelyJSONTest.cs" />
Expand Down Expand Up @@ -150,7 +151,6 @@
<ItemGroup>
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
</ItemGroup>
<ItemGroup />
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
Expand Down
26 changes: 26 additions & 0 deletions OptimizelySDK/Odp/ICache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* 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.
*/

namespace OptimizelySDK.Odp
{
public interface ICache<T>
where T : class
{
void Save(string key, T value);
T Lookup(string key);
void Reset();
}
}
Loading