From f23698533958d610f0510699495efc6067e20b92 Mon Sep 17 00:00:00 2001 From: Thomas Rasch Date: Tue, 25 Apr 2023 16:12:59 +0200 Subject: [PATCH] Added PostgresConnectionPool.poolInfo() --- README.md | 23 +++++- .../PoolConnection.swift | 20 +++-- .../PoolConnectionState.swift | 12 +++ .../PostgresConnectionPool/PoolError.swift | 2 + Sources/PostgresConnectionPool/PoolInfo.swift | 27 +++++++ .../PostgresConnectionPool.swift | 24 +++++- .../PostgresConnectionWrapper.swift | 78 +++++++++++++++++++ 7 files changed, 173 insertions(+), 13 deletions(-) create mode 100644 Sources/PostgresConnectionPool/PoolConnectionState.swift create mode 100644 Sources/PostgresConnectionPool/PoolInfo.swift create mode 100644 Sources/PostgresConnectionPool/PostgresConnectionWrapper.swift diff --git a/README.md b/README.md index de22e4f..42e14c5 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,19 @@ # PostgresConnectionPool -A simple connection pool on top of PostgresNIO. +A simple connection pool on top of [PostgresNIO](https://github.com/vapor/postgres-nio) and [PostgresKit](https://github.com/vapor/postgres-kit). [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FOutdooractive%2FPostgresConnectionPool%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/Outdooractive/PostgresConnectionPool) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FOutdooractive%2FPostgresConnectionPool%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/Outdooractive/PostgresConnectionPool) +## Requirements + +This package requires Swift 5.7 or higher (at least Xcode 13), and compiles on macOS (\>= macOS 10.15) and Linux. + ## Installation with Swift Package Manager ```swift dependencies: [ - .package(url: "https://github.com/Outdooractive/PostgresConnectionPool.git", from: "0.4.0"), + .package(url: "https://github.com/Outdooractive/PostgresConnectionPool.git", from: "0.5.0"), ], targets: [ .target(name: "MyTarget", dependencies: [ @@ -48,11 +52,24 @@ let pool = PostgresConnectionPool(configuration: configuration, logger: logger) try await pool.connection(callback: { connection in try await connection.query(PostgresQuery(stringLiteral: "SELECT 1"), logger: logger) }) + +// Generic object loading +func fetchObjects(_ sql: String) async throws -> [T] { + try await pool.connection({ connection in + return try await connection.sql().raw(SQLQueryString(stringLiteral: sql)).all(decoding: T.self) + }) +} + +// Open connections, current SQL queries, etc. +await print(pool.info()) + +// Always call `shutdown()` before releasing a pool +await pool.shutdown() ``` ## Contributing -Please create an issue or open a pull request with a fix. +Please create an issue or open a pull request with a fix or enhancement. ## License diff --git a/Sources/PostgresConnectionPool/PoolConnection.swift b/Sources/PostgresConnectionPool/PoolConnection.swift index 6b2dd2c..6fcea23 100644 --- a/Sources/PostgresConnectionPool/PoolConnection.swift +++ b/Sources/PostgresConnectionPool/PoolConnection.swift @@ -7,25 +7,29 @@ import PostgresNIO final class PoolConnection: Identifiable, Equatable { - enum State: Equatable { - case active(Date) - case available - case closed - case connecting - } - private static var connectionId: Int = 0 private(set) var usageCounter = 0 let id: Int var connection: PostgresConnection? - var state: State = .connecting { + var state: PoolConnectionState = .connecting { didSet { if case .active = state { usageCounter += 1 } } } + private var queryStartTimestamp: Date? + var query: String? { + didSet { + queryStartTimestamp = (query == nil ? nil : Date()) + } + } + var queryRuntime: TimeInterval? { + guard let queryStartTimestamp else { return nil } + return Date().timeIntervalSince(queryStartTimestamp) + } + init() { self.id = PoolConnection.connectionId diff --git a/Sources/PostgresConnectionPool/PoolConnectionState.swift b/Sources/PostgresConnectionPool/PoolConnectionState.swift new file mode 100644 index 0000000..f044849 --- /dev/null +++ b/Sources/PostgresConnectionPool/PoolConnectionState.swift @@ -0,0 +1,12 @@ +// +// Created by Thomas Rasch on 24.04.23. +// + +import Foundation + +public enum PoolConnectionState: Equatable { + case active(Date) + case available + case closed + case connecting +} diff --git a/Sources/PostgresConnectionPool/PoolError.swift b/Sources/PostgresConnectionPool/PoolError.swift index 56f3345..6445f94 100644 --- a/Sources/PostgresConnectionPool/PoolError.swift +++ b/Sources/PostgresConnectionPool/PoolError.swift @@ -13,5 +13,7 @@ public enum PoolError: Error { case connectionFailed /// The pool was already shut down. case poolDestroyed + /// Something unexpected happened. + case unknown } diff --git a/Sources/PostgresConnectionPool/PoolInfo.swift b/Sources/PostgresConnectionPool/PoolInfo.swift new file mode 100644 index 0000000..6acfec2 --- /dev/null +++ b/Sources/PostgresConnectionPool/PoolInfo.swift @@ -0,0 +1,27 @@ +// +// Created by Thomas Rasch on 24.04.23. +// + +import Foundation + +/// General information about the pool and its open connections. +public struct PoolInfo { + + /// Information about an open connection. + public struct ConnectionInfo { + public var id: Int + public var name: String + public var usageCounter: Int + public var query: String? + public var queryRuntime: TimeInterval? + public var state: PoolConnectionState + } + + public var name: String + public var openConnections: Int + public var activeConnections: Int + public var availableConnections: Int + + public var connections: [ConnectionInfo] + +} diff --git a/Sources/PostgresConnectionPool/PostgresConnectionPool.swift b/Sources/PostgresConnectionPool/PostgresConnectionPool.swift index 8f113c9..8b50197 100644 --- a/Sources/PostgresConnectionPool/PostgresConnectionPool.swift +++ b/Sources/PostgresConnectionPool/PostgresConnectionPool.swift @@ -75,7 +75,7 @@ public actor PostgresConnectionPool { /// Takes one connection from the pool and dishes it out to the caller. @discardableResult public func connection( - _ callback: (PostgresConnection) async throws -> T) + _ callback: (PostgresConnectionWrapper) async throws -> T) async throws -> T { @@ -89,7 +89,7 @@ public actor PostgresConnectionPool { throw PoolError.cancelled } - let result = try await callback(poolConnection!.connection!) + let result = try await PostgresConnectionWrapper.distribute(poolConnection: poolConnection, callback: callback) await releaseConnection(poolConnection!) @@ -169,6 +169,26 @@ public actor PostgresConnectionPool { try? await eventLoopGroup.shutdownGracefully() } + /// Information about the pool and its open connections. + func poolInfo() async -> PoolInfo { + let connections = connections.compactMap { connection -> PoolInfo.ConnectionInfo? in + return PoolInfo.ConnectionInfo( + id: connection.id, + name: nameForConnection(id: connection.id), + usageCounter: connection.usageCounter, + query: connection.query, + queryRuntime: connection.queryRuntime, + state: connection.state) + } + + return PoolInfo( + name: poolName, + openConnections: connections.count, + activeConnections: connections.count - available.count, + availableConnections: available.count, + connections: connections) + } + // MARK: - Private private func closeConnection(_ poolConnection: PoolConnection) async { diff --git a/Sources/PostgresConnectionPool/PostgresConnectionWrapper.swift b/Sources/PostgresConnectionPool/PostgresConnectionWrapper.swift new file mode 100644 index 0000000..323fa36 --- /dev/null +++ b/Sources/PostgresConnectionPool/PostgresConnectionWrapper.swift @@ -0,0 +1,78 @@ +// +// Created by Thomas Rasch on 24.04.23. +// + +import Foundation +import PostgresNIO + +public final class PostgresConnectionWrapper { + + private let poolConnection: PoolConnection + private let postgresConnection: PostgresConnection + + static func distribute( + poolConnection: PoolConnection?, + callback: (PostgresConnectionWrapper) async throws -> T) + async throws -> T + { + guard let poolConnection, + let connectionWrapper = PostgresConnectionWrapper(poolConnection) + else { throw PoolError.unknown } + + connectionWrapper.poolConnection.query = nil + let result = try await callback(connectionWrapper) + connectionWrapper.poolConnection.query = nil + + return result + } + + init?(_ poolConnection: PoolConnection) { + guard let postgresConnection = poolConnection.connection else { return nil } + + self.poolConnection = poolConnection + self.postgresConnection = postgresConnection + } + + // MARK: - Public interface, from PostgresConnection + + /// A logger to use in case + public var logger: Logger { + postgresConnection.logger + } + + public var isClosed: Bool { + postgresConnection.isClosed + } + + /// Run a query on the Postgres server the connection is connected to. + /// + /// - Parameters: + /// - query: The ``PostgresQuery`` to run + /// - logger: The `Logger` to log into for the query + /// - file: The file, the query was started in. Used for better error reporting. + /// - line: The line, the query was started in. Used for better error reporting. + /// - Returns: A ``PostgresRowSequence`` containing the rows the server sent as the query result. + /// The sequence can be discarded. + @discardableResult + public func query( + _ query: PostgresQuery, + logger: Logger, + file: String = #fileID, + line: Int = #line) + async throws -> PostgresRowSequence + { + poolConnection.query = query.sql + return try await postgresConnection.query(query, logger: logger, file: file, line: line) + } + + /// Add a handler for NotificationResponse messages on a certain channel. This is used in conjunction with PostgreSQL's `LISTEN`/`NOTIFY` support: to listen on a channel, you add a listener using this method to handle the NotificationResponse messages, then issue a `LISTEN` query to instruct PostgreSQL to begin sending NotificationResponse messages. + @discardableResult + public func addListener( + channel: String, + handler notificationHandler: @escaping (PostgresListenContext, PostgresMessage.NotificationResponse) -> Void) + -> PostgresListenContext + { + postgresConnection.addListener(channel: channel, handler: notificationHandler) + } + +}