From 03160c557ae14d7273d4fd2943117480cb41aa6f Mon Sep 17 00:00:00 2001 From: Jim Pelletier Date: Tue, 12 Dec 2023 08:17:59 +1100 Subject: [PATCH 1/4] Add support for using a provided connection factory --- source/Nevermore/Advanced/ReadTransaction.cs | 2 +- source/Nevermore/Advanced/WriteTransaction.cs | 16 ++++++++++++++++ source/Nevermore/RelationalStore.cs | 18 ++++++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/source/Nevermore/Advanced/ReadTransaction.cs b/source/Nevermore/Advanced/ReadTransaction.cs index 6fe857e0..e33e465e 100644 --- a/source/Nevermore/Advanced/ReadTransaction.cs +++ b/source/Nevermore/Advanced/ReadTransaction.cs @@ -86,7 +86,7 @@ public ReadTransaction( OwnsSqlTransaction = false; } - internal ReadTransaction( + public ReadTransaction( IRelationalStore store, IRelationalTransactionRegistry registry, RetriableOperation operationsToRetry, diff --git a/source/Nevermore/Advanced/WriteTransaction.cs b/source/Nevermore/Advanced/WriteTransaction.cs index 2befdd7c..e42e3f73 100644 --- a/source/Nevermore/Advanced/WriteTransaction.cs +++ b/source/Nevermore/Advanced/WriteTransaction.cs @@ -55,6 +55,22 @@ public WriteTransaction( this.keyAllocator = keyAllocator; builder = new DataModificationQueryBuilder(configuration, AllocateId); } + + public WriteTransaction( + IRelationalStore store, + IRelationalTransactionRegistry registry, + RetriableOperation operationsToRetry, + IRelationalStoreConfiguration configuration, + IKeyAllocator keyAllocator, + Func connectionFactory, + Action? customCommandTrace = null, + string? name = null + ) : base(store, registry, operationsToRetry, configuration, connectionFactory, customCommandTrace, name) + { + this.configuration = configuration; + this.keyAllocator = keyAllocator; + builder = new DataModificationQueryBuilder(configuration, AllocateId); + } #nullable disable public void Insert(TDocument document, InsertOptions options = null) where TDocument : class diff --git a/source/Nevermore/RelationalStore.cs b/source/Nevermore/RelationalStore.cs index 3456b6b6..0b141dac 100644 --- a/source/Nevermore/RelationalStore.cs +++ b/source/Nevermore/RelationalStore.cs @@ -140,6 +140,24 @@ public IWriteTransaction CreateWriteTransactionFromExistingConnectionAndTransact customCommandTrace, name); } + + public IWriteTransaction CreateWriteTransactionFromExistingConnectionFactory( + Func connectionFactory, + IRelationalTransactionRegistry? customRelationalTransactionRegistry = null, + Action? customCommandTrace = null, + RetriableOperation retriableOperation = NevermoreDefaults.RetriableOperations, + string? name = null) + { + return new WriteTransaction( + this, + customRelationalTransactionRegistry ?? registry.Value, + retriableOperation, + Configuration, + keyAllocator.Value, + connectionFactory, + customCommandTrace, + name); + } ReadTransaction CreateReadTransaction(RetriableOperation retriableOperation, string? name = null) { From dbf0cf1b4df540719631661dddb24f1580c4bc81 Mon Sep 17 00:00:00 2001 From: Jim Pelletier Date: Tue, 12 Dec 2023 17:59:58 +1100 Subject: [PATCH 2/4] Adds method to begin the transaction from a connection factory, not just create one --- source/Nevermore/RelationalStore.cs | 42 ++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/source/Nevermore/RelationalStore.cs b/source/Nevermore/RelationalStore.cs index 0b141dac..80dc04cc 100644 --- a/source/Nevermore/RelationalStore.cs +++ b/source/Nevermore/RelationalStore.cs @@ -96,6 +96,37 @@ public async Task BeginWriteTransactionAsync(IsolationLevel i throw; } } + + public IWriteTransaction BeginWriteTransactionFromExistingConnectionFactory(Func connectionFactory, IsolationLevel isolationLevel = NevermoreDefaults.IsolationLevel, RetriableOperation retriableOperation = NevermoreDefaults.RetriableOperations, string? name = null, CancellationToken cancellationToken = default) + { + var txn = CreateWriteTransactionFromExistingConnectionFactory(connectionFactory, retriableOperation, name); + try + { + txn.Open(isolationLevel); + return txn; + } + catch + { + txn.Dispose(); + throw; + } + } + + public async Task BeginWriteTransactionFromExistingConnectionFactoryAsync(Func connectionFactory, IsolationLevel isolationLevel = NevermoreDefaults.IsolationLevel, RetriableOperation retriableOperation = NevermoreDefaults.RetriableOperations, string? name = null, CancellationToken cancellationToken = default) + { + var txn = CreateWriteTransactionFromExistingConnectionFactory(connectionFactory, retriableOperation, name); + try + { + await txn.OpenAsync(isolationLevel, cancellationToken).ConfigureAwait(false); + return txn; + } + catch + { + txn.Dispose(); + throw; + } + } + public IRelationalTransaction BeginTransaction(IsolationLevel isolationLevel = NevermoreDefaults.IsolationLevel, RetriableOperation retriableOperation = NevermoreDefaults.RetriableOperations, string? name = null) { @@ -141,22 +172,19 @@ public IWriteTransaction CreateWriteTransactionFromExistingConnectionAndTransact name); } - public IWriteTransaction CreateWriteTransactionFromExistingConnectionFactory( + public WriteTransaction CreateWriteTransactionFromExistingConnectionFactory( Func connectionFactory, - IRelationalTransactionRegistry? customRelationalTransactionRegistry = null, - Action? customCommandTrace = null, - RetriableOperation retriableOperation = NevermoreDefaults.RetriableOperations, + RetriableOperation retriableOperation, string? name = null) { return new WriteTransaction( this, - customRelationalTransactionRegistry ?? registry.Value, + registry.Value, retriableOperation, Configuration, keyAllocator.Value, connectionFactory, - customCommandTrace, - name); + name: name); } ReadTransaction CreateReadTransaction(RetriableOperation retriableOperation, string? name = null) From 9b07aa12de529d894249273168c139bf3b112d2f Mon Sep 17 00:00:00 2001 From: Jim Pelletier Date: Fri, 5 Jan 2024 14:20:40 +1100 Subject: [PATCH 3/4] No longer tries to open externally owned connections --- .../RelationalStore/ReadTransactionFixture.cs | 46 ++++--------------- source/Nevermore/Advanced/ReadTransaction.cs | 24 +++++++--- source/Nevermore/Advanced/WriteTransaction.cs | 2 +- source/Nevermore/RelationalStore.cs | 6 +-- 4 files changed, 31 insertions(+), 47 deletions(-) diff --git a/source/Nevermore.Tests/RelationalStore/ReadTransactionFixture.cs b/source/Nevermore.Tests/RelationalStore/ReadTransactionFixture.cs index 8a13a130..9a10cc0e 100644 --- a/source/Nevermore.Tests/RelationalStore/ReadTransactionFixture.cs +++ b/source/Nevermore.Tests/RelationalStore/ReadTransactionFixture.cs @@ -19,11 +19,19 @@ public class ReadTransactionFixture RelationalTransactionRegistry registry; readonly List createdConnections = new(); - DbConnection ConnectionFactory(string s) + (DbConnection connection, bool ownsConnection) ConnectionFactory(string s) { var c = new FakeSqlConnection { ConnectionString = s }; createdConnections.Add(c); - return c; + return (c, false); + } + + + (DbConnection connection, bool ownsConnection) ConnectionFactoryTransientFailure(string s) + { + var c = new FakeSqlConnectionWhichThrowsOnFirstTransaction { ConnectionString = s }; + createdConnections.Add(c); + return (c, false); } [SetUp] @@ -95,13 +103,6 @@ public override void Open() [Test] public void OpenWillRetryATransientFailure() { - DbConnection ConnectionFactoryTransientFailure(string s) - { - var c = new FakeSqlConnectionWhichThrowsOnFirstOpen { ConnectionString = s }; - createdConnections.Add(c); - return c; - } - var c = new ReadTransaction(null!, registry, RetriableOperation.Select, new RelationalStoreConfiguration(FakeConnectionString), ConnectionFactoryTransientFailure); c.Open(); @@ -115,13 +116,6 @@ DbConnection ConnectionFactoryTransientFailure(string s) [Test] public async Task OpenAsyncWillRetryATransientFailure() { - DbConnection ConnectionFactoryTransientFailure(string s) - { - var c = new FakeSqlConnectionWhichThrowsOnFirstOpen { ConnectionString = s }; - createdConnections.Add(c); - return c; - } - var c = new ReadTransaction(null!, registry, RetriableOperation.Select, new RelationalStoreConfiguration(FakeConnectionString), ConnectionFactoryTransientFailure); await c.OpenAsync(); @@ -136,13 +130,6 @@ DbConnection ConnectionFactoryTransientFailure(string s) [Test] public void OpenWithIsolationWillRetryATransientFailure() { - DbConnection ConnectionFactoryTransientFailure(string s) - { - var c = new FakeSqlConnectionWhichThrowsOnFirstOpen { ConnectionString = s }; - createdConnections.Add(c); - return c; - } - var c = new ReadTransaction(null!, registry, RetriableOperation.Select, new RelationalStoreConfiguration(FakeConnectionString), ConnectionFactoryTransientFailure); c.Open(IsolationLevel.ReadCommitted); @@ -183,13 +170,6 @@ public override DbTransaction BeginTransaction(IsolationLevel iso, string transa [Test] public void OpenWithIsolationWillRetryATransientFailureFromTransaction() { - DbConnection ConnectionFactoryTransientFailure(string s) - { - var c = new FakeSqlConnectionWhichThrowsOnFirstTransaction { ConnectionString = s }; - createdConnections.Add(c); - return c; - } - var c = new ReadTransaction(null!, registry, RetriableOperation.Select, new RelationalStoreConfiguration(FakeConnectionString), ConnectionFactoryTransientFailure); c.Open(IsolationLevel.ReadCommitted); @@ -203,12 +183,6 @@ DbConnection ConnectionFactoryTransientFailure(string s) [Test] public async Task OpenAsyncWithIsolationWillRetryATransientFailureFromTransaction() { - DbConnection ConnectionFactoryTransientFailure(string s) - { - var c = new FakeSqlConnectionWhichThrowsOnFirstTransaction { ConnectionString = s }; - createdConnections.Add(c); - return c; - } var c = new ReadTransaction(null!, registry, RetriableOperation.Select, new RelationalStoreConfiguration(FakeConnectionString), ConnectionFactoryTransientFailure); diff --git a/source/Nevermore/Advanced/ReadTransaction.cs b/source/Nevermore/Advanced/ReadTransaction.cs index e33e465e..9724f2af 100644 --- a/source/Nevermore/Advanced/ReadTransaction.cs +++ b/source/Nevermore/Advanced/ReadTransaction.cs @@ -26,14 +26,14 @@ namespace Nevermore.Advanced public class ReadTransaction : IReadTransaction, ITransactionDiagnostic { static readonly ILog Log = LogProvider.For(); - protected static DbConnection DefaultConnectionFactory(string connectionString) => new SqlConnection(connectionString); + protected static (DbConnection connection, bool ownsConnection) DefaultConnectionFactory(string connectionString) => (new SqlConnection(connectionString), true); readonly IRelationalTransactionRegistry registry; readonly RetriableOperation operationsToRetry; readonly IRelationalStoreConfiguration configuration; readonly ITableAliasGenerator tableAliasGenerator = new TableAliasGenerator(); - readonly Func connectionFactory; + readonly Func connectionFactory; readonly Action? customCommandTrace; readonly string name; @@ -91,7 +91,7 @@ public ReadTransaction( IRelationalTransactionRegistry registry, RetriableOperation operationsToRetry, IRelationalStoreConfiguration configuration, - Func connectionFactory, + Func connectionFactory, Action? customCommandTrace = null, string? name = null) { @@ -126,8 +126,13 @@ public void Open() if (!OwnsSqlTransaction) throw new InvalidOperationException("An existing connection and transaction were provided, they should have been opened externally"); - connection = connectionFactory(configuration.ConnectionString); - connection.OpenWithRetry(); + var (connectionFactoryConnection, ownsConnection) = connectionFactory(configuration.ConnectionString); + connection = connectionFactoryConnection; + + if (ownsConnection) + { + connection.OpenWithRetry(); + } TransactionTimer = new TimedSection(ms => configuration.TransactionLogger.Write(ms, name)); } @@ -137,8 +142,13 @@ public async Task OpenAsync(CancellationToken cancellationToken = default) if (!OwnsSqlTransaction) throw new InvalidOperationException("An existing connection and transaction were provided, they should have been opened externally"); - connection = connectionFactory(configuration.ConnectionString); - await connection.OpenWithRetryAsync(cancellationToken).ConfigureAwait(false); + var (connectionFactoryConnection, ownsConnection) = connectionFactory(configuration.ConnectionString); + connection = connectionFactoryConnection; + + if (ownsConnection) + { + await connection.OpenWithRetryAsync(cancellationToken).ConfigureAwait(false); + } TransactionTimer = new TimedSection(ms => configuration.TransactionLogger.Write(ms, name)); } diff --git a/source/Nevermore/Advanced/WriteTransaction.cs b/source/Nevermore/Advanced/WriteTransaction.cs index e42e3f73..8f6abc95 100644 --- a/source/Nevermore/Advanced/WriteTransaction.cs +++ b/source/Nevermore/Advanced/WriteTransaction.cs @@ -62,7 +62,7 @@ public WriteTransaction( RetriableOperation operationsToRetry, IRelationalStoreConfiguration configuration, IKeyAllocator keyAllocator, - Func connectionFactory, + Func connectionFactory, Action? customCommandTrace = null, string? name = null ) : base(store, registry, operationsToRetry, configuration, connectionFactory, customCommandTrace, name) diff --git a/source/Nevermore/RelationalStore.cs b/source/Nevermore/RelationalStore.cs index 80dc04cc..836b6149 100644 --- a/source/Nevermore/RelationalStore.cs +++ b/source/Nevermore/RelationalStore.cs @@ -97,7 +97,7 @@ public async Task BeginWriteTransactionAsync(IsolationLevel i } } - public IWriteTransaction BeginWriteTransactionFromExistingConnectionFactory(Func connectionFactory, IsolationLevel isolationLevel = NevermoreDefaults.IsolationLevel, RetriableOperation retriableOperation = NevermoreDefaults.RetriableOperations, string? name = null, CancellationToken cancellationToken = default) + public IWriteTransaction BeginWriteTransactionFromExistingConnectionFactory(Func connectionFactory, IsolationLevel isolationLevel = NevermoreDefaults.IsolationLevel, RetriableOperation retriableOperation = NevermoreDefaults.RetriableOperations, string? name = null, CancellationToken cancellationToken = default) { var txn = CreateWriteTransactionFromExistingConnectionFactory(connectionFactory, retriableOperation, name); try @@ -112,7 +112,7 @@ public IWriteTransaction BeginWriteTransactionFromExistingConnectionFactory(Func } } - public async Task BeginWriteTransactionFromExistingConnectionFactoryAsync(Func connectionFactory, IsolationLevel isolationLevel = NevermoreDefaults.IsolationLevel, RetriableOperation retriableOperation = NevermoreDefaults.RetriableOperations, string? name = null, CancellationToken cancellationToken = default) + public async Task BeginWriteTransactionFromExistingConnectionFactoryAsync(Func connectionFactory, IsolationLevel isolationLevel = NevermoreDefaults.IsolationLevel, RetriableOperation retriableOperation = NevermoreDefaults.RetriableOperations, string? name = null, CancellationToken cancellationToken = default) { var txn = CreateWriteTransactionFromExistingConnectionFactory(connectionFactory, retriableOperation, name); try @@ -173,7 +173,7 @@ public IWriteTransaction CreateWriteTransactionFromExistingConnectionAndTransact } public WriteTransaction CreateWriteTransactionFromExistingConnectionFactory( - Func connectionFactory, + Func connectionFactory, RetriableOperation retriableOperation, string? name = null) { From 48c2109190592dfed8b8aedac57f5fbd225ebcef Mon Sep 17 00:00:00 2001 From: Jim Pelletier Date: Fri, 5 Jan 2024 15:49:12 +1100 Subject: [PATCH 4/4] Fixes unit tests --- .../RelationalStore/ReadTransactionFixture.cs | 44 +++++++++++++++---- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/source/Nevermore.Tests/RelationalStore/ReadTransactionFixture.cs b/source/Nevermore.Tests/RelationalStore/ReadTransactionFixture.cs index 9a10cc0e..2092ca2e 100644 --- a/source/Nevermore.Tests/RelationalStore/ReadTransactionFixture.cs +++ b/source/Nevermore.Tests/RelationalStore/ReadTransactionFixture.cs @@ -23,15 +23,7 @@ public class ReadTransactionFixture { var c = new FakeSqlConnection { ConnectionString = s }; createdConnections.Add(c); - return (c, false); - } - - - (DbConnection connection, bool ownsConnection) ConnectionFactoryTransientFailure(string s) - { - var c = new FakeSqlConnectionWhichThrowsOnFirstTransaction { ConnectionString = s }; - createdConnections.Add(c); - return (c, false); + return (c, true); } [SetUp] @@ -103,6 +95,13 @@ public override void Open() [Test] public void OpenWillRetryATransientFailure() { + (DbConnection connection, bool ownsConnection) ConnectionFactoryTransientFailure(string s) + { + var c = new FakeSqlConnectionWhichThrowsOnFirstOpen { ConnectionString = s }; + createdConnections.Add(c); + return (c, true); + } + var c = new ReadTransaction(null!, registry, RetriableOperation.Select, new RelationalStoreConfiguration(FakeConnectionString), ConnectionFactoryTransientFailure); c.Open(); @@ -116,6 +115,13 @@ public void OpenWillRetryATransientFailure() [Test] public async Task OpenAsyncWillRetryATransientFailure() { + (DbConnection connection, bool ownsConnection) ConnectionFactoryTransientFailure(string s) + { + var c = new FakeSqlConnectionWhichThrowsOnFirstOpen { ConnectionString = s }; + createdConnections.Add(c); + return (c, true); + } + var c = new ReadTransaction(null!, registry, RetriableOperation.Select, new RelationalStoreConfiguration(FakeConnectionString), ConnectionFactoryTransientFailure); await c.OpenAsync(); @@ -130,6 +136,13 @@ public async Task OpenAsyncWillRetryATransientFailure() [Test] public void OpenWithIsolationWillRetryATransientFailure() { + (DbConnection connection, bool ownsConnection) ConnectionFactoryTransientFailure(string s) + { + var c = new FakeSqlConnectionWhichThrowsOnFirstOpen { ConnectionString = s }; + createdConnections.Add(c); + return (c, true); + } + var c = new ReadTransaction(null!, registry, RetriableOperation.Select, new RelationalStoreConfiguration(FakeConnectionString), ConnectionFactoryTransientFailure); c.Open(IsolationLevel.ReadCommitted); @@ -170,6 +183,13 @@ public override DbTransaction BeginTransaction(IsolationLevel iso, string transa [Test] public void OpenWithIsolationWillRetryATransientFailureFromTransaction() { + (DbConnection connection, bool ownsConnection) ConnectionFactoryTransientFailure(string s) + { + var c = new FakeSqlConnectionWhichThrowsOnFirstTransaction { ConnectionString = s }; + createdConnections.Add(c); + return (c, true); + } + var c = new ReadTransaction(null!, registry, RetriableOperation.Select, new RelationalStoreConfiguration(FakeConnectionString), ConnectionFactoryTransientFailure); c.Open(IsolationLevel.ReadCommitted); @@ -183,6 +203,12 @@ public void OpenWithIsolationWillRetryATransientFailureFromTransaction() [Test] public async Task OpenAsyncWithIsolationWillRetryATransientFailureFromTransaction() { + (DbConnection connection, bool ownsConnection) ConnectionFactoryTransientFailure(string s) + { + var c = new FakeSqlConnectionWhichThrowsOnFirstTransaction { ConnectionString = s }; + createdConnections.Add(c); + return (c, true); + } var c = new ReadTransaction(null!, registry, RetriableOperation.Select, new RelationalStoreConfiguration(FakeConnectionString), ConnectionFactoryTransientFailure);