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