From f0571a39293b91c991f8c6bca075ea8690423932 Mon Sep 17 00:00:00 2001 From: Oleksii Datsiuk <58850773+oleksii-datsiuk@users.noreply.github.com> Date: Thu, 16 May 2024 19:45:29 +0300 Subject: [PATCH 1/7] Prevent using disposed snapshot --- LiteDB/Engine/Services/SnapShot.cs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/LiteDB/Engine/Services/SnapShot.cs b/LiteDB/Engine/Services/SnapShot.cs index 2f52af221..f8deaeba7 100644 --- a/LiteDB/Engine/Services/SnapShot.cs +++ b/LiteDB/Engine/Services/SnapShot.cs @@ -33,6 +33,8 @@ internal class Snapshot : IDisposable // local page cache - contains only pages about this collection (but do not contains CollectionPage - use this.CollectionPage) private readonly Dictionary _localPages = new Dictionary(); + private bool _disposed; + // expose public LockMode Mode => _mode; public string CollectionName => _collectionName; @@ -89,6 +91,8 @@ public Snapshot( /// public IEnumerable GetWritablePages(bool dirty, bool includeCollectionPage) { + ENSURE(!_disposed, "the snapshot is disposed"); + // if snapshot is read only, just exit if (_mode == LockMode.Read) yield break; @@ -110,6 +114,8 @@ public IEnumerable GetWritablePages(bool dirty, bool includeCollection /// public void Clear() { + ENSURE(!_disposed, "the snapshot is disposed"); + // release pages only if snapshot are read only if (_mode == LockMode.Read) { @@ -128,9 +134,16 @@ public void Clear() /// public void Dispose() { + if (_disposed) + { + return; + } + // release all data/index pages this.Clear(); + _disposed = true; + // release collection page (in read mode) if (_mode == LockMode.Read && _collectionPage != null) { @@ -160,6 +173,7 @@ public T GetPage(uint pageID) public T GetPage(uint pageID, out FileOrigin origin, out long position, out int walVersion) where T : BasePage { + ENSURE(!_disposed, "the snapshot is disposed"); ENSURE(pageID <= _header.LastPageID, "request page must be less or equals lastest page in data file"); // check for header page (return header single instance) @@ -259,6 +273,8 @@ private T ReadPage(uint pageID, out FileOrigin origin, out long position, out /// public DataPage GetFreeDataPage(int bytesLength) { + ENSURE(!_disposed, "the snapshot is disposed"); + var length = bytesLength + BasePage.SLOT_SIZE; // add +4 bytes for footer slot // get minimum slot to check for free page. Returns -1 if need NewPage @@ -292,6 +308,8 @@ public DataPage GetFreeDataPage(int bytesLength) /// public IndexPage GetFreeIndexPage(int bytesLength, ref uint freeIndexPageList) { + ENSURE(!_disposed, "the snapshot is disposed"); + IndexPage page; // if there is not page in list pages, create new page @@ -318,6 +336,7 @@ public IndexPage GetFreeIndexPage(int bytesLength, ref uint freeIndexPageList) public T NewPage() where T : BasePage { + ENSURE(!_disposed, "the snapshot is disposed"); ENSURE(_collectionPage == null, typeof(T) == typeof(CollectionPage), "if no collection page defined yet, must be first request"); ENSURE(typeof(T) == typeof(CollectionPage), _collectionPage == null, "there is no new collection page if page already exists"); @@ -392,6 +411,8 @@ public T NewPage() /// public void AddOrRemoveFreeDataList(DataPage page) { + ENSURE(!_disposed, "the snapshot is disposed"); + var newSlot = DataPage.FreeIndexSlot(page.FreeBytes); var initialSlot = page.PageListSlot; @@ -423,6 +444,8 @@ public void AddOrRemoveFreeDataList(DataPage page) /// public void AddOrRemoveFreeIndexList(IndexPage page, ref uint startPageID) { + ENSURE(!_disposed, "the snapshot is disposed"); + var newSlot = IndexPage.FreeIndexSlot(page.FreeBytes); var isOnList = page.PageListSlot == 0; var mustKeep = newSlot == 0; @@ -567,6 +590,8 @@ private void DeletePage(T page) /// public void DropCollection(Action safePoint) { + ENSURE(!_disposed, "the snapshot is disposed"); + var indexer = new IndexService(this, _header.Pragmas.Collation, _disk.MAX_ITEMS_COUNT); // CollectionPage will be last deleted page (there is no NextPageID from CollectionPage) From 5a4696606b4fc58554f01d725f621bd3e553fd18 Mon Sep 17 00:00:00 2001 From: Mauricio David Date: Mon, 27 May 2024 01:00:24 -0300 Subject: [PATCH 2/7] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 7a1796d50..264cd7876 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ [![Build status](https://ci.appveyor.com/api/projects/status/sfe8he0vik18m033?svg=true)](https://ci.appveyor.com/project/mbdavid/litedb) +--- +#### Read about LiteDB Project News: https://github.com/mbdavid/LiteDB/issues/2486 --- LiteDB is a small, fast and lightweight .NET NoSQL embedded database. From ece59bbc1ab91d38ac26dd5e1614f57e6cc538fd Mon Sep 17 00:00:00 2001 From: JKamsker Date: Sat, 1 Jun 2024 19:03:34 +0200 Subject: [PATCH 3/7] Fixed #2471, #2435, #2483 : Releasing read lock when done reading --- LiteDB.Tests/Issues/Issue2471_Test.cs | 49 +++++++++++++++++++ LiteDB/Engine/Query/QueryExecutor.cs | 28 +++++------ .../Utils/Extensions/EnumerableExtensions.cs | 27 ++++++++++ 3 files changed, 87 insertions(+), 17 deletions(-) create mode 100644 LiteDB.Tests/Issues/Issue2471_Test.cs create mode 100644 LiteDB/Utils/Extensions/EnumerableExtensions.cs diff --git a/LiteDB.Tests/Issues/Issue2471_Test.cs b/LiteDB.Tests/Issues/Issue2471_Test.cs new file mode 100644 index 000000000..c6d9ff1a2 --- /dev/null +++ b/LiteDB.Tests/Issues/Issue2471_Test.cs @@ -0,0 +1,49 @@ +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("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); + } + } +} \ No newline at end of file diff --git a/LiteDB/Engine/Query/QueryExecutor.cs b/LiteDB/Engine/Query/QueryExecutor.cs index 10d8e034b..522eb44af 100644 --- a/LiteDB/Engine/Query/QueryExecutor.cs +++ b/LiteDB/Engine/Query/QueryExecutor.cs @@ -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 @@ -71,8 +74,14 @@ internal BsonDataReader ExecuteQuery(bool executionPlan) transaction.OpenCursors.Add(_cursor); + var enumerable = RunQuery(); + 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 RunQuery() { @@ -89,11 +98,6 @@ IEnumerable RunQuery() transaction.OpenCursors.Remove(_cursor); - if (isNew) - { - _monitor.ReleaseTransaction(transaction); - } - yield break; } @@ -111,11 +115,6 @@ IEnumerable RunQuery() transaction.OpenCursors.Remove(_cursor); - if (isNew) - { - _monitor.ReleaseTransaction(transaction); - } - yield break; } @@ -169,11 +168,6 @@ IEnumerable RunQuery() _cursor.Elapsed.Stop(); transaction.OpenCursors.Remove(_cursor); - - if (isNew) - { - _monitor.ReleaseTransaction(transaction); - } }; } diff --git a/LiteDB/Utils/Extensions/EnumerableExtensions.cs b/LiteDB/Utils/Extensions/EnumerableExtensions.cs new file mode 100644 index 000000000..b79c1af12 --- /dev/null +++ b/LiteDB/Utils/Extensions/EnumerableExtensions.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LiteDB.Utils.Extensions +{ + internal static class EnumerableExtensions + { + // calls method on dispose + public static IEnumerable OnDispose(this IEnumerable source, Action onDispose) + { + try + { + foreach (var item in source) + { + yield return item; + } + } + finally + { + onDispose(); + } + } + } +} \ No newline at end of file From 72b1ac28d29100d0fcb380023dba4cff89015cd4 Mon Sep 17 00:00:00 2001 From: JKamsker <11245306+JKamsker@users.noreply.github.com> Date: Tue, 4 Jun 2024 17:48:42 +0200 Subject: [PATCH 4/7] Bump langversion to be able to use newer language features while all TargetFrameworks still build and run --- LiteDB/LiteDB.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/LiteDB/LiteDB.csproj b/LiteDB/LiteDB.csproj index 721395e91..4105be0f3 100644 --- a/LiteDB/LiteDB.csproj +++ b/LiteDB/LiteDB.csproj @@ -28,6 +28,7 @@ true LiteDB.snk true + 8.0