Skip to content

Commit

Permalink
Merge pull request #2488 from JKamsker/fix-2471
Browse files Browse the repository at this point in the history
Fix LockRecursionException when doing FindById
  • Loading branch information
JKamsker authored Jun 4, 2024
2 parents 5a46966 + 74e9ef1 commit e01b1f3
Show file tree
Hide file tree
Showing 6 changed files with 219 additions and 35 deletions.
44 changes: 44 additions & 0 deletions LiteDB.Tests/Internals/Extensions_Test.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using LiteDB.Utils.Extensions;

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using Xunit;

namespace LiteDB.Tests.Internals;

public class Extensions_Test
{
// Asserts that chained IEnumerable<T>.OnDispose(()=> { }) calls the action on dispose, even when chained
[Fact]
public void EnumerableExtensions_OnDispose()
{
var disposed = false;
var disposed1 = false;
var enumerable = new[] { 1, 2, 3 }.OnDispose(() => disposed = true).OnDispose(() => disposed1 = true);

foreach (var item in enumerable)
{
// do nothing
}

Assert.True(disposed);
Assert.True(disposed1);
}

// tests IDisposable StartDisposable(this Stopwatch stopwatch)
[Fact]
public async Task StopWatchExtensions_StartDisposable()
{
var stopwatch = new System.Diagnostics.Stopwatch();
using (stopwatch.StartDisposable())
{
await Task.Delay(100);
}

Assert.True(stopwatch.ElapsedMilliseconds > 0);
}
}
97 changes: 97 additions & 0 deletions LiteDB.Tests/Issues/Issue2471_Test.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using FluentAssertions;

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

using Xunit;

namespace LiteDB.Tests.Issues;

public class Issue2471_Test
{
[Fact]
public void TestFragmentDB_FindByIDException()
{
using var db = new LiteDatabase(":memory:");
var collection = db.GetCollection<object>("fragtest");

var fragment = new object { };
var id = collection.Insert(fragment);

id.Should().BeGreaterThan(0);

var frag2 = collection.FindById(id);
frag2.Should().NotBeNull();

Action act = () => db.Checkpoint();

act.Should().NotThrow();
}

[Fact]
public void MultipleReadCleansUpTransaction()
{
using var database = new LiteDatabase(":memory:");

var collection = database.GetCollection("test");
collection.Insert(new BsonDocument { ["_id"] = 1 });

for (int i = 0; i < 500; i++)
{
collection.FindById(1);
}
}

#region Model

public class User
{
public int Id { get; set; }
public string Name { get; set; }
public int[] Phones { get; set; }
public List<Address> Addresses { get; set; }
}

public class Address
{
public string Street { get; set; }
}

#endregion Model

// Copied from IndexMultiKeyIndex, but this time we ensure that the lock is released by calling db.Checkpoint()
[Fact]
public void Ensure_Query_GetPlan_Releases_Lock()
{
using var db = new LiteDatabase(new MemoryStream());
var col = db.GetCollection<User>();

col.Insert(new User { Name = "John Doe", Phones = new int[] { 1, 3, 5 }, Addresses = new List<Address> { new Address { Street = "Av.1" }, new Address { Street = "Av.3" } } });
col.Insert(new User { Name = "Joana Mark", Phones = new int[] { 1, 4 }, Addresses = new List<Address> { new Address { Street = "Av.3" } } });

// create indexes
col.EnsureIndex(x => x.Phones);
col.EnsureIndex(x => x.Addresses.Select(z => z.Street));

// testing indexes expressions
var indexes = db.GetCollection("$indexes").FindAll().ToArray();

indexes[1]["expression"].AsString.Should().Be("$.Phones[*]");
indexes[2]["expression"].AsString.Should().Be("MAP($.Addresses[*]=>@.Street)");

// doing Phone query
var queryPhone = col.Query()
.Where(x => x.Phones.Contains(3));

var planPhone = queryPhone.GetPlan();

Action act = () => db.Checkpoint();

act.Should().NotThrow();
}
}
57 changes: 22 additions & 35 deletions LiteDB/Engine/Query/QueryExecutor.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
using System;
using LiteDB.Utils.Extensions;

using System;
using System.Collections.Generic;
using System.Linq;

using static LiteDB.Constants;

namespace LiteDB.Engine
Expand All @@ -22,14 +25,14 @@ internal class QueryExecutor
private readonly IEnumerable<BsonDocument> _source;

public QueryExecutor(
LiteEngine engine,
LiteEngine engine,
EngineState state,
TransactionMonitor monitor,
SortDisk sortDisk,
TransactionMonitor monitor,
SortDisk sortDisk,
DiskService disk,
EnginePragmas pragmas,
string collection,
Query query,
EnginePragmas pragmas,
string collection,
Query query,
IEnumerable<BsonDocument> source)
{
_engine = engine;
Expand Down Expand Up @@ -71,8 +74,17 @@ internal BsonDataReader ExecuteQuery(bool executionPlan)

transaction.OpenCursors.Add(_cursor);

var enumerable = RunQuery();

enumerable = enumerable.OnDispose(() => transaction.OpenCursors.Remove(_cursor));

if (isNew)
{
enumerable = enumerable.OnDispose(() => _monitor.ReleaseTransaction(transaction));
}

// return new BsonDataReader with IEnumerable source
return new BsonDataReader(RunQuery(), _collection, _state);
return new BsonDataReader(enumerable, _collection, _state);

IEnumerable<BsonDocument> RunQuery()
{
Expand All @@ -87,13 +99,6 @@ IEnumerable<BsonDocument> RunQuery()
yield return _query.Select.ExecuteScalar(_pragmas.Collation).AsDocument;
}

transaction.OpenCursors.Remove(_cursor);

if (isNew)
{
_monitor.ReleaseTransaction(transaction);
}

yield break;
}

Expand All @@ -108,14 +113,6 @@ IEnumerable<BsonDocument> RunQuery()
if (executionPlan)
{
yield return queryPlan.GetExecutionPlan();

transaction.OpenCursors.Remove(_cursor);

if (isNew)
{
_monitor.ReleaseTransaction(transaction);
}

yield break;
}

Expand All @@ -125,8 +122,8 @@ IEnumerable<BsonDocument> RunQuery()
// get current query pipe: normal or groupby pipe
var pipe = queryPlan.GetPipe(transaction, snapshot, _sortDisk, _pragmas, _disk.MAX_ITEMS_COUNT);

// start cursor elapsed timer
_cursor.Elapsed.Start();
// start cursor elapsed timer which stops on dispose
using var _ = _cursor.Elapsed.StartDisposable();

using (var enumerator = pipe.Pipe(nodes, queryPlan).GetEnumerator())
{
Expand Down Expand Up @@ -164,16 +161,6 @@ IEnumerable<BsonDocument> RunQuery()
}
}
}

// stop cursor elapsed
_cursor.Elapsed.Stop();

transaction.OpenCursors.Remove(_cursor);

if (isNew)
{
_monitor.ReleaseTransaction(transaction);
}
};
}

Expand Down
1 change: 1 addition & 0 deletions LiteDB/LiteDB.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
<SignAssembly Condition="'$(OS)'=='Windows_NT'">true</SignAssembly>
<AssemblyOriginatorKeyFile Condition="'$(Configuration)' == 'Release'">LiteDB.snk</AssemblyOriginatorKeyFile>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<LangVersion>8.0</LangVersion>
</PropertyGroup>

<!--
Expand Down
24 changes: 24 additions & 0 deletions LiteDB/Utils/Extensions/EnumerableExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;

namespace LiteDB.Utils.Extensions
{
internal static class EnumerableExtensions
{
// calls method on dispose
public static IEnumerable<T> OnDispose<T>(this IEnumerable<T> source, Action onDispose)
{
try
{
foreach (var item in source)
{
yield return item;
}
}
finally
{
onDispose();
}
}
}
}
31 changes: 31 additions & 0 deletions LiteDB/Utils/Extensions/StopWatchExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System;
using System.Diagnostics;

namespace LiteDB.Utils.Extensions
{
public static class StopWatchExtensions
{
// Start the stopwatch and returns an IDisposable that will stop the stopwatch when disposed
public static IDisposable StartDisposable(this Stopwatch stopwatch)
{
stopwatch.Start();
return new DisposableAction(stopwatch.Stop);
}

private class DisposableAction : IDisposable
{
private readonly Action _action;

public DisposableAction(Action action)
{
_action = action;
}

public void Dispose()
{
_action();
}
}
}

}

0 comments on commit e01b1f3

Please sign in to comment.