first commit
This commit is contained in:
commit
553fa7d95e
28
SalmonCache.sln
Normal file
28
SalmonCache.sln
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 17
|
||||||
|
VisualStudioVersion = 17.0.31903.59
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SalmonCache", "SalmonCache\SalmonCache.csproj", "{554D35BB-451F-484B-A959-CF3438F951EA}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SalmonCacheTest", "SalmonCacheTest\SalmonCacheTest.csproj", "{FA3B3EDF-F9A8-47B2-AB2D-9844E92A86EB}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{554D35BB-451F-484B-A959-CF3438F951EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{554D35BB-451F-484B-A959-CF3438F951EA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{554D35BB-451F-484B-A959-CF3438F951EA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{554D35BB-451F-484B-A959-CF3438F951EA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{FA3B3EDF-F9A8-47B2-AB2D-9844E92A86EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{FA3B3EDF-F9A8-47B2-AB2D-9844E92A86EB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{FA3B3EDF-F9A8-47B2-AB2D-9844E92A86EB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{FA3B3EDF-F9A8-47B2-AB2D-9844E92A86EB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
146
SalmonCache/SalmonCache.cs
Normal file
146
SalmonCache/SalmonCache.cs
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
namespace SalmonCache;
|
||||||
|
|
||||||
|
public class SalmonCache<K, V> where K : notnull
|
||||||
|
{
|
||||||
|
private class SalmonCacheValue
|
||||||
|
{
|
||||||
|
public K Key { set; get; }
|
||||||
|
public V Value { set; get; }
|
||||||
|
public SalmonCacheValue(K key, V value)
|
||||||
|
{
|
||||||
|
Key = key;
|
||||||
|
Value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private int _capacity;
|
||||||
|
private bool _logRemovals;
|
||||||
|
private readonly ReaderWriterLockSlim _lock;
|
||||||
|
private readonly LinkedList<SalmonCacheValue> _storage;
|
||||||
|
private readonly Dictionary<K, LinkedListNode<SalmonCacheValue>> _cache;
|
||||||
|
public SalmonCache(int capacity, bool logRemovals = false)
|
||||||
|
{
|
||||||
|
_capacity = capacity;
|
||||||
|
_logRemovals = logRemovals;
|
||||||
|
_lock = new();
|
||||||
|
_storage = new();
|
||||||
|
_cache = new(_capacity);
|
||||||
|
}
|
||||||
|
public int Count
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_lock) { return _storage.Count; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public int Capacity
|
||||||
|
{
|
||||||
|
set
|
||||||
|
{
|
||||||
|
Resize(value);
|
||||||
|
}
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_lock) { return _capacity; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private SalmonCacheValue? LockedRemoveLast()
|
||||||
|
{
|
||||||
|
var node = _storage.Last;
|
||||||
|
if (node == null)
|
||||||
|
return null;
|
||||||
|
var results = node.Value;
|
||||||
|
_storage.Remove(node);
|
||||||
|
_cache.Remove(results.Key);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
public int Resize(int capacity)
|
||||||
|
{
|
||||||
|
if (capacity <= 0) throw new ArgumentOutOfRangeException("capacity must be >= 0");
|
||||||
|
int removedCount = 0;
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_capacity = capacity;
|
||||||
|
while (_storage.Count > _capacity)
|
||||||
|
{
|
||||||
|
var removed = LockedRemoveLast();
|
||||||
|
if (removed != null)
|
||||||
|
{
|
||||||
|
if (_logRemovals)
|
||||||
|
Console.WriteLine($"SalmonCache::Resize removed key \"{removed.Key}\"");
|
||||||
|
removedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return removedCount;
|
||||||
|
}
|
||||||
|
public (V?, bool) Lookup(K key)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
LinkedListNode<SalmonCacheValue>? node;
|
||||||
|
if (_cache.TryGetValue(key, out node))
|
||||||
|
{
|
||||||
|
_storage.Remove(node);
|
||||||
|
_storage.AddFirst(node);
|
||||||
|
return (node.Value.Value, true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return (default, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public bool Insert(K key, V value)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
LinkedListNode<SalmonCacheValue>? node;
|
||||||
|
if (_cache.TryGetValue(key, out node))
|
||||||
|
{
|
||||||
|
_storage.Remove(node);
|
||||||
|
_storage.AddFirst(node);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
while (_storage.Count + 1 > _capacity)
|
||||||
|
{
|
||||||
|
var removed = LockedRemoveLast();
|
||||||
|
if (removed != null && _logRemovals)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"SalmonCache::Insert removed key \"{removed.Key}\"");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
node = _storage.AddFirst(new SalmonCacheValue(key, value));
|
||||||
|
_cache[key] = node;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public (V?, bool) Remove(K key)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
LinkedListNode<SalmonCacheValue>? node;
|
||||||
|
if (_cache.TryGetValue(key, out node))
|
||||||
|
{
|
||||||
|
var stored = node.Value;
|
||||||
|
_storage.Remove(node);
|
||||||
|
_cache.Remove(key);
|
||||||
|
return (stored.Value, true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return (default, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_storage.Clear();
|
||||||
|
_cache.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
10
SalmonCache/SalmonCache.csproj
Normal file
10
SalmonCache/SalmonCache.csproj
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
|
<LangVersion>12.0</LangVersion>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
84
SalmonCacheTest/SalmonCacheTest.cs
Normal file
84
SalmonCacheTest/SalmonCacheTest.cs
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
namespace SalmonCacheTest;
|
||||||
|
|
||||||
|
public class SalmonCacheTests
|
||||||
|
{
|
||||||
|
private readonly List<string> _words = new();
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
StreamReader reader = new("/usr/share/dict/words");
|
||||||
|
Dictionary<string, bool> Words = new();
|
||||||
|
var line = reader.ReadLine();
|
||||||
|
while (line != null)
|
||||||
|
{
|
||||||
|
var word = line.Trim().ToLower();
|
||||||
|
if (!Words.ContainsKey(word)) Words.Add(word, true);
|
||||||
|
}
|
||||||
|
reader.Close();
|
||||||
|
_words.AddRange(Words.Keys.ToArray());
|
||||||
|
_words.Sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestBasicInsertAndLookup()
|
||||||
|
{
|
||||||
|
var cache = new SalmonCache.SalmonCache<string, string>(_words.Count);
|
||||||
|
Assert.That(cache.Capacity, Is.EqualTo(_words.Count));
|
||||||
|
foreach (var word in _words)
|
||||||
|
{
|
||||||
|
var inserted = cache.Insert(word, word.ToUpper());
|
||||||
|
Assert.That(inserted, Is.True);
|
||||||
|
var (stored, found) = cache.Lookup(word);
|
||||||
|
Assert.That(found, Is.True);
|
||||||
|
Assert.That(stored, Is.EqualTo(word.ToUpper()));
|
||||||
|
}
|
||||||
|
Assert.That(cache.Count, Is.EqualTo(_words.Count));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestRemove()
|
||||||
|
{
|
||||||
|
var cache = new SalmonCache.SalmonCache<string, string>(_words.Count);
|
||||||
|
foreach (var word in _words)
|
||||||
|
{
|
||||||
|
Assert.That(cache.Insert(word, word.ToUpper()), Is.True);
|
||||||
|
}
|
||||||
|
Assert.That(cache.Count, Is.EqualTo(_words.Count));
|
||||||
|
foreach (var word in _words)
|
||||||
|
{
|
||||||
|
var (stored, found) = cache.Remove(word);
|
||||||
|
Assert.That(found, Is.True);
|
||||||
|
Assert.That(stored, Is.EqualTo(word.ToUpper()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestResizeAndClear()
|
||||||
|
{
|
||||||
|
var cache = new SalmonCache.SalmonCache<string, string>(_words.Count);
|
||||||
|
foreach (var word in _words)
|
||||||
|
{
|
||||||
|
Assert.That(cache.Insert(word, word.ToUpper()), Is.True);
|
||||||
|
}
|
||||||
|
Assert.That(cache.Count, Is.EqualTo(_words.Count));
|
||||||
|
var capacity = _words.Count / 2;
|
||||||
|
Assert.That(cache.Count, Is.EqualTo(capacity));
|
||||||
|
// the first (_words.Count - capacity) words will have been removed
|
||||||
|
for (var i = 0; i < _words.Count - capacity; i++)
|
||||||
|
{
|
||||||
|
var word = _words.ElementAt(i);
|
||||||
|
var (stored, found) = cache.Lookup(word);
|
||||||
|
Assert.That(found, Is.False);
|
||||||
|
Assert.That(stored, Is.Empty);
|
||||||
|
}
|
||||||
|
for (var i = _words.Count - capacity; i < _words.Count; i++)
|
||||||
|
{
|
||||||
|
var word = _words.ElementAt(i);
|
||||||
|
var (stored, found) = cache.Lookup(word);
|
||||||
|
Assert.That(found, Is.True);
|
||||||
|
Assert.That(stored, Is.EqualTo(word.ToUpper()));
|
||||||
|
}
|
||||||
|
cache.Clear();
|
||||||
|
Assert.That(cache.Count, Is.Empty);
|
||||||
|
}
|
||||||
|
}
|
27
SalmonCacheTest/SalmonCacheTest.csproj
Normal file
27
SalmonCacheTest/SalmonCacheTest.csproj
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.2" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||||
|
<PackageReference Include="NUnit" Version="4.2.2" />
|
||||||
|
<PackageReference Include="NUnit.Analyzers" Version="4.4.0" />
|
||||||
|
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="NUnit.Framework" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\SalmonCache\SalmonCache.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
Loading…
x
Reference in New Issue
Block a user