Skip to content

Commit 839b255

Browse files
Fix for #844 (#845)
Bug fix to go in immediate patch release
1 parent 74bcfcc commit 839b255

File tree

2 files changed

+200
-29
lines changed

2 files changed

+200
-29
lines changed
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
// Copyright (c) "Neo4j"
2+
// Neo4j Sweden AB [https://neo4j.com]
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+
// http://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+
using System;
17+
using System.Collections.Generic;
18+
using System.Threading.Tasks;
19+
using FluentAssertions;
20+
using Neo4j.Driver.Internal.HomeDbCaching;
21+
using Xunit;
22+
23+
namespace Neo4j.Driver.Tests.HomeDbCaching;
24+
25+
public class HomeDbCacheTests
26+
{
27+
[Fact]
28+
public void ShouldAddAndRetrieveCacheItem()
29+
{
30+
// Arrange
31+
var cache = new HomeDbCache();
32+
var key = new HomeDbCacheKey("test-key");
33+
var databaseName = "testDatabase";
34+
35+
// Act
36+
cache.AddOrUpdate(key, databaseName);
37+
var found = cache.TryGetCached(key, out var retrievedValue);
38+
39+
// Assert
40+
found.Should().BeTrue();
41+
retrievedValue.Should().Be(databaseName);
42+
}
43+
44+
[Fact]
45+
public void ShouldUpdateCacheItem()
46+
{
47+
// Arrange
48+
var cache = new HomeDbCache();
49+
var key = new HomeDbCacheKey("test-key");
50+
var initialDatabaseName = "initialDatabase";
51+
var updatedDatabaseName = "updatedDatabase";
52+
53+
// Act
54+
cache.AddOrUpdate(key, initialDatabaseName);
55+
cache.AddOrUpdate(key, updatedDatabaseName);
56+
var found = cache.TryGetCached(key, out var retrievedValue);
57+
58+
// Assert
59+
found.Should().BeTrue();
60+
retrievedValue.Should().Be(updatedDatabaseName);
61+
}
62+
63+
[Fact]
64+
public void ShouldReturnFalseIfKeyNotFound()
65+
{
66+
// Arrange
67+
var cache = new HomeDbCache();
68+
var key = new HomeDbCacheKey("test-key");
69+
70+
// Act
71+
var found = cache.TryGetCached(key, out var retrievedValue);
72+
73+
// Assert
74+
found.Should().BeFalse();
75+
retrievedValue.Should().BeNull();
76+
}
77+
78+
[Fact]
79+
public void ShouldPurgeOldItemsWhenThresholdExceeded()
80+
{
81+
// Arrange
82+
var cache = new HomeDbCache();
83+
for (int i = 0; i < 10_001; i++)
84+
{
85+
var key = new HomeDbCacheKey($"test-key-{i}");
86+
cache.AddOrUpdate(key, $"database-{i}");
87+
}
88+
89+
// Act
90+
var oldestKey = new HomeDbCacheKey("test-key-0");
91+
var found = cache.TryGetCached(oldestKey, out var retrievedValue);
92+
93+
// Assert
94+
found.Should().BeFalse();
95+
retrievedValue.Should().BeNull();
96+
}
97+
98+
[Fact]
99+
public void ShouldMoveAccessedItemToFront()
100+
{
101+
// Arrange
102+
var cache = new HomeDbCache();
103+
var key1 = new HomeDbCacheKey("test-key-1");
104+
var key2 = new HomeDbCacheKey("test-key-2");
105+
cache.AddOrUpdate(key1, "database-1");
106+
cache.AddOrUpdate(key2, "database-2");
107+
108+
// Act
109+
cache.TryGetCached(key1, out _); // Access key1
110+
cache.AddOrUpdate(new HomeDbCacheKey("test-key-3"), "database-3");
111+
112+
// Assert
113+
cache.TryGetCached(key1, out var value1).Should().BeTrue();
114+
value1.Should().Be("database-1");
115+
}
116+
117+
[Fact]
118+
public async Task ShouldBeThreadSafe()
119+
{
120+
// Arrange
121+
var cache = new HomeDbCache();
122+
var tasks = new List<Task>();
123+
var random = new Random();
124+
125+
// Act
126+
for (int i = 0; i < 4; i++)
127+
{
128+
var task = Task.Run(
129+
() =>
130+
{
131+
for (int j = 0; j < 250; j++)
132+
{
133+
var key = new HomeDbCacheKey($"key-{random.Next(0, 50)}");
134+
var value = $"database-{random.Next(0, 50)}";
135+
136+
// Randomly perform one of the operations
137+
switch (random.Next(0, 3))
138+
{
139+
case 0: // Add or update
140+
cache.AddOrUpdate(key, value);
141+
break;
142+
143+
case 1: // Try to retrieve
144+
cache.TryGetCached(key, out _);
145+
break;
146+
147+
case 2: // Remove and re-add
148+
cache.AddOrUpdate(key, value);
149+
cache.TryGetCached(key, out _);
150+
break;
151+
}
152+
}
153+
});
154+
155+
tasks.Add(task);
156+
}
157+
158+
await Task.WhenAll(tasks);
159+
160+
// Assert
161+
// If no exceptions are thrown, the test passes
162+
}
163+
}

Neo4j.Driver/Neo4j.Driver/Internal/HomeDbCaching/HomeDbCache.cs

Lines changed: 37 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -19,54 +19,62 @@ public CacheItem(HomeDbCacheKey key, string databaseName)
1919
}
2020
}
2121

22+
private readonly object _lock = new();
2223
private readonly LinkedList<CacheItem> _cachedItems = new();
2324
private readonly Dictionary<HomeDbCacheKey, LinkedListNode<CacheItem>> _cacheLookup = new();
2425

2526
public bool TryGetCached(HomeDbCacheKey key, out string value)
2627
{
27-
value = null;
28-
var found = _cacheLookup.TryGetValue(key, out var node);
29-
if (!found)
28+
lock (_lock)
3029
{
31-
return false;
32-
}
33-
34-
_cachedItems.Remove(node);
35-
_cachedItems.AddFirst(node);
36-
value = node.Value.DatabaseName;
37-
return true;
30+
value = null;
31+
var found = _cacheLookup.TryGetValue(key, out var node);
32+
if (!found)
33+
{
34+
return false;
35+
}
3836

37+
_cachedItems.Remove(node);
38+
_cachedItems.AddFirst(node);
39+
value = node.Value.DatabaseName;
40+
return true;
41+
}
3942
}
4043

4144
public void AddOrUpdate(HomeDbCacheKey key, string value)
4245
{
43-
LinkedListNode<CacheItem> node;
44-
// if we already have an entry
45-
if (_cacheLookup.TryGetValue(key, out node))
46-
{
47-
_cachedItems.Remove(node);
48-
}
49-
else
46+
lock (_lock)
5047
{
51-
node = new LinkedListNode<CacheItem>(new CacheItem(key, value));
52-
_cacheLookup[key] = node;
53-
}
48+
// if we already have an entry
49+
if (_cacheLookup.TryGetValue(key, out var node))
50+
{
51+
_cachedItems.Remove(node);
52+
}
53+
else
54+
{
55+
node = new LinkedListNode<CacheItem>(new CacheItem(key, value));
56+
_cacheLookup[key] = node;
57+
}
5458

55-
node.Value.DatabaseName = value;
56-
_cachedItems.AddFirst(node);
57-
PurgeOldItems();
59+
node.Value.DatabaseName = value;
60+
_cachedItems.AddFirst(node);
61+
PurgeOldItems();
62+
}
5863
}
5964

6065
private void PurgeOldItems()
6166
{
62-
if (_cachedItems.Count < PurgeThreshold)
67+
lock (_lock)
6368
{
64-
return;
65-
}
69+
if (_cachedItems.Count < PurgeThreshold)
70+
{
71+
return;
72+
}
6673

67-
for (var i = 0; i < PurgeAmount; i++)
68-
{
69-
RemoveLastItem();
74+
for (var i = 0; i < PurgeAmount; i++)
75+
{
76+
RemoveLastItem();
77+
}
7078
}
7179
}
7280

0 commit comments

Comments
 (0)