Skip to content

Commit 5fb0bfc

Browse files
committed
Rename RedisCommandContext to RedisCommand, improve RedisCommandHandler usage semantics, and cleanup documentation.
Motivation: During proposal review, it was noted that `RedisCommandContext` was a bit misleading, and the hidden reference semantics worrisome. In addition, several of parts of the documentation around `RedisCommandHandler` were weak or also misleading. Modifications: - Rename `RedisCommandContext` to just `RedisCommand` - Update documentation to be more explicit about the module who owns the types being referenced - Update documentation to call out explicit usage semantics and behavior - Change `RedisCommandHandler` to close the socket connection on error thrown - Rename the "base" Redis Channel Handlers to be more explicitly named Result: Users should have clearer documentation on what happens when using `RedisCommandHandler` and `RedisCommand` without hidden semantics. This contributes to issue #47.
1 parent 15a3c03 commit 5fb0bfc

File tree

3 files changed

+49
-31
lines changed

3 files changed

+49
-31
lines changed

Sources/RedisNIO/ChannelHandlers/RedisCommandHandler.swift

Lines changed: 45 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,15 @@ import struct Foundation.UUID
1616
import Logging
1717
import NIO
1818

19-
/// A context for `RedisCommandHandler` to operate within.
20-
public struct RedisCommandContext {
21-
/// A full command keyword and arguments stored as a single `RESPValue`.
19+
/// The `NIO.ChannelOutboundHandler.OutboundIn` type for `RedisCommandHandler`.
20+
///
21+
/// This holds the command and its arguments stored as a single `RESPValue` to be sent to Redis,
22+
/// and an `NIO.EventLoopPromise` to be fulfilled when a response has been received.
23+
/// - Important: This struct has _reference semantics_ due to the retention of the `NIO.EventLoopPromise`.
24+
public struct RedisCommand {
25+
/// A command keyword and its arguments stored as a single `RESPValue.array`.
2226
public let command: RESPValue
23-
/// A promise expected to be fulfilled with the `RESPValue` response to the command from Redis.
27+
/// A promise to be fulfilled with the sent command's response from Redis.
2428
public let responsePromise: EventLoopPromise<RESPValue>
2529

2630
public init(command: RESPValue, promise: EventLoopPromise<RESPValue>) {
@@ -29,18 +33,28 @@ public struct RedisCommandContext {
2933
}
3034
}
3135

32-
/// A `ChannelDuplexHandler` that works with `RedisCommandContext`s to send commands and forward responses.
36+
/// An object that operates in a First In, First Out (FIFO) request-response cycle.
37+
///
38+
/// `RedisCommandHandler` is a `NIO.ChannelDuplexHandler` that sends `RedisCommand` instances to Redis,
39+
/// and fulfills the command's `NIO.EventLoopPromise` as soon as a `RESPValue` response has been received from Redis.
3340
public final class RedisCommandHandler {
34-
/// Queue of promises waiting to receive a response value from a sent command.
41+
/// FIFO queue of promises waiting to receive a response value from a sent command.
3542
private var commandResponseQueue: CircularBuffer<EventLoopPromise<RESPValue>>
3643
private var logger: Logger
3744

3845
deinit {
39-
guard commandResponseQueue.count > 0 else { return }
40-
logger.warning("Command handler deinit when queue is not empty. Current size: \(commandResponseQueue.count)")
46+
guard self.commandResponseQueue.count > 0 else { return }
47+
self.logger[metadataKey: "Queue Size"] = "\(self.commandResponseQueue.count)"
48+
self.logger.warning("Command handler deinit when queue is not empty")
4149
}
4250

43-
public init(logger: Logger = Logger(label: "RedisNIO.CommandHandler"), initialQueueCapacity: Int = 5) {
51+
/// - Parameters:
52+
/// - initialQueueCapacity: The initial queue size to start with. The default is `3`. `RedisCommandHandler` stores all
53+
/// `RedisCommand.responsePromise` objects into a buffer, and unless you intend to execute several concurrent commands against Redis,
54+
/// and don't want the buffer to resize, you shouldn't need to set this parameter.
55+
/// - logger: The `Logging.Logger` instance to use.
56+
/// The logger will have a `Foundation.UUID` value attached as metadata to uniquely identify this instance.
57+
public init(initialQueueCapacity: Int = 3, logger: Logger = Logger(label: "RedisNIO.CommandHandler")) {
4458
self.commandResponseQueue = CircularBuffer(initialCapacity: initialQueueCapacity)
4559
self.logger = logger
4660
self.logger[metadataKey: "CommandHandler"] = "\(UUID())"
@@ -50,13 +64,16 @@ public final class RedisCommandHandler {
5064
// MARK: ChannelInboundHandler
5165

5266
extension RedisCommandHandler: ChannelInboundHandler {
53-
/// See `ChannelInboundHandler.InboundIn`
67+
/// See `NIO.ChannelInboundHandler.InboundIn`
5468
public typealias InboundIn = RESPValue
5569

56-
/// Invoked by NIO when an error has been thrown. The command response promise at the front of the queue will be
57-
/// failed with the error.
70+
/// Invoked by SwiftNIO when an error has been thrown. The command queue will be drained, with each promise in the queue being failed with the error thrown.
5871
///
59-
/// See `ChannelInboundHandler.errorCaught(context:error:)`
72+
/// See `NIO.ChannelInboundHandler.errorCaught(context:error:)`
73+
/// - Important: This will also close the socket connection to Redis.
74+
/// - Note:`RedisMetrics.commandFailureCount` is **not** incremented from this error.
75+
///
76+
/// A `Logging.LogLevel.critical` message will be written with the caught error.
6077
public func errorCaught(context: ChannelHandlerContext, error: Error) {
6178
let queue = self.commandResponseQueue
6279

@@ -65,21 +82,22 @@ extension RedisCommandHandler: ChannelInboundHandler {
6582
self.commandResponseQueue.removeAll()
6683
queue.forEach { $0.fail(error) }
6784

68-
logger.critical("Error in channel pipeline.", metadata: ["error": .string(error.localizedDescription)])
69-
70-
context.fireErrorCaught(error)
85+
self.logger.critical("Error in channel pipeline.", metadata: ["error": .string(error.localizedDescription)])
86+
87+
context.close(promise: nil)
7188
}
7289

73-
/// Invoked by NIO when a read has been fired from earlier in the response chain. This forwards the unwrapped
74-
/// `RESPValue` to the promise awaiting a response at the front of the queue.
90+
/// Invoked by SwiftNIO when a read has been fired from earlier in the response chain.
91+
/// This forwards the decoded `RESPValue` response message to the promise waiting to be fulfilled at the front of the command queue.
92+
/// - Note: `RedisMetrics.commandFailureCount` and `RedisMetrics.commandSuccessCount` are incremented from this method.
7593
///
76-
/// See `ChannelInboundHandler.channelRead(context:data:)`
94+
/// See `NIO.ChannelInboundHandler.channelRead(context:data:)`
7795
public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
7896
let value = self.unwrapInboundIn(data)
7997

8098
guard let leadPromise = self.commandResponseQueue.popFirst() else {
8199
assertionFailure("Read triggered with an empty promise queue! Ignoring: \(value)")
82-
logger.critical("Read triggered with no promise waiting in the queue!")
100+
self.logger.critical("Read triggered with no promise waiting in the queue!")
83101
return
84102
}
85103

@@ -98,16 +116,16 @@ extension RedisCommandHandler: ChannelInboundHandler {
98116
// MARK: ChannelOutboundHandler
99117

100118
extension RedisCommandHandler: ChannelOutboundHandler {
101-
/// See `ChannelOutboundHandler.OutboundIn`
102-
public typealias OutboundIn = RedisCommandContext
103-
/// See `ChannelOutboundHandler.OutboundOut`
119+
/// See `NIO.ChannelOutboundHandler.OutboundIn`
120+
public typealias OutboundIn = RedisCommand
121+
/// See `NIO.ChannelOutboundHandler.OutboundOut`
104122
public typealias OutboundOut = RESPValue
105123

106-
/// Invoked by NIO when a `write` has been requested on the `Channel`.
107-
/// This unwraps a `RedisCommandContext`, retaining a callback to forward a response to later, and forwards
108-
/// the underlying command data further into the pipeline.
124+
/// Invoked by SwiftNIO when a `write` has been requested on the `Channel`.
125+
/// This unwraps a `RedisCommand`, storing the `NIO.EventLoopPromise` in a command queue,
126+
/// to fulfill later with the response to the command that is about to be sent through the `NIO.Channel`.
109127
///
110-
/// See `ChannelOutboundHandler.write(context:data:promise:)`
128+
/// See `NIO.ChannelOutboundHandler.write(context:data:promise:)`
111129
public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) {
112130
let commandContext = self.unwrapOutboundIn(data)
113131
self.commandResponseQueue.append(commandContext.responsePromise)

Sources/RedisNIO/Extensions/SwiftNIO.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ extension Channel {
4444
/// - Returns: An `EventLoopFuture` that resolves after all handlers have been added to the pipeline.
4545
public func addBaseRedisHandlers() -> EventLoopFuture<Void> {
4646
let handlers: [(ChannelHandler, name: String)] = [
47-
(MessageToByteHandler(RedisMessageEncoder()), "RedisNIO.Outgoing"),
48-
(ByteToMessageHandler(RedisByteDecoder()), "RedisNIO.Incoming"),
49-
(RedisCommandHandler(), "RedisNIO.CommandQueue")
47+
(MessageToByteHandler(RedisMessageEncoder()), "RedisNIO.OutgoingHandler"),
48+
(ByteToMessageHandler(RedisByteDecoder()), "RedisNIO.IncomingHandler"),
49+
(RedisCommandHandler(), "RedisNIO.CommandHandler")
5050
]
5151
return .andAllSucceed(
5252
handlers.map { self.pipeline.addHandler($0, name: $1) },

Sources/RedisNIO/RedisClient.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ public final class RedisConnection: RedisClient {
166166
let args = arguments.map { $0.convertedToRESPValue() }
167167

168168
let promise = channel.eventLoop.makePromise(of: RESPValue.self)
169-
let context = RedisCommandContext(
169+
let context = RedisCommand(
170170
command: .array([RESPValue(bulk: command)] + args),
171171
promise: promise
172172
)

0 commit comments

Comments
 (0)