419 lines
15 KiB
Swift
419 lines
15 KiB
Swift
// Copyright 2024 The NATS Authors
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
import JetStream
|
|
import Logging
|
|
import Nats
|
|
import NatsServer
|
|
import XCTest
|
|
|
|
class ConsumerTests: XCTestCase {
|
|
|
|
static var allTests = [
|
|
("testFetchWithDefaultOptions", testFetchWithDefaultOptions),
|
|
("testFetchConsumerDeleted", testFetchConsumerDeleted),
|
|
("testFetchExpires", testFetchExpires),
|
|
("testFetchInvalidIdleHeartbeat", testFetchInvalidIdleHeartbeat),
|
|
("testAck", testAck),
|
|
("testNak", testNak),
|
|
("testNakWithDelay", testNakWithDelay),
|
|
("testTerm", testTerm),
|
|
]
|
|
|
|
var natsServer = NatsServer()
|
|
|
|
override func tearDown() {
|
|
super.tearDown()
|
|
natsServer.stop()
|
|
}
|
|
|
|
func testFetchWithDefaultOptions() async throws {
|
|
let bundle = Bundle.module
|
|
natsServer.start(
|
|
cfg: bundle.url(forResource: "jetstream", withExtension: "conf")!.relativePath)
|
|
logger.logLevel = .critical
|
|
|
|
let client = NatsClientOptions().url(URL(string: natsServer.clientURL)!).build()
|
|
try await client.connect()
|
|
|
|
let ctx = JetStreamContext(client: client)
|
|
|
|
let streamCfg = StreamConfig(name: "test", subjects: ["foo.*"])
|
|
let stream = try await ctx.createStream(cfg: streamCfg)
|
|
|
|
let consumer = try await stream.createConsumer(cfg: ConsumerConfig(name: "cons"))
|
|
|
|
let payload = "hello".data(using: .utf8)!
|
|
// publish some messages on stream
|
|
for _ in 1...100 {
|
|
let ack = try await ctx.publish("foo.A", message: payload)
|
|
_ = try await ack.wait()
|
|
}
|
|
let info = try await stream.info()
|
|
XCTAssertEqual(info.state.messages, 100)
|
|
|
|
let batch = try await consumer.fetch(batch: 30)
|
|
|
|
var i = 0
|
|
for try await msg in batch {
|
|
try await msg.ack()
|
|
XCTAssertEqual(msg.payload, payload)
|
|
i += 1
|
|
}
|
|
XCTAssertEqual(i, 30)
|
|
}
|
|
|
|
func testFetchConsumerDeleted() async throws {
|
|
let bundle = Bundle.module
|
|
natsServer.start(
|
|
cfg: bundle.url(forResource: "jetstream", withExtension: "conf")!.relativePath)
|
|
logger.logLevel = .critical
|
|
|
|
let client = NatsClientOptions().url(URL(string: natsServer.clientURL)!).build()
|
|
try await client.connect()
|
|
|
|
let ctx = JetStreamContext(client: client)
|
|
|
|
let streamCfg = StreamConfig(name: "test", subjects: ["foo.*"])
|
|
let stream = try await ctx.createStream(cfg: streamCfg)
|
|
|
|
let consumer = try await stream.createConsumer(cfg: ConsumerConfig(name: "cons"))
|
|
|
|
let payload = "hello".data(using: .utf8)!
|
|
// publish some messages on stream
|
|
for _ in 1...10 {
|
|
let ack = try await ctx.publish("foo.A", message: payload)
|
|
_ = try await ack.wait()
|
|
}
|
|
let info = try await stream.info()
|
|
XCTAssertEqual(info.state.messages, 10)
|
|
|
|
let batch = try await consumer.fetch(batch: 30)
|
|
|
|
sleep(1)
|
|
try await stream.deleteConsumer(name: "cons")
|
|
var i = 0
|
|
do {
|
|
for try await msg in batch {
|
|
try await msg.ack()
|
|
XCTAssertEqual(msg.payload, payload)
|
|
i += 1
|
|
}
|
|
} catch JetStreamError.FetchError.consumerDeleted {
|
|
XCTAssertEqual(i, 10)
|
|
return
|
|
}
|
|
XCTFail("should get consumer deleted")
|
|
}
|
|
|
|
func testFetchExpires() async throws {
|
|
let bundle = Bundle.module
|
|
natsServer.start(
|
|
cfg: bundle.url(forResource: "jetstream", withExtension: "conf")!.relativePath)
|
|
logger.logLevel = .critical
|
|
|
|
let client = NatsClientOptions().url(URL(string: natsServer.clientURL)!).build()
|
|
try await client.connect()
|
|
|
|
let ctx = JetStreamContext(client: client)
|
|
|
|
let streamCfg = StreamConfig(name: "test", subjects: ["foo.*"])
|
|
let stream = try await ctx.createStream(cfg: streamCfg)
|
|
|
|
let consumer = try await stream.createConsumer(cfg: ConsumerConfig(name: "cons"))
|
|
|
|
let payload = "hello".data(using: .utf8)!
|
|
// publish some messages on stream
|
|
for _ in 1...10 {
|
|
let ack = try await ctx.publish("foo.A", message: payload)
|
|
_ = try await ack.wait()
|
|
}
|
|
let info = try await stream.info()
|
|
XCTAssertEqual(info.state.messages, 10)
|
|
|
|
let batch = try await consumer.fetch(batch: 30, expires: 1)
|
|
|
|
var i = 0
|
|
for try await msg in batch {
|
|
try await msg.ack()
|
|
XCTAssertEqual(msg.payload, payload)
|
|
i += 1
|
|
}
|
|
XCTAssertEqual(i, 10)
|
|
}
|
|
|
|
func testFetchInvalidIdleHeartbeat() async throws {
|
|
let bundle = Bundle.module
|
|
natsServer.start(
|
|
cfg: bundle.url(forResource: "jetstream", withExtension: "conf")!.relativePath)
|
|
logger.logLevel = .critical
|
|
|
|
let client = NatsClientOptions().url(URL(string: natsServer.clientURL)!).build()
|
|
try await client.connect()
|
|
|
|
let ctx = JetStreamContext(client: client)
|
|
|
|
let streamCfg = StreamConfig(name: "test", subjects: ["foo.*"])
|
|
let stream = try await ctx.createStream(cfg: streamCfg)
|
|
|
|
let consumer = try await stream.createConsumer(cfg: ConsumerConfig(name: "cons"))
|
|
|
|
let batch = try await consumer.fetch(batch: 30, expires: 1, idleHeartbeat: 2)
|
|
|
|
do {
|
|
for try await _ in batch {}
|
|
} catch JetStreamError.FetchError.badRequest {
|
|
// success
|
|
return
|
|
}
|
|
XCTFail("should get bad request")
|
|
}
|
|
|
|
func testFetchMissingHeartbeat() async throws {
|
|
let bundle = Bundle.module
|
|
natsServer.start(
|
|
cfg: bundle.url(forResource: "jetstream", withExtension: "conf")!.relativePath)
|
|
logger.logLevel = .critical
|
|
|
|
let client = NatsClientOptions().url(URL(string: natsServer.clientURL)!).build()
|
|
try await client.connect()
|
|
|
|
let ctx = JetStreamContext(client: client)
|
|
|
|
let streamCfg = StreamConfig(name: "test", subjects: ["foo.*"])
|
|
let stream = try await ctx.createStream(cfg: streamCfg)
|
|
|
|
let consumer = try await stream.createConsumer(cfg: ConsumerConfig(name: "cons"))
|
|
|
|
let payload = "hello".data(using: .utf8)!
|
|
// publish some messages on stream
|
|
for _ in 1...10 {
|
|
let ack = try await ctx.publish("foo.A", message: payload)
|
|
_ = try await ack.wait()
|
|
}
|
|
let info = try await stream.info()
|
|
XCTAssertEqual(info.state.messages, 10)
|
|
|
|
try await stream.deleteConsumer(name: "cons")
|
|
|
|
let batch = try await consumer.fetch(batch: 30, idleHeartbeat: 1)
|
|
|
|
do {
|
|
for try await _ in batch {}
|
|
} catch JetStreamError.FetchError.noHeartbeatReceived {
|
|
return
|
|
} catch JetStreamError.FetchError.noResponders {
|
|
// This is also expected when the consumer has been deleted
|
|
return
|
|
}
|
|
XCTFail("should get missing heartbeats or no responders error")
|
|
}
|
|
|
|
func testAck() async throws {
|
|
let bundle = Bundle.module
|
|
natsServer.start(
|
|
cfg: bundle.url(forResource: "jetstream", withExtension: "conf")!.relativePath)
|
|
logger.logLevel = .critical
|
|
|
|
let client = NatsClientOptions().url(URL(string: natsServer.clientURL)!).build()
|
|
try await client.connect()
|
|
|
|
let ctx = JetStreamContext(client: client)
|
|
|
|
let streamCfg = StreamConfig(name: "test", subjects: ["foo.*"])
|
|
let stream = try await ctx.createStream(cfg: streamCfg)
|
|
|
|
// create a consumer with 500ms ack wait
|
|
let consumer = try await stream.createConsumer(
|
|
cfg: ConsumerConfig(name: "cons", ackWait: NanoTimeInterval(0.5)))
|
|
|
|
// publish some messages on stream
|
|
for i in 0..<100 {
|
|
let ack = try await ctx.publish("foo.A", message: "\(i)".data(using: .utf8)!)
|
|
_ = try await ack.wait()
|
|
}
|
|
let info = try await stream.info()
|
|
XCTAssertEqual(info.state.messages, 100)
|
|
|
|
var batch = try await consumer.fetch(batch: 10)
|
|
|
|
var i = 0
|
|
for try await msg in batch {
|
|
try await msg.ack()
|
|
XCTAssertEqual(String(decoding: msg.payload!, as: UTF8.self), "\(i)")
|
|
i += 1
|
|
}
|
|
XCTAssertEqual(i, 10)
|
|
|
|
// now wait 1 second and make sure the messages are not re-delivered
|
|
sleep(1)
|
|
|
|
batch = try await consumer.fetch(batch: 10)
|
|
|
|
for try await msg in batch {
|
|
try await msg.ack()
|
|
XCTAssertEqual(String(decoding: msg.payload!, as: UTF8.self), "\(i)")
|
|
i += 1
|
|
}
|
|
XCTAssertEqual(i, 20)
|
|
}
|
|
|
|
func testNak() async throws {
|
|
let bundle = Bundle.module
|
|
natsServer.start(
|
|
cfg: bundle.url(forResource: "jetstream", withExtension: "conf")!.relativePath)
|
|
logger.logLevel = .critical
|
|
|
|
let client = NatsClientOptions().url(URL(string: natsServer.clientURL)!).build()
|
|
try await client.connect()
|
|
|
|
let ctx = JetStreamContext(client: client)
|
|
|
|
let streamCfg = StreamConfig(name: "test", subjects: ["foo.*"])
|
|
let stream = try await ctx.createStream(cfg: streamCfg)
|
|
|
|
// create a consumer with 500ms ack wait
|
|
let consumer = try await stream.createConsumer(
|
|
cfg: ConsumerConfig(name: "cons", ackWait: NanoTimeInterval(0.5)))
|
|
|
|
// publish some messages on stream
|
|
for i in 0..<10 {
|
|
let ack = try await ctx.publish("foo.A", message: "\(i)".data(using: .utf8)!)
|
|
_ = try await ack.wait()
|
|
}
|
|
let info = try await stream.info()
|
|
XCTAssertEqual(info.state.messages, 10)
|
|
|
|
var batch = try await consumer.fetch(batch: 1)
|
|
var iter = batch.makeAsyncIterator()
|
|
var msg = try await iter.next()!
|
|
XCTAssertEqual(String(decoding: msg.payload!, as: UTF8.self), "\(0)")
|
|
var meta = try msg.metadata()
|
|
XCTAssertEqual(meta.streamSequence, 1)
|
|
XCTAssertEqual(meta.consumerSequence, 1)
|
|
try await msg.ack(ackType: .nak())
|
|
|
|
// now fetch the message again, it should be redelivered
|
|
batch = try await consumer.fetch(batch: 1)
|
|
iter = batch.makeAsyncIterator()
|
|
msg = try await iter.next()!
|
|
XCTAssertEqual(String(decoding: msg.payload!, as: UTF8.self), "\(0)")
|
|
meta = try msg.metadata()
|
|
XCTAssertEqual(meta.streamSequence, 1)
|
|
XCTAssertEqual(meta.consumerSequence, 2)
|
|
try await msg.ack()
|
|
}
|
|
|
|
func testNakWithDelay() async throws {
|
|
let bundle = Bundle.module
|
|
natsServer.start(
|
|
cfg: bundle.url(forResource: "jetstream", withExtension: "conf")!.relativePath)
|
|
logger.logLevel = .critical
|
|
|
|
let client = NatsClientOptions().url(URL(string: natsServer.clientURL)!).build()
|
|
try await client.connect()
|
|
|
|
let ctx = JetStreamContext(client: client)
|
|
|
|
let streamCfg = StreamConfig(name: "test", subjects: ["foo.*"])
|
|
let stream = try await ctx.createStream(cfg: streamCfg)
|
|
|
|
// create a consumer with 500ms ack wait
|
|
let consumer = try await stream.createConsumer(cfg: ConsumerConfig(name: "cons"))
|
|
|
|
// publish some messages on stream
|
|
for i in 0..<10 {
|
|
let ack = try await ctx.publish("foo.A", message: "\(i)".data(using: .utf8)!)
|
|
_ = try await ack.wait()
|
|
}
|
|
let info = try await stream.info()
|
|
XCTAssertEqual(info.state.messages, 10)
|
|
|
|
var batch = try await consumer.fetch(batch: 1)
|
|
var iter = batch.makeAsyncIterator()
|
|
var msg = try await iter.next()!
|
|
XCTAssertEqual(String(decoding: msg.payload!, as: UTF8.self), "\(0)")
|
|
var meta = try msg.metadata()
|
|
XCTAssertEqual(meta.streamSequence, 1)
|
|
XCTAssertEqual(meta.consumerSequence, 1)
|
|
try await msg.ack(ackType: .nak(delay: 0.5))
|
|
|
|
// now fetch the next message immediately, it should be the next message
|
|
batch = try await consumer.fetch(batch: 1)
|
|
iter = batch.makeAsyncIterator()
|
|
msg = try await iter.next()!
|
|
XCTAssertEqual(String(decoding: msg.payload!, as: UTF8.self), "\(1)")
|
|
meta = try msg.metadata()
|
|
XCTAssertEqual(meta.streamSequence, 2)
|
|
XCTAssertEqual(meta.consumerSequence, 2)
|
|
try await msg.ack()
|
|
|
|
// wait a second, the first message should be redelivered at this point
|
|
sleep(1)
|
|
batch = try await consumer.fetch(batch: 1)
|
|
iter = batch.makeAsyncIterator()
|
|
msg = try await iter.next()!
|
|
XCTAssertEqual(String(decoding: msg.payload!, as: UTF8.self), "\(0)")
|
|
meta = try msg.metadata()
|
|
XCTAssertEqual(meta.streamSequence, 1)
|
|
XCTAssertEqual(meta.consumerSequence, 3)
|
|
}
|
|
|
|
func testTerm() async throws {
|
|
let bundle = Bundle.module
|
|
natsServer.start(
|
|
cfg: bundle.url(forResource: "jetstream", withExtension: "conf")!.relativePath)
|
|
logger.logLevel = .critical
|
|
|
|
let client = NatsClientOptions().url(URL(string: natsServer.clientURL)!).build()
|
|
try await client.connect()
|
|
|
|
let ctx = JetStreamContext(client: client)
|
|
|
|
let streamCfg = StreamConfig(name: "test", subjects: ["foo.*"])
|
|
let stream = try await ctx.createStream(cfg: streamCfg)
|
|
|
|
// create a consumer with 500ms ack wait
|
|
let consumer = try await stream.createConsumer(
|
|
cfg: ConsumerConfig(name: "cons", ackWait: NanoTimeInterval(0.5)))
|
|
|
|
// publish some messages on stream
|
|
for i in 0..<10 {
|
|
let ack = try await ctx.publish("foo.A", message: "\(i)".data(using: .utf8)!)
|
|
_ = try await ack.wait()
|
|
}
|
|
let info = try await stream.info()
|
|
XCTAssertEqual(info.state.messages, 10)
|
|
|
|
var batch = try await consumer.fetch(batch: 1)
|
|
var iter = batch.makeAsyncIterator()
|
|
var msg = try await iter.next()!
|
|
XCTAssertEqual(String(decoding: msg.payload!, as: UTF8.self), "\(0)")
|
|
var meta = try msg.metadata()
|
|
XCTAssertEqual(meta.streamSequence, 1)
|
|
XCTAssertEqual(meta.consumerSequence, 1)
|
|
try await msg.ack(ackType: .term())
|
|
|
|
// wait 1s, the first message should not be redelivered (even though we are past ack wait)
|
|
sleep(1)
|
|
batch = try await consumer.fetch(batch: 1)
|
|
iter = batch.makeAsyncIterator()
|
|
msg = try await iter.next()!
|
|
XCTAssertEqual(String(decoding: msg.payload!, as: UTF8.self), "\(1)")
|
|
meta = try msg.metadata()
|
|
XCTAssertEqual(meta.streamSequence, 2)
|
|
XCTAssertEqual(meta.consumerSequence, 2)
|
|
try await msg.ack()
|
|
}
|
|
}
|