New Swift client for Valkey/Redis

Hi,

I like to introduce valkey-swift a new package I've been working on alongside @fabianfett. A preview release v0.1.0 was tagged today.

What is Valkey?

Valkey is a high performance data structure server primarily used as a key/value datastore. It is a fork of Redis.

Valkey-swift

Valkey-swift is a modern swift client for Valkey, built with Swift concurrency in mind. The client includes the following features:

Commands

The code for the commands is generated from the command model files Valkey publishes. This means valkey-swift supports all of Valkey's commands (string, list, set, sortedset, stream, hash, geospatial, pub/sub, hyperloglog and bloom and json modules). It is up to date with the Valkey 8.1.3 and will be easy to update when 9.0 is released imminently. Commands are available from both the client and connection.

try await valkeyClient.set("Key1", value: "Test")
let value = try await valkeyClient.get("Key1")

or

try await valkeyClient.withConnection { connection in
    try await connection.set("Key1", value: "Test")
    let value = try await connection.get("Key1")
}

Pipelining

Valkey Pipelining is a technique for improving performance. It sends multiple commands at the same time without waiting for the response of each individual command. It avoids the round trip time between each command, and removes the relationship between receiving the response from a request and sending the next request.

Valkey-swift provides support for pipelining in a couple of different ways.
First, you can do this using the execute(_:) function available from both ValkeyClient and ValkeyConnection.
This sends all the commands off at the same time and receives a tuple of responses.

let (lpushResult, rpopResult) = await valkeyClient.execute(
    LPUSH("Key2", elements: ["entry1", "entry2"]),
    RPOP("Key2")
)
let count = try lpushResult.get()  // 2
let value = try rpopResult.get()  // ByteBuffer containing "entry1" string

The second way to take advantage of pipelining is to use Swift Concurrency. A single ValkeyConnection can be used across concurrent tasks without issue. Unlike the execute(_:) function the commands will be sent individually but the sending of a command is not dependent on a previous command returning a response. With this method you can use a single connection to execute an exceptionally large number of commands.

try await valkeyClient.withConnection { connection in
    try await withThrowingTaskGroup(of: Void.self) { group in
        // run LPUSH and RPUSH concurrently 
        group.addTask {
            try await connection.lpush(key: "foo1", element: ["bar"])
        }
        group.addTask {
            try await connection.rpush(key: "foo2", element: ["baz"])
        }
    }
}

Pub/Sub

Valkey can be used as a message broker using its publish/subscribe messaging model. A subscription is a stream of messages from a channel. The easiest way to model this is with a Swift AsyncSequence. The valkey-swift subscription API provides a simple way to manage subscriptions with a single function call that automatically subscribes and unsubscribes from channels as needed. You provide it with a closure, it calls SUBSCRIBE on the channels you specified, and provides an AsyncSequence of messages from those channels. When you exit the closure, the connection sends the relevant UNSUBSCRIBE commands. This avoids the common user error of forgetting to unsubscribe from a channel once it is no longer needed.

try await valkeyClient.withConnection { connection in
    try await connection.subscribe(channels: ["channel1"]) { subscription in
        for try await event in subscription {
            print(String(buffer: event.message))
        }
    }
}

Valkey Cluster

Valkey scales horizontally with a deployment called Valkey Cluster. Data is sharded across multiple Valkey servers based on the hash of the key being accessed. It also provides a level of availability, using replicas. You can continue operations even when a node fails or is unable to communicate.

Swift-valkey includes a cluster client ValkeyClusterClient. This includes support for:

  1. Election based cluster topology discovery and maintenance.
  2. Command routing to the appropriate node based on key hashslots.
  3. Handling of MOVED errors for proper cluster resharding.
  4. Connection pooling and failover.
  5. Circuit breaking during cluster disruptions.

The following example shows how to create a cluster client that uses ValkeyStaticNodeDiscovery to find the first node in the cluster. From there the client discovers the remains of the cluster topology.

let clusterClient = ValkeyClusterClient(
    clientConfiguration: clientConfiguration,
    nodeDiscovery: ValkeyStaticNodeDiscovery([
        .init(host: "127.0.0.1", port: 9000, useTLS: true)
    ]),
    logger: logger
)

All the standard Valkey commands are available to the cluster client. The only requirement is that if a command references two of more keys, they all come from the same shard in the cluster.

try await clusterClient.xread(
    milliseconds: 10000, 
    streams: .init(key: ["events"], id: ["0-0"])
)

Redis Compatibility

Valkey uses the same protocol (RESP) as Redis. Because of this valkey-swift is compatible with Redis databases as well. The only features you won't be able to use are those that have been added to Redis since v7.2.4 (when Valkey was forked).

Check it out

The github repository can be found in the valkey-io organisation GitHub - valkey-io/valkey-swift: Valkey client written in Swift.
Documentation can be found on the Swift Package Index.

25 Likes

This is amazing to see! Huge thanks to you and @fabianfett for working on this. The APIs look great and fit right into modern Structured Concurrency.

5 Likes

Congrats, @adam-fowler and @fabianfett - great work!

Two questions about the longer term approach:

  1. Are you planning to integrate Swift Metrics and Swift Distributed Tracing, in addition to Swift Log?
  2. What's a rough timeline for a 1.0 you're aiming for?
3 Likes

Yes we will be integrating them

At this point I couldn't confidently say when we will hit 1.0. There are a number of features and optimisations I'd like to implement before hitting 1.0. Also would like to respond to feedback before making that call.

3 Likes

Super cool, thanks for all the hard work.

Nit: Worth mentioning that Redis is OSS (AGPL3) again? Redis is open source again - <antirez>

2 Likes

I did not know that, thanks.
I edited the original post to remove that

I haven't touched it for a long time, but I also have a Redi/S compatible server built in Swift itself (along w/ performance comparisons, but Swift 4.x times IIRC): GitHub - NozeIO/redi-s: A performant Redis server implemented in SwiftNIO.

Maybe you can test it against the client, if it still compiles.

It still compiles and seems to run fine w/ redis-benchmark -p 1337 -t SET,GET,RPUSH,INCR -n 500000 -q. So it should work fine w/ your client as well.

This is truly great work @adam-fowler, thanks for taking me on this ride.

valkey-swift got its own valkey blog post, which is live now:

5 Likes