You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
294 lines
9.3 KiB
294 lines
9.3 KiB
package kafka
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/mq/pub_balancer"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/schema_pb"
|
|
)
|
|
|
|
// PartitionMapper provides consistent Kafka partition to SeaweedMQ ring mapping
|
|
// NOTE: This is test-only code and not used in the actual Kafka Gateway implementation
|
|
type PartitionMapper struct{}
|
|
|
|
// NewPartitionMapper creates a new partition mapper
|
|
func NewPartitionMapper() *PartitionMapper {
|
|
return &PartitionMapper{}
|
|
}
|
|
|
|
// GetRangeSize returns the consistent range size for Kafka partition mapping
|
|
// This ensures all components use the same calculation
|
|
func (pm *PartitionMapper) GetRangeSize() int32 {
|
|
// Use a range size that divides evenly into MaxPartitionCount (2520)
|
|
// Range size 35 gives us exactly 72 Kafka partitions: 2520 / 35 = 72
|
|
// This provides a good balance between partition granularity and ring utilization
|
|
return 35
|
|
}
|
|
|
|
// GetMaxKafkaPartitions returns the maximum number of Kafka partitions supported
|
|
func (pm *PartitionMapper) GetMaxKafkaPartitions() int32 {
|
|
// With range size 35, we can support: 2520 / 35 = 72 Kafka partitions
|
|
return int32(pub_balancer.MaxPartitionCount) / pm.GetRangeSize()
|
|
}
|
|
|
|
// MapKafkaPartitionToSMQRange maps a Kafka partition to SeaweedMQ ring range
|
|
func (pm *PartitionMapper) MapKafkaPartitionToSMQRange(kafkaPartition int32) (rangeStart, rangeStop int32) {
|
|
rangeSize := pm.GetRangeSize()
|
|
rangeStart = kafkaPartition * rangeSize
|
|
rangeStop = rangeStart + rangeSize - 1
|
|
return rangeStart, rangeStop
|
|
}
|
|
|
|
// CreateSMQPartition creates a SeaweedMQ partition from a Kafka partition
|
|
func (pm *PartitionMapper) CreateSMQPartition(kafkaPartition int32, unixTimeNs int64) *schema_pb.Partition {
|
|
rangeStart, rangeStop := pm.MapKafkaPartitionToSMQRange(kafkaPartition)
|
|
|
|
return &schema_pb.Partition{
|
|
RingSize: pub_balancer.MaxPartitionCount,
|
|
RangeStart: rangeStart,
|
|
RangeStop: rangeStop,
|
|
UnixTimeNs: unixTimeNs,
|
|
}
|
|
}
|
|
|
|
// ExtractKafkaPartitionFromSMQRange extracts the Kafka partition from SeaweedMQ range
|
|
func (pm *PartitionMapper) ExtractKafkaPartitionFromSMQRange(rangeStart int32) int32 {
|
|
rangeSize := pm.GetRangeSize()
|
|
return rangeStart / rangeSize
|
|
}
|
|
|
|
// ValidateKafkaPartition validates that a Kafka partition is within supported range
|
|
func (pm *PartitionMapper) ValidateKafkaPartition(kafkaPartition int32) bool {
|
|
return kafkaPartition >= 0 && kafkaPartition < pm.GetMaxKafkaPartitions()
|
|
}
|
|
|
|
// GetPartitionMappingInfo returns debug information about the partition mapping
|
|
func (pm *PartitionMapper) GetPartitionMappingInfo() map[string]interface{} {
|
|
return map[string]interface{}{
|
|
"ring_size": pub_balancer.MaxPartitionCount,
|
|
"range_size": pm.GetRangeSize(),
|
|
"max_kafka_partitions": pm.GetMaxKafkaPartitions(),
|
|
"ring_utilization": float64(pm.GetMaxKafkaPartitions()*pm.GetRangeSize()) / float64(pub_balancer.MaxPartitionCount),
|
|
}
|
|
}
|
|
|
|
// Global instance for consistent usage across the test codebase
|
|
var DefaultPartitionMapper = NewPartitionMapper()
|
|
|
|
func TestPartitionMapper_GetRangeSize(t *testing.T) {
|
|
mapper := NewPartitionMapper()
|
|
rangeSize := mapper.GetRangeSize()
|
|
|
|
if rangeSize != 35 {
|
|
t.Errorf("Expected range size 35, got %d", rangeSize)
|
|
}
|
|
|
|
// Verify that the range size divides evenly into available partitions
|
|
maxPartitions := mapper.GetMaxKafkaPartitions()
|
|
totalUsed := maxPartitions * rangeSize
|
|
|
|
if totalUsed > int32(pub_balancer.MaxPartitionCount) {
|
|
t.Errorf("Total used slots (%d) exceeds MaxPartitionCount (%d)", totalUsed, pub_balancer.MaxPartitionCount)
|
|
}
|
|
|
|
t.Logf("Range size: %d, Max Kafka partitions: %d, Ring utilization: %.2f%%",
|
|
rangeSize, maxPartitions, float64(totalUsed)/float64(pub_balancer.MaxPartitionCount)*100)
|
|
}
|
|
|
|
func TestPartitionMapper_MapKafkaPartitionToSMQRange(t *testing.T) {
|
|
mapper := NewPartitionMapper()
|
|
|
|
tests := []struct {
|
|
kafkaPartition int32
|
|
expectedStart int32
|
|
expectedStop int32
|
|
}{
|
|
{0, 0, 34},
|
|
{1, 35, 69},
|
|
{2, 70, 104},
|
|
{10, 350, 384},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run("", func(t *testing.T) {
|
|
start, stop := mapper.MapKafkaPartitionToSMQRange(tt.kafkaPartition)
|
|
|
|
if start != tt.expectedStart {
|
|
t.Errorf("Kafka partition %d: expected start %d, got %d", tt.kafkaPartition, tt.expectedStart, start)
|
|
}
|
|
|
|
if stop != tt.expectedStop {
|
|
t.Errorf("Kafka partition %d: expected stop %d, got %d", tt.kafkaPartition, tt.expectedStop, stop)
|
|
}
|
|
|
|
// Verify range size is consistent
|
|
rangeSize := stop - start + 1
|
|
if rangeSize != mapper.GetRangeSize() {
|
|
t.Errorf("Inconsistent range size: expected %d, got %d", mapper.GetRangeSize(), rangeSize)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPartitionMapper_ExtractKafkaPartitionFromSMQRange(t *testing.T) {
|
|
mapper := NewPartitionMapper()
|
|
|
|
tests := []struct {
|
|
rangeStart int32
|
|
expectedKafka int32
|
|
}{
|
|
{0, 0},
|
|
{35, 1},
|
|
{70, 2},
|
|
{350, 10},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run("", func(t *testing.T) {
|
|
kafkaPartition := mapper.ExtractKafkaPartitionFromSMQRange(tt.rangeStart)
|
|
|
|
if kafkaPartition != tt.expectedKafka {
|
|
t.Errorf("Range start %d: expected Kafka partition %d, got %d",
|
|
tt.rangeStart, tt.expectedKafka, kafkaPartition)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPartitionMapper_RoundTrip(t *testing.T) {
|
|
mapper := NewPartitionMapper()
|
|
|
|
// Test round-trip conversion for all valid Kafka partitions
|
|
maxPartitions := mapper.GetMaxKafkaPartitions()
|
|
|
|
for kafkaPartition := int32(0); kafkaPartition < maxPartitions; kafkaPartition++ {
|
|
// Kafka -> SMQ -> Kafka
|
|
rangeStart, rangeStop := mapper.MapKafkaPartitionToSMQRange(kafkaPartition)
|
|
extractedKafka := mapper.ExtractKafkaPartitionFromSMQRange(rangeStart)
|
|
|
|
if extractedKafka != kafkaPartition {
|
|
t.Errorf("Round-trip failed for partition %d: got %d", kafkaPartition, extractedKafka)
|
|
}
|
|
|
|
// Verify no overlap with next partition
|
|
if kafkaPartition < maxPartitions-1 {
|
|
nextStart, _ := mapper.MapKafkaPartitionToSMQRange(kafkaPartition + 1)
|
|
if rangeStop >= nextStart {
|
|
t.Errorf("Partition %d range [%d,%d] overlaps with partition %d start %d",
|
|
kafkaPartition, rangeStart, rangeStop, kafkaPartition+1, nextStart)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPartitionMapper_CreateSMQPartition(t *testing.T) {
|
|
mapper := NewPartitionMapper()
|
|
|
|
kafkaPartition := int32(5)
|
|
unixTimeNs := time.Now().UnixNano()
|
|
|
|
partition := mapper.CreateSMQPartition(kafkaPartition, unixTimeNs)
|
|
|
|
if partition.RingSize != pub_balancer.MaxPartitionCount {
|
|
t.Errorf("Expected ring size %d, got %d", pub_balancer.MaxPartitionCount, partition.RingSize)
|
|
}
|
|
|
|
expectedStart, expectedStop := mapper.MapKafkaPartitionToSMQRange(kafkaPartition)
|
|
if partition.RangeStart != expectedStart {
|
|
t.Errorf("Expected range start %d, got %d", expectedStart, partition.RangeStart)
|
|
}
|
|
|
|
if partition.RangeStop != expectedStop {
|
|
t.Errorf("Expected range stop %d, got %d", expectedStop, partition.RangeStop)
|
|
}
|
|
|
|
if partition.UnixTimeNs != unixTimeNs {
|
|
t.Errorf("Expected timestamp %d, got %d", unixTimeNs, partition.UnixTimeNs)
|
|
}
|
|
}
|
|
|
|
func TestPartitionMapper_ValidateKafkaPartition(t *testing.T) {
|
|
mapper := NewPartitionMapper()
|
|
|
|
tests := []struct {
|
|
partition int32
|
|
valid bool
|
|
}{
|
|
{-1, false},
|
|
{0, true},
|
|
{1, true},
|
|
{mapper.GetMaxKafkaPartitions() - 1, true},
|
|
{mapper.GetMaxKafkaPartitions(), false},
|
|
{1000, false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run("", func(t *testing.T) {
|
|
valid := mapper.ValidateKafkaPartition(tt.partition)
|
|
if valid != tt.valid {
|
|
t.Errorf("Partition %d: expected valid=%v, got %v", tt.partition, tt.valid, valid)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPartitionMapper_ConsistencyWithGlobalFunctions(t *testing.T) {
|
|
mapper := NewPartitionMapper()
|
|
|
|
kafkaPartition := int32(7)
|
|
unixTimeNs := time.Now().UnixNano()
|
|
|
|
// Test that global functions produce same results as mapper methods
|
|
start1, stop1 := mapper.MapKafkaPartitionToSMQRange(kafkaPartition)
|
|
start2, stop2 := MapKafkaPartitionToSMQRange(kafkaPartition)
|
|
|
|
if start1 != start2 || stop1 != stop2 {
|
|
t.Errorf("Global function inconsistent: mapper=(%d,%d), global=(%d,%d)",
|
|
start1, stop1, start2, stop2)
|
|
}
|
|
|
|
partition1 := mapper.CreateSMQPartition(kafkaPartition, unixTimeNs)
|
|
partition2 := CreateSMQPartition(kafkaPartition, unixTimeNs)
|
|
|
|
if partition1.RangeStart != partition2.RangeStart || partition1.RangeStop != partition2.RangeStop {
|
|
t.Errorf("Global CreateSMQPartition inconsistent")
|
|
}
|
|
|
|
extracted1 := mapper.ExtractKafkaPartitionFromSMQRange(start1)
|
|
extracted2 := ExtractKafkaPartitionFromSMQRange(start1)
|
|
|
|
if extracted1 != extracted2 {
|
|
t.Errorf("Global ExtractKafkaPartitionFromSMQRange inconsistent: %d vs %d", extracted1, extracted2)
|
|
}
|
|
}
|
|
|
|
func TestPartitionMapper_GetPartitionMappingInfo(t *testing.T) {
|
|
mapper := NewPartitionMapper()
|
|
|
|
info := mapper.GetPartitionMappingInfo()
|
|
|
|
// Verify all expected keys are present
|
|
expectedKeys := []string{"ring_size", "range_size", "max_kafka_partitions", "ring_utilization"}
|
|
for _, key := range expectedKeys {
|
|
if _, exists := info[key]; !exists {
|
|
t.Errorf("Missing key in mapping info: %s", key)
|
|
}
|
|
}
|
|
|
|
// Verify values are reasonable
|
|
if info["ring_size"].(int) != pub_balancer.MaxPartitionCount {
|
|
t.Errorf("Incorrect ring_size in info")
|
|
}
|
|
|
|
if info["range_size"].(int32) != mapper.GetRangeSize() {
|
|
t.Errorf("Incorrect range_size in info")
|
|
}
|
|
|
|
utilization := info["ring_utilization"].(float64)
|
|
if utilization <= 0 || utilization > 1 {
|
|
t.Errorf("Invalid ring utilization: %f", utilization)
|
|
}
|
|
|
|
t.Logf("Partition mapping info: %+v", info)
|
|
}
|