mirror of
https://github.com/swift-server/RediStack.git
synced 2026-06-02 07:37:33 +00:00
In some circumstances users may have connection pools configured without any SocketAddresses ready to go. This is particularly likely in service discovery configurations. Right now, the effect of attempting to use such a pool is two fold. First, we'll emit a bunch of error level logs telling users we have no addresses. Second, we'll fall into the exponential backoff phase of connection establishment. The first property is annoying, but the second one is actively harmful. If your construction is timed incorrectly, we'll have the awkward problem of burning a bunch of CPU trying to create connections we know we cannot, and then a lengthy delay after the addresses are actually configured before we start trying to use them. That's the worst of all worlds. This patch adds logic to detect the attempt to create connections when we don't have any configured addresses and delays them. This path should improve performance and ergonomics when in this mode.
158 lines
6.7 KiB
Swift
158 lines
6.7 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This source file is part of the RediStack open source project
|
|
//
|
|
// Copyright (c) 2020 RediStack project authors
|
|
// Licensed under Apache License v2.0
|
|
//
|
|
// See LICENSE.txt for license information
|
|
// See CONTRIBUTORS.txt for the list of RediStack project authors
|
|
//
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
import NIO
|
|
import Logging
|
|
@testable import RediStack
|
|
import RediStackTestUtils
|
|
import XCTest
|
|
|
|
final class RedisConnectionPoolTests: RediStackConnectionPoolIntegrationTestCase {
|
|
func test_basicPooledOperation() throws {
|
|
// We're going to insert a bunch of elements into a set, and then when all is done confirm that every
|
|
// element exists.
|
|
let operations = (0..<50).map { number in
|
|
self.pool.send(.sadd([number], to: #function))
|
|
}
|
|
let results = try EventLoopFuture<Int>.whenAllSucceed(operations, on: self.eventLoopGroup.next()).wait()
|
|
XCTAssertEqual(results, Array(repeating: 1, count: 50))
|
|
let whatRedisThinks = try self.pool
|
|
.send(.smembers(of: #function))
|
|
.flatMapThrowing { result in try result.map { try $0.map(to: Int.self) } }
|
|
.wait()
|
|
XCTAssertEqual(whatRedisThinks.compactMap { $0 }.sorted(), Array(0..<50))
|
|
}
|
|
|
|
func test_closedPoolDoesNothing() throws {
|
|
self.pool.close()
|
|
XCTAssertThrowsError(try self.pool.send(.incr(#function)).wait()) { error in
|
|
XCTAssertEqual(error as? RedisConnectionPoolError, .poolClosed)
|
|
}
|
|
}
|
|
|
|
func test_nilConnectionRetryTimeoutStillWorks() throws {
|
|
let pool = try self.makeNewPool(connectionRetryTimeout: nil)
|
|
defer { pool.close() }
|
|
XCTAssertNoThrow(try pool.get(#function).wait())
|
|
}
|
|
|
|
func test_noConnectionAttemptsUntilAddressesArePresent() throws {
|
|
// Note the config here: we have no initial addresses, the connecton backoff delay is 10 seconds, and the retry timeout is only 5 seconds.
|
|
// The effect of this config is that if we fail a connection attempt, we'll fail it forever.
|
|
let pool = try self.makeNewPool(initialAddresses: [], initialConnectionBackoffDelay: .seconds(10), connectionRetryTimeout: .seconds(5), minimumConnectionCount: 0)
|
|
defer { pool.close() }
|
|
|
|
// As above we're gonna try to insert a bunch of elements into a set. This time,
|
|
// the pool has no addresses yet. We expect that when we add an address later everything will work nicely.
|
|
// We do fewer here.
|
|
let operations = (0..<10).map { number in
|
|
pool.send(.sadd([number], to: #function))
|
|
}
|
|
|
|
// Now that we've kicked those off, let's hand over a new address.
|
|
try pool.updateConnectionAddresses([SocketAddress.makeAddressResolvingHost(self.redisHostname, port: self.redisPort)])
|
|
|
|
// We should get the results.
|
|
let results = try EventLoopFuture<Int>.whenAllSucceed(operations, on: self.eventLoopGroup.next()).wait()
|
|
XCTAssertEqual(results, Array(repeating: 1, count: 10))
|
|
}
|
|
|
|
func testDelayedConnectionsFailOnClose() throws {
|
|
// Note the config here: we have no initial addresses, the connecton backoff delay is 10 seconds, and the retry timeout is only 5 seconds.
|
|
// The effect of this config is that if we fail a connection attempt, we'll fail it forever.
|
|
let pool = try self.makeNewPool(initialAddresses: [], initialConnectionBackoffDelay: .seconds(10), connectionRetryTimeout: .seconds(5), minimumConnectionCount: 0)
|
|
defer { pool.close() }
|
|
|
|
// As above we're gonna try to insert a bunch of elements into a set. This time,
|
|
// the pool has no addresses yet. We expect that when we add an address later everything will work nicely.
|
|
// We do fewer here.
|
|
let operations = (0..<10).map { number in
|
|
pool.send(.sadd([number], to: #function))
|
|
}
|
|
|
|
// Now that we've kicked those off, let's close.
|
|
pool.close()
|
|
|
|
let results = try EventLoopFuture<Int>.whenAllComplete(operations, on: self.eventLoopGroup.next()).wait()
|
|
for result in results {
|
|
switch result {
|
|
case .success:
|
|
XCTFail("Request succeeded")
|
|
case .failure(let error) where error as? RedisConnectionPoolError == .poolClosed:
|
|
() // Pass
|
|
case .failure(let error):
|
|
XCTFail("Unexpected failure: \(error)")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: Leasing a connection
|
|
|
|
extension RedisConnectionPoolTests {
|
|
func test_borrowedConnectionStillReturnsOnError() throws {
|
|
enum TestError: Error { case expected }
|
|
|
|
let maxConnectionCount = 4
|
|
let pool = try self.makeNewPool(minimumConnectionCount: maxConnectionCount)
|
|
defer { pool.close() }
|
|
_ = try pool.ping().wait()
|
|
|
|
let promise = pool.eventLoop.makePromise(of: Void.self)
|
|
|
|
XCTAssertEqual(pool.availableConnectionCount, maxConnectionCount)
|
|
defer { XCTAssertEqual(pool.availableConnectionCount, maxConnectionCount) }
|
|
|
|
let future = pool.leaseConnection { _ in promise.futureResult }
|
|
|
|
promise.fail(TestError.expected)
|
|
XCTAssertThrowsError(try future.wait()) {
|
|
XCTAssertTrue($0 is TestError)
|
|
}
|
|
}
|
|
|
|
func test_borrowedConnectionClosureHasExclusiveAccess() throws {
|
|
let maxConnectionCount = 4
|
|
let pool = try self.makeNewPool(minimumConnectionCount: maxConnectionCount)
|
|
defer { pool.close() }
|
|
// populate the connection pool
|
|
_ = try pool.ping().wait()
|
|
|
|
// assert that we have the max number of connections available,
|
|
XCTAssertEqual(pool.availableConnectionCount, maxConnectionCount)
|
|
|
|
// borrow a connection, asserting that we've taken the connection out of the pool while we do "something" with it
|
|
// and then assert afterwards that it's back in the pool
|
|
|
|
let promises: [EventLoopPromise<Void>] = [pool.eventLoop.makePromise(), pool.eventLoop.makePromise()]
|
|
let futures = promises.indices
|
|
.map { index in
|
|
return pool
|
|
.leaseConnection { connection -> EventLoopFuture<Void> in
|
|
XCTAssertTrue(pool.availableConnectionCount < maxConnectionCount)
|
|
|
|
return promises[index].futureResult
|
|
}
|
|
}
|
|
|
|
promises.forEach { $0.succeed(()) }
|
|
_ = try EventLoopFuture<Void>
|
|
.whenAllSucceed(futures, on: pool.eventLoop)
|
|
.always { _ in
|
|
XCTAssertEqual(pool.availableConnectionCount, maxConnectionCount)
|
|
}
|
|
.wait()
|
|
}
|
|
}
|