Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed memory leak due to infinite static caches #2438

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 10 additions & 4 deletions LiteDB/Document/Expression/BsonExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using LiteDB.Utils;
using static LiteDB.Constants;

namespace LiteDB
Expand Down Expand Up @@ -279,8 +280,13 @@ internal BsonValue ExecuteScalar(IEnumerable<BsonDocument> source, BsonDocument

#region Static method

private static readonly ConcurrentDictionary<string, BsonExpressionEnumerableDelegate> _cacheEnumerable = new ConcurrentDictionary<string, BsonExpressionEnumerableDelegate>();
private static readonly ConcurrentDictionary<string, BsonExpressionScalarDelegate> _cacheScalar = new ConcurrentDictionary<string, BsonExpressionScalarDelegate>();
private static readonly SlidingCache<string, BsonExpressionEnumerableDelegate> _cacheEnumerable = new SlidingCache<string, BsonExpressionEnumerableDelegate>(TimeSpan.FromMinutes(1));
private static readonly SlidingCache<string, BsonExpressionScalarDelegate> _cacheScalar = new SlidingCache<string, BsonExpressionScalarDelegate>(TimeSpan.FromMinutes(1));

/// <summary>
/// Gets or sets how long a cache entry can be inactive (e.g. not accessed) before it will be removed.
/// </summary>
public static TimeSpan? CacheSlidingExpiration { get; set; }

/// <summary>
/// Parse string and create new instance of BsonExpression - can be cached
Expand Down Expand Up @@ -363,7 +369,7 @@ internal static void Compile(BsonExpression expr, ExpressionContext context)
var lambda = System.Linq.Expressions.Expression.Lambda<BsonExpressionScalarDelegate>(expr.Expression, context.Source, context.Root, context.Current, context.Collation, context.Parameters);

return lambda.Compile();
});
}, CacheSlidingExpiration);

expr._funcScalar = cached;
}
Expand All @@ -374,7 +380,7 @@ internal static void Compile(BsonExpression expr, ExpressionContext context)
var lambda = System.Linq.Expressions.Expression.Lambda<BsonExpressionEnumerableDelegate>(expr.Expression, context.Source, context.Root, context.Current, context.Collation, context.Parameters);

return lambda.Compile();
});
}, CacheSlidingExpiration);

expr._funcEnumerable = cached;
}
Expand Down
4 changes: 2 additions & 2 deletions LiteDB/LiteDB.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
<DefineConstants>TRACE;DEBUG</DefineConstants>
</PropertyGroup>

<PropertyGroup Condition="'$(TargetFramework)' == 'net45'">
<PropertyGroup Condition="'$(TargetFramework)' == 'net4.5'">
<DefineConstants>HAVE_SHA1_MANAGED;HAVE_APP_DOMAIN;HAVE_PROCESS;HAVE_ENVIRONMENT</DefineConstants>
</PropertyGroup>

Expand All @@ -56,7 +56,7 @@
<None Include="..\icon_64x64.png" Pack="true" PackagePath="\" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net45'">
<ItemGroup Condition="'$(TargetFramework)' == 'net4.5'">
<Reference Include="System" />
<Reference Include="System.Runtime" />
</ItemGroup>
Expand Down
70 changes: 70 additions & 0 deletions LiteDB/Utils/SlidingCache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using System;
using System.Collections.Concurrent;
using System.Threading;

namespace LiteDB.Utils
{
public class SlidingCache<TKey, TValue> : IDisposable
{
private readonly ConcurrentDictionary<TKey, CacheItem<TValue>> _cache = new ConcurrentDictionary<TKey, CacheItem<TValue>>();
private readonly Timer _timer;

public SlidingCache(TimeSpan expirationScanFrequency)
{
_timer = new Timer(OnTimerCallback, null, expirationScanFrequency, expirationScanFrequency);
}

public void Dispose()
{
_timer?.Dispose();
}

public TValue GetOrAdd(TKey key, Func<TKey, TValue> valueFactory, TimeSpan? slidingExpiration = default)
{
if (key == null)
throw new ArgumentNullException(nameof(key));
if (valueFactory == null)
throw new ArgumentNullException(nameof(valueFactory));

var cacheItem = _cache.GetOrAdd(key, k => new CacheItem<TValue>(valueFactory(k), DateTime.UtcNow, slidingExpiration));
cacheItem.LastAccessed = DateTime.UtcNow;

return cacheItem.Value;
}

private void OnTimerCallback(object state)
{
var now = DateTime.UtcNow;
foreach (var pair in _cache)
{
var key = pair.Key;
var item = pair.Value;
if (item.Expired(now))
_cache.TryRemove(key, out _);
}
}

private class CacheItem<T>
{
private readonly TimeSpan? _slidingExpiration;

public CacheItem(T value, DateTime lastAccessed, TimeSpan? slidingExpiration)
{
_slidingExpiration = slidingExpiration;
Value = value;
LastAccessed = lastAccessed;
}

public T Value { get; }
public DateTime LastAccessed { get; set; }

public bool Expired(DateTime now)
{
if (_slidingExpiration == null)
return false;

return now - LastAccessed > _slidingExpiration;
}
}
}
}