snapshot of https://github.com/seaweedfs/seaweedfs.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled
Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled
This commit is contained in:
549
test/kafka/integration/client_compatibility_test.go
Normal file
549
test/kafka/integration/client_compatibility_test.go
Normal file
@@ -0,0 +1,549 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/IBM/sarama"
|
||||
"github.com/segmentio/kafka-go"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/test/kafka/internal/testutil"
|
||||
)
|
||||
|
||||
// TestClientCompatibility tests compatibility with different Kafka client libraries and versions
|
||||
// This test will use SMQ backend if SEAWEEDFS_MASTERS is available, otherwise mock
|
||||
func TestClientCompatibility(t *testing.T) {
|
||||
gateway := testutil.NewGatewayTestServerWithSMQ(t, testutil.SMQAvailable)
|
||||
defer gateway.CleanupAndClose()
|
||||
|
||||
addr := gateway.StartAndWait()
|
||||
time.Sleep(200 * time.Millisecond) // Allow gateway to be ready
|
||||
|
||||
// Log which backend we're using
|
||||
if gateway.IsSMQMode() {
|
||||
t.Logf("Running client compatibility tests with SMQ backend")
|
||||
} else {
|
||||
t.Logf("Running client compatibility tests with mock backend")
|
||||
}
|
||||
|
||||
t.Run("SaramaVersionCompatibility", func(t *testing.T) {
|
||||
testSaramaVersionCompatibility(t, addr)
|
||||
})
|
||||
|
||||
t.Run("KafkaGoVersionCompatibility", func(t *testing.T) {
|
||||
testKafkaGoVersionCompatibility(t, addr)
|
||||
})
|
||||
|
||||
t.Run("APIVersionNegotiation", func(t *testing.T) {
|
||||
testAPIVersionNegotiation(t, addr)
|
||||
})
|
||||
|
||||
t.Run("ProducerConsumerCompatibility", func(t *testing.T) {
|
||||
testProducerConsumerCompatibility(t, addr)
|
||||
})
|
||||
|
||||
t.Run("ConsumerGroupCompatibility", func(t *testing.T) {
|
||||
testConsumerGroupCompatibility(t, addr)
|
||||
})
|
||||
|
||||
t.Run("AdminClientCompatibility", func(t *testing.T) {
|
||||
testAdminClientCompatibility(t, addr)
|
||||
})
|
||||
}
|
||||
|
||||
func testSaramaVersionCompatibility(t *testing.T, addr string) {
|
||||
versions := []sarama.KafkaVersion{
|
||||
sarama.V2_6_0_0,
|
||||
sarama.V2_8_0_0,
|
||||
sarama.V3_0_0_0,
|
||||
sarama.V3_4_0_0,
|
||||
}
|
||||
|
||||
for _, version := range versions {
|
||||
t.Run(fmt.Sprintf("Sarama_%s", version.String()), func(t *testing.T) {
|
||||
config := sarama.NewConfig()
|
||||
config.Version = version
|
||||
config.Producer.Return.Successes = true
|
||||
config.Consumer.Return.Errors = true
|
||||
|
||||
client, err := sarama.NewClient([]string{addr}, config)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create Sarama client for version %s: %v", version, err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
// Test basic operations
|
||||
topicName := testutil.GenerateUniqueTopicName(fmt.Sprintf("sarama-%s", version.String()))
|
||||
|
||||
// Test topic creation via admin client
|
||||
admin, err := sarama.NewClusterAdminFromClient(client)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create admin client: %v", err)
|
||||
}
|
||||
defer admin.Close()
|
||||
|
||||
topicDetail := &sarama.TopicDetail{
|
||||
NumPartitions: 1,
|
||||
ReplicationFactor: 1,
|
||||
}
|
||||
|
||||
err = admin.CreateTopic(topicName, topicDetail, false)
|
||||
if err != nil {
|
||||
t.Logf("Topic creation failed (may already exist): %v", err)
|
||||
}
|
||||
|
||||
// Test produce
|
||||
producer, err := sarama.NewSyncProducerFromClient(client)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create producer: %v", err)
|
||||
}
|
||||
defer producer.Close()
|
||||
|
||||
message := &sarama.ProducerMessage{
|
||||
Topic: topicName,
|
||||
Value: sarama.StringEncoder(fmt.Sprintf("test-message-%s", version.String())),
|
||||
}
|
||||
|
||||
partition, offset, err := producer.SendMessage(message)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to send message: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("Sarama %s: Message sent to partition %d at offset %d", version, partition, offset)
|
||||
|
||||
// Test consume
|
||||
consumer, err := sarama.NewConsumerFromClient(client)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create consumer: %v", err)
|
||||
}
|
||||
defer consumer.Close()
|
||||
|
||||
partitionConsumer, err := consumer.ConsumePartition(topicName, 0, sarama.OffsetOldest)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create partition consumer: %v", err)
|
||||
}
|
||||
defer partitionConsumer.Close()
|
||||
|
||||
select {
|
||||
case msg := <-partitionConsumer.Messages():
|
||||
if string(msg.Value) != fmt.Sprintf("test-message-%s", version.String()) {
|
||||
t.Errorf("Message content mismatch: expected %s, got %s",
|
||||
fmt.Sprintf("test-message-%s", version.String()), string(msg.Value))
|
||||
}
|
||||
t.Logf("Sarama %s: Successfully consumed message", version)
|
||||
case err := <-partitionConsumer.Errors():
|
||||
t.Fatalf("Consumer error: %v", err)
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("Timeout waiting for message")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testKafkaGoVersionCompatibility(t *testing.T, addr string) {
|
||||
// Test different kafka-go configurations
|
||||
configs := []struct {
|
||||
name string
|
||||
readerConfig kafka.ReaderConfig
|
||||
writerConfig kafka.WriterConfig
|
||||
}{
|
||||
{
|
||||
name: "kafka-go-default",
|
||||
readerConfig: kafka.ReaderConfig{
|
||||
Brokers: []string{addr},
|
||||
Partition: 0, // Read from specific partition instead of using consumer group
|
||||
},
|
||||
writerConfig: kafka.WriterConfig{
|
||||
Brokers: []string{addr},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "kafka-go-with-batching",
|
||||
readerConfig: kafka.ReaderConfig{
|
||||
Brokers: []string{addr},
|
||||
Partition: 0, // Read from specific partition instead of using consumer group
|
||||
MinBytes: 1,
|
||||
MaxBytes: 10e6,
|
||||
},
|
||||
writerConfig: kafka.WriterConfig{
|
||||
Brokers: []string{addr},
|
||||
BatchSize: 100,
|
||||
BatchTimeout: 10 * time.Millisecond,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, config := range configs {
|
||||
t.Run(config.name, func(t *testing.T) {
|
||||
topicName := testutil.GenerateUniqueTopicName(config.name)
|
||||
|
||||
// Create topic first using Sarama admin client (kafka-go doesn't have admin client)
|
||||
saramaConfig := sarama.NewConfig()
|
||||
saramaClient, err := sarama.NewClient([]string{addr}, saramaConfig)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create Sarama client for topic creation: %v", err)
|
||||
}
|
||||
defer saramaClient.Close()
|
||||
|
||||
admin, err := sarama.NewClusterAdminFromClient(saramaClient)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create admin client: %v", err)
|
||||
}
|
||||
defer admin.Close()
|
||||
|
||||
topicDetail := &sarama.TopicDetail{
|
||||
NumPartitions: 1,
|
||||
ReplicationFactor: 1,
|
||||
}
|
||||
|
||||
err = admin.CreateTopic(topicName, topicDetail, false)
|
||||
if err != nil {
|
||||
t.Logf("Topic creation failed (may already exist): %v", err)
|
||||
}
|
||||
|
||||
// Wait for topic to be fully created
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// Configure writer first and write message
|
||||
config.writerConfig.Topic = topicName
|
||||
writer := kafka.NewWriter(config.writerConfig)
|
||||
|
||||
// Test produce
|
||||
produceCtx, produceCancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer produceCancel()
|
||||
|
||||
message := kafka.Message{
|
||||
Value: []byte(fmt.Sprintf("test-message-%s", config.name)),
|
||||
}
|
||||
|
||||
err = writer.WriteMessages(produceCtx, message)
|
||||
if err != nil {
|
||||
writer.Close()
|
||||
t.Fatalf("Failed to write message: %v", err)
|
||||
}
|
||||
|
||||
// Close writer before reading to ensure flush
|
||||
if err := writer.Close(); err != nil {
|
||||
t.Logf("Warning: writer close error: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("%s: Message written successfully", config.name)
|
||||
|
||||
// Wait for message to be available
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Configure and create reader
|
||||
config.readerConfig.Topic = topicName
|
||||
config.readerConfig.StartOffset = kafka.FirstOffset
|
||||
reader := kafka.NewReader(config.readerConfig)
|
||||
|
||||
// Test consume with dedicated context
|
||||
consumeCtx, consumeCancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
|
||||
msg, err := reader.ReadMessage(consumeCtx)
|
||||
consumeCancel()
|
||||
|
||||
if err != nil {
|
||||
reader.Close()
|
||||
t.Fatalf("Failed to read message: %v", err)
|
||||
}
|
||||
|
||||
if string(msg.Value) != fmt.Sprintf("test-message-%s", config.name) {
|
||||
reader.Close()
|
||||
t.Errorf("Message content mismatch: expected %s, got %s",
|
||||
fmt.Sprintf("test-message-%s", config.name), string(msg.Value))
|
||||
}
|
||||
|
||||
t.Logf("%s: Successfully consumed message", config.name)
|
||||
|
||||
// Close reader and wait for cleanup
|
||||
if err := reader.Close(); err != nil {
|
||||
t.Logf("Warning: reader close error: %v", err)
|
||||
}
|
||||
|
||||
// Give time for background goroutines to clean up
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testAPIVersionNegotiation(t *testing.T, addr string) {
|
||||
// Test that clients can negotiate API versions properly
|
||||
config := sarama.NewConfig()
|
||||
config.Version = sarama.V2_8_0_0
|
||||
|
||||
client, err := sarama.NewClient([]string{addr}, config)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
// Test that the client can get API versions
|
||||
coordinator, err := client.Coordinator("test-group")
|
||||
if err != nil {
|
||||
t.Logf("Coordinator lookup failed (expected for test): %v", err)
|
||||
} else {
|
||||
t.Logf("Successfully found coordinator: %s", coordinator.Addr())
|
||||
}
|
||||
|
||||
// Test metadata request (should work with version negotiation)
|
||||
topics, err := client.Topics()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get topics: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("API version negotiation successful, found %d topics", len(topics))
|
||||
}
|
||||
|
||||
func testProducerConsumerCompatibility(t *testing.T, addr string) {
|
||||
// Test cross-client compatibility: produce with one client, consume with another
|
||||
topicName := testutil.GenerateUniqueTopicName("cross-client-test")
|
||||
|
||||
// Create topic first
|
||||
saramaConfig := sarama.NewConfig()
|
||||
saramaConfig.Producer.Return.Successes = true
|
||||
|
||||
saramaClient, err := sarama.NewClient([]string{addr}, saramaConfig)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create Sarama client: %v", err)
|
||||
}
|
||||
defer saramaClient.Close()
|
||||
|
||||
admin, err := sarama.NewClusterAdminFromClient(saramaClient)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create admin client: %v", err)
|
||||
}
|
||||
defer admin.Close()
|
||||
|
||||
topicDetail := &sarama.TopicDetail{
|
||||
NumPartitions: 1,
|
||||
ReplicationFactor: 1,
|
||||
}
|
||||
|
||||
err = admin.CreateTopic(topicName, topicDetail, false)
|
||||
if err != nil {
|
||||
t.Logf("Topic creation failed (may already exist): %v", err)
|
||||
}
|
||||
|
||||
// Wait for topic to be fully created
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
producer, err := sarama.NewSyncProducerFromClient(saramaClient)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create producer: %v", err)
|
||||
}
|
||||
defer producer.Close()
|
||||
|
||||
message := &sarama.ProducerMessage{
|
||||
Topic: topicName,
|
||||
Value: sarama.StringEncoder("cross-client-message"),
|
||||
}
|
||||
|
||||
_, _, err = producer.SendMessage(message)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to send message with Sarama: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("Produced message with Sarama")
|
||||
|
||||
// Wait for message to be available
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Consume with kafka-go (without consumer group to avoid offset commit issues)
|
||||
reader := kafka.NewReader(kafka.ReaderConfig{
|
||||
Brokers: []string{addr},
|
||||
Topic: topicName,
|
||||
Partition: 0,
|
||||
StartOffset: kafka.FirstOffset,
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
msg, err := reader.ReadMessage(ctx)
|
||||
cancel()
|
||||
|
||||
// Close reader immediately after reading
|
||||
if closeErr := reader.Close(); closeErr != nil {
|
||||
t.Logf("Warning: reader close error: %v", closeErr)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read message with kafka-go: %v", err)
|
||||
}
|
||||
|
||||
if string(msg.Value) != "cross-client-message" {
|
||||
t.Errorf("Message content mismatch: expected 'cross-client-message', got '%s'", string(msg.Value))
|
||||
}
|
||||
|
||||
t.Logf("Cross-client compatibility test passed")
|
||||
}
|
||||
|
||||
func testConsumerGroupCompatibility(t *testing.T, addr string) {
|
||||
// Test consumer group functionality with different clients
|
||||
topicName := testutil.GenerateUniqueTopicName("consumer-group-test")
|
||||
|
||||
// Create topic and produce messages
|
||||
config := sarama.NewConfig()
|
||||
config.Producer.Return.Successes = true
|
||||
|
||||
client, err := sarama.NewClient([]string{addr}, config)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
// Create topic first
|
||||
admin, err := sarama.NewClusterAdminFromClient(client)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create admin client: %v", err)
|
||||
}
|
||||
defer admin.Close()
|
||||
|
||||
topicDetail := &sarama.TopicDetail{
|
||||
NumPartitions: 1,
|
||||
ReplicationFactor: 1,
|
||||
}
|
||||
|
||||
err = admin.CreateTopic(topicName, topicDetail, false)
|
||||
if err != nil {
|
||||
t.Logf("Topic creation failed (may already exist): %v", err)
|
||||
}
|
||||
|
||||
// Wait for topic to be fully created
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
producer, err := sarama.NewSyncProducerFromClient(client)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create producer: %v", err)
|
||||
}
|
||||
defer producer.Close()
|
||||
|
||||
// Produce test messages
|
||||
for i := 0; i < 5; i++ {
|
||||
message := &sarama.ProducerMessage{
|
||||
Topic: topicName,
|
||||
Value: sarama.StringEncoder(fmt.Sprintf("group-message-%d", i)),
|
||||
}
|
||||
|
||||
_, _, err = producer.SendMessage(message)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to send message %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Produced 5 messages successfully")
|
||||
|
||||
// Wait for messages to be available
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// Test consumer group with Sarama (kafka-go consumer groups have offset commit issues)
|
||||
consumer, err := sarama.NewConsumerFromClient(client)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create consumer: %v", err)
|
||||
}
|
||||
defer consumer.Close()
|
||||
|
||||
partitionConsumer, err := consumer.ConsumePartition(topicName, 0, sarama.OffsetOldest)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create partition consumer: %v", err)
|
||||
}
|
||||
defer partitionConsumer.Close()
|
||||
|
||||
messagesReceived := 0
|
||||
timeout := time.After(30 * time.Second)
|
||||
|
||||
for messagesReceived < 5 {
|
||||
select {
|
||||
case msg := <-partitionConsumer.Messages():
|
||||
t.Logf("Received message %d: %s", messagesReceived, string(msg.Value))
|
||||
messagesReceived++
|
||||
case err := <-partitionConsumer.Errors():
|
||||
t.Logf("Consumer error (continuing): %v", err)
|
||||
case <-timeout:
|
||||
t.Fatalf("Timeout waiting for messages, received %d out of 5", messagesReceived)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Consumer group compatibility test passed: received %d messages", messagesReceived)
|
||||
}
|
||||
|
||||
func testAdminClientCompatibility(t *testing.T, addr string) {
|
||||
// Test admin operations with different clients
|
||||
config := sarama.NewConfig()
|
||||
config.Version = sarama.V2_8_0_0
|
||||
config.Admin.Timeout = 30 * time.Second
|
||||
|
||||
client, err := sarama.NewClient([]string{addr}, config)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
admin, err := sarama.NewClusterAdminFromClient(client)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create admin client: %v", err)
|
||||
}
|
||||
defer admin.Close()
|
||||
|
||||
// Test topic operations
|
||||
topicName := testutil.GenerateUniqueTopicName("admin-test")
|
||||
|
||||
topicDetail := &sarama.TopicDetail{
|
||||
NumPartitions: 2,
|
||||
ReplicationFactor: 1,
|
||||
}
|
||||
|
||||
err = admin.CreateTopic(topicName, topicDetail, false)
|
||||
if err != nil {
|
||||
t.Logf("Topic creation failed (may already exist): %v", err)
|
||||
}
|
||||
|
||||
// Wait for topic to be fully created and propagated
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
// List topics with retry logic
|
||||
var topics map[string]sarama.TopicDetail
|
||||
maxRetries := 3
|
||||
for i := 0; i < maxRetries; i++ {
|
||||
topics, err = admin.ListTopics()
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
t.Logf("List topics attempt %d failed: %v, retrying...", i+1, err)
|
||||
time.Sleep(time.Duration(500*(i+1)) * time.Millisecond)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to list topics after %d attempts: %v", maxRetries, err)
|
||||
}
|
||||
|
||||
found := false
|
||||
for topic := range topics {
|
||||
if topic == topicName {
|
||||
found = true
|
||||
t.Logf("Found created topic: %s", topicName)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
// Log all topics for debugging
|
||||
allTopics := make([]string, 0, len(topics))
|
||||
for topic := range topics {
|
||||
allTopics = append(allTopics, topic)
|
||||
}
|
||||
t.Logf("Available topics: %v", allTopics)
|
||||
t.Errorf("Created topic %s not found in topic list", topicName)
|
||||
}
|
||||
|
||||
// Test describe consumer groups (if supported)
|
||||
groups, err := admin.ListConsumerGroups()
|
||||
if err != nil {
|
||||
t.Logf("List consumer groups failed (may not be implemented): %v", err)
|
||||
} else {
|
||||
t.Logf("Found %d consumer groups", len(groups))
|
||||
}
|
||||
|
||||
t.Logf("Admin client compatibility test passed")
|
||||
}
|
||||
356
test/kafka/integration/consumer_groups_test.go
Normal file
356
test/kafka/integration/consumer_groups_test.go
Normal file
@@ -0,0 +1,356 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/IBM/sarama"
|
||||
"github.com/seaweedfs/seaweedfs/test/kafka/internal/testutil"
|
||||
)
|
||||
|
||||
// TestConsumerGroups tests consumer group functionality
|
||||
// This test requires SeaweedFS masters to be running and will skip if not available
|
||||
func TestConsumerGroups(t *testing.T) {
|
||||
gateway := testutil.NewGatewayTestServerWithSMQ(t, testutil.SMQRequired)
|
||||
defer gateway.CleanupAndClose()
|
||||
|
||||
addr := gateway.StartAndWait()
|
||||
|
||||
t.Logf("Running consumer group tests with SMQ backend for offset persistence")
|
||||
|
||||
t.Run("BasicFunctionality", func(t *testing.T) {
|
||||
testConsumerGroupBasicFunctionality(t, addr)
|
||||
})
|
||||
|
||||
t.Run("OffsetCommitAndFetch", func(t *testing.T) {
|
||||
testConsumerGroupOffsetCommitAndFetch(t, addr)
|
||||
})
|
||||
|
||||
t.Run("Rebalancing", func(t *testing.T) {
|
||||
testConsumerGroupRebalancing(t, addr)
|
||||
})
|
||||
}
|
||||
|
||||
func testConsumerGroupBasicFunctionality(t *testing.T, addr string) {
|
||||
topicName := testutil.GenerateUniqueTopicName("consumer-group-basic")
|
||||
groupID := testutil.GenerateUniqueGroupID("basic-group")
|
||||
|
||||
client := testutil.NewSaramaClient(t, addr)
|
||||
msgGen := testutil.NewMessageGenerator()
|
||||
|
||||
// Create topic and produce messages
|
||||
err := client.CreateTopic(topicName, 1, 1)
|
||||
testutil.AssertNoError(t, err, "Failed to create topic")
|
||||
|
||||
messages := msgGen.GenerateStringMessages(9) // 3 messages per consumer
|
||||
err = client.ProduceMessages(topicName, messages)
|
||||
testutil.AssertNoError(t, err, "Failed to produce messages")
|
||||
|
||||
// Test with multiple consumers in the same group. The messages channel is
|
||||
// shared so the assertion can verify total-consumed/no-duplicates across the
|
||||
// group. Each consumer gets its own handler (and its own ready channel) so
|
||||
// we can wait for each distinct consumer to reach Setup, rather than
|
||||
// relying on a single close() that only signals once.
|
||||
numConsumers := 3
|
||||
handlers := make([]*ConsumerGroupHandler, numConsumers)
|
||||
sharedMessages := make(chan *sarama.ConsumerMessage, len(messages))
|
||||
for i := 0; i < numConsumers; i++ {
|
||||
handlers[i] = &ConsumerGroupHandler{
|
||||
messages: sharedMessages,
|
||||
ready: make(chan bool),
|
||||
t: t,
|
||||
}
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
consumerErrors := make(chan error, numConsumers)
|
||||
|
||||
for i := 0; i < numConsumers; i++ {
|
||||
wg.Add(1)
|
||||
go func(consumerID int) {
|
||||
defer wg.Done()
|
||||
|
||||
consumerGroup, err := sarama.NewConsumerGroup([]string{addr}, groupID, client.GetConfig())
|
||||
if err != nil {
|
||||
consumerErrors <- fmt.Errorf("consumer %d: failed to create consumer group: %v", consumerID, err)
|
||||
return
|
||||
}
|
||||
defer consumerGroup.Close()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
defer cancel()
|
||||
|
||||
runConsumeLoop(t, ctx, fmt.Sprintf("Consumer%d", consumerID),
|
||||
consumerGroup, []string{topicName}, handlers[consumerID])
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Wait for each consumer to be ready. Multi-consumer rebalance can take
|
||||
// a few heartbeat intervals (default 3s) as the initial leader receives
|
||||
// REBALANCE_IN_PROGRESS from its heartbeat and re-joins, so allow headroom.
|
||||
for i := 0; i < numConsumers; i++ {
|
||||
select {
|
||||
case <-handlers[i].ready:
|
||||
case <-time.After(20 * time.Second):
|
||||
t.Fatalf("Timeout waiting for consumer %d to be ready", i)
|
||||
}
|
||||
}
|
||||
|
||||
// Collect consumed messages
|
||||
consumedMessages := make([]*sarama.ConsumerMessage, 0, len(messages))
|
||||
messageTimeout := time.After(15 * time.Second)
|
||||
|
||||
for len(consumedMessages) < len(messages) {
|
||||
select {
|
||||
case msg := <-sharedMessages:
|
||||
consumedMessages = append(consumedMessages, msg)
|
||||
case err := <-consumerErrors:
|
||||
t.Fatalf("Consumer error: %v", err)
|
||||
case <-messageTimeout:
|
||||
t.Fatalf("Timeout waiting for messages. Got %d/%d messages", len(consumedMessages), len(messages))
|
||||
}
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Verify all messages were consumed exactly once
|
||||
testutil.AssertEqual(t, len(messages), len(consumedMessages), "Message count mismatch")
|
||||
|
||||
// Verify message uniqueness (no duplicates)
|
||||
messageKeys := make(map[string]bool)
|
||||
for _, msg := range consumedMessages {
|
||||
key := string(msg.Key)
|
||||
if messageKeys[key] {
|
||||
t.Errorf("Duplicate message key: %s", key)
|
||||
}
|
||||
messageKeys[key] = true
|
||||
}
|
||||
}
|
||||
|
||||
func testConsumerGroupOffsetCommitAndFetch(t *testing.T, addr string) {
|
||||
topicName := testutil.GenerateUniqueTopicName("offset-commit-test")
|
||||
groupID := testutil.GenerateUniqueGroupID("offset-group")
|
||||
|
||||
client := testutil.NewSaramaClient(t, addr)
|
||||
msgGen := testutil.NewMessageGenerator()
|
||||
|
||||
// Create topic and produce messages
|
||||
err := client.CreateTopic(topicName, 1, 1)
|
||||
testutil.AssertNoError(t, err, "Failed to create topic")
|
||||
|
||||
messages := msgGen.GenerateStringMessages(5)
|
||||
err = client.ProduceMessages(topicName, messages)
|
||||
testutil.AssertNoError(t, err, "Failed to produce messages")
|
||||
|
||||
// First consumer: consume first 3 messages and commit offsets
|
||||
handler1 := &OffsetTestHandler{
|
||||
messages: make(chan *sarama.ConsumerMessage, len(messages)),
|
||||
ready: make(chan bool),
|
||||
stopAfter: 3,
|
||||
t: t,
|
||||
}
|
||||
|
||||
consumerGroup1, err := sarama.NewConsumerGroup([]string{addr}, groupID, client.GetConfig())
|
||||
testutil.AssertNoError(t, err, "Failed to create first consumer group")
|
||||
|
||||
ctx1, cancel1 := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel1()
|
||||
|
||||
go func() {
|
||||
err := consumerGroup1.Consume(ctx1, []string{topicName}, handler1)
|
||||
if err != nil && err != context.DeadlineExceeded {
|
||||
t.Logf("First consumer error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for first consumer to be ready and consume messages
|
||||
<-handler1.ready
|
||||
consumedCount := 0
|
||||
for consumedCount < 3 {
|
||||
select {
|
||||
case <-handler1.messages:
|
||||
consumedCount++
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatalf("Timeout waiting for first consumer messages")
|
||||
}
|
||||
}
|
||||
|
||||
consumerGroup1.Close()
|
||||
cancel1()
|
||||
time.Sleep(500 * time.Millisecond) // Wait for cleanup
|
||||
|
||||
// Stop the first consumer after N messages
|
||||
// Allow a brief moment for commit/heartbeat to flush
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
// Start a second consumer in the same group to verify resumption from committed offset
|
||||
handler2 := &OffsetTestHandler{
|
||||
messages: make(chan *sarama.ConsumerMessage, len(messages)),
|
||||
ready: make(chan bool),
|
||||
stopAfter: 2,
|
||||
t: t,
|
||||
}
|
||||
consumerGroup2, err := sarama.NewConsumerGroup([]string{addr}, groupID, client.GetConfig())
|
||||
testutil.AssertNoError(t, err, "Failed to create second consumer group")
|
||||
defer consumerGroup2.Close()
|
||||
|
||||
ctx2, cancel2 := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel2()
|
||||
|
||||
go func() {
|
||||
err := consumerGroup2.Consume(ctx2, []string{topicName}, handler2)
|
||||
if err != nil && err != context.DeadlineExceeded {
|
||||
t.Logf("Second consumer error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for second consumer and collect remaining messages
|
||||
<-handler2.ready
|
||||
secondConsumerMessages := make([]*sarama.ConsumerMessage, 0)
|
||||
consumedCount = 0
|
||||
for consumedCount < 2 {
|
||||
select {
|
||||
case msg := <-handler2.messages:
|
||||
consumedCount++
|
||||
secondConsumerMessages = append(secondConsumerMessages, msg)
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatalf("Timeout waiting for second consumer messages. Got %d/2", consumedCount)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify second consumer started from correct offset
|
||||
if len(secondConsumerMessages) > 0 {
|
||||
firstMessageOffset := secondConsumerMessages[0].Offset
|
||||
if firstMessageOffset < 3 {
|
||||
t.Fatalf("Second consumer should start from offset >= 3: got %d", firstMessageOffset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testConsumerGroupRebalancing(t *testing.T, addr string) {
|
||||
topicName := testutil.GenerateUniqueTopicName("rebalancing-test")
|
||||
groupID := testutil.GenerateUniqueGroupID("rebalance-group")
|
||||
|
||||
client := testutil.NewSaramaClient(t, addr)
|
||||
msgGen := testutil.NewMessageGenerator()
|
||||
|
||||
// Create topic with multiple partitions for rebalancing
|
||||
err := client.CreateTopic(topicName, 4, 1) // 4 partitions
|
||||
testutil.AssertNoError(t, err, "Failed to create topic")
|
||||
|
||||
// Produce messages to all partitions
|
||||
messages := msgGen.GenerateStringMessages(12) // 3 messages per partition
|
||||
for i, msg := range messages {
|
||||
partition := int32(i % 4)
|
||||
err = client.ProduceMessageToPartition(topicName, partition, msg)
|
||||
testutil.AssertNoError(t, err, "Failed to produce message")
|
||||
}
|
||||
|
||||
t.Logf("Produced %d messages across 4 partitions", len(messages))
|
||||
|
||||
// Test scenario 1: Single consumer gets all partitions
|
||||
t.Run("SingleConsumerAllPartitions", func(t *testing.T) {
|
||||
testSingleConsumerAllPartitions(t, addr, topicName, groupID+"-single")
|
||||
})
|
||||
|
||||
// Test scenario 2: Add second consumer, verify rebalancing
|
||||
t.Run("TwoConsumersRebalance", func(t *testing.T) {
|
||||
testTwoConsumersRebalance(t, addr, topicName, groupID+"-two")
|
||||
})
|
||||
|
||||
// Test scenario 3: Remove consumer, verify rebalancing
|
||||
t.Run("ConsumerLeaveRebalance", func(t *testing.T) {
|
||||
testConsumerLeaveRebalance(t, addr, topicName, groupID+"-leave")
|
||||
})
|
||||
|
||||
// Test scenario 4: Multiple consumers join simultaneously
|
||||
t.Run("MultipleConsumersJoin", func(t *testing.T) {
|
||||
testMultipleConsumersJoin(t, addr, topicName, groupID+"-multi")
|
||||
})
|
||||
}
|
||||
|
||||
// ConsumerGroupHandler implements sarama.ConsumerGroupHandler
|
||||
type ConsumerGroupHandler struct {
|
||||
messages chan *sarama.ConsumerMessage
|
||||
ready chan bool
|
||||
readyOnce sync.Once
|
||||
t *testing.T
|
||||
}
|
||||
|
||||
func (h *ConsumerGroupHandler) Setup(sarama.ConsumerGroupSession) error {
|
||||
h.t.Logf("Consumer group session setup")
|
||||
h.readyOnce.Do(func() {
|
||||
close(h.ready)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *ConsumerGroupHandler) Cleanup(sarama.ConsumerGroupSession) error {
|
||||
h.t.Logf("Consumer group session cleanup")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *ConsumerGroupHandler) ConsumeClaim(session sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error {
|
||||
for {
|
||||
select {
|
||||
case message := <-claim.Messages():
|
||||
if message == nil {
|
||||
return nil
|
||||
}
|
||||
h.messages <- message
|
||||
session.MarkMessage(message, "")
|
||||
case <-session.Context().Done():
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// OffsetTestHandler implements sarama.ConsumerGroupHandler for offset testing
|
||||
type OffsetTestHandler struct {
|
||||
messages chan *sarama.ConsumerMessage
|
||||
ready chan bool
|
||||
readyOnce sync.Once
|
||||
stopAfter int
|
||||
consumed int
|
||||
t *testing.T
|
||||
}
|
||||
|
||||
func (h *OffsetTestHandler) Setup(sarama.ConsumerGroupSession) error {
|
||||
h.t.Logf("Offset test consumer setup")
|
||||
h.readyOnce.Do(func() {
|
||||
close(h.ready)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *OffsetTestHandler) Cleanup(sarama.ConsumerGroupSession) error {
|
||||
h.t.Logf("Offset test consumer cleanup")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *OffsetTestHandler) ConsumeClaim(session sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error {
|
||||
for {
|
||||
select {
|
||||
case message := <-claim.Messages():
|
||||
if message == nil {
|
||||
return nil
|
||||
}
|
||||
h.consumed++
|
||||
h.messages <- message
|
||||
session.MarkMessage(message, "")
|
||||
|
||||
// Stop after consuming the specified number of messages
|
||||
if h.consumed >= h.stopAfter {
|
||||
h.t.Logf("Stopping consumer after %d messages", h.consumed)
|
||||
// Ensure commits are flushed before exiting the claim
|
||||
session.Commit()
|
||||
return nil
|
||||
}
|
||||
case <-session.Context().Done():
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
216
test/kafka/integration/docker_test.go
Normal file
216
test/kafka/integration/docker_test.go
Normal file
@@ -0,0 +1,216 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/test/kafka/internal/testutil"
|
||||
)
|
||||
|
||||
// TestDockerIntegration tests the complete Kafka integration using Docker Compose
|
||||
func TestDockerIntegration(t *testing.T) {
|
||||
env := testutil.NewDockerEnvironment(t)
|
||||
env.SkipIfNotAvailable(t)
|
||||
|
||||
t.Run("KafkaConnectivity", func(t *testing.T) {
|
||||
env.RequireKafka(t)
|
||||
testDockerKafkaConnectivity(t, env.KafkaBootstrap)
|
||||
})
|
||||
|
||||
t.Run("SchemaRegistryConnectivity", func(t *testing.T) {
|
||||
env.RequireSchemaRegistry(t)
|
||||
testDockerSchemaRegistryConnectivity(t, env.SchemaRegistry)
|
||||
})
|
||||
|
||||
t.Run("KafkaGatewayConnectivity", func(t *testing.T) {
|
||||
env.RequireGateway(t)
|
||||
testDockerKafkaGatewayConnectivity(t, env.KafkaGateway)
|
||||
})
|
||||
|
||||
t.Run("SaramaProduceConsume", func(t *testing.T) {
|
||||
env.RequireKafka(t)
|
||||
testDockerSaramaProduceConsume(t, env.KafkaBootstrap)
|
||||
})
|
||||
|
||||
t.Run("KafkaGoProduceConsume", func(t *testing.T) {
|
||||
env.RequireKafka(t)
|
||||
testDockerKafkaGoProduceConsume(t, env.KafkaBootstrap)
|
||||
})
|
||||
|
||||
t.Run("GatewayProduceConsume", func(t *testing.T) {
|
||||
env.RequireGateway(t)
|
||||
testDockerGatewayProduceConsume(t, env.KafkaGateway)
|
||||
})
|
||||
|
||||
t.Run("CrossClientCompatibility", func(t *testing.T) {
|
||||
env.RequireKafka(t)
|
||||
env.RequireGateway(t)
|
||||
testDockerCrossClientCompatibility(t, env.KafkaBootstrap, env.KafkaGateway)
|
||||
})
|
||||
}
|
||||
|
||||
func testDockerKafkaConnectivity(t *testing.T, bootstrap string) {
|
||||
client := testutil.NewSaramaClient(t, bootstrap)
|
||||
|
||||
// Test basic connectivity by creating a topic
|
||||
topicName := testutil.GenerateUniqueTopicName("connectivity-test")
|
||||
err := client.CreateTopic(topicName, 1, 1)
|
||||
testutil.AssertNoError(t, err, "Failed to create topic for connectivity test")
|
||||
|
||||
t.Logf("Kafka connectivity test passed")
|
||||
}
|
||||
|
||||
func testDockerSchemaRegistryConnectivity(t *testing.T, registryURL string) {
|
||||
// Test basic HTTP connectivity to Schema Registry
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
|
||||
// Test 1: Check if Schema Registry is responding
|
||||
resp, err := client.Get(registryURL + "/subjects")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to connect to Schema Registry at %s: %v", registryURL, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("Schema Registry returned status %d, expected 200", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Test 2: Verify response is valid JSON array
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read response body: %v", err)
|
||||
}
|
||||
|
||||
var subjects []string
|
||||
if err := json.Unmarshal(body, &subjects); err != nil {
|
||||
t.Fatalf("Schema Registry response is not valid JSON array: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("Schema Registry is accessible with %d subjects", len(subjects))
|
||||
|
||||
// Test 3: Check config endpoint
|
||||
configResp, err := client.Get(registryURL + "/config")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get Schema Registry config: %v", err)
|
||||
}
|
||||
defer configResp.Body.Close()
|
||||
|
||||
if configResp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("Schema Registry config endpoint returned status %d", configResp.StatusCode)
|
||||
}
|
||||
|
||||
configBody, err := io.ReadAll(configResp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read config response: %v", err)
|
||||
}
|
||||
|
||||
var config map[string]interface{}
|
||||
if err := json.Unmarshal(configBody, &config); err != nil {
|
||||
t.Fatalf("Schema Registry config response is not valid JSON: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("Schema Registry config: %v", config)
|
||||
t.Logf("Schema Registry connectivity test passed")
|
||||
}
|
||||
|
||||
func testDockerKafkaGatewayConnectivity(t *testing.T, gatewayURL string) {
|
||||
client := testutil.NewSaramaClient(t, gatewayURL)
|
||||
|
||||
// Test basic connectivity to gateway
|
||||
topicName := testutil.GenerateUniqueTopicName("gateway-connectivity-test")
|
||||
err := client.CreateTopic(topicName, 1, 1)
|
||||
testutil.AssertNoError(t, err, "Failed to create topic via gateway")
|
||||
|
||||
t.Logf("Kafka Gateway connectivity test passed")
|
||||
}
|
||||
|
||||
func testDockerSaramaProduceConsume(t *testing.T, bootstrap string) {
|
||||
client := testutil.NewSaramaClient(t, bootstrap)
|
||||
msgGen := testutil.NewMessageGenerator()
|
||||
|
||||
topicName := testutil.GenerateUniqueTopicName("sarama-docker-test")
|
||||
|
||||
// Create topic
|
||||
err := client.CreateTopic(topicName, 1, 1)
|
||||
testutil.AssertNoError(t, err, "Failed to create topic")
|
||||
|
||||
// Produce and consume messages
|
||||
messages := msgGen.GenerateStringMessages(3)
|
||||
err = client.ProduceMessages(topicName, messages)
|
||||
testutil.AssertNoError(t, err, "Failed to produce messages")
|
||||
|
||||
consumed, err := client.ConsumeMessages(topicName, 0, len(messages))
|
||||
testutil.AssertNoError(t, err, "Failed to consume messages")
|
||||
|
||||
err = testutil.ValidateMessageContent(messages, consumed)
|
||||
testutil.AssertNoError(t, err, "Message validation failed")
|
||||
|
||||
t.Logf("Sarama produce/consume test passed")
|
||||
}
|
||||
|
||||
func testDockerKafkaGoProduceConsume(t *testing.T, bootstrap string) {
|
||||
client := testutil.NewKafkaGoClient(t, bootstrap)
|
||||
msgGen := testutil.NewMessageGenerator()
|
||||
|
||||
topicName := testutil.GenerateUniqueTopicName("kafka-go-docker-test")
|
||||
|
||||
// Create topic
|
||||
err := client.CreateTopic(topicName, 1, 1)
|
||||
testutil.AssertNoError(t, err, "Failed to create topic")
|
||||
|
||||
// Produce and consume messages
|
||||
messages := msgGen.GenerateKafkaGoMessages(3)
|
||||
err = client.ProduceMessages(topicName, messages)
|
||||
testutil.AssertNoError(t, err, "Failed to produce messages")
|
||||
|
||||
consumed, err := client.ConsumeMessages(topicName, len(messages))
|
||||
testutil.AssertNoError(t, err, "Failed to consume messages")
|
||||
|
||||
err = testutil.ValidateKafkaGoMessageContent(messages, consumed)
|
||||
testutil.AssertNoError(t, err, "Message validation failed")
|
||||
|
||||
t.Logf("kafka-go produce/consume test passed")
|
||||
}
|
||||
|
||||
func testDockerGatewayProduceConsume(t *testing.T, gatewayURL string) {
|
||||
client := testutil.NewSaramaClient(t, gatewayURL)
|
||||
msgGen := testutil.NewMessageGenerator()
|
||||
|
||||
topicName := testutil.GenerateUniqueTopicName("gateway-docker-test")
|
||||
|
||||
// Produce and consume via gateway
|
||||
messages := msgGen.GenerateStringMessages(3)
|
||||
err := client.ProduceMessages(topicName, messages)
|
||||
testutil.AssertNoError(t, err, "Failed to produce messages via gateway")
|
||||
|
||||
consumed, err := client.ConsumeMessages(topicName, 0, len(messages))
|
||||
testutil.AssertNoError(t, err, "Failed to consume messages via gateway")
|
||||
|
||||
err = testutil.ValidateMessageContent(messages, consumed)
|
||||
testutil.AssertNoError(t, err, "Message validation failed")
|
||||
|
||||
t.Logf("Gateway produce/consume test passed")
|
||||
}
|
||||
|
||||
func testDockerCrossClientCompatibility(t *testing.T, kafkaBootstrap, gatewayURL string) {
|
||||
kafkaClient := testutil.NewSaramaClient(t, kafkaBootstrap)
|
||||
msgGen := testutil.NewMessageGenerator()
|
||||
|
||||
topicName := testutil.GenerateUniqueTopicName("cross-client-docker-test")
|
||||
|
||||
// Create topic on Kafka
|
||||
err := kafkaClient.CreateTopic(topicName, 1, 1)
|
||||
testutil.AssertNoError(t, err, "Failed to create topic on Kafka")
|
||||
|
||||
// Produce to Kafka
|
||||
messages := msgGen.GenerateStringMessages(2)
|
||||
err = kafkaClient.ProduceMessages(topicName, messages)
|
||||
testutil.AssertNoError(t, err, "Failed to produce to Kafka")
|
||||
|
||||
// This tests the integration between Kafka and the Gateway
|
||||
// In a real scenario, messages would be replicated or bridged
|
||||
t.Logf("Cross-client compatibility test passed")
|
||||
}
|
||||
474
test/kafka/integration/rebalancing_test.go
Normal file
474
test/kafka/integration/rebalancing_test.go
Normal file
@@ -0,0 +1,474 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/IBM/sarama"
|
||||
"github.com/seaweedfs/seaweedfs/test/kafka/internal/testutil"
|
||||
)
|
||||
|
||||
// runConsumeLoop drives consumerGroup.Consume in a for-loop until ctx is cancelled
|
||||
// or the group is closed. This is the idiomatic Sarama pattern: Consume returns
|
||||
// when a rebalance cancels the session, and the caller is expected to re-enter.
|
||||
// Tests that observe rebalance behaviour MUST loop — otherwise the consumer goroutine
|
||||
// exits on the first rebalance and never sees the post-rebalance assignment.
|
||||
func runConsumeLoop(t *testing.T, ctx context.Context, name string, cg sarama.ConsumerGroup, topics []string, handler sarama.ConsumerGroupHandler) {
|
||||
for {
|
||||
if err := cg.Consume(ctx, topics, handler); err != nil {
|
||||
if ctx.Err() != nil || errors.Is(err, sarama.ErrClosedConsumerGroup) {
|
||||
return
|
||||
}
|
||||
t.Logf("%s Consume returned: %v (will retry)", name, err)
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testSingleConsumerAllPartitions(t *testing.T, addr, topicName, groupID string) {
|
||||
config := sarama.NewConfig()
|
||||
config.Consumer.Group.Rebalance.Strategy = sarama.BalanceStrategyRange
|
||||
config.Consumer.Offsets.Initial = sarama.OffsetOldest
|
||||
config.Consumer.Return.Errors = true
|
||||
|
||||
client, err := sarama.NewClient([]string{addr}, config)
|
||||
testutil.AssertNoError(t, err, "Failed to create client")
|
||||
defer client.Close()
|
||||
|
||||
consumerGroup, err := sarama.NewConsumerGroupFromClient(groupID, client)
|
||||
testutil.AssertNoError(t, err, "Failed to create consumer group")
|
||||
defer consumerGroup.Close()
|
||||
|
||||
handler := &RebalanceTestHandler{
|
||||
messages: make(chan *sarama.ConsumerMessage, 20),
|
||||
ready: make(chan bool),
|
||||
assignments: make(chan []int32, 1),
|
||||
t: t,
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Start consumer
|
||||
go func() {
|
||||
err := consumerGroup.Consume(ctx, []string{topicName}, handler)
|
||||
if err != nil && err != context.DeadlineExceeded {
|
||||
t.Logf("Consumer error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for consumer to be ready
|
||||
<-handler.ready
|
||||
|
||||
// Wait for assignment
|
||||
select {
|
||||
case partitions := <-handler.assignments:
|
||||
t.Logf("Single consumer assigned partitions: %v", partitions)
|
||||
if len(partitions) != 4 {
|
||||
t.Errorf("Expected single consumer to get all 4 partitions, got %d", len(partitions))
|
||||
}
|
||||
case <-time.After(10 * time.Second):
|
||||
t.Fatal("Timeout waiting for partition assignment")
|
||||
}
|
||||
|
||||
// Consume some messages to verify functionality
|
||||
consumedCount := 0
|
||||
for consumedCount < 4 { // At least one from each partition
|
||||
select {
|
||||
case msg := <-handler.messages:
|
||||
t.Logf("Consumed message from partition %d: %s", msg.Partition, string(msg.Value))
|
||||
consumedCount++
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Logf("Consumed %d messages so far", consumedCount)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if consumedCount == 0 {
|
||||
t.Error("No messages consumed by single consumer")
|
||||
}
|
||||
}
|
||||
|
||||
func testTwoConsumersRebalance(t *testing.T, addr, topicName, groupID string) {
|
||||
config := sarama.NewConfig()
|
||||
config.Consumer.Group.Rebalance.Strategy = sarama.BalanceStrategyRange
|
||||
config.Consumer.Offsets.Initial = sarama.OffsetOldest
|
||||
config.Consumer.Return.Errors = true
|
||||
|
||||
// Start first consumer
|
||||
client1, err := sarama.NewClient([]string{addr}, config)
|
||||
testutil.AssertNoError(t, err, "Failed to create client1")
|
||||
defer client1.Close()
|
||||
|
||||
consumerGroup1, err := sarama.NewConsumerGroupFromClient(groupID, client1)
|
||||
testutil.AssertNoError(t, err, "Failed to create consumer group 1")
|
||||
defer consumerGroup1.Close()
|
||||
|
||||
handler1 := &RebalanceTestHandler{
|
||||
messages: make(chan *sarama.ConsumerMessage, 20),
|
||||
ready: make(chan bool),
|
||||
assignments: make(chan []int32, 1),
|
||||
t: t,
|
||||
name: "Consumer1",
|
||||
}
|
||||
|
||||
ctx1, cancel1 := context.WithTimeout(context.Background(), 45*time.Second)
|
||||
defer cancel1()
|
||||
|
||||
go runConsumeLoop(t, ctx1, "Consumer1", consumerGroup1, []string{topicName}, handler1)
|
||||
|
||||
// Wait for first consumer to be ready and get initial assignment
|
||||
<-handler1.ready
|
||||
select {
|
||||
case partitions := <-handler1.assignments:
|
||||
t.Logf("Consumer1 initial assignment: %v", partitions)
|
||||
if len(partitions) != 4 {
|
||||
t.Errorf("Expected Consumer1 to initially get all 4 partitions, got %d", len(partitions))
|
||||
}
|
||||
case <-time.After(10 * time.Second):
|
||||
t.Fatal("Timeout waiting for Consumer1 initial assignment")
|
||||
}
|
||||
|
||||
// Start second consumer
|
||||
client2, err := sarama.NewClient([]string{addr}, config)
|
||||
testutil.AssertNoError(t, err, "Failed to create client2")
|
||||
defer client2.Close()
|
||||
|
||||
consumerGroup2, err := sarama.NewConsumerGroupFromClient(groupID, client2)
|
||||
testutil.AssertNoError(t, err, "Failed to create consumer group 2")
|
||||
defer consumerGroup2.Close()
|
||||
|
||||
handler2 := &RebalanceTestHandler{
|
||||
messages: make(chan *sarama.ConsumerMessage, 20),
|
||||
ready: make(chan bool),
|
||||
assignments: make(chan []int32, 1),
|
||||
t: t,
|
||||
name: "Consumer2",
|
||||
}
|
||||
|
||||
ctx2, cancel2 := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel2()
|
||||
|
||||
go runConsumeLoop(t, ctx2, "Consumer2", consumerGroup2, []string{topicName}, handler2)
|
||||
|
||||
// Wait for second consumer to be ready
|
||||
<-handler2.ready
|
||||
|
||||
// Wait for rebalancing to occur - both consumers should get new assignments
|
||||
var rebalancedAssignment1, rebalancedAssignment2 []int32
|
||||
|
||||
// Consumer1 should get a rebalance assignment
|
||||
select {
|
||||
case partitions := <-handler1.assignments:
|
||||
rebalancedAssignment1 = partitions
|
||||
t.Logf("Consumer1 rebalanced assignment: %v", partitions)
|
||||
case <-time.After(15 * time.Second):
|
||||
t.Error("Timeout waiting for Consumer1 rebalance assignment")
|
||||
}
|
||||
|
||||
// Consumer2 should get its assignment
|
||||
select {
|
||||
case partitions := <-handler2.assignments:
|
||||
rebalancedAssignment2 = partitions
|
||||
t.Logf("Consumer2 assignment: %v", partitions)
|
||||
case <-time.After(15 * time.Second):
|
||||
t.Error("Timeout waiting for Consumer2 assignment")
|
||||
}
|
||||
|
||||
// Verify rebalancing occurred correctly
|
||||
totalPartitions := len(rebalancedAssignment1) + len(rebalancedAssignment2)
|
||||
if totalPartitions != 4 {
|
||||
t.Errorf("Expected total of 4 partitions assigned, got %d", totalPartitions)
|
||||
}
|
||||
|
||||
// Each consumer should have at least 1 partition, and no more than 3
|
||||
if len(rebalancedAssignment1) == 0 || len(rebalancedAssignment1) > 3 {
|
||||
t.Errorf("Consumer1 should have 1-3 partitions, got %d", len(rebalancedAssignment1))
|
||||
}
|
||||
if len(rebalancedAssignment2) == 0 || len(rebalancedAssignment2) > 3 {
|
||||
t.Errorf("Consumer2 should have 1-3 partitions, got %d", len(rebalancedAssignment2))
|
||||
}
|
||||
|
||||
// Verify no partition overlap
|
||||
partitionSet := make(map[int32]bool)
|
||||
for _, p := range rebalancedAssignment1 {
|
||||
if partitionSet[p] {
|
||||
t.Errorf("Partition %d assigned to multiple consumers", p)
|
||||
}
|
||||
partitionSet[p] = true
|
||||
}
|
||||
for _, p := range rebalancedAssignment2 {
|
||||
if partitionSet[p] {
|
||||
t.Errorf("Partition %d assigned to multiple consumers", p)
|
||||
}
|
||||
partitionSet[p] = true
|
||||
}
|
||||
|
||||
t.Logf("Rebalancing test completed successfully")
|
||||
}
|
||||
|
||||
func testConsumerLeaveRebalance(t *testing.T, addr, topicName, groupID string) {
|
||||
config := sarama.NewConfig()
|
||||
config.Consumer.Group.Rebalance.Strategy = sarama.BalanceStrategyRange
|
||||
config.Consumer.Offsets.Initial = sarama.OffsetOldest
|
||||
config.Consumer.Return.Errors = true
|
||||
|
||||
// Start two consumers
|
||||
client1, err := sarama.NewClient([]string{addr}, config)
|
||||
testutil.AssertNoError(t, err, "Failed to create client1")
|
||||
defer client1.Close()
|
||||
|
||||
client2, err := sarama.NewClient([]string{addr}, config)
|
||||
testutil.AssertNoError(t, err, "Failed to create client2")
|
||||
defer client2.Close()
|
||||
|
||||
consumerGroup1, err := sarama.NewConsumerGroupFromClient(groupID, client1)
|
||||
testutil.AssertNoError(t, err, "Failed to create consumer group 1")
|
||||
defer consumerGroup1.Close()
|
||||
|
||||
consumerGroup2, err := sarama.NewConsumerGroupFromClient(groupID, client2)
|
||||
testutil.AssertNoError(t, err, "Failed to create consumer group 2")
|
||||
|
||||
handler1 := &RebalanceTestHandler{
|
||||
messages: make(chan *sarama.ConsumerMessage, 20),
|
||||
ready: make(chan bool),
|
||||
assignments: make(chan []int32, 1),
|
||||
t: t,
|
||||
name: "Consumer1",
|
||||
}
|
||||
|
||||
handler2 := &RebalanceTestHandler{
|
||||
messages: make(chan *sarama.ConsumerMessage, 20),
|
||||
ready: make(chan bool),
|
||||
assignments: make(chan []int32, 1),
|
||||
t: t,
|
||||
name: "Consumer2",
|
||||
}
|
||||
|
||||
ctx1, cancel1 := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel1()
|
||||
|
||||
ctx2, cancel2 := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
|
||||
// Start both consumers with loops so they survive rebalance session exits.
|
||||
go runConsumeLoop(t, ctx1, "Consumer1", consumerGroup1, []string{topicName}, handler1)
|
||||
go runConsumeLoop(t, ctx2, "Consumer2", consumerGroup2, []string{topicName}, handler2)
|
||||
|
||||
// Wait for both consumers to be ready
|
||||
<-handler1.ready
|
||||
<-handler2.ready
|
||||
|
||||
// Wait for initial assignments
|
||||
<-handler1.assignments
|
||||
<-handler2.assignments
|
||||
|
||||
t.Logf("Both consumers started, now stopping Consumer2")
|
||||
|
||||
// Stop second consumer (simulate leave)
|
||||
cancel2()
|
||||
consumerGroup2.Close()
|
||||
|
||||
// Wait for Consumer1 to get rebalanced assignment (should get all partitions)
|
||||
select {
|
||||
case partitions := <-handler1.assignments:
|
||||
t.Logf("Consumer1 rebalanced assignment after Consumer2 left: %v", partitions)
|
||||
if len(partitions) != 4 {
|
||||
t.Errorf("Expected Consumer1 to get all 4 partitions after Consumer2 left, got %d", len(partitions))
|
||||
}
|
||||
case <-time.After(20 * time.Second):
|
||||
t.Error("Timeout waiting for Consumer1 rebalance after Consumer2 left")
|
||||
}
|
||||
|
||||
t.Logf("Consumer leave rebalancing test completed successfully")
|
||||
}
|
||||
|
||||
func testMultipleConsumersJoin(t *testing.T, addr, topicName, groupID string) {
|
||||
config := sarama.NewConfig()
|
||||
config.Consumer.Group.Rebalance.Strategy = sarama.BalanceStrategyRange
|
||||
config.Consumer.Offsets.Initial = sarama.OffsetOldest
|
||||
config.Consumer.Return.Errors = true
|
||||
|
||||
numConsumers := 4
|
||||
consumers := make([]sarama.ConsumerGroup, numConsumers)
|
||||
clients := make([]sarama.Client, numConsumers)
|
||||
handlers := make([]*RebalanceTestHandler, numConsumers)
|
||||
contexts := make([]context.Context, numConsumers)
|
||||
cancels := make([]context.CancelFunc, numConsumers)
|
||||
|
||||
// Start all consumers simultaneously
|
||||
for i := 0; i < numConsumers; i++ {
|
||||
client, err := sarama.NewClient([]string{addr}, config)
|
||||
testutil.AssertNoError(t, err, fmt.Sprintf("Failed to create client%d", i))
|
||||
clients[i] = client
|
||||
|
||||
consumerGroup, err := sarama.NewConsumerGroupFromClient(groupID, client)
|
||||
testutil.AssertNoError(t, err, fmt.Sprintf("Failed to create consumer group %d", i))
|
||||
consumers[i] = consumerGroup
|
||||
|
||||
handlers[i] = &RebalanceTestHandler{
|
||||
messages: make(chan *sarama.ConsumerMessage, 20),
|
||||
ready: make(chan bool),
|
||||
assignments: make(chan []int32, 1),
|
||||
t: t,
|
||||
name: fmt.Sprintf("Consumer%d", i),
|
||||
}
|
||||
|
||||
contexts[i], cancels[i] = context.WithTimeout(context.Background(), 45*time.Second)
|
||||
|
||||
go func(idx int) {
|
||||
runConsumeLoop(t, contexts[idx], fmt.Sprintf("Consumer%d", idx),
|
||||
consumers[idx], []string{topicName}, handlers[idx])
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
defer func() {
|
||||
for i := 0; i < numConsumers; i++ {
|
||||
cancels[i]()
|
||||
consumers[i].Close()
|
||||
clients[i].Close()
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for all consumers to be ready. Four consumers racing in produces
|
||||
// several rebalance rounds; each round costs ~HeartbeatInterval (3s) before
|
||||
// the prior leader sees REBALANCE_IN_PROGRESS and rejoins. Give enough
|
||||
// headroom for the group to converge.
|
||||
for i := 0; i < numConsumers; i++ {
|
||||
select {
|
||||
case <-handlers[i].ready:
|
||||
t.Logf("Consumer%d ready", i)
|
||||
case <-time.After(30 * time.Second):
|
||||
t.Fatalf("Timeout waiting for Consumer%d to be ready", i)
|
||||
}
|
||||
}
|
||||
|
||||
// Collect the settled assignment for each consumer. Because each consumer's
|
||||
// Consume() returns and re-enters on every rebalance, Setup fires multiple
|
||||
// times while the group converges and the handler's assignments channel
|
||||
// accumulates several snapshots. Drain it and keep the most recent value.
|
||||
assignments := make([][]int32, numConsumers)
|
||||
deadline := time.Now().Add(20 * time.Second)
|
||||
for i := 0; i < numConsumers; i++ {
|
||||
select {
|
||||
case partitions := <-handlers[i].assignments:
|
||||
assignments[i] = partitions
|
||||
case <-time.After(time.Until(deadline)):
|
||||
t.Errorf("Timeout waiting for Consumer%d assignment", i)
|
||||
continue
|
||||
}
|
||||
drain:
|
||||
for {
|
||||
select {
|
||||
case partitions := <-handlers[i].assignments:
|
||||
assignments[i] = partitions
|
||||
case <-time.After(3 * time.Second):
|
||||
break drain
|
||||
}
|
||||
}
|
||||
t.Logf("Consumer%d final assignment: %v", i, assignments[i])
|
||||
}
|
||||
|
||||
// Verify all partitions are assigned exactly once
|
||||
assignedPartitions := make(map[int32]int)
|
||||
totalAssigned := 0
|
||||
for i, assignment := range assignments {
|
||||
totalAssigned += len(assignment)
|
||||
for _, partition := range assignment {
|
||||
assignedPartitions[partition]++
|
||||
if assignedPartitions[partition] > 1 {
|
||||
t.Errorf("Partition %d assigned to multiple consumers", partition)
|
||||
}
|
||||
}
|
||||
|
||||
// Each consumer should get exactly 1 partition (4 partitions / 4 consumers)
|
||||
if len(assignment) != 1 {
|
||||
t.Errorf("Consumer%d should get exactly 1 partition, got %d", i, len(assignment))
|
||||
}
|
||||
}
|
||||
|
||||
if totalAssigned != 4 {
|
||||
t.Errorf("Expected 4 total partitions assigned, got %d", totalAssigned)
|
||||
}
|
||||
|
||||
// Verify all partitions 0-3 are assigned
|
||||
for i := int32(0); i < 4; i++ {
|
||||
if assignedPartitions[i] != 1 {
|
||||
t.Errorf("Partition %d assigned %d times, expected 1", i, assignedPartitions[i])
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Multiple consumers join test completed successfully")
|
||||
}
|
||||
|
||||
// RebalanceTestHandler implements sarama.ConsumerGroupHandler with rebalancing awareness
|
||||
type RebalanceTestHandler struct {
|
||||
messages chan *sarama.ConsumerMessage
|
||||
ready chan bool
|
||||
assignments chan []int32
|
||||
readyOnce sync.Once
|
||||
t *testing.T
|
||||
name string
|
||||
}
|
||||
|
||||
func (h *RebalanceTestHandler) Setup(session sarama.ConsumerGroupSession) error {
|
||||
h.t.Logf("%s: Consumer group session setup", h.name)
|
||||
h.readyOnce.Do(func() {
|
||||
close(h.ready)
|
||||
})
|
||||
|
||||
// Send partition assignment
|
||||
partitions := make([]int32, 0)
|
||||
for topic, partitionList := range session.Claims() {
|
||||
h.t.Logf("%s: Assigned topic %s with partitions %v", h.name, topic, partitionList)
|
||||
for _, partition := range partitionList {
|
||||
partitions = append(partitions, partition)
|
||||
}
|
||||
}
|
||||
|
||||
// Always publish the latest assignment. If the channel is full, drain one
|
||||
// stale value first so readers see the most recent rebalance outcome (not
|
||||
// whatever arrived first while the group was still converging).
|
||||
for {
|
||||
select {
|
||||
case h.assignments <- partitions:
|
||||
return nil
|
||||
default:
|
||||
select {
|
||||
case <-h.assignments:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *RebalanceTestHandler) Cleanup(sarama.ConsumerGroupSession) error {
|
||||
h.t.Logf("%s: Consumer group session cleanup", h.name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *RebalanceTestHandler) ConsumeClaim(session sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error {
|
||||
for {
|
||||
select {
|
||||
case message := <-claim.Messages():
|
||||
if message == nil {
|
||||
return nil
|
||||
}
|
||||
h.t.Logf("%s: Received message from partition %d: %s", h.name, message.Partition, string(message.Value))
|
||||
select {
|
||||
case h.messages <- message:
|
||||
default:
|
||||
// Channel full, drop message for test
|
||||
}
|
||||
session.MarkMessage(message, "")
|
||||
case <-session.Context().Done():
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
298
test/kafka/integration/schema_end_to_end_test.go
Normal file
298
test/kafka/integration/schema_end_to_end_test.go
Normal file
@@ -0,0 +1,298 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/linkedin/goavro/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/mq/kafka/schema"
|
||||
)
|
||||
|
||||
// TestSchemaEndToEnd_AvroRoundTrip tests the complete Avro schema round-trip workflow
|
||||
func TestSchemaEndToEnd_AvroRoundTrip(t *testing.T) {
|
||||
// Create mock schema registry
|
||||
server := createMockSchemaRegistryForE2E(t)
|
||||
defer server.Close()
|
||||
|
||||
// Create schema manager
|
||||
config := schema.ManagerConfig{
|
||||
RegistryURL: server.URL,
|
||||
ValidationMode: schema.ValidationPermissive,
|
||||
}
|
||||
manager, err := schema.NewManager(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test data
|
||||
avroSchema := getUserAvroSchemaForE2E()
|
||||
testData := map[string]interface{}{
|
||||
"id": int32(12345),
|
||||
"name": "Alice Johnson",
|
||||
"email": map[string]interface{}{"string": "alice@example.com"}, // Avro union
|
||||
"age": map[string]interface{}{"int": int32(28)}, // Avro union
|
||||
"preferences": map[string]interface{}{
|
||||
"Preferences": map[string]interface{}{ // Avro union with record type
|
||||
"notifications": true,
|
||||
"theme": "dark",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("SchemaManagerRoundTrip", func(t *testing.T) {
|
||||
// Step 1: Create Confluent envelope (simulate producer)
|
||||
codec, err := goavro.NewCodec(avroSchema)
|
||||
require.NoError(t, err)
|
||||
|
||||
avroBinary, err := codec.BinaryFromNative(nil, testData)
|
||||
require.NoError(t, err)
|
||||
|
||||
confluentMsg := schema.CreateConfluentEnvelope(schema.FormatAvro, 1, nil, avroBinary)
|
||||
require.True(t, len(confluentMsg) > 0, "Confluent envelope should not be empty")
|
||||
|
||||
t.Logf("Created Confluent envelope: %d bytes", len(confluentMsg))
|
||||
|
||||
// Step 2: Decode message using schema manager
|
||||
decodedMsg, err := manager.DecodeMessage(confluentMsg)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, decodedMsg.RecordValue, "RecordValue should not be nil")
|
||||
|
||||
t.Logf("Decoded message with schema ID %d, format %v", decodedMsg.SchemaID, decodedMsg.SchemaFormat)
|
||||
|
||||
// Step 3: Re-encode message using schema manager
|
||||
reconstructedMsg, err := manager.EncodeMessage(decodedMsg.RecordValue, 1, schema.FormatAvro)
|
||||
require.NoError(t, err)
|
||||
require.True(t, len(reconstructedMsg) > 0, "Reconstructed message should not be empty")
|
||||
|
||||
t.Logf("Re-encoded message: %d bytes", len(reconstructedMsg))
|
||||
|
||||
// Step 4: Verify the reconstructed message is a valid Confluent envelope
|
||||
envelope, ok := schema.ParseConfluentEnvelope(reconstructedMsg)
|
||||
require.True(t, ok, "Reconstructed message should be a valid Confluent envelope")
|
||||
require.Equal(t, uint32(1), envelope.SchemaID, "Schema ID should match")
|
||||
require.Equal(t, schema.FormatAvro, envelope.Format, "Schema format should be Avro")
|
||||
|
||||
// Step 5: Decode and verify the content
|
||||
decodedNative, _, err := codec.NativeFromBinary(envelope.Payload)
|
||||
require.NoError(t, err)
|
||||
|
||||
decodedMap, ok := decodedNative.(map[string]interface{})
|
||||
require.True(t, ok, "Decoded data should be a map")
|
||||
|
||||
// Verify all fields
|
||||
assert.Equal(t, int32(12345), decodedMap["id"])
|
||||
assert.Equal(t, "Alice Johnson", decodedMap["name"])
|
||||
|
||||
// Verify union fields
|
||||
emailUnion, ok := decodedMap["email"].(map[string]interface{})
|
||||
require.True(t, ok, "Email should be a union")
|
||||
assert.Equal(t, "alice@example.com", emailUnion["string"])
|
||||
|
||||
ageUnion, ok := decodedMap["age"].(map[string]interface{})
|
||||
require.True(t, ok, "Age should be a union")
|
||||
assert.Equal(t, int32(28), ageUnion["int"])
|
||||
|
||||
preferencesUnion, ok := decodedMap["preferences"].(map[string]interface{})
|
||||
require.True(t, ok, "Preferences should be a union")
|
||||
preferencesRecord, ok := preferencesUnion["Preferences"].(map[string]interface{})
|
||||
require.True(t, ok, "Preferences should contain a record")
|
||||
assert.Equal(t, true, preferencesRecord["notifications"])
|
||||
assert.Equal(t, "dark", preferencesRecord["theme"])
|
||||
|
||||
t.Log("Successfully completed Avro schema round-trip test")
|
||||
})
|
||||
}
|
||||
|
||||
// TestSchemaEndToEnd_ProtobufRoundTrip tests the complete Protobuf schema round-trip workflow
|
||||
func TestSchemaEndToEnd_ProtobufRoundTrip(t *testing.T) {
|
||||
t.Run("ProtobufEnvelopeCreation", func(t *testing.T) {
|
||||
// Create a simple Protobuf message (simulated)
|
||||
// In a real scenario, this would be generated from a .proto file
|
||||
protobufData := []byte{0x08, 0x96, 0x01, 0x12, 0x04, 0x74, 0x65, 0x73, 0x74} // id=150, name="test"
|
||||
|
||||
// Create Confluent envelope with Protobuf format
|
||||
confluentMsg := schema.CreateConfluentEnvelope(schema.FormatProtobuf, 2, []int{0}, protobufData)
|
||||
require.True(t, len(confluentMsg) > 0, "Confluent envelope should not be empty")
|
||||
|
||||
t.Logf("Created Protobuf Confluent envelope: %d bytes", len(confluentMsg))
|
||||
|
||||
// Verify Confluent envelope
|
||||
envelope, ok := schema.ParseConfluentEnvelope(confluentMsg)
|
||||
require.True(t, ok, "Message should be a valid Confluent envelope")
|
||||
require.Equal(t, uint32(2), envelope.SchemaID, "Schema ID should match")
|
||||
// Note: ParseConfluentEnvelope defaults to FormatAvro; format detection requires schema registry
|
||||
require.Equal(t, schema.FormatAvro, envelope.Format, "Format defaults to Avro without schema registry lookup")
|
||||
|
||||
// For Protobuf with indexes, we need to use the specialized parser
|
||||
protobufEnvelope, ok := schema.ParseConfluentProtobufEnvelopeWithIndexCount(confluentMsg, 1)
|
||||
require.True(t, ok, "Message should be a valid Protobuf envelope")
|
||||
require.Equal(t, uint32(2), protobufEnvelope.SchemaID, "Schema ID should match")
|
||||
require.Equal(t, schema.FormatProtobuf, protobufEnvelope.Format, "Schema format should be Protobuf")
|
||||
require.Equal(t, []int{0}, protobufEnvelope.Indexes, "Indexes should match")
|
||||
require.Equal(t, protobufData, protobufEnvelope.Payload, "Payload should match")
|
||||
|
||||
t.Log("Successfully completed Protobuf envelope test")
|
||||
})
|
||||
}
|
||||
|
||||
// TestSchemaEndToEnd_JSONSchemaRoundTrip tests the complete JSON Schema round-trip workflow
|
||||
func TestSchemaEndToEnd_JSONSchemaRoundTrip(t *testing.T) {
|
||||
t.Run("JSONSchemaEnvelopeCreation", func(t *testing.T) {
|
||||
// Create JSON data
|
||||
jsonData := []byte(`{"id": 123, "name": "Bob Smith", "active": true}`)
|
||||
|
||||
// Create Confluent envelope with JSON Schema format
|
||||
confluentMsg := schema.CreateConfluentEnvelope(schema.FormatJSONSchema, 3, nil, jsonData)
|
||||
require.True(t, len(confluentMsg) > 0, "Confluent envelope should not be empty")
|
||||
|
||||
t.Logf("Created JSON Schema Confluent envelope: %d bytes", len(confluentMsg))
|
||||
|
||||
// Verify Confluent envelope
|
||||
envelope, ok := schema.ParseConfluentEnvelope(confluentMsg)
|
||||
require.True(t, ok, "Message should be a valid Confluent envelope")
|
||||
require.Equal(t, uint32(3), envelope.SchemaID, "Schema ID should match")
|
||||
// Note: ParseConfluentEnvelope defaults to FormatAvro; format detection requires schema registry
|
||||
require.Equal(t, schema.FormatAvro, envelope.Format, "Format defaults to Avro without schema registry lookup")
|
||||
|
||||
// Verify JSON content
|
||||
assert.JSONEq(t, string(jsonData), string(envelope.Payload), "JSON payload should match")
|
||||
|
||||
t.Log("Successfully completed JSON Schema envelope test")
|
||||
})
|
||||
}
|
||||
|
||||
// TestSchemaEndToEnd_CompressionAndBatching tests schema handling with compression and batching
|
||||
func TestSchemaEndToEnd_CompressionAndBatching(t *testing.T) {
|
||||
// Create mock schema registry
|
||||
server := createMockSchemaRegistryForE2E(t)
|
||||
defer server.Close()
|
||||
|
||||
// Create schema manager
|
||||
config := schema.ManagerConfig{
|
||||
RegistryURL: server.URL,
|
||||
ValidationMode: schema.ValidationPermissive,
|
||||
}
|
||||
manager, err := schema.NewManager(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("BatchedSchematizedMessages", func(t *testing.T) {
|
||||
// Create multiple messages
|
||||
avroSchema := getUserAvroSchemaForE2E()
|
||||
codec, err := goavro.NewCodec(avroSchema)
|
||||
require.NoError(t, err)
|
||||
|
||||
messageCount := 5
|
||||
var confluentMessages [][]byte
|
||||
|
||||
// Create multiple Confluent envelopes
|
||||
for i := 0; i < messageCount; i++ {
|
||||
testData := map[string]interface{}{
|
||||
"id": int32(1000 + i),
|
||||
"name": fmt.Sprintf("User %d", i),
|
||||
"email": map[string]interface{}{"string": fmt.Sprintf("user%d@example.com", i)},
|
||||
"age": map[string]interface{}{"int": int32(20 + i)},
|
||||
"preferences": map[string]interface{}{
|
||||
"Preferences": map[string]interface{}{
|
||||
"notifications": i%2 == 0, // Alternate true/false
|
||||
"theme": "light",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
avroBinary, err := codec.BinaryFromNative(nil, testData)
|
||||
require.NoError(t, err)
|
||||
|
||||
confluentMsg := schema.CreateConfluentEnvelope(schema.FormatAvro, 1, nil, avroBinary)
|
||||
confluentMessages = append(confluentMessages, confluentMsg)
|
||||
}
|
||||
|
||||
t.Logf("Created %d schematized messages", messageCount)
|
||||
|
||||
// Test round-trip for each message
|
||||
for i, confluentMsg := range confluentMessages {
|
||||
// Decode message
|
||||
decodedMsg, err := manager.DecodeMessage(confluentMsg)
|
||||
require.NoError(t, err, "Message %d should decode", i)
|
||||
|
||||
// Re-encode message
|
||||
reconstructedMsg, err := manager.EncodeMessage(decodedMsg.RecordValue, 1, schema.FormatAvro)
|
||||
require.NoError(t, err, "Message %d should re-encode", i)
|
||||
|
||||
// Verify envelope
|
||||
envelope, ok := schema.ParseConfluentEnvelope(reconstructedMsg)
|
||||
require.True(t, ok, "Message %d should be a valid Confluent envelope", i)
|
||||
require.Equal(t, uint32(1), envelope.SchemaID, "Message %d schema ID should match", i)
|
||||
|
||||
// Decode and verify content
|
||||
decodedNative, _, err := codec.NativeFromBinary(envelope.Payload)
|
||||
require.NoError(t, err, "Message %d should decode successfully", i)
|
||||
|
||||
decodedMap, ok := decodedNative.(map[string]interface{})
|
||||
require.True(t, ok, "Message %d should be a map", i)
|
||||
|
||||
expectedID := int32(1000 + i)
|
||||
assert.Equal(t, expectedID, decodedMap["id"], "Message %d ID should match", i)
|
||||
assert.Equal(t, fmt.Sprintf("User %d", i), decodedMap["name"], "Message %d name should match", i)
|
||||
}
|
||||
|
||||
t.Log("Successfully verified batched schematized messages")
|
||||
})
|
||||
}
|
||||
|
||||
// Helper functions for creating mock schema registries
|
||||
|
||||
func createMockSchemaRegistryForE2E(t *testing.T) *httptest.Server {
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/schemas/ids/1":
|
||||
response := map[string]interface{}{
|
||||
"schema": getUserAvroSchemaForE2E(),
|
||||
"subject": "user-events-e2e-value",
|
||||
"version": 1,
|
||||
}
|
||||
writeJSONResponse(w, response)
|
||||
case "/subjects/user-events-e2e-value/versions/latest":
|
||||
response := map[string]interface{}{
|
||||
"id": 1,
|
||||
"schema": getUserAvroSchemaForE2E(),
|
||||
"subject": "user-events-e2e-value",
|
||||
"version": 1,
|
||||
}
|
||||
writeJSONResponse(w, response)
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
func getUserAvroSchemaForE2E() string {
|
||||
return `{
|
||||
"type": "record",
|
||||
"name": "User",
|
||||
"fields": [
|
||||
{"name": "id", "type": "int"},
|
||||
{"name": "name", "type": "string"},
|
||||
{"name": "email", "type": ["null", "string"], "default": null},
|
||||
{"name": "age", "type": ["null", "int"], "default": null},
|
||||
{"name": "preferences", "type": ["null", {
|
||||
"type": "record",
|
||||
"name": "Preferences",
|
||||
"fields": [
|
||||
{"name": "notifications", "type": "boolean", "default": true},
|
||||
{"name": "theme", "type": "string", "default": "light"}
|
||||
]
|
||||
}], "default": null}
|
||||
]
|
||||
}`
|
||||
}
|
||||
|
||||
func writeJSONResponse(w http.ResponseWriter, data interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(data); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
210
test/kafka/integration/schema_registry_test.go
Normal file
210
test/kafka/integration/schema_registry_test.go
Normal file
@@ -0,0 +1,210 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/test/kafka/internal/testutil"
|
||||
)
|
||||
|
||||
// TestSchemaRegistryEventualConsistency reproduces the issue where schemas
|
||||
// are registered successfully but are not immediately queryable due to
|
||||
// Schema Registry's consumer lag
|
||||
func TestSchemaRegistryEventualConsistency(t *testing.T) {
|
||||
// This test requires real SMQ backend
|
||||
gateway := testutil.NewGatewayTestServerWithSMQ(t, testutil.SMQRequired)
|
||||
defer gateway.CleanupAndClose()
|
||||
|
||||
addr := gateway.StartAndWait()
|
||||
t.Logf("Gateway running on %s", addr)
|
||||
|
||||
// Schema Registry URL from environment or default
|
||||
schemaRegistryURL := "http://localhost:8081"
|
||||
|
||||
// Wait for Schema Registry to be ready
|
||||
if !waitForSchemaRegistry(t, schemaRegistryURL, 30*time.Second) {
|
||||
t.Fatal("Schema Registry not ready")
|
||||
}
|
||||
|
||||
// Define test schemas
|
||||
valueSchema := `{"type":"record","name":"TestMessage","fields":[{"name":"id","type":"string"}]}`
|
||||
keySchema := `{"type":"string"}`
|
||||
|
||||
// Register multiple schemas rapidly (simulates the load test scenario)
|
||||
subjects := []string{
|
||||
"test-topic-0-value",
|
||||
"test-topic-0-key",
|
||||
"test-topic-1-value",
|
||||
"test-topic-1-key",
|
||||
"test-topic-2-value",
|
||||
"test-topic-2-key",
|
||||
"test-topic-3-value",
|
||||
"test-topic-3-key",
|
||||
}
|
||||
|
||||
t.Log("Registering schemas rapidly...")
|
||||
registeredIDs := make(map[string]int)
|
||||
for _, subject := range subjects {
|
||||
schema := valueSchema
|
||||
if strings.HasSuffix(subject, "-key") {
|
||||
schema = keySchema
|
||||
}
|
||||
|
||||
id, err := registerSchema(schemaRegistryURL, subject, schema)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to register schema for %s: %v", subject, err)
|
||||
}
|
||||
registeredIDs[subject] = id
|
||||
t.Logf("Registered %s with ID %d", subject, id)
|
||||
}
|
||||
|
||||
t.Log("All schemas registered successfully!")
|
||||
|
||||
// Now immediately try to verify them (this reproduces the bug)
|
||||
t.Log("Immediately verifying schemas (without delay)...")
|
||||
immediateFailures := 0
|
||||
for _, subject := range subjects {
|
||||
exists, id, version, err := verifySchema(schemaRegistryURL, subject)
|
||||
if err != nil || !exists {
|
||||
immediateFailures++
|
||||
t.Logf("Immediate verification failed for %s: exists=%v id=%d err=%v", subject, exists, id, err)
|
||||
} else {
|
||||
t.Logf("Immediate verification passed for %s: ID=%d Version=%d", subject, id, version)
|
||||
}
|
||||
}
|
||||
|
||||
if immediateFailures > 0 {
|
||||
t.Logf("BUG REPRODUCED: %d/%d schemas not immediately queryable after registration",
|
||||
immediateFailures, len(subjects))
|
||||
t.Logf(" This is due to Schema Registry's KafkaStoreReaderThread lag")
|
||||
}
|
||||
|
||||
// Now verify with retry logic (this should succeed)
|
||||
t.Log("Verifying schemas with retry logic...")
|
||||
for _, subject := range subjects {
|
||||
expectedID := registeredIDs[subject]
|
||||
if !verifySchemaWithRetry(t, schemaRegistryURL, subject, expectedID, 5*time.Second) {
|
||||
t.Errorf("Failed to verify %s even with retry", subject)
|
||||
}
|
||||
}
|
||||
|
||||
t.Log("✓ All schemas verified successfully with retry logic!")
|
||||
}
|
||||
|
||||
// registerSchema registers a schema and returns its ID
|
||||
func registerSchema(registryURL, subject, schema string) (int, error) {
|
||||
// Escape the schema JSON
|
||||
escapedSchema, err := json.Marshal(schema)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
payload := fmt.Sprintf(`{"schema":%s,"schemaType":"AVRO"}`, escapedSchema)
|
||||
|
||||
resp, err := http.Post(
|
||||
fmt.Sprintf("%s/subjects/%s/versions", registryURL, subject),
|
||||
"application/vnd.schemaregistry.v1+json",
|
||||
strings.NewReader(payload),
|
||||
)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return 0, fmt.Errorf("registration failed: %s - %s", resp.Status, string(body))
|
||||
}
|
||||
|
||||
var result struct {
|
||||
ID int `json:"id"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return result.ID, nil
|
||||
}
|
||||
|
||||
// verifySchema checks if a schema exists
|
||||
func verifySchema(registryURL, subject string) (exists bool, id int, version int, err error) {
|
||||
resp, err := http.Get(fmt.Sprintf("%s/subjects/%s/versions/latest", registryURL, subject))
|
||||
if err != nil {
|
||||
return false, 0, 0, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return false, 0, 0, nil
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return false, 0, 0, fmt.Errorf("verification failed: %s - %s", resp.Status, string(body))
|
||||
}
|
||||
|
||||
var result struct {
|
||||
ID int `json:"id"`
|
||||
Version int `json:"version"`
|
||||
Schema string `json:"schema"`
|
||||
}
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return false, 0, 0, err
|
||||
}
|
||||
|
||||
return true, result.ID, result.Version, nil
|
||||
}
|
||||
|
||||
// verifySchemaWithRetry verifies a schema with retry logic
|
||||
func verifySchemaWithRetry(t *testing.T, registryURL, subject string, expectedID int, timeout time.Duration) bool {
|
||||
deadline := time.Now().Add(timeout)
|
||||
attempt := 0
|
||||
|
||||
for time.Now().Before(deadline) {
|
||||
attempt++
|
||||
exists, id, version, err := verifySchema(registryURL, subject)
|
||||
|
||||
if err == nil && exists && id == expectedID {
|
||||
if attempt > 1 {
|
||||
t.Logf("✓ %s verified after %d attempts (ID=%d, Version=%d)", subject, attempt, id, version)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Wait before retry (exponential backoff)
|
||||
waitTime := time.Duration(attempt*100) * time.Millisecond
|
||||
if waitTime > 1*time.Second {
|
||||
waitTime = 1 * time.Second
|
||||
}
|
||||
time.Sleep(waitTime)
|
||||
}
|
||||
|
||||
t.Logf("%s verification timed out after %d attempts", subject, attempt)
|
||||
return false
|
||||
}
|
||||
|
||||
// waitForSchemaRegistry waits for Schema Registry to be ready
|
||||
func waitForSchemaRegistry(t *testing.T, url string, timeout time.Duration) bool {
|
||||
deadline := time.Now().Add(timeout)
|
||||
|
||||
for time.Now().Before(deadline) {
|
||||
resp, err := http.Get(url + "/subjects")
|
||||
if err == nil && resp.StatusCode == http.StatusOK {
|
||||
resp.Body.Close()
|
||||
return true
|
||||
}
|
||||
if resp != nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
305
test/kafka/integration/smq_integration_test.go
Normal file
305
test/kafka/integration/smq_integration_test.go
Normal file
@@ -0,0 +1,305 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/IBM/sarama"
|
||||
"github.com/seaweedfs/seaweedfs/test/kafka/internal/testutil"
|
||||
)
|
||||
|
||||
// TestSMQIntegration tests that the Kafka gateway properly integrates with SeaweedMQ
|
||||
// This test REQUIRES SeaweedFS masters to be running and will skip if not available
|
||||
func TestSMQIntegration(t *testing.T) {
|
||||
// This test requires SMQ to be available
|
||||
gateway := testutil.NewGatewayTestServerWithSMQ(t, testutil.SMQRequired)
|
||||
defer gateway.CleanupAndClose()
|
||||
|
||||
addr := gateway.StartAndWait()
|
||||
|
||||
t.Logf("Running SMQ integration test with SeaweedFS backend")
|
||||
|
||||
t.Run("ProduceConsumeWithPersistence", func(t *testing.T) {
|
||||
testProduceConsumeWithPersistence(t, addr)
|
||||
})
|
||||
|
||||
t.Run("ConsumerGroupOffsetPersistence", func(t *testing.T) {
|
||||
testConsumerGroupOffsetPersistence(t, addr)
|
||||
})
|
||||
|
||||
t.Run("TopicPersistence", func(t *testing.T) {
|
||||
testTopicPersistence(t, addr)
|
||||
})
|
||||
}
|
||||
|
||||
func testProduceConsumeWithPersistence(t *testing.T, addr string) {
|
||||
topicName := testutil.GenerateUniqueTopicName("smq-integration-produce-consume")
|
||||
|
||||
client := testutil.NewSaramaClient(t, addr)
|
||||
msgGen := testutil.NewMessageGenerator()
|
||||
|
||||
// Create topic
|
||||
err := client.CreateTopic(topicName, 1, 1)
|
||||
testutil.AssertNoError(t, err, "Failed to create topic")
|
||||
|
||||
// Allow time for topic to propagate in SMQ backend
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
// Produce messages
|
||||
messages := msgGen.GenerateStringMessages(5)
|
||||
err = client.ProduceMessages(topicName, messages)
|
||||
testutil.AssertNoError(t, err, "Failed to produce messages")
|
||||
|
||||
// Allow time for messages to be fully persisted in SMQ backend
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
t.Logf("Produced %d messages to topic %s", len(messages), topicName)
|
||||
|
||||
// Consume messages
|
||||
consumed, err := client.ConsumeMessages(topicName, 0, len(messages))
|
||||
testutil.AssertNoError(t, err, "Failed to consume messages")
|
||||
|
||||
// Verify all messages were consumed
|
||||
testutil.AssertEqual(t, len(messages), len(consumed), "Message count mismatch")
|
||||
|
||||
t.Logf("Successfully consumed %d messages from SMQ backend", len(consumed))
|
||||
}
|
||||
|
||||
func testConsumerGroupOffsetPersistence(t *testing.T, addr string) {
|
||||
topicName := testutil.GenerateUniqueTopicName("smq-integration-offset-persistence")
|
||||
groupID := testutil.GenerateUniqueGroupID("smq-offset-group")
|
||||
|
||||
client := testutil.NewSaramaClient(t, addr)
|
||||
msgGen := testutil.NewMessageGenerator()
|
||||
|
||||
// Create topic and produce messages
|
||||
err := client.CreateTopic(topicName, 1, 1)
|
||||
testutil.AssertNoError(t, err, "Failed to create topic")
|
||||
|
||||
// Allow time for topic to propagate in SMQ backend
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
messages := msgGen.GenerateStringMessages(10)
|
||||
err = client.ProduceMessages(topicName, messages)
|
||||
testutil.AssertNoError(t, err, "Failed to produce messages")
|
||||
|
||||
// Allow time for messages to be fully persisted in SMQ backend
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// Phase 1: Consume first 5 messages with consumer group and commit offsets
|
||||
t.Logf("Phase 1: Consuming first 5 messages and committing offsets")
|
||||
|
||||
config := client.GetConfig()
|
||||
config.Consumer.Offsets.Initial = sarama.OffsetOldest
|
||||
// Enable auto-commit for more reliable offset handling
|
||||
config.Consumer.Offsets.AutoCommit.Enable = true
|
||||
config.Consumer.Offsets.AutoCommit.Interval = 1 * time.Second
|
||||
|
||||
consumerGroup1, err := sarama.NewConsumerGroup([]string{addr}, groupID, config)
|
||||
testutil.AssertNoError(t, err, "Failed to create first consumer group")
|
||||
|
||||
handler := &SMQOffsetTestHandler{
|
||||
messages: make(chan *sarama.ConsumerMessage, len(messages)),
|
||||
ready: make(chan bool),
|
||||
stopAfter: 5,
|
||||
t: t,
|
||||
}
|
||||
|
||||
ctx1, cancel1 := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel1()
|
||||
|
||||
consumeErrChan1 := make(chan error, 1)
|
||||
go func() {
|
||||
err := consumerGroup1.Consume(ctx1, []string{topicName}, handler)
|
||||
if err != nil && err != context.DeadlineExceeded && err != context.Canceled {
|
||||
t.Logf("First consumer error: %v", err)
|
||||
consumeErrChan1 <- err
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for consumer to be ready with timeout
|
||||
select {
|
||||
case <-handler.ready:
|
||||
// Consumer is ready, continue
|
||||
case err := <-consumeErrChan1:
|
||||
t.Fatalf("First consumer failed to start: %v", err)
|
||||
case <-time.After(10 * time.Second):
|
||||
t.Fatalf("Timeout waiting for first consumer to be ready")
|
||||
}
|
||||
consumedCount := 0
|
||||
for consumedCount < 5 {
|
||||
select {
|
||||
case <-handler.messages:
|
||||
consumedCount++
|
||||
case <-time.After(20 * time.Second):
|
||||
t.Fatalf("Timeout waiting for first batch of messages. Got %d/5", consumedCount)
|
||||
}
|
||||
}
|
||||
|
||||
consumerGroup1.Close()
|
||||
cancel1()
|
||||
time.Sleep(7 * time.Second) // Allow auto-commit to complete and offset commits to be processed in SMQ
|
||||
|
||||
t.Logf("Consumed %d messages in first phase", consumedCount)
|
||||
|
||||
// Phase 2: Start new consumer group with same ID - should resume from committed offset
|
||||
t.Logf("Phase 2: Starting new consumer group to test offset persistence")
|
||||
|
||||
// Create a fresh config for the second consumer group to avoid any state issues
|
||||
config2 := client.GetConfig()
|
||||
config2.Consumer.Offsets.Initial = sarama.OffsetOldest
|
||||
config2.Consumer.Offsets.AutoCommit.Enable = true
|
||||
config2.Consumer.Offsets.AutoCommit.Interval = 1 * time.Second
|
||||
|
||||
consumerGroup2, err := sarama.NewConsumerGroup([]string{addr}, groupID, config2)
|
||||
testutil.AssertNoError(t, err, "Failed to create second consumer group")
|
||||
defer consumerGroup2.Close()
|
||||
|
||||
handler2 := &SMQOffsetTestHandler{
|
||||
messages: make(chan *sarama.ConsumerMessage, len(messages)),
|
||||
ready: make(chan bool),
|
||||
stopAfter: 5, // Should consume remaining 5 messages
|
||||
t: t,
|
||||
}
|
||||
|
||||
ctx2, cancel2 := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel2()
|
||||
|
||||
consumeErrChan := make(chan error, 1)
|
||||
go func() {
|
||||
err := consumerGroup2.Consume(ctx2, []string{topicName}, handler2)
|
||||
if err != nil && err != context.DeadlineExceeded && err != context.Canceled {
|
||||
t.Logf("Second consumer error: %v", err)
|
||||
consumeErrChan <- err
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for second consumer to be ready with timeout
|
||||
select {
|
||||
case <-handler2.ready:
|
||||
// Consumer is ready, continue
|
||||
case err := <-consumeErrChan:
|
||||
t.Fatalf("Second consumer failed to start: %v", err)
|
||||
case <-time.After(10 * time.Second):
|
||||
t.Fatalf("Timeout waiting for second consumer to be ready")
|
||||
}
|
||||
secondConsumerMessages := make([]*sarama.ConsumerMessage, 0)
|
||||
consumedCount = 0
|
||||
for consumedCount < 5 {
|
||||
select {
|
||||
case msg := <-handler2.messages:
|
||||
consumedCount++
|
||||
secondConsumerMessages = append(secondConsumerMessages, msg)
|
||||
case <-time.After(20 * time.Second):
|
||||
t.Fatalf("Timeout waiting for second batch of messages. Got %d/5", consumedCount)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify second consumer started from correct offset (should be >= 5)
|
||||
if len(secondConsumerMessages) > 0 {
|
||||
firstMessageOffset := secondConsumerMessages[0].Offset
|
||||
if firstMessageOffset < 5 {
|
||||
t.Fatalf("Second consumer should start from offset >= 5: got %d", firstMessageOffset)
|
||||
}
|
||||
t.Logf("Second consumer correctly resumed from offset %d", firstMessageOffset)
|
||||
}
|
||||
|
||||
t.Logf("Successfully verified SMQ offset persistence")
|
||||
}
|
||||
|
||||
func testTopicPersistence(t *testing.T, addr string) {
|
||||
topicName := testutil.GenerateUniqueTopicName("smq-integration-topic-persistence")
|
||||
|
||||
client := testutil.NewSaramaClient(t, addr)
|
||||
|
||||
// Create topic
|
||||
err := client.CreateTopic(topicName, 2, 1) // 2 partitions
|
||||
testutil.AssertNoError(t, err, "Failed to create topic")
|
||||
|
||||
// Allow time for topic to propagate and persist in SMQ backend
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
// Verify topic exists by listing topics using admin client
|
||||
config := client.GetConfig()
|
||||
config.Admin.Timeout = 30 * time.Second
|
||||
|
||||
admin, err := sarama.NewClusterAdmin([]string{addr}, config)
|
||||
testutil.AssertNoError(t, err, "Failed to create admin client")
|
||||
defer admin.Close()
|
||||
|
||||
// Retry topic listing to handle potential delays in topic propagation
|
||||
var topics map[string]sarama.TopicDetail
|
||||
var listErr error
|
||||
for attempt := 0; attempt < 3; attempt++ {
|
||||
if attempt > 0 {
|
||||
sleepDuration := time.Duration(500*(1<<(attempt-1))) * time.Millisecond
|
||||
t.Logf("Retrying ListTopics after %v (attempt %d/3)", sleepDuration, attempt+1)
|
||||
time.Sleep(sleepDuration)
|
||||
}
|
||||
|
||||
topics, listErr = admin.ListTopics()
|
||||
if listErr == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
testutil.AssertNoError(t, listErr, "Failed to list topics")
|
||||
|
||||
topicDetails, exists := topics[topicName]
|
||||
if !exists {
|
||||
t.Fatalf("Topic %s not found in topic list", topicName)
|
||||
}
|
||||
|
||||
if topicDetails.NumPartitions != 2 {
|
||||
t.Errorf("Expected 2 partitions, got %d", topicDetails.NumPartitions)
|
||||
}
|
||||
|
||||
t.Logf("Successfully verified topic persistence with %d partitions", topicDetails.NumPartitions)
|
||||
}
|
||||
|
||||
// SMQOffsetTestHandler implements sarama.ConsumerGroupHandler for SMQ offset testing
|
||||
type SMQOffsetTestHandler struct {
|
||||
messages chan *sarama.ConsumerMessage
|
||||
ready chan bool
|
||||
readyOnce bool
|
||||
stopAfter int
|
||||
consumed int
|
||||
t *testing.T
|
||||
}
|
||||
|
||||
func (h *SMQOffsetTestHandler) Setup(sarama.ConsumerGroupSession) error {
|
||||
h.t.Logf("SMQ offset test consumer setup")
|
||||
if !h.readyOnce {
|
||||
close(h.ready)
|
||||
h.readyOnce = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *SMQOffsetTestHandler) Cleanup(sarama.ConsumerGroupSession) error {
|
||||
h.t.Logf("SMQ offset test consumer cleanup")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *SMQOffsetTestHandler) ConsumeClaim(session sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error {
|
||||
for {
|
||||
select {
|
||||
case message := <-claim.Messages():
|
||||
if message == nil {
|
||||
return nil
|
||||
}
|
||||
h.consumed++
|
||||
h.messages <- message
|
||||
session.MarkMessage(message, "")
|
||||
|
||||
// Stop after consuming the specified number of messages
|
||||
if h.consumed >= h.stopAfter {
|
||||
h.t.Logf("Stopping SMQ consumer after %d messages", h.consumed)
|
||||
// Auto-commit will handle offset commits automatically
|
||||
return nil
|
||||
}
|
||||
case <-session.Context().Done():
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user