diff --git a/DynamORM.Tests/Helpers/ConnectionPoolingAndLockingTests.cs b/DynamORM.Tests/Helpers/ConnectionPoolingAndLockingTests.cs index d94481b..88dbab0 100644 --- a/DynamORM.Tests/Helpers/ConnectionPoolingAndLockingTests.cs +++ b/DynamORM.Tests/Helpers/ConnectionPoolingAndLockingTests.cs @@ -80,6 +80,26 @@ namespace DynamORM.Tests.Helpers Assert.GreaterOrEqual(_state.DisposeCalls, 1); } + [Test] + public void TestConnectionPoolingTrimsIdleConnectionsAbovePreferredLimit() + { + CreateDatabase(DynamicDatabaseOptions.ConnectionPooling); + _db.ConnectionPoolingKeepOpenCount = 1; + _db.ConnectionPoolingMaximumOpenCount = 3; + + var c1 = _db.Open(); + var c2 = _db.Open(); + var c3 = _db.Open(); + + c1.Dispose(); + c2.Dispose(); + c3.Dispose(); + + Assert.AreEqual(3, _state.CreatedConnections); + Assert.AreEqual(2, _state.CloseCalls); + Assert.AreEqual(2, _state.DisposeCalls); + } + [Test] public void TestDirectTransactionUsesSameThreadConnectionAndSeparateThreadGetsDifferentOne() { @@ -112,6 +132,29 @@ namespace DynamORM.Tests.Helpers } } + [Test] + public void TestDirectTransactionUsesTransactionOnlyOnOwningThread() + { + CreateDatabase(DynamicDatabaseOptions.ConnectionPooling); + + using (var transaction = _db.BeginTransaction()) + { + ExecuteFakeCommand(); + + var otherThread = Task.Run(() => ExecuteFakeCommand()); + Assert.IsTrue(otherThread.Wait(TimeSpan.FromSeconds(2))); + + transaction.Commit(); + } + + lock (_state.SeenTransactions) + { + Assert.AreEqual(2, _state.SeenTransactions.Count); + Assert.NotNull(_state.SeenTransactions[0]); + Assert.IsNull(_state.SeenTransactions[1]); + } + } + [TestCase(DynamicDatabaseOptions.SingleConnection)] [TestCase(DynamicDatabaseOptions.SingleTransaction)] public void TestSingleModesSerializeCommandExecution(DynamicDatabaseOptions option) @@ -134,6 +177,44 @@ namespace DynamORM.Tests.Helpers Assert.AreEqual(1, _state.MaxConcurrentExecutions); } + [TestCase(DynamicDatabaseOptions.SingleConnection)] + [TestCase(DynamicDatabaseOptions.SingleTransaction)] + public void TestSingleModesSerializeReaderLifetime(DynamicDatabaseOptions option) + { + CreateDatabase(option); + _state.BlockFirstReader = true; + _state.AllowReader.Reset(); + + IDataReader reader = null; + var task1 = Task.Run(() => + { + using (var connection = _db.Open()) + using (var command = connection.CreateCommand()) + { + command.SetCommand("SELECT 1;"); + reader = command.ExecuteReader(); + } + }); + + Assert.IsTrue(_state.FirstReaderEntered.Wait(TimeSpan.FromSeconds(2))); + + var secondCompleted = new ManualResetEventSlim(false); + var task2 = Task.Run(() => + { + ExecuteFakeCommand(); + secondCompleted.Set(); + }); + + Assert.IsFalse(secondCompleted.Wait(TimeSpan.FromMilliseconds(200))); + + _state.AllowReader.Set(); + SpinWait.SpinUntil(() => reader != null, TimeSpan.FromSeconds(2)); + reader.Dispose(); + + Assert.IsTrue(Task.WaitAll(new[] { task1, task2 }, TimeSpan.FromSeconds(5))); + Assert.AreEqual(1, _state.MaxConcurrentExecutions); + } + private void ExecuteFakeCommand() { using (var connection = _db.Open()) diff --git a/DynamORM.Tests/Helpers/FakeProviderFactory.cs b/DynamORM.Tests/Helpers/FakeProviderFactory.cs index a904e74..aa4e85b 100644 --- a/DynamORM.Tests/Helpers/FakeProviderFactory.cs +++ b/DynamORM.Tests/Helpers/FakeProviderFactory.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.Data; using System.Data.Common; using System.Threading; +using System.Threading.Tasks; namespace DynamORM.Tests.Helpers { @@ -23,11 +24,16 @@ namespace DynamORM.Tests.Helpers public int MaxConcurrentExecutions; public int CurrentConcurrentExecutions; public object LastTransactionSeen; + public readonly List SeenTransactions = new List(); public readonly List Connections = new List(); public ManualResetEventSlim FirstExecutionEntered = new ManualResetEventSlim(false); public ManualResetEventSlim AllowExecution = new ManualResetEventSlim(true); public bool BlockFirstExecution; + public bool BlockFirstReader; + public ManualResetEventSlim FirstReaderEntered = new ManualResetEventSlim(false); + public ManualResetEventSlim AllowReader = new ManualResetEventSlim(true); private int _blocked; + private int _readerBlocked; public void RecordExecution(IDbTransaction transaction) { @@ -39,6 +45,8 @@ namespace DynamORM.Tests.Helpers Interlocked.Increment(ref ExecuteNonQueryCalls); if (transaction != null) LastTransactionSeen = transaction; + lock (SeenTransactions) + SeenTransactions.Add(transaction); if (BlockFirstExecution && Interlocked.CompareExchange(ref _blocked, 1, 0) == 0) { @@ -51,6 +59,25 @@ namespace DynamORM.Tests.Helpers { Interlocked.Decrement(ref CurrentConcurrentExecutions); } + + public void RecordReaderOpen(IDbTransaction transaction) + { + int current = Interlocked.Increment(ref CurrentConcurrentExecutions); + int snapshot; + while ((snapshot = MaxConcurrentExecutions) < current) + Interlocked.CompareExchange(ref MaxConcurrentExecutions, current, snapshot); + + if (transaction != null) + LastTransactionSeen = transaction; + lock (SeenTransactions) + SeenTransactions.Add(transaction); + + if (BlockFirstReader && Interlocked.CompareExchange(ref _readerBlocked, 1, 0) == 0) + { + FirstReaderEntered.Set(); + AllowReader.Wait(TimeSpan.FromSeconds(5)); + } + } } internal sealed class FakeProviderFactory : DbProviderFactory @@ -192,7 +219,67 @@ namespace DynamORM.Tests.Helpers protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) { - throw new NotSupportedException(); + _state.RecordReaderOpen(DbTransaction); + return new FakeProviderDataReader(_state); + } + } + + internal sealed class FakeProviderDataReader : DbDataReader + { + private readonly FakeProviderState _state; + private bool _closed; + + public FakeProviderDataReader(FakeProviderState state) + { + _state = state; + } + + public override object this[int ordinal] { get { return 1; } } + public override object this[string name] { get { return 1; } } + public override int Depth { get { return 0; } } + public override int FieldCount { get { return 1; } } + public override bool HasRows { get { return true; } } + public override bool IsClosed { get { return _closed; } } + public override int RecordsAffected { get { return 0; } } + + public override bool GetBoolean(int ordinal) { return true; } + public override byte GetByte(int ordinal) { return 1; } + public override long GetBytes(int ordinal, long dataOffset, byte[] buffer, int bufferOffset, int length) { return 0; } + public override char GetChar(int ordinal) { return '1'; } + public override long GetChars(int ordinal, long dataOffset, char[] buffer, int bufferOffset, int length) { return 0; } + public override string GetDataTypeName(int ordinal) { return "Int32"; } + public override DateTime GetDateTime(int ordinal) { return DateTime.UtcNow; } + public override decimal GetDecimal(int ordinal) { return 1m; } + public override double GetDouble(int ordinal) { return 1d; } + public override System.Collections.IEnumerator GetEnumerator() { yield break; } + public override Type GetFieldType(int ordinal) { return typeof(int); } + public override float GetFloat(int ordinal) { return 1f; } + public override Guid GetGuid(int ordinal) { return Guid.Empty; } + public override short GetInt16(int ordinal) { return 1; } + public override int GetInt32(int ordinal) { return 1; } + public override long GetInt64(int ordinal) { return 1L; } + public override string GetName(int ordinal) { return "Value"; } + public override int GetOrdinal(string name) { return 0; } + public override string GetString(int ordinal) { return "1"; } + public override object GetValue(int ordinal) { return 1; } + public override int GetValues(object[] values) { values[0] = 1; return 1; } + public override bool IsDBNull(int ordinal) { return false; } + public override bool NextResult() { return false; } + public override bool Read() { return false; } + + public override void Close() + { + if (!_closed) + { + _closed = true; + _state.ExitExecution(); + } + } + + protected override void Dispose(bool disposing) + { + Close(); + base.Dispose(disposing); } }