Skip to content

Commit 6c39eb3

Browse files
committed
Merge branch '47-proposal-feedback' into 'master'
47 -- SSWG Review Feedback Closes #55, #57, #54, #49, and #47 See merge request Mordil/swift-redis-nio-client!53
2 parents cc47773 + 238ebb7 commit 6c39eb3

36 files changed

+2123
-1561
lines changed

Package.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ let package = Package(
2727
],
2828
targets: [
2929
.target(name: "RedisNIO", dependencies: ["NIO", "Logging", "Metrics"]),
30-
.testTarget(name: "RedisNIOTests", dependencies: ["RedisNIO", "NIO"])
30+
.target(name: "RedisNIOTestUtils", dependencies: ["NIO", "RedisNIO"]),
31+
.testTarget(name: "RedisNIOTests", dependencies: ["RedisNIO", "NIO", "RedisNIOTestUtils"])
3132
]
3233
)

README.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,26 +24,28 @@ To install **RedisNIO**, just add the package as a dependency in your [**Package
2424

2525
```swift
2626
dependencies: [
27-
.package(url: "https://github.com/Mordil/swift-redis-nio-client.git", from: "1.0.0-alpha.1")
27+
.package(url: "https://github.com/Mordil/swift-redis-nio-client.git", from: "1.0.0-alpha.4")
2828
]
2929
```
3030

3131
and run the following command: `swift package resolve`
3232

3333
## :zap: Getting Started
3434

35-
**RedisNIO** is ready to use right after installation.
35+
**RedisNIO** is quick to use - all you need is an [`EventLoop`](https://apple.github.io/swift-nio/docs/current/NIO/Protocols/EventLoop.html) from **SwiftNIO**.
3636

3737
```swift
38+
import NIO
3839
import RedisNIO
3940

40-
let connection = Redis.makeConnection(
41+
let eventLoop: EventLoop = ...
42+
let connection = RedisConnection.connect(
4143
to: try .init(ipAddress: "127.0.0.1", port: RedisConnection.defaultPort),
42-
password: "my_pass"
44+
on: eventLoop
4345
).wait()
4446

4547
let result = try connection.set("my_key", to: "some value")
46-
.flatMap { return connection.get("my_key" }
48+
.flatMap { return connection.get("my_key") }
4749
.wait()
4850

4951
print(result) // Optional("some value")

Sources/RedisNIO/ChannelHandlers/RedisByteDecoder.swift

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,19 @@ import NIO
2121
public final class RedisByteDecoder: ByteToMessageDecoder {
2222
/// `ByteToMessageDecoder.InboundOut`
2323
public typealias InboundOut = RESPValue
24+
25+
private let parser: RESPTranslator
26+
27+
public init() {
28+
self.parser = RESPTranslator()
29+
}
2430

2531
/// See `ByteToMessageDecoder.decode(context:buffer:)`
2632
public func decode(context: ChannelHandlerContext, buffer: inout ByteBuffer) throws -> DecodingState {
27-
var position = 0
28-
29-
switch try RESPTranslator.parseBytes(&buffer, fromIndex: &position) {
30-
case .incomplete: return .needMoreData
31-
case let .parsed(value):
32-
context.fireChannelRead(wrapInboundOut(value))
33-
buffer.moveReaderIndex(forwardBy: position)
34-
return .continue
35-
}
33+
guard let value = try self.parser.parseBytes(from: &buffer) else { return .needMoreData }
34+
35+
context.fireChannelRead(wrapInboundOut(value))
36+
return .continue
3637
}
3738

3839
/// See `ByteToMessageDecoder.decodeLast(context:buffer:seenEOF)`

Sources/RedisNIO/ChannelHandlers/RedisCommandHandler.swift

Lines changed: 46 additions & 28 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.
33-
open class RedisCommandHandler {
34-
/// Queue of promises waiting to receive a response value from a sent command.
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.
40+
public final class RedisCommandHandler {
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 @@ open 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": "\(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/ChannelHandlers/RedisMessageEncoder.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public final class RedisMessageEncoder: MessageToByteEncoder {
3333
///
3434
/// See [https://redis.io/topics/protocol](https://redis.io/topics/protocol) and `RESPEncoder.encode(data:out:)`
3535
public func encode(data: RESPValue, out: inout ByteBuffer) throws {
36-
RESPTranslator.writeValue(data, into: &out)
36+
out.writeRESPValue(data)
3737

3838
logger.debug("Encoded \(data) to \(getPrintableString(for: &out))")
3939
}

Sources/RedisNIO/Commands/BasicCommands.swift

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ extension RedisClient {
2222
/// - Returns: The message sent with the command.
2323
@inlinable
2424
public func echo(_ message: String) -> EventLoopFuture<String> {
25-
return send(command: "ECHO", with: [message])
25+
let args = [RESPValue(bulk: message)]
26+
return send(command: "ECHO", with: args)
2627
.convertFromRESPValue()
2728
}
2829

@@ -33,8 +34,10 @@ extension RedisClient {
3334
/// - Returns: The provided message or Redis' default response of `"PONG"`.
3435
@inlinable
3536
public func ping(with message: String? = nil) -> EventLoopFuture<String> {
36-
let arg = message != nil ? [message] : []
37-
return send(command: "PING", with: arg)
37+
let args: [RESPValue] = message != nil
38+
? [.init(bulk: message!)] // safe because we did a nil pre-check
39+
: []
40+
return send(command: "PING", with: args)
3841
.convertFromRESPValue()
3942
}
4043

@@ -46,7 +49,8 @@ extension RedisClient {
4649
/// - Returns: An `EventLoopFuture` that resolves when the operation has succeeded, or fails with a `RedisError`.
4750
@inlinable
4851
public func select(database index: Int) -> EventLoopFuture<Void> {
49-
return send(command: "SELECT", with: [index])
52+
let args = [RESPValue(bulk: index)]
53+
return send(command: "SELECT", with: args)
5054
.map { _ in return () }
5155
}
5256

@@ -59,8 +63,11 @@ extension RedisClient {
5963
/// - Returns: `true` if the swap was successful.
6064
@inlinable
6165
public func swapDatabase(_ first: Int, with second: Int) -> EventLoopFuture<Bool> {
62-
/// connection.swapDatabase(index: 0, withIndex: 10)
63-
return send(command: "SWAPDB", with: [first, second])
66+
let args: [RESPValue] = [
67+
.init(bulk: first),
68+
.init(bulk: second)
69+
]
70+
return send(command: "SWAPDB", with: args)
6471
.convertFromRESPValue(to: String.self)
6572
.map { return $0 == "OK" }
6673
}
@@ -74,7 +81,8 @@ extension RedisClient {
7481
public func delete(_ keys: [String]) -> EventLoopFuture<Int> {
7582
guard keys.count > 0 else { return self.eventLoop.makeSucceededFuture(0) }
7683

77-
return send(command: "DEL", with: keys)
84+
let args = keys.map(RESPValue.init)
85+
return send(command: "DEL", with: args)
7886
.convertFromRESPValue()
7987
}
8088

@@ -84,12 +92,16 @@ extension RedisClient {
8492
/// [https://redis.io/commands/expire](https://redis.io/commands/expire)
8593
/// - Parameters:
8694
/// - key: The key to set the expiration on.
87-
/// - deadline: The time from now the key will expire at.
95+
/// - timeout: The time from now the key will expire at.
8896
/// - Returns: `true` if the expiration was set.
8997
@inlinable
90-
public func expire(_ key: String, after deadline: TimeAmount) -> EventLoopFuture<Bool> {
91-
let amount = deadline.nanoseconds / 1_000_000_000
92-
return send(command: "EXPIRE", with: [key, amount])
98+
public func expire(_ key: String, after timeout: TimeAmount) -> EventLoopFuture<Bool> {
99+
let amount = timeout.nanoseconds / 1_000_000_000
100+
let args: [RESPValue] = [
101+
.init(bulk: key),
102+
.init(bulk: amount.description)
103+
]
104+
return send(command: "EXPIRE", with: args)
93105
.convertFromRESPValue(to: Int.self)
94106
.map { return $0 == 1 }
95107
}
@@ -116,7 +128,7 @@ extension RedisClient {
116128
}
117129

118130
@usableFromInline
119-
func _scan<T>(
131+
internal func _scan<T>(
120132
command: String,
121133
resultType: T.Type = T.self,
122134
_ key: String?,
@@ -127,19 +139,19 @@ extension RedisClient {
127139
where
128140
T: RESPValueConvertible
129141
{
130-
var args: [RESPValueConvertible] = [pos]
142+
var args: [RESPValue] = [.init(bulk: pos)]
131143

132144
if let k = key {
133-
args.insert(k, at: 0)
145+
args.insert(.init(bulk: k), at: 0)
134146
}
135147

136148
if let m = match {
137-
args.append("match")
138-
args.append(m)
149+
args.append(.init(bulk: "match"))
150+
args.append(.init(bulk: m))
139151
}
140152
if let c = count {
141-
args.append("count")
142-
args.append(c)
153+
args.append(.init(bulk: "count"))
154+
args.append(.init(bulk: c))
143155
}
144156

145157
let response = send(command: command, with: args).convertFromRESPValue(to: [RESPValue].self)

0 commit comments

Comments
 (0)