main.go called Reconcile(ctx, req.Name) without req.Namespace, so the Get against the namespaced Queue CRD always 404'd and was silently swallowed as success -- no shard topics or Redis state were ever created. Separately, ShardStatus.HashRangeStart/End were uint32, but controller-gen maps that to OpenAPI format:int32, whose max (2147483647) is smaller than FullHashRangeEnd (0xFFFFFFFF), so the apiserver rejected every status update with the (misleadingly empty-looking) "must be of type integer with format int32" error. Widened to int64, regenerated the CRD, and synced the chart's bundled copy.
87 lines
2.4 KiB
Go
87 lines
2.4 KiB
Go
package operator
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strconv"
|
|
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
|
|
kmsvcv1 "github.com/rockliang/kafka-management-service/apis/kmsvc/v1"
|
|
"github.com/rockliang/kafka-management-service/internal/kafka"
|
|
)
|
|
|
|
// reconcileSplits samples each Active shard's throughput and splits any shard
|
|
// that's sustained over spec.ShardSplitThresholdBytesPerSec into two children,
|
|
// per design.md §2c.
|
|
func (r *QueueReconciler) reconcileSplits(ctx context.Context, queue *kmsvcv1.Queue) error {
|
|
if r.sampleState == nil {
|
|
r.sampleState = map[string]sample{}
|
|
}
|
|
|
|
maxShards := queue.Spec.MaxShards
|
|
if maxShards <= 0 {
|
|
maxShards = 8
|
|
}
|
|
threshold := queue.Spec.ShardSplitThresholdBytesPerSec
|
|
if threshold <= 0 {
|
|
return nil
|
|
}
|
|
cooldown := queue.Spec.ShardSplitCooldownSeconds
|
|
|
|
activeCount := 0
|
|
for _, s := range queue.Status.Shards {
|
|
if s.Phase == kmsvcv1.ShardPhaseActive {
|
|
activeCount++
|
|
}
|
|
}
|
|
|
|
for i := range queue.Status.Shards {
|
|
s := &queue.Status.Shards[i]
|
|
if s.Phase != kmsvcv1.ShardPhaseActive {
|
|
continue
|
|
}
|
|
if int32(activeCount) >= maxShards {
|
|
break
|
|
}
|
|
if !s.CreatedAt.IsZero() && r.now().Before(s.CreatedAt.Add(secondsToDuration(cooldown))) {
|
|
continue
|
|
}
|
|
|
|
offsetSum, err := r.Admin.LogEndOffsetSum(ctx, s.Topic)
|
|
if err != nil {
|
|
return fmt.Errorf("sample throughput %s: %w", s.Topic, err)
|
|
}
|
|
prev, seen := r.sampleState[s.Topic]
|
|
now := r.now()
|
|
r.sampleState[s.Topic] = sample{offsetSum: offsetSum, at: now}
|
|
if !seen {
|
|
continue
|
|
}
|
|
elapsed := now.Sub(prev.at).Seconds()
|
|
if elapsed <= 0 {
|
|
continue
|
|
}
|
|
recordsPerSec := float64(offsetSum-prev.offsetSum) / elapsed
|
|
bytesPerSec := recordsPerSec * avgMessageSizeBytesEstimate
|
|
if bytesPerSec < float64(threshold) {
|
|
continue
|
|
}
|
|
|
|
mid := kafka.SplitHashRange(uint32(s.HashRangeStart), uint32(s.HashRangeEnd))
|
|
childAID := nextShardID(queue.Status.Shards)
|
|
childA := r.newShard(childAID, s.ID, uint32(s.HashRangeStart), mid, queue)
|
|
childA.CreatedAt = metav1.NewTime(now)
|
|
childAIDNum, _ := strconv.Atoi(childAID)
|
|
childBID := strconv.Itoa(childAIDNum + 1)
|
|
childB := r.newShard(childBID, s.ID, mid, uint32(s.HashRangeEnd), queue)
|
|
childB.CreatedAt = metav1.NewTime(now)
|
|
|
|
s.Phase = kmsvcv1.ShardPhaseClosing
|
|
queue.Status.Shards = append(queue.Status.Shards, childA, childB)
|
|
activeCount += 1 // net: -1 parent +2 children
|
|
delete(r.sampleState, s.Topic)
|
|
}
|
|
return nil
|
|
}
|