Skip to content

Commit 0b17329

Browse files
authored
Implement Time-To-Live (TTL) Support (#23)
* set support for TTL * TTL support for get/set * remove debug statements * update doc comments * keep our main example the same * naming consistancy in test * PR comment fixes * refactored flags to hold TimeToLive * indefinitely test * naming consistency * TTL conformance for Eq + Hash * refactored MemcachedConnection * soundness * remove flgs from set * clean up remaining PR cmnts * no longer using these * closes #17
1 parent 67477dd commit 0b17329

14 files changed

+415
-54
lines changed

Package.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ import PackageDescription
1818
let package = Package(
1919
name: "swift-memcache-gsoc",
2020
platforms: [
21-
.macOS(.v10_15),
22-
.iOS(.v13),
23-
.watchOS(.v6),
24-
.tvOS(.v13),
21+
.macOS(.v13),
22+
.iOS(.v16),
23+
.watchOS(.v9),
24+
.tvOS(.v16),
2525
],
2626
products: [
2727
.library(

Sources/SwiftMemcache/Extensions/ByteBuffer+SwiftMemcache.swift

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15+
import Foundation
1516
import NIOCore
1617

1718
extension ByteBuffer {
@@ -30,13 +31,13 @@ extension ByteBuffer {
3031
/// Reads an integer from ASCII characters directly from this `ByteBuffer`.
3132
/// The reading stops as soon as a non-digit character is encountered.
3233
///
33-
/// - Returns: A `UInt64` integer read from the buffer.
34+
/// - Returns: A `T` integer read from the buffer.
3435
/// If the buffer does not contain any digits at the current reading position, returns `nil`.
35-
mutating func readIntegerFromASCII() -> UInt64? {
36-
var value: UInt64 = 0
36+
mutating func readIntegerFromASCII<T: FixedWidthInteger>() -> T? {
37+
var value: T = 0
3738
while self.readableBytes > 0, let currentByte = self.readInteger(as: UInt8.self),
3839
currentByte >= UInt8.zero && currentByte <= UInt8.nine {
39-
value = (value * 10) + UInt64(currentByte - UInt8.zero)
40+
value = (value * 10) + T(currentByte - UInt8.zero)
4041
}
4142
return value > 0 ? value : nil
4243
}
@@ -56,6 +57,35 @@ extension ByteBuffer {
5657
self.writeInteger(UInt8.whitespace)
5758
self.writeInteger(UInt8.v)
5859
}
60+
61+
if let timeToLive = flags.timeToLive {
62+
switch timeToLive {
63+
case .indefinitely:
64+
self.writeInteger(UInt8.whitespace)
65+
self.writeInteger(UInt8.T)
66+
self.writeInteger(UInt8.zero)
67+
case .expiresAt(let instant):
68+
let now = ContinuousClock.now
69+
let duration = now.duration(to: instant)
70+
let ttlSeconds = duration.components.seconds
71+
let maximumOffset = 60 * 60 * 24 * 30
72+
73+
if ttlSeconds > maximumOffset {
74+
// The Time-To-Live is treated as Unix time.
75+
var timespec = timespec()
76+
timespec_get(&timespec, TIME_UTC)
77+
let timeIntervalNow = Double(timespec.tv_sec) + Double(timespec.tv_nsec) / 1_000_000_000
78+
let ttlUnixTime = Int32(timeIntervalNow) + Int32(ttlSeconds)
79+
self.writeInteger(UInt8.whitespace)
80+
self.writeInteger(UInt8.T)
81+
self.writeIntegerAsASCII(ttlUnixTime)
82+
} else {
83+
self.writeInteger(UInt8.whitespace)
84+
self.writeInteger(UInt8.T)
85+
self.writeIntegerAsASCII(ttlSeconds)
86+
}
87+
}
88+
}
5989
}
6090
}
6191

Sources/SwiftMemcache/Extensions/UInt8+Characters.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ extension UInt8 {
2020
static var s: UInt8 = .init(ascii: "s")
2121
static var g: UInt8 = .init(ascii: "g")
2222
static var v: UInt8 = .init(ascii: "v")
23+
static var T: UInt8 = .init(ascii: "T")
2324
static var zero: UInt8 = .init(ascii: "0")
2425
static var nine: UInt8 = .init(ascii: "9")
2526
}

Sources/SwiftMemcache/MemcachedConnection.swift

Lines changed: 75 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -136,21 +136,13 @@ public actor MemcachedConnection {
136136
}
137137
}
138138

139-
/// Fetch the value for a key from the Memcache server.
140-
///
141-
/// - Parameter key: The key to fetch the value for.
142-
/// - Returns: A `Value` containing the fetched value, or `nil` if no value was found.
143-
public func get<Value: MemcachedValue>(_ key: String, as valueType: Value.Type = Value.self) async throws -> Value? {
139+
/// Send a request to the Memcached server and returns a `MemcachedResponse`.
140+
private func sendRequest(_ request: MemcachedRequest) async throws -> MemcachedResponse {
144141
switch self.state {
145142
case .initial(_, _, _, let requestContinuation),
146143
.running(_, _, _, let requestContinuation):
147144

148-
var flags = MemcachedFlags()
149-
flags.shouldReturnValue = true
150-
let command = MemcachedRequest.GetCommand(key: key, flags: flags)
151-
let request = MemcachedRequest.get(command)
152-
153-
let response = try await withCheckedThrowingContinuation { continuation in
145+
return try await withCheckedThrowingContinuation { continuation in
154146
switch requestContinuation.yield((request, continuation)) {
155147
case .enqueued:
156148
break
@@ -159,7 +151,31 @@ public actor MemcachedConnection {
159151
default:
160152
break
161153
}
162-
}.value
154+
}
155+
156+
case .finished:
157+
throw MemcachedConnectionError.connectionShutdown
158+
}
159+
}
160+
161+
// MARK: - Fetching Values
162+
163+
/// Fetch the value for a key from the Memcache server.
164+
///
165+
/// - Parameter key: The key to fetch the value for.
166+
/// - Returns: A `Value` containing the fetched value, or `nil` if no value was found.
167+
public func get<Value: MemcachedValue>(_ key: String, as valueType: Value.Type = Value.self) async throws -> Value? {
168+
switch self.state {
169+
case .initial(_, _, _, _),
170+
.running:
171+
172+
var flags = MemcachedFlags()
173+
flags.shouldReturnValue = true
174+
175+
let command = MemcachedRequest.GetCommand(key: key, flags: flags)
176+
let request = MemcachedRequest.get(command)
177+
178+
let response = try await sendRequest(request).value
163179

164180
if var unwrappedResponse = response {
165181
return Value.readFromBuffer(&unwrappedResponse)
@@ -171,31 +187,61 @@ public actor MemcachedConnection {
171187
}
172188
}
173189

174-
/// Set the value for a key on the Memcache server.
190+
// MARK: - Touch
191+
192+
/// Update the time-to-live for a key.
193+
///
194+
/// This method changes the expiration time of an existing item without fetching it. If the key does not exist or if the new expiration time is already passed, the operation will not succeed.
175195
///
176196
/// - Parameters:
177-
/// - key: The key to set the value for.
178-
/// - value: The `Value` to set for the key.
179-
public func set(_ key: String, value: some MemcachedValue) async throws {
197+
/// - key: The key to update the time-to-live for.
198+
/// - newTimeToLive: The new time-to-live.
199+
/// - Throws: A `MemcachedConnectionError` if the connection is shutdown or if there's an unexpected nil response.
200+
public func touch(_ key: String, newTimeToLive: TimeToLive) async throws {
180201
switch self.state {
181-
case .initial(_, let bufferAllocator, _, let requestContinuation),
182-
.running(let bufferAllocator, _, _, let requestContinuation):
202+
case .initial(_, _, _, _),
203+
.running:
204+
205+
var flags = MemcachedFlags()
206+
flags.timeToLive = newTimeToLive
207+
208+
let command = MemcachedRequest.GetCommand(key: key, flags: flags)
209+
let request = MemcachedRequest.get(command)
210+
211+
_ = try await self.sendRequest(request)
212+
213+
case .finished:
214+
throw MemcachedConnectionError.connectionShutdown
215+
}
216+
}
217+
218+
// MARK: - Setting a Value
219+
220+
/// Sets a value for a specified key in the Memcache server with an optional Time-to-Live (TTL) parameter.
221+
///
222+
/// - Parameters:
223+
/// - key: The key for which the value is to be set.
224+
/// - value: The `MemcachedValue` to set for the key.
225+
/// - expiration: An optional `TimeToLive` value specifying the TTL (Time-To-Live) for the key-value pair.
226+
/// If provided, the key-value pair will be removed from the cache after the specified TTL duration has passed.
227+
/// If not provided, the key-value pair will persist indefinitely in the cache.
228+
/// - Throws: A `MemcachedConnectionError` if the connection to the Memcached server is shut down.
229+
public func set(_ key: String, value: some MemcachedValue, timeToLive: TimeToLive = .indefinitely) async throws {
230+
switch self.state {
231+
case .initial(_, let bufferAllocator, _, _),
232+
.running(let bufferAllocator, _, _, _):
183233

184234
var buffer = bufferAllocator.buffer(capacity: 0)
185235
value.writeToBuffer(&buffer)
186-
let command = MemcachedRequest.SetCommand(key: key, value: buffer)
236+
var flags: MemcachedFlags?
237+
238+
flags = MemcachedFlags()
239+
flags?.timeToLive = timeToLive
240+
241+
let command = MemcachedRequest.SetCommand(key: key, value: buffer, flags: flags)
187242
let request = MemcachedRequest.set(command)
188243

189-
_ = try await withCheckedThrowingContinuation { continuation in
190-
switch requestContinuation.yield((request, continuation)) {
191-
case .enqueued:
192-
break
193-
case .dropped, .terminated:
194-
continuation.resume(throwing: MemcachedConnectionError.connectionShutdown)
195-
default:
196-
break
197-
}
198-
}.value
244+
_ = try await self.sendRequest(request)
199245

200246
case .finished:
201247
throw MemcachedConnectionError.connectionShutdown

Sources/SwiftMemcache/MemcachedFlags.swift

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,32 @@
1414

1515
/// Struct representing the flags of a Memcached command.
1616
///
17-
/// Flags for the 'mg' (meta get) command are represented in this struct.
18-
/// Currently, only the 'v' flag for the meta get command is supported,
19-
/// which dictates whether the item value should be returned in the data block.
17+
/// Flags for the 'mg' (meta get) and 'ms' (meta set) commands are represented in this struct.
18+
/// The 'v' flag for the meta get command dictates whether the item value should be returned in the data block.
19+
/// The 'T' flag is used for both the meta get and meta set commands to specify the Time-To-Live (TTL) for an item.
20+
/// The 't' flag for the meta get command indicates whether the Time-To-Live (TTL) for the item should be returned.
2021
struct MemcachedFlags {
2122
/// Flag 'v' for the 'mg' (meta get) command.
2223
///
2324
/// If true, the item value is returned in the data block.
2425
/// If false, the data block for the 'mg' response is optional, and the response code changes from "HD" to "VA <size>".
2526
var shouldReturnValue: Bool?
2627

28+
/// Flag 'T' for the 'mg' (meta get) and 'ms' (meta set) commands.
29+
///
30+
/// Represents the Time-To-Live (TTL) for an item, in seconds.
31+
/// If set, the item is considered to be expired after this number of seconds.
32+
var timeToLive: TimeToLive?
33+
2734
init() {}
2835
}
2936

37+
/// Enum representing the Time-To-Live (TTL) of a Memcached value.
38+
public enum TimeToLive: Equatable, Hashable {
39+
/// The value should never expire.
40+
case indefinitely
41+
/// The value should expire after a specified time.
42+
case expiresAt(ContinuousClock.Instant)
43+
}
44+
3045
extension MemcachedFlags: Hashable {}

Sources/SwiftMemcache/MemcachedRequest.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@
1313
//===----------------------------------------------------------------------===//
1414

1515
import NIOCore
16-
1716
enum MemcachedRequest {
1817
struct SetCommand {
1918
let key: String
2019
var value: ByteBuffer
20+
var flags: MemcachedFlags?
2121
}
2222

2323
struct GetCommand {

Sources/SwiftMemcache/MemcachedRequestEncoder.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ struct MemcachedRequestEncoder: MessageToByteEncoder {
3434
let length = command.value.readableBytes
3535
out.writeIntegerAsASCII(length)
3636

37+
// write flags if there are any
38+
if let flags = command.flags {
39+
out.writeMemcachedFlags(flags: flags)
40+
}
41+
3742
// write separator
3843
out.writeInteger(UInt8.carriageReturn)
3944
out.writeInteger(UInt8.newline)

Sources/SwiftMemcache/MemcachedResponse.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ struct MemcachedResponse {
2121
case EX
2222
case NF
2323
case VA
24+
case EN
2425

2526
init(_ bytes: UInt16) {
2627
switch bytes {
@@ -34,6 +35,8 @@ struct MemcachedResponse {
3435
self = .NF
3536
case 0x5641:
3637
self = .VA
38+
case 0x454E:
39+
self = .EN
3740
default:
3841
preconditionFailure("Unrecognized response code.")
3942
}

Sources/SwiftMemcache/MemcachedResponseDecoder.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ struct MemcachedResponseDecoder: NIOSingleStepByteToMessageDecoder {
134134
buffer.moveReaderIndex(forwardBy: 1)
135135
}
136136

137-
guard let dataLength = buffer.readIntegerFromASCII() else {
137+
guard let dataLength: UInt64 = buffer.readIntegerFromASCII() else {
138138
throw MemcachedDecoderError.unexpectedCharacter(buffer.readableBytesView[buffer.readerIndex])
139139
}
140140

0 commit comments

Comments
 (0)