diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 86da91f0e..4e9c4c0fb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -3,9 +3,9 @@ name: DotNet Build and Test # Triggers the workflow on push or pull request events but only for the master branch on: push: - branches: [ master ] + branches: [ master, pool/SNOW-860872-connection-pool ] pull_request: - branches: [ master ] + branches: [ master, pool/SNOW-860872-connection-pool ] workflow_dispatch: inputs: logLevel: diff --git a/CodingConventions.md b/CodingConventions.md index 19ca8fc75..0242f583e 100644 --- a/CodingConventions.md +++ b/CodingConventions.md @@ -85,6 +85,18 @@ public class ExampleClass } ``` +#### Property + +Use PascalCase, eg. `SomeProperty`. + +```csharp +public ExampleProperty +{ + get; + set; +} +``` + ### Local variables Use camelCase, eg. `someVariable`. diff --git a/README.md b/README.md index 13f2ca41d..7e748c8a5 100644 --- a/README.md +++ b/README.md @@ -133,37 +133,44 @@ i.e "\=\;\=\...". The following table lists all valid connection properties:
-| Connection Property | Required | Comment | -|--------------------------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| ACCOUNT | Yes | Your full account name might include additional segments that identify the region and cloud platform where your account is hosted | -| APPLICATION | No | **_Snowflake partner use only_**: Specifies the name of a partner application to connect through .NET. The name must match the following pattern: ^\[A-Za-z](\[A-Za-z0-9.-]){1,50}$ (one letter followed by 1 to 50 letter, digit, .,- or, \_ characters). | -| DB | No | | -| HOST | No | Specifies the hostname for your account in the following format: \.snowflakecomputing.com.
If no value is specified, the driver uses \.snowflakecomputing.com. | -| PASSWORD | Depends | Required if AUTHENTICATOR is set to `snowflake` (the default value) or the URL for native SSO through Okta. Ignored for all the other authentication types. | -| ROLE | No | | -| SCHEMA | No | | -| USER | Depends | If AUTHENTICATOR is set to `externalbrowser` this is optional. For native SSO through Okta, set this to the login name for your identity provider (IdP). | -| WAREHOUSE | No | | -| CONNECTION_TIMEOUT | No | Total timeout in seconds when connecting to Snowflake. The default is 300 seconds | -| RETRY_TIMEOUT | No | Total timeout in seconds for supported endpoints of retry policy. The default is 300 seconds. The value can only be increased from the default value or set to 0 for infinite timeout | -| MAXHTTPRETRIES | No | Maximum number of times to retry failed HTTP requests (default: 7). You can set `MAXHTTPRETRIES=0` to remove the retry limit, but doing so runs the risk of the .NET driver infinitely retrying failed HTTP calls. | -| CLIENT_SESSION_KEEP_ALIVE | No | Whether to keep the current session active after a period of inactivity, or to force the user to login again. If the value is `true`, Snowflake keeps the session active indefinitely, even if there is no activity from the user. If the value is `false`, the user must log in again after four hours of inactivity. The default is `false`. Setting this value overrides the server session property for the current session. | -| DISABLERETRY | No | Set this property to `true` to prevent the driver from reconnecting automatically when the connection fails or drops. The default value is `false`. | +| Connection Property | Required | Comment | +|--------------------------------|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| ACCOUNT | Yes | Your full account name might include additional segments that identify the region and cloud platform where your account is hosted | +| APPLICATION | No | **_Snowflake partner use only_**: Specifies the name of a partner application to connect through .NET. The name must match the following pattern: ^\[A-Za-z](\[A-Za-z0-9.-]){1,50}$ (one letter followed by 1 to 50 letter, digit, .,- or, \_ characters). | +| DB | No | | +| HOST | No | Specifies the hostname for your account in the following format: \.snowflakecomputing.com.
If no value is specified, the driver uses \.snowflakecomputing.com. | +| PASSWORD | Depends | Required if AUTHENTICATOR is set to `snowflake` (the default value) or the URL for native SSO through Okta. Ignored for all the other authentication types. | +| ROLE | No | | +| SCHEMA | No | | +| USER | Depends | If AUTHENTICATOR is set to `externalbrowser` this is optional. For native SSO through Okta, set this to the login name for your identity provider (IdP). | +| WAREHOUSE | No | | +| CONNECTION_TIMEOUT | No | Total timeout in seconds when connecting to Snowflake. The default is 300 seconds | +| RETRY_TIMEOUT | No | Total timeout in seconds for supported endpoints of retry policy. The default is 300 seconds. The value can only be increased from the default value or set to 0 for infinite timeout | +| MAXHTTPRETRIES | No | Maximum number of times to retry failed HTTP requests (default: 7). You can set `MAXHTTPRETRIES=0` to remove the retry limit, but doing so runs the risk of the .NET driver infinitely retrying failed HTTP calls. | +| CLIENT_SESSION_KEEP_ALIVE | No | Whether to keep the current session active after a period of inactivity, or to force the user to login again. If the value is `true`, Snowflake keeps the session active indefinitely, even if there is no activity from the user. If the value is `false`, the user must log in again after four hours of inactivity. The default is `false`. Setting this value overrides the server session property for the current session. | +| DISABLERETRY | No | Set this property to `true` to prevent the driver from reconnecting automatically when the connection fails or drops. The default value is `false`. | | AUTHENTICATOR | No | The method of authentication. Currently supports the following values:
- snowflake (default): You must also set USER and PASSWORD.
- [the URL for native SSO through Okta](https://docs.snowflake.com/en/user-guide/admin-security-fed-auth-use.html#native-sso-okta-only): You must also set USER and PASSWORD.
- [externalbrowser](https://docs.snowflake.com/en/user-guide/admin-security-fed-auth-use.html#browser-based-sso): You must also set USER.
- [snowflake_jwt](https://docs.snowflake.com/en/user-guide/key-pair-auth.html): You must also set PRIVATE_KEY_FILE or PRIVATE_KEY.
- [oauth](https://docs.snowflake.com/en/user-guide/oauth.html): You must also set TOKEN. | BROWSER_RESPONSE_TIMEOUT | No | Number to seconds to wait for authentication in an external browser (default: 120). | -| VALIDATE_DEFAULT_PARAMETERS | No | Whether DB, SCHEMA and WAREHOUSE should be verified when making connection. Default to be true. | -| PRIVATE_KEY_FILE | Depends | The path to the private key file to use for key-pair authentication. Must be used in combination with AUTHENTICATOR=snowflake_jwt | -| PRIVATE_KEY_PWD | No | The passphrase to use for decrypting the private key, if the key is encrypted. | -| PRIVATE_KEY | Depends | The private key to use for key-pair authentication. Must be used in combination with AUTHENTICATOR=snowflake_jwt.
If the private key value includes any equal signs (=), make sure to replace each equal sign with two signs (==) to ensure that the connection string is parsed correctly. | -| TOKEN | Depends | The OAuth token to use for OAuth authentication. Must be used in combination with AUTHENTICATOR=oauth. | -| INSECUREMODE | No | Set to true to disable the certificate revocation list check. Default is false. | -| USEPROXY | No | Set to true if you need to use a proxy server. The default value is false.

This parameter was introduced in v2.0.4. | -| PROXYHOST | Depends | The hostname of the proxy server.

If USEPROXY is set to `true`, you must set this parameter.

This parameter was introduced in v2.0.4. | -| PROXYPORT | Depends | The port number of the proxy server.

If USEPROXY is set to `true`, you must set this parameter.

This parameter was introduced in v2.0.4. | -| PROXYUSER | No | The username for authenticating to the proxy server.

This parameter was introduced in v2.0.4. | -| PROXYPASSWORD | Depends | The password for authenticating to the proxy server.

If USEPROXY is `true` and PROXYUSER is set, you must set this parameter.

This parameter was introduced in v2.0.4. | -| NONPROXYHOSTS | No | The list of hosts that the driver should connect to directly, bypassing the proxy server. Separate the hostnames with a pipe symbol (\|). You can also use an asterisk (`*`) as a wildcard.

This parameter was introduced in v2.0.4. | -| FILE_TRANSFER_MEMORY_THRESHOLD | No | The maximum number of bytes to store in memory used in order to provide a file encryption. If encrypting/decrypting file size exeeds provided value a temporary file will be created and the work will be continued in the temporary file instead of memory.
If no value provided 1MB will be used as a default value (that is 1048576 bytes).
It is possible to configure any integer value bigger than zero representing maximal number of bytes to reside in memory. | -| CLIENT_CONFIG_FILE | No | The location of the client configuration json file. In this file you can configure easy logging feature. | +| VALIDATE_DEFAULT_PARAMETERS | No | Whether DB, SCHEMA and WAREHOUSE should be verified when making connection. Default to be true. | +| PRIVATE_KEY_FILE | Depends | The path to the private key file to use for key-pair authentication. Must be used in combination with AUTHENTICATOR=snowflake_jwt | +| PRIVATE_KEY_PWD | No | The passphrase to use for decrypting the private key, if the key is encrypted. | +| PRIVATE_KEY | Depends | The private key to use for key-pair authentication. Must be used in combination with AUTHENTICATOR=snowflake_jwt.
If the private key value includes any equal signs (=), make sure to replace each equal sign with two signs (==) to ensure that the connection string is parsed correctly. | +| TOKEN | Depends | The OAuth token to use for OAuth authentication. Must be used in combination with AUTHENTICATOR=oauth. | +| INSECUREMODE | No | Set to true to disable the certificate revocation list check. Default is false. | +| USEPROXY | No | Set to true if you need to use a proxy server. The default value is false.

This parameter was introduced in v2.0.4. | +| PROXYHOST | Depends | The hostname of the proxy server.

If USEPROXY is set to `true`, you must set this parameter.

This parameter was introduced in v2.0.4. | +| PROXYPORT | Depends | The port number of the proxy server.

If USEPROXY is set to `true`, you must set this parameter.

This parameter was introduced in v2.0.4. | +| PROXYUSER | No | The username for authenticating to the proxy server.

This parameter was introduced in v2.0.4. | +| PROXYPASSWORD | Depends | The password for authenticating to the proxy server.

If USEPROXY is `true` and PROXYUSER is set, you must set this parameter.

This parameter was introduced in v2.0.4. | +| NONPROXYHOSTS | No | The list of hosts that the driver should connect to directly, bypassing the proxy server. Separate the hostnames with a pipe symbol (\|). You can also use an asterisk (`*`) as a wildcard.

This parameter was introduced in v2.0.4. | +| FILE_TRANSFER_MEMORY_THRESHOLD | No | The maximum number of bytes to store in memory used in order to provide a file encryption. If encrypting/decrypting file size exeeds provided value a temporary file will be created and the work will be continued in the temporary file instead of memory.
If no value provided 1MB will be used as a default value (that is 1048576 bytes).
It is possible to configure any integer value bigger than zero representing maximal number of bytes to reside in memory. | +| CLIENT_CONFIG_FILE | No | The location of the client configuration json file. In this file you can configure easy logging feature. | +| MAXPOOLSIZE | No | Maximum number of connections in a pool. Default value is 10. `maxPoolSize` value cannot be lower than `minPoolSize` value. | +| MINPOOLSIZE | No | Expected minimum number of connections in pool. When you get a connection from the pool, more connections might be initialised in background to increase the pool size to `minPoolSize`. If you specify 0 or 1 there will be no attempts to create extra initialisations in background. The default value is 2. `maxPoolSize` value cannot be lower than `minPoolSize` value. The parameter is used only in a new version of connection pool. | +| CHANGEDSESSION | No | Specifies what should happen with a closed connection when some of its session variables are altered (e. g. you used `ALTER SESSION SET SCHEMA` to change the databese schema). The default behaviour is `OriginalPool` which means the session stays in the original pool. Currently no other option is possible. Parameter used only in a new version of connection pool. | +| WAITINGFORIDLESESSIONTIMEOUT | No | Timeout for waiting for an idle session when pool is full. It happens when there is no idle session and we cannot create a new one because of reaching `maxPoolSize`. The default value is 30 seconds. Usage of units possible and allowed are: e. g. `1000ms` (milliseconds), `15s` (seconds), `2m` (minutes) where seconds are default for a skipped postfix. Special values: `0` - immediate fail for new connection to open when session is full. You cannot specify infinite value. | +| EXPIRATIONTIMEOUT | No | Timeout for using each connection. Connections which last more than specified timeout are considered to be expired and are being removed from the pool. The default is 1 hour. Usage of units possible and allowed are: e. g. `360000ms` (milliseconds), `3600s` (seconds), `60m` (minutes) where seconds are default for a skipped postfix. Special values: `0` - immediate expiration of the connection just after its creation. Expiration timeout cannot be set to infinity. | +| POOLINGENABLED | No | Boolean flag indicating if the connection should be a part of a pool. The default value is `true`. | +
@@ -660,6 +667,12 @@ CancellationTokenSource cancellationTokenSource = new CancellationTokenSource() ((SnowflakeDbConnection)conn).CloseAsync(cancellationTokenSource.Token); ``` +Evict the Connection +-------------------- + +For the open connection, call the `PreventPooling()` to mark the connection to be removed on close instead being still pooled. +The busy sessions counter will be decreased when the connection is closed. + Logging ------- The Snowflake Connector for .NET uses [log4net](http://logging.apache.org/log4net/) as the logging framework. diff --git a/Snowflake.Data.Tests/IntegrationTests/ConnectionMultiplePoolsAsyncIT.cs b/Snowflake.Data.Tests/IntegrationTests/ConnectionMultiplePoolsAsyncIT.cs new file mode 100644 index 000000000..05e05ebbc --- /dev/null +++ b/Snowflake.Data.Tests/IntegrationTests/ConnectionMultiplePoolsAsyncIT.cs @@ -0,0 +1,93 @@ +using System.Data; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; +using Moq; +using NUnit.Framework; +using Snowflake.Data.Client; +using Snowflake.Data.Core.Session; +using Snowflake.Data.Tests.Mock; +using Snowflake.Data.Tests.Util; + +namespace Snowflake.Data.Tests.IntegrationTests +{ + [TestFixture] + [NonParallelizable] + public class ConnectionMultiplePoolsAsyncIT: SFBaseTestAsync + { + private readonly PoolConfig _previousPoolConfig = new PoolConfig(); + + [SetUp] + public new void BeforeTest() + { + SnowflakeDbConnectionPool.SetConnectionPoolVersion(ConnectionPoolType.MultipleConnectionPool); + SnowflakeDbConnectionPool.ClearAllPools(); + } + + [TearDown] + public new void AfterTest() + { + _previousPoolConfig.Reset(); + } + + [Test] + public async Task TestMinPoolSizeAsync() + { + // arrange + var connection = new SnowflakeDbConnection(); + connection.ConnectionString = ConnectionString + "application=TestMinPoolSizeAsync;minPoolSize=3"; + + // act + await connection.OpenAsync().ConfigureAwait(false); + Thread.Sleep(3000); + + // assert + var pool = SnowflakeDbConnectionPool.GetPool(connection.ConnectionString); + Assert.AreEqual(3, pool.GetCurrentPoolSize()); + + // cleanup + await connection.CloseAsync(CancellationToken.None).ConfigureAwait(false); + } + + [Test] + public async Task TestPreventConnectionFromReturningToPool() + { + // arrange + var connectionString = ConnectionString + "minPoolSize=0"; + var connection = new SnowflakeDbConnection(connectionString); + await connection.OpenAsync().ConfigureAwait(false); + var pool = SnowflakeDbConnectionPool.GetPool(connectionString); + Assert.AreEqual(1, pool.GetCurrentPoolSize()); + + // act + connection.PreventPooling(); + await connection.CloseAsync(CancellationToken.None).ConfigureAwait(false); + + // assert + Assert.AreEqual(0, pool.GetCurrentPoolSize()); + } + + [Test] + public async Task TestReleaseConnectionWhenRollbackFailsAsync() + { + // arrange + var connectionString = ConnectionString + "minPoolSize=0"; + var pool = SnowflakeDbConnectionPool.GetPool(connectionString); + var commandThrowingExceptionOnlyForRollback = MockHelper.CommandThrowingExceptionOnlyForRollback(); + var mockDbProviderFactory = new Mock(); + mockDbProviderFactory.Setup(p => p.CreateCommand()).Returns(commandThrowingExceptionOnlyForRollback.Object); + Assert.AreEqual(0, pool.GetCurrentPoolSize()); + var connection = new TestSnowflakeDbConnection(mockDbProviderFactory.Object); + connection.ConnectionString = connectionString; + await connection.OpenAsync().ConfigureAwait(false); + connection.BeginTransaction(); // not using async version because it is not available on .net framework + Assert.AreEqual(true, connection.HasActiveExplicitTransaction()); + + // act + await connection.CloseAsync(CancellationToken.None).ConfigureAwait(false); + + // assert + Assert.AreEqual(0, pool.GetCurrentPoolSize(), "Should not return connection to the pool"); + } + } +} diff --git a/Snowflake.Data.Tests/IntegrationTests/ConnectionMultiplePoolsIT.cs b/Snowflake.Data.Tests/IntegrationTests/ConnectionMultiplePoolsIT.cs new file mode 100644 index 000000000..9adfc6b4a --- /dev/null +++ b/Snowflake.Data.Tests/IntegrationTests/ConnectionMultiplePoolsIT.cs @@ -0,0 +1,442 @@ +using System; +using System.Data; +using System.Data.Common; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Moq; +using NUnit.Framework; +using Snowflake.Data.Client; +using Snowflake.Data.Core.Session; +using Snowflake.Data.Tests.Mock; +using Snowflake.Data.Tests.Util; + +namespace Snowflake.Data.Tests.IntegrationTests +{ + [TestFixture] + [NonParallelizable] + public class ConnectionMultiplePoolsIT: SFBaseTest + { + private readonly PoolConfig _previousPoolConfig = new PoolConfig(); + + [SetUp] + public new void BeforeTest() + { + SnowflakeDbConnectionPool.SetConnectionPoolVersion(ConnectionPoolType.MultipleConnectionPool); + SnowflakeDbConnectionPool.ClearAllPools(); + } + + [TearDown] + public new void AfterTest() + { + _previousPoolConfig.Reset(); + } + + [OneTimeTearDown] + public static void AfterAllTests() + { + SnowflakeDbConnectionPool.ClearAllPools(); + } + + [Test] + public void TestBasicConnectionPool() + { + var connectionString = ConnectionString + "minPoolSize=0;maxPoolSize=1"; + var conn1 = new SnowflakeDbConnection(connectionString); + conn1.Open(); + Assert.AreEqual(ConnectionState.Open, conn1.State); + conn1.Close(); + + // assert + Assert.AreEqual(ConnectionState.Closed, conn1.State); + Assert.AreEqual(1, SnowflakeDbConnectionPool.GetPool(connectionString).GetCurrentPoolSize()); + } + + [Test] + public void TestReuseSessionInConnectionPool() // old name: TestConnectionPool + { + var connectionString = ConnectionString + "minPoolSize=1"; + var conn1 = new SnowflakeDbConnection(connectionString); + conn1.Open(); + Assert.AreEqual(ConnectionState.Open, conn1.State); + conn1.Close(); + Assert.AreEqual(1, SnowflakeDbConnectionPool.GetPool(connectionString).GetCurrentPoolSize()); + + var conn2 = new SnowflakeDbConnection(); + conn2.ConnectionString = connectionString; + conn2.Open(); + Assert.AreEqual(ConnectionState.Open, conn2.State); + Assert.AreEqual(1, SnowflakeDbConnectionPool.GetPool(connectionString).GetCurrentPoolSize()); + + conn2.Close(); + Assert.AreEqual(1, SnowflakeDbConnectionPool.GetPool(connectionString).GetCurrentPoolSize()); + Assert.AreEqual(ConnectionState.Closed, conn1.State); + Assert.AreEqual(ConnectionState.Closed, conn2.State); + } + + [Test] + public void TestReuseSessionInConnectionPoolReachingMaxConnections() // old name: TestConnectionPoolFull + { + var connectionString = ConnectionString + "maxPoolSize=2;minPoolSize=1"; + var pool = SnowflakeDbConnectionPool.GetPool(connectionString); + + var conn1 = new SnowflakeDbConnection(); + conn1.ConnectionString = connectionString; + conn1.Open(); + Assert.AreEqual(ConnectionState.Open, conn1.State); + + var conn2 = new SnowflakeDbConnection(); + conn2.ConnectionString = connectionString; + conn2.Open(); + Assert.AreEqual(ConnectionState.Open, conn2.State); + + Assert.AreEqual(2, pool.GetCurrentPoolSize()); + conn1.Close(); + conn2.Close(); + Assert.AreEqual(2, pool.GetCurrentPoolSize()); + + var conn3 = new SnowflakeDbConnection(); + conn3.ConnectionString = connectionString; + conn3.Open(); + Assert.AreEqual(ConnectionState.Open, conn3.State); + + var conn4 = new SnowflakeDbConnection(); + conn4.ConnectionString = connectionString; + conn4.Open(); + Assert.AreEqual(ConnectionState.Open, conn4.State); + + conn3.Close(); + Assert.AreEqual(2, pool.GetCurrentPoolSize()); + conn4.Close(); + Assert.AreEqual(2, pool.GetCurrentPoolSize()); + + Assert.AreEqual(ConnectionState.Closed, conn1.State); + Assert.AreEqual(ConnectionState.Closed, conn2.State); + Assert.AreEqual(ConnectionState.Closed, conn3.State); + Assert.AreEqual(ConnectionState.Closed, conn4.State); + } + + [Test] + public void TestWaitForTheIdleConnectionWhenExceedingMaxConnectionsLimit() + { + // arrange + var connectionString = ConnectionString + "application=TestWaitForMaxSize1;waitingForIdleSessionTimeout=1s;maxPoolSize=2;minPoolSize=1"; + var pool = SnowflakeDbConnectionPool.GetPool(connectionString); + Assert.AreEqual(0, pool.GetCurrentPoolSize(), "expecting pool to be empty"); + var conn1 = OpenConnection(connectionString); + var conn2 = OpenConnection(connectionString); + var watch = new StopWatch(); + + // act + watch.Start(); + var thrown = Assert.Throws(() => OpenConnection(connectionString)); + watch.Stop(); + + // assert + Assert.That(thrown.Message, Does.Contain("Unable to connect. Could not obtain a connection from the pool within a given timeout")); + Assert.That(watch.ElapsedMilliseconds, Is.InRange(1000, 1500)); + Assert.AreEqual(pool.GetCurrentPoolSize(), 2); + + // cleanup + conn1.Close(); + conn2.Close(); + } + + [Test] + public void TestWaitForTheIdleConnectionWhenExceedingMaxConnectionsLimitAsync() + { + // arrange + var connectionString = ConnectionString + "application=TestWaitForMaxSize2;waitingForIdleSessionTimeout=1s;maxPoolSize=2;minPoolSize=1"; + var pool = SnowflakeDbConnectionPool.GetPool(connectionString); + Assert.AreEqual(0, pool.GetCurrentPoolSize(), "expecting pool to be empty"); + var conn1 = OpenConnection(connectionString); + var conn2 = OpenConnection(connectionString); + var watch = new StopWatch(); + + // act + watch.Start(); + var thrown = Assert.ThrowsAsync(() => OpenConnectionAsync(connectionString)); + watch.Stop(); + + // assert + Assert.That(thrown.Message, Does.Contain("Unable to connect")); + Assert.IsTrue(thrown.InnerException is AggregateException); + var nestedException = ((AggregateException)thrown.InnerException).InnerException; + Assert.That(nestedException.Message, Does.Contain("Could not obtain a connection from the pool within a given timeout")); + Assert.That(watch.ElapsedMilliseconds, Is.InRange(1000, 1500)); + Assert.AreEqual(pool.GetCurrentPoolSize(), 2); + + // cleanup + conn1.Close(); + conn2.Close(); + } + + [Test] + public void TestWaitInAQueueForAnIdleSession() + { + // arrange + var connectionString = ConnectionString + "application=TestWaitForMaxSize3;waitingForIdleSessionTimeout=3s;maxPoolSize=2;minPoolSize=0"; + var pool = SnowflakeDbConnectionPool.GetPool(connectionString); + Assert.AreEqual(0, pool.GetCurrentPoolSize(), "the pool is expected to be empty"); + const long ADelay = 0; + const long BDelay = 400; + const long CDelay = 2 * BDelay; + const long DDelay = 3 * BDelay; + const long ABDelayAfterConnect = 2000; + const long ConnectPessimisticEstimate = 1300; + const long StartDelayPessimisticEstimate = 350; + const long AMinConnectionReleaseTime = ADelay + ABDelayAfterConnect; // 2000 + const long AMaxConnectionReleaseTime = ADelay + StartDelayPessimisticEstimate + ConnectPessimisticEstimate + ABDelayAfterConnect; // 3650 + const long BMinConnectionReleaseTime = BDelay + ABDelayAfterConnect; // 2400 + const long BMaxConnectionReleaseTime = BDelay + StartDelayPessimisticEstimate + ConnectPessimisticEstimate + ABDelayAfterConnect; // 4050 + const long CMinConnectDuration = AMinConnectionReleaseTime - CDelay - StartDelayPessimisticEstimate; // 2000 - 800 - 350 = 850 + const long CMaxConnectDuration = AMaxConnectionReleaseTime - CDelay; // 3650 - 800 = 2850 + const long DMinConnectDuration = BMinConnectionReleaseTime - DDelay - StartDelayPessimisticEstimate; // 2400 - 1200 - 350 = 850 + const long DMaxConnectDuration = BMaxConnectionReleaseTime - DDelay; // 3650 - 800 = 2850 + + var threads = new ConnectingThreads(connectionString) + .NewThread("A", ADelay, ABDelayAfterConnect, true) + .NewThread("B", BDelay, ABDelayAfterConnect, true) + .NewThread("C", CDelay, 0, true) + .NewThread("D", DDelay, 0, true); + pool.SetSessionPoolEventHandler(new SessionPoolThreadEventHandler(threads)); + + // act + threads.StartAll().JoinAll(); + + // assert + var events = threads.Events().ToList(); + Assert.AreEqual(6, events.Count); // A,B - connected; C,D - waiting, connected + var waitingEvents = events.Where(e => e.IsWaitingEvent()).ToList(); + Assert.AreEqual(2, waitingEvents.Count); + CollectionAssert.AreEquivalent(new[] { "C", "D" }, waitingEvents.Select(e => e.ThreadName)); // equivalent = in any order + var connectedEvents = events.Where(e => e.IsConnectedEvent()).ToList(); + Assert.AreEqual(4, connectedEvents.Count); + var firstConnectedEventsGroup = connectedEvents.GetRange(0, 2); + CollectionAssert.AreEquivalent(new[] { "A", "B" }, firstConnectedEventsGroup.Select(e => e.ThreadName)); + var lastConnectingEventsGroup = connectedEvents.GetRange(2, 2); + CollectionAssert.AreEquivalent(new[] { "C", "D" }, lastConnectingEventsGroup.Select(e => e.ThreadName)); + Assert.LessOrEqual(firstConnectedEventsGroup[0].Duration, ConnectPessimisticEstimate); + Assert.LessOrEqual(firstConnectedEventsGroup[1].Duration, ConnectPessimisticEstimate); + // first to wait from C and D should first to connect, because we won't create a new session, we just reuse sessions returned by A and B threads + Assert.AreEqual(waitingEvents[0].ThreadName, lastConnectingEventsGroup[0].ThreadName); + Assert.AreEqual(waitingEvents[1].ThreadName, lastConnectingEventsGroup[1].ThreadName); + Assert.That(lastConnectingEventsGroup[0].Duration, Is.InRange(CMinConnectDuration, CMaxConnectDuration)); + Assert.That(lastConnectingEventsGroup[1].Duration, Is.InRange(DMinConnectDuration, DMaxConnectDuration)); + } + + [Test] + public void TestBusyAndIdleConnectionsCountedInPoolSize() + { + // arrange + var connectionString = ConnectionString + "maxPoolSize=2;minPoolSize=1"; + var pool = SnowflakeDbConnectionPool.GetPool(connectionString); + var connection = new SnowflakeDbConnection(); + connection.ConnectionString = connectionString; + + // act + connection.Open(); + + // assert + Assert.AreEqual(1, pool.GetCurrentPoolSize()); + + // act + connection.Close(); + + // assert + Assert.AreEqual(1, pool.GetCurrentPoolSize()); + } + + [Test] + public void TestConnectionPoolNotPossibleToDisableForAllPools() + { + // act + var thrown = Assert.Throws(() => SnowflakeDbConnectionPool.SetPooling(false)); + + // assert + Assert.IsNotNull(thrown); + } + + [Test] + public void TestConnectionPoolDisable() + { + // arrange + var pool = SnowflakeDbConnectionPool.GetPool(ConnectionString); + pool.SetPooling(false); + var conn1 = new SnowflakeDbConnection(); + conn1.ConnectionString = ConnectionString; + + // act + conn1.Open(); + + // assert + Assert.AreEqual(ConnectionState.Open, conn1.State); + Assert.AreEqual(0, pool.GetCurrentPoolSize()); + + // act + conn1.Close(); + + // assert + Assert.AreEqual(ConnectionState.Closed, conn1.State); + Assert.AreEqual(0, pool.GetCurrentPoolSize()); + } + + [Test] + public void TestNewConnectionPoolClean() + { + var connectionString = ConnectionString + "maxPoolSize=2;minPoolSize=1;"; + var conn1 = new SnowflakeDbConnection(); + conn1.ConnectionString = connectionString; + conn1.Open(); + Assert.AreEqual(ConnectionState.Open, conn1.State); + + var conn2 = new SnowflakeDbConnection(); + conn2.ConnectionString = connectionString + "retryCount=1"; + conn2.Open(); + Assert.AreEqual(ConnectionState.Open, conn2.State); + + var conn3 = new SnowflakeDbConnection(); + conn3.ConnectionString = connectionString + "retryCount=2"; + conn3.Open(); + Assert.AreEqual(ConnectionState.Open, conn3.State); + + conn1.Close(); + conn2.Close(); + Assert.AreEqual(1, SnowflakeDbConnectionPool.GetPool(conn1.ConnectionString).GetCurrentPoolSize()); + Assert.AreEqual(1, SnowflakeDbConnectionPool.GetPool(conn2.ConnectionString).GetCurrentPoolSize()); + SnowflakeDbConnectionPool.ClearAllPools(); + Assert.AreEqual(0, SnowflakeDbConnectionPool.GetPool(conn1.ConnectionString).GetCurrentPoolSize()); + Assert.AreEqual(0, SnowflakeDbConnectionPool.GetPool(conn2.ConnectionString).GetCurrentPoolSize()); + conn3.Close(); + Assert.AreEqual(1, SnowflakeDbConnectionPool.GetPool(conn3.ConnectionString).GetCurrentPoolSize()); + + Assert.AreEqual(ConnectionState.Closed, conn1.State); + Assert.AreEqual(ConnectionState.Closed, conn2.State); + Assert.AreEqual(ConnectionState.Closed, conn3.State); + } + + [Test] + public void TestConnectionPoolExpirationWorks() + { + // arrange + const int ExpirationTimeoutInSeconds = 10; + var connectionString = ConnectionString + $"expirationTimeout={ExpirationTimeoutInSeconds};maxPoolSize=4;minPoolSize=2"; + var pool = SnowflakeDbConnectionPool.GetPool(connectionString); + Assert.AreEqual(0, pool.GetCurrentPoolSize()); + + // act + var conn1 = OpenConnection(connectionString); + var conn2 = OpenConnection(connectionString); + var conn3 = OpenConnection(connectionString); + var conn4 = OpenConnection(connectionString); + + // assert + Assert.AreEqual(4, pool.GetCurrentPoolSize()); + + // act + WaitUntilAllSessionsCreatedOrTimeout(pool); + var beforeSleepMillis = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + Thread.Sleep(TimeSpan.FromSeconds(ExpirationTimeoutInSeconds)); + conn1.Close(); + conn2.Close(); + conn3.Close(); + conn4.Close(); + + // assert + Assert.AreEqual(2, pool.GetCurrentPoolSize()); // 2 idle sessions, but expired because close doesn't remove expired sessions + + // act + WaitUntilAllSessionsCreatedOrTimeout(pool); + var conn5 = OpenConnection(connectionString); + WaitUntilAllSessionsCreatedOrTimeout(pool); + + // assert + Assert.AreEqual(2, pool.GetCurrentPoolSize()); // 1 idle session and 1 busy + var sessionStartTimes = pool.GetIdleSessionsStartTimes(); + Assert.AreEqual(1, sessionStartTimes.Count); + Assert.That(sessionStartTimes.First(), Is.GreaterThan(beforeSleepMillis)); + Assert.That(conn5.SfSession.GetStartTime(), Is.GreaterThan(beforeSleepMillis)); + } + + [Test] + public void TestMinPoolSize() + { + // arrange + var connection = new SnowflakeDbConnection(); + connection.ConnectionString = ConnectionString + "application=TestMinPoolSize;minPoolSize=3"; + + // act + connection.Open(); + Thread.Sleep(3000); + + // assert + var pool = SnowflakeDbConnectionPool.GetPool(connection.ConnectionString); + Assert.AreEqual(3, pool.GetCurrentPoolSize()); + + // cleanup + connection.Close(); + } + + [Test] + public void TestPreventConnectionFromReturningToPool() + { + // arrange + var connectionString = ConnectionString + "minPoolSize=0"; + var connection = OpenConnection(connectionString); + var pool = SnowflakeDbConnectionPool.GetPool(connectionString); + Assert.AreEqual(1, pool.GetCurrentPoolSize()); + + // act + connection.PreventPooling(); + connection.Close(); + + // assert + Assert.AreEqual(0, pool.GetCurrentPoolSize()); + } + + [Test] + public void TestReleaseConnectionWhenRollbackFails() + { + // arrange + var connectionString = ConnectionString + "minPoolSize=0"; + var pool = SnowflakeDbConnectionPool.GetPool(connectionString); + var commandThrowingExceptionOnlyForRollback = MockHelper.CommandThrowingExceptionOnlyForRollback(); + var mockDbProviderFactory = new Mock(); + mockDbProviderFactory.Setup(p => p.CreateCommand()).Returns(commandThrowingExceptionOnlyForRollback.Object); + Assert.AreEqual(0, pool.GetCurrentPoolSize()); + var connection = new TestSnowflakeDbConnection(mockDbProviderFactory.Object); + connection.ConnectionString = connectionString; + connection.Open(); + connection.BeginTransaction(); + Assert.AreEqual(true, connection.HasActiveExplicitTransaction()); + + // act + connection.Close(); + + // assert + Assert.AreEqual(0, pool.GetCurrentPoolSize(), "Should not return connection to the pool"); + } + + private void WaitUntilAllSessionsCreatedOrTimeout(SessionPool pool) + { + var expectingToWaitAtMostForSessionCreations = TimeSpan.FromSeconds(15); + Awaiter.WaitUntilConditionOrTimeout(() => pool.OngoingSessionCreationsCount() == 0, expectingToWaitAtMostForSessionCreations); + } + + private SnowflakeDbConnection OpenConnection(string connectionString) + { + var connection = new SnowflakeDbConnection(); + connection.ConnectionString = connectionString; + connection.Open(); + return connection; + } + + private async Task OpenConnectionAsync(string connectionString) + { + var connection = new SnowflakeDbConnection(); + connection.ConnectionString = connectionString; + await connection.OpenAsync().ConfigureAwait(false); + return connection; + } + } +} diff --git a/Snowflake.Data.Tests/IntegrationTests/ConnectionPoolCommonIT.cs b/Snowflake.Data.Tests/IntegrationTests/ConnectionPoolCommonIT.cs new file mode 100644 index 000000000..796779883 --- /dev/null +++ b/Snowflake.Data.Tests/IntegrationTests/ConnectionPoolCommonIT.cs @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2012-2023 Snowflake Computing Inc. All rights reserved. + */ + +using System; +using System.Data; +using System.Threading; +using NUnit.Framework; +using Snowflake.Data.Core; +using Snowflake.Data.Client; +using Snowflake.Data.Core.Session; +using Snowflake.Data.Log; +using Snowflake.Data.Tests.Util; + +namespace Snowflake.Data.Tests.IntegrationTests +{ + [TestFixture(ConnectionPoolType.SingleConnectionCache)] + [TestFixture(ConnectionPoolType.MultipleConnectionPool)] + [NonParallelizable] + class ConnectionPoolCommonIT : SFBaseTest + { + private readonly ConnectionPoolType _connectionPoolTypeUnderTest; + private static readonly SFLogger s_logger = SFLoggerFactory.GetLogger(); + private readonly PoolConfig _previousPoolConfig; + + public ConnectionPoolCommonIT(ConnectionPoolType connectionPoolTypeUnderTest) + { + _connectionPoolTypeUnderTest = connectionPoolTypeUnderTest; + _previousPoolConfig = new PoolConfig(); + } + + [SetUp] + public new void BeforeTest() + { + SnowflakeDbConnectionPool.SetConnectionPoolVersion(_connectionPoolTypeUnderTest); + SnowflakeDbConnectionPool.ClearAllPools(); + if (_connectionPoolTypeUnderTest == ConnectionPoolType.SingleConnectionCache) + { + SnowflakeDbConnectionPool.SetPooling(true); + } + s_logger.Debug($"---------------- BeforeTest ---------------------"); + s_logger.Debug($"Testing Pool Type: {SnowflakeDbConnectionPool.GetConnectionPoolVersion()}"); + } + + [TearDown] + public new void AfterTest() + { + _previousPoolConfig.Reset(); + } + + [OneTimeTearDown] + public static void AfterAllTests() + { + SnowflakeDbConnectionPool.ClearAllPools(); + } + + [Test] + public void TestConnectionPoolMultiThreading() + { + Thread t1 = new Thread(() => ThreadProcess1(ConnectionString)); + Thread t2 = new Thread(() => ThreadProcess2(ConnectionString)); + + t1.Start(); + t2.Start(); + + t1.Join(); + t2.Join(); + } + + void ThreadProcess1(string connstr) + { + var conn1 = new SnowflakeDbConnection(); + conn1.ConnectionString = connstr; + conn1.Open(); + Thread.Sleep(1000); + conn1.Close(); + Thread.Sleep(4000); + Assert.AreEqual(ConnectionState.Closed, conn1.State); + } + + void ThreadProcess2(string connstr) + { + var conn1 = new SnowflakeDbConnection(); + conn1.ConnectionString = connstr; + conn1.Open(); + + Thread.Sleep(5000); + SFStatement statement = new SFStatement(conn1.SfSession); + SFBaseResultSet resultSet = statement.Execute(0, "select 1", null, false); + Assert.AreEqual(true, resultSet.Next()); + Assert.AreEqual("1", resultSet.GetString(0)); + conn1.Close(); + } + + [Test] + public void TestConnectionPoolWithDispose() + { + if (_connectionPoolTypeUnderTest == ConnectionPoolType.SingleConnectionCache) + { + SnowflakeDbConnectionPool.SetMaxPoolSize(1); + } + var conn1 = new SnowflakeDbConnection(); + conn1.ConnectionString = "bad connection string"; + Assert.Throws(() => conn1.Open()); + conn1.Close(); + Thread.Sleep(3000); // minPoolSize = 2 causes that another thread has been started. We sleep to make that thread finish. + + Assert.AreEqual(ConnectionState.Closed, conn1.State); + Assert.AreEqual(0, SnowflakeDbConnectionPool.GetPool(conn1.ConnectionString).GetCurrentPoolSize()); + } + + [Test] + public void TestFailWhenPreventingFromReturningToPoolNotOpenedConnection() + { + // arrange + var connection = new SnowflakeDbConnection(ConnectionString); + + // act + var thrown = Assert.Throws(() => connection.PreventPooling()); + + // assert + Assert.That(thrown.Message, Does.Contain("Session not yet created for this connection. Unable to prevent the session from pooling")); + } + } +} diff --git a/Snowflake.Data.Tests/IntegrationTests/SFConnectionPoolIT.cs b/Snowflake.Data.Tests/IntegrationTests/ConnectionSinglePoolCacheIT.cs similarity index 66% rename from Snowflake.Data.Tests/IntegrationTests/SFConnectionPoolIT.cs rename to Snowflake.Data.Tests/IntegrationTests/ConnectionSinglePoolCacheIT.cs index 5c4529225..3ec344f63 100644 --- a/Snowflake.Data.Tests/IntegrationTests/SFConnectionPoolIT.cs +++ b/Snowflake.Data.Tests/IntegrationTests/ConnectionSinglePoolCacheIT.cs @@ -1,52 +1,57 @@ -/* - * Copyright (c) 2012-2023 Snowflake Computing Inc. All rights reserved. - */ - -using Snowflake.Data.Tests.Util; using System; using System.Data; using System.Data.Common; -using System.Threading; using System.Threading.Tasks; -using Snowflake.Data.Core; -using Snowflake.Data.Client; -using Snowflake.Data.Log; using NUnit.Framework; +using Snowflake.Data.Client; +using Snowflake.Data.Core.Session; +using Snowflake.Data.Tests.Mock; +using Snowflake.Data.Tests.Util; +using Moq; namespace Snowflake.Data.Tests.IntegrationTests { - [TestFixture, NonParallelizable] - class SFConnectionPoolIT : SFBaseTest + [TestFixture] + [NonParallelizable] + public class ConnectionSinglePoolCacheIT: SFBaseTest { - private static PoolConfig s_previousPoolConfig; + private readonly PoolConfig _previousPoolConfig = new PoolConfig(); - [OneTimeSetUp] - public static void BeforeAllTests() - { - s_previousPoolConfig = new PoolConfig(); - } - [SetUp] public new void BeforeTest() { - SnowflakeDbConnectionPool.SetPooling(true); + SnowflakeDbConnectionPool.SetConnectionPoolVersion(ConnectionPoolType.SingleConnectionCache); SnowflakeDbConnectionPool.ClearAllPools(); + SnowflakeDbConnectionPool.SetPooling(true); } - + [TearDown] public new void AfterTest() { - s_previousPoolConfig.Reset(); + _previousPoolConfig.Reset(); } - + [OneTimeTearDown] public static void AfterAllTests() { SnowflakeDbConnectionPool.ClearAllPools(); + } + + [Test] + public void TestBasicConnectionPool() + { + SnowflakeDbConnectionPool.SetMaxPoolSize(1); + + var conn1 = new SnowflakeDbConnection(ConnectionString); + conn1.Open(); + Assert.AreEqual(ConnectionState.Open, conn1.State); + conn1.Close(); + + Assert.AreEqual(ConnectionState.Closed, conn1.State); + Assert.AreEqual(1, SnowflakeDbConnectionPool.GetPool(ConnectionString).GetCurrentPoolSize()); } [Test] - // test connection pooling with concurrent connection public void TestConcurrentConnectionPooling() { // add test case name in connection string to make in unique for each test case @@ -74,6 +79,7 @@ static void ConcurrentPoolingHelper(string connectionString, bool closeConnectio const int PoolTimeout = 3; // reset to default settings in case it changed by other test cases + Assert.AreEqual(true, SnowflakeDbConnectionPool.GetPool(connectionString).GetPooling()); // to instantiate pool SnowflakeDbConnectionPool.SetMaxPoolSize(10); SnowflakeDbConnectionPool.SetTimeout(PoolTimeout); @@ -86,10 +92,8 @@ static void ConcurrentPoolingHelper(string connectionString, bool closeConnectio }); } Task.WaitAll(threads); - // set pooling timeout back to default to avoid impact on other test cases - SnowflakeDbConnectionPool.SetTimeout(3600); } - + // thead to execute query with new connection in a loop static void QueryExecutionThread(string connectionString, bool closeConnection) { @@ -130,45 +134,31 @@ static void QueryExecutionThread(string connectionString, bool closeConnection) } [Test] - public void TestBasicConnectionPool() + public void TestPoolContainsClosedConnections() // old name: TestConnectionPool { - SnowflakeDbConnectionPool.SetMaxPoolSize(1); - var conn1 = new SnowflakeDbConnection(ConnectionString); conn1.Open(); Assert.AreEqual(ConnectionState.Open, conn1.State); conn1.Close(); - - Assert.AreEqual(ConnectionState.Closed, conn1.State); - Assert.AreEqual(1, SnowflakeDbConnectionPool.GetCurrentPoolSize()); - } - - [Test] - public void TestConnectionPool() - { - var conn1 = new SnowflakeDbConnection(ConnectionString); - conn1.Open(); - Assert.AreEqual(ConnectionState.Open, conn1.State); - conn1.Close(); - Assert.AreEqual(1, SnowflakeDbConnectionPool.GetCurrentPoolSize()); + Assert.AreEqual(1, SnowflakeDbConnectionPool.GetPool(ConnectionString).GetCurrentPoolSize()); var conn2 = new SnowflakeDbConnection(); conn2.ConnectionString = ConnectionString; conn2.Open(); Assert.AreEqual(ConnectionState.Open, conn2.State); - Assert.AreEqual(0, SnowflakeDbConnectionPool.GetCurrentPoolSize()); + Assert.AreEqual(0, SnowflakeDbConnectionPool.GetPool(ConnectionString).GetCurrentPoolSize()); conn2.Close(); - Assert.AreEqual(1, SnowflakeDbConnectionPool.GetCurrentPoolSize()); + Assert.AreEqual(1, SnowflakeDbConnectionPool.GetPool(ConnectionString).GetCurrentPoolSize()); Assert.AreEqual(ConnectionState.Closed, conn1.State); Assert.AreEqual(ConnectionState.Closed, conn2.State); - SnowflakeDbConnectionPool.ClearAllPools(); } - + [Test] - public void TestConnectionPoolIsFull() + public void TestPoolContainsAtMostMaxPoolSizeConnections() // old name: TestConnectionPoolFull { SnowflakeDbConnectionPool.SetMaxPoolSize(2); + var conn1 = new SnowflakeDbConnection(); conn1.ConnectionString = ConnectionString; conn1.Open(); @@ -178,53 +168,77 @@ public void TestConnectionPoolIsFull() conn2.ConnectionString = ConnectionString + " retryCount=1"; conn2.Open(); Assert.AreEqual(ConnectionState.Open, conn2.State); - + Assert.AreEqual(0, SnowflakeDbConnectionPool.GetCurrentPoolSize()); + conn1.Close(); + conn2.Close(); + Assert.AreEqual(2, SnowflakeDbConnectionPool.GetCurrentPoolSize()); var conn3 = new SnowflakeDbConnection(); conn3.ConnectionString = ConnectionString + " retryCount=2"; conn3.Open(); Assert.AreEqual(ConnectionState.Open, conn3.State); - SnowflakeDbConnectionPool.ClearAllPools(); - conn1.Close(); - Assert.AreEqual(1, SnowflakeDbConnectionPool.GetCurrentPoolSize()); - conn2.Close(); - Assert.AreEqual(2, SnowflakeDbConnectionPool.GetCurrentPoolSize()); + var conn4 = new SnowflakeDbConnection(); + conn4.ConnectionString = ConnectionString + " retryCount=3"; + conn4.Open(); + Assert.AreEqual(ConnectionState.Open, conn4.State); + conn3.Close(); Assert.AreEqual(2, SnowflakeDbConnectionPool.GetCurrentPoolSize()); + conn4.Close(); + Assert.AreEqual(2, SnowflakeDbConnectionPool.GetCurrentPoolSize()); Assert.AreEqual(ConnectionState.Closed, conn1.State); Assert.AreEqual(ConnectionState.Closed, conn2.State); Assert.AreEqual(ConnectionState.Closed, conn3.State); + Assert.AreEqual(ConnectionState.Closed, conn4.State); SnowflakeDbConnectionPool.ClearAllPools(); } [Test] - public void TestConnectionPoolExpirationWorks() + public void TestConnectionPoolDisableFromPoolManagerLevel() { - SnowflakeDbConnectionPool.SetMaxPoolSize(2); - SnowflakeDbConnectionPool.SetTimeout(10); - + // arrange + SnowflakeDbConnectionPool.SetPooling(false); var conn1 = new SnowflakeDbConnection(); conn1.ConnectionString = ConnectionString; - + + // act conn1.Open(); + + // assert + Assert.AreEqual(ConnectionState.Open, conn1.State); + Assert.AreEqual(0, SnowflakeDbConnectionPool.GetCurrentPoolSize()); + + // act conn1.Close(); - SnowflakeDbConnectionPool.SetTimeout(-1); - - var conn2 = new SnowflakeDbConnection(); - conn2.ConnectionString = ConnectionString; - conn2.Open(); - conn2.Close(); - var conn3 = new SnowflakeDbConnection(); - conn3.ConnectionString = ConnectionString; - conn3.Open(); - conn3.Close(); - // The pooling timeout should apply to all connections being pooled, - // not just the connections created after the new setting, - // so expected result should be 0 + // assert + Assert.AreEqual(ConnectionState.Closed, conn1.State); Assert.AreEqual(0, SnowflakeDbConnectionPool.GetCurrentPoolSize()); - SnowflakeDbConnectionPool.SetPooling(false); + } + + [Test] + public void TestConnectionPoolDisable() + { + // arrange + var pool = SnowflakeDbConnectionPool.GetPool(ConnectionString); + pool.SetPooling(false); + var conn1 = new SnowflakeDbConnection(); + conn1.ConnectionString = ConnectionString; + + // act + conn1.Open(); + + // assert + Assert.AreEqual(ConnectionState.Open, conn1.State); + Assert.AreEqual(0, pool.GetCurrentPoolSize()); + + // act + conn1.Close(); + + // assert + Assert.AreEqual(ConnectionState.Closed, conn1.State); + Assert.AreEqual(0, pool.GetCurrentPoolSize()); } [Test] @@ -257,143 +271,75 @@ public void TestConnectionPoolClean() Assert.AreEqual(ConnectionState.Closed, conn1.State); Assert.AreEqual(ConnectionState.Closed, conn2.State); Assert.AreEqual(ConnectionState.Closed, conn3.State); - SnowflakeDbConnectionPool.ClearAllPools(); } - + [Test] - public void TestConnectionPoolFull() + public void TestConnectionPoolExpirationWorks() { SnowflakeDbConnectionPool.SetMaxPoolSize(2); + SnowflakeDbConnectionPool.SetTimeout(10); var conn1 = new SnowflakeDbConnection(); conn1.ConnectionString = ConnectionString; + conn1.Open(); - Assert.AreEqual(ConnectionState.Open, conn1.State); + conn1.Close(); + SnowflakeDbConnectionPool.SetTimeout(0); var conn2 = new SnowflakeDbConnection(); - conn2.ConnectionString = ConnectionString + " retryCount=1"; + conn2.ConnectionString = ConnectionString; conn2.Open(); - Assert.AreEqual(ConnectionState.Open, conn2.State); - Assert.AreEqual(0, SnowflakeDbConnectionPool.GetCurrentPoolSize()); - conn1.Close(); conn2.Close(); - Assert.AreEqual(2, SnowflakeDbConnectionPool.GetCurrentPoolSize()); + var conn3 = new SnowflakeDbConnection(); - conn3.ConnectionString = ConnectionString + " retryCount=2"; + conn3.ConnectionString = ConnectionString; conn3.Open(); - Assert.AreEqual(ConnectionState.Open, conn3.State); - - var conn4 = new SnowflakeDbConnection(); - conn4.ConnectionString = ConnectionString + " retryCount=3"; - conn4.Open(); - Assert.AreEqual(ConnectionState.Open, conn4.State); - conn3.Close(); - Assert.AreEqual(2, SnowflakeDbConnectionPool.GetCurrentPoolSize()); - conn4.Close(); - Assert.AreEqual(2, SnowflakeDbConnectionPool.GetCurrentPoolSize()); - - Assert.AreEqual(ConnectionState.Closed, conn1.State); - Assert.AreEqual(ConnectionState.Closed, conn2.State); - Assert.AreEqual(ConnectionState.Closed, conn3.State); - Assert.AreEqual(ConnectionState.Closed, conn4.State); - SnowflakeDbConnectionPool.ClearAllPools(); - } - - [Test] - public void TestConnectionPoolMultiThreading() - { - Thread t1 = new Thread(() => ThreadProcess1(ConnectionString)); - Thread t2 = new Thread(() => ThreadProcess2(ConnectionString)); - - t1.Start(); - t2.Start(); - - t1.Join(); - t2.Join(); - } - - void ThreadProcess1(string connstr) - { - var conn1 = new SnowflakeDbConnection(); - conn1.ConnectionString = connstr; - conn1.Open(); - Thread.Sleep(1000); - conn1.Close(); - Thread.Sleep(4000); - Assert.AreEqual(ConnectionState.Closed, conn1.State); - } - - void ThreadProcess2(string connstr) - { - var conn1 = new SnowflakeDbConnection(); - conn1.ConnectionString = connstr; - conn1.Open(); - Thread.Sleep(5000); - SFStatement statement = new SFStatement(conn1.SfSession); - SFBaseResultSet resultSet = statement.Execute(0, "select 1", null, false); - Assert.AreEqual(true, resultSet.Next()); - Assert.AreEqual("1", resultSet.GetString(0)); - SnowflakeDbConnectionPool.ClearAllPools(); - SnowflakeDbConnectionPool.SetMaxPoolSize(0); - SnowflakeDbConnectionPool.SetPooling(false); + // The pooling timeout should apply to all connections being pooled, + // not just the connections created after the new setting, + // so expected result should be 0 + Assert.AreEqual(0, SnowflakeDbConnectionPool.GetPool(ConnectionString).GetCurrentPoolSize()); } - + [Test] - public void TestConnectionPoolDisable() + public void TestPreventConnectionFromReturningToPool() { - SnowflakeDbConnectionPool.SetPooling(false); - - var conn1 = new SnowflakeDbConnection(); - conn1.ConnectionString = ConnectionString; - conn1.Open(); - Assert.AreEqual(ConnectionState.Open, conn1.State); - conn1.Close(); - - Assert.AreEqual(ConnectionState.Closed, conn1.State); - Assert.AreEqual(0, SnowflakeDbConnectionPool.GetCurrentPoolSize()); + // arrange + var connection = new SnowflakeDbConnection(ConnectionString); + connection.Open(); + var pool = SnowflakeDbConnectionPool.GetPool(ConnectionString); + Assert.AreEqual(0, pool.GetCurrentPoolSize()); + + // act + connection.PreventPooling(); + connection.Close(); + + // assert + Assert.AreEqual(0, pool.GetCurrentPoolSize()); } - + [Test] - public void TestConnectionPoolWithDispose() + public void TestReleaseConnectionWhenRollbackFails() { - SnowflakeDbConnectionPool.SetMaxPoolSize(1); - - var conn1 = new SnowflakeDbConnection(); - conn1.ConnectionString = ""; - try - { - conn1.Open(); - } - catch (SnowflakeDbException ex) - { - conn1.Close(); - } - - Assert.AreEqual(ConnectionState.Closed, conn1.State); + // arrange + SnowflakeDbConnectionPool.SetMaxPoolSize(10); + var commandThrowingExceptionOnlyForRollback = MockHelper.CommandThrowingExceptionOnlyForRollback(); + var mockDbProviderFactory = new Mock(); + mockDbProviderFactory.Setup(p => p.CreateCommand()).Returns(commandThrowingExceptionOnlyForRollback.Object); Assert.AreEqual(0, SnowflakeDbConnectionPool.GetCurrentPoolSize()); - } - - [Test] - public void TestConnectionPoolTurnOff() - { - SnowflakeDbConnectionPool.SetPooling(false); - SnowflakeDbConnectionPool.SetPooling(true); - SnowflakeDbConnectionPool.SetMaxPoolSize(1); - SnowflakeDbConnectionPool.ClearAllPools(); - - var conn1 = new SnowflakeDbConnection(); - conn1.ConnectionString = ConnectionString; - conn1.Open(); - Assert.AreEqual(ConnectionState.Open, conn1.State); - conn1.Close(); - - Assert.AreEqual(ConnectionState.Closed, conn1.State); - Assert.AreEqual(1, SnowflakeDbConnectionPool.GetCurrentPoolSize()); - - SnowflakeDbConnectionPool.SetPooling(false); - //Put a breakpoint at SFSession close function, after connection pool is off, it will send close session request. + var connection = new TestSnowflakeDbConnection(mockDbProviderFactory.Object); + connection.ConnectionString = ConnectionString; + connection.Open(); + connection.BeginTransaction(); + Assert.AreEqual(true, connection.HasActiveExplicitTransaction()); + // no Rollback or Commit; during internal Rollback while closing a connection a mocked exception will be thrown + + // act + connection.Close(); + + // assert + Assert.AreEqual(0, SnowflakeDbConnectionPool.GetCurrentPoolSize(), "Should not return connection to the pool"); } } } diff --git a/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs b/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs deleted file mode 100644 index eea852af2..000000000 --- a/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs +++ /dev/null @@ -1,2018 +0,0 @@ -/* - * Copyright (c) 2012-2021 Snowflake Computing Inc. All rights reserved. - */ - -using System.Data.Common; - -namespace Snowflake.Data.Tests.IntegrationTests -{ - using NUnit.Framework; - using Snowflake.Data.Client; - using System.Data; - using System; - using Snowflake.Data.Core; - using System.Threading.Tasks; - using System.Threading; - using Snowflake.Data.Log; - using System.Diagnostics; - using Snowflake.Data.Tests.Mock; - using System.Runtime.InteropServices; - - [TestFixture] - class SFConnectionIT : SFBaseTest - { - private static readonly SFLogger s_logger = SFLoggerFactory.GetLogger(); - - [Test] - public void TestBasicConnection() - { - using (IDbConnection conn = new SnowflakeDbConnection()) - { - conn.ConnectionString = ConnectionString; - conn.Open(); - Assert.AreEqual(ConnectionState.Open, conn.State); - - Assert.AreEqual(SFSessionHttpClientProperties.s_retryTimeoutDefault, conn.ConnectionTimeout); - // Data source is empty string for now - Assert.AreEqual("", ((SnowflakeDbConnection)conn).DataSource); - - string serverVersion = ((SnowflakeDbConnection)conn).ServerVersion; - if (!string.Equals(serverVersion, "Dev")) - { - string[] versionElements = serverVersion.Split('.'); - Assert.AreEqual(3, versionElements.Length); - } - - conn.Close(); - Assert.AreEqual(ConnectionState.Closed, conn.State); - } - } - - [Test] - public void TestApplicationName() - { - string[] validApplicationNames = { "test1234", "test_1234", "test-1234", "test.1234"}; - string[] invalidApplicationNames = { "1234test", "test$A", "test