Skip to content

Commit 018a9b9

Browse files
authored
Add RedisCommandEncoder (#69)
We want to be able to efficiently encode Redis commands that are sent to a server. This patch adds a new `RedisCommandEncoder` that allows us to efficiently create Redis commands without needing to go through RESP representations, that may require us to create Arrays. Further it introduces a `RESP3BlobStringEncodable` that must be implement to send blob strings using `RedisCommandEncoder`. This patch also adds implementations for `String` and `ByteBuffer` for the new `RESP3BlobStringEncodable` protocol.
1 parent d7c4121 commit 018a9b9

File tree

4 files changed

+326
-0
lines changed

4 files changed

+326
-0
lines changed
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the RediStack open source project
4+
//
5+
// Copyright (c) 2023 RediStack project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of RediStack project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
//
15+
// NOTE: THIS FILE IS AUTO-GENERATED BY scripts/generate_rediscommandencoder_multi_encode.sh
16+
17+
#if swift(<5.9)
18+
extension RedisCommandEncoder {
19+
@inlinable
20+
mutating func encodeRESPArray<T0: RESP3BlobStringEncodable>(_ t0: T0) {
21+
self.buffer.writeBytes("*1\r\n".utf8)
22+
t0.encodeRedisBlobString(into: &self.buffer)
23+
}
24+
25+
@inlinable
26+
mutating func encodeRESPArray<T0: RESP3BlobStringEncodable, T1: RESP3BlobStringEncodable>(_ t0: T0, _ t1: T1) {
27+
self.buffer.writeBytes("*2\r\n".utf8)
28+
t0.encodeRedisBlobString(into: &self.buffer)
29+
t1.encodeRedisBlobString(into: &self.buffer)
30+
}
31+
32+
@inlinable
33+
mutating func encodeRESPArray<T0: RESP3BlobStringEncodable, T1: RESP3BlobStringEncodable, T2: RESP3BlobStringEncodable>(_ t0: T0, _ t1: T1, _ t2: T2) {
34+
self.buffer.writeBytes("*3\r\n".utf8)
35+
t0.encodeRedisBlobString(into: &self.buffer)
36+
t1.encodeRedisBlobString(into: &self.buffer)
37+
t2.encodeRedisBlobString(into: &self.buffer)
38+
}
39+
40+
@inlinable
41+
mutating func encodeRESPArray<T0: RESP3BlobStringEncodable, T1: RESP3BlobStringEncodable, T2: RESP3BlobStringEncodable, T3: RESP3BlobStringEncodable>(_ t0: T0, _ t1: T1, _ t2: T2, _ t3: T3) {
42+
self.buffer.writeBytes("*4\r\n".utf8)
43+
t0.encodeRedisBlobString(into: &self.buffer)
44+
t1.encodeRedisBlobString(into: &self.buffer)
45+
t2.encodeRedisBlobString(into: &self.buffer)
46+
t3.encodeRedisBlobString(into: &self.buffer)
47+
}
48+
49+
@inlinable
50+
mutating func encodeRESPArray<T0: RESP3BlobStringEncodable, T1: RESP3BlobStringEncodable, T2: RESP3BlobStringEncodable, T3: RESP3BlobStringEncodable, T4: RESP3BlobStringEncodable>(_ t0: T0, _ t1: T1, _ t2: T2, _ t3: T3, _ t4: T4) {
51+
self.buffer.writeBytes("*5\r\n".utf8)
52+
t0.encodeRedisBlobString(into: &self.buffer)
53+
t1.encodeRedisBlobString(into: &self.buffer)
54+
t2.encodeRedisBlobString(into: &self.buffer)
55+
t3.encodeRedisBlobString(into: &self.buffer)
56+
t4.encodeRedisBlobString(into: &self.buffer)
57+
}
58+
59+
@inlinable
60+
mutating func encodeRESPArray<T0: RESP3BlobStringEncodable, T1: RESP3BlobStringEncodable, T2: RESP3BlobStringEncodable, T3: RESP3BlobStringEncodable, T4: RESP3BlobStringEncodable, T5: RESP3BlobStringEncodable>(_ t0: T0, _ t1: T1, _ t2: T2, _ t3: T3, _ t4: T4, _ t5: T5) {
61+
self.buffer.writeBytes("*6\r\n".utf8)
62+
t0.encodeRedisBlobString(into: &self.buffer)
63+
t1.encodeRedisBlobString(into: &self.buffer)
64+
t2.encodeRedisBlobString(into: &self.buffer)
65+
t3.encodeRedisBlobString(into: &self.buffer)
66+
t4.encodeRedisBlobString(into: &self.buffer)
67+
t5.encodeRedisBlobString(into: &self.buffer)
68+
}
69+
70+
@inlinable
71+
mutating func encodeRESPArray<T0: RESP3BlobStringEncodable, T1: RESP3BlobStringEncodable, T2: RESP3BlobStringEncodable, T3: RESP3BlobStringEncodable, T4: RESP3BlobStringEncodable, T5: RESP3BlobStringEncodable, T6: RESP3BlobStringEncodable>(_ t0: T0, _ t1: T1, _ t2: T2, _ t3: T3, _ t4: T4, _ t5: T5, _ t6: T6) {
72+
self.buffer.writeBytes("*7\r\n".utf8)
73+
t0.encodeRedisBlobString(into: &self.buffer)
74+
t1.encodeRedisBlobString(into: &self.buffer)
75+
t2.encodeRedisBlobString(into: &self.buffer)
76+
t3.encodeRedisBlobString(into: &self.buffer)
77+
t4.encodeRedisBlobString(into: &self.buffer)
78+
t5.encodeRedisBlobString(into: &self.buffer)
79+
t6.encodeRedisBlobString(into: &self.buffer)
80+
}
81+
}
82+
#endif
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the RediStack open source project
4+
//
5+
// Copyright (c) 2023 RediStack project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of RediStack project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import NIOCore
16+
17+
protocol RESP3BlobStringEncodable {
18+
func encodeRedisBlobString(into buffer: inout ByteBuffer)
19+
}
20+
21+
/// A Redis client sends commands to a Redis server using RESP. However clients are expected to only
22+
/// sent [RESP Array of Bulk Strings](https://redis.io/docs/reference/protocol-spec/).
23+
/// This allows us to make some heavy optimizations.
24+
///
25+
/// ``RedisCommandEncoder`` supports writing ``RESP3BlobStringEncodable`` into an outgoing buffer.
26+
struct RedisCommandEncoder {
27+
var buffer: ByteBuffer
28+
29+
#if swift(>=5.9)
30+
mutating func encodeRESPArray<each S: RESP3BlobStringEncodable>(_ first: some RESP3BlobStringEncodable, _ args: repeat each S) {
31+
let count = ComputeParameterPackLength.count(ofPack: repeat each args)
32+
33+
self.buffer.writeBytes("*".utf8)
34+
self.buffer.writeBytes("\(count + 1)".utf8)
35+
self.buffer.writeRESPNewLine()
36+
first.encodeRedisBlobString(into: &self.buffer)
37+
repeat (
38+
(each args).encodeRedisBlobString(into: &self.buffer)
39+
)
40+
}
41+
#endif
42+
}
43+
44+
extension String: RESP3BlobStringEncodable {
45+
func encodeRedisBlobString(into buffer: inout ByteBuffer) {
46+
buffer.writeBytes("$".utf8)
47+
buffer.writeBytes("\(self.utf8.count)".utf8)
48+
buffer.writeRESPNewLine()
49+
buffer.writeBytes(self.utf8)
50+
buffer.writeRESPNewLine()
51+
}
52+
}
53+
54+
extension ByteBuffer: RESP3BlobStringEncodable {
55+
func encodeRedisBlobString(into buffer: inout ByteBuffer) {
56+
var mutable = self
57+
buffer.writeBytes("$".utf8)
58+
buffer.writeBytes("\(self.readableBytes)".utf8)
59+
buffer.writeRESPNewLine()
60+
buffer.writeBuffer(&mutable)
61+
buffer.writeRESPNewLine()
62+
}
63+
}
64+
65+
66+
#if swift(>=5.9)
67+
private enum ComputeParameterPackLength {
68+
enum BoolConverter<T> {
69+
typealias Bool = Swift.Bool
70+
}
71+
72+
static func count<each T>(ofPack t: repeat each T) -> Int {
73+
MemoryLayout<(repeat BoolConverter<each T>.Bool)>.size / MemoryLayout<Bool>.stride
74+
}
75+
}
76+
#endif
77+
78+
extension ByteBuffer {
79+
mutating func writeRESPNewLine() {
80+
self.writeBytes("\r\n".utf8)
81+
}
82+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the RediStack open source project
4+
//
5+
// Copyright (c) 2023 RediStack project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of RediStack project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import NIOCore
16+
@testable import RediStack
17+
import XCTest
18+
19+
final class RedisCommandEncoderTests: XCTestCase {
20+
21+
var encoder: RedisCommandEncoder!
22+
23+
override func setUp() {
24+
self.encoder = RedisCommandEncoder(buffer: ByteBuffer())
25+
super.setUp()
26+
}
27+
28+
func testSimple() {
29+
self.encoder.encodeRESPArray("GET", "foo")
30+
var buffer = self.encoder.buffer
31+
32+
var resp: RESPValue?
33+
XCTAssertNoThrow(resp = try RESPTranslator().parseBytes(from: &buffer))
34+
XCTAssertEqual(resp, .array([.bulkString(.init(string: "GET")), .bulkString(.init(string: "foo"))]))
35+
}
36+
37+
func testStringAndByteBuffer() {
38+
let twelves = ByteBuffer(repeating: UInt8(ascii: "a"), count: 8)
39+
self.encoder.encodeRESPArray("SET", twelves)
40+
var buffer = self.encoder.buffer
41+
42+
XCTAssert(buffer.readableBytesView.elementsEqual("*2\r\n$3\r\nSET\r\n$8\r\naaaaaaaa\r\n".utf8))
43+
44+
var resp: RESPValue?
45+
XCTAssertNoThrow(resp = try RESPTranslator().parseBytes(from: &buffer))
46+
XCTAssertEqual(resp, .array([.bulkString(.init(string: "SET")), .bulkString(twelves)]))
47+
}
48+
49+
func testSingleElement() {
50+
self.encoder.encodeRESPArray("FOO")
51+
var buffer = self.encoder.buffer
52+
53+
XCTAssert(buffer.readableBytesView.elementsEqual("*1\r\n$3\r\nFOO\r\n".utf8))
54+
55+
var resp: RESPValue?
56+
XCTAssertNoThrow(resp = try RESPTranslator().parseBytes(from: &buffer))
57+
XCTAssertEqual(resp, .array([.bulkString(.init(string: "FOO"))]))
58+
}
59+
60+
func testSevenElements() {
61+
self.encoder.encodeRESPArray("SET", "key", "value", "NX", "GET", "EX", "60")
62+
var buffer = self.encoder.buffer
63+
64+
65+
var resp: RESPValue?
66+
XCTAssertNoThrow(resp = try RESPTranslator().parseBytes(from: &buffer))
67+
let expected = RESPValue.array([
68+
.bulkString(.init(string: "SET")),
69+
.bulkString(.init(string: "key")),
70+
.bulkString(.init(string: "value")),
71+
.bulkString(.init(string: "NX")),
72+
.bulkString(.init(string: "GET")),
73+
.bulkString(.init(string: "EX")),
74+
.bulkString(.init(string: "60"))
75+
])
76+
XCTAssertEqual(resp, expected)
77+
}
78+
79+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
#!/bin/bash
2+
##===----------------------------------------------------------------------===##
3+
##
4+
## This source file is part of the RediStack open source project
5+
##
6+
## Copyright (c) 2023 RediStack project authors
7+
## Licensed under Apache License v2.0
8+
##
9+
## See LICENSE.txt for license information
10+
## See CONTRIBUTORS.txt for the list of RediStack project authors
11+
##
12+
## SPDX-License-Identifier: Apache-2.0
13+
##
14+
##===----------------------------------------------------------------------===##
15+
16+
set -eu
17+
18+
here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
19+
20+
function genWithContextParameter() {
21+
how_many=$1
22+
23+
if [[ $how_many -ne 1 ]] ; then
24+
echo ""
25+
fi
26+
27+
echo " @inlinable"
28+
echo -n " mutating func encodeRESPArray<T0: RESP3BlobStringEncodable"
29+
for ((n = 1; n<$how_many; n +=1)); do
30+
echo -n ", T$(($n)): RESP3BlobStringEncodable"
31+
done
32+
33+
echo -n ">(_ t0: T0"
34+
for ((n = 1; n<$how_many; n +=1)); do
35+
echo -n ", _ t$(($n)): T$(($n))"
36+
done
37+
echo ") {"
38+
39+
echo " self.buffer.writeBytes(\"*"$how_many"\\r\\n\".utf8)"
40+
41+
for ((n = 0; n<$how_many; n +=1)); do
42+
echo " t$(($n)).encodeRedisBlobString(into: &self.buffer)"
43+
done
44+
echo " }"
45+
}
46+
47+
grep -q "ByteBuffer" "${BASH_SOURCE[0]}" || {
48+
echo >&2 "ERROR: ${BASH_SOURCE[0]}: file or directory not found (this should be this script)"
49+
exit 1
50+
}
51+
52+
{
53+
cat <<"EOF"
54+
//===----------------------------------------------------------------------===//
55+
//
56+
// This source file is part of the RediStack open source project
57+
//
58+
// Copyright (c) 2023 RediStack project authors
59+
// Licensed under Apache License v2.0
60+
//
61+
// See LICENSE.txt for license information
62+
// See CONTRIBUTORS.txt for the list of RediStack project authors
63+
//
64+
// SPDX-License-Identifier: Apache-2.0
65+
//
66+
//===----------------------------------------------------------------------===//
67+
//
68+
// NOTE: THIS FILE IS AUTO-GENERATED BY scripts/generate_rediscommandencoder_multi_encode.sh
69+
EOF
70+
echo
71+
72+
echo "#if swift(<5.9)"
73+
echo "extension RedisCommandEncoder {"
74+
75+
# note:
76+
# - widening the inverval below (eg. going from {1..15} to {1..25}) is Semver minor
77+
# - narrowing the interval below is SemVer _MAJOR_!
78+
for n in {1..7}; do
79+
genWithContextParameter "$n"
80+
done
81+
echo "}"
82+
echo "#endif"
83+
} > "$here/../Sources/RediStack/RedisCommandEncoder-multi-encode.swift"

0 commit comments

Comments
 (0)