Skip to content

Commit 0e3e17f

Browse files
authored
ValkeyKey storage (#116)
1 parent 107b2c1 commit 0e3e17f

File tree

10 files changed

+204
-54
lines changed

10 files changed

+204
-54
lines changed

Benchmarks/ValkeyBenchmarks/ValkeyBenchmarks.swift

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ let benchmarks: @Sendable () -> Void = {
104104
}
105105

106106
Benchmark("ValkeyCommandEncoder – Simple MGET 15 keys", configuration: .init(metrics: defaultMetrics, scalingFactor: .kilo)) { benchmark in
107-
let keys = (0..<15).map { ValkeyKey(rawValue: "foo-\($0)") }
107+
let keys = (0..<15).map { ValkeyKey("foo-\($0)") }
108108
let command = MGET(key: keys)
109109
benchmark.startMeasurement()
110110

@@ -135,8 +135,7 @@ let benchmarks: @Sendable () -> Void = {
135135
}
136136

137137
Benchmark("HashSlot – {user}.whatever", configuration: .init(metrics: defaultMetrics, scalingFactor: .mega)) { benchmark in
138-
let key = "{user}.whatever"
139-
138+
let key: ValkeyKey = "{user}.whatever"
140139
benchmark.startMeasurement()
141140
for _ in benchmark.scaledIterations {
142141
blackHole(HashSlot(key: key))

Sources/Valkey/Cluster/HashSlot.swift

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15+
import NIOCore
16+
1517
// This is a derived version from:
1618
// https://github.com/swift-server/RediStack/blob/2df32390e2366b58cc15c2612bb324b3fc37a190/Sources/RediStack/Cluster/RedisHashSlot.swift
1719

@@ -130,7 +132,8 @@ extension HashSlot {
130132
///
131133
/// - Parameter key: The key used in a Valkey command
132134
/// - Returns: A HashSlot representing where this key would be stored in the cluster
133-
public init(key: String) {
135+
@inlinable
136+
public init(key: some BidirectionalCollection<UInt8>) {
134137
// Banging is safe because the modulo ensures we are in range
135138
self.init(rawValue: UInt16(HashSlot.crc16(HashSlot.hashTag(forKey: key)) % 16384))!
136139
}
@@ -139,8 +142,16 @@ extension HashSlot {
139142
///
140143
/// - Parameter key: The Valkey key for which to calculate the hash slot
141144
/// - Returns: A HashSlot representing where this key would be stored in the cluster
145+
@inlinable
142146
public init(key: ValkeyKey) {
143-
self.init(key: key.rawValue)
147+
switch key._storage {
148+
case .string(let string):
149+
self.init(key: string.utf8)
150+
case .buffer(let buffer):
151+
self = buffer.withUnsafeReadableBytes { bytes in
152+
HashSlot(key: bytes)
153+
}
154+
}
144155
}
145156

146157
/// Computes the portion of the key that should be used for hash slot calculation.
@@ -150,37 +161,36 @@ extension HashSlot {
150161
/// - If the pattern is empty "{}", or doesn't exist, the entire key is used
151162
/// - Only the first occurrence of "{...}" is considered
152163
///
153-
/// - Parameter key: The key for your operation
164+
/// - Parameter keyUTF8View: The UTF8 view of key for your operation
154165
/// - Returns: A substring UTF8 view that will be used in the CRC16 computation
155-
package static func hashTag(forKey key: String) -> Substring.UTF8View {
156-
let utf8View = key.utf8
157-
158-
var firstOpenCurly: String.UTF8View.Index?
159-
var index = utf8View.startIndex
166+
@inlinable
167+
package static func hashTag<Bytes: BidirectionalCollection<UInt8>>(forKey keyUTF8View: Bytes) -> Bytes.SubSequence {
168+
var firstOpenCurly: Bytes.Index?
169+
var index = keyUTF8View.startIndex
160170

161-
while index < utf8View.endIndex {
162-
defer { index = utf8View.index(after: index) }
171+
while index < keyUTF8View.endIndex {
172+
defer { index = keyUTF8View.index(after: index) }
163173

164-
switch utf8View[index] {
174+
switch keyUTF8View[index] {
165175
case UInt8(ascii: "{") where firstOpenCurly == nil:
166176
firstOpenCurly = index
167177
case UInt8(ascii: "}"):
168178
guard let firstOpenCurly = firstOpenCurly else {
169179
continue
170180
}
171181

172-
if firstOpenCurly == utf8View.index(before: index) {
182+
if firstOpenCurly == keyUTF8View.index(before: index) {
173183
// we had a `{}` combination... this means the complete key shall be used for hashing
174-
return utf8View[...]
184+
return keyUTF8View[...]
175185
}
176186

177-
return utf8View[(utf8View.index(after: firstOpenCurly))..<index]
187+
return keyUTF8View[(keyUTF8View.index(after: firstOpenCurly))..<index]
178188
default:
179189
continue
180190
}
181191
}
182192

183-
return utf8View[...]
193+
return keyUTF8View[...]
184194
}
185195
}
186196

@@ -227,7 +237,8 @@ extension HashSlot {
227237
* Output for "123456789" : 31C3
228238
*/
229239

230-
private let crc16tab: [UInt16] = [
240+
@usableFromInline
241+
/* private */ let crc16tab: [UInt16] = [
231242
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
232243
0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
233244
0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
@@ -269,6 +280,7 @@ extension HashSlot {
269280
///
270281
/// - Parameter bytes: A sequence of bytes to compute the CRC16 value for
271282
/// - Returns: The computed CRC16 value
283+
@inlinable
272284
package static func crc16<Bytes: Sequence>(_ bytes: Bytes) -> UInt16 where Bytes.Element == UInt8 {
273285
var crc: UInt16 = 0
274286
for byte in bytes {

Sources/Valkey/Subscriptions/ValkeyConnection+subscribe.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ extension ValkeyConnection {
192192
process: (AsyncMapSequence<ValkeySubscription, ValkeyKey>) async throws -> sending Value
193193
) async throws -> sending Value {
194194
try await self.subscribe(to: [ValkeySubscriptions.invalidateChannel]) { subscription in
195-
let keys = subscription.map { ValkeyKey(rawValue: $0.message) }
195+
let keys = subscription.map { ValkeyKey($0.message) }
196196
return try await process(keys)
197197
}
198198
}

Sources/Valkey/Subscriptions/ValkeySubscriptions.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ struct ValkeySubscriptions {
8787
case .forwardMessage(let subscriptions):
8888
for subscription in subscriptions {
8989
for key in keys {
90-
subscription.sendMessage(.init(channel: Self.invalidateChannel, message: key.rawValue))
90+
subscription.sendMessage(.init(channel: Self.invalidateChannel, message: String(valkeyKey: key)))
9191
}
9292
}
9393
case .doNothing, .none:

Sources/Valkey/ValkeyKey.swift

Lines changed: 86 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,43 +15,117 @@
1515
import NIOCore
1616

1717
/// Type representing a Valkey Key
18-
public struct ValkeyKey: RawRepresentable, Sendable, Equatable, Hashable {
19-
public var rawValue: String
18+
public struct ValkeyKey: Sendable, Equatable, Hashable {
19+
@usableFromInline
20+
enum _Storage: Sendable {
21+
case string(String)
22+
case buffer(ByteBuffer)
23+
}
24+
@usableFromInline
25+
let _storage: _Storage
26+
27+
/// Initialize ValkeyKey with String
28+
/// - Parameter string: string
29+
@inlinable
30+
public init(_ string: String) {
31+
self._storage = .string(string)
32+
}
33+
34+
/// Initialize ValkeyKey with ByteBuffer
35+
/// - Parameter buffer: ByteBuffer
36+
@inlinable
37+
public init(_ buffer: ByteBuffer) {
38+
self._storage = .buffer(buffer)
39+
}
40+
41+
@inlinable
42+
static public func == (_ lhs: Self, _ rhs: Self) -> Bool {
43+
switch (lhs._storage, rhs._storage) {
44+
case (.string(let lhs), .string(let rhs)):
45+
lhs == rhs
46+
case (.buffer(let lhs), .buffer(let rhs)):
47+
lhs == rhs
48+
case (.string(let lhs), .buffer(let rhs)):
49+
lhs.utf8.elementsEqual(rhs.readableBytesView)
50+
case (.buffer(let lhs), .string(let rhs)):
51+
rhs.utf8.elementsEqual(lhs.readableBytesView)
52+
}
53+
}
2054

21-
public init(rawValue: String) {
22-
self.rawValue = rawValue
55+
@inlinable
56+
public func hash(into hasher: inout Hasher) {
57+
switch self._storage {
58+
case .string(let string): string.hash(into: &hasher)
59+
case .buffer(let buffer): buffer.hash(into: &hasher)
60+
}
2361
}
2462
}
2563

2664
extension ValkeyKey: RESPTokenDecodable {
65+
@inlinable
2766
public init(fromRESP token: RESPToken) throws {
2867
switch token.value {
2968
case .simpleString(let buffer), .bulkString(let buffer):
30-
self.rawValue = String(buffer: buffer)
69+
self._storage = .buffer(buffer)
3170
default:
3271
throw RESPParsingError(code: .unexpectedType, buffer: token.base)
3372
}
3473
}
3574
}
3675

37-
extension ValkeyKey: CustomStringConvertible {
38-
public var description: String { rawValue.description }
39-
}
40-
4176
extension ValkeyKey: RESPRenderable {
42-
4377
@inlinable
4478
public var respEntries: Int { 1 }
4579

4680
@inlinable
4781
public func encode(into commandEncoder: inout ValkeyCommandEncoder) {
48-
self.rawValue.encode(into: &commandEncoder)
82+
switch self._storage {
83+
case .string(let string):
84+
string.encode(into: &commandEncoder)
85+
case .buffer(let buffer):
86+
buffer.encode(into: &commandEncoder)
87+
}
88+
}
89+
}
90+
91+
extension ValkeyKey: RESPStringRenderable {}
92+
93+
extension ValkeyKey: CustomStringConvertible {
94+
public var description: String {
95+
switch self._storage {
96+
case .string(let string): string
97+
case .buffer(let buffer): String(buffer: buffer)
98+
}
4999
}
50100
}
51101

52102
extension ValkeyKey: ExpressibleByStringLiteral {
53103
@inlinable
54104
public init(stringLiteral string: String) {
55-
self.init(rawValue: string)
105+
self.init(string)
106+
}
107+
}
108+
109+
extension String {
110+
/// Initialize String from ValkeyKey
111+
/// - Parameter valkeyKey: key
112+
@inlinable
113+
public init(valkeyKey: ValkeyKey) {
114+
switch valkeyKey._storage {
115+
case .string(let string): self = string
116+
case .buffer(let buffer): self = String(buffer: buffer)
117+
}
118+
}
119+
}
120+
121+
extension ByteBuffer {
122+
/// Initialize ByteBuffer from ValkeyKey
123+
/// - Parameter valkeyKey: key
124+
@inlinable
125+
public init(valkeyKey: ValkeyKey) {
126+
switch valkeyKey._storage {
127+
case .string(let string): self = ByteBuffer(string: string)
128+
case .buffer(let buffer): self = buffer
129+
}
56130
}
57131
}

Tests/ClusterIntegrationTests/ClusterIntegrationTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ struct ClusterIntegrationTests {
4141
connection: some ValkeyConnectionProtocol,
4242
_ operation: (ValkeyKey) async throws -> Value
4343
) async throws -> Value {
44-
let key = ValkeyKey(rawValue: UUID().uuidString)
44+
let key = ValkeyKey(UUID().uuidString)
4545
let result: Result<Value, any Error>
4646
do {
4747
result = try await .success(operation(key))

Tests/IntegrationTests/ValkeyTests.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import Valkey
2323
struct GeneratedCommands {
2424
let valkeyHostname = ProcessInfo.processInfo.environment["VALKEY_HOSTNAME"] ?? "localhost"
2525
func withKey<Value>(connection: some ValkeyConnectionProtocol, _ operation: (ValkeyKey) async throws -> Value) async throws -> Value {
26-
let key = ValkeyKey(rawValue: UUID().uuidString)
26+
let key = ValkeyKey(UUID().uuidString)
2727
let value: Value
2828
do {
2929
value = try await operation(key)
@@ -338,9 +338,9 @@ struct GeneratedCommands {
338338
for _ in 0..<100 {
339339
group.addTask {
340340
try await withKey(connection: connection) { key in
341-
_ = try await connection.set(key: key, value: key.rawValue)
342-
let response = try await connection.get(key: key).map { String(buffer: $0) }
343-
#expect(response == key.rawValue)
341+
_ = try await connection.set(key: key, value: key)
342+
let response = try await connection.get(key: key).map { ValkeyKey($0) }
343+
#expect(response == key)
344344
}
345345
}
346346
}

Tests/ValkeyTests/Cluster/HashSlotTests.swift

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
//===----------------------------------------------------------------------===//
3131

3232
import Foundation
33+
import NIOCore
3334
import Testing
3435
import Valkey
3536

@@ -88,20 +89,27 @@ struct HashSlotTests {
8889
HashSlot.hashTag(forKey: "{user1000}.followers")
8990
)
9091
)
91-
#expect(HashSlot.hashTag(forKey: "{user1000}.following").elementsEqual("user1000".utf8))
92-
#expect(HashSlot.hashTag(forKey: "{user1000}.followers").elementsEqual("user1000".utf8))
93-
94-
#expect(HashSlot.hashTag(forKey: "foo{}{bar}").elementsEqual("foo{}{bar}".utf8))
95-
#expect(HashSlot.hashTag(forKey: "foo{{bar}}zap").elementsEqual("{bar".utf8))
96-
#expect(HashSlot.hashTag(forKey: "foo{bar}{zap}").elementsEqual("bar".utf8))
97-
#expect(HashSlot.hashTag(forKey: "{}foo{bar}{zap}").elementsEqual("{}foo{bar}{zap}".utf8))
98-
#expect(HashSlot.hashTag(forKey: "foo").elementsEqual("foo".utf8))
99-
#expect(HashSlot.hashTag(forKey: "foo}").elementsEqual("foo}".utf8))
100-
#expect(HashSlot.hashTag(forKey: "{foo}").elementsEqual("foo".utf8))
101-
#expect(HashSlot.hashTag(forKey: "bar{foo}").elementsEqual("foo".utf8))
102-
#expect(HashSlot.hashTag(forKey: "bar{}").elementsEqual("bar{}".utf8))
103-
#expect(HashSlot.hashTag(forKey: "{}").elementsEqual("{}".utf8))
104-
#expect(HashSlot.hashTag(forKey: "{}bar").elementsEqual("{}bar".utf8))
92+
#expect(HashSlot.hashTag(forKey: "{user1000}.following").elementsEqual("user1000"))
93+
#expect(HashSlot.hashTag(forKey: "{user1000}.followers").elementsEqual("user1000"))
94+
95+
#expect(HashSlot.hashTag(forKey: "foo{}{bar}").elementsEqual("foo{}{bar}"))
96+
#expect(HashSlot.hashTag(forKey: "foo{{bar}}zap").elementsEqual("{bar"))
97+
#expect(HashSlot.hashTag(forKey: "foo{bar}{zap}").elementsEqual("bar"))
98+
#expect(HashSlot.hashTag(forKey: "{}foo{bar}{zap}").elementsEqual("{}foo{bar}{zap}"))
99+
#expect(HashSlot.hashTag(forKey: "foo").elementsEqual("foo"))
100+
#expect(HashSlot.hashTag(forKey: "foo}").elementsEqual("foo}"))
101+
#expect(HashSlot.hashTag(forKey: "{foo}").elementsEqual("foo"))
102+
#expect(HashSlot.hashTag(forKey: "bar{foo}").elementsEqual("foo"))
103+
#expect(HashSlot.hashTag(forKey: "bar{}").elementsEqual("bar{}"))
104+
#expect(HashSlot.hashTag(forKey: "{}").elementsEqual("{}"))
105+
#expect(HashSlot.hashTag(forKey: "{}bar").elementsEqual("{}bar"))
106+
}
107+
108+
@Test
109+
func byteBufferHashTagComputation() {
110+
#expect(HashSlot(key: ValkeyKey(ByteBuffer(string: "foo"))) == HashSlot(key: "foo"))
111+
#expect(HashSlot(key: ValkeyKey(ByteBuffer(string: "foo{bar}"))) == HashSlot(key: "foo{bar}"))
112+
#expect(HashSlot(key: ValkeyKey(ByteBuffer(string: "foo{bar}"))) == HashSlot(key: "bar"))
105113
}
106114

107115
@Test
@@ -113,3 +121,12 @@ struct HashSlotTests {
113121
#expect("\(HashSlot(rawValue: 20)!)" == "20")
114122
}
115123
}
124+
125+
extension HashSlot {
126+
/// Computes the portion of the key that should be used for hash slot calculation.
127+
///
128+
/// Helper for tests
129+
static func hashTag(forKey key: String) -> Substring {
130+
Substring(Self.hashTag(forKey: key.utf8))
131+
}
132+
}

Tests/ValkeyTests/CommandTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,11 +236,11 @@ struct CommandTests {
236236
try await withThrowingTaskGroup(of: Void.self) { group in
237237
group.addTask {
238238
var result = try await connection.zmpop(key: ["key", "key2"], where: .max)
239-
#expect(result?.key == ValkeyKey(rawValue: "key"))
239+
#expect(result?.key == "key")
240240
#expect(result?.values[0].score == 3)
241241
#expect((result?.values[0].value).map { String(buffer: $0) } == "three")
242242
result = try await connection.zmpop(key: ["key", "key2"], where: .max, count: 2)
243-
#expect(result?.key == ValkeyKey(rawValue: "key2"))
243+
#expect(result?.key == ValkeyKey("key2"))
244244
#expect(result?.values[0].score == 5)
245245
#expect((result?.values[0].value).map { String(buffer: $0) } == "five")
246246
#expect(result?.values[1].score == 4)

0 commit comments

Comments
 (0)