Browse Source
feat: enhance Fetch API with proper request parsing and record batch construction
feat: enhance Fetch API with proper request parsing and record batch construction
- Added comprehensive Fetch request parsing for different API versions - Implemented constructRecordBatchFromLedger to return actual messages - Added support for dynamic topic/partition handling in Fetch responses - Enhanced record batch format with proper Kafka v2 structure - Added varint encoding for record fields - Improved error handling and validation TODO: Debug consumer integration issues and test with actual message retrievalpull/7231/head
4 changed files with 1152 additions and 50 deletions
-
472test/kafka/comprehensive_e2e_test.go
-
186test/kafka/kafka_go_produce_only_test.go
-
110test/kafka/sarama_simple_test.go
-
434weed/mq/kafka/protocol/fetch.go
@ -0,0 +1,472 @@ |
|||
package kafka |
|||
|
|||
import ( |
|||
"context" |
|||
"fmt" |
|||
"strings" |
|||
"testing" |
|||
"time" |
|||
|
|||
"github.com/IBM/sarama" |
|||
"github.com/seaweedfs/seaweedfs/weed/mq/kafka/gateway" |
|||
"github.com/segmentio/kafka-go" |
|||
) |
|||
|
|||
// TestComprehensiveE2E tests both kafka-go and Sarama clients in a comprehensive scenario
|
|||
func TestComprehensiveE2E(t *testing.T) { |
|||
// Start gateway
|
|||
gatewayServer := gateway.NewServer(gateway.Options{ |
|||
Listen: "127.0.0.1:0", |
|||
UseSeaweedMQ: false, // Use in-memory mode for testing
|
|||
}) |
|||
|
|||
go func() { |
|||
if err := gatewayServer.Start(); err != nil { |
|||
t.Errorf("Failed to start gateway: %v", err) |
|||
} |
|||
}() |
|||
defer gatewayServer.Close() |
|||
|
|||
time.Sleep(100 * time.Millisecond) |
|||
|
|||
host, port := gatewayServer.GetListenerAddr() |
|||
addr := fmt.Sprintf("%s:%d", host, port) |
|||
t.Logf("Gateway running on %s", addr) |
|||
|
|||
// Create multiple topics for different test scenarios
|
|||
topics := []string{ |
|||
"e2e-kafka-go-topic", |
|||
"e2e-sarama-topic", |
|||
"e2e-mixed-topic", |
|||
} |
|||
|
|||
for _, topic := range topics { |
|||
gatewayServer.GetHandler().AddTopicForTesting(topic, 1) |
|||
t.Logf("Added topic: %s", topic) |
|||
} |
|||
|
|||
// Test 1: kafka-go producer -> kafka-go consumer
|
|||
t.Run("KafkaGo_to_KafkaGo", func(t *testing.T) { |
|||
testKafkaGoToKafkaGo(t, addr, topics[0]) |
|||
}) |
|||
|
|||
// Test 2: Sarama producer -> Sarama consumer
|
|||
t.Run("Sarama_to_Sarama", func(t *testing.T) { |
|||
testSaramaToSarama(t, addr, topics[1]) |
|||
}) |
|||
|
|||
// Test 3: Mixed clients - kafka-go producer -> Sarama consumer
|
|||
t.Run("KafkaGo_to_Sarama", func(t *testing.T) { |
|||
testKafkaGoToSarama(t, addr, topics[2]) |
|||
}) |
|||
|
|||
// Test 4: Mixed clients - Sarama producer -> kafka-go consumer
|
|||
t.Run("Sarama_to_KafkaGo", func(t *testing.T) { |
|||
testSaramaToKafkaGo(t, addr, topics[2]) |
|||
}) |
|||
} |
|||
|
|||
func testKafkaGoToKafkaGo(t *testing.T, addr, topic string) { |
|||
messages := []kafka.Message{ |
|||
{Key: []byte("kgo-key1"), Value: []byte("kafka-go to kafka-go message 1")}, |
|||
{Key: []byte("kgo-key2"), Value: []byte("kafka-go to kafka-go message 2")}, |
|||
} |
|||
|
|||
// Produce with kafka-go
|
|||
w := &kafka.Writer{ |
|||
Addr: kafka.TCP(addr), |
|||
Topic: topic, |
|||
Balancer: &kafka.LeastBytes{}, |
|||
BatchTimeout: 50 * time.Millisecond, |
|||
RequiredAcks: kafka.RequireOne, |
|||
} |
|||
defer w.Close() |
|||
|
|||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) |
|||
defer cancel() |
|||
|
|||
err := w.WriteMessages(ctx, messages...) |
|||
if err != nil { |
|||
t.Fatalf("kafka-go produce failed: %v", err) |
|||
} |
|||
t.Logf("✅ kafka-go produced %d messages", len(messages)) |
|||
|
|||
// Consume with kafka-go
|
|||
r := kafka.NewReader(kafka.ReaderConfig{ |
|||
Brokers: []string{addr}, |
|||
Topic: topic, |
|||
StartOffset: kafka.FirstOffset, |
|||
MinBytes: 1, |
|||
MaxBytes: 10e6, |
|||
}) |
|||
defer r.Close() |
|||
|
|||
consumeCtx, consumeCancel := context.WithTimeout(context.Background(), 10*time.Second) |
|||
defer consumeCancel() |
|||
|
|||
for i := 0; i < len(messages); i++ { |
|||
msg, err := r.ReadMessage(consumeCtx) |
|||
if err != nil { |
|||
t.Fatalf("kafka-go consume failed: %v", err) |
|||
} |
|||
t.Logf("✅ kafka-go consumed: key=%s, value=%s", string(msg.Key), string(msg.Value)) |
|||
|
|||
// Validate message
|
|||
expected := messages[i] |
|||
if string(msg.Key) != string(expected.Key) || string(msg.Value) != string(expected.Value) { |
|||
t.Errorf("Message mismatch: got key=%s value=%s, want key=%s value=%s", |
|||
string(msg.Key), string(msg.Value), string(expected.Key), string(expected.Value)) |
|||
} |
|||
} |
|||
|
|||
t.Logf("🎉 kafka-go to kafka-go test PASSED") |
|||
} |
|||
|
|||
func testSaramaToSarama(t *testing.T, addr, topic string) { |
|||
// Configure Sarama
|
|||
config := sarama.NewConfig() |
|||
config.Version = sarama.V0_11_0_0 |
|||
config.Producer.Return.Successes = true |
|||
config.Producer.RequiredAcks = sarama.WaitForAll |
|||
config.Consumer.Return.Errors = true |
|||
|
|||
messages := []string{ |
|||
"Sarama to Sarama message 1", |
|||
"Sarama to Sarama message 2", |
|||
} |
|||
|
|||
// Produce with Sarama
|
|||
producer, err := sarama.NewSyncProducer([]string{addr}, config) |
|||
if err != nil { |
|||
t.Fatalf("Failed to create Sarama producer: %v", err) |
|||
} |
|||
defer producer.Close() |
|||
|
|||
for i, msgText := range messages { |
|||
msg := &sarama.ProducerMessage{ |
|||
Topic: topic, |
|||
Key: sarama.StringEncoder(fmt.Sprintf("sarama-key-%d", i)), |
|||
Value: sarama.StringEncoder(msgText), |
|||
} |
|||
|
|||
partition, offset, err := producer.SendMessage(msg) |
|||
if err != nil { |
|||
t.Fatalf("Sarama produce failed: %v", err) |
|||
} |
|||
t.Logf("✅ Sarama produced message %d: partition=%d, offset=%d", i, partition, offset) |
|||
} |
|||
|
|||
// Consume with Sarama
|
|||
consumer, err := sarama.NewConsumer([]string{addr}, config) |
|||
if err != nil { |
|||
t.Fatalf("Failed to create Sarama consumer: %v", err) |
|||
} |
|||
defer consumer.Close() |
|||
|
|||
partitionConsumer, err := consumer.ConsumePartition(topic, 0, sarama.OffsetOldest) |
|||
if err != nil { |
|||
t.Fatalf("Failed to create Sarama partition consumer: %v", err) |
|||
} |
|||
defer partitionConsumer.Close() |
|||
|
|||
consumedCount := 0 |
|||
timeout := time.After(10 * time.Second) |
|||
|
|||
for consumedCount < len(messages) { |
|||
select { |
|||
case msg := <-partitionConsumer.Messages(): |
|||
t.Logf("✅ Sarama consumed: key=%s, value=%s, offset=%d", |
|||
string(msg.Key), string(msg.Value), msg.Offset) |
|||
|
|||
expectedValue := messages[consumedCount] |
|||
if string(msg.Value) != expectedValue { |
|||
t.Errorf("Message mismatch: got %s, want %s", string(msg.Value), expectedValue) |
|||
} |
|||
consumedCount++ |
|||
|
|||
case err := <-partitionConsumer.Errors(): |
|||
t.Fatalf("Sarama consumer error: %v", err) |
|||
|
|||
case <-timeout: |
|||
t.Fatalf("Timeout waiting for Sarama messages. Consumed %d/%d", consumedCount, len(messages)) |
|||
} |
|||
} |
|||
|
|||
t.Logf("🎉 Sarama to Sarama test PASSED") |
|||
} |
|||
|
|||
func testKafkaGoToSarama(t *testing.T, addr, topic string) { |
|||
// Note: In a real test environment, we'd need to ensure topic isolation
|
|||
// For now, we'll use a different key prefix to distinguish messages
|
|||
|
|||
messages := []kafka.Message{ |
|||
{Key: []byte("mixed-kgo-key1"), Value: []byte("kafka-go producer to Sarama consumer")}, |
|||
{Key: []byte("mixed-kgo-key2"), Value: []byte("Cross-client compatibility test")}, |
|||
} |
|||
|
|||
// Produce with kafka-go
|
|||
w := &kafka.Writer{ |
|||
Addr: kafka.TCP(addr), |
|||
Topic: topic, |
|||
Balancer: &kafka.LeastBytes{}, |
|||
BatchTimeout: 50 * time.Millisecond, |
|||
RequiredAcks: kafka.RequireOne, |
|||
} |
|||
defer w.Close() |
|||
|
|||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) |
|||
defer cancel() |
|||
|
|||
err := w.WriteMessages(ctx, messages...) |
|||
if err != nil { |
|||
t.Fatalf("kafka-go produce failed: %v", err) |
|||
} |
|||
t.Logf("✅ kafka-go produced %d messages for Sarama consumer", len(messages)) |
|||
|
|||
// Consume with Sarama
|
|||
config := sarama.NewConfig() |
|||
config.Version = sarama.V0_11_0_0 |
|||
config.Consumer.Return.Errors = true |
|||
|
|||
consumer, err := sarama.NewConsumer([]string{addr}, config) |
|||
if err != nil { |
|||
t.Fatalf("Failed to create Sarama consumer: %v", err) |
|||
} |
|||
defer consumer.Close() |
|||
|
|||
// Start from latest to avoid consuming previous test messages
|
|||
partitionConsumer, err := consumer.ConsumePartition(topic, 0, sarama.OffsetNewest) |
|||
if err != nil { |
|||
t.Fatalf("Failed to create Sarama partition consumer: %v", err) |
|||
} |
|||
defer partitionConsumer.Close() |
|||
|
|||
// Give a moment for the consumer to be ready
|
|||
time.Sleep(100 * time.Millisecond) |
|||
|
|||
// Produce again to ensure we get fresh messages
|
|||
err = w.WriteMessages(ctx, messages...) |
|||
if err != nil { |
|||
t.Fatalf("kafka-go second produce failed: %v", err) |
|||
} |
|||
|
|||
consumedCount := 0 |
|||
timeout := time.After(10 * time.Second) |
|||
|
|||
for consumedCount < len(messages) { |
|||
select { |
|||
case msg := <-partitionConsumer.Messages(): |
|||
// Only count messages with our test key prefix
|
|||
if strings.HasPrefix(string(msg.Key), "mixed-kgo-key") { |
|||
t.Logf("✅ Sarama consumed kafka-go message: key=%s, value=%s", |
|||
string(msg.Key), string(msg.Value)) |
|||
consumedCount++ |
|||
} |
|||
|
|||
case err := <-partitionConsumer.Errors(): |
|||
t.Fatalf("Sarama consumer error: %v", err) |
|||
|
|||
case <-timeout: |
|||
t.Fatalf("Timeout waiting for mixed messages. Consumed %d/%d", consumedCount, len(messages)) |
|||
} |
|||
} |
|||
|
|||
t.Logf("🎉 kafka-go to Sarama test PASSED") |
|||
} |
|||
|
|||
func testSaramaToKafkaGo(t *testing.T, addr, topic string) { |
|||
// Configure Sarama
|
|||
config := sarama.NewConfig() |
|||
config.Version = sarama.V0_11_0_0 |
|||
config.Producer.Return.Successes = true |
|||
config.Producer.RequiredAcks = sarama.WaitForAll |
|||
|
|||
messages := []string{ |
|||
"Sarama producer to kafka-go consumer", |
|||
"Reverse cross-client compatibility test", |
|||
} |
|||
|
|||
// Produce with Sarama
|
|||
producer, err := sarama.NewSyncProducer([]string{addr}, config) |
|||
if err != nil { |
|||
t.Fatalf("Failed to create Sarama producer: %v", err) |
|||
} |
|||
defer producer.Close() |
|||
|
|||
for i, msgText := range messages { |
|||
msg := &sarama.ProducerMessage{ |
|||
Topic: topic, |
|||
Key: sarama.StringEncoder(fmt.Sprintf("mixed-sarama-key-%d", i)), |
|||
Value: sarama.StringEncoder(msgText), |
|||
} |
|||
|
|||
partition, offset, err := producer.SendMessage(msg) |
|||
if err != nil { |
|||
t.Fatalf("Sarama produce failed: %v", err) |
|||
} |
|||
t.Logf("✅ Sarama produced message %d for kafka-go consumer: partition=%d, offset=%d", i, partition, offset) |
|||
} |
|||
|
|||
// Consume with kafka-go
|
|||
r := kafka.NewReader(kafka.ReaderConfig{ |
|||
Brokers: []string{addr}, |
|||
Topic: topic, |
|||
StartOffset: kafka.LastOffset, // Start from latest to avoid previous messages
|
|||
MinBytes: 1, |
|||
MaxBytes: 10e6, |
|||
}) |
|||
defer r.Close() |
|||
|
|||
// Give a moment for the reader to be ready, then produce fresh messages
|
|||
time.Sleep(100 * time.Millisecond) |
|||
|
|||
// Produce again to ensure fresh messages for the latest offset reader
|
|||
for i, msgText := range messages { |
|||
msg := &sarama.ProducerMessage{ |
|||
Topic: topic, |
|||
Key: sarama.StringEncoder(fmt.Sprintf("mixed-sarama-fresh-key-%d", i)), |
|||
Value: sarama.StringEncoder(msgText), |
|||
} |
|||
|
|||
_, _, err := producer.SendMessage(msg) |
|||
if err != nil { |
|||
t.Fatalf("Sarama second produce failed: %v", err) |
|||
} |
|||
} |
|||
|
|||
consumeCtx, consumeCancel := context.WithTimeout(context.Background(), 10*time.Second) |
|||
defer consumeCancel() |
|||
|
|||
consumedCount := 0 |
|||
for consumedCount < len(messages) { |
|||
msg, err := r.ReadMessage(consumeCtx) |
|||
if err != nil { |
|||
t.Fatalf("kafka-go consume failed: %v", err) |
|||
} |
|||
|
|||
// Only count messages with our fresh test key prefix
|
|||
if strings.HasPrefix(string(msg.Key), "mixed-sarama-fresh-key") { |
|||
t.Logf("✅ kafka-go consumed Sarama message: key=%s, value=%s", |
|||
string(msg.Key), string(msg.Value)) |
|||
consumedCount++ |
|||
} |
|||
} |
|||
|
|||
t.Logf("🎉 Sarama to kafka-go test PASSED") |
|||
} |
|||
|
|||
// TestOffsetManagement tests offset commit and fetch operations
|
|||
func TestOffsetManagement(t *testing.T) { |
|||
// Start gateway
|
|||
gatewayServer := gateway.NewServer(gateway.Options{ |
|||
Listen: "127.0.0.1:0", |
|||
UseSeaweedMQ: false, |
|||
}) |
|||
|
|||
go func() { |
|||
if err := gatewayServer.Start(); err != nil { |
|||
t.Errorf("Failed to start gateway: %v", err) |
|||
} |
|||
}() |
|||
defer gatewayServer.Close() |
|||
|
|||
time.Sleep(100 * time.Millisecond) |
|||
|
|||
host, port := gatewayServer.GetListenerAddr() |
|||
addr := fmt.Sprintf("%s:%d", host, port) |
|||
topic := "offset-management-topic" |
|||
groupID := "offset-test-group" |
|||
|
|||
gatewayServer.GetHandler().AddTopicForTesting(topic, 1) |
|||
t.Logf("Testing offset management on %s with topic %s", addr, topic) |
|||
|
|||
// Produce test messages
|
|||
messages := []kafka.Message{ |
|||
{Key: []byte("offset-key1"), Value: []byte("Offset test message 1")}, |
|||
{Key: []byte("offset-key2"), Value: []byte("Offset test message 2")}, |
|||
{Key: []byte("offset-key3"), Value: []byte("Offset test message 3")}, |
|||
{Key: []byte("offset-key4"), Value: []byte("Offset test message 4")}, |
|||
{Key: []byte("offset-key5"), Value: []byte("Offset test message 5")}, |
|||
} |
|||
|
|||
w := &kafka.Writer{ |
|||
Addr: kafka.TCP(addr), |
|||
Topic: topic, |
|||
Balancer: &kafka.LeastBytes{}, |
|||
BatchTimeout: 50 * time.Millisecond, |
|||
RequiredAcks: kafka.RequireOne, |
|||
} |
|||
defer w.Close() |
|||
|
|||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) |
|||
defer cancel() |
|||
|
|||
err := w.WriteMessages(ctx, messages...) |
|||
if err != nil { |
|||
t.Fatalf("Failed to produce offset test messages: %v", err) |
|||
} |
|||
t.Logf("✅ Produced %d messages for offset test", len(messages)) |
|||
|
|||
// Test 1: Consume first 3 messages and commit offsets
|
|||
t.Logf("=== Phase 1: Consuming first 3 messages ===") |
|||
|
|||
r1 := kafka.NewReader(kafka.ReaderConfig{ |
|||
Brokers: []string{addr}, |
|||
Topic: topic, |
|||
GroupID: groupID, |
|||
StartOffset: kafka.FirstOffset, |
|||
MinBytes: 1, |
|||
MaxBytes: 10e6, |
|||
}) |
|||
|
|||
consumeCtx1, consumeCancel1 := context.WithTimeout(context.Background(), 10*time.Second) |
|||
defer consumeCancel1() |
|||
|
|||
for i := 0; i < 3; i++ { |
|||
msg, err := r1.ReadMessage(consumeCtx1) |
|||
if err != nil { |
|||
t.Fatalf("Failed to read message %d: %v", i, err) |
|||
} |
|||
t.Logf("✅ Phase 1 consumed message %d: key=%s, offset=%d", |
|||
i, string(msg.Key), msg.Offset) |
|||
} |
|||
|
|||
// Commit the offset (kafka-go automatically commits when using GroupID)
|
|||
r1.Close() |
|||
t.Logf("✅ Phase 1 completed - offsets should be committed") |
|||
|
|||
// Test 2: Create new consumer with same group ID - should resume from committed offset
|
|||
t.Logf("=== Phase 2: Resuming from committed offset ===") |
|||
|
|||
r2 := kafka.NewReader(kafka.ReaderConfig{ |
|||
Brokers: []string{addr}, |
|||
Topic: topic, |
|||
GroupID: groupID, // Same group ID
|
|||
StartOffset: kafka.FirstOffset, // This should be ignored due to committed offset
|
|||
MinBytes: 1, |
|||
MaxBytes: 10e6, |
|||
}) |
|||
defer r2.Close() |
|||
|
|||
consumeCtx2, consumeCancel2 := context.WithTimeout(context.Background(), 10*time.Second) |
|||
defer consumeCancel2() |
|||
|
|||
remainingCount := 0 |
|||
expectedRemaining := len(messages) - 3 // Should get the last 2 messages
|
|||
|
|||
for remainingCount < expectedRemaining { |
|||
msg, err := r2.ReadMessage(consumeCtx2) |
|||
if err != nil { |
|||
t.Fatalf("Failed to read remaining message %d: %v", remainingCount, err) |
|||
} |
|||
t.Logf("✅ Phase 2 consumed remaining message %d: key=%s, offset=%d", |
|||
remainingCount, string(msg.Key), msg.Offset) |
|||
remainingCount++ |
|||
} |
|||
|
|||
if remainingCount != expectedRemaining { |
|||
t.Errorf("Expected %d remaining messages, got %d", expectedRemaining, remainingCount) |
|||
} |
|||
|
|||
t.Logf("🎉 SUCCESS: Offset management test completed - consumed 3 + %d messages", remainingCount) |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue