Files
RediStack/Tests/RediStackIntegrationTests/RedisConnectionPoolTests.swift
T
Cory BenfieldandNathan Harris 338a6f4aa1 Delay connection attempts without addresses.
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.
2021-03-03 21:53:45 +00:00

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()
}
}