chore: add allZones which returns a sorted iterator over zones (#19888)

pull/19894/head
George Robinson 6 months ago committed by GitHub
parent 810fb53f8e
commit ec2b16a63d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 72
      pkg/limits/frontend/ring.go

@ -2,6 +2,7 @@ package frontend
import (
"context"
"iter"
"slices"
"sort"
"strings"
@ -76,24 +77,10 @@ func (r *ringLimitsClient) ExceedsLimits(ctx context.Context, req *proto.Exceeds
if len(req.Streams) == 0 {
return nil, nil
}
rs, err := r.ring.GetAllHealthy(LimitsRead)
if err != nil {
return nil, err
}
// Get the partition consumers for each zone.
zonesPartitions, err := r.getZoneAwarePartitionConsumers(ctx, rs.Instances)
zonesIter, err := r.allZones(ctx)
if err != nil {
return nil, err
}
// In practice we want zones to be queried in random order to spread
// reads. However, in tests we want a deterministic order so test cases
// are stable and reproducible. Having a custom sort func supports both
// use cases as zoneCmp can be switched out in tests.
zonesToQuery := make([]string, 0, len(zonesPartitions))
for zone := range zonesPartitions {
zonesToQuery = append(zonesToQuery, zone)
}
slices.SortFunc(zonesToQuery, r.zoneCmp)
// Make a copy of the streams from the request. We will prune this slice
// each time we receive the responses from a zone.
streams := make([]*proto.StreamMetadata, 0, len(req.Streams))
@ -107,12 +94,12 @@ func (r *ringLimitsClient) ExceedsLimits(ctx context.Context, req *proto.Exceeds
// process until all streams have been queried or we have exhausted all
// zones.
responses := make([]*proto.ExceedsLimitsResponse, 0)
for _, zone := range zonesToQuery {
for zone, consumers := range zonesIter {
// All streams been checked against per-tenant limits.
if len(streams) == 0 {
break
}
resps, answered, err := r.doExceedsLimitsRPCs(ctx, req.Tenant, streams, zonesPartitions[zone], zone)
resps, answered, err := r.doExceedsLimitsRPCs(ctx, req.Tenant, streams, zone, consumers)
if err != nil {
continue
}
@ -143,12 +130,12 @@ func (r *ringLimitsClient) ExceedsLimits(ctx context.Context, req *proto.Exceeds
return responses, nil
}
func (r *ringLimitsClient) doExceedsLimitsRPCs(ctx context.Context, tenant string, streams []*proto.StreamMetadata, partitions map[int32]string, zone string) ([]*proto.ExceedsLimitsResponse, []uint64, error) {
func (r *ringLimitsClient) doExceedsLimitsRPCs(ctx context.Context, tenant string, streams []*proto.StreamMetadata, zone string, consumers map[int32]string) ([]*proto.ExceedsLimitsResponse, []uint64, error) {
// For each stream, figure out which instance consume its partition.
instancesForStreams := make(map[string][]*proto.StreamMetadata)
for _, stream := range streams {
partition := int32(stream.StreamHash % uint64(r.numPartitions))
addr, ok := partitions[partition]
addr, ok := consumers[partition]
if !ok {
r.partitionsMissing.WithLabelValues(zone).Inc()
continue
@ -194,9 +181,37 @@ func (r *ringLimitsClient) doExceedsLimitsRPCs(ctx context.Context, tenant strin
return responses, answered, nil
}
type zonePartitionConsumersResult struct {
zone string
partitions map[int32]string
// allZones returns an iterator over all zones and the consumers for each
// partition in each zone. If a zone has no active partition consumers, the
// zone will still be returned but its partition consumers will be nil.
// If ZoneAwarenessEnabled is false, it returns all partition consumers under
// a pseudo-zone ("").
func (r *ringLimitsClient) allZones(ctx context.Context) (iter.Seq2[string, map[int32]string], error) {
rs, err := r.ring.GetAllHealthy(LimitsRead)
if err != nil {
return nil, err
}
// Get the partition consumers for each zone.
zonesPartitions, err := r.getZoneAwarePartitionConsumers(ctx, rs.Instances)
if err != nil {
return nil, err
}
// In practice we want zones to be queried in random order to spread
// reads. However, in tests we want a deterministic order so test cases
// are stable and reproducible. Having a custom sort func supports both
// use cases as zoneCmp can be switched out in tests.
zonesToQuery := make([]string, 0, len(zonesPartitions))
for zone := range zonesPartitions {
zonesToQuery = append(zonesToQuery, zone)
}
slices.SortFunc(zonesToQuery, r.zoneCmp)
return func(yield func(string, map[int32]string) bool) {
for _, zone := range zonesToQuery {
if !yield(zone, zonesPartitions[zone]) {
return
}
}
}, nil
}
// getZoneAwarePartitionConsumers returns partition consumers for each zone
@ -210,6 +225,10 @@ func (r *ringLimitsClient) getZoneAwarePartitionConsumers(ctx context.Context, i
zoneDescs[instance.Zone] = append(zoneDescs[instance.Zone], instance)
}
// Get the partition consumers for each zone.
type zonePartitionConsumersResult struct {
zone string
partitions map[int32]string
}
resultsCh := make(chan zonePartitionConsumersResult, len(zoneDescs))
errg, ctx := errgroup.WithContext(ctx)
for zone, instances := range zoneDescs {
@ -236,11 +255,6 @@ func (r *ringLimitsClient) getZoneAwarePartitionConsumers(ctx context.Context, i
return results, nil
}
type getAssignedPartitionsResponse struct {
addr string
response *proto.GetAssignedPartitionsResponse
}
// getPartitionConsumers returns the consumer for each partition.
// In some cases, it might not be possible to know the consumer for a
@ -259,6 +273,10 @@ type getAssignedPartitionsResponse struct {
// to find the most up to date consumer for each partition across all zones.
func (r *ringLimitsClient) getPartitionConsumers(ctx context.Context, instances []ring.InstanceDesc) (map[int32]string, error) {
errg, ctx := errgroup.WithContext(ctx)
type getAssignedPartitionsResponse struct {
addr string
response *proto.GetAssignedPartitionsResponse
}
responseCh := make(chan getAssignedPartitionsResponse, len(instances))
for _, instance := range instances {
errg.Go(func() error {

Loading…
Cancel
Save