Skip to content

Commit 2be276f

Browse files
committed
Added more tests
1 parent 6289953 commit 2be276f

File tree

5 files changed

+259
-75
lines changed

5 files changed

+259
-75
lines changed

Sources/PostgresConnectionPool/PoolInfo.swift

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,49 @@ public struct PoolInfo {
4545
public let shutdownError: PSQLError?
4646

4747
}
48+
49+
// MARK: - CustomStringConvertible
50+
51+
extension PoolInfo: CustomStringConvertible {
52+
53+
public var description: String {
54+
var lines: [String] = [
55+
"Pool: \(name)",
56+
"Connections: \(openConnections)/\(activeConnections)/\(availableConnections) (open/active/available)",
57+
"Usage: \(usageCounter)",
58+
"Shutdown? \(isShutdown) \(shutdownError != nil ? "(\(shutdownError!.description))" : "")",
59+
]
60+
61+
if connections.isNotEmpty {
62+
lines.append("Connections:")
63+
64+
for connection in connections.sorted(by: { $0.id < $1.id }) {
65+
lines.append(contentsOf: connection.description.components(separatedBy: "\n").map({ " " + $0 }))
66+
}
67+
}
68+
69+
return lines.joined(separator: "\n")
70+
}
71+
72+
}
73+
74+
extension PoolInfo.ConnectionInfo: CustomStringConvertible {
75+
76+
public var description: String {
77+
var lines: [String] = [
78+
"Connection: \(id) (\(name))",
79+
" State: \(state)",
80+
" Usage: \(usageCounter)",
81+
]
82+
83+
if let query {
84+
lines.append(" Query: \(query)")
85+
if let queryRuntime {
86+
lines.append(" Runtime: \(queryRuntime.rounded(toPlaces: 3))s")
87+
}
88+
}
89+
90+
return lines.joined(separator: "\n")
91+
}
92+
93+
}

Sources/PostgresConnectionPool/PostgresConnectionPool.swift

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,25 @@ public actor PostgresConnectionPool {
200200
await shutdown()
201201
}
202202

203+
/// Forcibly close all idle connections.
204+
public func closeIdleConnections() async {
205+
guard !isShutdown else { return }
206+
207+
let availableCopy = available
208+
available.removeAll()
209+
210+
logger.debug("[\(poolName)] Closing \(availableCopy.count) idle connections")
211+
212+
for poolConnection in availableCopy {
213+
await closeConnection(poolConnection)
214+
}
215+
216+
connections.removeAll(where: { connection in
217+
connection.state == .closed
218+
|| (connection.state != .connecting && connection.connection?.isClosed ?? false)
219+
})
220+
}
221+
203222
/// Information about the pool and its open connections.
204223
public func poolInfo() async -> PoolInfo {
205224
let connections = connections.compactMap { connection -> PoolInfo.ConnectionInfo? in
@@ -259,13 +278,13 @@ public actor PostgresConnectionPool {
259278
})
260279
}
261280

281+
await checkIdleConnections()
282+
262283
connections.removeAll(where: { connection in
263284
connection.state == .closed
264285
|| (connection.state != .connecting && connection.connection?.isClosed ?? false)
265286
})
266287

267-
await closeIdleConnections()
268-
269288
let usageCounter = connections.reduce(0) { $0 + $1.usageCounter }
270289
logger.debug("[\(poolName)] \(connections.count) connections (\(available.count) available, \(usageCounter) queries), \(continuations.count) continuations left")
271290

@@ -281,7 +300,7 @@ public actor PostgresConnectionPool {
281300

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

287306
// 60 seconds

Tests/PostgresConnectionPoolTests/ConnectionErrorTests.swift

Lines changed: 23 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -16,37 +16,42 @@ final class ConnectionErrorTests: XCTestCase {
1616

1717
// MARK: -
1818

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

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

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

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

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

41-
// TODO: Clean up the error checking
42-
// TODO: Check that the Docker PostgreSQL server is actually up and available first or most tests will fail anyway
38+
func testConnectInvalidTLSConfig() async throws {
39+
var tlsConfiguration: TLSConfiguration = .clientDefault
40+
tlsConfiguration.maximumTLSVersion = .tlsv1 // New Postgres versions want at least TLSv1.2
41+
42+
let tls: PostgresConnection.Configuration.TLS = .require(try .init(configuration: tlsConfiguration))
43+
try await withConfiguration(PostgresHelpers.poolConfiguration(tls: tls), expectedErrorDescription: "<PSQLError: sslUnsupported>")
44+
}
45+
46+
// MARK: -
4347

4448
private func withConfiguration(
4549
_ configuration: PoolConfiguration,
4650
expectedErrorDescription: String)
4751
async throws
4852
{
4953
let pool = PostgresConnectionPool(configuration: configuration, logger: logger)
54+
5055
do {
5156
try await pool.connection { connection in
5257
try await connection.query("SELECT 1", logger: logger)
@@ -66,58 +71,4 @@ final class ConnectionErrorTests: XCTestCase {
6671
XCTAssertTrue(didShutdown)
6772
}
6873

69-
func testConnectWrongHost() async throws {
70-
try await withConfiguration(self.poolConfiguration(host: "notworking"), expectedErrorDescription: "<PSQLError: connectionError>")
71-
}
72-
73-
func testConnectWrongPort() async throws {
74-
try await withConfiguration(self.poolConfiguration(port: 99999), expectedErrorDescription: "<PSQLError: connectionError>")
75-
}
76-
77-
func testConnectWrongUsername() async throws {
78-
try await withConfiguration(self.poolConfiguration(username: "notworking"), expectedErrorDescription: "<PSQLError: FATAL: password authentication failed for user \"notworking\">")
79-
}
80-
81-
func testConnectWrongPassword() async throws {
82-
try await withConfiguration(self.poolConfiguration(password: "notworking"), expectedErrorDescription: "<PSQLError: FATAL: password authentication failed for user \"test_username\">")
83-
}
84-
85-
func testConnectInvalidTLSConfig() async throws {
86-
var tlsConfiguration: TLSConfiguration = .clientDefault
87-
tlsConfiguration.maximumTLSVersion = .tlsv1 // New Postgres versions want at least TLSv1.2
88-
89-
let tls: PostgresConnection.Configuration.TLS = .require(try .init(configuration: tlsConfiguration))
90-
try await withConfiguration(self.poolConfiguration(tls: tls), expectedErrorDescription: "<PSQLError: sslUnsupported>")
91-
}
92-
93-
// MARK: -
94-
95-
private func poolConfiguration(
96-
host: String? = nil,
97-
port: Int? = nil,
98-
username: String? = nil,
99-
password: String? = nil,
100-
database: String? = nil,
101-
tls: PostgresConnection.Configuration.TLS = .disable)
102-
-> PoolConfiguration
103-
{
104-
let postgresConfiguration = PostgresConnection.Configuration(
105-
host: host ?? env("POSTGRES_HOSTNAME") ?? "localhost",
106-
port: port ?? env("POSTGRES_PORT").flatMap(Int.init(_:)) ?? 5432,
107-
username: username ?? env("POSTGRES_USER") ?? "test_username",
108-
password: password ?? env("POSTGRES_PASSWORD") ?? "test_password",
109-
database: database ?? env("POSTGRES_DB") ?? "test_database",
110-
tls: tls)
111-
return PoolConfiguration(
112-
applicationName: "ConnectionErrorTests",
113-
postgresConfiguration: postgresConfiguration,
114-
connectTimeout: 10.0,
115-
queryTimeout: 10.0,
116-
poolSize: 5,
117-
maxIdleConnections: 1)
118-
}
119-
120-
private func env(_ name: String) -> String? {
121-
getenv(name).flatMap { String(cString: $0) }
122-
}
12374
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
//
2+
// Created by Thomas Rasch on 11.05.23.
3+
//
4+
5+
@testable import PostgresConnectionPool
6+
import PostgresNIO
7+
import XCTest
8+
9+
final class ConnectionTests: XCTestCase {
10+
11+
private var logger: Logger = {
12+
var logger = Logger(label: "ConnectionTests")
13+
logger.logLevel = .info
14+
return logger
15+
}()
16+
17+
// MARK: -
18+
19+
// Test that the pool can actually connect to the server.
20+
func testCanConnect() async throws {
21+
let pool = PostgresConnectionPool(configuration: PostgresHelpers.poolConfiguration(), logger: logger)
22+
23+
do {
24+
try await pool.connection { connection in
25+
try await connection.query("SELECT 1", logger: logger)
26+
}
27+
await pool.shutdown()
28+
}
29+
catch {
30+
XCTFail("Is the cocker container running? (\(String(describing: (error as? PoolError)?.debugDescription))")
31+
}
32+
33+
let didShutdown = await pool.isShutdown
34+
XCTAssertTrue(didShutdown)
35+
}
36+
37+
func testPoolInfo() async throws {
38+
let initialUsageCounter = PoolConnection.globalUsageCounter
39+
let pool = PostgresConnectionPool(configuration: PostgresHelpers.poolConfiguration(), logger: logger)
40+
41+
let poolInfoBefore = await pool.poolInfo()
42+
print(poolInfoBefore)
43+
XCTAssertEqual(poolInfoBefore.activeConnections, 0)
44+
XCTAssertEqual(poolInfoBefore.availableConnections, 0)
45+
XCTAssertEqual(poolInfoBefore.openConnections, 0)
46+
XCTAssertEqual(poolInfoBefore.usageCounter, initialUsageCounter)
47+
XCTAssertEqual(poolInfoBefore.connections.count, poolInfoBefore.openConnections)
48+
XCTAssertFalse(poolInfoBefore.isShutdown)
49+
XCTAssertNil(poolInfoBefore.shutdownError)
50+
51+
let start = 1
52+
let end = 1000
53+
54+
await withThrowingTaskGroup(of: Void.self) { taskGroup in
55+
for _ in 1 ... 1000 {
56+
taskGroup.addTask {
57+
try await pool.connection { connection in
58+
_ = try await connection.query("SELECT generate_series(\(start), \(end));", logger: self.logger)
59+
}
60+
}
61+
}
62+
}
63+
64+
let poolInfo = await pool.poolInfo()
65+
print(poolInfo)
66+
XCTAssertEqual(poolInfo.activeConnections, 0)
67+
XCTAssertGreaterThan(poolInfo.availableConnections, 0)
68+
XCTAssertGreaterThan(poolInfo.openConnections, 0)
69+
XCTAssertEqual(poolInfo.usageCounter, 1000 + initialUsageCounter)
70+
XCTAssertEqual(poolInfo.connections.count, poolInfo.openConnections)
71+
XCTAssertFalse(poolInfo.isShutdown)
72+
XCTAssertNil(poolInfo.shutdownError)
73+
74+
await pool.shutdown()
75+
76+
let poolInfoAfterShutdown = await pool.poolInfo()
77+
print(poolInfoAfterShutdown)
78+
XCTAssertEqual(poolInfoAfterShutdown.activeConnections, 0)
79+
XCTAssertEqual(poolInfoAfterShutdown.availableConnections, 0)
80+
XCTAssertEqual(poolInfoAfterShutdown.openConnections, 0)
81+
XCTAssertGreaterThan(poolInfoAfterShutdown.usageCounter, 0)
82+
XCTAssertEqual(poolInfoAfterShutdown.connections.count, 0)
83+
XCTAssertTrue(poolInfoAfterShutdown.isShutdown)
84+
XCTAssertNil(poolInfoAfterShutdown.shutdownError)
85+
}
86+
87+
func testPoolSize100() async throws {
88+
let initialUsageCounter = PoolConnection.globalUsageCounter
89+
let pool = PostgresConnectionPool(configuration: PostgresHelpers.poolConfiguration(poolSize: 100), logger: logger)
90+
91+
let start = 1
92+
let end = 100
93+
94+
await withThrowingTaskGroup(of: Void.self) { taskGroup in
95+
for _ in 1 ... 10000 {
96+
taskGroup.addTask {
97+
try await pool.connection { connection in
98+
_ = try await connection.query("SELECT generate_series(\(start), \(end));", logger: self.logger)
99+
}
100+
}
101+
}
102+
}
103+
104+
let poolInfo = await pool.poolInfo()
105+
XCTAssertEqual(poolInfo.activeConnections, 0)
106+
XCTAssertGreaterThan(poolInfo.availableConnections, 0)
107+
XCTAssertGreaterThan(poolInfo.openConnections, 0)
108+
XCTAssertEqual(poolInfo.usageCounter, 10000 + initialUsageCounter)
109+
XCTAssertEqual(poolInfo.connections.count, poolInfo.openConnections)
110+
XCTAssertFalse(poolInfo.isShutdown)
111+
XCTAssertNil(poolInfo.shutdownError)
112+
113+
await pool.closeIdleConnections()
114+
115+
let poolInfoIdleClosed = await pool.poolInfo()
116+
XCTAssertEqual(poolInfoIdleClosed.activeConnections, 0)
117+
XCTAssertEqual(poolInfoIdleClosed.availableConnections, 0)
118+
XCTAssertEqual(poolInfoIdleClosed.openConnections, 0)
119+
XCTAssertEqual(poolInfoIdleClosed.usageCounter, 10000 + initialUsageCounter)
120+
XCTAssertEqual(poolInfoIdleClosed.connections.count, poolInfoIdleClosed.openConnections)
121+
XCTAssertFalse(poolInfoIdleClosed.isShutdown)
122+
XCTAssertNil(poolInfoIdleClosed.shutdownError)
123+
124+
await pool.shutdown()
125+
}
126+
127+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
//
2+
// Created by Thomas Rasch on 11.05.23.
3+
//
4+
5+
import Foundation
6+
import PostgresConnectionPool
7+
import PostgresNIO
8+
9+
enum PostgresHelpers {
10+
11+
static func poolConfiguration(
12+
host: String? = nil,
13+
port: Int? = nil,
14+
username: String? = nil,
15+
password: String? = nil,
16+
database: String? = nil,
17+
tls: PostgresConnection.Configuration.TLS = .disable,
18+
poolSize: Int = 5)
19+
-> PoolConfiguration
20+
{
21+
let postgresConfiguration = PostgresConnection.Configuration(
22+
host: host ?? env("POSTGRES_HOSTNAME") ?? "localhost",
23+
port: port ?? env("POSTGRES_PORT").flatMap(Int.init(_:)) ?? 5432,
24+
username: username ?? env("POSTGRES_USER") ?? "test_username",
25+
password: password ?? env("POSTGRES_PASSWORD") ?? "test_password",
26+
database: database ?? env("POSTGRES_DB") ?? "test_database",
27+
tls: tls)
28+
return PoolConfiguration(
29+
applicationName: "PoolTests",
30+
postgresConfiguration: postgresConfiguration,
31+
connectTimeout: 10.0,
32+
queryTimeout: 10.0,
33+
poolSize: poolSize,
34+
maxIdleConnections: 1)
35+
}
36+
37+
private static func env(_ name: String) -> String? {
38+
getenv(name).flatMap { String(cString: $0) }
39+
}
40+
41+
}

0 commit comments

Comments
 (0)