init
This commit is contained in:
418
Tests/JetStreamTests/Integration/ConsumerTests.swift
Normal file
418
Tests/JetStreamTests/Integration/ConsumerTests.swift
Normal file
@@ -0,0 +1,418 @@
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user