Skip to content

Commit

Permalink
Added more tests
Browse files Browse the repository at this point in the history
  • Loading branch information
trasch committed May 11, 2023
1 parent 6289953 commit 2be276f
Show file tree
Hide file tree
Showing 5 changed files with 259 additions and 75 deletions.
46 changes: 46 additions & 0 deletions Sources/PostgresConnectionPool/PoolInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,49 @@ public struct PoolInfo {
public let shutdownError: PSQLError?

}

// MARK: - CustomStringConvertible

extension PoolInfo: CustomStringConvertible {

public var description: String {
var lines: [String] = [
"Pool: \(name)",
"Connections: \(openConnections)/\(activeConnections)/\(availableConnections) (open/active/available)",
"Usage: \(usageCounter)",
"Shutdown? \(isShutdown) \(shutdownError != nil ? "(\(shutdownError!.description))" : "")",
]

if connections.isNotEmpty {
lines.append("Connections:")

for connection in connections.sorted(by: { $0.id < $1.id }) {
lines.append(contentsOf: connection.description.components(separatedBy: "\n").map({ " " + $0 }))
}
}

return lines.joined(separator: "\n")
}

}

extension PoolInfo.ConnectionInfo: CustomStringConvertible {

public var description: String {
var lines: [String] = [
"Connection: \(id) (\(name))",
" State: \(state)",
" Usage: \(usageCounter)",
]

if let query {
lines.append(" Query: \(query)")
if let queryRuntime {
lines.append(" Runtime: \(queryRuntime.rounded(toPlaces: 3))s")
}
}

return lines.joined(separator: "\n")
}

}
25 changes: 22 additions & 3 deletions Sources/PostgresConnectionPool/PostgresConnectionPool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,25 @@ public actor PostgresConnectionPool {
await shutdown()
}

/// Forcibly close all idle connections.
public func closeIdleConnections() async {
guard !isShutdown else { return }

let availableCopy = available
available.removeAll()

logger.debug("[\(poolName)] Closing \(availableCopy.count) idle connections")

for poolConnection in availableCopy {
await closeConnection(poolConnection)
}

connections.removeAll(where: { connection in
connection.state == .closed
|| (connection.state != .connecting && connection.connection?.isClosed ?? false)
})
}

/// Information about the pool and its open connections.
public func poolInfo() async -> PoolInfo {
let connections = connections.compactMap { connection -> PoolInfo.ConnectionInfo? in
Expand Down Expand Up @@ -259,13 +278,13 @@ public actor PostgresConnectionPool {
})
}

await checkIdleConnections()

connections.removeAll(where: { connection in
connection.state == .closed
|| (connection.state != .connecting && connection.connection?.isClosed ?? false)
})

await closeIdleConnections()

let usageCounter = connections.reduce(0) { $0 + $1.usageCounter }
logger.debug("[\(poolName)] \(connections.count) connections (\(available.count) available, \(usageCounter) queries), \(continuations.count) continuations left")

Expand All @@ -281,7 +300,7 @@ public actor PostgresConnectionPool {

// TODO: This doesn't work well with short bursts of activity that fall between the 5 seconds check interval
/// CLose idle connections.
private func closeIdleConnections() async {
private func checkIdleConnections() async {
guard let maxIdleConnections else { return }

// 60 seconds
Expand Down
95 changes: 23 additions & 72 deletions Tests/PostgresConnectionPoolTests/ConnectionErrorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,37 +16,42 @@ final class ConnectionErrorTests: XCTestCase {

// MARK: -

// Test that the pool can actually connect to the server.
// TODO: Clean up the error checking
// TODO: Check that the Docker PostgreSQL server is actually up and available first or most tests will fail anyway

func testCanConnect() async throws {
let configuration = poolConfiguration()
let pool = PostgresConnectionPool(configuration: configuration, logger: logger)
func testConnectWrongHost() async throws {
try await withConfiguration(PostgresHelpers.poolConfiguration(host: "notworking"), expectedErrorDescription: "<PSQLError: connectionError>")
}

do {
try await pool.connection { connection in
try await connection.query("SELECT 1", logger: logger)
}
await pool.shutdown()
}
catch {
XCTFail("Is the cocker container running? (\(String(describing: (error as? PoolError)?.debugDescription))")
}
func testConnectWrongPort() async throws {
try await withConfiguration(PostgresHelpers.poolConfiguration(port: 99999), expectedErrorDescription: "<PSQLError: connectionError>")
}

let didShutdown = await pool.isShutdown
XCTAssertTrue(didShutdown)
func testConnectWrongUsername() async throws {
try await withConfiguration(PostgresHelpers.poolConfiguration(username: "notworking"), expectedErrorDescription: "<PSQLError: FATAL: password authentication failed for user \"notworking\">")
}

// MARK: -
func testConnectWrongPassword() async throws {
try await withConfiguration(PostgresHelpers.poolConfiguration(password: "notworking"), expectedErrorDescription: "<PSQLError: FATAL: password authentication failed for user \"test_username\">")
}

// TODO: Clean up the error checking
// TODO: Check that the Docker PostgreSQL server is actually up and available first or most tests will fail anyway
func testConnectInvalidTLSConfig() async throws {
var tlsConfiguration: TLSConfiguration = .clientDefault
tlsConfiguration.maximumTLSVersion = .tlsv1 // New Postgres versions want at least TLSv1.2

let tls: PostgresConnection.Configuration.TLS = .require(try .init(configuration: tlsConfiguration))
try await withConfiguration(PostgresHelpers.poolConfiguration(tls: tls), expectedErrorDescription: "<PSQLError: sslUnsupported>")
}

// MARK: -

private func withConfiguration(
_ configuration: PoolConfiguration,
expectedErrorDescription: String)
async throws
{
let pool = PostgresConnectionPool(configuration: configuration, logger: logger)

do {
try await pool.connection { connection in
try await connection.query("SELECT 1", logger: logger)
Expand All @@ -66,58 +71,4 @@ final class ConnectionErrorTests: XCTestCase {
XCTAssertTrue(didShutdown)
}

func testConnectWrongHost() async throws {
try await withConfiguration(self.poolConfiguration(host: "notworking"), expectedErrorDescription: "<PSQLError: connectionError>")
}

func testConnectWrongPort() async throws {
try await withConfiguration(self.poolConfiguration(port: 99999), expectedErrorDescription: "<PSQLError: connectionError>")
}

func testConnectWrongUsername() async throws {
try await withConfiguration(self.poolConfiguration(username: "notworking"), expectedErrorDescription: "<PSQLError: FATAL: password authentication failed for user \"notworking\">")
}

func testConnectWrongPassword() async throws {
try await withConfiguration(self.poolConfiguration(password: "notworking"), expectedErrorDescription: "<PSQLError: FATAL: password authentication failed for user \"test_username\">")
}

func testConnectInvalidTLSConfig() async throws {
var tlsConfiguration: TLSConfiguration = .clientDefault
tlsConfiguration.maximumTLSVersion = .tlsv1 // New Postgres versions want at least TLSv1.2

let tls: PostgresConnection.Configuration.TLS = .require(try .init(configuration: tlsConfiguration))
try await withConfiguration(self.poolConfiguration(tls: tls), expectedErrorDescription: "<PSQLError: sslUnsupported>")
}

// MARK: -

private func poolConfiguration(
host: String? = nil,
port: Int? = nil,
username: String? = nil,
password: String? = nil,
database: String? = nil,
tls: PostgresConnection.Configuration.TLS = .disable)
-> PoolConfiguration
{
let postgresConfiguration = PostgresConnection.Configuration(
host: host ?? env("POSTGRES_HOSTNAME") ?? "localhost",
port: port ?? env("POSTGRES_PORT").flatMap(Int.init(_:)) ?? 5432,
username: username ?? env("POSTGRES_USER") ?? "test_username",
password: password ?? env("POSTGRES_PASSWORD") ?? "test_password",
database: database ?? env("POSTGRES_DB") ?? "test_database",
tls: tls)
return PoolConfiguration(
applicationName: "ConnectionErrorTests",
postgresConfiguration: postgresConfiguration,
connectTimeout: 10.0,
queryTimeout: 10.0,
poolSize: 5,
maxIdleConnections: 1)
}

private func env(_ name: String) -> String? {
getenv(name).flatMap { String(cString: $0) }
}
}
127 changes: 127 additions & 0 deletions Tests/PostgresConnectionPoolTests/ConnectionTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
//
// Created by Thomas Rasch on 11.05.23.
//

@testable import PostgresConnectionPool
import PostgresNIO
import XCTest

final class ConnectionTests: XCTestCase {

private var logger: Logger = {
var logger = Logger(label: "ConnectionTests")
logger.logLevel = .info
return logger
}()

// MARK: -

// Test that the pool can actually connect to the server.
func testCanConnect() async throws {
let pool = PostgresConnectionPool(configuration: PostgresHelpers.poolConfiguration(), logger: logger)

do {
try await pool.connection { connection in
try await connection.query("SELECT 1", logger: logger)
}
await pool.shutdown()
}
catch {
XCTFail("Is the cocker container running? (\(String(describing: (error as? PoolError)?.debugDescription))")
}

let didShutdown = await pool.isShutdown
XCTAssertTrue(didShutdown)
}

func testPoolInfo() async throws {
let initialUsageCounter = PoolConnection.globalUsageCounter
let pool = PostgresConnectionPool(configuration: PostgresHelpers.poolConfiguration(), logger: logger)

let poolInfoBefore = await pool.poolInfo()
print(poolInfoBefore)
XCTAssertEqual(poolInfoBefore.activeConnections, 0)
XCTAssertEqual(poolInfoBefore.availableConnections, 0)
XCTAssertEqual(poolInfoBefore.openConnections, 0)
XCTAssertEqual(poolInfoBefore.usageCounter, initialUsageCounter)
XCTAssertEqual(poolInfoBefore.connections.count, poolInfoBefore.openConnections)
XCTAssertFalse(poolInfoBefore.isShutdown)
XCTAssertNil(poolInfoBefore.shutdownError)

let start = 1
let end = 1000

await withThrowingTaskGroup(of: Void.self) { taskGroup in
for _ in 1 ... 1000 {
taskGroup.addTask {
try await pool.connection { connection in
_ = try await connection.query("SELECT generate_series(\(start), \(end));", logger: self.logger)
}
}
}
}

let poolInfo = await pool.poolInfo()
print(poolInfo)
XCTAssertEqual(poolInfo.activeConnections, 0)
XCTAssertGreaterThan(poolInfo.availableConnections, 0)
XCTAssertGreaterThan(poolInfo.openConnections, 0)
XCTAssertEqual(poolInfo.usageCounter, 1000 + initialUsageCounter)
XCTAssertEqual(poolInfo.connections.count, poolInfo.openConnections)
XCTAssertFalse(poolInfo.isShutdown)
XCTAssertNil(poolInfo.shutdownError)

await pool.shutdown()

let poolInfoAfterShutdown = await pool.poolInfo()
print(poolInfoAfterShutdown)
XCTAssertEqual(poolInfoAfterShutdown.activeConnections, 0)
XCTAssertEqual(poolInfoAfterShutdown.availableConnections, 0)
XCTAssertEqual(poolInfoAfterShutdown.openConnections, 0)
XCTAssertGreaterThan(poolInfoAfterShutdown.usageCounter, 0)
XCTAssertEqual(poolInfoAfterShutdown.connections.count, 0)
XCTAssertTrue(poolInfoAfterShutdown.isShutdown)
XCTAssertNil(poolInfoAfterShutdown.shutdownError)
}

func testPoolSize100() async throws {
let initialUsageCounter = PoolConnection.globalUsageCounter
let pool = PostgresConnectionPool(configuration: PostgresHelpers.poolConfiguration(poolSize: 100), logger: logger)

let start = 1
let end = 100

await withThrowingTaskGroup(of: Void.self) { taskGroup in
for _ in 1 ... 10000 {
taskGroup.addTask {
try await pool.connection { connection in
_ = try await connection.query("SELECT generate_series(\(start), \(end));", logger: self.logger)
}
}
}
}

let poolInfo = await pool.poolInfo()
XCTAssertEqual(poolInfo.activeConnections, 0)
XCTAssertGreaterThan(poolInfo.availableConnections, 0)
XCTAssertGreaterThan(poolInfo.openConnections, 0)
XCTAssertEqual(poolInfo.usageCounter, 10000 + initialUsageCounter)
XCTAssertEqual(poolInfo.connections.count, poolInfo.openConnections)
XCTAssertFalse(poolInfo.isShutdown)
XCTAssertNil(poolInfo.shutdownError)

await pool.closeIdleConnections()

let poolInfoIdleClosed = await pool.poolInfo()
XCTAssertEqual(poolInfoIdleClosed.activeConnections, 0)
XCTAssertEqual(poolInfoIdleClosed.availableConnections, 0)
XCTAssertEqual(poolInfoIdleClosed.openConnections, 0)
XCTAssertEqual(poolInfoIdleClosed.usageCounter, 10000 + initialUsageCounter)
XCTAssertEqual(poolInfoIdleClosed.connections.count, poolInfoIdleClosed.openConnections)
XCTAssertFalse(poolInfoIdleClosed.isShutdown)
XCTAssertNil(poolInfoIdleClosed.shutdownError)

await pool.shutdown()
}

}
41 changes: 41 additions & 0 deletions Tests/PostgresConnectionPoolTests/PostgresHelpers.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//
// Created by Thomas Rasch on 11.05.23.
//

import Foundation
import PostgresConnectionPool
import PostgresNIO

enum PostgresHelpers {

static func poolConfiguration(
host: String? = nil,
port: Int? = nil,
username: String? = nil,
password: String? = nil,
database: String? = nil,
tls: PostgresConnection.Configuration.TLS = .disable,
poolSize: Int = 5)
-> PoolConfiguration
{
let postgresConfiguration = PostgresConnection.Configuration(
host: host ?? env("POSTGRES_HOSTNAME") ?? "localhost",
port: port ?? env("POSTGRES_PORT").flatMap(Int.init(_:)) ?? 5432,
username: username ?? env("POSTGRES_USER") ?? "test_username",
password: password ?? env("POSTGRES_PASSWORD") ?? "test_password",
database: database ?? env("POSTGRES_DB") ?? "test_database",
tls: tls)
return PoolConfiguration(
applicationName: "PoolTests",
postgresConfiguration: postgresConfiguration,
connectTimeout: 10.0,
queryTimeout: 10.0,
poolSize: poolSize,
maxIdleConnections: 1)
}

private static func env(_ name: String) -> String? {
getenv(name).flatMap { String(cString: $0) }
}

}

0 comments on commit 2be276f

Please sign in to comment.