From ca7037f257d95c296230a85497189ab7f3ed04c5 Mon Sep 17 00:00:00 2001 From: ejdre Date: Thu, 30 Oct 2025 14:27:19 +0000 Subject: [PATCH 1/2] Updated the transaction finalizer to not try to release a lock that can never be successful --- LiteDB/Engine/Services/TransactionMonitor.cs | 25 +++++++++++++------- LiteDB/Engine/Services/TransactionService.cs | 5 +--- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/LiteDB/Engine/Services/TransactionMonitor.cs b/LiteDB/Engine/Services/TransactionMonitor.cs index ebfee2ce3..a1c2560a0 100644 --- a/LiteDB/Engine/Services/TransactionMonitor.cs +++ b/LiteDB/Engine/Services/TransactionMonitor.cs @@ -1,8 +1,6 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using System.Runtime.InteropServices; using System.Threading; using static LiteDB.Constants; @@ -105,9 +103,10 @@ public TransactionService GetTransaction(bool create, bool queryOnly, out bool i } /// - /// Release current thread transaction + /// Dispose and remove transaction from monitor + /// without releasing thread lock /// - public void ReleaseTransaction(TransactionService transaction) + public bool RemoveTransaction(TransactionService transaction) { // dispose current transaction transaction.Dispose(); @@ -123,8 +122,16 @@ public void ReleaseTransaction(TransactionService transaction) _freePages += transaction.MaxTransactionSize; // check if current thread contains more query transactions - keepLocked = _transactions.Values.Any(x => x.ThreadID == Environment.CurrentManagedThreadId); + return keepLocked = _transactions.Values.Any(x => x.ThreadID == Environment.CurrentManagedThreadId); } + } + + /// + /// Release current thread transaction + /// + public void ReleaseTransaction(TransactionService transaction) + { + var keepLocked = RemoveTransaction(transaction); // unlock thread-transaction only if there is no more transactions if (keepLocked == false) @@ -150,7 +157,7 @@ public TransactionService GetThreadTransaction() { lock (_transactions) { - return + return _slot.Value ?? _transactions.Values.FirstOrDefault(x => x.ThreadID == Environment.CurrentManagedThreadId); } @@ -191,7 +198,7 @@ private int GetInitialSize() /// private bool TryExtend(TransactionService trans) { - lock(_transactions) + lock (_transactions) { if (_freePages >= _initialSize) { @@ -211,7 +218,7 @@ private bool TryExtend(TransactionService trans) /// public bool CheckSafepoint(TransactionService trans) { - return + return trans.Pages.TransactionSize >= trans.MaxTransactionSize && this.TryExtend(trans) == false; } @@ -232,4 +239,4 @@ public void Dispose() } } } -} \ No newline at end of file +} diff --git a/LiteDB/Engine/Services/TransactionService.cs b/LiteDB/Engine/Services/TransactionService.cs index 7e2d3de0a..244487010 100644 --- a/LiteDB/Engine/Services/TransactionService.cs +++ b/LiteDB/Engine/Services/TransactionService.cs @@ -1,9 +1,6 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using System.Runtime.InteropServices; -using System.Threading; using static LiteDB.Constants; namespace LiteDB.Engine @@ -444,7 +441,7 @@ protected virtual void Dispose(bool dispose) if (!dispose) { // Remove transaction monitor's dictionary - _monitor.ReleaseTransaction(this); + _monitor.RemoveTransaction(this); } } } From 3959a69ad5d00fa9d4aa258f4369b429cf74a532 Mon Sep 17 00:00:00 2001 From: ejdre Date: Fri, 31 Oct 2025 11:36:59 +0000 Subject: [PATCH 2/2] Added test to ensure that finalizing a transaction does not throw an exception --- LiteDB.Tests/Engine/Transactions_Tests.cs | 35 ++++++++++++++++------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/LiteDB.Tests/Engine/Transactions_Tests.cs b/LiteDB.Tests/Engine/Transactions_Tests.cs index 3fc266151..1131cb842 100644 --- a/LiteDB.Tests/Engine/Transactions_Tests.cs +++ b/LiteDB.Tests/Engine/Transactions_Tests.cs @@ -17,7 +17,7 @@ namespace LiteDB.Tests.Engine public class Transactions_Tests { const int MIN_CPU_COUNT = 2; - + [CpuBoundFact(MIN_CPU_COUNT)] public async Task Transaction_Write_Lock_Timeout() { @@ -75,7 +75,7 @@ public async Task Transaction_Write_Lock_Timeout() } } - + [CpuBoundFact(MIN_CPU_COUNT)] public async Task Transaction_Avoid_Dirty_Read() { @@ -135,7 +135,7 @@ public async Task Transaction_Avoid_Dirty_Read() await Task.WhenAll(ta, tb); } } - + [CpuBoundFact(MIN_CPU_COUNT)] public async Task Transaction_Read_Version() @@ -233,6 +233,19 @@ public void Test_Transaction_States() } } + [CpuBoundFact(MIN_CPU_COUNT)] + public void Test_Transaction_Finalizer() + { + var db = new LiteDatabase(new MemoryStream()); + db.BeginTrans(); + + GC.Collect(0, GCCollectionMode.Forced); + + // Finalizer should not throw exception + // If it does, it will be an unhandled exception + GC.WaitForPendingFinalizers(); + } + #if DEBUG || TESTING [Fact] public void Transaction_Rollback_Should_Skip_ReadOnly_Buffers_From_Safepoint() @@ -339,9 +352,9 @@ public void Transaction_Rollback_Should_Discard_Writable_Dirty_Pages() private class BlockingStream : MemoryStream { - public readonly AutoResetEvent Blocked = new AutoResetEvent(false); + public readonly AutoResetEvent Blocked = new AutoResetEvent(false); public readonly ManualResetEvent ShouldUnblock = new ManualResetEvent(false); - public bool ShouldBlock; + public bool ShouldBlock; public override void Write(byte[] buffer, int offset, int count) { @@ -358,10 +371,10 @@ public override void Write(byte[] buffer, int offset, int count) [CpuBoundFact(MIN_CPU_COUNT)] public void Test_Transaction_ReleaseWhenFailToStart() { - var blockingStream = new BlockingStream(); - var db = new LiteDatabase(blockingStream); + var blockingStream = new BlockingStream(); + var db = new LiteDatabase(blockingStream); SetEngineTimeout(db, TimeSpan.FromMilliseconds(50)); - Thread lockerThread = null; + Thread lockerThread = null; try { lockerThread = new Thread(() => @@ -432,11 +445,11 @@ private static void SetEngineTimeout(LiteDatabase database, TimeSpan timeout) var engine = GetLiteEngine(database); var headerField = typeof(LiteEngine).GetField("_header", BindingFlags.Instance | BindingFlags.NonPublic); - var header = headerField?.GetValue(engine) ?? throw new InvalidOperationException("LiteEngine header not available."); + var header = headerField?.GetValue(engine) ?? throw new InvalidOperationException("LiteEngine header not available."); var pragmasProp = header.GetType().GetProperty("Pragmas", BindingFlags.Instance | BindingFlags.Public) ?? throw new InvalidOperationException("Engine pragmas not accessible."); - var pragmas = pragmasProp.GetValue(header) ?? throw new InvalidOperationException("Engine pragmas not available."); + var pragmas = pragmasProp.GetValue(header) ?? throw new InvalidOperationException("Engine pragmas not available."); var timeoutProp = pragmas.GetType().GetProperty("Timeout", BindingFlags.Instance | BindingFlags.Public) ?? throw new InvalidOperationException("Timeout property not found."); - var setter = timeoutProp.GetSetMethod(true) ?? throw new InvalidOperationException("Timeout setter not accessible."); + var setter = timeoutProp.GetSetMethod(true) ?? throw new InvalidOperationException("Timeout setter not accessible."); setter.Invoke(pragmas, new object[] { timeout }); }