Skip to content

Commit eaa9d2b

Browse files
committed
Add Blocking List Pop Commands
Motivation: To be a comprehensive library, all commands should be implemented, even if they are highly discouraged. List's collection of commands were missing `brpop`, `blpop`, and `brpoplpush`. Modifications: `brpop`, `blpop` and `brpoplpush` are supported with defaults and overloads for an easier API. Result: Users now have access to `brpop`, `blpop` and `brpoplpush` commands.
1 parent 0c4db21 commit eaa9d2b

File tree

2 files changed

+201
-0
lines changed

2 files changed

+201
-0
lines changed

Sources/NIORedis/Commands/ListCommands.swift

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,33 @@ extension RedisClient {
117117
public func rpoplpush(from source: String, to dest: String) -> EventLoopFuture<RESPValue> {
118118
return send(command: "RPOPLPUSH", with: [source, dest])
119119
}
120+
121+
/// Pops the last element from a source list and pushes it to a destination list, blocking until
122+
/// an element is available from the source list.
123+
///
124+
/// - Important:
125+
/// This will block the connection from completing further commands until an element
126+
/// is available to pop from the source list.
127+
///
128+
/// It is **highly** recommended to set a reasonable `timeout`
129+
/// or to use the non-blocking `rpoplpush` method where possible.
130+
///
131+
/// See [https://redis.io/commands/brpoplpush](https://redis.io/commands/brpoplpush)
132+
/// - Parameters:
133+
/// - source: The key of the list to pop from.
134+
/// - dest: The key of the list to push to.
135+
/// - timeout: The time (in seconds) to wait. `0` means indefinitely.
136+
/// - Returns: The element popped from the source list and pushed to the destination,
137+
/// or `nil` if the timeout was reached.
138+
@inlinable
139+
public func brpoplpush(
140+
from source: String,
141+
to dest: String,
142+
timeout: Int = 0
143+
) -> EventLoopFuture<RESPValue?> {
144+
return send(command: "BRPOPLPUSH", with: [source, dest, timeout])
145+
.map { $0.isNull ? nil: $0 }
146+
}
120147
}
121148

122149
// MARK: Insert
@@ -250,3 +277,116 @@ extension RedisClient {
250277
.mapFromRESP()
251278
}
252279
}
280+
281+
// MARK: Blocking Pop
282+
283+
extension RedisClient {
284+
/// Removes the first element of a list, blocking until an element is available.
285+
///
286+
/// - Important:
287+
/// This will block the connection from completing further commands until an element
288+
/// is available to pop from the list.
289+
///
290+
/// It is **highly** recommended to set a reasonable `timeout`
291+
/// or to use the non-blocking `lpop` method where possible.
292+
///
293+
/// See [https://redis.io/commands/blpop](https://redis.io/commands/blpop)
294+
/// - Parameters:
295+
/// - key: The key of the list to pop from.
296+
/// - Returns: The element that was popped from the list, or `nil` if the timout was reached.
297+
@inlinable
298+
public func blpop(from key: String, timeout: Int = 0) -> EventLoopFuture<RESPValue?> {
299+
return blpop(from: [key], timeout: timeout)
300+
.map { $0?.1 }
301+
}
302+
303+
/// Removes the first element of a list, blocking until an element is available.
304+
///
305+
/// - Important:
306+
/// This will block the connection from completing further commands until an element
307+
/// is available to pop from the group of lists.
308+
///
309+
/// It is **highly** recommended to set a reasonable `timeout`
310+
/// or to use the non-blocking `lpop` method where possible.
311+
///
312+
/// See [https://redis.io/commands/blpop](https://redis.io/commands/blpop)
313+
/// - Parameters:
314+
/// - keys: The keys of lists in Redis that should be popped from.
315+
/// - timeout: The time (in seconds) to wait. `0` means indefinitely.
316+
/// - Returns:
317+
/// If timeout was reached, `nil`.
318+
///
319+
/// Otherwise, the key of the list the element was removed from and the popped element.
320+
@inlinable
321+
public func blpop(
322+
from keys: [String],
323+
timeout: Int = 0
324+
) -> EventLoopFuture<(String, RESPValue)?> {
325+
return _bpop(command: "BLPOP", keys, timeout)
326+
}
327+
328+
/// Removes the last element of a list, blocking until an element is available.
329+
///
330+
/// - Important:
331+
/// This will block the connection from completing further commands until an element
332+
/// is available to pop from the list.
333+
///
334+
/// It is **highly** recommended to set a reasonable `timeout`
335+
/// or to use the non-blocking `rpop` method where possible.
336+
///
337+
/// See [https://redis.io/commands/brpop](https://redis.io/commands/brpop)
338+
/// - Parameters:
339+
/// - key: The key of the list to pop from.
340+
/// - Returns: The element that was popped from the list, or `nil` if the timout was reached.
341+
@inlinable
342+
public func brpop(from key: String, timeout: Int = 0) -> EventLoopFuture<RESPValue?> {
343+
return brpop(from: [key], timeout: timeout)
344+
.map { $0?.1 }
345+
}
346+
347+
/// Removes the last element of a list, blocking until an element is available.
348+
///
349+
/// - Important:
350+
/// This will block the connection from completing further commands until an element
351+
/// is available to pop from the group of lists.
352+
///
353+
/// It is **highly** recommended to set a reasonable `timeout`
354+
/// or to use the non-blocking `rpop` method where possible.
355+
///
356+
/// See [https://redis.io/commands/brpop](https://redis.io/commands/brpop)
357+
/// - Parameters:
358+
/// - keys: The keys of lists in Redis that should be popped from.
359+
/// - timeout: The time (in seconds) to wait. `0` means indefinitely.
360+
/// - Returns:
361+
/// If timeout was reached, `nil`.
362+
///
363+
/// Otherwise, the key of the list the element was removed from and the popped element.
364+
@inlinable
365+
public func brpop(
366+
from keys: [String],
367+
timeout: Int = 0
368+
) -> EventLoopFuture<(String, RESPValue)?> {
369+
return _bpop(command: "BRPOP", keys, timeout)
370+
}
371+
372+
@usableFromInline
373+
func _bpop(
374+
command: String,
375+
_ keys: [String],
376+
_ timeout: Int
377+
) -> EventLoopFuture<(String, RESPValue)?> {
378+
let args = keys as [RESPValueConvertible] + [timeout]
379+
return send(command: command, with: args)
380+
.flatMapThrowing {
381+
guard !$0.isNull else { return nil }
382+
guard let response = [RESPValue]($0) else {
383+
throw NIORedisError.responseConversion(to: [RESPValue].self)
384+
}
385+
assert(response.count == 2, "Unexpected response size returned!")
386+
guard let key = response[0].string else {
387+
throw NIORedisError.assertionFailure(message: "Unexpected structure in response: \(response)")
388+
}
389+
return (key, response[1])
390+
}
391+
}
392+
}

Tests/NIORedisTests/Commands/ListCommandsTests.swift

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,22 @@ final class ListCommandsTests: XCTestCase {
102102
XCTAssertEqual(try connection.llen(of: "second").wait(), 1)
103103
}
104104

105+
func test_brpoplpush() throws {
106+
_ = try connection.lpush([10], into: "first").wait()
107+
108+
let element = try connection.brpoplpush(from: "first", to: "second").wait() ?? .null
109+
XCTAssertEqual(Int(element), 10)
110+
111+
let blockingConnection = try Redis.makeConnection().wait()
112+
let expectation = XCTestExpectation(description: "brpoplpush should never return")
113+
_ = blockingConnection.bzpopmin(from: #function)
114+
.always { _ in expectation.fulfill() }
115+
116+
let result = XCTWaiter.wait(for: [expectation], timeout: 1)
117+
XCTAssertEqual(result, .timedOut)
118+
try blockingConnection.channel.close().wait()
119+
}
120+
105121
func test_linsert() throws {
106122
_ = try connection.lpush([10], into: #function).wait()
107123

@@ -129,6 +145,27 @@ final class ListCommandsTests: XCTestCase {
129145
XCTAssertEqual(Int(element), 30)
130146
}
131147

148+
func test_blpop() throws {
149+
let nilPop = try connection.blpop(from: #function, timeout: 1).wait()
150+
XCTAssertNil(nilPop)
151+
152+
_ = try connection.lpush([10, 20, 30], into: "first").wait()
153+
let pop1 = try connection.blpop(from: "first").wait() ?? .null
154+
XCTAssertEqual(Int(pop1), 30)
155+
156+
let pop2 = try connection.blpop(from: ["fake", "first"]).wait()
157+
XCTAssertEqual(pop2?.0, "first")
158+
159+
let blockingConnection = try Redis.makeConnection().wait()
160+
let expectation = XCTestExpectation(description: "blpop should never return")
161+
_ = blockingConnection.bzpopmin(from: #function)
162+
.always { _ in expectation.fulfill() }
163+
164+
let result = XCTWaiter.wait(for: [expectation], timeout: 1)
165+
XCTAssertEqual(result, .timedOut)
166+
try blockingConnection.channel.close().wait()
167+
}
168+
132169
func test_lpush() throws {
133170
_ = try connection.rpush([10, 20, 30], into: #function).wait()
134171

@@ -165,6 +202,27 @@ final class ListCommandsTests: XCTestCase {
165202
XCTAssertTrue(result.isNull)
166203
}
167204

205+
func test_brpop() throws {
206+
let nilPop = try connection.brpop(from: #function, timeout: 1).wait()
207+
XCTAssertNil(nilPop)
208+
209+
_ = try connection.lpush([10, 20, 30], into: "first").wait()
210+
let pop1 = try connection.brpop(from: "first").wait() ?? .null
211+
XCTAssertEqual(Int(pop1), 10)
212+
213+
let pop2 = try connection.brpop(from: ["fake", "first"]).wait()
214+
XCTAssertEqual(pop2?.0, "first")
215+
216+
let blockingConnection = try Redis.makeConnection().wait()
217+
let expectation = XCTestExpectation(description: "brpop should never return")
218+
_ = blockingConnection.bzpopmin(from: #function)
219+
.always { _ in expectation.fulfill() }
220+
221+
let result = XCTWaiter.wait(for: [expectation], timeout: 1)
222+
XCTAssertEqual(result, .timedOut)
223+
try blockingConnection.channel.close().wait()
224+
}
225+
168226
func test_rpush() throws {
169227
_ = try connection.lpush([10, 20, 30], into: #function).wait()
170228

@@ -195,11 +253,14 @@ final class ListCommandsTests: XCTestCase {
195253
("test_lrem", test_lrem),
196254
("test_lrange", test_lrange),
197255
("test_rpoplpush", test_rpoplpush),
256+
("test_brpoplpush", test_brpoplpush),
198257
("test_linsert", test_linsert),
199258
("test_lpop", test_lpop),
259+
("test_blpop", test_blpop),
200260
("test_lpush", test_lpush),
201261
("test_lpushx", test_lpushx),
202262
("test_rpop", test_rpop),
263+
("test_brpop", test_brpop),
203264
("test_rpush", test_rpush),
204265
("test_rpushx", test_rpushx),
205266
]

0 commit comments

Comments
 (0)