Files
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Mario
a739cb2805 Bump github.com/twmb/franz-go from 1.18.1 to 1.20.0 (#5766)
* Bump github.com/twmb/franz-go from 1.18.1 to 1.20.0

Bumps [github.com/twmb/franz-go](https://github.com/twmb/franz-go) from 1.18.1 to 1.20.0.
- [Changelog](https://github.com/twmb/franz-go/blob/master/CHANGELOG.md)
- [Commits](https://github.com/twmb/franz-go/compare/v1.18.1...v1.20.0)

---
updated-dependencies:
- dependency-name: github.com/twmb/franz-go
  dependency-version: 1.20.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>

* Update kfake

---------

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Mario <[email protected]>
2025-10-20 13:55:55 +02:00

382 lines
14 KiB
Go

package kgo
import (
"context"
"errors"
"fmt"
"io"
"net"
"os"
"github.com/twmb/franz-go/pkg/kerr"
)
// IsRetryableBrokerErr returns whether the client considers an error from a
// broker retrayble. This returns true specifically if the client thinks it can
// retry whatever it was just trying to do with a broker. It returns false in
// all other cases.
//
// This can used external to the library to help filter errors if use kgo
// hooks: errors may be sent to hooks before the client retries whatever it was
// just attempting.
func IsRetryableBrokerErr(err error) bool {
return isRetryableBrokerErr(err)
}
func isRetryableBrokerErr(err error) bool {
// The error could be nil if we are evaluating multiple errors at once,
// and only one is non-nil. The intent of this function is to evaluate
// whether an **error** is retryable, not a non-error. We return that
// nil is not retryable -- the calling code evaluating multiple errors
// at once would not call into this function if all errors were nil.
if err == nil {
return false
}
// https://github.com/golang/go/issues/45729
//
// Temporary is relatively useless. We will still check for the
// temporary interface, and in all cases, even with timeouts, we want
// to retry.
//
// More generally, we will retry for any error that unwraps into an
// os.SyscallError. Looking at Go's net package, the error we care
// about is net.OpError. Looking into that further, any error that
// reaches into the operating system return a syscall error, which is
// then put in net.OpError's Err field as an os.SyscallError. There are
// a few non-os.SyscallError errors, these are where Go itself detects
// a hard failure. We do not retry those.
//
// We blanket retry os.SyscallError because a lot of the times, what
// appears as a hard failure can actually be retried. For example, a
// failed dial can be retried, maybe the resolver temporarily had a
// problem.
//
// We favor testing os.SyscallError first, because net.OpError _always_
// implements Temporary, so if we test that first, it'll return false
// in many cases when we want to return true from os.SyscallError.
if se := (*os.SyscallError)(nil); errors.As(err, &se) {
// If a dial fails, potentially we could retry if the resolver
// had a temporary hiccup, but we will err on the side of this
// being a slightly less temporary error.
return !isDialNonTimeoutErr(err)
}
// EOF can be returned if a broker kills a connection unexpectedly, and
// we can retry that. Same for ErrClosed.
if errors.Is(err, net.ErrClosed) || errors.Is(err, io.EOF) {
// If the FIRST read is EOF, that is usually not a good sign,
// often it's from bad SASL. We err on the side of pessimism
// and do not retry.
if ee := (*ErrFirstReadEOF)(nil); errors.As(err, &ee) {
return false
}
return true
}
// We could have a retryable producer ID failure, which then bubbled up
// as errProducerIDLoadFail so as to be retried later.
if pe := (*errProducerIDLoadFail)(nil); errors.As(err, &pe) {
return true
}
// We could have chosen a broker, and then a concurrent metadata update
// could have removed it.
if errors.Is(err, errChosenBrokerDead) {
return true
}
// A broker kept giving us short sasl lifetimes, so we killed the
// connection ourselves. We can retry on a new connection.
if errors.Is(err, errSaslReauthLoop) {
return true
}
// We really should not get correlation mismatch, but if we do, we can
// retry.
if errors.Is(err, errCorrelationIDMismatch) {
return true
}
// We sometimes load the controller before issuing requests, and the
// cluster may not yet be ready and will return -1 for the controller.
// We can backoff and retry and hope the cluster has stabilized.
if ce := (*errUnknownController)(nil); errors.As(err, &ce) {
return true
}
// Same thought for a non-existing coordinator.
if ce := (*errUnknownCoordinator)(nil); errors.As(err, &ce) {
return true
}
var tempErr interface{ Temporary() bool }
if errors.As(err, &tempErr) {
return tempErr.Temporary()
}
return false
}
func isDialNonTimeoutErr(err error) bool {
var ne *net.OpError
return errors.As(err, &ne) && ne.Op == "dial" && !ne.Timeout()
}
func isAnyDialErr(err error) bool {
var ne *net.OpError
return errors.As(err, &ne) && ne.Op == "dial"
}
func isContextErr(err error) bool {
return errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)
}
func isSkippableBrokerErr(err error) bool {
// Some broker errors are not retryable for the given broker itself,
// but we *could* skip the broker and try again on the next broker. For
// example, if the user input an invalid address and a valid address
// for seeds, when we fail dialing the first seed, we cannot retry that
// broker, but we can skip to the next.
//
// We take anything that returns an OpError that *is not* a context
// error deep inside.
if errors.Is(err, errUnknownBroker) {
return true
}
var ne *net.OpError
if errors.As(err, &ne) && !isContextErr(err) {
return true
}
return false
}
var (
//////////////
// INTERNAL // -- when used multiple times or checked in different areas of the client
//////////////
// Returned when issuing a request to a broker that the client does not
// know about (maybe missing from metadata responses now).
errUnknownBroker = errors.New("unknown broker")
// A temporary error returned when a broker chosen for a request is
// stopped due to a concurrent metadata response.
errChosenBrokerDead = errors.New("the internal broker struct chosen to issue this request has died--either the broker id is migrating or no longer exists")
// If a broker repeatedly gives us tiny sasl lifetimes, we fail a
// request after a few tries to forcefully kill the connection and
// restart a new connection ourselves.
errSaslReauthLoop = errors.New("the broker is repeatedly giving us sasl lifetimes that are too short to write a request")
// A temporary error returned when Kafka replies with a different
// correlation ID than we were expecting for the request the client
// issued.
//
// If this error happens, the client closes the broker connection.
errCorrelationIDMismatch = errors.New("correlation ID mismatch")
// Returned when using a kmsg.Request with a key larger than kmsg.MaxKey.
errUnknownRequestKey = errors.New("request key is unknown")
// Returned if a connection has loaded broker ApiVersions and knows
// that the broker cannot handle the request to-be-issued request.
errBrokerTooOld = errors.New("broker is too old; the broker has already indicated it will not know how to handle the request")
// Returned when trying to call group functions when the client is not
// assigned a group.
errNotGroup = errors.New("invalid group function call when not assigned a group")
// Returned when trying to begin a transaction with a client that does
// not have a transactional ID.
errNotTransactional = errors.New("invalid attempt to begin a transaction with a non-transactional client")
// Returned when trying to produce a record outside of a transaction.
errNotInTransaction = errors.New("cannot produce record transactionally if not in a transaction")
errNoTopic = errors.New("cannot produce record with no topic and no default topic")
// Returned for all buffered produce records when a user purges topics.
errPurged = errors.New("topic purged while buffered")
errMissingMetadataPartition = errors.New("metadata update is missing a partition that we were previously using")
errNoCommittedOffset = errors.New("partition has no prior committed offset")
//////////////
// EXTERNAL //
//////////////
// ErrRecordTimeout is passed to produce promises when records are
// unable to be produced within the RecordDeliveryTimeout.
ErrRecordTimeout = errors.New("records have timed out before they were able to be produced")
// ErrRecordRetries is passed to produce promises when records are
// unable to be produced after RecordRetries attempts.
ErrRecordRetries = errors.New("record failed after being retried too many times")
// ErrMaxBuffered is returned when the maximum amount of records are
// buffered and either manual flushing is enabled or you are using
// TryProduce.
ErrMaxBuffered = errors.New("the maximum amount of records are buffered, cannot buffer more")
// ErrAborting is returned for all buffered records while
// AbortBufferedRecords is being called.
ErrAborting = errors.New("client is aborting buffered records")
// ErrClientClosed is returned in various places when the client's
// Close function has been called.
//
// For producing, records are failed with this error.
//
// For consuming, a fake partition is injected into a poll response
// that has this error.
//
// For any request, the request is failed with this error.
ErrClientClosed = errors.New("client closed")
)
// ErrFirstReadEOF is returned for responses that immediately error with
// io.EOF. This is the client's guess as to why a read from a broker is
// failing with io.EOF. Two cases are currently handled,
//
// - When the client is using TLS but brokers are not, brokers close
// connections immediately because the incoming request looks wrong.
// - When SASL is required but missing, brokers close connections immediately.
//
// There may be other reasons that an immediate io.EOF is encountered (perhaps
// the connection truly was severed before a response was received), but this
// error can help you quickly check common problems.
type ErrFirstReadEOF struct {
kind uint8
err error
}
type errProducerIDLoadFail struct {
err error
}
func (e *errProducerIDLoadFail) Error() string {
if e.err == nil {
return "unable to initialize a producer ID due to request failures"
}
return fmt.Sprintf("unable to initialize a producer ID due to request failures: %v", e.err)
}
func (e *errProducerIDLoadFail) Unwrap() error { return e.err }
const (
firstReadDial uint8 = iota
firstReadTLS
firstReadSASL
)
func (e *ErrFirstReadEOF) Error() string {
switch e.kind {
case firstReadDial:
return "broker closed the connection immediately after a dial, which often happens if the client is using TLS when the broker is not expecting it: is TLS misconfigured on the client or the broker?"
case firstReadTLS:
return "broker closed the connection immediately during api versions negotiation, which often happens when the broker requires TLS but the client is using plaintext: is TLS missing?"
default: // firstReadSASL
return "broker closed the connection immediately after a request was issued, which often happens when SASL is required but not provided: is SASL missing?"
}
}
// Unwrap returns io.EOF (or, if a custom dialer returned a wrapped io.EOF,
// this returns the custom dialer's wrapped error).
func (e *ErrFirstReadEOF) Unwrap() error { return e.err }
// ErrDataLoss is returned for Kafka >=2.1 when data loss is detected and the
// client is able to reset to the last valid offset.
type ErrDataLoss struct {
// Topic is the topic data loss was detected on.
Topic string
// Partition is the partition data loss was detected on.
Partition int32
// ConsumedTo is what the client had consumed to for this partition before
// data loss was detected.
ConsumedTo int64
// ConsumedToEpoch is the epoch for the offset the client was currently
// consuming.
ConsumedToEpoch int32
// ResetTo is what the client reset the partition to; everything from
// ResetTo to ConsumedTo was lost.
ResetTo int64
// ResetToEpoch is the epoch the client was reset to.
ResetToEpoch int32
}
func (e *ErrDataLoss) Error() string {
return fmt.Sprintf("topic %s partition %d lost records;"+
" the client consumed to offset %d epoch %d but was reset to offset %d epoch %d",
e.Topic, e.Partition, e.ConsumedTo, e.ConsumedToEpoch, e.ResetTo, e.ResetToEpoch)
}
type errUnknownController struct {
id int32
}
func (e *errUnknownController) Error() string {
if e.id == -1 {
return "broker replied that the controller broker is not available"
}
return fmt.Sprintf("broker replied that the controller broker is %d,"+
" but did not reply with that broker in the broker list", e.id)
}
type errUnknownCoordinator struct {
coordinator int32
key coordinatorKey
}
func (e *errUnknownCoordinator) Error() string {
switch e.key.typ {
case coordinatorTypeGroup:
return fmt.Sprintf("broker replied that group %s has broker coordinator %d,"+
" but did not reply with that broker in the broker list",
e.key.name, e.coordinator)
case coordinatorTypeTxn:
return fmt.Sprintf("broker replied that txn id %s has broker coordinator %d,"+
" but did not reply with that broker in the broker list",
e.key.name, e.coordinator)
default:
return fmt.Sprintf("broker replied to an unknown coordinator key %s (type %d) that it has a broker coordinator %d,"+
" but did not reply with that broker in the broker list", e.key.name, e.key.typ, e.coordinator)
}
}
// ErrGroupSession is injected into a poll if an error occurred such that your
// consumer group member was kicked from the group or was never able to join
// the group.
type ErrGroupSession struct {
Err error
}
func (e *ErrGroupSession) Error() string {
return fmt.Sprintf("unable to join group session: %v", e.Err)
}
func (e *ErrGroupSession) Unwrap() error { return e.Err }
type errDecompress struct {
err error
}
func (e *errDecompress) Error() string {
return fmt.Sprintf("unable to decompress batch: %v", e.err)
}
func (e *errDecompress) Unwrap() error { return e.err }
func isDecompressErr(err error) bool {
var ed *errDecompress
return errors.As(err, &ed)
}
func errCodeMessage(code int16, errMessage *string) error {
if err := kerr.ErrorForCode(code); err != nil {
if errMessage != nil {
return fmt.Errorf("%w: %s", err, *errMessage)
}
return err
}
return nil
}
type errApiVersionsReset struct {
err error
}
func (e *errApiVersionsReset) Error() string { return e.err.Error() }
func (e *errApiVersionsReset) Unwrap() error { return e.err }