Files
renovate-sh-app[bot]GitHubrenovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com>
5d0e09cf8d fix(deps): update module github.com/twmb/franz-go to v1.20.5 (#6049)
| datasource | package                  | from    | to      |
| ---------- | ------------------------ | ------- | ------- |
| go         | github.com/twmb/franz-go | v1.20.4 | v1.20.5 |

Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com>
Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com>
2025-12-10 08:30:02 +01:00

5026 lines
154 KiB
Go

// Package kgo provides a pure Go efficient Kafka client for Kafka 0.8+ with
// support for transactions, regex topic consuming, the latest partition
// strategies, and more. This client supports all client related KIPs.
//
// This client aims to be simple to use while still interacting with Kafka in a
// near ideal way. For more overview of the entire client itself, please see
// the README on the project's Github page.
package kgo
import (
"context"
"crypto/tls"
"errors"
"fmt"
"hash/crc32"
"maps"
"math"
"math/rand"
"net"
"reflect"
"runtime"
"slices"
"sort"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/twmb/franz-go/pkg/kerr"
"github.com/twmb/franz-go/pkg/kmsg"
"github.com/twmb/franz-go/pkg/sasl"
)
var crc32c = crc32.MakeTable(crc32.Castagnoli) // record crc's use Castagnoli table; for consuming/producing
// Client issues requests and handles responses to a Kafka cluster.
type Client struct {
cfg cfg
opts []Opt
ctx context.Context
ctxCancel func()
rng func(func(*rand.Rand))
brokersMu sync.RWMutex
brokers []*broker // ordered by broker ID
seeds atomic.Value // []*broker, seed brokers, also ordered by ID
anyBrokerOrd []int32 // shuffled brokers, for random ordering
anySeedIdx int32
stopBrokers bool // set to true on close to stop updateBrokers
// A sink and a source is created once per node ID and persists
// forever. We expect the list to be small.
//
// The mutex only exists to allow consumer session stopping to read
// sources to notify when starting a session; all writes happen in the
// metadata loop.
sinksAndSourcesMu sync.Mutex
sinksAndSources map[int32]sinkAndSource
reqFormatter *kmsg.RequestFormatter
connTimeouter connTimeouter
bufPool bufPool // for to brokers to share underlying reusable request buffers
prsPool prsPool // for sinks to reuse []promisedNumberedRecord
controllerIDMu sync.Mutex
controllerID int32
clusterID *string // we piggy back updating clusterID
// The following two ensure that we only have one fetchBrokerMetadata
// at once. This avoids unnecessary broker metadata requests and
// metadata trampling.
fetchingBrokersMu sync.Mutex
fetchingBrokers *struct {
done chan struct{}
err error
}
producer producer
consumer consumer
id2t atomic.Value // map[[16]byte]string
metrics metrics
coordinatorsMu sync.Mutex
coordinators map[coordinatorKey]*coordinatorLoad
updateMetadataCh chan string
updateMetadataNowCh chan string // like above, but with high priority
blockingMetadataFnCh chan func()
metawait metawait
metadone chan struct{}
mappedMetaMu sync.Mutex
mappedMeta map[string]mappedMetadataTopic
}
func (cl *Client) idempotent() bool { return !cl.cfg.disableIdempotency }
type sinkAndSource struct {
sink *sink
source *source
}
func (cl *Client) allSinksAndSources(fn func(sns sinkAndSource)) {
cl.sinksAndSourcesMu.Lock()
defer cl.sinksAndSourcesMu.Unlock()
for _, sns := range cl.sinksAndSources {
fn(sns)
}
}
type hostport struct {
host string
port int32
}
// ValidateOpts returns an error if the options are invalid.
func ValidateOpts(opts ...Opt) error {
_, _, err := validateCfg(opts...)
return err
}
func parseSeeds(addrs []string) ([]hostport, error) {
seeds := make([]hostport, 0, len(addrs))
for _, seedBroker := range addrs {
hp, err := parseBrokerAddr(seedBroker)
if err != nil {
return nil, err
}
seeds = append(seeds, hp)
}
return seeds, nil
}
// This function validates the configuration and returns a few things that we
// initialize while validating. The difference between this and NewClient
// initialization is all NewClient initialization is infallible.
func validateCfg(opts ...Opt) (cfg, []hostport, error) {
cfg := defaultCfg()
for _, opt := range opts {
opt.apply(&cfg)
}
if err := cfg.validate(); err != nil {
return cfg, nil, err
}
seeds, err := parseSeeds(cfg.seedBrokers)
if err != nil {
return cfg, nil, err
}
if cfg.compressor == nil {
cfg.compressor, err = DefaultCompressor(cfg.compression...)
if err != nil {
return cfg, nil, err
}
}
if cfg.decompressor == nil {
cfg.decompressor = DefaultDecompressor(cfg.pools...)
}
return cfg, seeds, nil
}
func namefn(fn any) string {
v := reflect.ValueOf(fn)
if v.Type().Kind() != reflect.Func {
return ""
}
name := runtime.FuncForPC(v.Pointer()).Name()
dot := strings.LastIndexByte(name, '.')
if dot >= 0 {
return name[dot+1:]
}
return name
}
// OptValue returns the value for the given configuration option. If the
// given option does not exist, this returns nil. This function takes either a
// raw Opt, or an Opt function name.
//
// If a configuration option has multiple inputs, this function returns only
// the first input. If the function is a boolean function (such as
// BlockRebalanceOnPoll), this function returns the value of the internal bool.
// Variadic option inputs are returned as a single slice. Options that are
// internally stored as a pointer (ClientID, TransactionalID, and InstanceID)
// are returned as their string input; you can see if the option is internally
// nil by looking at the second value returned from OptValues.
//
// var (
// cl, _ := NewClient(
// InstanceID("foo"),
// ConsumeTopics("foo", "bar"),
// )
// iid = cl.OptValue(InstanceID) // iid is "foo"
// gid = cl.OptValue(ConsumerGroup) // gid is "" since groups are not used
// topics = cl.OptValue("ConsumeTopics") // topics is []string{"foo", "bar"}; string lookup for the option works
// bpoll = cl.OptValue(BlockRebalanceOnPoll) // bpoll is false
// t = cl.OptValue(SessionTimeout) // t is 45s, the internal default
// td = t.(time.Duration) // safe conversion since SessionTimeout's input is a time.Duration
// unk = cl.OptValue("Unknown"), // unk is nil
// )
func (cl *Client) OptValue(opt any) any {
vs := cl.OptValues(opt)
if len(vs) > 0 {
return vs[0]
}
return nil
}
// OptValues returns all values for options. This method is useful for
// options that have multiple inputs (notably, SoftwareNameAndVersion). This is
// also useful for options that are internally stored as a pointer (ClientID,
// TransactionalID, and InstanceID) -- this function will return the string
// value of the option but also whether the option is non-nil. Boolean options
// are returned as a single-element slice with the bool value. Variadic inputs
// are returned as a signle slice. If the input option does not exist, this
// returns nil.
//
// var (
// cl, _ = NewClient(
// InstanceID("foo"),
// ConsumeTopics("foo", "bar"),
// )
// idValues = cl.OptValues(InstanceID) // idValues is []any{"foo", true}
// tValues = cl.OptValues(SessionTimeout) // tValues is []any{45 * time.Second}
// topics = cl.OptValues(ConsumeTopics) // topics is []any{[]string{"foo", "bar"}
// bpoll = cl.OptValues(BlockRebalanceOnPoll) // bpoll is []any{false}
// unknown = cl.OptValues("Unknown") // unknown is nil
// )
func (cl *Client) OptValues(opt any) []any {
name := namefn(opt)
if s, ok := opt.(string); ok {
name = s
}
cfg := &cl.cfg
switch name {
case namefn(ClientID):
if cfg.id != nil {
return []any{*cfg.id, true}
}
return []any{"", false}
case namefn(SoftwareNameAndVersion):
return []any{cfg.softwareName, cfg.softwareVersion}
case namefn(WithLogger):
if _, wrapped := cfg.logger.(*wrappedLogger); wrapped {
return []any{cfg.logger.(*wrappedLogger).inner}
}
return []any{nil}
case namefn(RequestTimeoutOverhead):
return []any{cfg.requestTimeoutOverhead}
case namefn(ConnIdleTimeout):
return []any{cfg.connIdleTimeout}
case namefn(Dialer):
return []any{cfg.dialFn}
case namefn(DialTLSConfig):
return []any{cfg.dialTLS}
case namefn(DialTLS):
return []any{cfg.dialTLS != nil}
case namefn(DialTimeout):
return []any{cfg.dialTimeout}
case namefn(SeedBrokers):
return []any{cfg.seedBrokers}
case namefn(MaxVersions):
return []any{cfg.maxVersions}
case namefn(MinVersions):
return []any{cfg.minVersions}
case namefn(RetryBackoffFn):
return []any{cfg.retryBackoff}
case namefn(RequestRetries):
return []any{cfg.retries}
case namefn(RetryTimeout):
return []any{cfg.retryTimeout(0)}
case namefn(RetryTimeoutFn):
return []any{cfg.retryTimeout}
case namefn(AllowAutoTopicCreation):
return []any{cfg.allowAutoTopicCreation}
case namefn(BrokerMaxWriteBytes):
return []any{cfg.maxBrokerWriteBytes}
case namefn(BrokerMaxReadBytes):
return []any{cfg.maxBrokerReadBytes}
case namefn(MetadataMaxAge):
return []any{cfg.metadataMaxAge}
case namefn(MetadataMinAge):
return []any{cfg.metadataMinAge}
case namefn(SASL):
return []any{cfg.sasls}
case namefn(WithHooks):
return []any{cfg.hooks}
case namefn(WithPools):
return []any{cfg.pools}
case namefn(ConcurrentTransactionsBackoff):
return []any{cfg.txnBackoff}
case namefn(ConsiderMissingTopicDeletedAfter):
return []any{cfg.missingTopicDelete}
case namefn(OnRebootstrapRequired):
return []any{cfg.onRebootstrapRequired}
case namefn(WithContext):
return []any{cfg.ctx}
case namefn(DisableClientMetrics):
return []any{cfg.disableClientMetrics}
case namefn(UserMetricsFn):
return []any{cfg.userMetrics}
case namefn(DefaultProduceTopic):
return []any{cfg.defaultProduceTopic}
case namefn(DefaultProduceTopicAlways):
return []any{cfg.defaultProduceTopicAlways}
case namefn(RequiredAcks):
return []any{cfg.acks}
case namefn(DisableIdempotentWrite):
return []any{cfg.disableIdempotency}
case namefn(MaxProduceRequestsInflightPerBroker):
return []any{cfg.maxProduceInflight}
case namefn(ProducerBatchCompression):
return []any{cfg.compression}
case namefn(WithCompressor):
return []any{cfg.compressor}
case namefn(ProducerBatchMaxBytes):
return []any{cfg.maxRecordBatchBytes}
case namefn(MaxBufferedRecords):
return []any{cfg.maxBufferedRecords}
case namefn(MaxBufferedBytes):
return []any{cfg.maxBufferedBytes}
case namefn(RecordPartitioner):
return []any{cfg.partitioner}
case namefn(ProduceRequestTimeout):
return []any{cfg.produceTimeout}
case namefn(RecordRetries):
return []any{cfg.recordRetries}
case namefn(UnknownTopicRetries):
return []any{cfg.maxUnknownFailures}
case namefn(StopProducerOnDataLossDetected):
return []any{cfg.stopOnDataLoss}
case namefn(ProducerOnDataLossDetected):
return []any{cfg.onDataLoss}
case namefn(ProducerLinger):
return []any{cfg.linger}
case namefn(ManualFlushing):
return []any{cfg.manualFlushing}
case namefn(RecordDeliveryTimeout):
return []any{cfg.recordTimeout}
case namefn(TransactionalID):
if cfg.txnID != nil {
return []any{cfg.txnID, true}
}
return []any{"", false}
case namefn(TransactionTimeout):
return []any{cfg.txnTimeout}
case namefn(ConsumePartitions):
return []any{cfg.partitions}
case namefn(ConsumePreferringLagFn):
return []any{cfg.preferLagFn}
case namefn(WithDecompressor):
return []any{cfg.decompressor}
case namefn(ConsumeRegex):
return []any{cfg.regex}
case namefn(ConsumeExcludeTopics):
return []any{slices.Collect(maps.Keys(cfg.excludeTopics))}
case namefn(ConsumeStartOffset):
return []any{cfg.startOffset}
case namefn(ConsumeResetOffset):
return []any{cfg.resetOffset}
case namefn(ConsumeTopics):
return []any{cfg.topics}
case namefn(DisableFetchSessions):
return []any{cfg.disableFetchSessions}
case namefn(FetchIsolationLevel):
return []any{cfg.isolationLevel}
case namefn(FetchMaxBytes):
return []any{int32(cfg.maxBytes)}
case namefn(FetchMaxPartitionBytes):
return []any{int32(cfg.maxPartBytes)}
case namefn(FetchMaxWait):
return []any{time.Duration(cfg.maxWait) * time.Millisecond}
case namefn(FetchMinBytes):
return []any{cfg.minBytes}
case namefn(KeepControlRecords):
return []any{cfg.keepControl}
case namefn(MaxConcurrentFetches):
return []any{cfg.maxConcurrentFetches}
case namefn(Rack):
return []any{cfg.rack}
case namefn(KeepRetryableFetchErrors):
return []any{cfg.keepRetryableFetchErrors}
case namefn(DisableFetchCRCValidation):
return []any{cfg.disableFetchCRCValidation}
case namefn(RecheckPreferredReplicaInterval):
return []any{cfg.recheckPreferredReplicaInterval}
case namefn(AdjustFetchOffsetsFn):
return []any{cfg.adjustOffsetsBeforeAssign}
case namefn(AutoCommitCallback):
return []any{cfg.commitCallback}
case namefn(AutoCommitInterval):
return []any{cfg.autocommitInterval}
case namefn(AutoCommitMarks):
return []any{cfg.autocommitMarks}
case namefn(Balancers):
return []any{cfg.balancers}
case namefn(BlockRebalanceOnPoll):
return []any{cfg.blockRebalanceOnPoll}
case namefn(ConsumerGroup):
return []any{cfg.group}
case namefn(DisableAutoCommit):
return []any{cfg.autocommitDisable}
case namefn(GreedyAutoCommit):
return []any{cfg.autocommitGreedy}
case namefn(GroupProtocol):
return []any{cfg.protocol}
case namefn(HeartbeatInterval):
return []any{cfg.heartbeatInterval}
case namefn(InstanceID):
if cfg.instanceID != nil {
return []any{*cfg.instanceID, true}
}
return []any{"", false}
case namefn(OnOffsetsFetched):
return []any{cfg.onFetched}
case namefn(OnPartitionsAssigned):
return []any{cfg.onAssigned}
case namefn(OnPartitionsLost):
return []any{cfg.onLost}
case namefn(OnPartitionsRevoked):
return []any{cfg.onRevoked}
case namefn(OnPartitionsCallbackBlocked):
return []any{cfg.onBlocked}
case namefn(RebalanceTimeout):
return []any{cfg.rebalanceTimeout}
case namefn(RequireStableFetchOffsets):
return []any{cfg.requireStable}
case namefn(SessionTimeout):
return []any{cfg.sessionTimeout}
default:
return nil
}
}
// NewClient returns a new Kafka client with the given options or an error if
// the options are invalid. Connections to brokers are lazily created only when
// requests are written to them.
//
// By default, the client uses the latest stable request versions when talking
// to Kafka. If you use a broker older than 0.10.0, then you need to manually
// set a MaxVersions option. Otherwise, there is usually no harm in defaulting
// to the latest API versions, although occasionally Kafka introduces new
// required parameters that do not have zero value defaults.
//
// NewClient also launches a goroutine which periodically updates the cached
// topic metadata.
func NewClient(opts ...Opt) (*Client, error) {
cfg, seeds, err := validateCfg(opts...)
if err != nil {
return nil, err
}
if cfg.retryTimeout == nil {
cfg.retryTimeout = func(key int16) time.Duration {
switch key {
case ((*kmsg.JoinGroupRequest)(nil)).Key(),
((*kmsg.SyncGroupRequest)(nil)).Key(),
((*kmsg.HeartbeatRequest)(nil)).Key(),
((*kmsg.ConsumerGroupHeartbeatRequest)(nil)).Key():
return cfg.sessionTimeout
}
return 30 * time.Second
}
}
if cfg.dialFn == nil {
dialer := &net.Dialer{Timeout: cfg.dialTimeout}
cfg.dialFn = dialer.DialContext
if cfg.dialTLS != nil {
cfg.dialFn = func(ctx context.Context, network, host string) (net.Conn, error) {
c := cfg.dialTLS.Clone()
if c.ServerName == "" {
server, _, err := net.SplitHostPort(host)
if err != nil {
return nil, fmt.Errorf("unable to split host:port for dialing: %w", err)
}
c.ServerName = server
}
return (&tls.Dialer{
NetDialer: dialer,
Config: c,
}).DialContext(ctx, network, host)
}
}
}
if cfg.setResetOffset && !cfg.setStartOffset {
cfg.startOffset = cfg.resetOffset
} else if cfg.setStartOffset && !cfg.setResetOffset {
cfg.resetOffset = cfg.startOffset
} // else they are both set (keep) or both unset (defaults)
ctx := context.Background()
if cfg.ctx != nil {
ctx = cfg.ctx
}
ctx, cancel := context.WithCancel(ctx)
cl := &Client{
cfg: cfg,
opts: opts,
ctx: ctx,
ctxCancel: cancel,
rng: func() func(func(*rand.Rand)) {
var mu sync.Mutex
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
return func(fn func(*rand.Rand)) {
mu.Lock()
defer mu.Unlock()
fn(rng)
}
}(),
controllerID: unknownControllerID,
sinksAndSources: make(map[int32]sinkAndSource),
reqFormatter: kmsg.NewRequestFormatter(),
connTimeouter: connTimeouter{def: cfg.requestTimeoutOverhead},
bufPool: newBufPool(),
prsPool: newPrsPool(),
coordinators: make(map[coordinatorKey]*coordinatorLoad),
updateMetadataCh: make(chan string, 1),
updateMetadataNowCh: make(chan string, 1),
blockingMetadataFnCh: make(chan func()),
metadone: make(chan struct{}),
}
// Before we start any goroutines below, we must notify any interested
// hooks of our existence.
cl.cfg.hooks.each(func(h Hook) {
if h, ok := h.(HookNewClient); ok {
h.OnNewClient(cl)
}
})
cl.producer.init(cl)
cl.consumer.init(cl)
cl.metawait.init()
cl.metrics.init(cl)
if cfg.id != nil {
cl.reqFormatter = kmsg.NewRequestFormatter(kmsg.FormatterClientID(*cfg.id))
}
seedBrokers := make([]*broker, 0, len(seeds))
for i, seed := range seeds {
b := cl.newBroker(unknownSeedID(i), seed.host, seed.port, nil)
seedBrokers = append(seedBrokers, b)
}
cl.seeds.Store(seedBrokers)
go cl.updateMetadataLoop()
go cl.reapConnectionsLoop()
go cl.pushMetrics()
return cl, nil
}
// Opts returns the options that were used to create this client. This can be
// as a base to generate a new client, where you can add override options to
// the end of the original input list. If you want to know a specific option
// value, you can use OptValue or OptValues.
func (cl *Client) Opts() []Opt {
return cl.opts
}
// Context returns the internal context used wherever possible in the client.
// By default this is context.WithCancel(context.Background()). You may
// override the background context with your own via [WithContext].
// The context is occasionally wrapped further internally in client subsystems.
func (cl *Client) Context() context.Context {
return cl.ctx
}
func (cl *Client) loadSeeds() []*broker {
return cl.seeds.Load().([]*broker)
}
// Ping returns whether any broker is reachable and that the client can
// communicate with it, iterating over any discovered broker or seed broker
// until one returns a successful response to a broker-only Metadata request.
// No discovered broker nor seed broker is attempted more than once. If all
// requests fail, this returns final error.
func (cl *Client) Ping(ctx context.Context) error {
req := kmsg.NewPtrMetadataRequest()
req.Topics = []kmsg.MetadataRequestTopic{}
cl.brokersMu.RLock()
brokers := slices.Clone(cl.brokers)
cl.brokersMu.RUnlock()
var lastErr error
for _, brs := range [2][]*broker{
brokers,
cl.loadSeeds(),
} {
for _, br := range brs {
resp, err := br.waitResp(ctx, req)
if lastErr = err; lastErr == nil {
cl.updateMetadataBrokers(resp.(*kmsg.MetadataResponse))
return nil
} else if isContextErr(lastErr) && ctx.Err() != nil {
// No point in trying the next broker if context is done
// as it will create noise in OnBrokerConnect hook.
// Check both lastErr and ctx.Err() to avoid race condition
// where context error happens immediately after waitResp.
return lastErr
}
}
}
return lastErr
}
// PurgeTopicsFromClient internally removes all internal information about the
// input topics. If you want to purge information for only consuming or
// only producing, see the related functions [PurgeTopicsFromConsuming] and
// [PurgeTopicsFromProducing].
//
// For producing, this clears all knowledge that these topics have ever been
// produced to. Producing to the topic again may result in out of order
// sequence number errors, or, if idempotency is disabled and the sequence
// numbers align, may result in invisibly discarded records at the broker.
// Purging a topic that was previously produced to may be useful to free up
// resources if you are producing to many disparate and short lived topic in
// the lifetime of this client and you do not plan to produce to the topic
// anymore. You may want to flush buffered records before purging if records
// for a topic you are purging are currently in flight.
//
// For consuming, this removes all concept of the topic from being consumed.
// This is different from PauseFetchTopics, which literally pauses the fetching
// of topics but keeps the topic information around for resuming fetching
// later. Purging a topic that was being consumed can be useful if you know the
// topic no longer exists, or if you are consuming via regex and know that some
// previously consumed topics no longer exist, or if you simply do not want to
// ever consume from a topic again. If you are group consuming, this function
// will likely cause a rebalance. If you are consuming via regex and the topic
// still exists on the broker, this function will at most only temporarily
// remove the topic from the client and the topic will be re-discovered.
//
// For admin requests, this deletes the topic from the cached metadata map for
// sharded requests. Metadata for sharded admin requests is only cached for
// MetadataMinAge anyway, but the map is not cleaned up one the metadata
// expires. This function ensures the map is purged.
func (cl *Client) PurgeTopicsFromClient(topics ...string) {
if len(topics) == 0 {
return
}
sort.Strings(topics) // for logging in the functions
cl.blockingMetadataFn(func() { // make reasoning about concurrency easier
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
cl.producer.purgeTopics(topics)
}()
go func() {
defer wg.Done()
cl.consumer.purgeTopics(topics)
}()
wg.Wait()
})
cl.mappedMetaMu.Lock()
for _, t := range topics {
delete(cl.mappedMeta, t)
}
cl.mappedMetaMu.Unlock()
}
// PurgeTopicsFromProducing internally removes all internal information for
// producing about the input topics. This runs the producer bit of logic that
// is documented in [PurgeTopicsFromClient]; see that function for more
// details.
func (cl *Client) PurgeTopicsFromProducing(topics ...string) {
if len(topics) == 0 {
return
}
sort.Strings(topics)
cl.blockingMetadataFn(func() {
cl.producer.purgeTopics(topics)
})
}
// PurgeTopicsFromConsuming internally removes all internal information for
// consuming about the input topics. This runs the consumer bit of logic that
// is documented in [PurgeTopicsFromClient]; see that function for more
// details.
func (cl *Client) PurgeTopicsFromConsuming(topics ...string) {
if len(topics) == 0 {
return
}
sort.Strings(topics)
cl.blockingMetadataFn(func() {
cl.consumer.purgeTopics(topics)
})
}
// Parse broker IP/host and port from a string, using the default Kafka port if
// unspecified. Supported address formats:
//
// - IPv4 host/IP without port: "127.0.0.1", "localhost"
// - IPv4 host/IP with port: "127.0.0.1:1234", "localhost:1234"
// - IPv6 IP without port: "[2001:1000:2000::1]", "::1"
// - IPv6 IP with port: "[2001:1000:2000::1]:1234"
func parseBrokerAddr(addr string) (hostport, error) {
const defaultKafkaPort = 9092
// Bracketed IPv6
if strings.IndexByte(addr, '[') == 0 {
parts := strings.Split(addr[1:], "]")
if len(parts) != 2 {
return hostport{}, fmt.Errorf("invalid addr: %s", addr)
}
// No port specified -> use default
if len(parts[1]) == 0 {
return hostport{parts[0], defaultKafkaPort}, nil
}
port, err := strconv.ParseInt(parts[1][1:], 10, 32)
if err != nil {
return hostport{}, fmt.Errorf("unable to parse port from addr: %w", err)
}
return hostport{parts[0], int32(port)}, nil
}
// IPv4 with no port
if strings.IndexByte(addr, ':') == -1 {
return hostport{addr, defaultKafkaPort}, nil
}
// Either a IPv6 literal ("::1"), IP:port or host:port
// Try to parse as IP:port or host:port
h, p, err := net.SplitHostPort(addr)
if err != nil {
return hostport{addr, defaultKafkaPort}, nil //nolint:nilerr // ipv6 literal -- use default kafka port
}
port, err := strconv.ParseInt(p, 10, 32)
if err != nil {
return hostport{}, fmt.Errorf("unable to parse port from addr: %w", err)
}
return hostport{h, int32(port)}, nil
}
type connTimeouter struct {
def time.Duration
joinMu sync.Mutex
lastRebalanceTimeout time.Duration
}
func (c *connTimeouter) timeouts(req kmsg.Request) (r, w time.Duration) {
def := c.def
millis := func(m int32) time.Duration { return time.Duration(m) * time.Millisecond }
switch t := req.(type) {
default:
if timeoutRequest, ok := req.(kmsg.TimeoutRequest); ok {
timeoutMillis := timeoutRequest.Timeout()
return def + millis(timeoutMillis), def
}
return def, def
case *produceRequest:
return def + millis(t.timeout), def
case *fetchRequest:
return def + millis(t.maxWait), def
case *kmsg.FetchRequest:
return def + millis(t.MaxWaitMillis), def
// Join and sync can take a long time. Sync has no notion of
// timeouts, but since the flow of requests should be first
// join, then sync, we can stash the timeout from the join.
case *kmsg.JoinGroupRequest:
c.joinMu.Lock()
c.lastRebalanceTimeout = millis(t.RebalanceTimeoutMillis)
c.joinMu.Unlock()
return def + millis(t.RebalanceTimeoutMillis), def
case *kmsg.SyncGroupRequest:
read := def
c.joinMu.Lock()
if c.lastRebalanceTimeout != 0 {
read = c.lastRebalanceTimeout
}
c.joinMu.Unlock()
return read, def
}
}
func (cl *Client) reinitAnyBrokerOrd() {
cl.anyBrokerOrd = append(cl.anyBrokerOrd[:0], make([]int32, len(cl.brokers))...)
for i := range cl.anyBrokerOrd {
cl.anyBrokerOrd[i] = int32(i)
}
cl.rng(func(r *rand.Rand) {
r.Shuffle(len(cl.anyBrokerOrd), func(i, j int) {
cl.anyBrokerOrd[i], cl.anyBrokerOrd[j] = cl.anyBrokerOrd[j], cl.anyBrokerOrd[i]
})
})
}
// broker returns a random broker from all brokers ever known.
func (cl *Client) broker() *broker {
cl.brokersMu.Lock()
defer cl.brokersMu.Unlock()
// Every time we loop through all discovered brokers, we issue one
// request to the next seed. This ensures that if all discovered
// brokers are down, we will *eventually* loop through seeds and
// hopefully have a reachable seed.
var b *broker
if len(cl.anyBrokerOrd) > 0 {
b = cl.brokers[cl.anyBrokerOrd[0]]
cl.anyBrokerOrd = cl.anyBrokerOrd[1:]
return b
}
seeds := cl.loadSeeds()
cl.anySeedIdx %= int32(len(seeds))
b = seeds[cl.anySeedIdx]
cl.anySeedIdx++
// If we have brokers, we ranged past discovered brokers.
// We now reset the anyBrokerOrd to begin ranging through
// discovered brokers again. If there are still no brokers,
// this reinit will do nothing and we will keep looping seeds.
cl.reinitAnyBrokerOrd()
return b
}
func (cl *Client) waitTries(ctx context.Context, backoff time.Duration) bool {
after := time.NewTimer(backoff)
defer after.Stop()
select {
case <-ctx.Done():
return false
case <-cl.ctx.Done():
return false
case <-after.C:
return true
}
}
// A broker may sometimes indicate it supports offset for leader epoch v2+ when
// it does not. We need to catch that and avoid issuing offset for leader
// epoch, because we will just loop continuously failing. We do not catch every
// case, such as when a person explicitly assigns offsets with epochs, but we
// catch a few areas that would be returned from a broker itself.
//
// This function is always used *after* at least one request has been issued.
//
// NOTE: This is a weak check; we check if any broker in the cluster supports
// the request. We use this function in three locations:
//
// 1. When using the LeaderEpoch returned in a metadata response. This guards
// against buggy brokers that return 0 rather than -1 even if they do not
// support OffsetForLeaderEpoch. If any support, the cluster is in the
// middle of an upgrade and we can start using the epoch.
// 2. When deciding whether to keep LeaderEpoch from fetched offsets.
// Realistically, clients should only commit epochs if the cluster supports
// them.
// 3. When receiving OffsetOutOfRange when follower fetching and we fetched
// past the end.
//
// In any of these cases, if we OffsetForLeaderEpoch against a broker that does
// not support (even though one in the cluster does), we will loop fail until
// the rest of the cluster is upgraded and supports the request.
func (cl *Client) supportsOffsetForLeaderEpoch() bool {
return cl.supportsKeyVersion(int16(kmsg.OffsetForLeaderEpoch), 2)
}
// Called after the first metadata request, before we go into either
// (*groupConsumer).manage or (*groupConsumer).manage848.
//
// v1 introduces support for regex and requires the client to generate
// the member ID, and fully stabilizes KIP-848.
func (cl *Client) supportsKIP848v1() bool {
return cl.supportsKeyVersion(int16(kmsg.ConsumerGroupHeartbeat), 1)
}
// Called after the first metric observed, which is always after a response.
func (cl *Client) supportsClientMetrics() bool {
return cl.supportsKeyVersion(int16(kmsg.GetTelemetrySubscriptions), 0) &&
cl.supportsKeyVersion(int16(kmsg.PushTelemetry), 0)
}
// A broker may not support some requests we want to make. This function checks
// support. This should only be used *after* at least one successful response.
func (cl *Client) supportsKeyVersion(key, version int16) bool {
cl.brokersMu.RLock()
defer cl.brokersMu.RUnlock()
for _, brokers := range [][]*broker{
cl.brokers,
cl.loadSeeds(),
} {
for _, b := range brokers {
if v := b.loadVersions(); v != nil && v.maxVersion(key) >= version {
return true
}
}
}
return false
}
func (cl *Client) supportsKIP890p2() bool {
return cl.supportsFeature("transaction.version", 2)
}
// Same as above. A cluster returns a max version for a feature only once the
// entire cluster supports the feature. This should only be used *after* at
// least one successful response.
func (cl *Client) supportsFeature(name string, version int16) bool {
cl.brokersMu.RLock()
defer cl.brokersMu.RUnlock()
for _, brokers := range [][]*broker{
cl.brokers,
cl.loadSeeds(),
} {
for _, b := range brokers {
if v := b.loadVersions(); v != nil && v.features[name] >= version {
return true
}
}
}
return false
}
// fetchBrokerMetadata issues a metadata request solely for broker information.
func (cl *Client) fetchBrokerMetadata(ctx context.Context) error {
cl.fetchingBrokersMu.Lock()
wait := cl.fetchingBrokers
if wait != nil {
cl.fetchingBrokersMu.Unlock()
<-wait.done
return wait.err
}
wait = &struct {
done chan struct{}
err error
}{done: make(chan struct{})}
cl.fetchingBrokers = wait
cl.fetchingBrokersMu.Unlock()
defer func() {
cl.fetchingBrokersMu.Lock()
defer cl.fetchingBrokersMu.Unlock()
cl.fetchingBrokers = nil
close(wait.done)
}()
req := kmsg.NewPtrMetadataRequest()
req.Topics = []kmsg.MetadataRequestTopic{}
_, _, wait.err = cl.fetchMetadata(ctx, req, true, nil)
return wait.err
}
func (cl *Client) fetchMetadataForTopics(ctx context.Context, all bool, topics []string, intoMapped map[string]mappedMetadataTopic) (*broker, *kmsg.MetadataResponse, error) {
req := kmsg.NewPtrMetadataRequest()
req.AllowAutoTopicCreation = cl.cfg.allowAutoTopicCreation
if all {
req.Topics = nil
} else if len(topics) == 0 {
req.Topics = []kmsg.MetadataRequestTopic{}
} else {
for _, topic := range topics {
reqTopic := kmsg.NewMetadataRequestTopic()
reqTopic.Topic = kmsg.StringPtr(topic)
req.Topics = append(req.Topics, reqTopic)
}
}
return cl.fetchMetadata(ctx, req, true, intoMapped)
}
func (cl *Client) fetchMetadata(ctx context.Context, req *kmsg.MetadataRequest, limitRetries bool, intoMapped map[string]mappedMetadataTopic) (*broker, *kmsg.MetadataResponse, error) {
r := cl.retryable()
var rebootstrapped bool
start:
// We limit retries for internal metadata refreshes, because these do
// not need to retry forever and are usually blocking *other* requests.
// e.g., producing bumps load errors when metadata returns, so 3
// failures here will correspond to 1 bumped error count. To make the
// number more accurate, we should *never* retry here, but this is
// pretty intolerant of immediately-temporary network issues. Rather,
// we use a small count of 3 retries, which with the default backoff,
// will be <2s of retrying. This is still intolerant of temporary
// failures, but it does allow recovery from a dns issue / bad path.
if limitRetries {
r.limitRetries = 3
}
meta, err := req.RequestWith(ctx, r)
if err == nil {
if err = kerr.ErrorForCode(meta.ErrorCode); !rebootstrapped && errors.Is(err, kerr.RebootstrapRequired) && cl.cfg.onRebootstrapRequired != nil {
var seeds []string
seeds, err = cl.cfg.onRebootstrapRequired()
if err == nil && len(seeds) > 0 {
err = cl.UpdateSeedBrokers(seeds...)
if err == nil {
cl.updateBrokers(nil)
rebootstrapped = true
goto start
}
}
}
cl.updateMetadataBrokers(meta)
// Cache the mapped metadata, and potentially store each topic in the results.
cl.storeCachedMappedMetadata(meta, intoMapped)
}
return r.last, meta, err
}
func (cl *Client) updateMetadataBrokers(resp *kmsg.MetadataResponse) {
cl.controllerIDMu.Lock()
if resp.ControllerID >= 0 {
cl.controllerID = resp.ControllerID
}
cl.clusterID = resp.ClusterID
cl.controllerIDMu.Unlock()
cl.updateBrokers(resp.Brokers)
}
// updateBrokers is called with the broker portion of every metadata response.
// All metadata responses contain all known live brokers, so we can always
// use the response.
func (cl *Client) updateBrokers(brokers []kmsg.MetadataResponseBroker) {
sort.Slice(brokers, func(i, j int) bool { return brokers[i].NodeID < brokers[j].NodeID })
newBrokers := make([]*broker, 0, len(brokers))
cl.brokersMu.Lock()
defer cl.brokersMu.Unlock()
if cl.stopBrokers {
return
}
for len(brokers) > 0 && len(cl.brokers) > 0 {
ob := cl.brokers[0]
nb := brokers[0]
switch {
case ob.meta.NodeID < nb.NodeID:
ob.stopForever()
cl.brokers = cl.brokers[1:]
case ob.meta.NodeID == nb.NodeID:
if !ob.meta.equals(nb) {
ob.stopForever()
ob = cl.newBroker(nb.NodeID, nb.Host, nb.Port, nb.Rack)
}
newBrokers = append(newBrokers, ob)
cl.brokers = cl.brokers[1:]
brokers = brokers[1:]
case ob.meta.NodeID > nb.NodeID:
newBrokers = append(newBrokers, cl.newBroker(nb.NodeID, nb.Host, nb.Port, nb.Rack))
brokers = brokers[1:]
}
}
for len(cl.brokers) > 0 {
ob := cl.brokers[0]
ob.stopForever()
cl.brokers = cl.brokers[1:]
}
for len(brokers) > 0 {
nb := brokers[0]
newBrokers = append(newBrokers, cl.newBroker(nb.NodeID, nb.Host, nb.Port, nb.Rack))
brokers = brokers[1:]
}
cl.brokers = newBrokers
cl.reinitAnyBrokerOrd()
}
// CloseAllowingRebalance allows rebalances, leaves any group, and closes all
// connections and goroutines. This function is only useful if you are using
// the BlockRebalanceOnPoll option. Close itself does not allow rebalances and
// will hang if you polled, did not allow rebalances, and want to close. Close
// does not automatically allow rebalances because leaving a group causes a
// revoke, and the client does not assume that the final revoke is concurrency
// safe. The CloseAllowingRebalance function exists a shortcut to opt into
// allowing rebalance while closing.
//
// If you are using static membership, CloseAllowingRebalance will NOT send a
// leave group request. See InstanceID for more details.
func (cl *Client) CloseAllowingRebalance() {
cl.AllowRebalance()
cl.Close()
}
// Close leaves any group and closes all connections and goroutines. This
// function waits for the group to be left. If you want to force leave a group
// immediately and ensure a speedy shutdown you can use LeaveGroupContext first
// (and then Close will be immediate).
//
// If you are group consuming and have overridden the default
// OnPartitionsRevoked, you must manually commit offsets before closing the
// client.
//
// If you are using the BlockRebalanceOnPoll option and have polled, this
// function does not automatically allow rebalancing. You must AllowRebalance
// before calling this function. Internally, this function leaves the group,
// and leaving a group causes a rebalance so that you can get one final
// notification of revoked partitions. If you want to automatically allow
// rebalancing, use CloseAllowingRebalance.
//
// If you are using static membership, Close will NOT send a leave group
// request. See InstanceID for more details.
func (cl *Client) Close() {
cl.close(cl.ctx)
}
func (cl *Client) close(ctx context.Context) (rerr error) {
defer cl.cfg.hooks.each(func(h Hook) {
if h, ok := h.(HookClientClosed); ok {
h.OnClientClosed(cl)
}
})
c := &cl.consumer
c.kill.Store(true)
if c.g != nil {
rerr = cl.LeaveGroupContext(ctx)
} else if c.d != nil {
c.mu.Lock() // lock for assign
c.assignPartitions(nil, assignInvalidateAll, nil, "") // we do not use a log message when not in a group
c.mu.Unlock()
}
// After the above, consumers cannot consume anymore. LeaveGroup
// internally assigns nil, which uses noConsumerSession, which prevents
// loopFetch from starting. Assigning also waits for the prior session
// to be complete, meaning loopFetch cannot be running.
sessCloseCtx, sessCloseCancel := context.WithTimeout(ctx, time.Second)
var wg sync.WaitGroup
cl.allSinksAndSources(func(sns sinkAndSource) {
if sns.source.session.id != 0 {
sns := sns
wg.Add(1)
go func() {
defer wg.Done()
sns.source.killSessionOnClose(sessCloseCtx)
}()
}
})
wg.Wait()
sessCloseCancel()
// Now we kill the client context and all brokers, ensuring all
// requests fail. This will finish all producer callbacks and
// stop the metadata loop and metrics loop.
cl.ctxCancel()
// Before killing brokers, give metrics 1s to push any final
// terminating message. The client context cancelation awakens
// the push-period-wait loop.
after := time.NewTimer(time.Second)
select {
case <-cl.metrics.ctx.Done():
case <-after.C:
cl.metrics.ctxCancel()
case <-ctx.Done():
cl.metrics.ctxCancel()
}
cl.brokersMu.Lock()
cl.stopBrokers = true
for _, broker := range cl.brokers {
broker.stopForever()
}
cl.brokersMu.Unlock()
for _, broker := range cl.loadSeeds() {
broker.stopForever()
}
// Wait for metadata to quit so we know no more erroring topic
// partitions will be created. After metadata has quit, we can
// safely stop sinks and sources, as no more will be made.
<-cl.metadone
// We do not need a lock in `sink` and `source` access because,
// with the metadata loop done, partition migration will not occur.
for _, sns := range cl.sinksAndSources {
sns.sink.maybeDrain() // awaken anything in backoff
sns.source.maybeConsume() // same
}
cl.failBufferedRecords(ErrClientClosed)
// We need one final poll: if any sources buffered a fetch, then the
// manageFetchConcurrency loop only exits when all fetches have been
// drained, because draining a fetch is what decrements an "active"
// fetch. PollFetches with `nil` is instant.
cl.PollFetches(nil)
for _, s := range cl.cfg.sasls {
if closing, ok := s.(sasl.ClosingMechanism); ok {
closing.Close()
}
}
return rerr
}
// Request issues a request to Kafka, waiting for and returning the response.
// If a retryable network error occurs, or if a retryable group / transaction
// coordinator error occurs, the request is retried. All other errors are
// returned.
//
// If the request is an admin request, this will issue it to the Kafka
// controller. If the controller ID is unknown, this will attempt to fetch it.
// If the fetch errors, this will return an unknown controller error.
//
// If the request is a group or transaction coordinator request, this will
// issue the request to the appropriate group or transaction coordinator.
//
// For transaction requests, the request is issued to the transaction
// coordinator. However, if the request is an init producer ID request and the
// request has no transactional ID, the request goes to any broker.
//
// Some requests need to be split and sent to many brokers. For these requests,
// it is *highly* recommended to use RequestSharded. Not all responses from
// many brokers can be cleanly merged. However, for the requests that are
// split, this does attempt to merge them in a sane way.
//
// The following requests are split:
//
// ListOffsets
// OffsetFetch (if using v8+ for Kafka 3.0+)
// FindCoordinator (if using v4+ for Kafka 3.0+)
// DescribeGroups
// ListGroups
// DeleteRecords
// OffsetForLeaderEpoch
// DescribeConfigs
// AlterConfigs
// AlterReplicaLogDirs
// DescribeLogDirs
// DeleteGroups
// IncrementalAlterConfigs
// DescribeProducers
// DescribeTransactions
// ListTransactions
// ConsumerGroupDescribe
// ShareGroupDescribe
//
// Kafka 3.0 introduced batch OffsetFetch and batch FindCoordinator requests.
// This function is forward and backward compatible: old requests will be
// batched as necessary, and batched requests will be split as necessary. It is
// recommended to always use batch requests for simplicity.
//
// In short, this method tries to do the correct thing depending on what type
// of request is being issued.
//
// The passed context can be used to cancel a request and return early. Note
// that if the request was written to Kafka but the context canceled before a
// response is received, Kafka may still operate on the received request.
//
// If using this function to issue kmsg.ProduceRequest's, you must configure
// the client with the same RequiredAcks option that you use in the request.
// If you are issuing produce requests with 0 acks, you must configure the
// client with the same timeout you use in the request. The client will
// internally rewrite the incoming request's acks to match the client's
// configuration, and it will rewrite the timeout millis if the acks is 0. It
// is strongly recommended to not issue raw kmsg.ProduceRequest's.
func (cl *Client) Request(ctx context.Context, req kmsg.Request) (kmsg.Response, error) {
resps, merge := cl.shardedRequest(ctx, req)
// If there is no merge function, only one request was issued directly
// to a broker. Return the resp and err directly.
if merge == nil {
return resps[0].Resp, resps[0].Err
}
return merge(resps)
}
// RequestCachedMetadata returns a metadata response, using any cached topic
// data possible. Any topic with data cached longer than 'limit' has its
// metadata updated before being returned. If limit is zero or less,
// MetadataMinAge is used (default 5s).
//
// This function is useful if you run a lot of functions that internally
// fetch metadata to execute. As an example, many functions in the kadm
// package all require metadata to run; those functions use cached metadata
// as much as possible.
//
// This function does *not* return authorized operations, even if the request
// has IncludeClusterAuthorizedOperations or IncludeTopicAuthorizedOperations
// set to true. This function cannot be used to request topics via TopicID;
// the direct topic name must be used.
func (cl *Client) RequestCachedMetadata(ctx context.Context, req *kmsg.MetadataRequest, limit time.Duration) (*kmsg.MetadataResponse, error) {
var topics []string
if req.Topics != nil {
topics = make([]string, 0, len(req.Topics))
}
for _, t := range req.Topics {
if t.Topic == nil || *t.Topic == "" {
return nil, errors.New("unable to request cached metadata with a missing topic name (topic IDs are not supported)")
}
topics = append(topics, *t.Topic)
}
mapped, err := cl.fetchMappedMetadata(ctx, topics, true, limit)
if err != nil {
return nil, err
}
// With potentially cached data, we build the response. We deeply clone
// all cached data so that the end user cannot modify internal data.
dups := func(s *string) *string {
if s == nil {
return nil
}
s2 := *s
return &s2
}
dupp := func(p kmsg.MetadataResponseTopicPartition) kmsg.MetadataResponseTopicPartition {
p2 := p
p2.Replicas = slices.Clone(p2.Replicas)
p2.ISR = slices.Clone(p2.ISR)
p2.OfflineReplicas = slices.Clone(p2.OfflineReplicas)
p2.UnknownTags = kmsg.Tags{}
return p2
}
dupt := func(t kmsg.MetadataResponseTopic) kmsg.MetadataResponseTopic {
t2 := t
t2.Topic = dups(t2.Topic)
t2.Partitions = make([]kmsg.MetadataResponseTopicPartition, 0, len(t2.Partitions))
for _, p := range t.Partitions {
t2.Partitions = append(t2.Partitions, dupp(p))
}
t2.AuthorizedOperations = math.MinInt32
t2.UnknownTags = kmsg.Tags{}
return t2
}
resp := kmsg.NewPtrMetadataResponse()
cl.brokersMu.RLock()
for _, b := range cl.brokers {
resp.Brokers = append(resp.Brokers, kmsg.MetadataResponseBroker{
NodeID: b.meta.NodeID,
Host: b.meta.Host,
Port: b.meta.Port,
Rack: b.meta.Rack,
})
}
cl.brokersMu.RUnlock()
cl.controllerIDMu.Lock()
resp.ClusterID = dups(cl.clusterID)
resp.ControllerID = cl.controllerID
cl.controllerIDMu.Unlock()
for _, t := range mapped {
resp.Topics = append(resp.Topics, dupt(t.t))
}
resp.AuthorizedOperations = math.MinInt32
return resp, nil
}
func (cl *Client) retryable() *retryable {
return cl.retryableBrokerFn(func() (*broker, error) { return cl.broker(), nil })
}
func (cl *Client) retryableBrokerFn(fn func() (*broker, error)) *retryable {
return &retryable{cl: cl, br: fn}
}
func (cl *Client) shouldRetry(tries int, err error) bool {
return (kerr.IsRetriable(err) || isRetryableBrokerErr(err)) && int64(tries) <= cl.cfg.retries
}
func (cl *Client) shouldRetryNext(tries int, err error) bool {
return isSkippableBrokerErr(err) && int64(tries) <= cl.cfg.retries
}
type retryable struct {
cl *Client
br func() (*broker, error)
last *broker
// If non-zero, limitRetries may specify a smaller # of retries than
// the client RequestRetries number. This is used for internal requests
// that can fail / do not need to retry forever.
limitRetries int
// parseRetryErr, if non-nil, can delete stale cached brokers. We do
// *not* return the error from this function to the caller, but we do
// use it to potentially retry. It is not necessary, but also not
// harmful, to return the input error.
parseRetryErr func(kmsg.Response, error) error
}
type failDial struct{ fails int8 }
// The controller and group/txn coordinators are cached. If dialing the broker
// repeatedly fails, we need to forget our cache to force a re-load: the broker
// may have completely died.
func (d *failDial) isRepeatedDialFail(err error) bool {
if isAnyDialErr(err) {
d.fails++
if d.fails == 3 {
d.fails = 0
return true
}
}
return false
}
func (r *retryable) Request(ctx context.Context, req kmsg.Request) (kmsg.Response, error) {
tries := 0
tryStart := time.Now()
retryTimeout := r.cl.cfg.retryTimeout(req.Key())
next, nextErr := r.br()
start:
tries++
br, err := next, nextErr
r.last = br
var resp kmsg.Response
var retryErr error
if err == nil {
resp, err = r.last.waitResp(ctx, req)
if r.parseRetryErr != nil {
retryErr = r.parseRetryErr(resp, err)
}
}
log := func(backoff time.Duration) {
r.cl.cfg.logger.Log(LogLevelDebug, "retrying request",
"request", kmsg.NameForKey(req.Key()),
"tries", tries,
"backoff", backoff,
"time_since_start", time.Since(tryStart),
"request_error", err,
"response_error", retryErr,
)
}
if err != nil || retryErr != nil {
if r.limitRetries == 0 || tries <= r.limitRetries {
backoff := r.cl.cfg.retryBackoff(tries)
if retryTimeout == 0 || time.Now().Add(backoff).Sub(tryStart) <= retryTimeout {
// If this broker / request had a retryable error, we can
// just retry now. If the error is *not* retryable but
// is a broker-specific network error, and the next
// broker is different than the current, we also retry.
if r.cl.shouldRetry(tries, err) || r.cl.shouldRetry(tries, retryErr) {
log(backoff)
if r.cl.waitTries(ctx, backoff) {
next, nextErr = r.br()
goto start
}
} else if r.cl.shouldRetryNext(tries, err) {
log(backoff)
next, nextErr = r.br()
if next != br && r.cl.waitTries(ctx, backoff) {
goto start
}
}
}
}
}
return resp, err
}
// ResponseShard ties together a request with either the response it received
// or an error that prevented a response from being received.
type ResponseShard struct {
// Meta contains the broker that this request was issued to, or an
// unknown (node ID -1) metadata if the request could not be issued.
//
// Requests can fail to even be issued if an appropriate broker cannot
// be loaded of if the client cannot understand the request.
Meta BrokerMetadata
// Req is the request that was issued to this broker.
Req kmsg.Request
// Resp is the response received from the broker, if any.
Resp kmsg.Response
// Err, if non-nil, is the error that prevented a response from being
// received or the request from being issued.
Err error
}
// RequestSharded performs the same logic as Request, but returns all responses
// from any broker that the request was split to. This always returns at least
// one shard. If the request does not need to be issued (describing no groups),
// this issues the request to a random broker just to ensure that one shard
// exists.
//
// There are only a few requests that are strongly recommended to explicitly
// use RequestSharded; the rest can by default use Request. These few requests
// are mentioned in the documentation for Request.
//
// If, in the process of splitting a request, some topics or partitions are
// found to not exist, or Kafka replies that a request should go to a broker
// that does not exist, all those non-existent pieces are grouped into one
// request to the first seed broker. This will show up as a seed broker node ID
// (min int32) and the response will likely contain purely errors.
//
// The response shards are ordered by broker metadata.
func (cl *Client) RequestSharded(ctx context.Context, req kmsg.Request) []ResponseShard {
resps, _ := cl.shardedRequest(ctx, req)
sort.Slice(resps, func(i, j int) bool {
l := &resps[i].Meta
r := &resps[j].Meta
if l.NodeID < r.NodeID {
return true
}
if r.NodeID < l.NodeID {
return false
}
if l.Host < r.Host {
return true
}
if r.Host < l.Host {
return false
}
if l.Port < r.Port {
return true
}
if r.Port < l.Port {
return false
}
if l.Rack == nil {
return true
}
if r.Rack == nil {
return false
}
return *l.Rack < *r.Rack
})
return resps
}
type shardMerge func([]ResponseShard) (kmsg.Response, error)
func (cl *Client) shardedRequest(ctx context.Context, req kmsg.Request) ([]ResponseShard, shardMerge) {
ctx, cancel := context.WithCancel(ctx)
done := make(chan struct{})
defer close(done)
go func() {
defer cancel()
select {
case <-done:
case <-ctx.Done():
case <-cl.ctx.Done():
}
}()
// First, handle any sharded request. This comes before the conditional
// below because this handles two group requests, which we do not want
// to fall into the handleCoordinatorReq logic.
switch t := req.(type) {
case *kmsg.ListOffsetsRequest, // key 2
*kmsg.OffsetFetchRequest, // key 9
*kmsg.FindCoordinatorRequest, // key 10
*kmsg.DescribeGroupsRequest, // key 15
*kmsg.ListGroupsRequest, // key 16
*kmsg.DeleteRecordsRequest, // key 21
*kmsg.OffsetForLeaderEpochRequest, // key 23
*kmsg.AddPartitionsToTxnRequest, // key 24
*kmsg.WriteTxnMarkersRequest, // key 27
*kmsg.DescribeConfigsRequest, // key 32
*kmsg.AlterConfigsRequest, // key 33
*kmsg.AlterReplicaLogDirsRequest, // key 34
*kmsg.DescribeLogDirsRequest, // key 35
*kmsg.DeleteGroupsRequest, // key 42
*kmsg.IncrementalAlterConfigsRequest, // key 44
*kmsg.DescribeProducersRequest, // key 61
*kmsg.DescribeTransactionsRequest, // key 65
*kmsg.ListTransactionsRequest, // key 66
*kmsg.ConsumerGroupDescribeRequest, // key 69
*kmsg.ShareGroupDescribeRequest: // key 77
return cl.handleShardedReq(ctx, req)
case *kmsg.MetadataRequest:
// We hijack any metadata request so as to populate our
// own brokers and controller ID.
br, resp, err := cl.fetchMetadata(ctx, t, false, nil)
return shards(shard(br, req, resp, err)), nil
case kmsg.AdminRequest:
return shards(cl.handleAdminReq(ctx, t)), nil
case kmsg.GroupCoordinatorRequest,
kmsg.TxnCoordinatorRequest:
return shards(cl.handleCoordinatorReq(ctx, t)), nil
case *kmsg.ApiVersionsRequest:
// As of v3, software name and version are required.
// If they are missing, we use the config options.
if t.ClientSoftwareName == "" && t.ClientSoftwareVersion == "" {
dup := *t
dup.ClientSoftwareName = cl.cfg.softwareName
dup.ClientSoftwareVersion = cl.cfg.softwareVersion
req = &dup
}
}
// All other requests not handled above can be issued to any broker
// with the default retryable logic.
r := cl.retryable()
resp, err := r.Request(ctx, req)
return shards(shard(r.last, req, resp, err)), nil
}
func shard(br *broker, req kmsg.Request, resp kmsg.Response, err error) ResponseShard {
if br == nil { // the broker could be nil if loading the broker failed.
return ResponseShard{unknownBrokerMetadata, req, resp, err}
}
return ResponseShard{br.meta, req, resp, err}
}
func shards(shard ...ResponseShard) []ResponseShard {
return shard
}
func findBroker(candidates []*broker, node int32) *broker {
n := sort.Search(len(candidates), func(n int) bool { return candidates[n].meta.NodeID >= node })
var b *broker
if n < len(candidates) {
c := candidates[n]
if c.meta.NodeID == node {
b = c
}
}
return b
}
// brokerOrErr returns the broker for ID or the error if the broker does not
// exist.
//
// If tryLoad is true and the broker does not exist, this attempts a broker
// metadata load once before failing. If the metadata load fails, this returns
// that error.
func (cl *Client) brokerOrErr(ctx context.Context, id int32, err error) (*broker, error) {
if id < 0 {
return nil, err
}
tryLoad := ctx != nil
tries := 0
start:
var broker *broker
if id < 0 {
broker = findBroker(cl.loadSeeds(), id)
} else {
cl.brokersMu.RLock()
broker = findBroker(cl.brokers, id)
cl.brokersMu.RUnlock()
}
if broker == nil {
if tryLoad {
if loadErr := cl.fetchBrokerMetadata(ctx); loadErr != nil {
return nil, loadErr
}
// We will retry loading up to two times, if we load broker
// metadata twice successfully but neither load has the broker
// we are looking for, then we say our broker does not exist.
tries++
if tries < 2 {
goto start
}
}
return nil, err
}
return broker, nil
}
// controller returns the controller broker, forcing a broker load if
// necessary.
func (cl *Client) controller(ctx context.Context) (b *broker, err error) {
get := func() int32 {
cl.controllerIDMu.Lock()
defer cl.controllerIDMu.Unlock()
return cl.controllerID
}
defer func() {
if ec := (*errUnknownController)(nil); errors.As(err, &ec) {
cl.forgetControllerID(ec.id)
}
}()
var id int32
if id = get(); id < 0 {
if err := cl.fetchBrokerMetadata(ctx); err != nil {
return nil, err
}
if id = get(); id < 0 {
return nil, &errUnknownController{id}
}
}
return cl.brokerOrErr(nil, id, &errUnknownController{id})
}
// forgetControllerID is called once an admin requests sees NOT_CONTROLLER.
func (cl *Client) forgetControllerID(id int32) {
cl.controllerIDMu.Lock()
defer cl.controllerIDMu.Unlock()
if cl.controllerID == id {
cl.controllerID = unknownControllerID
}
}
const (
coordinatorTypeGroup int8 = 0
coordinatorTypeTxn int8 = 1
)
type coordinatorKey struct {
name string
typ int8
}
type coordinatorLoad struct {
loadWait chan struct{}
node int32
err error
}
func (cl *Client) loadCoordinator(ctx context.Context, typ int8, key string) (*broker, error) {
berr := cl.loadCoordinators(ctx, typ, key)[key]
return berr.b, berr.err
}
func (cl *Client) loadCoordinators(ctx context.Context, typ int8, keys ...string) map[string]brokerOrErr {
mch := make(chan map[string]brokerOrErr, 1)
go func() { mch <- cl.doLoadCoordinators(ctx, typ, keys...) }()
select {
case m := <-mch:
return m
case <-ctx.Done():
m := make(map[string]brokerOrErr, len(keys))
for _, k := range keys {
m[k] = brokerOrErr{nil, ctx.Err()}
}
return m
}
}
// doLoadCoordinators uses the caller context to cancel loading metadata
// (brokerOrErr), but we use the client context to actually issue the request.
// There should be only one direct call to doLoadCoordinators, just above in
// loadCoordinator. It is possible for two requests to be loading the same
// coordinator (in fact, that's the point of this function -- collapse these
// requests). We do not want the first request canceling it's context to cause
// errors for the second request.
//
// It is ok to leave FindCoordinator running even if the caller quits. Worst
// case, we just cache things for some time in the future; yay.
func (cl *Client) doLoadCoordinators(ctx context.Context, typ int8, keys ...string) map[string]brokerOrErr {
m := make(map[string]brokerOrErr, len(keys))
if len(keys) == 0 {
return m
}
toRequest := make(map[string]bool, len(keys)) // true == bypass the cache
for _, key := range keys {
toRequest[key] = false
}
// For each of these keys, we have two cases:
//
// 1) The key is cached. It is either loading or loaded. We do not
// request the key ourselves; we wait for the load to finish.
//
// 2) The key is not cached, and we request it.
//
// If a key is cached but the coordinator no longer exists for us, we
// re-request to refresh the coordinator by setting toRequest[key] to
// true (bypass cache).
//
// If we ever request a key ourselves, we do not request it again. We
// ensure this by deleting from toRequest. We also delete if the key
// was cached with no error.
//
// We could have some keys cached and some that need to be requested.
// We issue a request but do not request what is cached.
//
// Lastly, we only ever trigger one metadata update, which happens if
// we have an unknown coordinator after we load coordinators.
var hasLoadedBrokers bool
for len(toRequest) > 0 {
var loadWait chan struct{}
load2key := make(map[*coordinatorLoad][]string)
cl.coordinatorsMu.Lock()
for key, bypassCache := range toRequest {
c, ok := cl.coordinators[coordinatorKey{key, typ}]
if !ok || bypassCache {
if loadWait == nil {
loadWait = make(chan struct{})
}
c = &coordinatorLoad{
loadWait: loadWait,
err: errors.New("coordinator was not returned in broker response"),
}
cl.coordinators[coordinatorKey{key, typ}] = c
}
load2key[c] = append(load2key[c], key)
}
cl.coordinatorsMu.Unlock()
if loadWait == nil { // all coordinators were cached
hasLoadedBrokers = cl.waitCoordinatorLoad(ctx, typ, load2key, !hasLoadedBrokers, toRequest, m)
continue
}
key2load := make(map[string]*coordinatorLoad)
req := kmsg.NewPtrFindCoordinatorRequest()
req.CoordinatorType = typ
for c, keys := range load2key {
if c.loadWait == loadWait { // if this is our wait, this is ours to request
req.CoordinatorKeys = append(req.CoordinatorKeys, keys...)
for _, key := range keys {
key2load[key] = c
delete(toRequest, key)
}
}
}
cl.cfg.logger.Log(LogLevelDebug, "prepared to issue find coordinator request",
"coordinator_type", typ,
"coordinator_keys", req.CoordinatorKeys,
)
ctx := context.WithValue(cl.ctx, noShardRetryCtx, true)
shards := cl.RequestSharded(ctx, req)
for _, shard := range shards {
if shard.Err != nil {
req := shard.Req.(*kmsg.FindCoordinatorRequest)
for _, key := range req.CoordinatorKeys {
c, ok := key2load[key]
if ok {
c.err = shard.Err
}
}
} else {
resp := shard.Resp.(*kmsg.FindCoordinatorResponse)
for _, rc := range resp.Coordinators {
c, ok := key2load[rc.Key]
if ok {
c.err = kerr.ErrorForCode(rc.ErrorCode)
c.node = rc.NodeID
}
}
}
}
// For anything we loaded, if it has a load failure (including
// not being replied to), we remove the key from the cache. We
// do not want to cache erroring values.
//
// We range key2load, which contains only coordinators we are
// responsible for loading.
cl.coordinatorsMu.Lock()
for key, c := range key2load {
if c.err != nil {
ck := coordinatorKey{key, typ}
if loading, ok := cl.coordinators[ck]; ok && loading == c {
delete(cl.coordinators, ck)
}
}
}
cl.coordinatorsMu.Unlock()
close(loadWait)
hasLoadedBrokers = cl.waitCoordinatorLoad(ctx, typ, load2key, !hasLoadedBrokers, toRequest, m)
}
return m
}
// After some prep work, we wait for coordinators to load. We update toRequest
// values with true if the caller should bypass cache and re-load these
// coordinators.
//
// This returns if we load brokers, and populates m with results.
func (cl *Client) waitCoordinatorLoad(ctx context.Context, typ int8, load2key map[*coordinatorLoad][]string, shouldLoadBrokers bool, toRequest map[string]bool, m map[string]brokerOrErr) bool {
var loadedBrokers bool
for c, keys := range load2key {
<-c.loadWait
for _, key := range keys {
if c.err != nil {
delete(toRequest, key)
m[key] = brokerOrErr{nil, c.err}
continue
}
var brokerCtx context.Context
if shouldLoadBrokers && !loadedBrokers {
brokerCtx = ctx
loadedBrokers = true
}
b, err := cl.brokerOrErr(brokerCtx, c.node, &errUnknownCoordinator{c.node, coordinatorKey{key, typ}})
if err != nil {
if _, exists := toRequest[key]; exists {
toRequest[key] = true
continue
}
// If the key does not exist, we just loaded this
// coordinator and also the brokers. We do not
// re-request.
}
delete(toRequest, key)
m[key] = brokerOrErr{b, err}
}
}
return loadedBrokers
}
func (cl *Client) maybeDeleteStaleCoordinator(name string, typ int8, err error) bool {
switch {
case errors.Is(err, kerr.CoordinatorNotAvailable),
errors.Is(err, kerr.CoordinatorLoadInProgress),
errors.Is(err, kerr.NotCoordinator):
cl.deleteStaleCoordinator(name, typ)
return true
}
return false
}
func (cl *Client) deleteStaleCoordinator(name string, typ int8) {
cl.coordinatorsMu.Lock()
defer cl.coordinatorsMu.Unlock()
k := coordinatorKey{name, typ}
v := cl.coordinators[k]
if v == nil {
return
}
select {
case <-v.loadWait:
delete(cl.coordinators, k)
default:
// We are actively reloading this coordinator.
}
}
type brokerOrErr struct {
b *broker
err error
}
func (cl *Client) handleAdminReq(ctx context.Context, req kmsg.Request) ResponseShard {
// Loading a controller can perform some wait; we accept that and do
// not account for the retries or the time to load the controller as
// part of the retries / time to issue the req.
r := cl.retryableBrokerFn(func() (*broker, error) {
return cl.controller(ctx)
})
// The only request that can break mapped metadata is CreatePartitions,
// because our mapping will still be "valid" but behind the scenes,
// more partitions exist. If CreatePartitions is going through this
// client, we preemptively delete any mapping for these topics.
if t, ok := req.(*kmsg.CreatePartitionsRequest); ok {
var topics []string
for i := range t.Topics {
topics = append(topics, t.Topics[i].Topic)
}
cl.maybeDeleteMappedMetadata(false, topics...)
}
var d failDial
r.parseRetryErr = func(resp kmsg.Response, err error) error {
if err != nil {
if d.isRepeatedDialFail(err) {
cl.forgetControllerID(r.last.meta.NodeID)
}
return err
}
var code int16
switch t := resp.(type) {
case *kmsg.CreateTopicsResponse:
if len(t.Topics) > 0 {
code = t.Topics[0].ErrorCode
}
case *kmsg.DeleteTopicsResponse:
if len(t.Topics) > 0 {
code = t.Topics[0].ErrorCode
}
case *kmsg.CreatePartitionsResponse:
if len(t.Topics) > 0 {
code = t.Topics[0].ErrorCode
}
case *kmsg.ElectLeadersResponse:
if len(t.Topics) > 0 && len(t.Topics[0].Partitions) > 0 {
code = t.Topics[0].Partitions[0].ErrorCode
}
case *kmsg.AlterPartitionAssignmentsResponse:
code = t.ErrorCode
case *kmsg.ListPartitionReassignmentsResponse:
code = t.ErrorCode
case *kmsg.AlterUserSCRAMCredentialsResponse:
if len(t.Results) > 0 {
code = t.Results[0].ErrorCode
}
case *kmsg.VoteResponse:
code = t.ErrorCode
case *kmsg.BeginQuorumEpochResponse:
code = t.ErrorCode
case *kmsg.EndQuorumEpochResponse:
code = t.ErrorCode
case *kmsg.DescribeQuorumResponse:
code = t.ErrorCode
case *kmsg.AlterPartitionResponse:
code = t.ErrorCode
case *kmsg.UpdateFeaturesResponse:
code = t.ErrorCode
case *kmsg.EnvelopeResponse:
code = t.ErrorCode
}
if err := kerr.ErrorForCode(code); errors.Is(err, kerr.NotController) {
// There must be a last broker if we were able to issue
// the request and get a response.
cl.forgetControllerID(r.last.meta.NodeID)
return err
}
return nil
}
resp, err := r.Request(ctx, req)
return shard(r.last, req, resp, err)
}
// handleCoordinatorReq issues simple (non-shardable) group or txn requests.
func (cl *Client) handleCoordinatorReq(ctx context.Context, req kmsg.Request) ResponseShard {
switch t := req.(type) {
default:
// All group requests should be listed below, so if it isn't,
// then we do not know what this request is.
return shard(nil, req, nil, errors.New("client is too old; this client does not know what to do with this request"))
/////////
// TXN // -- all txn reqs are simple
/////////
case *kmsg.InitProducerIDRequest:
if t.TransactionalID != nil {
return cl.handleCoordinatorReqSimple(ctx, coordinatorTypeTxn, *t.TransactionalID, req)
}
// InitProducerID can go to any broker if the transactional ID
// is nil. By using handleReqWithCoordinator, we get the
// retryable-error parsing, even though we are not actually
// using a defined txn coordinator. This is fine; by passing no
// names, we delete no coordinator.
coordinator, resp, err := cl.handleReqWithCoordinator(ctx, func() (*broker, error) { return cl.broker(), nil }, coordinatorTypeTxn, "", req)
return shard(coordinator, req, resp, err)
case *kmsg.AddOffsetsToTxnRequest:
return cl.handleCoordinatorReqSimple(ctx, coordinatorTypeTxn, t.TransactionalID, req)
case *kmsg.EndTxnRequest:
return cl.handleCoordinatorReqSimple(ctx, coordinatorTypeTxn, t.TransactionalID, req)
///////////
// GROUP // -- most group reqs are simple
///////////
case *kmsg.OffsetCommitRequest:
return cl.handleCoordinatorReqSimple(ctx, coordinatorTypeGroup, t.Group, req)
case *kmsg.TxnOffsetCommitRequest:
return cl.handleCoordinatorReqSimple(ctx, coordinatorTypeGroup, t.Group, req)
case *kmsg.JoinGroupRequest:
return cl.handleCoordinatorReqSimple(ctx, coordinatorTypeGroup, t.Group, req)
case *kmsg.HeartbeatRequest:
return cl.handleCoordinatorReqSimple(ctx, coordinatorTypeGroup, t.Group, req)
case *kmsg.LeaveGroupRequest:
return cl.handleCoordinatorReqSimple(ctx, coordinatorTypeGroup, t.Group, req)
case *kmsg.SyncGroupRequest:
return cl.handleCoordinatorReqSimple(ctx, coordinatorTypeGroup, t.Group, req)
case *kmsg.OffsetDeleteRequest:
return cl.handleCoordinatorReqSimple(ctx, coordinatorTypeGroup, t.Group, req)
// ConsumerGroupHeartbeat cannot be retried at all
case *kmsg.ConsumerGroupHeartbeatRequest:
br, err := cl.loadCoordinator(ctx, coordinatorTypeGroup, t.Group)
var resp kmsg.Response
if err == nil {
resp, err = br.waitResp(ctx, req)
}
return shard(br, req, resp, err)
}
}
// handleCoordinatorReqSimple issues a request that contains a single group or
// txn to its coordinator.
//
// The error is inspected to see if it is a retryable error and, if so, the
// coordinator is deleted.
func (cl *Client) handleCoordinatorReqSimple(ctx context.Context, typ int8, name string, req kmsg.Request) ResponseShard {
coordinator, resp, err := cl.handleReqWithCoordinator(ctx, func() (*broker, error) {
return cl.loadCoordinator(ctx, typ, name)
}, typ, name, req)
return shard(coordinator, req, resp, err)
}
// handleReqWithCoordinator actually issues a request to a coordinator and
// does retry handling.
//
// This avoids retries on the two group requests that need to be sharded.
func (cl *Client) handleReqWithCoordinator(
ctx context.Context,
coordinator func() (*broker, error),
typ int8,
name string, // group ID or the transactional id
req kmsg.Request,
) (*broker, kmsg.Response, error) {
r := cl.retryableBrokerFn(coordinator)
var d failDial
r.parseRetryErr = func(resp kmsg.Response, err error) error {
if err != nil {
if d.isRepeatedDialFail(err) {
cl.deleteStaleCoordinator(name, typ)
}
return err
}
var code int16
switch t := resp.(type) {
// TXN
case *kmsg.InitProducerIDResponse:
code = t.ErrorCode
case *kmsg.AddOffsetsToTxnResponse:
code = t.ErrorCode
case *kmsg.EndTxnResponse:
code = t.ErrorCode
// GROUP
case *kmsg.OffsetCommitResponse:
if len(t.Topics) > 0 && len(t.Topics[0].Partitions) > 0 {
code = t.Topics[0].Partitions[0].ErrorCode
}
case *kmsg.TxnOffsetCommitResponse:
if len(t.Topics) > 0 && len(t.Topics[0].Partitions) > 0 {
code = t.Topics[0].Partitions[0].ErrorCode
}
case *kmsg.JoinGroupResponse:
code = t.ErrorCode
case *kmsg.HeartbeatResponse:
code = t.ErrorCode
case *kmsg.LeaveGroupResponse:
code = t.ErrorCode
case *kmsg.SyncGroupResponse:
code = t.ErrorCode
case *kmsg.ConsumerGroupHeartbeatResponse:
code = t.ErrorCode
}
// ListGroups, OffsetFetch, DeleteGroups, DescribeGroups, and
// DescribeTransactions handled in sharding.
if err := kerr.ErrorForCode(code); cl.maybeDeleteStaleCoordinator(name, typ, err) {
return err
}
return nil
}
resp, err := r.Request(ctx, req)
return r.last, resp, err
}
// Broker returns a handle to a specific broker to directly issue requests to.
// Note that there is no guarantee that this broker exists; if it does not,
// requests will fail with an unknown broker error.
func (cl *Client) Broker(id int) *Broker {
return &Broker{
id: int32(id),
cl: cl,
}
}
// DiscoveredBrokers returns all brokers that were discovered from prior
// metadata responses. This does not actually issue a metadata request to load
// brokers; if you wish to ensure this returns all brokers, be sure to manually
// issue a metadata request before this. This also does not include seed
// brokers, which are internally saved under special internal broker IDs (but,
// it does include those brokers under their normal IDs as returned from a
// metadata response).
func (cl *Client) DiscoveredBrokers() []*Broker {
cl.brokersMu.RLock()
defer cl.brokersMu.RUnlock()
var bs []*Broker
for _, broker := range cl.brokers {
bs = append(bs, &Broker{id: broker.meta.NodeID, cl: cl})
}
return bs
}
// SeedBrokers returns the all seed brokers.
func (cl *Client) SeedBrokers() []*Broker {
var bs []*Broker
for _, broker := range cl.loadSeeds() {
bs = append(bs, &Broker{id: broker.meta.NodeID, cl: cl})
}
return bs
}
// UpdateSeedBrokers updates the client's list of seed brokers. Over the course
// of a long period of time, your might replace all brokers that you originally
// specified as seeds. This command allows you to replace the client's list of
// seeds.
//
// This returns an error if any of the input addrs is not a host:port. If the
// input list is empty, the function returns without replacing the seeds.
func (cl *Client) UpdateSeedBrokers(addrs ...string) error {
if len(addrs) == 0 {
return nil
}
seeds, err := parseSeeds(addrs)
if err != nil {
return err
}
seedBrokers := make([]*broker, 0, len(seeds))
for i, seed := range seeds {
b := cl.newBroker(unknownSeedID(i), seed.host, seed.port, nil)
seedBrokers = append(seedBrokers, b)
}
// We lock to guard against concurrently updating seeds; we do not need
// the lock for what this usually guards.
cl.brokersMu.Lock()
old := cl.loadSeeds()
cl.seeds.Store(seedBrokers)
cl.brokersMu.Unlock()
for _, b := range old {
b.stopForever()
}
return nil
}
// Broker pairs a broker ID with a client to directly issue requests to a
// specific broker.
type Broker struct {
id int32
cl *Client
}
// Request issues a request to a broker. If the broker does not exist in the
// client, this returns an unknown broker error. Requests are not retried.
//
// The passed context can be used to cancel a request and return early.
// Note that if the request is not canceled before it is written to Kafka,
// you may just end up canceling and not receiving the response to what Kafka
// inevitably does.
//
// It is more beneficial to always use RetriableRequest.
func (b *Broker) Request(ctx context.Context, req kmsg.Request) (kmsg.Response, error) {
return b.request(ctx, false, req)
}
// RetriableRequest issues a request to a broker the same as Broker, but
// retries in the face of retryable broker connection errors. This does not
// retry on response internal errors.
func (b *Broker) RetriableRequest(ctx context.Context, req kmsg.Request) (kmsg.Response, error) {
return b.request(ctx, true, req)
}
func (b *Broker) request(ctx context.Context, retry bool, req kmsg.Request) (kmsg.Response, error) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
var resp kmsg.Response
var err error
done := make(chan struct{})
go func() {
defer close(done)
if !retry {
var br *broker
br, err = b.cl.brokerOrErr(ctx, b.id, errUnknownBroker)
if err == nil {
resp, err = br.waitResp(ctx, req)
}
} else {
resp, err = b.cl.retryableBrokerFn(func() (*broker, error) {
return b.cl.brokerOrErr(ctx, b.id, errUnknownBroker)
}).Request(ctx, req)
}
}()
select {
case <-done:
return resp, err
case <-ctx.Done():
return nil, ctx.Err()
case <-b.cl.ctx.Done():
return nil, b.cl.ctx.Err()
}
}
//////////////////////
// REQUEST SHARDING //
//////////////////////
// Below here lies all logic to handle requests that need to be split and sent
// to many brokers. A lot of the logic for each sharding function is very
// similar, but each sharding function uses slightly different types.
// issueShard is a request that has been split and is ready to be sent to the
// given broker ID.
type issueShard struct {
req kmsg.Request
pin *pinReq
broker int32
any bool
// if non-nil, we could not map this request shard to any broker, and
// this error is the reason.
err error
}
// sharder splits a request.
type sharder interface {
// shard splits a request and returns the requests to issue tied to the
// brokers to issue the requests to. This can return an error if there
// is some pre-loading that needs to happen. If an error is returned,
// the request that was intended for splitting is failed wholesale.
//
// Due to sharded requests not being retryable if a response is
// received, to avoid stale coordinator errors, this function should
// not use any previously cached metadata.
//
// This takes the last error if the request is being retried, which is
// currently only useful for errBrokerTooOld.
shard(context.Context, kmsg.Request, error) ([]issueShard, bool, error)
// onResp is called on a successful response to investigate the
// response and potentially perform cleanup, and potentially returns an
// error signifying to retry. See onShardRespErr below for more
// details.
onResp(kmsg.Request, kmsg.Response) error
// merge is a function that can be used to merge sharded responses into
// one response. This is used by the client.Request method.
merge([]ResponseShard) (kmsg.Response, error)
}
var noShardRetryCtx = func() *string { s := "no_shard_retry"; return &s }()
// handleShardedReq splits and issues requests to brokers, recursively
// splitting as necessary if requests fail and need remapping.
func (cl *Client) handleShardedReq(ctx context.Context, req kmsg.Request) ([]ResponseShard, shardMerge) {
// First, determine our sharder.
var sharder sharder
switch req.(type) {
case *kmsg.ListOffsetsRequest:
sharder = &listOffsetsSharder{cl}
case *kmsg.OffsetFetchRequest:
sharder = &offsetFetchSharder{cl}
case *kmsg.FindCoordinatorRequest:
sharder = &findCoordinatorSharder{cl}
case *kmsg.DescribeGroupsRequest:
sharder = &describeGroupsSharder{cl}
case *kmsg.ListGroupsRequest:
sharder = &listGroupsSharder{cl}
case *kmsg.DeleteRecordsRequest:
sharder = &deleteRecordsSharder{cl}
case *kmsg.OffsetForLeaderEpochRequest:
sharder = &offsetForLeaderEpochSharder{cl}
case *kmsg.AddPartitionsToTxnRequest:
sharder = &addPartitionsToTxnSharder{cl}
case *kmsg.WriteTxnMarkersRequest:
sharder = &writeTxnMarkersSharder{cl}
case *kmsg.DescribeConfigsRequest:
sharder = &describeConfigsSharder{cl}
case *kmsg.AlterConfigsRequest:
sharder = &alterConfigsSharder{cl}
case *kmsg.AlterReplicaLogDirsRequest:
sharder = &alterReplicaLogDirsSharder{cl}
case *kmsg.DescribeLogDirsRequest:
sharder = &describeLogDirsSharder{cl}
case *kmsg.DeleteGroupsRequest:
sharder = &deleteGroupsSharder{cl}
case *kmsg.IncrementalAlterConfigsRequest:
sharder = &incrementalAlterConfigsSharder{cl}
case *kmsg.DescribeProducersRequest:
sharder = &describeProducersSharder{cl}
case *kmsg.DescribeTransactionsRequest:
sharder = &describeTransactionsSharder{cl}
case *kmsg.ListTransactionsRequest:
sharder = &listTransactionsSharder{cl}
case *kmsg.ConsumerGroupDescribeRequest:
sharder = &consumerGroupDescribeSharder{cl}
case *kmsg.ShareGroupDescribeRequest:
sharder = &shareGroupDescribeSharder{cl}
}
// If a request fails, we re-shard it (in case it needs to be split
// again). reqTry tracks how many total tries a request piece has had;
// we quit at either the max configured tries or max configured time.
type reqTry struct {
tries int
req kmsg.Request
lastErr error
}
var (
shardsMu sync.Mutex
shards []ResponseShard
addShard = func(shard ResponseShard) {
shardsMu.Lock()
defer shardsMu.Unlock()
shards = append(shards, shard)
}
start = time.Now()
retryTimeout = cl.cfg.retryTimeout(req.Key())
wg sync.WaitGroup
issue func(reqTry, int32)
)
l := cl.cfg.logger
debug := l.Level() >= LogLevelDebug
noRetries := ctx != nil && ctx.Value(noShardRetryCtx) != nil
// issue is called to progressively split and issue requests.
//
// This recursively calls itself if a request fails and can be retried.
// We avoid stack problems because this calls itself in a goroutine.
issue = func(try reqTry, avoidBroker int32) {
issues, reshardable, err := sharder.shard(ctx, try.req, try.lastErr)
if err != nil {
l.Log(LogLevelDebug, "unable to shard request", "req", kmsg.Key(try.req.Key()).Name(), "previous_tries", try.tries, "err", err)
addShard(shard(nil, try.req, nil, err)) // failure to shard means data loading failed; this request is failed
return
}
// If the request actually does not need to be issued, we issue
// it to a random broker. There is no benefit to this, but at
// least we will return one shard.
if len(issues) == 0 {
issues = []issueShard{{
req: try.req,
any: true,
}}
reshardable = true
}
if debug {
var key int16
var brokerAnys []string
for _, issue := range issues {
key = issue.req.Key()
if issue.err != nil {
brokerAnys = append(brokerAnys, "err")
} else if issue.any {
brokerAnys = append(brokerAnys, "any")
} else {
brokerAnys = append(brokerAnys, strconv.Itoa(int(issue.broker)))
}
}
l.Log(LogLevelDebug, "sharded request", "req", kmsg.Key(key).Name(), "destinations", brokerAnys)
}
for i := range issues {
var (
myIssue = issues[i]
isPinned bool
ctx = ctx // loop local context, in case we override by pinning
avoidBroker = avoidBroker // same
tries = try.tries // same
)
if isPinned = myIssue.pin != nil; isPinned {
ctx = context.WithValue(ctx, ctxPinReq, myIssue.pin)
}
if myIssue.err != nil {
addShard(shard(nil, myIssue.req, nil, myIssue.err))
continue
}
wg.Add(1)
go func() {
defer wg.Done()
start:
tries++
br := cl.broker()
var err error
if !myIssue.any {
br, err = cl.brokerOrErr(ctx, myIssue.broker, errUnknownBroker)
} else if avoidBroker != -1 {
for i := 0; i < 3 && br.meta.NodeID == avoidBroker; i++ {
br = cl.broker()
}
}
if err != nil {
addShard(shard(nil, myIssue.req, nil, err)) // failure to load a broker is a failure to issue a request
return
}
resp, err := br.waitResp(ctx, myIssue.req)
var errIsFromResp bool
if err == nil {
err = sharder.onResp(myIssue.req, resp) // perform some potential cleanup, and potentially receive an error to retry
if ke := (*kerr.Error)(nil); errors.As(err, &ke) {
errIsFromResp = true
}
}
// If we failed to issue the request, we *maybe* will retry.
// We could have failed to even issue the request or receive
// a response, which is retryable.
//
// If a pinned req fails with errBrokerTooOld, we always retry
// immediately. The request was not even issued. However, as a
// safety, we only do this 3 times to avoid some super weird
// pathological spin loop.
//
// We do retry on pinnedOld even if noRetries==true because
// the request was not issued; the sharder may handle
// errBrokerTooOld by pinning / splitting differently next try.
var (
backoff = cl.cfg.retryBackoff(tries)
pinnedOld = reshardable && isPinned && errors.Is(err, errBrokerTooOld) && tries <= 3
notTimedOut = retryTimeout == 0 || time.Now().Add(backoff).Sub(start) <= retryTimeout
shouldRetry = cl.shouldRetry(tries, err)
shouldRetryNext = myIssue.any && cl.shouldRetryNext(tries, err)
)
// If we retried on a "next" broker, but we randomly chose
// that same broker 3x, then we avoid retrying again on a
// "next" broker.
//
// If we retry at all, we need to clear `avoidBroker` in
// case it's already set. however, if we *do* need to retry
// on a different broker, then we set it.
if avoidBroker != -1 && br.meta.NodeID == avoidBroker {
shouldRetryNext = false
}
avoidBroker = -1
if shouldRetryNext {
avoidBroker = br.meta.NodeID
}
if err != nil &&
(pinnedOld ||
!noRetries && notTimedOut && (shouldRetry || shouldRetryNext) && cl.waitTries(ctx, backoff)) {
// Non-reshardable re-requests just jump back to the
// top where the broker is loaded. This is the case on
// requests where the original request is split to
// dedicated brokers; we do not want to re-shard that.
if !reshardable {
l.Log(LogLevelDebug, "sharded request failed, reissuing without resharding", "req", kmsg.Key(myIssue.req.Key()).Name(), "time_since_start", time.Since(start), "tries", tries, "err", err)
goto start
}
l.Log(LogLevelDebug, "sharded request failed, resharding and reissuing", "req", kmsg.Key(myIssue.req.Key()).Name(), "time_since_start", time.Since(start), "tries", tries, "err", err)
issue(reqTry{tries, myIssue.req, err}, avoidBroker)
return
}
// If we pulled an error out of the response body in an attempt
// to possibly retry, the request was NOT an error that we want
// to bubble as a shard error. The request was successful, we
// have a response. Before we add the shard, strip the error.
// The end user can parse the response ErrorCode.
if errIsFromResp {
err = nil
}
addShard(shard(br, myIssue.req, resp, err)) // the error was not retryable
}()
}
}
issue(reqTry{0, req, nil}, -1)
wg.Wait()
return shards, sharder.merge
}
// For sharded errors, we prefer to keep retryable errors rather than
// non-retryable errors. We keep the non-retryable if everything is
// non-retryable.
//
// We favor retryable because retryable means we used a stale cache value; we
// clear the stale entries on failure and the retry uses fresh data. The
// request will be split and remapped, and the non-retryable errors will be
// encountered again.
func onRespShardErr(err *error, newKerr error) {
if newKerr == nil || *err != nil && kerr.IsRetriable(*err) {
return
}
*err = newKerr
}
// a convenience function for when a request needs to be issued identically to
// all brokers.
func (cl *Client) allBrokersShardedReq(ctx context.Context, fn func() kmsg.Request) ([]issueShard, bool, error) {
if err := cl.fetchBrokerMetadata(ctx); err != nil {
return nil, false, err
}
var issues []issueShard
cl.brokersMu.RLock()
for _, broker := range cl.brokers {
issues = append(issues, issueShard{
req: fn(),
broker: broker.meta.NodeID,
})
}
cl.brokersMu.RUnlock()
return issues, false, nil // we do NOT re-shard these requests request
}
// a convenience function for saving the first ResponseShard error.
func firstErrMerger(sresps []ResponseShard, merge func(kresp kmsg.Response)) error {
var firstErr error
for _, sresp := range sresps {
if sresp.Err != nil {
if firstErr == nil {
firstErr = sresp.Err
}
continue
}
merge(sresp.Resp)
}
return firstErr
}
type mappedMetadataTopic struct {
t kmsg.MetadataResponseTopic
ps map[int32]kmsg.MetadataResponseTopicPartition
when time.Time
}
// For NOT_LEADER_FOR_PARTITION:
// We always delete stale metadata. It's possible that a leader rebalance
// happened immediately after we requested metadata; we should not pin to
// the stale metadata for 1s.
//
// For UNKNOWN_TOPIC_OR_PARTITION:
// We only delete stale metadata if it is older than the min age or 1s,
// whichever is smaller. We use 1s even if min age is larger, because we want
// to encourage larger min age for caching purposes. More obvious would be to
// *always* evict the cache here, but if we *just* requested metadata, then
// evicting the cache would cause churn for a topic that genuinely does not
// exist.
func (cl *Client) maybeDeleteMappedMetadata(unknownTopic bool, ts ...string) (shouldRetry bool) {
if len(ts) == 0 {
return shouldRetry
}
var min time.Duration
if unknownTopic {
min = time.Second
if cl.cfg.metadataMinAge < min {
min = cl.cfg.metadataMinAge
}
}
now := time.Now()
cl.mappedMetaMu.Lock()
defer cl.mappedMetaMu.Unlock()
for _, t := range ts {
tcached, exists := cl.mappedMeta[t]
if exists && (min == 0 || now.Sub(tcached.when) > min) {
shouldRetry = true
delete(cl.mappedMeta, t)
}
}
return shouldRetry
}
// We only cache for metadata min age. We could theoretically cache forever,
// but an out of band CreatePartitions can result in our metadata being stale
// and us never knowing. So, we choose metadata min age. There are only a few
// requests that are sharded and use metadata, and the one this benefits most
// is ListOffsets. Likely, ListOffsets for the same topic will be issued back
// to back, so not caching for so long is ok.
func (cl *Client) fetchCachedMappedMetadata(limit time.Duration, ts ...string) (map[string]mappedMetadataTopic, []string) {
cl.mappedMetaMu.Lock()
defer cl.mappedMetaMu.Unlock()
if len(cl.mappedMeta) == 0 {
return nil, ts
}
cached := make(map[string]mappedMetadataTopic)
needed := ts[:0]
if limit <= 0 {
limit = cl.cfg.metadataMinAge
}
for _, t := range ts {
tcached, exists := cl.mappedMeta[t]
if exists && time.Since(tcached.when) < limit {
cached[t] = tcached
} else {
needed = append(needed, t)
delete(cl.mappedMeta, t)
}
}
return cached, needed
}
// fetchMappedMetadata provides a convenience type of working with metadata;
// this is garbage heavy, so it is only used in one off requests in this
// package.
func (cl *Client) fetchMappedMetadata(ctx context.Context, topics []string, useCache bool, limit time.Duration) (map[string]mappedMetadataTopic, error) {
var intoMapped map[string]mappedMetadataTopic
needed := topics
if useCache {
intoMapped, needed = cl.fetchCachedMappedMetadata(limit, topics...)
// If intoMapped is nil, we have no cached topics at all and
// need to force a metadata load to satisfy broker/controller
// aspects of the metadata response. We have either never
// issued a metadata request, or the cached data (of no topics)
// could be super old. Cache age is only tracked per topic.
if intoMapped != nil && len(needed) == 0 {
return intoMapped, nil
}
}
if intoMapped == nil {
intoMapped = make(map[string]mappedMetadataTopic)
}
_, _, err := cl.fetchMetadataForTopics(ctx, topics == nil, needed, intoMapped)
return intoMapped, err
}
// storeCachedMappedMetadata caches the fetched metadata in the Client, and calls the onEachTopic callback
// function for each topic in the MetadataResponse.
func (cl *Client) storeCachedMappedMetadata(meta *kmsg.MetadataResponse, intoMapped map[string]mappedMetadataTopic) {
cl.mappedMetaMu.Lock()
defer cl.mappedMetaMu.Unlock()
if cl.mappedMeta == nil {
cl.mappedMeta = make(map[string]mappedMetadataTopic)
}
when := time.Now()
for _, topic := range meta.Topics {
if topic.Topic == nil {
// We do not request with topic IDs, so we should not
// receive topic IDs in the response.
continue
}
t := mappedMetadataTopic{
t: topic,
ps: make(map[int32]kmsg.MetadataResponseTopicPartition),
when: when,
}
cl.mappedMeta[*topic.Topic] = t
for _, partition := range topic.Partitions {
t.ps[partition.Partition] = partition
}
if intoMapped != nil {
intoMapped[*t.t.Topic] = t
}
}
if len(meta.Topics) != len(cl.mappedMeta) {
now := time.Now()
for topic, mapped := range cl.mappedMeta {
if mapped.when.Equal(when) {
continue
}
if now.Sub(mapped.when) > cl.cfg.metadataMinAge {
delete(cl.mappedMeta, topic)
}
}
}
}
func unknownOrCode(exists bool, code int16) error {
if !exists {
return kerr.UnknownTopicOrPartition
}
return kerr.ErrorForCode(code)
}
func noLeader(l int32) error {
if l < 0 {
return kerr.LeaderNotAvailable
}
return nil
}
// This is a helper for the sharded requests below; if mapping metadata fails
// to load topics or partitions, we group the failures by error.
//
// We use a lot of reflect magic to make the actual usage much nicer.
type unknownErrShards struct {
// load err => topic => mystery slice type
//
// The mystery type is basically just []Partition, where Partition can
// be any kmsg type.
mapped map[error]map[string]reflect.Value
}
// err stores a new failing partition with its failing error.
//
// partition's type is equal to the arg1 type of l.fn.
func (l *unknownErrShards) err(err error, topic string, partition any) {
if l.mapped == nil {
l.mapped = make(map[error]map[string]reflect.Value)
}
t := l.mapped[err]
if t == nil {
t = make(map[string]reflect.Value)
l.mapped[err] = t
}
slice, ok := t[topic]
if !ok {
// We make a slice of the input partition type.
slice = reflect.MakeSlice(reflect.SliceOf(reflect.TypeOf(partition)), 0, 1)
}
t[topic] = reflect.Append(slice, reflect.ValueOf(partition))
}
// errs takes an input slice of partitions and stores each with its failing
// error.
//
// partitions is a slice where each element has type of arg1 of l.fn.
func (l *unknownErrShards) errs(err error, topic string, partitions any) {
v := reflect.ValueOf(partitions)
for i := range v.Len() {
l.err(err, topic, v.Index(i).Interface())
}
}
// Returns issueShards for each error stored in l.
//
// This takes a factory function: the first return is a new kmsg.Request, the
// second is a function that adds a topic and its partitions to that request.
//
// Thus, fn is of type func() (kmsg.Request, func(string, []P))
func (l *unknownErrShards) collect(mkreq, mergeParts any) []issueShard {
if len(l.mapped) == 0 {
return nil
}
var shards []issueShard
factory := reflect.ValueOf(mkreq)
perTopic := reflect.ValueOf(mergeParts)
for err, topics := range l.mapped {
req := factory.Call(nil)[0]
var ntopics, npartitions int
for topic, partitions := range topics {
ntopics++
npartitions += partitions.Len()
perTopic.Call([]reflect.Value{req, reflect.ValueOf(topic), partitions})
}
shards = append(shards, issueShard{
req: req.Interface().(kmsg.Request),
err: err,
})
}
return shards
}
// handles sharding ListOffsetsRequest
type listOffsetsSharder struct{ *Client }
func (cl *listOffsetsSharder) shard(ctx context.Context, kreq kmsg.Request, _ error) ([]issueShard, bool, error) {
req := kreq.(*kmsg.ListOffsetsRequest)
// For listing offsets, we need the broker leader for each partition we
// are listing. Thus, we first load metadata for the topics.
//
// Metadata loading performs retries; if we fail here, the we do not
// issue sharded requests.
var need []string
for _, topic := range req.Topics {
need = append(need, topic.Topic)
}
mapping, err := cl.fetchMappedMetadata(ctx, need, true, 0)
if err != nil {
return nil, false, err
}
brokerReqs := make(map[int32]map[string][]kmsg.ListOffsetsRequestTopicPartition)
var unknowns unknownErrShards
// For any topic or partition that had an error load, we blindly issue
// a load to the first seed broker. We expect the list to fail, but it
// is the best we could do.
for _, topic := range req.Topics {
t := topic.Topic
tmapping, exists := mapping[t]
if err := unknownOrCode(exists, tmapping.t.ErrorCode); err != nil {
unknowns.errs(err, t, topic.Partitions)
continue
}
for _, partition := range topic.Partitions {
p, exists := tmapping.ps[partition.Partition]
if err := unknownOrCode(exists, p.ErrorCode); err != nil {
unknowns.err(err, t, partition)
continue
}
if err := noLeader(p.Leader); err != nil {
unknowns.err(err, t, partition)
continue
}
brokerReq := brokerReqs[p.Leader]
if brokerReq == nil {
brokerReq = make(map[string][]kmsg.ListOffsetsRequestTopicPartition)
brokerReqs[p.Leader] = brokerReq
}
brokerReq[t] = append(brokerReq[t], partition)
}
}
mkreq := func() *kmsg.ListOffsetsRequest {
r := kmsg.NewPtrListOffsetsRequest()
r.ReplicaID = req.ReplicaID
r.IsolationLevel = req.IsolationLevel
return r
}
var issues []issueShard
for brokerID, brokerReq := range brokerReqs {
req := mkreq()
for topic, parts := range brokerReq {
reqTopic := kmsg.NewListOffsetsRequestTopic()
reqTopic.Topic = topic
reqTopic.Partitions = parts
req.Topics = append(req.Topics, reqTopic)
}
issues = append(issues, issueShard{
req: req,
broker: brokerID,
})
}
return append(issues, unknowns.collect(mkreq, func(r *kmsg.ListOffsetsRequest, topic string, parts []kmsg.ListOffsetsRequestTopicPartition) {
reqTopic := kmsg.NewListOffsetsRequestTopic()
reqTopic.Topic = topic
reqTopic.Partitions = parts
r.Topics = append(r.Topics, reqTopic)
})...), true, nil // this is reshardable
}
func (cl *listOffsetsSharder) onResp(_ kmsg.Request, kresp kmsg.Response) error {
var (
resp = kresp.(*kmsg.ListOffsetsResponse)
del []string
retErr error
unknownTopic bool
)
for i := range resp.Topics {
t := &resp.Topics[i]
for j := range t.Partitions {
p := &t.Partitions[j]
err := kerr.ErrorForCode(p.ErrorCode)
if err == kerr.UnknownTopicOrPartition || err == kerr.NotLeaderForPartition {
del = append(del, t.Topic)
unknownTopic = unknownTopic || err == kerr.UnknownTopicOrPartition
}
onRespShardErr(&retErr, err)
}
}
if cl.maybeDeleteMappedMetadata(unknownTopic, del...) {
return retErr
}
return nil
}
func (*listOffsetsSharder) merge(sresps []ResponseShard) (kmsg.Response, error) {
merged := kmsg.NewPtrListOffsetsResponse()
topics := make(map[string][]kmsg.ListOffsetsResponseTopicPartition)
firstErr := firstErrMerger(sresps, func(kresp kmsg.Response) {
resp := kresp.(*kmsg.ListOffsetsResponse)
merged.Version = resp.Version
merged.ThrottleMillis = resp.ThrottleMillis
for _, topic := range resp.Topics {
topics[topic.Topic] = append(topics[topic.Topic], topic.Partitions...)
}
})
for topic, partitions := range topics {
respTopic := kmsg.NewListOffsetsResponseTopic()
respTopic.Topic = topic
respTopic.Partitions = partitions
merged.Topics = append(merged.Topics, respTopic)
}
return merged, firstErr
}
// handles sharding OffsetFetchRequest
type offsetFetchSharder struct{ *Client }
func offsetFetchReqToGroup(req *kmsg.OffsetFetchRequest) kmsg.OffsetFetchRequestGroup {
g := kmsg.NewOffsetFetchRequestGroup()
g.Group = req.Group
for _, topic := range req.Topics {
reqTopic := kmsg.NewOffsetFetchRequestGroupTopic()
reqTopic.Topic = topic.Topic
reqTopic.Partitions = topic.Partitions
g.Topics = append(g.Topics, reqTopic)
}
return g
}
func offsetFetchGroupToReq(requireStable bool, group kmsg.OffsetFetchRequestGroup) *kmsg.OffsetFetchRequest {
req := kmsg.NewPtrOffsetFetchRequest()
req.RequireStable = requireStable
req.Group = group.Group
for _, topic := range group.Topics {
reqTopic := kmsg.NewOffsetFetchRequestTopic()
reqTopic.Topic = topic.Topic
reqTopic.Partitions = topic.Partitions
req.Topics = append(req.Topics, reqTopic)
}
return req
}
func offsetFetchRespToGroup(req *kmsg.OffsetFetchRequest, resp *kmsg.OffsetFetchResponse) kmsg.OffsetFetchResponseGroup {
g := kmsg.NewOffsetFetchResponseGroup()
g.Group = req.Group
g.ErrorCode = resp.ErrorCode
for _, topic := range resp.Topics {
t := kmsg.NewOffsetFetchResponseGroupTopic()
t.Topic = topic.Topic
for _, partition := range topic.Partitions {
p := kmsg.NewOffsetFetchResponseGroupTopicPartition()
p.Partition = partition.Partition
p.Offset = partition.Offset
p.LeaderEpoch = partition.LeaderEpoch
p.Metadata = partition.Metadata
p.ErrorCode = partition.ErrorCode
t.Partitions = append(t.Partitions, p)
}
g.Topics = append(g.Topics, t)
}
return g
}
func offsetFetchRespGroupIntoResp(g kmsg.OffsetFetchResponseGroup, into *kmsg.OffsetFetchResponse) {
into.ErrorCode = g.ErrorCode
into.Topics = into.Topics[:0]
for _, topic := range g.Topics {
t := kmsg.NewOffsetFetchResponseTopic()
t.Topic = topic.Topic
for _, partition := range topic.Partitions {
p := kmsg.NewOffsetFetchResponseTopicPartition()
p.Partition = partition.Partition
p.Offset = partition.Offset
p.LeaderEpoch = partition.LeaderEpoch
p.Metadata = partition.Metadata
p.ErrorCode = partition.ErrorCode
t.Partitions = append(t.Partitions, p)
}
into.Topics = append(into.Topics, t)
}
}
func (cl *offsetFetchSharder) shard(ctx context.Context, kreq kmsg.Request, lastErr error) ([]issueShard, bool, error) {
req := kreq.(*kmsg.OffsetFetchRequest)
// We always try batching and only split at the end if lastErr
// indicates too old. We convert to batching immediately.
dup := *req
req = &dup
if len(req.Groups) == 0 {
req.Groups = append(req.Groups, offsetFetchReqToGroup(req))
}
groups := make([]string, 0, len(req.Groups))
for i := range req.Groups {
groups = append(groups, req.Groups[i].Group)
}
coordinators := cl.loadCoordinators(ctx, coordinatorTypeGroup, groups...)
// Loading coordinators can have each group fail with its unique error,
// or with a kerr.Error that can be merged. Unique errors get their own
// failure shard, while kerr.Error's get merged.
type unkerr struct {
err error
group kmsg.OffsetFetchRequestGroup
}
var (
brokerReqs = make(map[int32]*kmsg.OffsetFetchRequest)
kerrs = make(map[*kerr.Error][]kmsg.OffsetFetchRequestGroup)
unkerrs []unkerr
)
newReq := func(groups ...kmsg.OffsetFetchRequestGroup) *kmsg.OffsetFetchRequest {
newReq := kmsg.NewPtrOffsetFetchRequest()
newReq.RequireStable = req.RequireStable
newReq.Groups = groups
return newReq
}
for _, group := range req.Groups {
berr := coordinators[group.Group]
var ke *kerr.Error
switch {
case berr.err == nil:
brokerReq := brokerReqs[berr.b.meta.NodeID]
if brokerReq == nil {
brokerReq = newReq()
brokerReqs[berr.b.meta.NodeID] = brokerReq
}
brokerReq.Groups = append(brokerReq.Groups, group)
case errors.As(berr.err, &ke):
kerrs[ke] = append(kerrs[ke], group)
default:
unkerrs = append(unkerrs, unkerr{berr.err, group})
}
}
splitReq := errors.Is(lastErr, errBrokerTooOld)
var issues []issueShard
for id, req := range brokerReqs {
if splitReq {
for _, group := range req.Groups {
req := offsetFetchGroupToReq(req.RequireStable, group)
issues = append(issues, issueShard{
req: req,
pin: &pinReq{pinMax: true, max: 7},
broker: id,
})
}
} else if len(req.Groups) <= 1 {
single := offsetFetchGroupToReq(req.RequireStable, req.Groups[0])
single.Groups = req.Groups
issues = append(issues, issueShard{
req: single,
broker: id,
})
} else {
issues = append(issues, issueShard{
req: req,
pin: &pinReq{pinMin: true, min: 8},
broker: id,
})
}
}
for _, unkerr := range unkerrs {
issues = append(issues, issueShard{
req: newReq(unkerr.group),
err: unkerr.err,
})
}
for kerr, groups := range kerrs {
issues = append(issues, issueShard{
req: newReq(groups...),
err: kerr,
})
}
return issues, true, nil // reshardable to load correct coordinators
}
func (cl *offsetFetchSharder) onResp(kreq kmsg.Request, kresp kmsg.Response) error {
req := kreq.(*kmsg.OffsetFetchRequest)
resp := kresp.(*kmsg.OffsetFetchResponse)
switch len(resp.Groups) {
case 0:
// Requested no groups: move top level into batch for v0-v7 to
// v8 forward compat.
resp.Groups = append(resp.Groups, offsetFetchRespToGroup(req, resp))
case 1:
// Requested 1 group v8+: set top level for v0-v7 back-compat.
offsetFetchRespGroupIntoResp(resp.Groups[0], resp)
default:
}
var retErr error
for i := range resp.Groups {
group := &resp.Groups[i]
err := kerr.ErrorForCode(group.ErrorCode)
cl.maybeDeleteStaleCoordinator(group.Group, coordinatorTypeGroup, err)
onRespShardErr(&retErr, err)
}
// For a final bit of extra fun, v0 and v1 do not have a top level
// error code but instead a per-partition error code. If the
// coordinator is loading &c, then all per-partition error codes are
// the same so we only need to look at the first partition.
if resp.Version < 2 && len(resp.Topics) > 0 && len(resp.Topics[0].Partitions) > 0 {
code := resp.Topics[0].Partitions[0].ErrorCode
err := kerr.ErrorForCode(code)
cl.maybeDeleteStaleCoordinator(req.Group, coordinatorTypeGroup, err)
onRespShardErr(&retErr, err)
}
return retErr
}
func (*offsetFetchSharder) merge(sresps []ResponseShard) (kmsg.Response, error) {
merged := kmsg.NewPtrOffsetFetchResponse()
return merged, firstErrMerger(sresps, func(kresp kmsg.Response) {
resp := kresp.(*kmsg.OffsetFetchResponse)
merged.Version = resp.Version
merged.ThrottleMillis = resp.ThrottleMillis
merged.Groups = append(merged.Groups, resp.Groups...)
// Old requests only support one group; *either* the commit
// used multiple groups and they are expecting the batch
// response, *or* the commit used one group and we always merge
// that one group into the old format.
if len(resp.Groups) == 1 {
offsetFetchRespGroupIntoResp(resp.Groups[0], merged)
}
})
}
// handles sharding FindCoordinatorRequest
type findCoordinatorSharder struct{ *Client }
func findCoordinatorRespCoordinatorIntoResp(c kmsg.FindCoordinatorResponseCoordinator, into *kmsg.FindCoordinatorResponse) {
into.NodeID = c.NodeID
into.Host = c.Host
into.Port = c.Port
into.ErrorCode = c.ErrorCode
into.ErrorMessage = c.ErrorMessage
}
func (*findCoordinatorSharder) shard(_ context.Context, kreq kmsg.Request, lastErr error) ([]issueShard, bool, error) {
req := kreq.(*kmsg.FindCoordinatorRequest)
// We always try batching and only split at the end if lastErr
// indicates too old. We convert to batching immediately.
dup := *req
req = &dup
uniq := make(map[string]struct{}, len(req.CoordinatorKeys))
if len(req.CoordinatorKeys) == 0 {
uniq[req.CoordinatorKey] = struct{}{}
} else {
for _, key := range req.CoordinatorKeys {
uniq[key] = struct{}{}
}
}
req.CoordinatorKeys = req.CoordinatorKeys[:0]
for key := range uniq {
req.CoordinatorKeys = append(req.CoordinatorKeys, key)
}
if len(req.CoordinatorKeys) == 1 {
req.CoordinatorKey = req.CoordinatorKeys[0]
}
splitReq := errors.Is(lastErr, errBrokerTooOld)
if !splitReq {
// With only one key, we do not need to split nor pin this.
if len(req.CoordinatorKeys) <= 1 {
return []issueShard{{req: req, any: true}}, false, nil
}
return []issueShard{{
req: req,
pin: &pinReq{pinMin: true, min: 4},
any: true,
}}, true, nil // this is "reshardable", in that we will split the request next
}
var issues []issueShard
for _, key := range req.CoordinatorKeys {
sreq := kmsg.NewPtrFindCoordinatorRequest()
sreq.CoordinatorType = req.CoordinatorType
sreq.CoordinatorKey = key
issues = append(issues, issueShard{
req: sreq,
pin: &pinReq{pinMax: true, max: 3},
any: true,
})
}
return issues, false, nil // not reshardable
}
func (*findCoordinatorSharder) onResp(kreq kmsg.Request, kresp kmsg.Response) error {
req := kreq.(*kmsg.FindCoordinatorRequest)
resp := kresp.(*kmsg.FindCoordinatorResponse)
switch len(resp.Coordinators) {
case 0:
// Convert v3 and prior to v4+
rc := kmsg.NewFindCoordinatorResponseCoordinator()
rc.Key = req.CoordinatorKey
rc.NodeID = resp.NodeID
rc.Host = resp.Host
rc.Port = resp.Port
rc.ErrorCode = resp.ErrorCode
rc.ErrorMessage = resp.ErrorMessage
resp.Coordinators = append(resp.Coordinators, rc)
case 1:
// Convert v4 to v3 and prior
findCoordinatorRespCoordinatorIntoResp(resp.Coordinators[0], resp)
}
return nil
}
func (*findCoordinatorSharder) merge(sresps []ResponseShard) (kmsg.Response, error) {
merged := kmsg.NewPtrFindCoordinatorResponse()
return merged, firstErrMerger(sresps, func(kresp kmsg.Response) {
resp := kresp.(*kmsg.FindCoordinatorResponse)
merged.Version = resp.Version
merged.ThrottleMillis = resp.ThrottleMillis
merged.Coordinators = append(merged.Coordinators, resp.Coordinators...)
if len(resp.Coordinators) == 1 {
findCoordinatorRespCoordinatorIntoResp(resp.Coordinators[0], merged)
}
})
}
// handles sharding DescribeGroupsRequest
type describeGroupsSharder struct{ *Client }
func (cl *describeGroupsSharder) shard(ctx context.Context, kreq kmsg.Request, _ error) ([]issueShard, bool, error) {
req := kreq.(*kmsg.DescribeGroupsRequest)
coordinators := cl.loadCoordinators(ctx, coordinatorTypeGroup, req.Groups...)
type unkerr struct {
err error
group string
}
var (
brokerReqs = make(map[int32]*kmsg.DescribeGroupsRequest)
kerrs = make(map[*kerr.Error][]string)
unkerrs []unkerr
)
newReq := func(groups ...string) *kmsg.DescribeGroupsRequest {
newReq := kmsg.NewPtrDescribeGroupsRequest()
newReq.IncludeAuthorizedOperations = req.IncludeAuthorizedOperations
newReq.Groups = groups
return newReq
}
for _, group := range req.Groups {
berr := coordinators[group]
var ke *kerr.Error
switch {
case berr.err == nil:
brokerReq := brokerReqs[berr.b.meta.NodeID]
if brokerReq == nil {
brokerReq = newReq()
brokerReqs[berr.b.meta.NodeID] = brokerReq
}
brokerReq.Groups = append(brokerReq.Groups, group)
case errors.As(berr.err, &ke):
kerrs[ke] = append(kerrs[ke], group)
default:
unkerrs = append(unkerrs, unkerr{berr.err, group})
}
}
var issues []issueShard
for id, req := range brokerReqs {
issues = append(issues, issueShard{
req: req,
broker: id,
})
}
for _, unkerr := range unkerrs {
issues = append(issues, issueShard{
req: newReq(unkerr.group),
err: unkerr.err,
})
}
for kerr, groups := range kerrs {
issues = append(issues, issueShard{
req: newReq(groups...),
err: kerr,
})
}
return issues, true, nil // reshardable to load correct coordinators
}
func (cl *describeGroupsSharder) onResp(_ kmsg.Request, kresp kmsg.Response) error { // cleanup any stale groups
resp := kresp.(*kmsg.DescribeGroupsResponse)
var retErr error
for i := range resp.Groups {
group := &resp.Groups[i]
err := kerr.ErrorForCode(group.ErrorCode)
cl.maybeDeleteStaleCoordinator(group.Group, coordinatorTypeGroup, err)
onRespShardErr(&retErr, err)
}
return retErr
}
func (*describeGroupsSharder) merge(sresps []ResponseShard) (kmsg.Response, error) {
merged := kmsg.NewPtrDescribeGroupsResponse()
return merged, firstErrMerger(sresps, func(kresp kmsg.Response) {
resp := kresp.(*kmsg.DescribeGroupsResponse)
merged.Version = resp.Version
merged.ThrottleMillis = resp.ThrottleMillis
merged.Groups = append(merged.Groups, resp.Groups...)
})
}
// handles sharding ListGroupsRequest
type listGroupsSharder struct{ *Client }
func (cl *listGroupsSharder) shard(ctx context.Context, kreq kmsg.Request, _ error) ([]issueShard, bool, error) {
req := kreq.(*kmsg.ListGroupsRequest)
return cl.allBrokersShardedReq(ctx, func() kmsg.Request {
dup := *req
return &dup
})
}
func (*listGroupsSharder) onResp(_ kmsg.Request, kresp kmsg.Response) error {
resp := kresp.(*kmsg.ListGroupsResponse)
return kerr.ErrorForCode(resp.ErrorCode)
}
func (*listGroupsSharder) merge(sresps []ResponseShard) (kmsg.Response, error) {
merged := kmsg.NewPtrListGroupsResponse()
return merged, firstErrMerger(sresps, func(kresp kmsg.Response) {
resp := kresp.(*kmsg.ListGroupsResponse)
merged.Version = resp.Version
merged.ThrottleMillis = resp.ThrottleMillis
if merged.ErrorCode == 0 {
merged.ErrorCode = resp.ErrorCode
}
merged.Groups = append(merged.Groups, resp.Groups...)
})
}
// handle sharding DeleteRecordsRequest
type deleteRecordsSharder struct{ *Client }
func (cl *deleteRecordsSharder) shard(ctx context.Context, kreq kmsg.Request, _ error) ([]issueShard, bool, error) {
req := kreq.(*kmsg.DeleteRecordsRequest)
var need []string
for _, topic := range req.Topics {
need = append(need, topic.Topic)
}
mapping, err := cl.fetchMappedMetadata(ctx, need, true, 0)
if err != nil {
return nil, false, err
}
brokerReqs := make(map[int32]map[string][]kmsg.DeleteRecordsRequestTopicPartition)
var unknowns unknownErrShards
for _, topic := range req.Topics {
t := topic.Topic
tmapping, exists := mapping[t]
if err := unknownOrCode(exists, tmapping.t.ErrorCode); err != nil {
unknowns.errs(err, t, topic.Partitions)
continue
}
for _, partition := range topic.Partitions {
p, exists := tmapping.ps[partition.Partition]
if err := unknownOrCode(exists, p.ErrorCode); err != nil {
unknowns.err(err, t, partition)
continue
}
if err := noLeader(p.Leader); err != nil {
unknowns.err(err, t, partition)
continue
}
brokerReq := brokerReqs[p.Leader]
if brokerReq == nil {
brokerReq = make(map[string][]kmsg.DeleteRecordsRequestTopicPartition)
brokerReqs[p.Leader] = brokerReq
}
brokerReq[t] = append(brokerReq[t], partition)
}
}
mkreq := func() *kmsg.DeleteRecordsRequest {
r := kmsg.NewPtrDeleteRecordsRequest()
r.TimeoutMillis = req.TimeoutMillis
return r
}
var issues []issueShard
for brokerID, brokerReq := range brokerReqs {
req := mkreq()
for topic, parts := range brokerReq {
reqTopic := kmsg.NewDeleteRecordsRequestTopic()
reqTopic.Topic = topic
reqTopic.Partitions = parts
req.Topics = append(req.Topics, reqTopic)
}
issues = append(issues, issueShard{
req: req,
broker: brokerID,
})
}
return append(issues, unknowns.collect(mkreq, func(r *kmsg.DeleteRecordsRequest, topic string, parts []kmsg.DeleteRecordsRequestTopicPartition) {
reqTopic := kmsg.NewDeleteRecordsRequestTopic()
reqTopic.Topic = topic
reqTopic.Partitions = parts
r.Topics = append(r.Topics, reqTopic)
})...), true, nil // this is reshardable
}
func (cl *deleteRecordsSharder) onResp(_ kmsg.Request, kresp kmsg.Response) error {
var (
resp = kresp.(*kmsg.DeleteRecordsResponse)
del []string
retErr error
unknownTopic bool
)
for i := range resp.Topics {
t := &resp.Topics[i]
for j := range t.Partitions {
p := &t.Partitions[j]
err := kerr.ErrorForCode(p.ErrorCode)
if err == kerr.UnknownTopicOrPartition || err == kerr.NotLeaderForPartition {
del = append(del, t.Topic)
unknownTopic = unknownTopic || err == kerr.UnknownTopicOrPartition
}
onRespShardErr(&retErr, err)
}
}
if cl.maybeDeleteMappedMetadata(unknownTopic, del...) {
return retErr
}
return nil
}
func (*deleteRecordsSharder) merge(sresps []ResponseShard) (kmsg.Response, error) {
merged := kmsg.NewPtrDeleteRecordsResponse()
topics := make(map[string][]kmsg.DeleteRecordsResponseTopicPartition)
firstErr := firstErrMerger(sresps, func(kresp kmsg.Response) {
resp := kresp.(*kmsg.DeleteRecordsResponse)
merged.Version = resp.Version
merged.ThrottleMillis = resp.ThrottleMillis
for _, topic := range resp.Topics {
topics[topic.Topic] = append(topics[topic.Topic], topic.Partitions...)
}
})
for topic, partitions := range topics {
respTopic := kmsg.NewDeleteRecordsResponseTopic()
respTopic.Topic = topic
respTopic.Partitions = partitions
merged.Topics = append(merged.Topics, respTopic)
}
return merged, firstErr
}
// handle sharding OffsetForLeaderEpochRequest
type offsetForLeaderEpochSharder struct{ *Client }
func (cl *offsetForLeaderEpochSharder) shard(ctx context.Context, kreq kmsg.Request, _ error) ([]issueShard, bool, error) {
req := kreq.(*kmsg.OffsetForLeaderEpochRequest)
var need []string
for _, topic := range req.Topics {
need = append(need, topic.Topic)
}
mapping, err := cl.fetchMappedMetadata(ctx, need, true, 0)
if err != nil {
return nil, false, err
}
brokerReqs := make(map[int32]map[string][]kmsg.OffsetForLeaderEpochRequestTopicPartition)
var unknowns unknownErrShards
for _, topic := range req.Topics {
t := topic.Topic
tmapping, exists := mapping[t]
if err := unknownOrCode(exists, tmapping.t.ErrorCode); err != nil {
unknowns.errs(err, t, topic.Partitions)
continue
}
for _, partition := range topic.Partitions {
p, exists := tmapping.ps[partition.Partition]
if err := unknownOrCode(exists, p.ErrorCode); err != nil {
unknowns.err(err, t, partition)
continue
}
if err := noLeader(p.Leader); err != nil {
unknowns.err(err, t, partition)
continue
}
brokerReq := brokerReqs[p.Leader]
if brokerReq == nil {
brokerReq = make(map[string][]kmsg.OffsetForLeaderEpochRequestTopicPartition)
brokerReqs[p.Leader] = brokerReq
}
brokerReq[topic.Topic] = append(brokerReq[topic.Topic], partition)
}
}
mkreq := func() *kmsg.OffsetForLeaderEpochRequest {
r := kmsg.NewPtrOffsetForLeaderEpochRequest()
r.ReplicaID = req.ReplicaID
return r
}
var issues []issueShard
for brokerID, brokerReq := range brokerReqs {
req := mkreq()
for topic, parts := range brokerReq {
reqTopic := kmsg.NewOffsetForLeaderEpochRequestTopic()
reqTopic.Topic = topic
reqTopic.Partitions = parts
req.Topics = append(req.Topics, reqTopic)
}
issues = append(issues, issueShard{
req: req,
broker: brokerID,
})
}
return append(issues, unknowns.collect(mkreq, func(r *kmsg.OffsetForLeaderEpochRequest, topic string, parts []kmsg.OffsetForLeaderEpochRequestTopicPartition) {
reqTopic := kmsg.NewOffsetForLeaderEpochRequestTopic()
reqTopic.Topic = topic
reqTopic.Partitions = parts
r.Topics = append(r.Topics, reqTopic)
})...), true, nil // this is reshardable
}
func (cl *offsetForLeaderEpochSharder) onResp(_ kmsg.Request, kresp kmsg.Response) error {
var (
resp = kresp.(*kmsg.OffsetForLeaderEpochResponse)
del []string
retErr error
unknownTopic bool
)
for i := range resp.Topics {
t := &resp.Topics[i]
for j := range t.Partitions {
p := &t.Partitions[j]
err := kerr.ErrorForCode(p.ErrorCode)
if err == kerr.UnknownTopicOrPartition || err == kerr.NotLeaderForPartition {
del = append(del, t.Topic)
unknownTopic = unknownTopic || err == kerr.UnknownTopicOrPartition
}
onRespShardErr(&retErr, err)
}
}
if cl.maybeDeleteMappedMetadata(unknownTopic, del...) {
return retErr
}
return nil
}
func (*offsetForLeaderEpochSharder) merge(sresps []ResponseShard) (kmsg.Response, error) {
merged := kmsg.NewPtrOffsetForLeaderEpochResponse()
topics := make(map[string][]kmsg.OffsetForLeaderEpochResponseTopicPartition)
firstErr := firstErrMerger(sresps, func(kresp kmsg.Response) {
resp := kresp.(*kmsg.OffsetForLeaderEpochResponse)
merged.Version = resp.Version
merged.ThrottleMillis = resp.ThrottleMillis
for _, topic := range resp.Topics {
topics[topic.Topic] = append(topics[topic.Topic], topic.Partitions...)
}
})
for topic, partitions := range topics {
respTopic := kmsg.NewOffsetForLeaderEpochResponseTopic()
respTopic.Topic = topic
respTopic.Partitions = partitions
merged.Topics = append(merged.Topics, respTopic)
}
return merged, firstErr
}
// handle sharding AddPartitionsToTXn, where v4+ switched to batch requests
type addPartitionsToTxnSharder struct{ *Client }
func addPartitionsReqToTxn(req *kmsg.AddPartitionsToTxnRequest) {
t := kmsg.NewAddPartitionsToTxnRequestTransaction()
t.TransactionalID = req.TransactionalID
t.ProducerID = req.ProducerID
t.ProducerEpoch = req.ProducerEpoch
for i := range req.Topics {
rt := &req.Topics[i]
tt := kmsg.NewAddPartitionsToTxnRequestTransactionTopic()
tt.Topic = rt.Topic
tt.Partitions = rt.Partitions
t.Topics = append(t.Topics, tt)
}
req.Transactions = append(req.Transactions, t)
}
func addPartitionsTxnToReq(req *kmsg.AddPartitionsToTxnRequest) {
if len(req.Transactions) != 1 {
return
}
t0 := &req.Transactions[0]
req.TransactionalID = t0.TransactionalID
req.ProducerID = t0.ProducerID
req.ProducerEpoch = t0.ProducerEpoch
for _, tt := range t0.Topics {
rt := kmsg.NewAddPartitionsToTxnRequestTopic()
rt.Topic = tt.Topic
rt.Partitions = tt.Partitions
req.Topics = append(req.Topics, rt)
}
}
func addPartitionsTxnToResp(resp *kmsg.AddPartitionsToTxnResponse) {
if len(resp.Transactions) == 0 {
return
}
t0 := &resp.Transactions[0]
for _, tt := range t0.Topics {
rt := kmsg.NewAddPartitionsToTxnResponseTopic()
rt.Topic = tt.Topic
for _, tp := range tt.Partitions {
rp := kmsg.NewAddPartitionsToTxnResponseTopicPartition()
rp.Partition = tp.Partition
rp.ErrorCode = tp.ErrorCode
rt.Partitions = append(rt.Partitions, rp)
}
resp.Topics = append(resp.Topics, rt)
}
}
func (cl *addPartitionsToTxnSharder) shard(ctx context.Context, kreq kmsg.Request, _ error) ([]issueShard, bool, error) {
req := kreq.(*kmsg.AddPartitionsToTxnRequest)
if len(req.Transactions) == 0 {
addPartitionsReqToTxn(req)
}
txnIDs := make([]string, 0, len(req.Transactions))
for i := range req.Transactions {
txnIDs = append(txnIDs, req.Transactions[i].TransactionalID)
}
coordinators := cl.loadCoordinators(ctx, coordinatorTypeTxn, txnIDs...)
type unkerr struct {
err error
txn kmsg.AddPartitionsToTxnRequestTransaction
}
var (
brokerReqs = make(map[int32]*kmsg.AddPartitionsToTxnRequest)
kerrs = make(map[*kerr.Error][]kmsg.AddPartitionsToTxnRequestTransaction)
unkerrs []unkerr
)
newReq := func(txns ...kmsg.AddPartitionsToTxnRequestTransaction) *kmsg.AddPartitionsToTxnRequest {
req := kmsg.NewPtrAddPartitionsToTxnRequest()
req.Transactions = txns
addPartitionsTxnToReq(req)
return req
}
for _, txn := range req.Transactions {
berr := coordinators[txn.TransactionalID]
var ke *kerr.Error
switch {
case berr.err == nil:
brokerReq := brokerReqs[berr.b.meta.NodeID]
if brokerReq == nil {
brokerReq = newReq(txn)
brokerReqs[berr.b.meta.NodeID] = brokerReq
} else {
brokerReq.Transactions = append(brokerReq.Transactions, txn)
}
case errors.As(berr.err, &ke):
kerrs[ke] = append(kerrs[ke], txn)
default:
unkerrs = append(unkerrs, unkerr{berr.err, txn})
}
}
var issues []issueShard
for id, req := range brokerReqs {
if len(req.Transactions) <= 1 || len(req.Transactions) == 1 && !req.Transactions[0].VerifyOnly {
issues = append(issues, issueShard{
req: req,
pin: &pinReq{pinMax: true, max: 3},
broker: id,
})
} else {
issues = append(issues, issueShard{
req: req,
broker: id,
})
}
}
for _, unkerr := range unkerrs {
issues = append(issues, issueShard{
req: newReq(unkerr.txn),
err: unkerr.err,
})
}
for kerr, txns := range kerrs {
issues = append(issues, issueShard{
req: newReq(txns...),
err: kerr,
})
}
return issues, true, nil // reshardable to load correct coordinators
}
func (cl *addPartitionsToTxnSharder) onResp(kreq kmsg.Request, kresp kmsg.Response) error {
req := kreq.(*kmsg.AddPartitionsToTxnRequest)
resp := kresp.(*kmsg.AddPartitionsToTxnResponse)
// We default to the top level error, which is used in v4+. For v3
// (case 0), we use the per-partition error, which is the same for
// every partition on not_coordinator errors.
code := resp.ErrorCode
if code == 0 && len(resp.Transactions) == 0 {
// Convert v3 and prior to v4+
resptxn := kmsg.NewAddPartitionsToTxnResponseTransaction()
resptxn.TransactionalID = req.TransactionalID
for _, rt := range resp.Topics {
respt := kmsg.NewAddPartitionsToTxnResponseTransactionTopic()
respt.Topic = rt.Topic
for _, rp := range rt.Partitions {
respp := kmsg.NewAddPartitionsToTxnResponseTransactionTopicPartition()
respp.Partition = rp.Partition
respp.ErrorCode = rp.ErrorCode
code = rp.ErrorCode // v3 and prior has per-partition errors, not top level
respt.Partitions = append(respt.Partitions, respp)
}
resptxn.Topics = append(resptxn.Topics, respt)
}
resp.Transactions = append(resp.Transactions, resptxn)
} else {
// Convert v4 to v3 and prior: either we have a top level error
// code or we have at least one transaction.
//
// If the code is non-zero, we convert it to per-partition error
// codes; v3 does not have a top level err.
addPartitionsTxnToResp(resp)
if code != 0 {
for _, reqt := range req.Topics {
respt := kmsg.NewAddPartitionsToTxnResponseTopic()
respt.Topic = reqt.Topic
for _, reqp := range reqt.Partitions {
respp := kmsg.NewAddPartitionsToTxnResponseTopicPartition()
respp.Partition = reqp
respp.ErrorCode = resp.ErrorCode
respt.Partitions = append(respt.Partitions, respp)
}
resp.Topics = append(resp.Topics, respt)
}
}
}
if err := kerr.ErrorForCode(code); cl.maybeDeleteStaleCoordinator(req.TransactionalID, coordinatorTypeTxn, err) {
return err
}
return nil
}
func (*addPartitionsToTxnSharder) merge(sresps []ResponseShard) (kmsg.Response, error) {
merged := kmsg.NewPtrAddPartitionsToTxnResponse()
firstErr := firstErrMerger(sresps, func(kresp kmsg.Response) {
resp := kresp.(*kmsg.AddPartitionsToTxnResponse)
merged.Version = resp.Version
merged.ThrottleMillis = resp.ThrottleMillis
merged.ErrorCode = resp.ErrorCode
merged.Transactions = append(merged.Transactions, resp.Transactions...)
})
addPartitionsTxnToResp(merged)
return merged, firstErr
}
// handle sharding WriteTxnMarkersRequest
type writeTxnMarkersSharder struct{ *Client }
func (cl *writeTxnMarkersSharder) shard(ctx context.Context, kreq kmsg.Request, _ error) ([]issueShard, bool, error) {
req := kreq.(*kmsg.WriteTxnMarkersRequest)
var need []string
for _, marker := range req.Markers {
for _, topic := range marker.Topics {
need = append(need, topic.Topic)
}
}
mapping, err := cl.fetchMappedMetadata(ctx, need, true, 0)
if err != nil {
return nil, false, err
}
type pidEpochCommit struct {
pid int64
epoch int16
commit bool
}
brokerReqs := make(map[int32]map[pidEpochCommit]map[string][]int32)
unknown := make(map[error]map[pidEpochCommit]map[string][]int32) // err => pec => topic => partitions
addreq := func(b int32, pec pidEpochCommit, t string, p int32) {
pecs := brokerReqs[b]
if pecs == nil {
pecs = make(map[pidEpochCommit]map[string][]int32)
brokerReqs[b] = pecs
}
ts := pecs[pec]
if ts == nil {
ts = make(map[string][]int32)
pecs[pec] = ts
}
ts[t] = append(ts[t], p)
}
addunk := func(err error, pec pidEpochCommit, t string, p int32) {
pecs := unknown[err]
if pecs == nil {
pecs = make(map[pidEpochCommit]map[string][]int32)
unknown[err] = pecs
}
ts := pecs[pec]
if ts == nil {
ts = make(map[string][]int32)
pecs[pec] = ts
}
ts[t] = append(ts[t], p)
}
for _, marker := range req.Markers {
pec := pidEpochCommit{
marker.ProducerID,
marker.ProducerEpoch,
marker.Committed,
}
for _, topic := range marker.Topics {
t := topic.Topic
tmapping, exists := mapping[t]
if err := unknownOrCode(exists, tmapping.t.ErrorCode); err != nil {
for _, partition := range topic.Partitions {
addunk(err, pec, t, partition)
}
continue
}
for _, partition := range topic.Partitions {
p, exists := tmapping.ps[partition]
if err := unknownOrCode(exists, p.ErrorCode); err != nil {
addunk(err, pec, t, partition)
continue
}
if err := noLeader(p.Leader); err != nil {
addunk(err, pec, t, partition)
continue
}
addreq(p.Leader, pec, t, partition)
}
}
}
mkreq := kmsg.NewPtrWriteTxnMarkersRequest
var issues []issueShard
for brokerID, brokerReq := range brokerReqs {
req := mkreq()
for pec, topics := range brokerReq {
rm := kmsg.NewWriteTxnMarkersRequestMarker()
rm.ProducerID = pec.pid
rm.ProducerEpoch = pec.epoch
rm.Committed = pec.commit
for topic, parts := range topics {
rt := kmsg.NewWriteTxnMarkersRequestMarkerTopic()
rt.Topic = topic
rt.Partitions = parts
rm.Topics = append(rm.Topics, rt)
}
req.Markers = append(req.Markers, rm)
}
issues = append(issues, issueShard{
req: req,
broker: brokerID,
})
}
for err, errReq := range unknown {
req := mkreq()
for pec, topics := range errReq {
rm := kmsg.NewWriteTxnMarkersRequestMarker()
rm.ProducerID = pec.pid
rm.ProducerEpoch = pec.epoch
rm.Committed = pec.commit
for topic, parts := range topics {
rt := kmsg.NewWriteTxnMarkersRequestMarkerTopic()
rt.Topic = topic
rt.Partitions = parts
rm.Topics = append(rm.Topics, rt)
}
req.Markers = append(req.Markers, rm)
}
issues = append(issues, issueShard{
req: req,
err: err,
})
}
return issues, true, nil // this is reshardable
}
func (cl *writeTxnMarkersSharder) onResp(_ kmsg.Request, kresp kmsg.Response) error {
var (
resp = kresp.(*kmsg.WriteTxnMarkersResponse)
del []string
retErr error
unknownTopic bool
)
for i := range resp.Markers {
m := &resp.Markers[i]
for j := range m.Topics {
t := &m.Topics[j]
for k := range t.Partitions {
p := &t.Partitions[k]
err := kerr.ErrorForCode(p.ErrorCode)
if err == kerr.UnknownTopicOrPartition || err == kerr.NotLeaderForPartition {
del = append(del, t.Topic)
unknownTopic = unknownTopic || err == kerr.UnknownTopicOrPartition
}
onRespShardErr(&retErr, err)
}
}
}
if cl.maybeDeleteMappedMetadata(unknownTopic, del...) {
return retErr
}
return nil
}
func (*writeTxnMarkersSharder) merge(sresps []ResponseShard) (kmsg.Response, error) {
merged := kmsg.NewPtrWriteTxnMarkersResponse()
markers := make(map[int64]map[string][]kmsg.WriteTxnMarkersResponseMarkerTopicPartition)
firstErr := firstErrMerger(sresps, func(kresp kmsg.Response) {
resp := kresp.(*kmsg.WriteTxnMarkersResponse)
merged.Version = resp.Version
for _, marker := range resp.Markers {
topics := markers[marker.ProducerID]
if topics == nil {
topics = make(map[string][]kmsg.WriteTxnMarkersResponseMarkerTopicPartition)
markers[marker.ProducerID] = topics
}
for _, topic := range marker.Topics {
topics[topic.Topic] = append(topics[topic.Topic], topic.Partitions...)
}
}
})
for pid, topics := range markers {
respMarker := kmsg.NewWriteTxnMarkersResponseMarker()
respMarker.ProducerID = pid
for topic, partitions := range topics {
respTopic := kmsg.NewWriteTxnMarkersResponseMarkerTopic()
respTopic.Topic = topic
respTopic.Partitions = append(respTopic.Partitions, partitions...)
respMarker.Topics = append(respMarker.Topics, respTopic)
}
merged.Markers = append(merged.Markers, respMarker)
}
return merged, firstErr
}
// handle sharding DescribeConfigsRequest
type describeConfigsSharder struct{ *Client }
func (*describeConfigsSharder) shard(_ context.Context, kreq kmsg.Request, _ error) ([]issueShard, bool, error) {
req := kreq.(*kmsg.DescribeConfigsRequest)
brokerReqs := make(map[int32][]kmsg.DescribeConfigsRequestResource)
var any []kmsg.DescribeConfigsRequestResource
for i := range req.Resources {
resource := req.Resources[i]
switch resource.ResourceType {
case kmsg.ConfigResourceTypeBroker:
case kmsg.ConfigResourceTypeBrokerLogger:
default:
any = append(any, resource)
continue
}
id, err := strconv.ParseInt(resource.ResourceName, 10, 32)
if err != nil || id < 0 {
any = append(any, resource)
continue
}
brokerReqs[int32(id)] = append(brokerReqs[int32(id)], resource)
}
var issues []issueShard
for brokerID, brokerReq := range brokerReqs {
newReq := kmsg.NewPtrDescribeConfigsRequest()
newReq.Resources = brokerReq
newReq.IncludeSynonyms = req.IncludeSynonyms
newReq.IncludeDocumentation = req.IncludeDocumentation
issues = append(issues, issueShard{
req: newReq,
broker: brokerID,
})
}
if len(any) > 0 {
newReq := kmsg.NewPtrDescribeConfigsRequest()
newReq.Resources = any
newReq.IncludeSynonyms = req.IncludeSynonyms
newReq.IncludeDocumentation = req.IncludeDocumentation
issues = append(issues, issueShard{
req: newReq,
any: true,
})
}
return issues, false, nil // this is not reshardable, but the any block can go anywhere
}
func (*describeConfigsSharder) onResp(kmsg.Request, kmsg.Response) error { return nil } // configs: topics not mapped, nothing retryable
func (*describeConfigsSharder) merge(sresps []ResponseShard) (kmsg.Response, error) {
merged := kmsg.NewPtrDescribeConfigsResponse()
return merged, firstErrMerger(sresps, func(kresp kmsg.Response) {
resp := kresp.(*kmsg.DescribeConfigsResponse)
merged.Version = resp.Version
merged.ThrottleMillis = resp.ThrottleMillis
merged.Resources = append(merged.Resources, resp.Resources...)
})
}
// handle sharding AlterConfigsRequest
type alterConfigsSharder struct{ *Client }
func (*alterConfigsSharder) shard(_ context.Context, kreq kmsg.Request, _ error) ([]issueShard, bool, error) {
req := kreq.(*kmsg.AlterConfigsRequest)
brokerReqs := make(map[int32][]kmsg.AlterConfigsRequestResource)
var any []kmsg.AlterConfigsRequestResource
for i := range req.Resources {
resource := req.Resources[i]
switch resource.ResourceType {
case kmsg.ConfigResourceTypeBroker:
case kmsg.ConfigResourceTypeBrokerLogger:
default:
any = append(any, resource)
continue
}
id, err := strconv.ParseInt(resource.ResourceName, 10, 32)
if err != nil || id < 0 {
any = append(any, resource)
continue
}
brokerReqs[int32(id)] = append(brokerReqs[int32(id)], resource)
}
var issues []issueShard
for brokerID, brokerReq := range brokerReqs {
newReq := kmsg.NewPtrAlterConfigsRequest()
newReq.Resources = brokerReq
newReq.ValidateOnly = req.ValidateOnly
issues = append(issues, issueShard{
req: newReq,
broker: brokerID,
})
}
if len(any) > 0 {
newReq := kmsg.NewPtrAlterConfigsRequest()
newReq.Resources = any
newReq.ValidateOnly = req.ValidateOnly
issues = append(issues, issueShard{
req: newReq,
any: true,
})
}
return issues, false, nil // this is not reshardable, but the any block can go anywhere
}
func (*alterConfigsSharder) onResp(kmsg.Request, kmsg.Response) error { return nil } // configs: topics not mapped, nothing retryable
func (*alterConfigsSharder) merge(sresps []ResponseShard) (kmsg.Response, error) {
merged := kmsg.NewPtrAlterConfigsResponse()
return merged, firstErrMerger(sresps, func(kresp kmsg.Response) {
resp := kresp.(*kmsg.AlterConfigsResponse)
merged.Version = resp.Version
merged.ThrottleMillis = resp.ThrottleMillis
merged.Resources = append(merged.Resources, resp.Resources...)
})
}
// handles sharding AlterReplicaLogDirsRequest
type alterReplicaLogDirsSharder struct{ *Client }
func (cl *alterReplicaLogDirsSharder) shard(ctx context.Context, kreq kmsg.Request, _ error) ([]issueShard, bool, error) {
req := kreq.(*kmsg.AlterReplicaLogDirsRequest)
needMap := make(map[string]struct{})
for _, dir := range req.Dirs {
for _, topic := range dir.Topics {
needMap[topic.Topic] = struct{}{}
}
}
var need []string
for topic := range needMap {
need = append(need, topic)
}
mapping, err := cl.fetchMappedMetadata(ctx, need, false, 0) // bypass cache, tricky to manage response
if err != nil {
return nil, false, err
}
brokerReqs := make(map[int32]map[string]map[string][]int32) // broker => dir => topic => partitions
unknowns := make(map[error]map[string]map[string][]int32) // err => dir => topic => partitions
addBroker := func(broker int32, dir, topic string, partition int32) {
brokerDirs := brokerReqs[broker]
if brokerDirs == nil {
brokerDirs = make(map[string]map[string][]int32)
brokerReqs[broker] = brokerDirs
}
dirTopics := brokerDirs[dir]
if dirTopics == nil {
dirTopics = make(map[string][]int32)
brokerDirs[dir] = dirTopics
}
dirTopics[topic] = append(dirTopics[topic], partition)
}
addUnknown := func(err error, dir, topic string, partition int32) {
dirs := unknowns[err]
if dirs == nil {
dirs = make(map[string]map[string][]int32)
unknowns[err] = dirs
}
dirTopics := dirs[dir]
if dirTopics == nil {
dirTopics = make(map[string][]int32)
dirs[dir] = dirTopics
}
dirTopics[topic] = append(dirTopics[topic], partition)
}
for _, dir := range req.Dirs {
for _, topic := range dir.Topics {
t := topic.Topic
tmapping, exists := mapping[t]
if err := unknownOrCode(exists, tmapping.t.ErrorCode); err != nil {
for _, partition := range topic.Partitions {
addUnknown(err, dir.Dir, t, partition)
}
continue
}
for _, partition := range topic.Partitions {
p, exists := tmapping.ps[partition]
if err := unknownOrCode(exists, p.ErrorCode); err != nil {
addUnknown(err, dir.Dir, t, partition)
continue
}
for _, replica := range p.Replicas {
addBroker(replica, dir.Dir, t, partition)
}
}
}
}
var issues []issueShard
for brokerID, brokerReq := range brokerReqs {
req := kmsg.NewPtrAlterReplicaLogDirsRequest()
for dir, topics := range brokerReq {
rd := kmsg.NewAlterReplicaLogDirsRequestDir()
rd.Dir = dir
for topic, partitions := range topics {
rdTopic := kmsg.NewAlterReplicaLogDirsRequestDirTopic()
rdTopic.Topic = topic
rdTopic.Partitions = partitions
rd.Topics = append(rd.Topics, rdTopic)
}
req.Dirs = append(req.Dirs, rd)
}
issues = append(issues, issueShard{
req: req,
broker: brokerID,
})
}
for err, dirs := range unknowns {
req := kmsg.NewPtrAlterReplicaLogDirsRequest()
for dir, topics := range dirs {
rd := kmsg.NewAlterReplicaLogDirsRequestDir()
rd.Dir = dir
for topic, partitions := range topics {
rdTopic := kmsg.NewAlterReplicaLogDirsRequestDirTopic()
rdTopic.Topic = topic
rdTopic.Partitions = partitions
rd.Topics = append(rd.Topics, rdTopic)
}
req.Dirs = append(req.Dirs, rd)
}
issues = append(issues, issueShard{
req: req,
err: err,
})
}
return issues, true, nil // this is reshardable
}
func (*alterReplicaLogDirsSharder) onResp(kmsg.Request, kmsg.Response) error { return nil } // topic / partitions: not retried
// merge does not make sense for this function, but we provide a one anyway.
func (*alterReplicaLogDirsSharder) merge(sresps []ResponseShard) (kmsg.Response, error) {
merged := kmsg.NewPtrAlterReplicaLogDirsResponse()
topics := make(map[string][]kmsg.AlterReplicaLogDirsResponseTopicPartition)
firstErr := firstErrMerger(sresps, func(kresp kmsg.Response) {
resp := kresp.(*kmsg.AlterReplicaLogDirsResponse)
merged.Version = resp.Version
merged.ThrottleMillis = resp.ThrottleMillis
for _, topic := range resp.Topics {
topics[topic.Topic] = append(topics[topic.Topic], topic.Partitions...)
}
})
for topic, partitions := range topics {
respTopic := kmsg.NewAlterReplicaLogDirsResponseTopic()
respTopic.Topic = topic
respTopic.Partitions = partitions
merged.Topics = append(merged.Topics, respTopic)
}
return merged, firstErr
}
// handles sharding DescribeLogDirsRequest
type describeLogDirsSharder struct{ *Client }
func (cl *describeLogDirsSharder) shard(ctx context.Context, kreq kmsg.Request, _ error) ([]issueShard, bool, error) {
req := kreq.(*kmsg.DescribeLogDirsRequest)
// If req.Topics is nil, the request is to describe all logdirs. Thus,
// we will issue the request to all brokers (similar to ListGroups).
if req.Topics == nil {
return cl.allBrokersShardedReq(ctx, func() kmsg.Request {
dup := *req
return &dup
})
}
var need []string
for _, topic := range req.Topics {
need = append(need, topic.Topic)
}
mapping, err := cl.fetchMappedMetadata(ctx, need, false, 0) // bypass cache, tricky to manage response
if err != nil {
return nil, false, err
}
brokerReqs := make(map[int32]map[string][]int32)
var unknowns unknownErrShards
for _, topic := range req.Topics {
t := topic.Topic
tmapping, exists := mapping[t]
if err := unknownOrCode(exists, tmapping.t.ErrorCode); err != nil {
unknowns.errs(err, t, topic.Partitions)
continue
}
for _, partition := range topic.Partitions {
p, exists := tmapping.ps[partition]
if err := unknownOrCode(exists, p.ErrorCode); err != nil {
unknowns.err(err, t, partition)
continue
}
for _, replica := range p.Replicas {
brokerReq := brokerReqs[replica]
if brokerReq == nil {
brokerReq = make(map[string][]int32)
brokerReqs[replica] = brokerReq
}
brokerReq[topic.Topic] = append(brokerReq[topic.Topic], partition)
}
}
}
mkreq := kmsg.NewPtrDescribeLogDirsRequest
var issues []issueShard
for brokerID, brokerReq := range brokerReqs {
req := mkreq()
for topic, parts := range brokerReq {
reqTopic := kmsg.NewDescribeLogDirsRequestTopic()
reqTopic.Topic = topic
reqTopic.Partitions = parts
req.Topics = append(req.Topics, reqTopic)
}
issues = append(issues, issueShard{
req: req,
broker: brokerID,
})
}
return append(issues, unknowns.collect(mkreq, func(r *kmsg.DescribeLogDirsRequest, topic string, parts []int32) {
reqTopic := kmsg.NewDescribeLogDirsRequestTopic()
reqTopic.Topic = topic
reqTopic.Partitions = parts
r.Topics = append(r.Topics, reqTopic)
})...), true, nil // this is reshardable
}
func (*describeLogDirsSharder) onResp(kmsg.Request, kmsg.Response) error { return nil } // topic / configs: not retried
// merge does not make sense for this function, but we provide one anyway.
// We lose the error code for directories.
func (*describeLogDirsSharder) merge(sresps []ResponseShard) (kmsg.Response, error) {
merged := kmsg.NewPtrDescribeLogDirsResponse()
dirs := make(map[string]map[string][]kmsg.DescribeLogDirsResponseDirTopicPartition)
firstErr := firstErrMerger(sresps, func(kresp kmsg.Response) {
resp := kresp.(*kmsg.DescribeLogDirsResponse)
merged.Version = resp.Version
merged.ThrottleMillis = resp.ThrottleMillis
for _, dir := range resp.Dirs {
mergeDir := dirs[dir.Dir]
if mergeDir == nil {
mergeDir = make(map[string][]kmsg.DescribeLogDirsResponseDirTopicPartition)
dirs[dir.Dir] = mergeDir
}
for _, topic := range dir.Topics {
mergeDir[topic.Topic] = append(mergeDir[topic.Topic], topic.Partitions...)
}
}
})
for dir, topics := range dirs {
md := kmsg.NewDescribeLogDirsResponseDir()
md.Dir = dir
for topic, partitions := range topics {
mdTopic := kmsg.NewDescribeLogDirsResponseDirTopic()
mdTopic.Topic = topic
mdTopic.Partitions = partitions
md.Topics = append(md.Topics, mdTopic)
}
merged.Dirs = append(merged.Dirs, md)
}
return merged, firstErr
}
// handles sharding DeleteGroupsRequest
type deleteGroupsSharder struct{ *Client }
func (cl *deleteGroupsSharder) shard(ctx context.Context, kreq kmsg.Request, _ error) ([]issueShard, bool, error) {
req := kreq.(*kmsg.DeleteGroupsRequest)
coordinators := cl.loadCoordinators(ctx, coordinatorTypeGroup, req.Groups...)
type unkerr struct {
err error
group string
}
var (
brokerReqs = make(map[int32]*kmsg.DeleteGroupsRequest)
kerrs = make(map[*kerr.Error][]string)
unkerrs []unkerr
)
newReq := func(groups ...string) *kmsg.DeleteGroupsRequest {
newReq := kmsg.NewPtrDeleteGroupsRequest()
newReq.Groups = groups
return newReq
}
for _, group := range req.Groups {
berr := coordinators[group]
var ke *kerr.Error
switch {
case berr.err == nil:
brokerReq := brokerReqs[berr.b.meta.NodeID]
if brokerReq == nil {
brokerReq = newReq()
brokerReqs[berr.b.meta.NodeID] = brokerReq
}
brokerReq.Groups = append(brokerReq.Groups, group)
case errors.As(berr.err, &ke):
kerrs[ke] = append(kerrs[ke], group)
default:
unkerrs = append(unkerrs, unkerr{berr.err, group})
}
}
var issues []issueShard
for id, req := range brokerReqs {
issues = append(issues, issueShard{
req: req,
broker: id,
})
}
for _, unkerr := range unkerrs {
issues = append(issues, issueShard{
req: newReq(unkerr.group),
err: unkerr.err,
})
}
for kerr, groups := range kerrs {
issues = append(issues, issueShard{
req: newReq(groups...),
err: kerr,
})
}
return issues, true, nil // reshardable to load correct coordinators
}
func (cl *deleteGroupsSharder) onResp(_ kmsg.Request, kresp kmsg.Response) error {
resp := kresp.(*kmsg.DeleteGroupsResponse)
var retErr error
for i := range resp.Groups {
group := &resp.Groups[i]
err := kerr.ErrorForCode(group.ErrorCode)
cl.maybeDeleteStaleCoordinator(group.Group, coordinatorTypeGroup, err)
onRespShardErr(&retErr, err)
}
return retErr
}
func (*deleteGroupsSharder) merge(sresps []ResponseShard) (kmsg.Response, error) {
merged := kmsg.NewPtrDeleteGroupsResponse()
return merged, firstErrMerger(sresps, func(kresp kmsg.Response) {
resp := kresp.(*kmsg.DeleteGroupsResponse)
merged.Version = resp.Version
merged.ThrottleMillis = resp.ThrottleMillis
merged.Groups = append(merged.Groups, resp.Groups...)
})
}
// handle sharding IncrementalAlterConfigsRequest
type incrementalAlterConfigsSharder struct{ *Client }
func (*incrementalAlterConfigsSharder) shard(_ context.Context, kreq kmsg.Request, _ error) ([]issueShard, bool, error) {
req := kreq.(*kmsg.IncrementalAlterConfigsRequest)
brokerReqs := make(map[int32][]kmsg.IncrementalAlterConfigsRequestResource)
var any []kmsg.IncrementalAlterConfigsRequestResource
for i := range req.Resources {
resource := req.Resources[i]
switch resource.ResourceType {
case kmsg.ConfigResourceTypeBroker:
case kmsg.ConfigResourceTypeBrokerLogger:
default:
any = append(any, resource)
continue
}
id, err := strconv.ParseInt(resource.ResourceName, 10, 32)
if err != nil || id < 0 {
any = append(any, resource)
continue
}
brokerReqs[int32(id)] = append(brokerReqs[int32(id)], resource)
}
var issues []issueShard
for brokerID, brokerReq := range brokerReqs {
newReq := kmsg.NewPtrIncrementalAlterConfigsRequest()
newReq.Resources = brokerReq
newReq.ValidateOnly = req.ValidateOnly
issues = append(issues, issueShard{
req: newReq,
broker: brokerID,
})
}
if len(any) > 0 {
newReq := kmsg.NewPtrIncrementalAlterConfigsRequest()
newReq.Resources = any
newReq.ValidateOnly = req.ValidateOnly
issues = append(issues, issueShard{
req: newReq,
any: true,
})
}
return issues, false, nil // this is not reshardable, but the any block can go anywhere
}
func (*incrementalAlterConfigsSharder) onResp(kmsg.Request, kmsg.Response) error { return nil } // configs: topics not mapped, nothing retryable
func (*incrementalAlterConfigsSharder) merge(sresps []ResponseShard) (kmsg.Response, error) {
merged := kmsg.NewPtrIncrementalAlterConfigsResponse()
return merged, firstErrMerger(sresps, func(kresp kmsg.Response) {
resp := kresp.(*kmsg.IncrementalAlterConfigsResponse)
merged.Version = resp.Version
merged.ThrottleMillis = resp.ThrottleMillis
merged.Resources = append(merged.Resources, resp.Resources...)
})
}
// handle sharding DescribeProducersRequest
type describeProducersSharder struct{ *Client }
func (cl *describeProducersSharder) shard(ctx context.Context, kreq kmsg.Request, _ error) ([]issueShard, bool, error) {
req := kreq.(*kmsg.DescribeProducersRequest)
var need []string
for _, topic := range req.Topics {
need = append(need, topic.Topic)
}
mapping, err := cl.fetchMappedMetadata(ctx, need, true, 0)
if err != nil {
return nil, false, err
}
brokerReqs := make(map[int32]map[string][]int32) // broker => topic => partitions
var unknowns unknownErrShards
for _, topic := range req.Topics {
t := topic.Topic
tmapping, exists := mapping[t]
if err := unknownOrCode(exists, tmapping.t.ErrorCode); err != nil {
unknowns.errs(err, t, topic.Partitions)
continue
}
for _, partition := range topic.Partitions {
p, exists := tmapping.ps[partition]
if err := unknownOrCode(exists, p.ErrorCode); err != nil {
unknowns.err(err, t, partition)
continue
}
brokerReq := brokerReqs[p.Leader]
if brokerReq == nil {
brokerReq = make(map[string][]int32)
brokerReqs[p.Leader] = brokerReq
}
brokerReq[topic.Topic] = append(brokerReq[topic.Topic], partition)
}
}
mkreq := kmsg.NewPtrDescribeProducersRequest
var issues []issueShard
for brokerID, brokerReq := range brokerReqs {
req := mkreq()
for topic, parts := range brokerReq {
reqTopic := kmsg.NewDescribeProducersRequestTopic()
reqTopic.Topic = topic
reqTopic.Partitions = parts
req.Topics = append(req.Topics, reqTopic)
}
issues = append(issues, issueShard{
req: req,
broker: brokerID,
})
}
return append(issues, unknowns.collect(mkreq, func(r *kmsg.DescribeProducersRequest, topic string, parts []int32) {
reqTopic := kmsg.NewDescribeProducersRequestTopic()
reqTopic.Topic = topic
reqTopic.Partitions = parts
r.Topics = append(r.Topics, reqTopic)
})...), true, nil // this is reshardable
}
func (cl *describeProducersSharder) onResp(_ kmsg.Request, kresp kmsg.Response) error {
var (
resp = kresp.(*kmsg.DescribeProducersResponse)
del []string
retErr error
unknownTopic bool
)
for i := range resp.Topics {
t := &resp.Topics[i]
for j := range t.Partitions {
p := &t.Partitions[j]
err := kerr.ErrorForCode(p.ErrorCode)
if err == kerr.UnknownTopicOrPartition || err == kerr.NotLeaderForPartition {
del = append(del, t.Topic)
unknownTopic = unknownTopic || err == kerr.UnknownTopicOrPartition
}
onRespShardErr(&retErr, err)
}
}
if cl.maybeDeleteMappedMetadata(unknownTopic, del...) {
return retErr
}
return nil
}
func (*describeProducersSharder) merge(sresps []ResponseShard) (kmsg.Response, error) {
merged := kmsg.NewPtrDescribeProducersResponse()
topics := make(map[string][]kmsg.DescribeProducersResponseTopicPartition)
firstErr := firstErrMerger(sresps, func(kresp kmsg.Response) {
resp := kresp.(*kmsg.DescribeProducersResponse)
merged.Version = resp.Version
merged.ThrottleMillis = resp.ThrottleMillis
for _, topic := range resp.Topics {
topics[topic.Topic] = append(topics[topic.Topic], topic.Partitions...)
}
})
for topic, partitions := range topics {
respTopic := kmsg.NewDescribeProducersResponseTopic()
respTopic.Topic = topic
respTopic.Partitions = partitions
merged.Topics = append(merged.Topics, respTopic)
}
return merged, firstErr
}
// handles sharding DescribeTransactionsRequest
type describeTransactionsSharder struct{ *Client }
func (cl *describeTransactionsSharder) shard(ctx context.Context, kreq kmsg.Request, _ error) ([]issueShard, bool, error) {
req := kreq.(*kmsg.DescribeTransactionsRequest)
coordinators := cl.loadCoordinators(ctx, coordinatorTypeTxn, req.TransactionalIDs...)
type unkerr struct {
err error
txnID string
}
var (
brokerReqs = make(map[int32]*kmsg.DescribeTransactionsRequest)
kerrs = make(map[*kerr.Error][]string)
unkerrs []unkerr
)
newReq := func(txnIDs ...string) *kmsg.DescribeTransactionsRequest {
r := kmsg.NewPtrDescribeTransactionsRequest()
r.TransactionalIDs = txnIDs
return r
}
for _, txnID := range req.TransactionalIDs {
berr := coordinators[txnID]
var ke *kerr.Error
switch {
case berr.err == nil:
brokerReq := brokerReqs[berr.b.meta.NodeID]
if brokerReq == nil {
brokerReq = newReq()
brokerReqs[berr.b.meta.NodeID] = brokerReq
}
brokerReq.TransactionalIDs = append(brokerReq.TransactionalIDs, txnID)
case errors.As(berr.err, &ke):
kerrs[ke] = append(kerrs[ke], txnID)
default:
unkerrs = append(unkerrs, unkerr{berr.err, txnID})
}
}
var issues []issueShard
for id, req := range brokerReqs {
issues = append(issues, issueShard{
req: req,
broker: id,
})
}
for _, unkerr := range unkerrs {
issues = append(issues, issueShard{
req: newReq(unkerr.txnID),
err: unkerr.err,
})
}
for kerr, txnIDs := range kerrs {
issues = append(issues, issueShard{
req: newReq(txnIDs...),
err: kerr,
})
}
return issues, true, nil // reshardable to load correct coordinators
}
func (cl *describeTransactionsSharder) onResp(_ kmsg.Request, kresp kmsg.Response) error { // cleanup any stale coordinators
resp := kresp.(*kmsg.DescribeTransactionsResponse)
var retErr error
for i := range resp.TransactionStates {
txnState := &resp.TransactionStates[i]
err := kerr.ErrorForCode(txnState.ErrorCode)
cl.maybeDeleteStaleCoordinator(txnState.TransactionalID, coordinatorTypeTxn, err)
onRespShardErr(&retErr, err)
}
return retErr
}
func (*describeTransactionsSharder) merge(sresps []ResponseShard) (kmsg.Response, error) {
merged := kmsg.NewPtrDescribeTransactionsResponse()
return merged, firstErrMerger(sresps, func(kresp kmsg.Response) {
resp := kresp.(*kmsg.DescribeTransactionsResponse)
merged.Version = resp.Version
merged.ThrottleMillis = resp.ThrottleMillis
merged.TransactionStates = append(merged.TransactionStates, resp.TransactionStates...)
})
}
// handles sharding ListTransactionsRequest
type listTransactionsSharder struct{ *Client }
func (cl *listTransactionsSharder) shard(ctx context.Context, kreq kmsg.Request, _ error) ([]issueShard, bool, error) {
req := kreq.(*kmsg.ListTransactionsRequest)
return cl.allBrokersShardedReq(ctx, func() kmsg.Request {
dup := *req
return &dup
})
}
func (*listTransactionsSharder) onResp(_ kmsg.Request, kresp kmsg.Response) error {
resp := kresp.(*kmsg.ListTransactionsResponse)
return kerr.ErrorForCode(resp.ErrorCode)
}
func (*listTransactionsSharder) merge(sresps []ResponseShard) (kmsg.Response, error) {
merged := kmsg.NewPtrListTransactionsResponse()
unknownStates := make(map[string]struct{})
firstErr := firstErrMerger(sresps, func(kresp kmsg.Response) {
resp := kresp.(*kmsg.ListTransactionsResponse)
merged.Version = resp.Version
merged.ThrottleMillis = resp.ThrottleMillis
if merged.ErrorCode == 0 {
merged.ErrorCode = resp.ErrorCode
}
for _, state := range resp.UnknownStateFilters {
unknownStates[state] = struct{}{}
}
merged.TransactionStates = append(merged.TransactionStates, resp.TransactionStates...)
})
for unknownState := range unknownStates {
merged.UnknownStateFilters = append(merged.UnknownStateFilters, unknownState)
}
return merged, firstErr
}
// handles sharding ConsumerGroupDescribeRequest
type consumerGroupDescribeSharder struct{ *Client }
func (cl *consumerGroupDescribeSharder) shard(ctx context.Context, kreq kmsg.Request, _ error) ([]issueShard, bool, error) {
req := kreq.(*kmsg.ConsumerGroupDescribeRequest)
coordinators := cl.loadCoordinators(ctx, coordinatorTypeGroup, req.Groups...)
type unkerr struct {
err error
group string
}
var (
brokerReqs = make(map[int32]*kmsg.ConsumerGroupDescribeRequest)
kerrs = make(map[*kerr.Error][]string)
unkerrs []unkerr
)
newReq := func(groups ...string) *kmsg.ConsumerGroupDescribeRequest {
newReq := kmsg.NewPtrConsumerGroupDescribeRequest()
newReq.IncludeAuthorizedOperations = req.IncludeAuthorizedOperations
newReq.Groups = groups
return newReq
}
for _, group := range req.Groups {
berr := coordinators[group]
var ke *kerr.Error
switch {
case berr.err == nil:
brokerReq := brokerReqs[berr.b.meta.NodeID]
if brokerReq == nil {
brokerReq = newReq()
brokerReqs[berr.b.meta.NodeID] = brokerReq
}
brokerReq.Groups = append(brokerReq.Groups, group)
case errors.As(berr.err, &ke):
kerrs[ke] = append(kerrs[ke], group)
default:
unkerrs = append(unkerrs, unkerr{berr.err, group})
}
}
var issues []issueShard
for id, req := range brokerReqs {
issues = append(issues, issueShard{
req: req,
broker: id,
})
}
for _, unkerr := range unkerrs {
issues = append(issues, issueShard{
req: newReq(unkerr.group),
err: unkerr.err,
})
}
for kerr, groups := range kerrs {
issues = append(issues, issueShard{
req: newReq(groups...),
err: kerr,
})
}
return issues, true, nil // reshardable to load correct coordinators
}
func (cl *consumerGroupDescribeSharder) onResp(_ kmsg.Request, kresp kmsg.Response) error {
resp := kresp.(*kmsg.ConsumerGroupDescribeResponse)
var retErr error
for i := range resp.Groups {
group := &resp.Groups[i]
err := kerr.ErrorForCode(group.ErrorCode)
cl.maybeDeleteStaleCoordinator(group.Group, coordinatorTypeGroup, err)
onRespShardErr(&retErr, err)
}
return retErr
}
func (*consumerGroupDescribeSharder) merge(sresps []ResponseShard) (kmsg.Response, error) {
merged := kmsg.NewPtrConsumerGroupDescribeResponse()
return merged, firstErrMerger(sresps, func(kresp kmsg.Response) {
resp := kresp.(*kmsg.ConsumerGroupDescribeResponse)
merged.Version = resp.Version
merged.ThrottleMillis = resp.ThrottleMillis
merged.Groups = append(merged.Groups, resp.Groups...)
})
}
// handles sharding ShareGroupDescribeRequest
type shareGroupDescribeSharder struct{ *Client }
func (cl *shareGroupDescribeSharder) shard(ctx context.Context, kreq kmsg.Request, _ error) ([]issueShard, bool, error) {
req := kreq.(*kmsg.ShareGroupDescribeRequest)
coordinators := cl.loadCoordinators(ctx, coordinatorTypeGroup, req.GroupIDs...)
type unkerr struct {
err error
groupID string
}
var (
brokerReqs = make(map[int32]*kmsg.ShareGroupDescribeRequest)
kerrs = make(map[*kerr.Error][]string)
unkerrs []unkerr
)
newReq := func(groupIDs ...string) *kmsg.ShareGroupDescribeRequest {
newReq := kmsg.NewPtrShareGroupDescribeRequest()
newReq.IncludeAuthorizedOperations = req.IncludeAuthorizedOperations
newReq.GroupIDs = groupIDs
return newReq
}
for _, groupID := range req.GroupIDs {
berr := coordinators[groupID]
var ke *kerr.Error
switch {
case berr.err == nil:
brokerReq := brokerReqs[berr.b.meta.NodeID]
if brokerReq == nil {
brokerReq = newReq()
brokerReqs[berr.b.meta.NodeID] = brokerReq
}
brokerReq.GroupIDs = append(brokerReq.GroupIDs, groupID)
case errors.As(berr.err, &ke):
kerrs[ke] = append(kerrs[ke], groupID)
default:
unkerrs = append(unkerrs, unkerr{berr.err, groupID})
}
}
var issues []issueShard
for id, req := range brokerReqs {
issues = append(issues, issueShard{
req: req,
broker: id,
})
}
for _, unkerr := range unkerrs {
issues = append(issues, issueShard{
req: newReq(unkerr.groupID),
err: unkerr.err,
})
}
for kerr, groupIDs := range kerrs {
issues = append(issues, issueShard{
req: newReq(groupIDs...),
err: kerr,
})
}
return issues, true, nil // reshardable to load correct coordinators
}
func (cl *shareGroupDescribeSharder) onResp(_ kmsg.Request, kresp kmsg.Response) error {
resp := kresp.(*kmsg.ShareGroupDescribeResponse)
var retErr error
for i := range resp.Groups {
group := &resp.Groups[i]
err := kerr.ErrorForCode(group.ErrorCode)
cl.maybeDeleteStaleCoordinator(group.GroupID, coordinatorTypeGroup, err)
onRespShardErr(&retErr, err)
}
return retErr
}
func (*shareGroupDescribeSharder) merge(sresps []ResponseShard) (kmsg.Response, error) {
merged := kmsg.NewPtrShareGroupDescribeResponse()
return merged, firstErrMerger(sresps, func(kresp kmsg.Response) {
resp := kresp.(*kmsg.ShareGroupDescribeResponse)
merged.Version = resp.Version
merged.ThrottleMillis = resp.ThrottleMillis
merged.Groups = append(merged.Groups, resp.Groups...)
})
}