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

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>
2026-01-07 23:39:49 +00:00

1267 lines
37 KiB
Go

package kgo
import (
"context"
"errors"
"fmt"
"math"
"sync"
"sync/atomic"
"time"
"github.com/twmb/franz-go/pkg/kerr"
"github.com/twmb/franz-go/pkg/kmsg"
)
type producer struct {
// mu and c are used for flush and drain notifications; mu is used for
// a few other tight locks.
mu sync.Mutex
c *sync.Cond
bufferedRecords int64
bufferedBytes int64
cl *Client
topicsMu sync.Mutex // locked to prevent concurrent updates; reads are always atomic
topics *topicsPartitions
// Hooks exist behind a pointer because likely they are not used.
// We only take up one byte vs. 6.
hooks *struct {
buffered []HookProduceRecordBuffered
partitioned []HookProduceRecordPartitioned
unbuffered []HookProduceRecordUnbuffered
}
hasHookBatchWritten bool
// unknownTopics buffers all records for topics that are not loaded.
// The map is to a pointer to a slice for reasons documented in
// waitUnknownTopic.
unknownTopicsMu sync.Mutex
unknownTopics map[string]*unknownTopicProduces
id atomic.Value
producingTxn atomic.Bool
// We must have a producer field for flushing; we cannot just have a
// field on recBufs that is toggled on flush. If we did, then a new
// recBuf could be created and records sent to while we are flushing.
flushing atomic.Int32 // >0 if flushing, can Flush many times concurrently
blocked atomic.Int32 // >0 if over max recs or bytes
blockedBytes int64
aborting atomic.Int32 // >0 if aborting, can abort many times concurrently
idMu sync.Mutex
idVersion int16
batchPromises ring[batchPromise] // we never call die() on it
txnMu sync.Mutex
inTxn bool
tx890p2 bool
}
// BufferedProduceRecords returns the number of records currently buffered for
// producing within the client.
//
// This can be used as a gauge to determine how far behind the client is for
// flushing records produced by your client (which can help determine network /
// cluster health).
func (cl *Client) BufferedProduceRecords() int64 {
cl.producer.mu.Lock()
defer cl.producer.mu.Unlock()
return cl.producer.bufferedRecords + int64(cl.producer.blocked.Load())
}
// BufferedProduceBytes returns the number of bytes currently buffered for
// producing within the client. This is the sum of all keys, values, and header
// keys/values. See the related [BufferedProduceRecords] for more information.
func (cl *Client) BufferedProduceBytes() int64 {
cl.producer.mu.Lock()
defer cl.producer.mu.Unlock()
return cl.producer.bufferedBytes + cl.producer.blockedBytes
}
// EnsureProduceConnectionIsOpen attempts to open a produce connection to all
// specified brokers, or all brokers if `brokers` is empty or contains -1.
//
// This can be used in an attempt to reduce the latency when producing if your
// application produces infrequently: you can force open a produce connection a
// bit earlier than you intend to produce, rather than at the moment you
// produce. In rare circumstances, it is possible that a connection that was
// ensured to be open may close before you produce.
//
// This returns an errors.Join'd error that merges a message for all brokers
// that failed to be opened as well as why.
func (cl *Client) EnsureProduceConnectionIsOpen(ctx context.Context, brokers ...int32) error {
var (
keep = brokers[:0]
all bool
wg sync.WaitGroup
mu sync.Mutex
errs []error
)
for _, b := range brokers {
switch {
case b < -1:
case b == -1:
all = true
case b > -1:
keep = append(keep, b)
}
}
var toOpen []*broker
if all || len(brokers) == 0 {
cl.brokersMu.RLock()
toOpen = cl.brokers
if len(toOpen) == 0 {
cl.brokersMu.RUnlock()
if err := cl.fetchBrokerMetadata(ctx); err != nil {
return err
}
cl.brokersMu.RLock()
toOpen = cl.brokers
}
cl.brokersMu.RUnlock()
} else {
for _, b := range brokers {
wg.Add(1)
go func() {
defer wg.Done()
br, err := cl.brokerOrErr(ctx, b, errUnknownBroker)
mu.Lock()
defer mu.Unlock()
if err != nil {
errs = append(errs, fmt.Errorf("%d: %w", b, err))
return
}
toOpen = append(toOpen, br)
}()
}
wg.Wait()
}
for _, br := range toOpen {
wg.Add(1)
br.do(ctx, &forceOpenReq{new(kmsg.ProduceRequest)}, func(_ kmsg.Response, err error) {
defer wg.Done()
if err != nil {
mu.Lock()
errs = append(errs, fmt.Errorf("%d: %w", br.meta.NodeID, err))
mu.Unlock()
}
})
}
wg.Wait()
return errors.Join(errs...)
}
type unknownTopicProduces struct {
buffered []promisedRec
wait chan error // retryable errors
fatal chan error // must-signal quit errors; capacity 1
}
func (p *producer) init(cl *Client) {
p.cl = cl
p.topics = newTopicsPartitions()
p.unknownTopics = make(map[string]*unknownTopicProduces)
p.idVersion = -1
p.id.Store(&producerID{
id: -1,
epoch: -1,
err: errReloadProducerID,
})
p.c = sync.NewCond(&p.mu)
inithooks := func() {
if p.hooks == nil {
p.hooks = &struct {
buffered []HookProduceRecordBuffered
partitioned []HookProduceRecordPartitioned
unbuffered []HookProduceRecordUnbuffered
}{}
}
}
cl.cfg.hooks.each(func(h Hook) {
if h, ok := h.(HookProduceRecordBuffered); ok {
inithooks()
p.hooks.buffered = append(p.hooks.buffered, h)
}
if h, ok := h.(HookProduceRecordPartitioned); ok {
inithooks()
p.hooks.partitioned = append(p.hooks.partitioned, h)
}
if h, ok := h.(HookProduceRecordUnbuffered); ok {
inithooks()
p.hooks.unbuffered = append(p.hooks.unbuffered, h)
}
if _, ok := h.(HookProduceBatchWritten); ok {
p.hasHookBatchWritten = true
}
})
}
func (p *producer) purgeTopics(topics []string) {
p.topicsMu.Lock()
defer p.topicsMu.Unlock()
p.unknownTopicsMu.Lock()
for _, topic := range topics {
if unknown, exists := p.unknownTopics[topic]; exists {
delete(p.unknownTopics, topic)
close(unknown.wait)
p.promiseBatch(batchPromise{
recs: unknown.buffered,
err: errPurged,
})
}
}
p.unknownTopicsMu.Unlock()
toStore := p.topics.clone()
defer p.topics.storeData(toStore)
for _, topic := range topics {
d := toStore.loadTopic(topic)
if d == nil {
continue
}
delete(toStore, topic)
for _, p := range d.partitions {
r := p.records
// First we set purged, so that anything in the process
// of being buffered will immediately fail when it goes
// to buffer.
r.mu.Lock()
r.purged = true
r.mu.Unlock()
// Now we remove from the sink. When we do, the recBuf
// is effectively abandonded. Any active produces may
// finish before we fail the records; if they finish
// after they will no longer belong in the batch, but
// they may have been produced. This is the duplicate
// risk a user runs when purging.
//
// We do not need to lock for `r.sink` access because
// this is ran in a blocking metadata fn, meaning the
// sink cannot change. We do not WANT to lock because
// r.mu => r.sink.recBufsMu would cause lock inversion.
r.sink.removeRecBuf(r)
// Once abandonded, we now need to fail anything that
// was buffered.
go func() {
r.mu.Lock()
defer r.mu.Unlock()
r.failAllRecords(errPurged)
}()
}
}
}
func (p *producer) isAborting() bool { return p.aborting.Load() > 0 }
func noPromise(*Record, error) {}
// ProduceResult is the result of producing a record in a synchronous manner.
type ProduceResult struct {
// Record is the produced record. It is always non-nil.
//
// If this record was produced successfully, its attrs / offset / id /
// epoch / etc. fields are filled in on return if possible (i.e. when
// producing with acks required).
Record *Record
// Err is a potential produce error. If this is non-nil, the record was
// not produced successfully.
Err error
}
// ProduceResults is a collection of produce results.
type ProduceResults []ProduceResult
// FirstErr returns the first erroring result, if any.
func (rs ProduceResults) FirstErr() error {
for _, r := range rs {
if r.Err != nil {
return r.Err
}
}
return nil
}
// First the first record and error in the produce results.
//
// This function is useful if you only passed one record to ProduceSync.
func (rs ProduceResults) First() (*Record, error) {
return rs[0].Record, rs[0].Err
}
// ProduceSync is a synchronous produce. See the [Produce] documentation for an
// in depth description of how producing works.
//
// This function simply produces all records in one range loop and waits for
// them all to be produced before returning.
func (cl *Client) ProduceSync(ctx context.Context, rs ...*Record) ProduceResults {
var (
wg sync.WaitGroup
results = make(ProduceResults, 0, len(rs))
promise = func(r *Record, err error) {
results = append(results, ProduceResult{r, err})
wg.Done()
}
)
wg.Add(len(rs))
for _, r := range rs {
cl.Produce(ctx, r, promise)
}
wg.Wait()
return results
}
// FirstErrPromise is a helper type to capture only the first failing error
// when producing a batch of records with this type's Promise function.
//
// This is useful for when you only care about any record failing, and can use
// that as a signal (i.e., to abort a batch). The AbortingFirstErrPromise
// function can be used to abort all records as soon as the first error is
// encountered. If you do not need to abort, you can use this type with no
// constructor.
//
// This is similar to using ProduceResult's FirstErr function.
type FirstErrPromise struct {
wg sync.WaitGroup
once atomic.Bool
err error
cl *Client
}
// AbortingFirstErrPromise returns a FirstErrPromise that will call the
// client's AbortBufferedRecords function if an error is encountered.
//
// This can be used to quickly exit when any error is encountered, rather than
// waiting while flushing only to discover things errored.
func AbortingFirstErrPromise(cl *Client) *FirstErrPromise {
return &FirstErrPromise{
cl: cl,
}
}
// Promise is a promise for producing that will store the first error
// encountered.
func (f *FirstErrPromise) promise(_ *Record, err error) {
defer f.wg.Done()
if err != nil && !f.once.Swap(true) {
f.err = err
if f.cl != nil {
f.wg.Add(1)
go func() {
defer f.wg.Done()
f.cl.AbortBufferedRecords(context.Background())
}()
}
}
}
// Promise returns a promise for producing that will store the first error
// encountered.
//
// The returned promise must eventually be called, because a FirstErrPromise
// does not return from 'Err' until all promises are completed.
func (f *FirstErrPromise) Promise() func(*Record, error) {
f.wg.Add(1)
return f.promise
}
// Err waits for all promises to complete and then returns any stored error.
func (f *FirstErrPromise) Err() error {
f.wg.Wait()
return f.err
}
// TryProduce is similar to Produce, but rather than blocking if the client
// currently has MaxBufferedRecords or MaxBufferedBytes buffered, this fails
// immediately with ErrMaxBuffered. See the Produce documentation for more
// details.
func (cl *Client) TryProduce(
ctx context.Context,
r *Record,
promise func(*Record, error),
) {
cl.produce(ctx, r, promise, false)
}
// Produce sends a Kafka record to the topic in the record's Topic field,
// calling an optional `promise` with the record and a potential error when
// Kafka replies. For a synchronous produce, see ProduceSync. Records are
// produced in order per partition if the record is produced successfully.
// Successfully produced records will have their attributes, offset, and
// partition set before the promise is called. All promises are called serially
// (and should be relatively fast). If a record's timestamp is unset, this
// sets the timestamp to time.Now.
//
// If the topic field is empty, the client will use the DefaultProduceTopic; if
// that is also empty, the record is failed immediately. If the record is too
// large to fit in a batch on its own in a produce request, the record will be
// failed with immediately kerr.MessageTooLarge.
//
// If the client is configured to automatically flush the client currently has
// the configured maximum amount of records buffered, Produce will block. The
// context can be used to cancel waiting while records flush to make space. In
// contrast, if flushing is configured, the record will be failed immediately
// with ErrMaxBuffered (this same behavior can be had with TryProduce).
//
// Once a record is buffered into a batch, it can be canceled in three ways:
// canceling the context, the record timing out, or hitting the maximum
// retries. If any of these conditions are hit and it is currently safe to fail
// records, all buffered records for the relevant partition are failed. Only
// the first record's context in a batch is considered when determining whether
// the batch should be canceled. A record is not safe to fail if the client
// is idempotently producing and a request has been sent; in this case, the
// client cannot know if the broker actually processed the request (if so, then
// removing the records from the client will create errors the next time you
// produce).
//
// If the client is transactional and a transaction has not been begun, the
// promise is immediately called with an error corresponding to not being in a
// transaction.
func (cl *Client) Produce(
ctx context.Context,
r *Record,
promise func(*Record, error),
) {
cl.produce(ctx, r, promise, true)
}
func (cl *Client) produce(
ctx context.Context,
r *Record,
promise func(*Record, error),
block bool,
) {
if ctx == nil {
ctx = context.Background()
}
if r.Context == nil {
r.Context = ctx
}
if promise == nil {
promise = noPromise
}
if r.Topic == "" || cl.cfg.defaultProduceTopicAlways {
r.Topic = cl.cfg.defaultProduceTopic
}
p := &cl.producer
if p.hooks != nil && len(p.hooks.buffered) > 0 {
for _, h := range p.hooks.buffered {
h.OnProduceRecordBuffered(r)
}
}
// We can now fail the rec after the buffered hook.
if r.Topic == "" {
p.promiseRecordBeforeBuf(promisedRec{ctx, promise, r}, errNoTopic)
return
}
if cl.cfg.txnID != nil && !p.producingTxn.Load() {
p.promiseRecordBeforeBuf(promisedRec{ctx, promise, r}, errNotInTransaction)
return
}
userSize := r.userSize()
if cl.cfg.maxBufferedBytes > 0 && userSize > cl.cfg.maxBufferedBytes {
p.promiseRecordBeforeBuf(promisedRec{ctx, promise, r}, kerr.MessageTooLarge)
return
}
// We have to grab the produce lock to check if this record will exceed
// configured limits. We try to keep the logic tight since this is
// effectively a global lock around producing.
var (
nextBufRecs, nextBufBytes int64
overMaxRecs, overMaxBytes bool
calcNums = func() {
nextBufRecs = p.bufferedRecords + 1
nextBufBytes = p.bufferedBytes + userSize
overMaxRecs = nextBufRecs > cl.cfg.maxBufferedRecords
overMaxBytes = cl.cfg.maxBufferedBytes > 0 && nextBufBytes > cl.cfg.maxBufferedBytes
}
)
p.mu.Lock()
calcNums()
if overMaxRecs || overMaxBytes {
if !block || cl.cfg.manualFlushing {
p.mu.Unlock()
p.promiseRecordBeforeBuf(promisedRec{ctx, promise, r}, ErrMaxBuffered)
return
}
// Before we potentially unlinger, add that we are blocked to
// ensure we do NOT start a linger anymore. We THEN wakeup
// anything that is actively lingering. Note that blocked is
// also used when finishing promises to see if we need to be
// notified.
p.blocked.Add(1)
p.blockedBytes += userSize
p.mu.Unlock()
cl.cfg.logger.Log(LogLevelDebug, "blocking Produce because we are either over max buffered records or max buffered bytes",
"over_max_records", overMaxRecs,
"over_max_bytes", overMaxBytes,
)
cl.unlingerDueToMaxRecsBuffered()
// We keep the lock when we exit. If we are flushing, we want
// this blocked record to be produced before we return from
// flushing. This blocked record will be accounted for in the
// bufferedRecords addition below, after being removed from
// blocked in the goroutine.
wait := make(chan struct{})
var quit bool
go func() {
defer close(wait)
p.mu.Lock()
calcNums()
for !quit && (overMaxRecs || overMaxBytes) {
p.c.Wait()
calcNums()
}
p.blocked.Add(-1)
p.blockedBytes -= userSize
}()
drainBuffered := func(err error) {
// The expected case here is that a context was
// canceled while we waiting for space, so we are
// exiting and need to kill the goro above.
//
// However, it is possible that the goro above has
// already exited AND the context was canceled, and
// `select` chose the context-canceled case.
//
// So, to avoid a deadlock, we need to wakeup the
// goro above in another goroutine.
go func() {
p.mu.Lock()
quit = true
p.mu.Unlock()
p.c.Broadcast()
}()
<-wait // we wait for the goroutine to exit, then unlock again (since the goroutine leaves the mutex locked)
p.mu.Unlock()
p.promiseRecordBeforeBuf(promisedRec{ctx, promise, r}, err)
}
select {
case <-wait:
cl.cfg.logger.Log(LogLevelDebug, "Produce block awoken, we now have space to produce, continuing to partition and produce")
case <-cl.ctx.Done():
drainBuffered(ErrClientClosed)
cl.cfg.logger.Log(LogLevelDebug, "client ctx canceled while blocked in Produce, returning")
return
case <-ctx.Done():
drainBuffered(ctx.Err())
cl.cfg.logger.Log(LogLevelDebug, "produce ctx canceled while blocked in Produce, returning")
return
}
}
p.bufferedRecords = nextBufRecs
p.bufferedBytes = nextBufBytes
p.mu.Unlock()
cl.loadPartsAndPartition(promisedRec{ctx, promise, r})
}
type batchPromise struct {
baseOffset int64
pid int64
epoch int16
attrs RecordAttrs
beforeBuf bool
partition int32
recs []promisedRec
err error
}
func (p *producer) promiseBatch(b batchPromise) {
if first, _ := p.batchPromises.push(b); first {
go p.finishPromises(b)
}
}
func (p *producer) promiseRecord(pr promisedRec, err error) {
p.promiseBatch(batchPromise{recs: []promisedRec{pr}, err: err})
}
func (p *producer) promiseRecordBeforeBuf(pr promisedRec, err error) {
p.promiseBatch(batchPromise{recs: []promisedRec{pr}, beforeBuf: true, err: err})
}
func (p *producer) finishPromises(b batchPromise) {
cl := p.cl
var more bool
var broadcast bool
defer func() {
if broadcast {
p.c.Broadcast()
}
}()
start:
for i, pr := range b.recs {
pr.LeaderEpoch = -1
if b.baseOffset == -1 {
// if the base offset is invalid/unknown (-1), all record offsets should
// be treated as unknown
pr.Offset = -1
} else {
pr.Offset = b.baseOffset + int64(i)
}
pr.Partition = b.partition
pr.ProducerID = b.pid
pr.ProducerEpoch = b.epoch
pr.Attrs = b.attrs
recBroadcast := cl.finishRecordPromise(pr, b.err, b.beforeBuf)
broadcast = broadcast || recBroadcast
b.recs[i] = promisedRec{}
}
if cap(b.recs) > 4 {
cl.prsPool.put(b.recs)
}
b, more, _ = p.batchPromises.dropPeek()
if more {
goto start
}
}
func (cl *Client) finishRecordPromise(pr promisedRec, err error, beforeBuffering bool) (broadcast bool) {
p := &cl.producer
if p.hooks != nil && len(p.hooks.unbuffered) > 0 {
for _, h := range p.hooks.unbuffered {
h.OnProduceRecordUnbuffered(pr.Record, err)
}
}
// Capture user size before potential modification by the promise.
//
// We call the promise before finishing the flush notification,
// allowing users of Flush to know all buf recs are done by the
// time we notify flush below.
userSize := pr.userSize()
pr.promise(pr.Record, err)
// If this record was never buffered, it's size was never accounted
// for on any p field: return early.
if beforeBuffering {
return broadcast
}
// Keep the lock as tight as possible: the broadcast can come after.
p.mu.Lock()
p.bufferedBytes -= userSize
p.bufferedRecords--
broadcast = p.blocked.Load() > 0 || p.bufferedRecords == 0 && p.flushing.Load() > 0
p.mu.Unlock()
return broadcast
}
// loadPartsAndPartition loads the partitions for a topic and produce to them.
// If the topic does not currently exist, the record is buffered in
// unknownTopics for a metadata update to deal with.
func (cl *Client) loadPartsAndPartition(pr promisedRec) {
parts, partsData := cl.partitionsForTopicProduce(pr)
if parts == nil { // saved in unknownTopics
return
}
cl.doPartition(parts, partsData, pr)
}
func (cl *Client) doPartition(parts *topicPartitions, partsData *topicPartitionsData, pr promisedRec) {
if partsData.loadErr != nil && !kerr.IsRetriable(partsData.loadErr) {
cl.producer.promiseRecord(pr, partsData.loadErr)
return
}
parts.partsMu.Lock()
defer parts.partsMu.Unlock()
if parts.partitioner == nil {
parts.partitioner = cl.cfg.partitioner.ForTopic(pr.Topic)
}
mapping := partsData.writablePartitions
if parts.partitioner.RequiresConsistency(pr.Record) {
mapping = partsData.partitions
}
if len(mapping) == 0 {
cl.producer.promiseRecord(pr, errors.New("unable to partition record due to no usable partitions"))
return
}
var pick int
tlp, _ := parts.partitioner.(TopicBackupPartitioner)
if tlp != nil {
if parts.lb == nil {
parts.lb = new(leastBackupInput)
}
parts.lb.mapping = mapping
pick = tlp.PartitionByBackup(pr.Record, len(mapping), parts.lb)
} else {
pick = parts.partitioner.Partition(pr.Record, len(mapping))
}
if pick < 0 || pick >= len(mapping) {
cl.producer.promiseRecord(pr, fmt.Errorf("invalid record partitioning choice of %d from %d available", pick, len(mapping)))
return
}
partition := mapping[pick]
onNewBatch, _ := parts.partitioner.(TopicPartitionerOnNewBatch)
abortOnNewBatch := onNewBatch != nil
processed := partition.records.bufferRecord(pr, abortOnNewBatch) // KIP-480
if !processed {
onNewBatch.OnNewBatch()
if tlp != nil {
parts.lb.mapping = mapping
pick = tlp.PartitionByBackup(pr.Record, len(mapping), parts.lb)
} else {
pick = parts.partitioner.Partition(pr.Record, len(mapping))
}
if pick < 0 || pick >= len(mapping) {
cl.producer.promiseRecord(pr, fmt.Errorf("invalid record partitioning choice of %d from %d available", pick, len(mapping)))
return
}
partition = mapping[pick]
partition.records.bufferRecord(pr, false) // KIP-480
}
}
// ProducerID returns, loading if necessary, the current producer ID and epoch.
// This returns an error if the producer ID could not be loaded, if the
// producer ID has fatally errored, or if the context is canceled.
func (cl *Client) ProducerID(ctx context.Context) (int64, int16, error) {
var (
id int64
epoch int16
err error
done = make(chan struct{})
)
go func() {
defer close(done)
id, epoch, err = cl.producerID(ctx2fn(ctx))
}()
select {
case <-ctx.Done():
return 0, 0, ctx.Err()
case <-done:
return id, epoch, err
}
}
type producerID struct {
id int64
epoch int16
err error
}
var errReloadProducerID = errors.New("producer id needs reloading")
// initProducerID initializes the client's producer ID for idempotent
// producing only (no transactions, which are more special). After the first
// load, this clears all buffered unknown topics.
func (cl *Client) producerID(ctxFn func() context.Context) (int64, int16, error) {
p := &cl.producer
id := p.id.Load().(*producerID)
if errors.Is(id.err, errReloadProducerID) {
p.idMu.Lock()
defer p.idMu.Unlock()
if id = p.id.Load().(*producerID); errors.Is(id.err, errReloadProducerID) {
if cl.cfg.disableIdempotency {
cl.cfg.logger.Log(LogLevelInfo, "skipping producer id initialization because the client was configured to disable idempotent writes")
id = &producerID{
id: -1,
epoch: -1,
err: nil,
}
p.id.Store(id)
} else if cl.cfg.txnID == nil && id.id >= 0 && id.epoch < math.MaxInt16-1 {
// For the idempotent producer, as specified in KIP-360,
// if we had an ID, we can bump the epoch locally.
// If we are at the max epoch, we will ask for a new ID.
cl.resetAllProducerSequences()
id = &producerID{
id: id.id,
epoch: id.epoch + 1,
err: nil,
}
p.id.Store(id)
} else {
newID, keep := cl.doInitProducerID(ctxFn, id.id, id.epoch)
if keep {
id = newID
// Whenever we have a new producer ID, we need
// our sequence numbers to be 0. On the first
// record produced, this will be true, but if
// we were signaled to reset the producer ID,
// then we definitely still need to reset here.
cl.resetAllProducerSequences()
p.id.Store(id)
} else {
// If we are not keeping the producer ID,
// we will return our old ID but with a
// static error that we can check or bubble
// up where needed.
id = &producerID{
id: id.id,
epoch: id.epoch,
err: &errProducerIDLoadFail{newID.err},
}
}
}
}
}
return id.id, id.epoch, id.err
}
// As seen in KAFKA-12152, if we bump an epoch, we have to reset sequence nums
// for every partition. Otherwise, we will use a new id/epoch for a partition
// and trigger OOOSN errors.
//
// Pre 2.5, this function is only be called if it is acceptable to continue
// on data loss (idempotent producer with no StopOnDataLoss option).
//
// 2.5+, it is safe to call this if the producer ID can be reset (KIP-360),
// in EndTransaction.
func (cl *Client) resetAllProducerSequences() {
for _, tp := range cl.producer.topics.load() {
for _, p := range tp.load().partitions {
p.records.mu.Lock()
p.records.needSeqReset = true
p.records.mu.Unlock()
}
}
}
func (cl *Client) failProducerID(id int64, epoch int16, err error) {
p := &cl.producer
// We do not lock the idMu when failing a producer ID, for two reasons.
//
// 1) With how we store below, we do not need to. We only fail if the
// ID we are failing has not changed and if the ID we are failing has
// not failed already. Failing outside the lock is the same as failing
// within the lock.
//
// 2) Locking would cause a deadlock, because producerID locks
// idMu=>recBuf.Mu, whereas we failing while locked within a recBuf in
// sink.go.
new := &producerID{
id: id,
epoch: epoch,
err: err,
}
for {
current := p.id.Load().(*producerID)
if current.id != id || current.epoch != epoch {
cl.cfg.logger.Log(LogLevelInfo, "ignoring a fail producer id request due to current id being different",
"current_id", current.id,
"current_epoch", current.epoch,
"current_err", current.err,
"fail_id", id,
"fail_epoch", epoch,
"fail_err", err,
)
return
}
if current.err != nil {
cl.cfg.logger.Log(LogLevelInfo, "ignoring a fail producer id because our producer id has already been failed",
"current_id", current.id,
"current_epoch", current.epoch,
"current_err", current.err,
"fail_err", err,
)
return
}
if p.id.CompareAndSwap(current, new) {
return
}
}
}
// doInitProducerID inits the idempotent ID and potentially the transactional
// producer epoch, returning whether to keep the result.
func (cl *Client) doInitProducerID(ctxFn func() context.Context, lastID int64, lastEpoch int16) (*producerID, bool) {
cl.cfg.logger.Log(LogLevelInfo, "initializing producer id")
req := kmsg.NewPtrInitProducerIDRequest()
req.TransactionalID = cl.cfg.txnID
req.ProducerID = lastID
req.ProducerEpoch = lastEpoch
if cl.cfg.txnID != nil {
req.TransactionTimeoutMillis = int32(cl.cfg.txnTimeout.Milliseconds())
}
ctx := ctxFn()
var resp *kmsg.InitProducerIDResponse
err := cl.doWithConcurrentTransactions(ctx, "InitProducerID", func() error {
var err error
resp, err = req.RequestWith(ctx, cl)
if err != nil {
return err
}
return nil // resp.ErrorCode handled below
})
if err != nil {
if errors.Is(err, errUnknownRequestKey) || errors.Is(err, errBrokerTooOld) {
cl.cfg.logger.Log(LogLevelInfo, "unable to initialize a producer id because the broker is too old or the client is pinned to an old version, continuing without a producer id")
return &producerID{-1, -1, nil}, true
}
if errors.Is(err, errChosenBrokerDead) {
select {
case <-cl.ctx.Done():
cl.cfg.logger.Log(LogLevelInfo, "producer id initialization failure due to dying client", "err", err)
return &producerID{lastID, lastEpoch, ErrClientClosed}, true
default:
}
}
cl.cfg.logger.Log(LogLevelInfo, "producer id initialization failure, discarding initialization attempt", "err", err)
return &producerID{lastID, lastEpoch, err}, false
}
if err = kerr.ErrorForCode(resp.ErrorCode); err != nil {
// We could receive concurrent transactions; this is ignorable
// and we just want to re-init.
if kerr.IsRetriable(err) || errors.Is(err, kerr.ConcurrentTransactions) {
cl.cfg.logger.Log(LogLevelInfo, "producer id initialization resulted in retryable error, discarding initialization attempt", "err", err)
return &producerID{lastID, lastEpoch, err}, false
}
cl.cfg.logger.Log(LogLevelInfo, "producer id initialization errored", "err", err)
return &producerID{lastID, lastEpoch, err}, true
}
cl.cfg.logger.Log(LogLevelInfo, "producer id initialization success", "id", resp.ProducerID, "epoch", resp.ProducerEpoch)
// We track if this was v3. We do not need to gate this behind a mutex,
// because the only other use is EndTransaction's read, which is
// documented to only be called sequentially after producing.
if cl.producer.idVersion == -1 {
cl.producer.idVersion = req.Version
}
return &producerID{resp.ProducerID, resp.ProducerEpoch, nil}, true
}
// partitionsForTopicProduce returns the topic partitions for a record.
// If the topic is not loaded yet, this buffers the record and returns
// nil, nil.
func (cl *Client) partitionsForTopicProduce(pr promisedRec) (*topicPartitions, *topicPartitionsData) {
p := &cl.producer
topic := pr.Topic
topics := p.topics.load()
parts, exists := topics[topic]
if exists {
if v := parts.load(); len(v.partitions) > 0 {
return parts, v
}
}
if !exists { // topic did not exist: check again under mu and potentially create it
p.topicsMu.Lock()
defer p.topicsMu.Unlock()
if parts, exists = p.topics.load()[topic]; !exists { // update parts for below
// Before we store the new topic, we lock unknown
// topics to prevent a concurrent metadata update
// seeing our new topic before we are waiting from the
// addUnknownTopicRecord fn. Otherwise, we would wait
// and never be re-notified.
p.unknownTopicsMu.Lock()
defer p.unknownTopicsMu.Unlock()
p.topics.storeTopics([]string{topic})
cl.addUnknownTopicRecord(pr)
cl.triggerUpdateMetadataNow("forced load because we are producing to a topic for the first time")
return nil, nil
}
}
// Here, the topic existed, but maybe has not loaded partitions yet. We
// have to lock unknown topics first to ensure ordering just in case a
// load has not happened.
p.unknownTopicsMu.Lock()
defer p.unknownTopicsMu.Unlock()
if v := parts.load(); len(v.partitions) > 0 {
return parts, v
}
cl.addUnknownTopicRecord(pr)
cl.triggerUpdateMetadata(false, "reload trigger due to produce topic still not known")
return nil, nil // our record is buffered waiting for metadata update; nothing to return
}
// addUnknownTopicRecord adds a record to a topic whose partitions are
// currently unknown. This is always called with the unknownTopicsMu held.
func (cl *Client) addUnknownTopicRecord(pr promisedRec) {
unknown := cl.producer.unknownTopics[pr.Topic]
if unknown == nil {
unknown = &unknownTopicProduces{
buffered: make([]promisedRec, 0, 100),
wait: make(chan error, 5),
fatal: make(chan error, 1),
}
cl.producer.unknownTopics[pr.Topic] = unknown
}
unknown.buffered = append(unknown.buffered, pr)
if len(unknown.buffered) == 1 {
go cl.waitUnknownTopic(pr.ctx, pr.Context, pr.Topic, unknown)
}
}
// waitUnknownTopic waits for a notification
func (cl *Client) waitUnknownTopic(
pctx context.Context, // context passed to Produce
rctx context.Context, // context on the record itself
topic string,
unknown *unknownTopicProduces,
) {
cl.cfg.logger.Log(LogLevelInfo, "producing to a new topic for the first time, fetching metadata to learn its partitions", "topic", topic)
var (
tries int
unknownTries int64
err error
after <-chan time.Time
)
if timeout := cl.cfg.recordTimeout; timeout > 0 {
timer := time.NewTimer(cl.cfg.recordTimeout)
defer timer.Stop()
after = timer.C
}
// Ordering: aborting is set first, then unknown topics are manually
// canceled in a lock. New unknown topics after that lock will see
// aborting here and immediately cancel themselves.
if cl.producer.isAborting() {
err = ErrAborting
}
for err == nil {
select {
case <-pctx.Done():
err = pctx.Err()
case <-rctx.Done():
err = rctx.Err()
case <-cl.ctx.Done():
err = ErrClientClosed
case <-after:
err = ErrRecordTimeout
case err = <-unknown.fatal:
case retryableErr, ok := <-unknown.wait:
if !ok {
cl.cfg.logger.Log(LogLevelInfo, "done waiting for metadata for new topic", "topic", topic)
return // metadata was successful!
}
cl.cfg.logger.Log(LogLevelInfo, "new topic metadata wait failed, retrying wait", "topic", topic, "err", retryableErr)
tries++
if int64(tries) > cl.cfg.recordRetries {
err = fmt.Errorf("no partitions available after attempting to refresh metadata %d times, last err: %w", tries, retryableErr)
}
if cl.cfg.maxUnknownFailures >= 0 && errors.Is(retryableErr, kerr.UnknownTopicOrPartition) {
unknownTries++
if unknownTries > cl.cfg.maxUnknownFailures {
err = retryableErr
}
}
}
}
// If we errored above, we come down here to potentially clear the
// topic wait and fail all buffered records. However, under some
// extreme conditions, a quickly following metadata update could delete
// our unknown topic, and then a produce could recreate a new unknown
// topic. We only delete and finish promises if the pointer in the
// unknown topic map is still the same.
p := &cl.producer
p.unknownTopicsMu.Lock()
defer p.unknownTopicsMu.Unlock()
nowUnknown := p.unknownTopics[topic]
if nowUnknown != unknown {
return
}
cl.cfg.logger.Log(LogLevelInfo, "new topic metadata wait failed, done retrying, failing all records", "topic", topic, "err", err)
delete(p.unknownTopics, topic)
p.promiseBatch(batchPromise{
recs: unknown.buffered,
err: err,
})
}
func (cl *Client) unlingerDueToMaxRecsBuffered() {
if cl.cfg.linger <= 0 {
return
}
for _, parts := range cl.producer.topics.load() {
for _, part := range parts.load().partitions {
part.records.unlingerAndManuallyDrain()
}
}
cl.cfg.logger.Log(LogLevelDebug, "unlingered all partitions due to hitting max buffered")
}
// Flush hangs waiting for all buffered records to be flushed, stopping all
// lingers if necessary.
//
// If the context finishes (Done), this returns the context's error.
//
// This function is safe to call multiple times concurrently, and safe to call
// concurrent with Flush.
func (cl *Client) Flush(ctx context.Context) error {
p := &cl.producer
// Signal to finishRecord that we want to be notified once buffered hits 0.
// Also forbid any new producing to start a linger.
p.flushing.Add(1)
defer p.flushing.Add(-1)
cl.cfg.logger.Log(LogLevelInfo, "flushing")
defer cl.cfg.logger.Log(LogLevelDebug, "flushed")
// At this point, if lingering is configured, nothing will _start_ a
// linger because the producer's flushing atomic int32 is nonzero. We
// must wake anything that could be lingering up, after which all sinks
// will loop draining.
if cl.cfg.linger > 0 || cl.cfg.manualFlushing {
for _, parts := range p.topics.load() {
for _, part := range parts.load().partitions {
part.records.unlingerAndManuallyDrain()
}
}
}
quit := false
done := make(chan struct{})
go func() {
p.mu.Lock()
defer p.mu.Unlock()
defer close(done)
for !quit && p.bufferedRecords+int64(p.blocked.Load()) > 0 {
p.c.Wait()
}
}()
select {
case <-done:
return nil
case <-ctx.Done():
p.mu.Lock()
quit = true
p.mu.Unlock()
p.c.Broadcast()
return ctx.Err()
}
}
// Bumps the tries for all buffered records in the client.
//
// This is called whenever there is a problematic error that would affect the
// state of all buffered records as a whole:
//
// - if we cannot init a producer ID due to RequestWith errors, producing is useless
// - if we cannot add partitions to a txn due to RequestWith errors, producing is useless
//
// Note that these are specifically due to RequestWith errors, not due to
// receiving a response that has a retryable error code. That is, if our
// request keeps dying.
func (cl *Client) bumpRepeatedLoadErr(err error) {
p := &cl.producer
for _, partitions := range p.topics.load() {
for _, partition := range partitions.load().partitions {
partition.records.bumpRepeatedLoadErr(err)
}
}
p.unknownTopicsMu.Lock()
defer p.unknownTopicsMu.Unlock()
for _, unknown := range p.unknownTopics {
select {
case unknown.wait <- err:
default:
}
}
}
// Clears all buffered records in the client with the given error.
//
// - closing client
// - aborting transaction
// - fatal AddPartitionsToTxn
//
// Because the error fails everything, we also empty our unknown topics and
// delete any topics that were still unknown from the producer's topics.
func (cl *Client) failBufferedRecords(err error) {
p := &cl.producer
for _, partitions := range p.topics.load() {
for _, partition := range partitions.load().partitions {
recBuf := partition.records
recBuf.mu.Lock()
recBuf.failAllRecords(err)
recBuf.mu.Unlock()
}
}
p.topicsMu.Lock()
defer p.topicsMu.Unlock()
p.unknownTopicsMu.Lock()
defer p.unknownTopicsMu.Unlock()
toStore := p.topics.clone()
defer p.topics.storeData(toStore)
var toFail [][]promisedRec
for topic, unknown := range p.unknownTopics {
delete(toStore, topic)
delete(p.unknownTopics, topic)
close(unknown.wait)
toFail = append(toFail, unknown.buffered)
}
for _, fail := range toFail {
p.promiseBatch(batchPromise{
recs: fail,
err: err,
})
}
}