Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
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
8 changes: 8 additions & 0 deletions HzMemoryCache/Diagnostics/TrashingDetector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace HzCache.Diagnostics
{
internal class TrashingDetector
{
public string? Checksum { get; set; } = null;
public int Counter { get; set; } = 0;
}
}
60 changes: 55 additions & 5 deletions HzMemoryCache/HzMemoryCache.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
#nullable enable

using HzCache.Diagnostics;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Collections;
using System.Collections.Concurrent;
Expand All @@ -10,8 +15,6 @@
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
using HzCache.Diagnostics;
using Microsoft.Extensions.Logging;

namespace HzCache
{
Expand All @@ -27,6 +30,7 @@ public partial class HzMemoryCache : IEnumerable<KeyValuePair<string, object>>,
private readonly ConcurrentDictionary<string, TTLValue> dictionary = new();
private readonly HzCacheMemoryLocker memoryLocker = new(new HzCacheMemoryLockerOptions());
private readonly HzCacheOptions options;
private readonly MemoryCache trashingDetectorCache = new(new MemoryCacheOptions());

//IDispisable members
private bool _disposedValue;
Expand Down Expand Up @@ -244,7 +248,7 @@ public bool Remove(string key, bool sendBackplaneNotification, Func<string, bool

public CacheStatistics GetStatistics()
{
return new CacheStatistics {Counts = Count, SizeInBytes = SizeInBytes};
return new CacheStatistics { Counts = Count, SizeInBytes = SizeInBytes };
}

/// <summary>
Expand Down Expand Up @@ -292,7 +296,7 @@ public static IPropagatorBlock<TIn, IList<TIn>> CreateBuffer<TIn>(TimeSpan timeS

private void StartUpdateChecksumAndNotify()
{
var options = new DataflowLinkOptions {PropagateCompletion = true};
var options = new DataflowLinkOptions { PropagateCompletion = true };
var actionBlock = new ActionBlock<IList<TTLValue>>(ttlValues =>
{
try
Expand Down Expand Up @@ -325,6 +329,8 @@ private bool RemoveItem(string key, CacheItemChangeType changeType, bool sendNot

if (result)
{
DetectCacheTrashing(key, ttlValue?.checksum);

result = dictionary.TryRemove(key, out ttlValue);
if (result)
{
Expand All @@ -340,6 +346,49 @@ private bool RemoveItem(string key, CacheItemChangeType changeType, bool sendNot
return result;
}

//Remember the value we are removing from the local cache, if the same value is being removed again and again in a short time frame, we are likely experiencing cache trashing.
private void DetectCacheTrashing(string key, string ttlValueChecksum)
{
try
{
if (!options.LogCacheTrashing)
{
return;
}

if (ttlValueChecksum == null || options.logger == null)
return;
if (!trashingDetectorCache.TryGetValue(key, out TrashingDetector? trashingDetector))
{
trashingDetectorCache.Set(key, new TrashingDetector
{
Checksum = ttlValueChecksum
}, options.TrashingWindow);
return;
}

if (trashingDetector.Checksum == ttlValueChecksum)
{
trashingDetector.Counter++;
}
else
{
trashingDetectorCache.Remove(key);
}

if (trashingDetector.Counter == options.TrashingLimit)
{
options.logger.LogWarning(
"Cache Trashing Detected: {Key} has been removed from local cache {TrashingLimit} times in the last {TrashingWindow}s. Checksum of existing value:{Checksum}",
key, options.TrashingLimit, options.TrashingWindow.TotalSeconds, ttlValueChecksum);
}
}
catch (Exception e)
{
options.logger?.LogError(e, "Error in DetectCacheTrashing {Key}", key);
}
}

private void NotifyItemChange(string key, CacheItemChangeType changeType, TTLValue ttlValue, byte[]? objectData = null, bool isPattern = false)
{
options.valueChangeListener(key, changeType, ttlValue, objectData, isPattern);
Expand All @@ -357,6 +406,7 @@ private void Dispose(bool disposing)
if (disposing)
{
cleanUpTimer.Dispose();
trashingDetectorCache.Dispose();
}

_disposedValue = true;
Expand Down Expand Up @@ -405,4 +455,4 @@ public static bool IsNullOrDefault<T>(T argument)
return false;
}
}
}
}
19 changes: 17 additions & 2 deletions HzMemoryCache/IHzCache.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#nullable enable

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
Expand Down Expand Up @@ -29,7 +30,6 @@ public enum NotificationType
Async, Sync, None
}


/// <summary>
/// The eviction policy to use for the cache.
/// LRU is "Least Recently Used" and FIFO is "First In First Out". Which is almost true.
Expand Down Expand Up @@ -92,6 +92,21 @@ public class HzCacheOptions
/// benefit of compression.
/// </summary>
public long compressionThreshold { get; set; } = Int64.MaxValue;

/// <summary>
/// Enables logging of cache trashing events, which occur when keys are removed frequently.
/// </summary>
public bool LogCacheTrashing { get; set; } = false;

/// <summary>
/// The time window during which cache removals are counted for trashing detection.
/// </summary>
public TimeSpan TrashingWindow { get; set; } = TimeSpan.FromSeconds(60);

/// <summary>
/// The number of removals within <see cref="TrashingWindow"/> that triggers a cache trashing warning.
/// </summary>
public int TrashingLimit { get; set; } = 3;
}

public interface IHzCache
Expand Down Expand Up @@ -182,6 +197,7 @@ public interface IHzCache
bool Remove(string key);

Task ClearAsync();

Task<bool> RemoveAsync(string key);
}

Expand All @@ -198,7 +214,6 @@ public interface IDetailedHzCache : IHzCache
/// </summary>
void Clear();


/// <summary>
/// Tries to remove item with the specified key, also returns the object removed in an "out" var
/// </summary>
Expand Down
Loading