Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
8e8ea22
LRU cache first part.
emreyigit Feb 21, 2023
0cf9517
LRU cache implemented.
emreyigit Feb 22, 2023
653bcfc
Integrated sql service and partition aware feature.
emreyigit Feb 24, 2023
4350283
Better eviction and benchmark.
emreyigit Feb 28, 2023
a54e29c
Merge branch 'master' into sql-partition-aware
emreyigit Mar 1, 2023
2564033
Integrations tests added and protocol files regenerated.
emreyigit Mar 17, 2023
1d6b76f
Merge branch 'master' into sql-partition-aware
emreyigit Mar 17, 2023
9e3794c
Option name corrected.
emreyigit Mar 17, 2023
dba8aa7
Fix failing tests.
emreyigit Mar 20, 2023
3efefd8
Update src/Hazelcast.Net/Core/ReadOptimizedLruCache.cs
emreyigit Mar 25, 2023
f4e9dbb
Update src/Hazelcast.Net/Core/ReadOptimizedLruCache.cs
emreyigit Mar 25, 2023
08c9b81
Update src/Hazelcast.Net/Core/ReadOptimizedLruCache.cs
emreyigit Mar 25, 2023
cc2f6fa
Update src/Hazelcast.Net/Core/ReadOptimizedLruCache.cs
emreyigit Mar 25, 2023
49ea63b
Update src/Hazelcast.Net/Core/ReadOptimizedLruCache.cs
emreyigit Mar 25, 2023
2a09ccb
Update src/Hazelcast.Net/Core/ReadOptimizedLruCache.cs
emreyigit Mar 25, 2023
662ebf7
Update src/Hazelcast.Net/Core/ReadOptimizedLruCache.cs
emreyigit Mar 25, 2023
bd1d896
Update src/Hazelcast.Net/Core/ReadOptimizedLruCache.cs
emreyigit Mar 25, 2023
7391256
Review changes.
emreyigit Mar 27, 2023
54480b6
Review changes.
emreyigit Mar 27, 2023
a91d960
Review changes+latest protocol update.
emreyigit Apr 4, 2023
861f234
Merge branch 'master' into sql-partition-aware
emreyigit Apr 4, 2023
9a8ab57
Fix build.
emreyigit Apr 4, 2023
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
78 changes: 78 additions & 0 deletions src/Hazelcast.Net.Benchmarks/LruCacheEviction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright (c) 2008-2022, Hazelcast, Inc. All Rights Reserved.
//
// 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
//
// http://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;
using System.Collections.Generic;
using System.Linq;
using BenchmarkDotNet.Attributes;

namespace Hazelcast.Benchmarks;

/*
| Method | Size | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated |
|----------------- |------- |--------------:|------------:|------------:|---------:|---------:|---------:|-----------:|
| EvictWithMinHeap | 100 | 2.403 us | 0.0055 us | 0.0046 us | 0.1335 | - | - | 1.09 KB |
| EvictWithSort | 100 | 4.318 us | 0.0207 us | 0.0194 us | 0.2365 | - | - | 1.95 KB |
| EvictWithMinHeap | 1000 | 36.793 us | 0.1643 us | 0.1537 us | 0.5493 | - | - | 4.88 KB |
| EvictWithSort | 1000 | 85.071 us | 0.5419 us | 0.5069 us | 2.0752 | - | - | 17.06 KB |
| EvictWithMinHeap | 10000 | 517.023 us | 2.6572 us | 2.4855 us | 3.9063 | - | - | 36.67 KB |
| EvictWithSort | 10000 | 1,085.996 us | 11.2326 us | 10.5069 us | 19.5313 | 1.9531 | - | 168.24 KB |
| EvictWithMinHeap | 100000 | 6,507.747 us | 55.3840 us | 51.8063 us | 54.6875 | 39.0625 | 39.0625 | 477.66 KB |
| EvictWithSort | 100000 | 14,858.008 us | 168.6865 us | 149.5362 us | 484.3750 | 484.3750 | 484.3750 | 1680.26 KB |
*/


public class LruCacheEviction
{
[Params(100, 1000, 10_000, 100_000)] public int Size { get; set; }

public int NumRemove { get; set; }

private List<int> _numbers;

[GlobalSetup]
public void Setup()
{
var rnd = new Random();
NumRemove = Size / 10;
_numbers = Enumerable.Repeat(0, Size + NumRemove).Select(p => rnd.Next(1_000_000)).ToList();
}


[Benchmark]
public void EvictWithMinHeap()
{
var q = new PriorityQueue<int, int>();

foreach (var val in _numbers)
{
q.Enqueue(val, val);
}

for (int i = 0; i < NumRemove; i++)
{
q.Dequeue();
}

var cutOff = q.Dequeue();

var _ = _numbers.Where(p => p >= cutOff).OrderBy(p => p).ToList();
}

[Benchmark]
public void EvictWithSort()
{
var _= _numbers.OrderBy(p => p).Skip(NumRemove).ToList();
}
}
223 changes: 223 additions & 0 deletions src/Hazelcast.Net.Tests/Core/ReadOptimizedLRUCacheTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
// Copyright (c) 2008-2022, Hazelcast, Inc. All Rights Reserved.
//
// 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
//
// http://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;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using Hazelcast.Core;
using Hazelcast.Models;

namespace Hazelcast.Tests.Core;

public class ReadOptimizedLruCacheTests
{
[Test]
public void TestWrongCapacityThrows()
{
Assert.Throws<ArgumentException>(() => new ReadOptimizedLruCache<int, int>(-1, 0));
Assert.Throws<ArgumentException>(() => new ReadOptimizedLruCache<int, int>(1, 0));
Assert.Throws<ArgumentException>(() => new ReadOptimizedLruCache<int, int>(0, -1));
Assert.Throws<ArgumentException>(() => new ReadOptimizedLruCache<int, int>(10, 9));
var correct = new ReadOptimizedLruCache<int, int>(10, 15);
}

[Test]
public void TestAdd()
{
var cache = new ReadOptimizedLruCache<string, string>(10, 15);

cache.Add("1", "1");
cache.Add("2", null);

Assert.Throws<ArgumentNullException>(() => cache.Add(null, "1"));
}

[Test]
public async Task TestAddAndGetRefreshEntry()
{
var cache = new ReadOptimizedLruCache<int, int>(10, 15);

cache.Add(1, 1);
cache.Cache.TryGetValue(1, out var entry);
var firstTouch = entry.LastTouch;
await Task.Delay(100);
var keyExist = cache.TryGetValue(1, out var _);

Assert.True(keyExist);

var secondTouch = entry.LastTouch;

Assert.Greater(secondTouch, firstTouch);
}

[Test]
public async Task TestEviction()
{
var capacity = 10;
var threshold = 15;
var cache = new ReadOptimizedLruCache<int, int>(capacity, threshold);

for (var i = 0; i < threshold; i++)
{
cache.Add(i, i);
await Task.Delay(10);
}


Assert.AreEqual(cache.Cache.Count, threshold);
// Last stroke to break camel's back
cache.Add(15, 15);
// The cache size exceeds the threshold, so remove the least recent used # of (threshold - capacity) items
Assert.AreEqual(capacity, cache.Cache.Count);
}

[Test]
public void TestOperationsThrowAfterDispose()
{
var cache = new ReadOptimizedLruCache<int, int>(5, 10);
cache.Dispose();

Assert.Throws<ObjectDisposedException>(() => cache.Add(1, 1));
Assert.Throws<ObjectDisposedException>(() => cache.TryGetValue(1, out _));
//doesn't throw.
cache.Dispose();
}

[Test]
public async Task TestEvictionHappensWithSynchronization()
{
var capacity = 10;
var threshold = 15;
var cache = new ReadOptimizedLruCache<int, int>(capacity, threshold);
var slim = new SemaphoreSlim(0, 2);

for (var i = 0; i < threshold; i++)
{
cache.Add(i, i);
await Task.Delay(10);
}


Assert.AreEqual(cache.Cache.Count, threshold);

var taskAddAndEvict = Task.Run(() =>
{
slim.Wait();
cache.Add(15, 15);
slim.Release();
});

Assert.AreNotEqual(TaskStatus.RanToCompletion, taskAddAndEvict.Status);

// Add one more item, expect that first task `taskAddAndEvict` will remove the old ones,
// and `taskAddAndEvict2` won't do eviction since it won't be necessary.
var taskAddAndEvict2 = Task.Run(() =>
{
slim.Wait();
cache.Add(16, 16);
slim.Release();
});

Assert.AreNotEqual(TaskStatus.RanToCompletion, taskAddAndEvict2.Status);

Thread.Sleep(500);
slim.Release(2);
await Task.WhenAll(taskAddAndEvict, taskAddAndEvict2);

// Cache size will shrink eventually.
Assert.LessOrEqual(cache.Cache.Count, threshold);
}

[Test]
public async Task TestEvictionIsCorrect()
{
var capacity = 10;
var threshold = 15;
var cache = new ReadOptimizedLruCache<int, int>(capacity, threshold);

// +1 to escape from default value -0- of int.
for (var i = 1; i < threshold + 1; i++)
cache.Add(i, i);

Assert.AreEqual(cache.Cache.Count, threshold);

// Refresh some of the entries.
for (var i = 1; i < capacity + 1; i++)
{
await Task.Delay(10);
cache.TryGetValue(i, out _);
}

await Task.Delay(10);
//Note: Here some delays happened to assert exact evicted items, specially in the head and tail.
//In real world, it doesn't matter since we already know that eviction behaves correct.

// Keys between [2,10] and 16 will stay, rest will be evicted.
cache.Add(16, 16);

Assert.LessOrEqual(cache.Cache.Count, threshold);

// Notice that although key 1 refreshed key 16 is more recent than key 1.
for (var i = 2; i < capacity + 1; i++)
{
var result = cache.TryGetValue(i, out var val);
Assert.AreEqual(i, val);
Assert.True(result);
}

Assert.True(cache.TryGetValue(16, out var val15));
Assert.AreEqual(16, val15);
Assert.False(cache.TryGetValue(13, out _));
}

[Test]
public void TestCacheGetCorrect()
{
var capacity = 10;
var threshold = 15;
var cache = new ReadOptimizedLruCache<int, int>(capacity, threshold);

cache.Add(1, 1);
Assert.True(cache.TryGetValue(1, out var val));
Assert.AreEqual(1, val);

Assert.False(cache.TryGetValue(2, out var defaultVal));
Assert.AreEqual(default(int), defaultVal);
}

[Test]
public async Task TestTimeBasedEntryWorks()
{
var entry = new TimeBasedEntry<int>(1);
await Task.Delay(10);
var firstTouch = entry.LastTouch;
Assert.Greater(Clock.Milliseconds, firstTouch);
entry.Touch();
Assert.Greater(entry.LastTouch, firstTouch);
}

[Test]
public void TestTryRemove()
{
var cache = new ReadOptimizedLruCache<string, string>(1, 2);

Assert.Throws<ArgumentNullException>(() => cache.TryRemove(null, out _));
Assert.True(!cache.TryRemove("1", out var val) && val == default);
cache.Add("1", "1");
Assert.True(cache.TryRemove("1", out var val2) && val2 == "1");
cache.Dispose();
Assert.Throws<ObjectDisposedException>(() => cache.TryRemove("1", out _));
}
}
Loading