SwiftNIO Redis Client

Thanks @Mordil for sending this proposal, this looks like a great start. Redis is very important and fits really well with the goals of the SSWG.

Also having NIORedis and Redis as separate modules (where Redis uses NIORedis) feels like a really good fit for the side-side Swift ecosystem.

Personally I'm not super keen on the design of the NIORedis module: The only NIO-things it exposes are the EventLoopFuture/EventLoopPromise types. One of the key design points in NIO is that it makes things composeable through the ChannelPipeline. But if I see this correctly, there's no way for me to create a NIO Channel and add the NIORedis handlers to the ChannelPipeline.

As a super dumb and contrived example, let's imagine I would like to implement a library that implements a 'kitten store' on top of some data store. So we would have our main entity

struct Kitten {
    var name: String
    ... // some properties
}

and a few 'kitten store requests' which allow the user to save and receive kittens from the data store

enum KittenStoreRequest {
    case saveKitten(Kitten)
    case receiveKitten(name: String)
}

after each command, the kitten store would reply with some response

enum KittenStoreResponse {
    case ok(Kitten)
    case error(Error)
}

Now, as an implementation detail, the kitten store could be implemented with Redis, and if I choose NIO to implement it all, I'd expect to be able to do this:

let channel = ClientBootstrap(group: group)
                 .channelInitializer { channel in
                     channel.pipeline.add(RedisDecoder()).then {
                         channel.pipeline.add(RedisEncoder())
                     }.then {
                         channel.pipeline.add(KittenStoreOnRedisHandler())
                     }
                 }.connect(...)

channel.then { channel in
    channel.write(KittenStoreRequest.saveKitten(Kitten(name: "Lisa", ...)), promise: nil)
    channel.write(KittenStoreRequest.saveKitten(Kitten(name: "Bart", ...)), promise: nil)
    channel.writeAndFlush(KittenStoreRequest.saveKitten(Kitten(name: "Foobar", ...)))
}

The KittenStoreOnRedisHandler would probably be a ChannelDuplexHandler that transforms KittenStoreRequests into the required Redis commands to store/retrieve a Redis-encoded kitten from the store. KittenStoreOnRedisHandler would be more than a encoder/decoder as we might require multiple Redis commands to fully execute one KittenStoreRequest.

I do realise that most people are not direct NIO users and therefore a fully abstracted interface to Redis (like your Redis module) is absolutely necessary but I do think we should offer a proper NIO interface which uses the ChannelPipeline as the composition mechanism. That would allow writing libraries that use Redis as an underlying implementation detail to use all the NIO features for composition.

That's also why I believe that NIORedis had to reimplement functionality that NIO already provides. For example NIORedisPipeline has a way to enqueue outgoing messages (enqueue) and a way to flush them (execute). NIO implements this already with a way to enqueue messages (write) and a way to flush them (flush).

In other words: I think the low-level parts of a Redis library should provide all the 'bricks' which are necessary to compose a NIO application which means pretty much all of it would be ChannelHandlers and some extra convenience.

Does that make sense? And do you see the value in being able to use NIO's ChannelPipeline as the composition mechanism? Also please don't get me wrong, I think a lot of the design works well but I think we could implement (and expose) more of the functionality with the tools that NIO offers rather than needing to reimplement many of them.

2 Likes