Commit 9f049eb4 authored by Nathan Harris's avatar Nathan Harris

Simplify `RedisCommandExecutor` and `RedisConnection`

Motivation:

`RedisCommandExecutor` was a complex and "wordy" name that was not 100% clear as to how it relates to other types.

`RedisConnection` also has not had a strong use case shown for it to exists as a separate protocol - using up a great name for the "out of the box" implementation.

Result:

`RedisClient` instead of `RedisCommandExecutor` is more clear as to what it is, in Redis terminology, a communication client.

`RedisConnection` as a concrete class provides an identifiable basic block for making connections to Redis.

`RedisConnection` also saw some fixes to `close()` while having some names and comment blocks tweaked for updated naming.
parent 869625d4
import Foundation
import NIO
extension RedisCommandExecutor {
extension RedisClient {
/// Echos the provided message through the Redis instance.
///
/// See [https://redis.io/commands/echo](https://redis.io/commands/echo)
......@@ -95,7 +95,7 @@ extension RedisCommandExecutor {
// MARK: Scan
extension RedisCommandExecutor {
extension RedisClient {
/// Incrementally iterates over all keys in the currently selected database.
///
/// [https://redis.io/commands/scan](https://redis.io/commands/scan)
......
......@@ -2,7 +2,7 @@ import NIO
// MARK: Static Helpers
extension RedisCommandExecutor {
extension RedisClient {
@usableFromInline
static func _mapHashResponse(_ values: [String]) -> [String: String] {
guard values.count > 0 else { return [:] }
......@@ -23,7 +23,7 @@ extension RedisCommandExecutor {
// MARK: General
extension RedisCommandExecutor {
extension RedisClient {
/// Removes the specified fields from a hash.
///
/// See [https://redis.io/commands/hdel](https://redis.io/commands/hdel)
......@@ -125,7 +125,7 @@ extension RedisCommandExecutor {
// MARK: Set
extension RedisCommandExecutor {
extension RedisClient {
/// Sets a hash field to the value specified.
/// - Note: If you do not want to overwrite existing values, use `hsetnx(_:field:to:)`.
///
......@@ -192,7 +192,7 @@ extension RedisCommandExecutor {
// MARK: Get
extension RedisCommandExecutor {
extension RedisClient {
/// Gets a hash field's value.
///
/// See [https://redis.io/commands/hget](https://redis.io/commands/hget)
......@@ -237,7 +237,7 @@ extension RedisCommandExecutor {
// MARK: Increment
extension RedisCommandExecutor {
extension RedisClient {
/// Increments a hash field's value and returns the new value.
///
/// See [https://redis.io/commands/hincrby](https://redis.io/commands/hincrby)
......
......@@ -2,7 +2,7 @@ import NIO
// MARK: General
extension RedisCommandExecutor {
extension RedisClient {
/// Gets the length of a list.
///
/// See [https://redis.io/commands/llen](https://redis.io/commands/llen)
......@@ -107,7 +107,7 @@ extension RedisCommandExecutor {
// MARK: Insert
extension RedisCommandExecutor {
extension RedisClient {
/// Inserts the element before the first element matching the "pivot" value specified.
///
/// See [https://redis.io/commands/linsert](https://redis.io/commands/linsert)
......@@ -152,7 +152,7 @@ extension RedisCommandExecutor {
// MARK: Head Operations
extension RedisCommandExecutor {
extension RedisClient {
/// Removes the first element of a list.
///
/// See [https://redis.io/commands/lpop](https://redis.io/commands/lpop)
......@@ -196,7 +196,7 @@ extension RedisCommandExecutor {
// MARK: Tail Operations
extension RedisCommandExecutor {
extension RedisClient {
/// Removes the last element a list.
///
/// See [https://redis.io/commands/rpop](https://redis.io/commands/rpop)
......
......@@ -3,7 +3,7 @@ import NIO
// MARK: General
extension RedisCommandExecutor {
extension RedisClient {
/// Gets all of the elements contained in a set.
/// - Note: Ordering of results are stable between multiple calls of this method to the same set.
///
......@@ -152,7 +152,7 @@ extension RedisCommandExecutor {
// MARK: Diff
extension RedisCommandExecutor {
extension RedisClient {
/// Calculates the difference between two or more sets.
///
/// See [https://redis.io/commands/sdiff](https://redis.io/commands/sdiff)
......@@ -185,7 +185,7 @@ extension RedisCommandExecutor {
// MARK: Intersect
extension RedisCommandExecutor {
extension RedisClient {
/// Calculates the intersection of two or more sets.
///
/// See [https://redis.io/commands/sinter](https://redis.io/commands/sinter)
......@@ -218,7 +218,7 @@ extension RedisCommandExecutor {
// MARK: Union
extension RedisCommandExecutor {
extension RedisClient {
/// Calculates the union of two or more sets.
///
/// See [https://redis.io/commands/sunion](https://redis.io/commands/sunion)
......
......@@ -2,7 +2,7 @@ import NIO
// MARK: Static Helpers
extension RedisCommandExecutor {
extension RedisClient {
@usableFromInline
static func _mapSortedSetResponse(
_ response: [RESPValue],
......@@ -32,7 +32,7 @@ extension RedisCommandExecutor {
// MARK: General
extension RedisCommandExecutor {
extension RedisClient {
/// Adds elements to a sorted set, assigning their score to the values provided.
///
/// See [https://redis.io/commands/zadd](https://redis.io/commands/zadd)
......@@ -140,7 +140,7 @@ extension RedisCommandExecutor {
// MARK: Rank
extension RedisCommandExecutor {
extension RedisClient {
/// Returns the rank (index) of the specified element in a sorted set.
/// - Note: This treats the ordered set as ordered from low to high.
/// For the inverse, see `zrevrank(of:in:)`.
......@@ -174,7 +174,7 @@ extension RedisCommandExecutor {
// MARK: Count
extension RedisCommandExecutor {
extension RedisClient {
/// Returns the number of elements in a sorted set with a score within the range specified.
///
/// See [https://redis.io/commands/zcount](https://redis.io/commands/zcount)
......@@ -211,7 +211,7 @@ extension RedisCommandExecutor {
// MARK: Pop
extension RedisCommandExecutor {
extension RedisClient {
/// Removes elements from a sorted set with the lowest scores.
///
/// See [https://redis.io/commands/zpopmin](https://redis.io/commands/zpopmin)
......@@ -280,7 +280,7 @@ extension RedisCommandExecutor {
// MARK: Increment
extension RedisCommandExecutor {
extension RedisClient {
/// Increments the score of the specified element in a sorted set.
///
/// See [https://redis.io/commands/zincrby](https://redis.io/commands/zincrby)
......@@ -302,7 +302,7 @@ extension RedisCommandExecutor {
// MARK: Intersect and Union
extension RedisCommandExecutor {
extension RedisClient {
/// Calculates the union of two or more sorted sets and stores the result.
/// - Note: This operation overwrites any value stored at the destination key.
///
......@@ -377,7 +377,7 @@ extension RedisCommandExecutor {
// MARK: Range
extension RedisCommandExecutor {
extension RedisClient {
/// Gets the specified range of elements in a sorted set.
/// - Note: This treats the ordered set as ordered from low to high.
///
......@@ -437,7 +437,7 @@ extension RedisCommandExecutor {
// MARK: Range by Score
extension RedisCommandExecutor {
extension RedisClient {
/// Gets elements from a sorted set whose score fits within the range specified.
/// - Note: This treats the ordered set as ordered from low to high.
///
......@@ -506,7 +506,7 @@ extension RedisCommandExecutor {
// MARK: Range by Lexiographical
extension RedisCommandExecutor {
extension RedisClient {
/// Gets elements from a sorted set whose lexiographical values are between the range specified.
/// - Important: This assumes all elements in the sorted set have the same score. If not, the returned elements are unspecified.
/// - Note: This treats the ordered set as ordered from low to high.
......@@ -570,7 +570,7 @@ extension RedisCommandExecutor {
// MARK: Remove
extension RedisCommandExecutor {
extension RedisClient {
/// Removes the specified elements from a sorted set.
///
/// See [https://redis.io/commands/zrem](https://redis.io/commands/zrem)
......
......@@ -2,7 +2,7 @@ import NIO
// MARK: Get
extension RedisCommandExecutor {
extension RedisClient {
/// Get the value of a key.
/// - Note: This operation only works with string values.
/// The `EventLoopFuture` will fail with a `RedisError` if the value is not a string, such as a Set.
......@@ -32,7 +32,7 @@ extension RedisCommandExecutor {
// MARK: Set
extension RedisCommandExecutor {
extension RedisClient {
/// Sets the value stored in the key provided, overwriting the previous value.
///
/// Any previous expiration set on the key is discarded if the SET operation was successful.
......@@ -93,7 +93,7 @@ extension RedisCommandExecutor {
// MARK: Increment
extension RedisCommandExecutor {
extension RedisClient {
/// Increments the stored value by 1.
///
/// See [https://redis.io/commands/incr](https://redis.io/commands/incr)
......@@ -136,7 +136,7 @@ extension RedisCommandExecutor {
// MARK: Decrement
extension RedisCommandExecutor {
extension RedisClient {
/// Decrements the stored value by 1.
///
/// See [https://redis.io/commands/decr](https://redis.io/commands/decr)
......
......@@ -5,13 +5,13 @@ import NIOConcurrencyHelpers
/// An object capable of sending commands and receiving responses.
///
/// let executor = ...
/// let result = executor.send(command: "GET", arguments: ["my_key"]
/// let client = ...
/// let result = client.send(command: "GET", arguments: ["my_key"])
/// // result == EventLoopFuture<RESPValue>
///
/// See [https://redis.io/commands](https://redis.io/commands)
public protocol RedisCommandExecutor {
/// The `EventLoop` that this executor operates on.
public protocol RedisClient {
/// The `EventLoop` that this client operates on.
var eventLoop: EventLoop { get }
/// Sends the desired command with the specified arguments.
......@@ -22,56 +22,44 @@ public protocol RedisCommandExecutor {
func send(command: String, with arguments: [RESPValueConvertible]) -> EventLoopFuture<RESPValue>
}
extension RedisCommandExecutor {
extension RedisClient {
/// Sends the desired command without arguments.
/// - Parameter command: The command keyword to execute.
/// - Returns: An `EventLoopFuture` that will resolve with the Redis command response.
func send(command: String) -> EventLoopFuture<RESPValue> {
public func send(command: String) -> EventLoopFuture<RESPValue> {
return self.send(command: command, with: [])
}
}
/// An individual connection to a Redis database instance for executing commands or building `RedisPipeline`s.
///
/// See `RedisCommandExecutor`.
public protocol RedisConnection: AnyObject, RedisCommandExecutor {
/// The `Channel` this connection is associated with.
var channel: Channel { get }
/// Has the connection been closed?
var isClosed: Bool { get }
/// Creates a `RedisPipeline` for executing a batch of commands.
func makePipeline() -> RedisPipeline
/// Closes the connection to Redis.
/// - Returns: An `EventLoopFuture` that resolves when the connection has been closed.
@discardableResult
func close() -> EventLoopFuture<Void>
}
extension RedisConnection {
public var eventLoop: EventLoop { return self.channel.eventLoop }
}
private let loggingKeyID = "RedisConnection"
/// A basic `RedisConnection`.
public final class NIORedisConnection: RedisConnection {
/// See `RedisConnection.channel`.
public let channel: Channel
/// See `RedisConnection.isClosed`.
public var isClosed: Bool { return _isClosed.load() }
private var _isClosed = Atomic<Bool>(value: false)
/// A `RedisClient` implementation that represents an individual connection
/// to a Redis database instance.
///
/// `RedisConnection` comes with logging and a method for creating `RedisPipeline` instances.
///
/// See `RedisClient`
public final class RedisConnection: RedisClient {
/// See `RedisClient.eventLoop`
public var eventLoop: EventLoop { return channel.eventLoop }
/// Is the client still connected to Redis?
public var isConnected: Bool { return !sentQuitCommand.load() }
private let channel: Channel
private var logger: Logger
private var sentQuitCommand = Atomic<Bool>(value: false)
deinit { assert(_isClosed.load(), "Redis connection was not properly shut down!") }
deinit {
assert(sentQuitCommand.load(), "RedisConnection did not properly shutdown before deinit!")
}
/// Creates a new connection on the provided channel.
/// - Note: This connection will take ownership of the `Channel` object.
/// Creates a new connection on the provided `Channel`.
/// - Important: Call `close()` before deinitializing to properly cleanup resources.
public init(channel: Channel, logger: Logger = Logger(label: "NIORedis.Connection")) {
/// - Note: This connection will take ownership of the channel.
/// - Parameters:
/// - channel: The `Channel` to read and write from.
/// - logger: The `Logger` instance to use for all logging purposes.
public init(channel: Channel, logger: Logger = Logger(label: "NIORedis.RedisConnection")) {
self.channel = channel
self.logger = logger
......@@ -79,34 +67,55 @@ public final class NIORedisConnection: RedisConnection {
self.logger.debug("Connection created.")
}
/// See `RedisConnection.close()`.
/// Sends a `QUIT` command, then closes the `Channel` this instance was initialized with.
///
/// See [https://redis.io/commands/quit](https://redis.io/commands/quit)
/// - Returns: An `EventLoopFuture` that resolves when the connection has been closed.
@discardableResult
public func close() -> EventLoopFuture<Void> {
guard !_isClosed.exchange(with: true) else {
// this needs to be true in order to prevent multiple close() chains, and to stop
// allowing commands to be sent - but we don't want to set it before we send the QUIT command
defer { sentQuitCommand.store(true) }
guard isConnected else {
logger.notice("Connection received more than one close() request.")
return channel.eventLoop.makeSucceededFuture(())
}
return send(command: "QUIT")
let result = send(command: "QUIT")
.flatMap { _ in
let promise = self.channel.eventLoop.makePromise(of: Void.self)
self.channel.close(promise: promise)
return promise.futureResult
}
.map { self.logger.debug("Connection closed.") }
.recover {
self.logger.error("Encountered error during close(): \($0)")
self.sentQuitCommand.store(false)
}
return result
}
/// See `RedisConnection.makePipeline()`.
/// Creates a `RedisPipeline` for executing a batch of commands.
/// - Note: The instance is given a `Logger` with the metadata property "RedisConnection"
/// that contains the unique ID of the `RedisConnection` that created it.
///
/// - Returns: An `EventLoopFuture` resolving the `RedisPipeline` instance.
public func makePipeline() -> RedisPipeline {
var logger = Logger(label: "NIORedis.Pipeline")
var logger = Logger(label: "NIORedis.RedisPipeline")
logger[metadataKey: loggingKeyID] = self.logger[metadataKey: loggingKeyID]
return NIORedisPipeline(channel: channel, logger: logger)
}
/// See `RedisCommandExecutor.send(command:with:)`.
public func send(command: String, with arguments: [RESPValueConvertible] = []) -> EventLoopFuture<RESPValue> {
guard !_isClosed.load() else {
logger.error("Received command when connection is closed.")
/// See `RedisClient.send(command:with:)`
public func send(
command: String,
with arguments: [RESPValueConvertible]
) -> EventLoopFuture<RESPValue> {
guard isConnected else {
logger.error("Received command when connection was closed.")
return channel.eventLoop.makeFailedFuture(RedisError.connectionClosed)
}
......
......@@ -51,11 +51,11 @@ public final class RedisDriver {
hostname: String = "localhost",
port: Int = 6379,
password: String? = nil
) -> EventLoopFuture<NIORedisConnection> {
) -> EventLoopFuture<RedisConnection> {
let bootstrap = ClientBootstrap.makeForRedis(using: eventLoopGroup)
return bootstrap.connect(host: hostname, port: port)
.map { return NIORedisConnection(channel: $0) }
.map { return RedisConnection(channel: $0) }
.flatMap { connection in
guard let pw = password else {
return self.eventLoopGroup.next().makeSucceededFuture(connection)
......
......@@ -18,18 +18,18 @@ public protocol RedisPipeline {
/// The number of commands in the pipeline.
var count: Int { get }
/// Queues an operation executed with the provided `RedisCommandExecutor` that will be executed in sequence when
/// Queues an operation executed with the provided `RedisClient` that will be executed in sequence when
/// `execute()` is invoked.
///
/// let pipeline = connection.makePipeline()
/// .enqueue { $0.set("my_key", "3") }
/// .enqueue { $0.send(command: "INCR", with: ["my_key"]) }
///
/// See `RedisCommandExecutor`.
/// - Parameter operation: The operation specified with `RedisCommandExecutor` provided.
/// See `RedisClient`.
/// - Parameter operation: The operation specified with `RedisClient` provided.
/// - Returns: A self-reference for chaining commands.
@discardableResult
func enqueue<T>(operation: (RedisCommandExecutor) -> EventLoopFuture<T>) -> RedisPipeline
func enqueue<T>(operation: (RedisClient) -> EventLoopFuture<T>) -> RedisPipeline
/// Flushes the queue, sending all of the commands to Redis.
/// - Returns: An `EventLoopFuture` that resolves the `RESPValue` responses, in the same order as the command queue.
......@@ -64,7 +64,7 @@ extension NIORedisPipeline: RedisPipeline {
/// See `RedisPipeline.enqueue(operation:)`.
@discardableResult
public func enqueue<T>(operation: (RedisCommandExecutor) -> EventLoopFuture<T>) -> RedisPipeline {
public func enqueue<T>(operation: (RedisClient) -> EventLoopFuture<T>) -> RedisPipeline {
// We are passing ourselves in as the executor instance,
// and our implementation of `RedisCommandExecutor.send(command:with:) handles the actual queueing.
_ = operation(self)
......@@ -97,7 +97,7 @@ extension NIORedisPipeline: RedisPipeline {
}
}
extension NIORedisPipeline: RedisCommandExecutor {
extension NIORedisPipeline: RedisClient {
/// See `RedisCommandExecutor.eventLoop`.
public var eventLoop: EventLoop { return self.channel.eventLoop }
......
......@@ -11,7 +11,7 @@ final class BasicCommandsTests: XCTestCase {
do {
connection = try redis.makeConnection().wait()
} catch {
XCTFail("Failed to create NIORedisConnection!")
XCTFail("Failed to create RedisConnection!")
}
}
......
......@@ -11,7 +11,7 @@ final class HashCommandsTests: XCTestCase {
do {
connection = try redis.makeConnection().wait()
} catch {
XCTFail("Failed to create NIORedisConnection!")
XCTFail("Failed to create RedisConnection!")
}
}
......
......@@ -11,7 +11,7 @@ final class ListCommandsTests: XCTestCase {
do {
connection = try redis.makeConnection().wait()
} catch {
XCTFail("Failed to create NIORedisConnection!")
XCTFail("Failed to create RedisConnection!")
}
}
......
......@@ -11,7 +11,7 @@ final class SetCommandsTests: XCTestCase {
do {
connection = try redis.makeConnection().wait()
} catch {
XCTFail("Failed to create NIORedisConnection!")
XCTFail("Failed to create RedisConnection!")
}
}
......
......@@ -21,7 +21,7 @@ final class SortedSetCommandsTests: XCTestCase {
_ = try connection.zadd(dataset, to: SortedSetCommandsTests.testKey).wait()
} catch {
XCTFail("Failed to create NIORedisConnection!")
XCTFail("Failed to create RedisConnection!")
}
}
......@@ -205,7 +205,7 @@ final class SortedSetCommandsTests: XCTestCase {
elements = try connection.zrange(within: (1, 3), from: key, withScores: true).wait()
XCTAssertEqual(elements.count, 6)
let values = try NIORedisConnection._mapSortedSetResponse(elements, scoreIsFirst: false)
let values = try RedisConnection._mapSortedSetResponse(elements, scoreIsFirst: false)
.map { (value, _) in return Int(value) }
XCTAssertEqual(values[0], 2)
......@@ -219,7 +219,7 @@ final class SortedSetCommandsTests: XCTestCase {
elements = try connection.zrevrange(within: (1, 3), from: key, withScores: true).wait()
XCTAssertEqual(elements.count, 6)
let values = try NIORedisConnection._mapSortedSetResponse(elements, scoreIsFirst: false)
let values = try RedisConnection._mapSortedSetResponse(elements, scoreIsFirst: false)
.map { (value, _) in return Int(value) }
XCTAssertEqual(values[0], 9)
......@@ -233,7 +233,7 @@ final class SortedSetCommandsTests: XCTestCase {
elements = try connection.zrangebyscore(within: ("1", "3"), from: key, withScores: true).wait()
XCTAssertEqual(elements.count, 6)
let values = try NIORedisConnection._mapSortedSetResponse(elements, scoreIsFirst: false)
let values = try RedisConnection._mapSortedSetResponse(elements, scoreIsFirst: false)
.map { (_, score) in return score }
XCTAssertEqual(values[0], 1.0)
......@@ -247,7 +247,7 @@ final class SortedSetCommandsTests: XCTestCase {
elements = try connection.zrevrangebyscore(within: ("1", "3"), from: key, withScores: true).wait()
XCTAssertEqual(elements.count, 6)
let values = try NIORedisConnection._mapSortedSetResponse(elements, scoreIsFirst: false)
let values = try RedisConnection._mapSortedSetResponse(elements, scoreIsFirst: false)
.map { (_, score) in return score }
XCTAssertEqual(values[0], 3.0)
......
......@@ -13,7 +13,7 @@ final class StringCommandsTests: XCTestCase {
do {
connection = try redis.makeConnection().wait()
} catch {
XCTFail("Failed to create NIORedisConnection!")
XCTFail("Failed to create RedisConnection!")
}
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment