Skip to content

Commit 49f74a6

Browse files
authored
Fix transaction finalizer causing LiteDB.LiteException/System.ObjectDisposedException (#2721)
* Updated the transaction finalizer to not try to release a lock that can never be successful * Added test to ensure that finalizing a transaction does not throw an exception
1 parent 0086a5c commit 49f74a6

File tree

3 files changed

+41
-24
lines changed

3 files changed

+41
-24
lines changed

LiteDB.Tests/Engine/Transactions_Tests.cs

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ namespace LiteDB.Tests.Engine
1717
public class Transactions_Tests
1818
{
1919
const int MIN_CPU_COUNT = 2;
20-
20+
2121
[CpuBoundFact(MIN_CPU_COUNT)]
2222
public async Task Transaction_Write_Lock_Timeout()
2323
{
@@ -75,7 +75,7 @@ public async Task Transaction_Write_Lock_Timeout()
7575
}
7676
}
7777

78-
78+
7979
[CpuBoundFact(MIN_CPU_COUNT)]
8080
public async Task Transaction_Avoid_Dirty_Read()
8181
{
@@ -135,7 +135,7 @@ public async Task Transaction_Avoid_Dirty_Read()
135135
await Task.WhenAll(ta, tb);
136136
}
137137
}
138-
138+
139139

140140
[CpuBoundFact(MIN_CPU_COUNT)]
141141
public async Task Transaction_Read_Version()
@@ -233,6 +233,19 @@ public void Test_Transaction_States()
233233
}
234234
}
235235

236+
[CpuBoundFact(MIN_CPU_COUNT)]
237+
public void Test_Transaction_Finalizer()
238+
{
239+
var db = new LiteDatabase(new MemoryStream());
240+
db.BeginTrans();
241+
242+
GC.Collect(0, GCCollectionMode.Forced);
243+
244+
// Finalizer should not throw exception
245+
// If it does, it will be an unhandled exception
246+
GC.WaitForPendingFinalizers();
247+
}
248+
236249
#if DEBUG || TESTING
237250
[Fact]
238251
public void Transaction_Rollback_Should_Skip_ReadOnly_Buffers_From_Safepoint()
@@ -339,9 +352,9 @@ public void Transaction_Rollback_Should_Discard_Writable_Dirty_Pages()
339352

340353
private class BlockingStream : MemoryStream
341354
{
342-
public readonly AutoResetEvent Blocked = new AutoResetEvent(false);
355+
public readonly AutoResetEvent Blocked = new AutoResetEvent(false);
343356
public readonly ManualResetEvent ShouldUnblock = new ManualResetEvent(false);
344-
public bool ShouldBlock;
357+
public bool ShouldBlock;
345358

346359
public override void Write(byte[] buffer, int offset, int count)
347360
{
@@ -358,10 +371,10 @@ public override void Write(byte[] buffer, int offset, int count)
358371
[CpuBoundFact(MIN_CPU_COUNT)]
359372
public void Test_Transaction_ReleaseWhenFailToStart()
360373
{
361-
var blockingStream = new BlockingStream();
362-
var db = new LiteDatabase(blockingStream);
374+
var blockingStream = new BlockingStream();
375+
var db = new LiteDatabase(blockingStream);
363376
SetEngineTimeout(db, TimeSpan.FromMilliseconds(50));
364-
Thread lockerThread = null;
377+
Thread lockerThread = null;
365378
try
366379
{
367380
lockerThread = new Thread(() =>
@@ -432,11 +445,11 @@ private static void SetEngineTimeout(LiteDatabase database, TimeSpan timeout)
432445
var engine = GetLiteEngine(database);
433446

434447
var headerField = typeof(LiteEngine).GetField("_header", BindingFlags.Instance | BindingFlags.NonPublic);
435-
var header = headerField?.GetValue(engine) ?? throw new InvalidOperationException("LiteEngine header not available.");
448+
var header = headerField?.GetValue(engine) ?? throw new InvalidOperationException("LiteEngine header not available.");
436449
var pragmasProp = header.GetType().GetProperty("Pragmas", BindingFlags.Instance | BindingFlags.Public) ?? throw new InvalidOperationException("Engine pragmas not accessible.");
437-
var pragmas = pragmasProp.GetValue(header) ?? throw new InvalidOperationException("Engine pragmas not available.");
450+
var pragmas = pragmasProp.GetValue(header) ?? throw new InvalidOperationException("Engine pragmas not available.");
438451
var timeoutProp = pragmas.GetType().GetProperty("Timeout", BindingFlags.Instance | BindingFlags.Public) ?? throw new InvalidOperationException("Timeout property not found.");
439-
var setter = timeoutProp.GetSetMethod(true) ?? throw new InvalidOperationException("Timeout setter not accessible.");
452+
var setter = timeoutProp.GetSetMethod(true) ?? throw new InvalidOperationException("Timeout setter not accessible.");
440453

441454
setter.Invoke(pragmas, new object[] { timeout });
442455
}

LiteDB/Engine/Services/TransactionMonitor.cs

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
using System;
2-
using System.Collections.Concurrent;
32
using System.Collections.Generic;
43
using System.Linq;
5-
using System.Runtime.InteropServices;
64
using System.Threading;
75
using static LiteDB.Constants;
86

@@ -105,9 +103,10 @@ public TransactionService GetTransaction(bool create, bool queryOnly, out bool i
105103
}
106104

107105
/// <summary>
108-
/// Release current thread transaction
106+
/// Dispose and remove transaction from monitor
107+
/// without releasing thread lock
109108
/// </summary>
110-
public void ReleaseTransaction(TransactionService transaction)
109+
public bool RemoveTransaction(TransactionService transaction)
111110
{
112111
// dispose current transaction
113112
transaction.Dispose();
@@ -123,8 +122,16 @@ public void ReleaseTransaction(TransactionService transaction)
123122
_freePages += transaction.MaxTransactionSize;
124123

125124
// check if current thread contains more query transactions
126-
keepLocked = _transactions.Values.Any(x => x.ThreadID == Environment.CurrentManagedThreadId);
125+
return keepLocked = _transactions.Values.Any(x => x.ThreadID == Environment.CurrentManagedThreadId);
127126
}
127+
}
128+
129+
/// <summary>
130+
/// Release current thread transaction
131+
/// </summary>
132+
public void ReleaseTransaction(TransactionService transaction)
133+
{
134+
var keepLocked = RemoveTransaction(transaction);
128135

129136
// unlock thread-transaction only if there is no more transactions
130137
if (keepLocked == false)
@@ -150,7 +157,7 @@ public TransactionService GetThreadTransaction()
150157
{
151158
lock (_transactions)
152159
{
153-
return
160+
return
154161
_slot.Value ??
155162
_transactions.Values.FirstOrDefault(x => x.ThreadID == Environment.CurrentManagedThreadId);
156163
}
@@ -191,7 +198,7 @@ private int GetInitialSize()
191198
/// </summary>
192199
private bool TryExtend(TransactionService trans)
193200
{
194-
lock(_transactions)
201+
lock (_transactions)
195202
{
196203
if (_freePages >= _initialSize)
197204
{
@@ -211,7 +218,7 @@ private bool TryExtend(TransactionService trans)
211218
/// </summary>
212219
public bool CheckSafepoint(TransactionService trans)
213220
{
214-
return
221+
return
215222
trans.Pages.TransactionSize >= trans.MaxTransactionSize &&
216223
this.TryExtend(trans) == false;
217224
}
@@ -232,4 +239,4 @@ public void Dispose()
232239
}
233240
}
234241
}
235-
}
242+
}

LiteDB/Engine/Services/TransactionService.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
using System;
2-
using System.Collections.Concurrent;
32
using System.Collections.Generic;
43
using System.Linq;
5-
using System.Runtime.InteropServices;
6-
using System.Threading;
74
using static LiteDB.Constants;
85

96
namespace LiteDB.Engine
@@ -444,7 +441,7 @@ protected virtual void Dispose(bool dispose)
444441
if (!dispose)
445442
{
446443
// Remove transaction monitor's dictionary
447-
_monitor.ReleaseTransaction(this);
444+
_monitor.RemoveTransaction(this);
448445
}
449446
}
450447
}

0 commit comments

Comments
 (0)