Skip to content

Commit 5953e57

Browse files
authored
Implement GetOrAdd (#62)
* getoradd * basic unit tests * lru order tests * use add instead of new * test classic dispose on addorupdate
1 parent 63e253e commit 5953e57

File tree

5 files changed

+174
-0
lines changed

5 files changed

+174
-0
lines changed

BitFaster.Caching.UnitTests/Lru/ClassicLruTests.cs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,5 +276,56 @@ public void WhenKeyDoesNotExistTryUpdateReturnsFalse()
276276

277277
lru.TryUpdate(2, "3").Should().BeFalse();
278278
}
279+
280+
[Fact]
281+
public void WhenKeyDoesNotExistAddOrUpdateAddsNewItem()
282+
{
283+
lru.AddOrUpdate(1, "1");
284+
285+
lru.TryGet(1, out var value).Should().BeTrue();
286+
value.Should().Be("1");
287+
}
288+
289+
[Fact]
290+
public void WhenKeyExistsAddOrUpdatUpdatesExistingItem()
291+
{
292+
lru.AddOrUpdate(1, "1");
293+
lru.AddOrUpdate(1, "2");
294+
295+
lru.TryGet(1, out var value).Should().BeTrue();
296+
value.Should().Be("2");
297+
}
298+
299+
[Fact]
300+
public void WhenKeyDoesNotExistAddOrUpdateMaintainsLruOrder()
301+
{
302+
lru.AddOrUpdate(1, "1");
303+
lru.AddOrUpdate(2, "2");
304+
lru.AddOrUpdate(3, "3");
305+
lru.AddOrUpdate(4, "4");
306+
307+
// verify first item added is removed
308+
lru.Count.Should().Be(3);
309+
lru.TryGet(1, out var value).Should().BeFalse();
310+
}
311+
312+
[Fact]
313+
public void WhenAddOrUpdateExpiresItemsTheyAreDisposed()
314+
{
315+
var lruOfDisposable = new ClassicLru<int, DisposableItem>(1, 3, EqualityComparer<int>.Default);
316+
317+
var items = Enumerable.Range(1, 4).Select(i => new DisposableItem()).ToList();
318+
319+
for (int i = 0; i < 4; i++)
320+
{
321+
lruOfDisposable.AddOrUpdate(i, items[i]);
322+
}
323+
324+
// first item is evicted and disposed
325+
items[0].IsDisposed.Should().BeTrue();
326+
327+
// all other items are not disposed
328+
items.Skip(1).All(i => i.IsDisposed == false).Should().BeTrue();
329+
}
279330
}
280331
}

BitFaster.Caching.UnitTests/Lru/ConcurrentLruTests.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,5 +392,36 @@ public void WhenKeyDoesNotExistTryUpdateReturnsFalse()
392392

393393
lru.TryUpdate(2, "3").Should().BeFalse();
394394
}
395+
396+
[Fact]
397+
public void WhenKeyDoesNotExistAddOrUpdateAddsNewItem()
398+
{
399+
lru.AddOrUpdate(1, "1");
400+
401+
lru.TryGet(1, out var value).Should().BeTrue();
402+
value.Should().Be("1");
403+
}
404+
405+
[Fact]
406+
public void WhenKeyExistsAddOrUpdatUpdatesExistingItem()
407+
{
408+
lru.AddOrUpdate(1, "1");
409+
lru.AddOrUpdate(1, "2");
410+
411+
lru.TryGet(1, out var value).Should().BeTrue();
412+
value.Should().Be("2");
413+
}
414+
415+
[Fact]
416+
public void WhenKeyDoesNotExistAddOrUpdateMaintainsLruOrder()
417+
{
418+
lru.AddOrUpdate(1, "1");
419+
lru.AddOrUpdate(2, "2");
420+
lru.AddOrUpdate(3, "3");
421+
lru.AddOrUpdate(4, "4");
422+
423+
lru.HotCount.Should().Be(3);
424+
lru.ColdCount.Should().Be(1); // items must have been enqueued and cycled for one of them to reach the cold queue
425+
}
395426
}
396427
}

BitFaster.Caching/ICache.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,5 +54,13 @@ public interface ICache<K, V>
5454
/// <param name="value">The new value.</param>
5555
/// <returns>true if the object was updated successfully; otherwise, false.</returns>
5656
bool TryUpdate(K key, V value);
57+
58+
/// <summary>
59+
/// Adds a key/value pair to the cache if the key does not already exist, or updates a key/value pair if the
60+
/// key already exists.
61+
/// </summary>
62+
/// <param name="key">The key of the element to update.</param>
63+
/// <param name="value">The new value.</param>
64+
void AddOrUpdate(K key, V value);
5765
}
5866
}

BitFaster.Caching/Lru/ClassicLru.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,58 @@ public bool TryUpdate(K key, V value)
206206
return false;
207207
}
208208

209+
///<inheritdoc/>
210+
///<remarks>Note: Updates to existing items do not affect LRU order. Added items are at the top of the LRU.</remarks>
211+
public void AddOrUpdate(K key, V value)
212+
{
213+
// first, try to update
214+
if (this.dictionary.TryGetValue(key, out var existingNode))
215+
{
216+
existingNode.Value.Value = value;
217+
return;
218+
}
219+
220+
// then try add
221+
var newNode = new LinkedListNode<LruItem>(new LruItem(key, value));
222+
223+
if (this.dictionary.TryAdd(key, newNode))
224+
{
225+
LinkedListNode<LruItem> first = null;
226+
227+
lock (this.linkedList)
228+
{
229+
if (linkedList.Count >= capacity)
230+
{
231+
first = linkedList.First;
232+
linkedList.RemoveFirst();
233+
}
234+
235+
linkedList.AddLast(newNode);
236+
}
237+
238+
// Remove from the dictionary outside the lock. This means that the dictionary at this moment
239+
// contains an item that is not in the linked list. If another thread fetches this item,
240+
// LockAndMoveToEnd will ignore it, since it is detached. This means we potentially 'lose' an
241+
// item just as it was about to move to the back of the LRU list and be preserved. The next request
242+
// for the same key will be a miss. Dictionary and list are eventually consistent.
243+
// However, all operations inside the lock are extremely fast, so contention is minimized.
244+
if (first != null)
245+
{
246+
dictionary.TryRemove(first.Value.Key, out var removed);
247+
248+
if (removed.Value.Value is IDisposable d)
249+
{
250+
d.Dispose();
251+
}
252+
}
253+
254+
return;
255+
}
256+
257+
// if both update and add failed there was a race, try again
258+
AddOrUpdate(key, value);
259+
}
260+
209261
// Thead A reads x from the dictionary. Thread B adds a new item. Thread A moves x to the end. Thread B now removes the new first Node (removal is atomic on both data structures).
210262
private void LockAndMoveToEnd(LinkedListNode<LruItem> node)
211263
{

BitFaster.Caching/Lru/TemplateConcurrentLru.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,38 @@ public bool TryUpdate(K key, V value)
242242
return false;
243243
}
244244

245+
///<inheritdoc/>
246+
///<remarks>Note: Updates to existing items do not affect LRU order. Added items are at the top of the LRU.</remarks>
247+
public void AddOrUpdate(K key, V value)
248+
{
249+
// first, try to update
250+
if (this.dictionary.TryGetValue(key, out var existing))
251+
{
252+
lock (existing)
253+
{
254+
if (!existing.WasRemoved)
255+
{
256+
existing.Value = value;
257+
return;
258+
}
259+
}
260+
}
261+
262+
// then try add
263+
var newItem = this.policy.CreateItem(key, value);
264+
265+
if (this.dictionary.TryAdd(key, newItem))
266+
{
267+
this.hotQueue.Enqueue(newItem);
268+
Interlocked.Increment(ref hotCount);
269+
Cycle();
270+
return;
271+
}
272+
273+
// if both update and add failed there was a race, try again
274+
AddOrUpdate(key, value);
275+
}
276+
245277
private void Cycle()
246278
{
247279
// There will be races when queue count == queue capacity. Two threads may each dequeue items.

0 commit comments

Comments
 (0)