Skip to content

Commit 4cd5855

Browse files
glbrnttMordil
authored andcommitted
Add TTL and PTTL commands
Motivation: The TTL and PTTL commands are missing. Modifications: - Add TTL and PTTL commands - Add integration tests Result: - Users can query the ttl in seconds or milliseconds of a key
1 parent 123d9c9 commit 4cd5855

File tree

5 files changed

+229
-0
lines changed

5 files changed

+229
-0
lines changed

Sources/RediStack/Commands/BasicCommands.swift

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,105 @@ extension RedisClient {
142142
}
143143
}
144144

145+
// MARK: TTL
146+
147+
extension RedisClient {
148+
/// Returns the remaining time-to-live (in seconds) of the provided key.
149+
///
150+
/// [https://redis.io/commands/ttl](https://redis.io/commands/ttl)
151+
/// - Parameter key: The key to check the time-to-live on.
152+
/// - Returns: The number of seconds before the given key will expire.
153+
public func ttl(_ key: RedisKey) -> EventLoopFuture<RedisKeyLifetime> {
154+
let args: [RESPValue] = [RESPValue(from: key)]
155+
return self.send(command: "TTL", with: args)
156+
.map(to: Int64.self)
157+
.map { RedisKeyLifetime(seconds: $0) }
158+
}
159+
160+
/// Returns the remaining time-to-live (in milliseconds) of the provided key.
161+
///
162+
/// [https://redis.io/commands/pttl](https://redis.io/commands/pttl)
163+
/// - Parameter key: The key to check the time-to-live on.
164+
/// - Returns: The number of milliseconds before the given key will expire.
165+
public func pttl(_ key: RedisKey) -> EventLoopFuture<RedisKeyLifetime> {
166+
let args: [RESPValue] = [RESPValue(from: key)]
167+
return self.send(command: "PTTL", with: args)
168+
.map(to: Int64.self)
169+
.map { RedisKeyLifetime(milliseconds: $0) }
170+
}
171+
}
172+
173+
174+
/// The lifetime of a `RedisKey` as determined by `ttl` or `pttl`.
175+
public enum RedisKeyLifetime: Hashable {
176+
/// The key does not exist.
177+
case keyDoesNotExist
178+
/// The key exists but has no expiry associated with it.
179+
case unlimited
180+
/// The key exists for the given lifetime.
181+
case limited(Lifetime)
182+
}
183+
184+
extension RedisKeyLifetime {
185+
/// The lifetime for a `RedisKey` which has an expiry set.
186+
public enum Lifetime: Comparable, Hashable {
187+
/// The remaining time-to-live in seconds.
188+
case seconds(Int64)
189+
/// The remaining time-to-live in milliseconds.
190+
case milliseconds(Int64)
191+
192+
/// The remaining time-to-live.
193+
public var timeAmount: TimeAmount {
194+
switch self {
195+
case .seconds(let amount): return .seconds(amount)
196+
case .milliseconds(let amount): return .milliseconds(amount)
197+
}
198+
}
199+
200+
public static func <(lhs: Lifetime, rhs: Lifetime) -> Bool {
201+
return lhs.timeAmount < rhs.timeAmount
202+
}
203+
204+
public static func ==(lhs: Lifetime, rhs: Lifetime) -> Bool {
205+
return lhs.timeAmount == rhs.timeAmount
206+
}
207+
}
208+
}
209+
210+
extension RedisKeyLifetime {
211+
/// The remaining time-to-live for the key, or `nil` if the key does not exist or will not expire.
212+
public var timeAmount: TimeAmount? {
213+
switch self {
214+
case .keyDoesNotExist, .unlimited: return nil
215+
case .limited(let lifetime): return lifetime.timeAmount
216+
}
217+
}
218+
}
219+
220+
extension RedisKeyLifetime {
221+
internal init(seconds: Int64) {
222+
switch seconds {
223+
case -2:
224+
self = .keyDoesNotExist
225+
case -1:
226+
self = .unlimited
227+
default:
228+
self = .limited(.seconds(seconds))
229+
}
230+
}
231+
232+
internal init(milliseconds: Int64) {
233+
switch milliseconds {
234+
case -2:
235+
self = .keyDoesNotExist
236+
case -1:
237+
self = .unlimited
238+
default:
239+
self = .limited(.milliseconds(milliseconds))
240+
}
241+
}
242+
}
243+
145244
// MARK: Scan
146245

147246
extension RedisClient {

Tests/RediStackIntegrationTests/Commands/BasicCommandsTests.swift

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,68 @@ final class BasicCommandsTests: RediStackIntegrationTestCase {
6565
XCTAssertNotNil(try connection.get(#function).wait())
6666
}
6767

68+
func test_ttl() throws {
69+
try self.connection.set("first", to: "value").wait()
70+
let expire = try self.connection.expire("first", after: .minutes(1)).wait()
71+
XCTAssertTrue(expire)
72+
73+
let ttl = try self.connection.ttl("first").wait()
74+
switch ttl {
75+
case .keyDoesNotExist, .unlimited:
76+
XCTFail("Expected an expiry to be set on key 'first'")
77+
case .limited(let lifetime):
78+
XCTAssertGreaterThanOrEqual(lifetime.timeAmount.nanoseconds, 0)
79+
}
80+
81+
let doesNotExist = try self.connection.ttl("second").wait()
82+
switch doesNotExist {
83+
case .keyDoesNotExist:
84+
() // Expected
85+
case .unlimited, .limited:
86+
XCTFail("Expected '.keyDoesNotExist' but lifetime was \(doesNotExist)")
87+
}
88+
89+
try self.connection.set("second", to: "value").wait()
90+
let hasNoExpire = try self.connection.ttl("second").wait()
91+
switch hasNoExpire {
92+
case .unlimited:
93+
() // Expected
94+
case .keyDoesNotExist, .limited:
95+
XCTFail("Expected '.noExpiry' but lifetime was \(hasNoExpire)")
96+
}
97+
}
98+
99+
func test_pttl() throws {
100+
try self.connection.set("first", to: "value").wait()
101+
let expire = try self.connection.expire("first", after: .minutes(1)).wait()
102+
XCTAssertTrue(expire)
103+
104+
let pttl = try self.connection.pttl("first").wait()
105+
switch pttl {
106+
case .keyDoesNotExist, .unlimited:
107+
XCTFail("Expected an expiry to be set on key 'first'")
108+
case .limited(let lifetime):
109+
XCTAssertGreaterThanOrEqual(lifetime.timeAmount.nanoseconds, 0)
110+
}
111+
112+
let doesNotExist = try self.connection.ttl("second").wait()
113+
switch doesNotExist {
114+
case .keyDoesNotExist:
115+
() // Expected
116+
case .unlimited, .limited:
117+
XCTFail("Expected '.keyDoesNotExist' but lifetime was \(doesNotExist)")
118+
}
119+
120+
try self.connection.set("second", to: "value").wait()
121+
let hasNoExpire = try self.connection.ttl("second").wait()
122+
switch hasNoExpire {
123+
case .unlimited:
124+
() // Expected
125+
case .keyDoesNotExist, .limited:
126+
XCTFail("Expected '.noExpiry' but lifetime was \(hasNoExpire)")
127+
}
128+
}
129+
68130
func test_ping() throws {
69131
let first = try connection.ping().wait()
70132
XCTAssertEqual(first, "PONG")

Tests/RediStackIntegrationTests/XCTestManifests.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ extension BasicCommandsTests {
1111
("test_exists", test_exists),
1212
("test_expire", test_expire),
1313
("test_ping", test_ping),
14+
("test_pttl", test_pttl),
1415
("test_select", test_select),
1516
("test_swapDatabase", test_swapDatabase),
17+
("test_ttl", test_ttl),
1618
]
1719
}
1820

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the RediStack open source project
4+
//
5+
// Copyright (c) 2020 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+
@testable import RediStack
16+
import XCTest
17+
18+
final class RedisKeyLifetimeTests: XCTestCase {
19+
func test_initFromSeconds() {
20+
XCTAssertEqual(RedisKeyLifetime(seconds: -2), .keyDoesNotExist)
21+
XCTAssertEqual(RedisKeyLifetime(seconds: -1), .unlimited)
22+
XCTAssertEqual(RedisKeyLifetime(seconds: 42), .limited(.seconds(42)))
23+
}
24+
25+
func test_initFromMilliseconds() {
26+
XCTAssertEqual(RedisKeyLifetime(milliseconds: -2), .keyDoesNotExist)
27+
XCTAssertEqual(RedisKeyLifetime(milliseconds: -1), .unlimited)
28+
XCTAssertEqual(RedisKeyLifetime(milliseconds: 42), .limited(.milliseconds(42)))
29+
}
30+
31+
func test_timeAmount() {
32+
XCTAssertNil(RedisKeyLifetime.keyDoesNotExist.timeAmount)
33+
XCTAssertNil(RedisKeyLifetime.unlimited.timeAmount)
34+
35+
XCTAssertEqual(RedisKeyLifetime.limited(.seconds(42)).timeAmount, .seconds(42))
36+
XCTAssertEqual(RedisKeyLifetime.limited(.milliseconds(42)).timeAmount, .milliseconds(42))
37+
}
38+
39+
func test_lifetimeCompare() {
40+
XCTAssertLessThan(RedisKeyLifetime.Lifetime.seconds(42), .seconds(43))
41+
XCTAssertLessThan(RedisKeyLifetime.Lifetime.seconds(42), .milliseconds(42001))
42+
XCTAssertLessThan(RedisKeyLifetime.Lifetime.milliseconds(41999), .milliseconds(42000))
43+
XCTAssertLessThan(RedisKeyLifetime.Lifetime.milliseconds(41999), .seconds(42))
44+
}
45+
46+
func test_lifetimeEqual() {
47+
XCTAssertEqual(RedisKeyLifetime.Lifetime.seconds(42), .seconds(42))
48+
XCTAssertEqual(RedisKeyLifetime.Lifetime.seconds(42), .milliseconds(42000))
49+
XCTAssertEqual(RedisKeyLifetime.Lifetime.milliseconds(42000), .milliseconds(42000))
50+
XCTAssertEqual(RedisKeyLifetime.Lifetime.milliseconds(42000), .seconds(42))
51+
}
52+
}

Tests/RediStackTests/XCTestManifests.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,19 @@ extension RedisByteDecoderTests {
8080
]
8181
}
8282

83+
extension RedisKeyLifetimeTests {
84+
// DO NOT MODIFY: This is autogenerated, use:
85+
// `swift test --generate-linuxmain`
86+
// to regenerate.
87+
static let __allTests__RedisKeyLifetimeTests = [
88+
("test_initFromMilliseconds", test_initFromMilliseconds),
89+
("test_initFromSeconds", test_initFromSeconds),
90+
("test_lifetimeCompare", test_lifetimeCompare),
91+
("test_lifetimeEqual", test_lifetimeEqual),
92+
("test_timeAmount", test_timeAmount),
93+
]
94+
}
95+
8396
extension RedisMessageEncoderTests {
8497
// DO NOT MODIFY: This is autogenerated, use:
8598
// `swift test --generate-linuxmain`
@@ -99,6 +112,7 @@ public func __allTests() -> [XCTestCaseEntry] {
99112
testCase(RESPTranslatorTests.__allTests__RESPTranslatorTests),
100113
testCase(RESPValueTests.__allTests__RESPValueTests),
101114
testCase(RedisByteDecoderTests.__allTests__RedisByteDecoderTests),
115+
testCase(RedisKeyLifetimeTests.__allTests__RedisKeyLifetimeTests),
102116
testCase(RedisMessageEncoderTests.__allTests__RedisMessageEncoderTests),
103117
]
104118
}

0 commit comments

Comments
 (0)