NIORedis: Client for Redis server built on NIO
Introduction
This package includes two modules: NIORedis
and Redis
, which provide clients that handle connection to, authorizing, and executing commands against a Redis server.
NIORedis
provides channel handlers for encoding / decoding between Swift native types and Redis' Serialization Protocol (RESP).
Redis
is an abstraction layer that wraps NIORedis
to be callback based with DispatchQueue
.
Motivation
Implementations of Redis connections have decayed as newer capabilities of the Swift stdlib, SwiftNIO, and Swift itself have developed.
As part of the initiative of trying to push the ecosystem to be centered around SwiftNIO, a framework-agnostic driver on Redis can provide an easier time for feature development on Redis.
Proposed Solution
A barebones implementation is available at mordil/nio-redis.
The following are already implemented, with unit tests:
- Connection and Authorization
- Raw commands
- Convienence methods for:
- GET
- SET
- AUTH
- DEL
- SELECT
- EXPIRE
- NIO-wrapped abstractions for
- Client
- Connection
- Pipelines
- GET command
- Unit tests for
- Response decoding to native Swift
- Message encoding to RESP
- Connections
- implemented commands
- pipelines
This package is a re-implementation of vapor/redis stripped down to only build on SwiftNIO to be framework agnostic.
Much of this was inspired by the NIOPostgres pitch.
Detailed Solution
NOTE: This this is written against SwiftNIO 2.0, and as such requires Swift 5.0!
This is to take advantage of the Result
type in the Redis
module,
and to stay ahead of development of the next version of SwiftNIO.
NIORedis
Most use of this library will be focused on a NIORedisConnection
type that works explicitly in a SwiftNIO EventLoop
context - with
return values all being EventLoopFuture
.
import NIORedis
let elg = MultiThreadedEventLoopGroup(numberOfThreads: 1)
let redis = NIORedis(executionModel: .eventLoopGroup(elg))
// connections
// passing a value to `password` will automatically authenticate with Redis before resolving the connection
let connection = try redis.makeConnection(
hostname: "localhost", // this is the default
port: 6379, // this is the default
password: "MY_PASS" // default is `nil`
).wait()
print(connection) // NIORedisConnection
// convienence methods for commands
let result = try connection.set("my_key", to: "some value")
.then {
return connection.get("my_key")
}.wait()
print(result) // Optional("some value")
// raw commands
let keyCount = try connection.command("DEL", [RedisData(bulk: "my_key")])
.thenThrowing { res in
guard case let .integer(count) else {
// throw Error
}
return count
}.wait()
print(keyCount) // 1
let pipelineResults = try connection.makePipeline()
.enqueue(command: "SET", arguments: ["my_key", 3])
.enqueue(command: "INCR", arguments: ["my_key"])
.execute()
.wait()
print(pipelineResults) // [ .simpleString("OK"), .integer(4) ]
// cleanup
connection.close()
try redis.terminate()
try elg.syncShutdownGracefully()
RedisData & RedisDataConvertible
This is a 1:1 mapping enum of the RESP
types: Simple String
, Bulk String
, Array
, Integer
and Error
.
Conforming to RedisDataConvertible
allows Swift types to more easily convert between RedisData
and native types.
Array
, Data
, Float
, Double
, FixedWidthInteger
, String
, and of course RedisData
all conform in this package.
A ByteToMessageDecoder
and MessageToByteEncoder
are used for the conversion process on connections.
NIORedisConnection
This class uses a ChannelInboundHandler
that handles the actual process of sending and receiving commands.
It retains a queue of promises to resolve when messages have been received.
To support pipelining, a user should create a NIORedisPipeline
with makePipeline()
.
NIORedisPipeline
A NIORedisPipeline
is a small abstraction that buffers an array of complete messages as RedisData
, and executes them in sequence after a user has invoked execute()
.
It returns an EventLoopFuture<[RedisData]>
with the results of all commands executed - unless one errors.
Redis
To support contexts where someone either doesn't want to work in a SwiftNIO context, the Redis
module provides a callback-based interface that wraps all of NIORedis
.
A Redis
instance manages a NIORedis
object under the hood, with RedisConnection
doing the same for NIORedisConnection
.
Redis Module | NIORedis Module |
---|---|
Redis |
NIORedis |
RedisConnection |
NIORedisConnection |
RedisPipeline |
NIORedisPipeline |
import Redis
let redis = Redis(threadCount: 1) // default is 1
// connections
// passing a value to `password` will automatically authenticate with Redis before resolving the connection
redis.makeConnection(
hostname: "localhost", // this is the default
port: 6379, // this is the default
password: "MY_PASS", // default is `nil`
queue: DispatchQueue(label: "com.MyPackage.redis") // default is `.main`
) { result in
switch result {
case .success(let conn):
showCommands(on: conn)
case .failure(let error):
fatalError("Could not create RedisConnection!")
}
}
// convenience methods for commands
func showCommands(on conn: RedisConnection) {
conn.get("my_key") { result in
switch result {
case .success(let value):
// use value, which is String?
case .failure(let error):
// do something on error
}
}
}
// cleanup is handled by deinit blocks