Skip to content

Commit 9ae24ae

Browse files
authored
Implement Add and Replace support (#28)
* add + replace #25 * soundness * more test for add + replace * build faiure fix?
1 parent 2ac2b44 commit 9ae24ae

File tree

7 files changed

+259
-20
lines changed

7 files changed

+259
-20
lines changed

Sources/SwiftMemcache/Extensions/ByteBuffer+SwiftMemcache.swift

+6-4
Original file line numberDiff line numberDiff line change
@@ -88,15 +88,17 @@ extension ByteBuffer {
8888
}
8989

9090
if let storageMode = flags.storageMode {
91+
self.writeInteger(UInt8.whitespace)
92+
self.writeInteger(UInt8.M)
9193
switch storageMode {
94+
case .add:
95+
self.writeInteger(UInt8.E)
9296
case .append:
93-
self.writeInteger(UInt8.whitespace)
94-
self.writeInteger(UInt8.M)
9597
self.writeInteger(UInt8.A)
9698
case .prepend:
97-
self.writeInteger(UInt8.whitespace)
98-
self.writeInteger(UInt8.M)
9999
self.writeInteger(UInt8.P)
100+
case .replace:
101+
self.writeInteger(UInt8.R)
100102
}
101103
}
102104
}

Sources/SwiftMemcache/Extensions/UInt8+Characters.swift

+2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ extension UInt8 {
2525
static var M: UInt8 = .init(ascii: "M")
2626
static var P: UInt8 = .init(ascii: "P")
2727
static var A: UInt8 = .init(ascii: "A")
28+
static var E: UInt8 = .init(ascii: "E")
29+
static var R: UInt8 = .init(ascii: "R")
2830
static var zero: UInt8 = .init(ascii: "0")
2931
static var nine: UInt8 = .init(ascii: "9")
3032
}

Sources/SwiftMemcache/MemcachedConnection.swift

+84
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ public actor MemcachedConnection {
6161
case unexpectedNilResponse
6262
/// Indicates that the key was not found.
6363
case keyNotFound
64+
/// Indicates that the key already exist
65+
case keyExist
6466
}
6567

6668
private var state: State
@@ -340,4 +342,86 @@ public actor MemcachedConnection {
340342
throw MemcachedConnectionError.connectionShutdown
341343
}
342344
}
345+
346+
// MARK: - Adding a Value
347+
348+
/// Adds a new key-value pair in the Memcached server.
349+
/// The operation will fail if the key already exists.
350+
///
351+
/// - Parameters:
352+
/// - key: The key to add the value to.
353+
/// - value: The `MemcachedValue` to add.
354+
/// - Throws: A `MemcachedConnectionError.connectionShutdown` if the connection to the Memcached server is shut down.
355+
/// - Throws: A `MemcachedConnectionError.keyExist` if the key already exists in the Memcached server.
356+
/// - Throws: A `MemcachedConnectionError.unexpectedNilResponse` if an unexpected response code is returned.
357+
public func add(_ key: String, value: some MemcachedValue) async throws {
358+
switch self.state {
359+
case .initial(_, let bufferAllocator, _, _),
360+
.running(let bufferAllocator, _, _, _):
361+
362+
var buffer = bufferAllocator.buffer(capacity: 0)
363+
value.writeToBuffer(&buffer)
364+
var flags: MemcachedFlags
365+
366+
flags = MemcachedFlags()
367+
flags.storageMode = .add
368+
369+
let command = MemcachedRequest.SetCommand(key: key, value: buffer, flags: flags)
370+
let request = MemcachedRequest.set(command)
371+
372+
let response = try await sendRequest(request)
373+
374+
switch response.returnCode {
375+
case .HD:
376+
return
377+
case .NS:
378+
throw MemcachedConnectionError.keyExist
379+
default:
380+
throw MemcachedConnectionError.unexpectedNilResponse
381+
}
382+
383+
case .finished:
384+
throw MemcachedConnectionError.connectionShutdown
385+
}
386+
}
387+
388+
// MARK: - Replacing a Value
389+
390+
/// Replace the value for an existing key in the Memcache server.
391+
/// The operation will fail if the key does not exist.
392+
///
393+
/// - Parameters:
394+
/// - key: The key to replace the value for.
395+
/// - value: The `MemcachedValue` to replace.
396+
/// - Throws: A `MemcachedConnectionError` if the connection to the Memcached server is shut down.
397+
public func replace(_ key: String, value: some MemcachedValue) async throws {
398+
switch self.state {
399+
case .initial(_, let bufferAllocator, _, _),
400+
.running(let bufferAllocator, _, _, _):
401+
402+
var buffer = bufferAllocator.buffer(capacity: 0)
403+
value.writeToBuffer(&buffer)
404+
var flags: MemcachedFlags
405+
406+
flags = MemcachedFlags()
407+
flags.storageMode = .replace
408+
409+
let command = MemcachedRequest.SetCommand(key: key, value: buffer, flags: flags)
410+
let request = MemcachedRequest.set(command)
411+
412+
let response = try await sendRequest(request)
413+
414+
switch response.returnCode {
415+
case .HD:
416+
return
417+
case .NS:
418+
throw MemcachedConnectionError.keyNotFound
419+
default:
420+
throw MemcachedConnectionError.unexpectedNilResponse
421+
}
422+
423+
case .finished:
424+
throw MemcachedConnectionError.connectionShutdown
425+
}
426+
}
343427
}

Sources/SwiftMemcache/MemcachedFlags.swift

+5-1
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,15 @@ public enum TimeToLive: Equatable, Hashable {
4949
}
5050

5151
/// Enum representing the Memcached 'ms' (meta set) command modes (corresponding to the 'M' flag).
52-
public enum StorageMode: Equatable, Hashable {
52+
enum StorageMode: Equatable, Hashable {
53+
/// The "add" command. If the item exists, LRU is bumped and NS is returned.
54+
case add
5355
/// The 'append' command. If the item exists, append the new value to its data.
5456
case append
5557
/// The 'prepend' command. If the item exists, prepend the new value to its data.
5658
case prepend
59+
/// The "replace" command. The new value is set only if the item already exists.
60+
case replace
5761
}
5862

5963
extension MemcachedFlags: Hashable {}

Tests/SwiftMemcacheTests/IntegrationTest/MemcachedIntegrationTests.swift

+128
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,134 @@ final class MemcachedIntegrationTest: XCTestCase {
306306
}
307307
}
308308

309+
func testAddValue() async throws {
310+
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
311+
defer {
312+
XCTAssertNoThrow(try! group.syncShutdownGracefully())
313+
}
314+
let memcachedConnection = MemcachedConnection(host: "memcached", port: 11211, eventLoopGroup: group)
315+
316+
try await withThrowingTaskGroup(of: Void.self) { group in
317+
group.addTask { try await memcachedConnection.run() }
318+
319+
// Add a value to a key
320+
let addValue = "foo"
321+
322+
// Attempt to delete the key, but ignore the error if it doesn't exist
323+
do {
324+
try await memcachedConnection.delete("adds")
325+
} catch {
326+
if "\(error)" != "keyNotFound" {
327+
throw error
328+
}
329+
}
330+
331+
// Proceed with adding the key-value pair
332+
try await memcachedConnection.add("adds", value: addValue)
333+
334+
// Get value for the key after add operation
335+
let addedValue: String? = try await memcachedConnection.get("adds")
336+
XCTAssertEqual(addedValue, addValue, "Received value should be the same as the added value")
337+
338+
group.cancelAll()
339+
}
340+
}
341+
342+
func testAddValueKeyExists() async throws {
343+
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
344+
defer {
345+
XCTAssertNoThrow(try! group.syncShutdownGracefully())
346+
}
347+
let memcachedConnection = MemcachedConnection(host: "memcached", port: 11211, eventLoopGroup: group)
348+
349+
try await withThrowingTaskGroup(of: Void.self) { group in
350+
group.addTask { try await memcachedConnection.run() }
351+
352+
// Add a value to a key
353+
let initialValue = "foo"
354+
let newValue = "bar"
355+
356+
// Attempt to delete the key, but ignore the error if it doesn't exist
357+
do {
358+
try await memcachedConnection.delete("adds")
359+
} catch {
360+
if "\(error)" != "keyNotFound" {
361+
throw error
362+
}
363+
}
364+
365+
// Set an initial value for the key
366+
try await memcachedConnection.add("adds", value: initialValue)
367+
368+
do {
369+
// Attempt to add a new value to the existing key
370+
try await memcachedConnection.add("adds", value: newValue)
371+
XCTFail("Expected an error indicating the key exists, but no error was thrown.")
372+
} catch {
373+
// Check if the error description or localized description matches the expected error
374+
if "\(error)" != "keyExist" {
375+
XCTFail("Unexpected error: \(error)")
376+
}
377+
}
378+
379+
group.cancelAll()
380+
}
381+
}
382+
383+
func testReplaceValue() async throws {
384+
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
385+
defer {
386+
XCTAssertNoThrow(try! group.syncShutdownGracefully())
387+
}
388+
let memcachedConnection = MemcachedConnection(host: "memcached", port: 11211, eventLoopGroup: group)
389+
390+
try await withThrowingTaskGroup(of: Void.self) { group in
391+
group.addTask { try await memcachedConnection.run() }
392+
393+
// Set key and initial value
394+
let initialValue = "foo"
395+
try await memcachedConnection.set("greet", value: initialValue)
396+
397+
// Replace value for the key
398+
let replaceValue = "hi"
399+
try await memcachedConnection.replace("greet", value: replaceValue)
400+
401+
// Get value for the key after replace operation
402+
let replacedValue: String? = try await memcachedConnection.get("greet")
403+
XCTAssertEqual(replacedValue, replaceValue, "Received value should be the same as the replaceValue")
404+
405+
group.cancelAll()
406+
}
407+
}
408+
409+
func testReplaceNonExistentKey() async throws {
410+
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
411+
defer {
412+
XCTAssertNoThrow(try! group.syncShutdownGracefully())
413+
}
414+
let memcachedConnection = MemcachedConnection(host: "memcached", port: 11211, eventLoopGroup: group)
415+
416+
await withThrowingTaskGroup(of: Void.self) { group in
417+
group.addTask { try await memcachedConnection.run() }
418+
419+
do {
420+
// Ensure the key is clean
421+
try await memcachedConnection.delete("nonExistentKey")
422+
// Attempt to replace value for a non-existent key
423+
let replaceValue = "testValue"
424+
try await memcachedConnection.replace("nonExistentKey", value: replaceValue)
425+
XCTFail("Expected an error indicating the key was not found, but no error was thrown.")
426+
} catch {
427+
// Check if the error description or localized description matches the expected error
428+
if "\(error)" != "keyNotFound" {
429+
XCTFail("Unexpected error: \(error)")
430+
}
431+
}
432+
433+
group.cancelAll()
434+
}
435+
}
436+
309437
func testMemcachedConnectionWithUInt() async throws {
310438
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
311439
defer {

Tests/SwiftMemcacheTests/UnitTest/MemcachedFlagsTests.swift

+20
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,16 @@ final class MemcachedFlagsTests: XCTestCase {
3838
}
3939
}
4040

41+
func testStorageModeAdd() {
42+
var flags = MemcachedFlags()
43+
flags.storageMode = .add
44+
if case .add? = flags.storageMode {
45+
XCTAssertTrue(true)
46+
} else {
47+
XCTFail("Flag storageMode is not .add")
48+
}
49+
}
50+
4151
func testStorageModeAppend() {
4252
var flags = MemcachedFlags()
4353
flags.storageMode = .append
@@ -57,4 +67,14 @@ final class MemcachedFlagsTests: XCTestCase {
5767
XCTFail("Flag storageMode is not .prepend")
5868
}
5969
}
70+
71+
func testStorageModeReplace() {
72+
var flags = MemcachedFlags()
73+
flags.storageMode = .replace
74+
if case .replace? = flags.storageMode {
75+
XCTAssertTrue(true)
76+
} else {
77+
XCTFail("Flag storageMode is not .replace")
78+
}
79+
}
6080
}

Tests/SwiftMemcacheTests/UnitTest/MemcachedRequestEncoderTests.swift

+14-15
Original file line numberDiff line numberDiff line change
@@ -48,38 +48,37 @@ final class MemcachedRequestEncoderTests: XCTestCase {
4848
XCTAssertEqual(outBuffer.getString(at: 0, length: outBuffer.readableBytes), expectedEncodedData)
4949
}
5050

51-
func testEncodeAppendRequest() {
51+
func testEncodeStorageRequest(withMode mode: StorageMode, expectedEncodedData: String) {
5252
// Prepare a MemcachedRequest
5353
var buffer = ByteBufferAllocator().buffer(capacity: 2)
5454
buffer.writeString("hi")
5555

5656
var flags = MemcachedFlags()
57-
flags.storageMode = .append
57+
flags.storageMode = mode
5858
let command = MemcachedRequest.SetCommand(key: "foo", value: buffer, flags: flags)
5959
let request = MemcachedRequest.set(command)
6060

6161
// pass our request through the encoder
6262
let outBuffer = self.encodeRequest(request)
6363

64-
let expectedEncodedData = "ms foo 2 MA\r\nhi\r\n"
64+
// assert the encoded request
6565
XCTAssertEqual(outBuffer.getString(at: 0, length: outBuffer.readableBytes), expectedEncodedData)
6666
}
6767

68-
func testEncodePrependRequest() {
69-
// Prepare a MemcachedRequest
70-
var buffer = ByteBufferAllocator().buffer(capacity: 2)
71-
buffer.writeString("hi")
68+
func testEncodeAppendRequest() {
69+
self.testEncodeStorageRequest(withMode: .append, expectedEncodedData: "ms foo 2 MA\r\nhi\r\n")
70+
}
7271

73-
var flags = MemcachedFlags()
74-
flags.storageMode = .prepend
75-
let command = MemcachedRequest.SetCommand(key: "foo", value: buffer, flags: flags)
76-
let request = MemcachedRequest.set(command)
72+
func testEncodePrependRequest() {
73+
self.testEncodeStorageRequest(withMode: .prepend, expectedEncodedData: "ms foo 2 MP\r\nhi\r\n")
74+
}
7775

78-
// pass our request through the encoder
79-
let outBuffer = self.encodeRequest(request)
76+
func testEncodeAddRequest() {
77+
self.testEncodeStorageRequest(withMode: .add, expectedEncodedData: "ms foo 2 ME\r\nhi\r\n")
78+
}
8079

81-
let expectedEncodedData = "ms foo 2 MP\r\nhi\r\n"
82-
XCTAssertEqual(outBuffer.getString(at: 0, length: outBuffer.readableBytes), expectedEncodedData)
80+
func testEncodeReplaceRequest() {
81+
self.testEncodeStorageRequest(withMode: .replace, expectedEncodedData: "ms foo 2 MR\r\nhi\r\n")
8382
}
8483

8584
func testEncodeTouchRequest() {

0 commit comments

Comments
 (0)