From 9d367be714b7db84ff53037dcbfcaede36d4a31d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Hofman?= Date: Mon, 16 Oct 2023 14:21:57 +0200 Subject: [PATCH] SNOW-902608 unit tests for Connection Pool Manager; introduction of SessionFactory for unit testing of connection pool manager and session pooling --- .../UnitTests/ConnectionPoolManagerTest.cs | 215 ++++++++++++++++++ .../Client/SnowflakeDbConnectionPool.cs | 8 +- .../Core/Session/ConnectionManagerV1.cs | 2 +- ...nManagerV2.cs => ConnectionPoolManager.cs} | 8 +- .../Core/Session/ISessionFactory.cs | 9 + Snowflake.Data/Core/Session/SFSession.cs | 4 +- Snowflake.Data/Core/Session/SessionFactory.cs | 12 + Snowflake.Data/Core/Session/SessionPool.cs | 25 +- 8 files changed, 260 insertions(+), 23 deletions(-) create mode 100644 Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerTest.cs rename Snowflake.Data/Core/Session/{ConnectionManagerV2.cs => ConnectionPoolManager.cs} (93%) create mode 100644 Snowflake.Data/Core/Session/ISessionFactory.cs create mode 100644 Snowflake.Data/Core/Session/SessionFactory.cs diff --git a/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerTest.cs b/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerTest.cs new file mode 100644 index 000000000..39851c084 --- /dev/null +++ b/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerTest.cs @@ -0,0 +1,215 @@ +/* + * Copyright (c) 2023 Snowflake Computing Inc. All rights reserved. + */ + +using System; +using System.Security; +using System.Threading; +using System.Threading.Tasks; +using NUnit.Framework; +using Snowflake.Data.Core; +using Snowflake.Data.Core.Session; +using Moq; + +namespace Snowflake.Data.Tests.UnitTests +{ + [TestFixture, NonParallelizable] + class ConnectionPoolManagerTest + { + private readonly ConnectionPoolManager _connectionPoolManager = new ConnectionPoolManager(); + private readonly string _connectionString1 = "database=D1;warehouse=W1;account=A1;user=U1;password=P1;role=R1;"; + private readonly string _connectionString2 = "database=D2;warehouse=W2;account=A2;user=U2;password=P2;role=R2;"; + private readonly SecureString _password = new SecureString(); + + [OneTimeSetUp] + public static void BeforeAllTests() + { + // SnowflakeDbConnectionPool.SwapVersion(); // TODO: swap when new version is the default + SessionPool.SessionFactory = new MockSessionFactory(); + } + + [OneTimeTearDown] + public void AfterAllTests() + { + SessionPool.SessionFactory = new SessionFactory(); + } + + [Test] + public void TestPoolManagerReturnsSessionPoolForGivenConnectionString() + { + // Act + var sessionPool = _connectionPoolManager.GetPool(_connectionString1, _password); + + // Assert + Assert.AreEqual(_connectionString1, sessionPool.ConnectionString); + Assert.AreEqual(_password, sessionPool.Password); + } + + [Test] + public void TestPoolManagerReturnsSamePoolForGivenConnectionString() + { + // Arrange + var anotherConnectionString = _connectionString1; + + // Act + var sessionPool1 = _connectionPoolManager.GetPool(_connectionString1, _password); + var sessionPool2 = _connectionPoolManager.GetPool(anotherConnectionString, _password); + + // Assert + Assert.AreEqual(sessionPool1, sessionPool2); + } + + [Test] + public void TestDifferentPoolsAreReturnedForDifferentConnectionStrings() + { + // Arrange + Assert.AreNotSame(_connectionString1, _connectionString2); + + // Act + var sessionPool1 = _connectionPoolManager.GetPool(_connectionString1, _password); + var sessionPool2 = _connectionPoolManager.GetPool(_connectionString2, _password); + + // Assert + Assert.AreNotSame(sessionPool1, sessionPool2); + Assert.AreEqual(_connectionString1, sessionPool1.ConnectionString); + Assert.AreEqual(_connectionString2, sessionPool2.ConnectionString); + } + + + [Test] + public void TestGetSessionWorksForSpecifiedConnectionString() + { + // Act + var sfSession = _connectionPoolManager.GetSession(_connectionString1, _password); + + // Assert + Assert.AreEqual(_connectionString1, sfSession.ConnectionString); + Assert.AreEqual(_password, sfSession.Password); + } + + [Test] + public async Task TestGetSessionAsyncWorksForSpecifiedConnectionString() + { + // Act + var sfSession = await _connectionPoolManager.GetSessionAsync(_connectionString1, _password, CancellationToken.None); + + // Assert + Assert.AreEqual(_connectionString1, sfSession.ConnectionString); + Assert.AreEqual(_password, sfSession.Password); + } + + [Test] + [Ignore("Enable after completion of SNOW-937189")] // TODO: + public void TestCountingOfSessionProvidedByPool() + { + // Act + _connectionPoolManager.GetSession(_connectionString1, _password); + + // Assert + var sessionPool = _connectionPoolManager.GetPool(_connectionString1, _password); + Assert.AreEqual(1, sessionPool.GetCurrentPoolSize()); + } + + [Test] + [Ignore("Enable after completion of SNOW-937189")] // TODO: + public void TestCountingOfSessionReturnedBackToPool() + { + // Arrange + var sfSession = _connectionPoolManager.GetSession(_connectionString1, _password); + + // Act + _connectionPoolManager.AddSession(sfSession); + + // Assert + var sessionPool = _connectionPoolManager.GetPool(_connectionString1, _password); + Assert.AreEqual(1, sessionPool.GetCurrentPoolSize()); + } + + [Test] + public void TestSetMaxPoolSizeForAllPools() + { + // Arrange + var sessionPool1 = _connectionPoolManager.GetPool(_connectionString1, _password); + var sessionPool2 = _connectionPoolManager.GetPool(_connectionString2, _password); + + // Act + _connectionPoolManager.SetMaxPoolSize(3); + + // Assert + Assert.AreEqual(3, sessionPool1.GetMaxPoolSize()); + Assert.AreEqual(3, sessionPool2.GetMaxPoolSize()); + } + + [Test] + public void TestSetTimeoutForAllPools() + { + // Arrange + var sessionPool1 = _connectionPoolManager.GetPool(_connectionString1, _password); + var sessionPool2 = _connectionPoolManager.GetPool(_connectionString2, _password); + + // Act + _connectionPoolManager.SetTimeout(3000); + + // Assert + Assert.AreEqual(3000, sessionPool1.GetTimeout()); + Assert.AreEqual(3000, sessionPool2.GetTimeout()); + } + + [Test] + public void TestSetPoolingDisabledForAllPools() + { + // Arrange + var sessionPool1 = _connectionPoolManager.GetPool(_connectionString1, _password); + + // Act + _connectionPoolManager.SetPooling(false); + + // Assert + Assert.AreEqual(false, sessionPool1.GetPooling()); + } + + [Test] + public void TestSetPoolingEnabledBack() + { + // Arrange + var sessionPool1 = _connectionPoolManager.GetPool(_connectionString1, _password); + _connectionPoolManager.SetPooling(false); + + // Act + _connectionPoolManager.SetPooling(true); + + // Assert + Assert.AreEqual(true, sessionPool1.GetPooling()); + } + + [Test] + public void TestGetPoolingOnManagerLevelNotSupported() + { + Assert.Throws(() => _connectionPoolManager.GetPooling()); + } + + [Test] + public void TestGetTimeoutOnManagerLevelNotSupported() + { + Assert.Throws(() => _connectionPoolManager.GetTimeout()); + } + + [Test] + public void TestGetMaxPoolSizeOnManagerLevelNotSupported() + { + Assert.Throws(() => _connectionPoolManager.GetMaxPoolSize()); + } + } + + class MockSessionFactory : ISessionFactory + { + public SFSession NewSession(string connectionString, SecureString password) + { + var mockSfSession = new Mock(connectionString, password); + mockSfSession.Setup(x => x.Open()).Verifiable(); + mockSfSession.Setup(x => x.OpenAsync(default)).Returns(Task.FromResult(this)); + return mockSfSession.Object; + } + } + +} \ No newline at end of file diff --git a/Snowflake.Data/Client/SnowflakeDbConnectionPool.cs b/Snowflake.Data/Client/SnowflakeDbConnectionPool.cs index 265c05f63..54a3be0e7 100644 --- a/Snowflake.Data/Client/SnowflakeDbConnectionPool.cs +++ b/Snowflake.Data/Client/SnowflakeDbConnectionPool.cs @@ -25,7 +25,7 @@ private static IConnectionManager ConnectionManager return s_connectionManager; lock (s_connectionManagerInstanceLock) { - s_connectionManager = new ConnectionManagerV1(); // old implementation of the pool as a default + s_connectionManager = new ConnectionManagerV1(); // TODO: change to ConnectionPoolManager once new pool implementation is complete } return s_connectionManager; } @@ -97,16 +97,16 @@ public static bool GetPooling() return ConnectionManager.GetPooling(); } - internal static void SwapVersion() + internal static void SwapVersion() // TODO: make public once development of entire ConnectionPoolManager is complete { lock (s_connectionManagerInstanceLock) { if (ConnectionManager is ConnectionManagerV1) { s_connectionManager.ClearAllPools(); - s_connectionManager = new ConnectionManagerV2(); + s_connectionManager = new ConnectionPoolManager(); } - if (ConnectionManager is ConnectionManagerV2) + if (ConnectionManager is ConnectionPoolManager) { s_connectionManager.ClearAllPools(); s_connectionManager = new ConnectionManagerV1(); diff --git a/Snowflake.Data/Core/Session/ConnectionManagerV1.cs b/Snowflake.Data/Core/Session/ConnectionManagerV1.cs index e93f176a4..611ce449e 100644 --- a/Snowflake.Data/Core/Session/ConnectionManagerV1.cs +++ b/Snowflake.Data/Core/Session/ConnectionManagerV1.cs @@ -10,7 +10,7 @@ namespace Snowflake.Data.Core.Session { internal sealed class ConnectionManagerV1 : IConnectionManager { - private readonly SessionPool _sessionPool = SessionPool.CreateSessionPoolV1(); + private readonly SessionPool _sessionPool = SessionPool.CreateSessionCache(); public SFSession GetSession(string connectionString, SecureString password) => _sessionPool.GetSession(connectionString, password); public Task GetSessionAsync(string connectionString, SecureString password, CancellationToken cancellationToken) => _sessionPool.GetSessionAsync(connectionString, password, cancellationToken); diff --git a/Snowflake.Data/Core/Session/ConnectionManagerV2.cs b/Snowflake.Data/Core/Session/ConnectionPoolManager.cs similarity index 93% rename from Snowflake.Data/Core/Session/ConnectionManagerV2.cs rename to Snowflake.Data/Core/Session/ConnectionPoolManager.cs index e0042ac53..1de2d7f6e 100644 --- a/Snowflake.Data/Core/Session/ConnectionManagerV2.cs +++ b/Snowflake.Data/Core/Session/ConnectionPoolManager.cs @@ -11,13 +11,13 @@ namespace Snowflake.Data.Core.Session { - internal sealed class ConnectionManagerV2 : IConnectionManager + internal sealed class ConnectionPoolManager : IConnectionManager { - private static readonly SFLogger s_logger = SFLoggerFactory.GetLogger(); + private static readonly SFLogger s_logger = SFLoggerFactory.GetLogger(); private static readonly Object s_poolsLock = new Object(); private readonly Dictionary _pools; - internal ConnectionManagerV2() + internal ConnectionPoolManager() { lock (s_poolsLock) { @@ -92,7 +92,7 @@ internal SessionPool GetPool(string connectionString, SecureString password) return item; lock (s_poolsLock) { - var pool = SessionPool.CreateSessionPoolV2(connectionString, password); + var pool = SessionPool.CreateSessionPool(connectionString, password); _pools.Add(poolKey, pool); return pool; } diff --git a/Snowflake.Data/Core/Session/ISessionFactory.cs b/Snowflake.Data/Core/Session/ISessionFactory.cs new file mode 100644 index 000000000..f9416de8d --- /dev/null +++ b/Snowflake.Data/Core/Session/ISessionFactory.cs @@ -0,0 +1,9 @@ +using System.Security; + +namespace Snowflake.Data.Core.Session +{ + internal interface ISessionFactory + { + SFSession NewSession(string connectionString, SecureString password); + } +} diff --git a/Snowflake.Data/Core/Session/SFSession.cs b/Snowflake.Data/Core/Session/SFSession.cs index 625664795..99b7e66de 100755 --- a/Snowflake.Data/Core/Session/SFSession.cs +++ b/Snowflake.Data/Core/Session/SFSession.cs @@ -216,7 +216,7 @@ internal Uri BuildUri(string path, Dictionary queryParams = null return uriBuilder.Uri; } - internal void Open() + internal virtual void Open() { logger.Debug("Open Session"); @@ -228,7 +228,7 @@ internal void Open() authenticator.Authenticate(); } - internal async Task OpenAsync(CancellationToken cancellationToken) + internal virtual async Task OpenAsync(CancellationToken cancellationToken) { logger.Debug("Open Session Async"); diff --git a/Snowflake.Data/Core/Session/SessionFactory.cs b/Snowflake.Data/Core/Session/SessionFactory.cs new file mode 100644 index 000000000..0df4a557d --- /dev/null +++ b/Snowflake.Data/Core/Session/SessionFactory.cs @@ -0,0 +1,12 @@ +using System.Security; + +namespace Snowflake.Data.Core.Session +{ + internal class SessionFactory : ISessionFactory + { + public SFSession NewSession(string connectionString, SecureString password) + { + return new SFSession(connectionString, password); + } + } +} \ No newline at end of file diff --git a/Snowflake.Data/Core/Session/SessionPool.cs b/Snowflake.Data/Core/Session/SessionPool.cs index e2c037169..a4e477b84 100644 --- a/Snowflake.Data/Core/Session/SessionPool.cs +++ b/Snowflake.Data/Core/Session/SessionPool.cs @@ -22,12 +22,13 @@ sealed class SessionPool : IDisposable private long _timeout; private const int MaxPoolSize = 10; private const long Timeout = 3600; - private string _connectionString; - private SecureString _password; + internal string ConnectionString; + internal SecureString Password; private bool _pooling = true; private bool _allowExceedMaxPoolSize = true; + internal static ISessionFactory SessionFactory = new SessionFactory(); - internal SessionPool() + private SessionPool() { lock (s_sessionPoolLock) { @@ -37,16 +38,16 @@ internal SessionPool() } } - internal SessionPool(string connectionString, SecureString password) : this() + private SessionPool(string connectionString, SecureString password) : this() { - _connectionString = connectionString; - _password = password; + ConnectionString = connectionString; + Password = password; _allowExceedMaxPoolSize = false; // TODO: SNOW-937190 } - internal static SessionPool CreateSessionPoolV1() => new SessionPool(); + internal static SessionPool CreateSessionCache() => new SessionPool(); - internal static SessionPool CreateSessionPoolV2(string connectionString, SecureString password) => + internal static SessionPool CreateSessionPool(string connectionString, SecureString password) => new SessionPool(connectionString, password); ~SessionPool() @@ -95,10 +96,10 @@ internal Task GetSessionAsync(string connStr, SecureString password, return session != null ? Task.FromResult(session) : NewSessionAsync(connStr, password, cancellationToken); } - internal SFSession GetSession() => GetSession(_connectionString, _password); + internal SFSession GetSession() => GetSession(ConnectionString, Password); internal Task GetSessionAsync(CancellationToken cancellationToken) => - GetSessionAsync(_connectionString, _password, cancellationToken); + GetSessionAsync(ConnectionString, Password, cancellationToken); private SFSession GetIdleSession(string connStr) { @@ -133,7 +134,7 @@ private SFSession NewSession(String connectionString, SecureString password) s_logger.Debug("SessionPool::NewSession"); try { - var session = new SFSession(connectionString, password); + var session = SessionFactory.NewSession(connectionString, password); session.Open(); return session; } @@ -153,7 +154,7 @@ private SFSession NewSession(String connectionString, SecureString password) private Task NewSessionAsync(String connectionString, SecureString password, CancellationToken cancellationToken) { s_logger.Debug("SessionPool::NewSessionAsync"); - var session = new SFSession(connectionString, password); + var session = SessionFactory.NewSession(connectionString, password); return session .OpenAsync(cancellationToken) .ContinueWith(previousTask =>