Files

372 lines
13 KiB
Swift

//===----------------------------------------------------------------------===//
//
// This source file is part of the RediStack open source project
//
// Copyright (c) 2020-2022 Apple Inc. and the RediStack project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of RediStack project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import NIOCore
import RediStack
import RediStackTestUtils
import XCTest
final class RedisPubSubCommandsTests: RediStackIntegrationTestCase {
func test_singleChannel() throws {
let subscribeExpectation = self.expectation(description: "subscriber receives initial subscription message")
let messageExpectation = self.expectation(description: "subscriber receives published message")
let unsubscribeExpectation = self.expectation(description: "subscriber receives unsubscribe message")
let subscriber = try self.makeNewConnection()
defer { try? subscriber.close().wait() }
let message = "Hello from Redis!"
try subscriber.subscribe(
to: #function,
messageReceiver: {
guard
$0 == #function,
$1.string == message
else { return }
messageExpectation.fulfill()
},
onSubscribe: {
guard $0 == #function, $1 == 1 else { return }
subscribeExpectation.fulfill()
},
onUnsubscribe: {
guard $0 == #function, $1 == 0 else { return }
unsubscribeExpectation.fulfill()
}
).wait()
let subscribersCount = try self.connection.publish(message, to: #function).wait()
XCTAssertEqual(subscribersCount, 1)
try subscriber.unsubscribe(from: #function).wait()
self.waitForExpectations(timeout: 1)
}
func test_multiChannel() throws {
let channelMessageExpectation = self.expectation(description: "subscriber receives channel message")
let patternMessageExpectation = self.expectation(description: "subscriber receives pattern message")
let subscriber = try self.makeNewConnection()
defer { try? subscriber.close().wait() }
let channel = RedisChannelName(#function)
let pattern = "\(channel.rawValue.dropLast(channel.rawValue.count / 2))*"
try subscriber
.subscribe(to: channel) { (_, _) in channelMessageExpectation.fulfill() }
.wait()
try subscriber
.psubscribe(to: pattern) { (_, _) in patternMessageExpectation.fulfill() }
.wait()
let subscriberCount = try self.connection.publish("hello!", to: channel).wait()
XCTAssertEqual(subscriberCount, 2)
self.waitForExpectations(timeout: 1)
}
func test_unsubscribeWithoutSubscriptions() throws {
XCTAssertNoThrow(try self.connection.unsubscribe(from: #function).wait())
}
func test_blockedCommandsThrowInPubSubMode() throws {
try self.connection.subscribe(to: #function) { (_, _) in }.wait()
defer { try? self.connection.unsubscribe(from: #function).wait() }
XCTAssertThrowsError(try self.connection.lpush("value", into: "List").wait()) {
XCTAssertTrue($0 is RedisError)
}
}
func test_pingInPubSub() throws {
try self.connection.subscribe(to: #function) { (_, _) in }.wait()
defer { try? self.connection.unsubscribe(from: #function).wait() }
let pong = try self.connection.ping().wait()
XCTAssertEqual(pong, "PONG")
let message = try self.connection.ping(with: "Hello").wait()
XCTAssertEqual(message, "Hello")
}
func test_quitInPubSub() throws {
try self.connection.subscribe(to: #function) { (_, _) in }.wait()
defer { try? self.connection.unsubscribe(from: #function).wait() }
let value = try self.connection.send(command: "QUIT").wait()
XCTAssertEqual(value.string, "OK")
}
func test_unsubscribeFromAllChannels() throws {
let subscriber = try self.makeNewConnection()
defer { try? subscriber.close().wait() }
let channels = (1...5).map { RedisChannelName("\(#function)\($0)") }
let expectation = self.expectation(description: "all channel subscriptions should be cancelled")
expectation.expectedFulfillmentCount = channels.count
try subscriber.subscribe(
to: channels,
messageReceiver: { _, _ in },
onSubscribe: nil,
onUnsubscribe: { _, _ in expectation.fulfill() }
).wait()
XCTAssertTrue(subscriber.isSubscribed)
try subscriber.unsubscribe().wait()
XCTAssertFalse(subscriber.isSubscribed)
self.waitForExpectations(timeout: 1)
}
func test_unsubscribeFromAllPatterns() throws {
let subscriber = try self.makeNewConnection()
defer { try? subscriber.close().wait() }
let patterns = (1...3).map { ("*\(#function)\($0)") }
let expectation = self.expectation(description: "all pattern subscriptions should be cancelled")
expectation.expectedFulfillmentCount = patterns.count
try subscriber.psubscribe(
to: patterns,
messageReceiver: { _, _ in },
onSubscribe: nil,
onUnsubscribe: { _, _ in expectation.fulfill() }
).wait()
XCTAssertTrue(subscriber.isSubscribed)
try subscriber.punsubscribe().wait()
XCTAssertFalse(subscriber.isSubscribed)
self.waitForExpectations(timeout: 1)
}
func test_unsubscribeFromAllMixed() throws {
let subscriber = try self.makeNewConnection()
defer { try? subscriber.close().wait() }
let expectation = self.expectation(description: "both unsubscribes should be completed")
expectation.expectedFulfillmentCount = 2
XCTAssertFalse(subscriber.isSubscribed)
try subscriber.subscribe(
to: #function,
messageReceiver: { _, _ in },
onSubscribe: nil,
onUnsubscribe: { _, _ in expectation.fulfill() }
).wait()
XCTAssertTrue(subscriber.isSubscribed)
try subscriber.psubscribe(
to: "*\(#function)",
messageReceiver: { _, _ in },
onSubscribe: nil,
onUnsubscribe: { _, _ in expectation.fulfill() }
).wait()
XCTAssertTrue(subscriber.isSubscribed)
try subscriber.unsubscribe().wait()
XCTAssertTrue(subscriber.isSubscribed)
try subscriber.punsubscribe().wait()
XCTAssertFalse(subscriber.isSubscribed)
self.waitForExpectations(timeout: 1)
}
func test_pubSubNumpat() throws {
let queryConnection = try self.makeNewConnection()
defer { try? queryConnection.close().wait() }
let numPat = try queryConnection.patternSubscriberCount().wait()
XCTAssertGreaterThanOrEqual(numPat, 0)
}
func test_pubSubChannels() throws {
let fn = #function
let subscriber = try self.makeNewConnection()
defer { try? subscriber.close().wait() }
let channelNames = (1...10).map {
RedisChannelName("\(fn)\($0)\($0 % 2 == 0 ? "_even" : "_odd")")
}
for channelName in channelNames {
try subscriber.subscribe(
to: channelName,
messageReceiver: { _, _ in },
onSubscribe: nil,
onUnsubscribe: nil
).wait()
}
XCTAssertTrue(subscriber.isSubscribed)
defer {
// Unsubscribe (clean up)
try? subscriber.unsubscribe(from: channelNames).wait()
XCTAssertFalse(subscriber.isSubscribed)
}
// Make another connection to query on.
let queryConnection = try self.makeNewConnection()
defer { try? queryConnection.close().wait() }
let oddChannels = try queryConnection.activeChannels(matching: "\(fn)*_odd").wait()
XCTAssertEqual(oddChannels.count, channelNames.count / 2)
let allChannels = try queryConnection.activeChannels().wait()
XCTAssertGreaterThanOrEqual(allChannels.count, channelNames.count)
}
func test_pubSubNumsub() throws {
let fn = #function
let subscriber = try self.makeNewConnection()
defer { try? subscriber.close().wait() }
let channelNames = (1...5).map {
RedisChannelName("\(fn)\($0)")
}
for channelName in channelNames {
try subscriber.subscribe(
to: channelName,
messageReceiver: { _, _ in },
onSubscribe: nil,
onUnsubscribe: nil
).wait()
}
XCTAssertTrue(subscriber.isSubscribed)
defer {
// Unsubscribe (clean up)
try? subscriber.unsubscribe(from: channelNames).wait()
XCTAssertFalse(subscriber.isSubscribed)
}
// Make another connection to query on.
let queryConnection = try self.makeNewConnection()
defer { try? queryConnection.close().wait() }
let notSubscribedChannel = RedisChannelName("\(fn)_notsubbed")
let numSubs = try queryConnection.subscriberCount(forChannels: [channelNames[0], notSubscribedChannel]).wait()
XCTAssertEqual(numSubs.count, 2)
XCTAssertGreaterThanOrEqual(numSubs[channelNames[0]] ?? 0, 1)
XCTAssertEqual(numSubs[notSubscribedChannel], 0)
}
}
final class RedisPubSubCommandsPoolTests: RediStackConnectionPoolIntegrationTestCase {
func test_pool_singleChannel() throws {
let subscribeExpectation = self.expectation(description: "subscriber receives initial subscription message")
let messageExpectation = self.expectation(description: "subscriber receives published message")
let unsubscribeExpectation = self.expectation(description: "subscriber receives unsubscribe message")
let subscriber = try self.makeNewPool()
defer { subscriber.close() }
let message = "Hello from Redis!"
try subscriber.subscribe(
to: #function,
messageReceiver: {
guard
$0 == #function,
$1.string == message
else { return }
messageExpectation.fulfill()
},
onSubscribe: {
guard $0 == #function, $1 == 1 else { return }
subscribeExpectation.fulfill()
},
onUnsubscribe: {
guard $0 == #function, $1 == 0 else { return }
unsubscribeExpectation.fulfill()
}
).wait()
XCTAssertEqual(subscriber.leasedConnectionCount, 1)
let subscribersCount = try self.pool.publish(message, to: #function).wait()
XCTAssertEqual(subscribersCount, 1)
try subscriber.unsubscribe(from: #function).wait()
XCTAssertEqual(subscriber.leasedConnectionCount, 0)
self.waitForExpectations(timeout: 1)
}
func test_pool_multiChannel() throws {
let channelMessageExpectation = self.expectation(description: "subscriber receives channel message")
let patternMessageExpectation = self.expectation(description: "subscriber receives pattern message")
let subscriber = try self.makeNewPool()
defer { subscriber.close() }
let channel = RedisChannelName(#function)
let pattern = "\(channel.rawValue.dropLast(channel.rawValue.count / 2))*"
try subscriber
.subscribe(to: channel) { (_, _) in channelMessageExpectation.fulfill() }
.wait()
XCTAssertEqual(subscriber.leasedConnectionCount, 1)
try subscriber
.psubscribe(to: pattern) { (_, _) in patternMessageExpectation.fulfill() }
.wait()
XCTAssertEqual(subscriber.leasedConnectionCount, 1)
let subscriberCount = try self.pool.publish("hello!", to: channel).wait()
XCTAssertEqual(subscriberCount, 2)
self.waitForExpectations(timeout: 1)
}
func test_unsubscribeWithoutSubscriptions() throws {
XCTAssertEqual(self.pool.leasedConnectionCount, 0)
XCTAssertNoThrow(try self.pool.unsubscribe(from: #function).wait())
XCTAssertEqual(self.pool.leasedConnectionCount, 0)
}
}
// MARK: - #100 subscribe race condition
extension RedisPubSubCommandsTests {
func test_pubsub_pipelineChanges_hasNoRaceCondition() throws {
func runOperation(_ factory: (RedisChannelName) -> EventLoopFuture<Void>) -> EventLoopFuture<Void> {
.andAllSucceed(
(0...100_000).reduce(into: []) {
result,
index in
result.append(factory("\(#function)-\(index)"))
},
on: self.connection.eventLoop
)
}
// subscribing (adding handler)
try runOperation { self.connection.subscribe(to: $0) { _, _ in } }
.wait()
// unsubscribing (removing handler)
try runOperation { self.connection.unsubscribe(from: $0) }
.wait()
try self.connection.close().wait()
}
}