Files
RediStack/Sources/RediStackTestUtils/EmbeddedMockRedisServer.swift
T
Lukasa 5dbd716acf Implement a simple Redis Connection Pool.
Motivation:

Users of Redis will frequently want to be able to run queries in
parallel, while bounding the number of connections they use. They will
also often want to be able to reuse connections, without having to
arrange to manage those connections themselves. These are jobs usually
done by a Connection Pool.

This new connection pool will conform to `RedisClient` so a pool of clients and a single connection are interchangeable.

Connection Pools come in a wide range of shapes and sizes. In NIO
applications and frameworks, there are a number of questions that have
to be answered by any pool implementation:

1. Is the pool safe to share across EventLoops: that is, is its
   interface thread-safe?
2. Is the pool _tied_ to an EventLoop: that is, can the pool return
   connections that belong on lots of event loops, or just one?
3. If the pool is not tied to an EventLoop, is it possible to influence
   its choice about what event loop it uses for a given connection?

Question 1 is straightforward: it is almost always a trivial win to
ensure that the public interface to a connection pool is thread-safe.
NIO makes it possible to do this fairly cheaply in the case when the
pool is only used on a single loop.

Question 2 is a lot harder. Pools that are not tied to a specific
EventLoop have two advantages. The first is that it is easier to bound
maximum concurrency by simply configuring the pool, instead of needing
to do math on the number of pools and the number of event loops. The
second is that non-tied pools can arrange to keep busy applications
close to this maximum concurrency regardless of how the application
spreads its load across loops.

However, pools that are tied to a specific EventLoop have advantages
too. The first is one of implementation simplicity. As they always serve
connections on a single EventLoop, they can arrange to have all of their
state on that event loop too. This avoids the need to acquire locks on
that loop, making internal state management easier and more obviously
correct without having to worry about how long locks are held for.

The second advantage is that they can be used for latency sensitive
use-cases without needing to go to the work of (3). In cases where
latency is very important, it can be valuable to ensure that any Channel
that needs a connection can get one on the same event loop as itself.
This avoids the need to thread-hop in order to communicate between the
pooled connection and the user connection, reducing the latency of
operations.

Given the simplicity and latency benefits (which we deem particularly
important for Redis use-cases), we concluded that a good initial
implementation will be a pool that has a thread-safe interface, but is
tied to a single EventLoop. This allows a compact, easy-to-verify
implementation of the pool with great low-latency performance and simple
implementation logic, that can still be accessed from any EventLoop in
cases when latency is not a concern.

Modifications:

- Add new internal `ConnectionPool` object
- Add new `RedisConnectionPool` object
- Add new `RedisConnectionPoolError` type
- Add tests for new types

Results:

Users will have access to a pooled Redis client.
2020-06-03 16:43:10 +00:00

86 lines
2.5 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 RediStack
import XCTest
import NIO
internal enum MockConnectionPoolError: Error {
case unexpectedMessage
}
// TODO #64 -- Mock Redis Server
/// This is not really a Redis server: it's just something that lets us stub out the connection management in order to let
/// us test the connection pool.
internal final class EmbeddedMockRedisServer {
var channels: ArraySlice<EmbeddedChannel> = []
var loop: EmbeddedEventLoop = EmbeddedEventLoop()
// Run the fake redis server as long as there is work to do.
func runWhileActive() throws {
var anyReads = true
while anyReads {
self.loop.run()
anyReads = false
for channel in self.channels {
anyReads = try self.pumpChannel(channel) || anyReads
}
}
}
func pumpChannel(_ channel: EmbeddedChannel) throws -> Bool {
var didRead = false
while let nextRead = try channel.readOutbound(as: RedisCommand.self) {
didRead = true
try self.processChannelRead(nextRead, channel)
}
return didRead
}
func processChannelRead(_ data: RedisCommand, _ channel: Channel) throws {
switch data.message {
case .array([RESPValue(from: "QUIT")]):
// We always allow this.
let response = RESPValue.simpleString("OK".byteBuffer)
data.responsePromise.succeed(response)
default:
XCTFail("Unexpected message: \(data.message)")
data.responsePromise.fail(MockConnectionPoolError.unexpectedMessage)
}
}
func createConnectedChannel() -> Channel {
let channel = EmbeddedChannel(loop: self.loop)
channel.closeFuture.whenComplete { _ in
self.channels.removeAll(where: { $0 === channel })
}
// Activate it
channel.connect(to: try! SocketAddress(unixDomainSocketPath: "/foo"), promise: nil)
self.channels.append(channel)
return channel
}
func shutdown() throws {
try self.runWhileActive()
try self.loop.close()
}
}