Add managed connection pooling and execution locking

This commit is contained in:
2026-02-27 17:28:50 +01:00
parent e43d7863ec
commit aedb97e879
12 changed files with 1554 additions and 348 deletions

View File

@@ -0,0 +1,153 @@
/*
* DynamORM - Dynamic Object-Relational Mapping library.
* Copyright (c) 2012-2026, Grzegorz Russek (grzegorz.russek@gmail.com)
* All rights reserved.
*/
using System;
using System.Data;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
namespace DynamORM.Tests.Helpers
{
[TestFixture]
public class ConnectionPoolingAndLockingTests
{
private FakeProviderState _state;
private DynamicDatabase _db;
[TearDown]
public void TearDown()
{
if (_db != null)
_db.Dispose();
}
[Test]
public void TestConnectionPoolingReusesIdleConnection()
{
CreateDatabase(DynamicDatabaseOptions.ConnectionPooling);
using (_db.Open()) { }
using (_db.Open()) { }
Assert.AreEqual(1, _state.CreatedConnections);
Assert.AreEqual(1, _state.OpenCalls);
Assert.AreEqual(0, _state.CloseCalls);
}
[Test]
public void TestConnectionPoolingWaitsForReleasedConnectionWhenAtMaximum()
{
CreateDatabase(DynamicDatabaseOptions.ConnectionPooling);
_db.ConnectionPoolingKeepOpenCount = 1;
_db.ConnectionPoolingMaximumOpenCount = 1;
var first = _db.Open();
var started = new ManualResetEventSlim(false);
var completed = new ManualResetEventSlim(false);
var task = Task.Run(() =>
{
started.Set();
using (_db.Open()) { }
completed.Set();
});
Assert.IsTrue(started.Wait(TimeSpan.FromSeconds(2)));
Assert.IsFalse(completed.Wait(TimeSpan.FromMilliseconds(200)));
first.Dispose();
Assert.IsTrue(completed.Wait(TimeSpan.FromSeconds(2)));
task.Wait(TimeSpan.FromSeconds(2));
Assert.AreEqual(1, _state.CreatedConnections);
}
[Test]
public void TestConnectionPoolingClosesExpiredIdleConnections()
{
CreateDatabase(DynamicDatabaseOptions.ConnectionPooling);
_db.ConnectionPoolingConnectionLifetime = TimeSpan.Zero;
using (_db.Open()) { }
using (_db.Open()) { }
Assert.AreEqual(2, _state.CreatedConnections);
Assert.GreaterOrEqual(_state.CloseCalls, 1);
Assert.GreaterOrEqual(_state.DisposeCalls, 1);
}
[Test]
public void TestDirectTransactionUsesSameThreadConnectionAndSeparateThreadGetsDifferentOne()
{
CreateDatabase(DynamicDatabaseOptions.ConnectionPooling);
using (var trans = _db.BeginTransaction())
{
IDbConnection threadLocalA = _db.Open();
IDbConnection threadLocalB = _db.Open();
try
{
Assert.AreSame(((DynamicConnection)threadLocalA).Connection, ((DynamicConnection)threadLocalB).Connection);
IDbConnection otherThreadConnection = null;
var task = Task.Run(() =>
{
using (var other = _db.Open())
otherThreadConnection = ((DynamicConnection)other).Connection;
});
Assert.IsTrue(task.Wait(TimeSpan.FromSeconds(2)));
Assert.AreNotSame(((DynamicConnection)threadLocalA).Connection, otherThreadConnection);
}
finally
{
threadLocalA.Dispose();
threadLocalB.Dispose();
}
}
}
[TestCase(DynamicDatabaseOptions.SingleConnection)]
[TestCase(DynamicDatabaseOptions.SingleTransaction)]
public void TestSingleModesSerializeCommandExecution(DynamicDatabaseOptions option)
{
CreateDatabase(option);
_state.BlockFirstExecution = true;
_state.AllowExecution.Reset();
Task task1 = Task.Run(() => ExecuteFakeCommand());
Assert.IsTrue(_state.FirstExecutionEntered.Wait(TimeSpan.FromSeconds(2)));
Task task2 = Task.Run(() => ExecuteFakeCommand());
Thread.Sleep(200);
Assert.AreEqual(1, _state.MaxConcurrentExecutions);
_state.AllowExecution.Set();
Assert.IsTrue(Task.WaitAll(new[] { task1, task2 }, TimeSpan.FromSeconds(5)));
Assert.AreEqual(1, _state.MaxConcurrentExecutions);
}
private void ExecuteFakeCommand()
{
using (var connection = _db.Open())
using (var command = connection.CreateCommand())
{
command.SetCommand("SELECT 1;");
command.ExecuteNonQuery();
}
}
private void CreateDatabase(DynamicDatabaseOptions options)
{
_state = new FakeProviderState();
_db = new DynamicDatabase(new FakeProviderFactory(_state), "fake", options);
}
}
}

View File

@@ -0,0 +1,236 @@
/*
* DynamORM - Dynamic Object-Relational Mapping library.
* Copyright (c) 2012-2026, Grzegorz Russek (grzegorz.russek@gmail.com)
* All rights reserved.
*/
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Threading;
namespace DynamORM.Tests.Helpers
{
internal sealed class FakeProviderState
{
public int CreatedConnections;
public int OpenCalls;
public int CloseCalls;
public int DisposeCalls;
public int BeginTransactionCalls;
public int ExecuteNonQueryCalls;
public int MaxConcurrentExecutions;
public int CurrentConcurrentExecutions;
public object LastTransactionSeen;
public readonly List<FakeProviderConnection> Connections = new List<FakeProviderConnection>();
public ManualResetEventSlim FirstExecutionEntered = new ManualResetEventSlim(false);
public ManualResetEventSlim AllowExecution = new ManualResetEventSlim(true);
public bool BlockFirstExecution;
private int _blocked;
public void RecordExecution(IDbTransaction transaction)
{
int current = Interlocked.Increment(ref CurrentConcurrentExecutions);
int snapshot;
while ((snapshot = MaxConcurrentExecutions) < current)
Interlocked.CompareExchange(ref MaxConcurrentExecutions, current, snapshot);
Interlocked.Increment(ref ExecuteNonQueryCalls);
if (transaction != null)
LastTransactionSeen = transaction;
if (BlockFirstExecution && Interlocked.CompareExchange(ref _blocked, 1, 0) == 0)
{
FirstExecutionEntered.Set();
AllowExecution.Wait(TimeSpan.FromSeconds(5));
}
}
public void ExitExecution()
{
Interlocked.Decrement(ref CurrentConcurrentExecutions);
}
}
internal sealed class FakeProviderFactory : DbProviderFactory
{
private readonly FakeProviderState _state;
public FakeProviderFactory(FakeProviderState state)
{
_state = state;
}
public override DbConnection CreateConnection()
{
var connection = new FakeProviderConnection(_state);
lock (_state.Connections)
_state.Connections.Add(connection);
Interlocked.Increment(ref _state.CreatedConnections);
return connection;
}
}
internal sealed class FakeProviderConnection : DbConnection
{
private readonly FakeProviderState _state;
private ConnectionState _connectionState = ConnectionState.Closed;
public FakeProviderConnection(FakeProviderState state)
{
_state = state;
}
public override string ConnectionString { get; set; }
public override string Database { get { return "fake"; } }
public override string DataSource { get { return "fake"; } }
public override string ServerVersion { get { return "1.0"; } }
public override ConnectionState State { get { return _connectionState; } }
public override void ChangeDatabase(string databaseName) { }
public override void Close()
{
if (_connectionState != ConnectionState.Closed)
{
_connectionState = ConnectionState.Closed;
Interlocked.Increment(ref _state.CloseCalls);
}
}
public override void Open()
{
if (_connectionState != ConnectionState.Open)
{
_connectionState = ConnectionState.Open;
Interlocked.Increment(ref _state.OpenCalls);
}
}
protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel)
{
Interlocked.Increment(ref _state.BeginTransactionCalls);
return new FakeProviderTransaction(this, isolationLevel);
}
protected override DbCommand CreateDbCommand()
{
return new FakeProviderCommand(_state) { Connection = this };
}
protected override void Dispose(bool disposing)
{
if (disposing)
Interlocked.Increment(ref _state.DisposeCalls);
base.Dispose(disposing);
}
}
internal sealed class FakeProviderTransaction : DbTransaction
{
private readonly FakeProviderConnection _connection;
private readonly IsolationLevel _isolationLevel;
public FakeProviderTransaction(FakeProviderConnection connection, IsolationLevel isolationLevel)
{
_connection = connection;
_isolationLevel = isolationLevel;
}
public override IsolationLevel IsolationLevel { get { return _isolationLevel; } }
protected override DbConnection DbConnection { get { return _connection; } }
public override void Commit() { }
public override void Rollback() { }
}
internal sealed class FakeProviderCommand : DbCommand
{
private readonly FakeProviderState _state;
private readonly FakeProviderParameterCollection _parameters = new FakeProviderParameterCollection();
public FakeProviderCommand(FakeProviderState state)
{
_state = state;
}
public override string CommandText { get; set; }
public override int CommandTimeout { get; set; }
public override CommandType CommandType { get; set; }
public override bool DesignTimeVisible { get; set; }
public override UpdateRowSource UpdatedRowSource { get; set; }
protected override DbConnection DbConnection { get; set; }
protected override DbParameterCollection DbParameterCollection { get { return _parameters; } }
protected override DbTransaction DbTransaction { get; set; }
public override void Cancel() { }
public override int ExecuteNonQuery()
{
_state.RecordExecution(DbTransaction);
try
{
return 1;
}
finally
{
_state.ExitExecution();
}
}
public override object ExecuteScalar()
{
return ExecuteNonQuery();
}
public override void Prepare() { }
protected override DbParameter CreateDbParameter()
{
return new FakeProviderParameter();
}
protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior)
{
throw new NotSupportedException();
}
}
internal sealed class FakeProviderParameter : DbParameter
{
public override DbType DbType { get; set; }
public override ParameterDirection Direction { get; set; }
public override bool IsNullable { get; set; }
public override string ParameterName { get; set; }
public override string SourceColumn { get; set; }
public override object Value { get; set; }
public override bool SourceColumnNullMapping { get; set; }
public override int Size { get; set; }
public override void ResetDbType() { }
}
internal sealed class FakeProviderParameterCollection : DbParameterCollection
{
private readonly List<DbParameter> _items = new List<DbParameter>();
public override int Count { get { return _items.Count; } }
public override object SyncRoot { get { return this; } }
public override int Add(object value) { _items.Add((DbParameter)value); return _items.Count - 1; }
public override void AddRange(Array values) { foreach (object value in values) Add(value); }
public override void Clear() { _items.Clear(); }
public override bool Contains(object value) { return _items.Contains((DbParameter)value); }
public override bool Contains(string value) { return IndexOf(value) >= 0; }
public override void CopyTo(Array array, int index) { _items.ToArray().CopyTo(array, index); }
public override System.Collections.IEnumerator GetEnumerator() { return _items.GetEnumerator(); }
protected override DbParameter GetParameter(int index) { return _items[index]; }
protected override DbParameter GetParameter(string parameterName) { return _items[IndexOf(parameterName)]; }
public override int IndexOf(object value) { return _items.IndexOf((DbParameter)value); }
public override int IndexOf(string parameterName) { return _items.FindIndex(x => x.ParameterName == parameterName); }
public override void Insert(int index, object value) { _items.Insert(index, (DbParameter)value); }
public override void Remove(object value) { _items.Remove((DbParameter)value); }
public override void RemoveAt(int index) { _items.RemoveAt(index); }
public override void RemoveAt(string parameterName) { int i = IndexOf(parameterName); if (i >= 0) _items.RemoveAt(i); }
protected override void SetParameter(int index, DbParameter value) { _items[index] = value; }
protected override void SetParameter(string parameterName, DbParameter value) { _items[IndexOf(parameterName)] = value; }
}
}

View File

@@ -92,9 +92,10 @@ namespace DynamORM.Tests.Helpers
finally
{
// Remove for next tests
Database.Dispose();
if (Database != null)
Database.Dispose();
Database = null;
}
}
}
}
}