Files
renovate-sh-app[bot]GitHubrenovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com>
0e3c9831b9 fix(deps): update module github.com/twmb/franz-go to v1.20.3 (#5880)
| datasource | package                  | from    | to      |
| ---------- | ------------------------ | ------- | ------- |
| go         | github.com/twmb/franz-go | v1.20.1 | v1.20.3 |

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-11-10 08:47:09 +00:00

2488 lines
79 KiB
Go

package kgo
import (
"bytes"
"context"
"errors"
"fmt"
"hash/crc32"
"math"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/twmb/franz-go/pkg/kbin"
"github.com/twmb/franz-go/pkg/kerr"
"github.com/twmb/franz-go/pkg/kmsg"
)
type sink struct {
cl *Client // our owning client, for cfg, metadata triggering, context, etc.
nodeID int32 // the node ID of the broker this sink belongs to
// inflightSem controls the number of concurrent produce requests. We
// start with a limit of 1, which covers Kafka v0.11.0. On the first
// response, we check what version was set in the request. If it is at
// least 4, which 1.0 introduced, we upgrade the sem size.
inflightSem atomic.Value
produceVersion atomic.Int32 // negative is unset, positive is version
drainState workLoop
// seqRespsMu, guarded by seqRespsMu, contains responses that must
// be handled sequentially. These responses are handled asynchronously,
// but sequentially.
seqResps ring[*seqResp] // we never call die() on it
backoffMu sync.Mutex // guards the following
needBackoff bool
backoffSeq uint32 // prevents pile on failures
// consecutiveFailures is incremented every backoff and cleared every
// successful response. For simplicity, if we have a good response
// following an error response before the error response's backoff
// occurs, the backoff is not cleared.
consecutiveFailures atomic.Uint32
recBufsMu sync.Mutex // guards the following
recBufs []*recBuf // contains all partition records for batch building
recBufsStart int // incremented every req to avoid large batch starvation
}
type seqResp struct {
resp kmsg.Response
err error
done chan struct{}
br *broker
promise func(*broker, kmsg.Response, error)
}
func (cl *Client) newSink(nodeID int32) *sink {
s := &sink{
cl: cl,
nodeID: nodeID,
}
s.produceVersion.Store(-1)
maxInflight := 1
if cl.cfg.disableIdempotency {
maxInflight = cl.cfg.maxProduceInflight
}
s.inflightSem.Store(make(chan struct{}, maxInflight))
return s
}
// createReq returns a produceRequest from currently buffered records
// and whether there are more records to create more requests immediately.
func (s *sink) createReq(id int64, epoch int16) (*produceRequest, *kmsg.AddPartitionsToTxnRequest, bool) {
s.cl.producer.txnMu.Lock()
tx890p2 := s.cl.producer.tx890p2
s.cl.producer.txnMu.Unlock()
req := &produceRequest{
can12: s.cl.cfg.txnID == nil || tx890p2,
txnID: s.cl.cfg.txnID,
acks: s.cl.cfg.acks.val,
timeout: int32(s.cl.cfg.produceTimeout.Milliseconds()),
producerID: id,
producerEpoch: epoch,
hasHook: s.cl.producer.hasHookBatchWritten,
compressor: s.cl.cfg.compressor,
wireLength: s.cl.baseProduceRequestLength(), // start length with no topics
wireLengthLimit: s.cl.cfg.maxBrokerWriteBytes,
}
txnBuilder := txnReqBuilder{
txnID: req.txnID,
id: id,
epoch: epoch,
pv12: req.can12, // produce request v12 && transaction.version >= 2 means we no longer send AddPartitionsToTxn
}
var moreToDrain bool
s.recBufsMu.Lock()
defer s.recBufsMu.Unlock()
recBufsIdx := s.recBufsStart
for range s.recBufs {
recBuf := s.recBufs[recBufsIdx]
recBufsIdx = (recBufsIdx + 1) % len(s.recBufs)
recBuf.mu.Lock()
if recBuf.failing || len(recBuf.batches) == recBuf.batchDrainIdx || recBuf.inflightOnSink != nil && recBuf.inflightOnSink != s || recBuf.inflight != 0 && !recBuf.okOnSink {
recBuf.mu.Unlock()
continue
}
batch := recBuf.batches[recBuf.batchDrainIdx]
if added := req.tryAddBatch(s.produceVersion.Load(), recBuf, batch); !added {
recBuf.mu.Unlock()
moreToDrain = true
continue
}
if s.cl.cfg.disableIdempotency {
if cctx := batch.records[0].cancelingCtx(); cctx != nil && req.firstCancelingCtx == nil {
req.firstCancelingCtx = cctx //nolint:fatcontext // we are only here if firstCancelingCtx is currently nil
}
}
recBuf.inflightOnSink = s
recBuf.inflight++
recBuf.batchDrainIdx++
recBuf.lockedStopLinger()
recBuf.seq = incrementSequence(recBuf.seq, int32(len(batch.records)))
moreToDrain = recBuf.checkIfShouldDrainOrStartLinger() || moreToDrain
recBuf.mu.Unlock()
txnBuilder.add(recBuf)
}
// We could have lost our only record buffer just before we grabbed the
// lock above, so we have to check there are recBufs.
if len(s.recBufs) > 0 {
s.recBufsStart = (s.recBufsStart + 1) % len(s.recBufs)
}
return req, txnBuilder.req, moreToDrain
}
func incrementSequence(sequence, increment int32) int32 {
if sequence > math.MaxInt32-increment {
return increment - (math.MaxInt32 - sequence) - 1
}
return sequence + increment
}
type txnReqBuilder struct {
txnID *string
req *kmsg.AddPartitionsToTxnRequest
id int64
epoch int16
pv12 bool
addedTopics map[string]int // topic => index into req
}
func (t *txnReqBuilder) add(rb *recBuf) {
// For produce v12+, we mark the partition as added to the transaction
// if there is no partition error code in a produce response.
//
// For prior versions, we actually issue an AddPartitionsToTxn request.
// The original logic was to mark addedToTxn before issuing the request
// and swap it back to false if the request failed or there was a
// partition error. We *could* swap this to only add to the txn on
// successful request, but there is other logic that needs to run on
// failure and the old code is well tested, so we'll keep it.
//
// We must keep the pv12 check first, otherwise we may accidentally
// mark something as added to the txn while produce requests fail.
if t.txnID == nil || t.pv12 || rb.addedToTxn.Swap(true) {
return
}
if t.req == nil {
req := kmsg.NewPtrAddPartitionsToTxnRequest()
req.TransactionalID = *t.txnID
req.ProducerID = t.id
req.ProducerEpoch = t.epoch
t.req = req
t.addedTopics = make(map[string]int, 10)
}
idx, exists := t.addedTopics[rb.topic]
if !exists {
idx = len(t.req.Topics)
t.addedTopics[rb.topic] = idx
reqTopic := kmsg.NewAddPartitionsToTxnRequestTopic()
reqTopic.Topic = rb.topic
t.req.Topics = append(t.req.Topics, reqTopic)
}
t.req.Topics[idx].Partitions = append(t.req.Topics[idx].Partitions, rb.partition)
}
func (s *sink) maybeDrain() {
if s.cl.cfg.manualFlushing && s.cl.producer.flushing.Load() == 0 {
return
}
if s.drainState.maybeBegin() {
go s.drain()
}
}
func (s *sink) maybeBackoff() {
s.backoffMu.Lock()
backoff := s.needBackoff
s.backoffMu.Unlock()
if !backoff {
return
}
defer s.clearBackoff()
s.cl.triggerUpdateMetadata(false, "opportunistic load during sink backoff") // as good a time as any
tries := int(s.consecutiveFailures.Add(1))
after := time.NewTimer(s.cl.cfg.retryBackoff(tries))
defer after.Stop()
select {
case <-after.C:
case <-s.cl.ctx.Done():
case <-s.anyCtx().Done():
}
}
func (s *sink) maybeTriggerBackoff(seq uint32) {
s.backoffMu.Lock()
defer s.backoffMu.Unlock()
if seq == s.backoffSeq {
s.needBackoff = true
}
}
func (s *sink) clearBackoff() {
s.backoffMu.Lock()
defer s.backoffMu.Unlock()
s.backoffSeq++
s.needBackoff = false
}
// drain drains buffered records and issues produce requests.
//
// This function is harmless if there are no records that need draining.
// We rely on that to not worry about accidental triggers of this function.
func (s *sink) drain() {
again := true
for again {
s.maybeBackoff()
sem := s.inflightSem.Load().(chan struct{})
select {
case sem <- struct{}{}:
case <-s.cl.ctx.Done():
s.drainState.hardFinish()
return
}
again = s.drainState.maybeFinish(s.produce(sem))
}
}
// Returns the first context encountered ranging across all records.
// This does not use defers to make it clear at the return that all
// unlocks are called in proper order. Ideally, do not call this func
// due to lock intensity.
func (s *sink) anyCtx() context.Context {
s.recBufsMu.Lock()
for _, recBuf := range s.recBufs {
recBuf.mu.Lock()
if len(recBuf.batches) > 0 {
batch0 := recBuf.batches[0]
batch0.mu.Lock()
if batch0.canFailFromLoadErrs && len(batch0.records) > 0 {
r0 := batch0.records[0]
if rctx := r0.cancelingCtx(); rctx != nil {
batch0.mu.Unlock()
recBuf.mu.Unlock()
s.recBufsMu.Unlock()
return rctx
}
}
batch0.mu.Unlock()
}
recBuf.mu.Unlock()
}
s.recBufsMu.Unlock()
return context.Background()
}
func (s *sink) produce(sem <-chan struct{}) bool {
var produced bool
defer func() {
if !produced {
<-sem
}
}()
// We could have some spurious produce calls while wrapping up.
if s.cl.BufferedProduceRecords() == 0 {
return false
}
// producerID can fail from:
// - retry failure
// - auth failure
// - transactional: a produce failure that failed the producer ID
// - AddPartitionsToTxn failure (see just below)
// - some head-of-line context failure
//
// All but the first error is fatal. Recovery may be possible with
// EndTransaction in specific cases, but regardless, all buffered
// records must fail.
//
// NOTE: we init the producer ID before creating a request to ensure we
// are always using the latest id/epoch with the proper sequence
// numbers. (i.e., resetAllSequenceNumbers && producerID logic combo).
//
// For the first-discovered-record-head-of-line context, we want to
// avoid looking it up if possible (which is why producerID takes a
// ctxFn). If we do use one, we want to be sure that the
// context.Canceled error is from *that* context rather than the client
// context or something else. So, we go through some special care to
// track setting the ctx / looking up if it is canceled.
var holCtxMu sync.Mutex
var holCtx context.Context
ctxFn := func() context.Context {
holCtxMu.Lock()
defer holCtxMu.Unlock()
if holCtx == nil {
holCtx = s.anyCtx() //nolint:fatcontext // not sure why this is flagged
}
return holCtx
}
isHolCtxDone := func() bool {
holCtxMu.Lock()
defer holCtxMu.Unlock()
if holCtx == nil {
return false
}
select {
case <-holCtx.Done():
return true
default:
}
return false
}
id, epoch, err := s.cl.producerID(ctxFn)
if err != nil {
var pe *errProducerIDLoadFail
switch {
case errors.As(err, &pe):
if errors.Is(pe.err, context.Canceled) && isHolCtxDone() {
// Some head-of-line record in a partition had a context cancelation.
// We look for any partition with HOL cancelations and fail them all.
s.cl.cfg.logger.Log(LogLevelInfo, "the first record in some partition(s) had a context cancelation; failing all relevant partitions", "broker", logID(s.nodeID))
s.recBufsMu.Lock()
defer s.recBufsMu.Unlock()
for _, recBuf := range s.recBufs {
recBuf.mu.Lock()
var failAll bool
if len(recBuf.batches) > 0 {
batch0 := recBuf.batches[0]
batch0.mu.Lock()
if batch0.canFailFromLoadErrs && len(batch0.records) > 0 {
r0 := batch0.records[0]
if rctx := r0.cancelingCtx(); rctx != nil {
select {
case <-rctx.Done():
failAll = true // we must not call failAllRecords here, because failAllRecords locks batches!
default:
}
}
}
batch0.mu.Unlock()
}
if failAll {
recBuf.failAllRecords(err)
}
recBuf.mu.Unlock()
}
return true
}
s.cl.bumpRepeatedLoadErr(err)
s.cl.cfg.logger.Log(LogLevelWarn, "unable to load producer ID, bumping client's buffered record load errors by 1 and retrying")
return true // whatever caused our produce, we did nothing, so keep going
case errors.Is(err, ErrClientClosed):
s.cl.failBufferedRecords(err)
default:
s.cl.cfg.logger.Log(LogLevelError, "fatal InitProducerID error, failing all buffered records", "broker", logID(s.nodeID), "err", err)
s.cl.failBufferedRecords(err)
}
return false
}
// NOTE: we create the req AFTER getting our producer ID!
//
// If a prior response caused errReloadProducerID, then calling
// producerID() sets needSeqReset, and creating the request resets
// sequence numbers. We need to have that logic occur before we create
// the request, otherwise we will create a request with the old
// sequence numbers using our new producer ID, which will then again
// fail with OOOSN.
req, txnReq, moreToDrain := s.createReq(id, epoch)
if len(req.batches.bs) == 0 { // everything was failing or lingering, or what is buffered is in flight already
return moreToDrain
}
if txnReq != nil {
// txnReq can fail from:
// - TransactionAbortable
// - retry failure
// - auth failure
// - producer id mapping / epoch errors
// The latter case can potentially recover with the kip logic
// we have defined in EndTransaction. Regardless, on failure
// here, all buffered records must fail.
batchesStripped, err := s.doTxnReq(req, txnReq)
if err != nil {
switch {
case errors.Is(err, kerr.TransactionAbortable):
// If we get TransactionAbortable, we continue into producing.
// The produce will fail with the same error, and this is the
// only way to notify the user to abort the txn.
case isRetryableBrokerErr(err) || isDialNonTimeoutErr(err):
s.cl.bumpRepeatedLoadErr(err)
s.cl.cfg.logger.Log(LogLevelWarn, "unable to AddPartitionsToTxn due to retryable broker err, bumping client's buffered record load errors by 1 and retrying", "err", err)
s.cl.triggerUpdateMetadata(false, "attempting to refresh broker list due to failed AddPartitionsToTxn requests")
return moreToDrain || len(req.batches.bs) > 0 // nothing stripped if request-issuing error
default:
// Note that err can be InvalidProducerEpoch, which is
// potentially recoverable in EndTransaction.
//
// We do not fail all buffered records here,
// because that can lead to undesirable behavior
// with produce request vs. end txn (KAFKA-12671)
s.cl.failProducerID(id, epoch, err)
s.cl.cfg.logger.Log(LogLevelError, "fatal AddPartitionsToTxn error, failing all buffered records (it is possible the client can recover after EndTransaction)", "broker", logID(s.nodeID), "err", err)
return false
}
}
// If we stripped everything, ensure we backoff to force a
// metadata load. If not everything was stripped, we issue our
// request and ensure we will retry a producing until
// everything is stripped (and we eventually back off).
if batchesStripped {
moreToDrain = true
if len(req.batches.bs) == 0 {
s.maybeTriggerBackoff(s.backoffSeq)
}
}
}
if len(req.batches.bs) == 0 { // txn req could have removed some partitions to retry later (unknown topic, etc.)
return moreToDrain
}
req.backoffSeq = s.backoffSeq // safe to read outside mu since we are in drain loop
produced = true
batches := req.batches.sliced()
s.doSequenced(req, func(br *broker, resp kmsg.Response, err error) {
s.handleReqResp(br, req, resp, err)
batches.eachOwnerLocked((*recBatch).decInflight)
<-sem
})
return moreToDrain
}
// With handleSeqResps below, this function ensures that all request responses
// are handled in order. We use this guarantee while in handleReqResp below.
func (s *sink) doSequenced(
req *produceRequest,
promise func(*broker, kmsg.Response, error),
) {
wait := &seqResp{
done: make(chan struct{}),
promise: promise,
}
// We can NOT use any record context. If we do, we force the request to
// fail while also force the batch to be unfailable (due to no
// response). If and only if the user has disabled idempotency, we
// allow the user to cancel the request via some random record with a
// canceling context.
ctx := req.firstCancelingCtx
if ctx == nil {
ctx = s.cl.ctx
}
br, err := s.cl.brokerOrErr(ctx, s.nodeID, errUnknownBroker)
if err != nil {
wait.err = err
close(wait.done)
} else {
br.do(ctx, req, func(resp kmsg.Response, err error) {
wait.resp = resp
wait.err = err
close(wait.done)
})
wait.br = br
}
if first, _ := s.seqResps.push(wait); first {
go s.handleSeqResps(wait)
}
}
// Ensures that all request responses are processed in order.
func (s *sink) handleSeqResps(wait *seqResp) {
var more bool
start:
<-wait.done
wait.promise(wait.br, wait.resp, wait.err)
wait, more, _ = s.seqResps.dropPeek()
if more {
goto start
}
}
// Issues an AddPartitionsToTxnRequest before a produce request for all
// partitions that need to be added to a transaction.
func (s *sink) doTxnReq(
req *produceRequest,
txnReq *kmsg.AddPartitionsToTxnRequest,
) (stripped bool, err error) {
// If we return an unretryable error, then we have to reset everything
// to not be in the transaction and begin draining at the start.
//
// These batches must be the first in their recBuf, because we would
// not be trying to add them to a partition if they were not.
defer func() {
if err != nil {
req.batches.eachOwnerLocked(seqRecBatch.removeFromTxn)
}
}()
// We do NOT let record context cancelations fail this request: doing
// so would put the transactional ID in an unknown state. This is
// similar to the warning we give in the txn.go file, but the
// difference there is the user knows explicitly at the function call
// that canceling the context will opt them into invalid state.
err = s.cl.doWithConcurrentTransactions(s.cl.ctx, fmt.Sprintf("AddPartitionsToTxn-sink%d", s.nodeID), func() error {
stripped, err = s.issueTxnReq(req, txnReq)
return err
})
return stripped, err
}
// Removing a batch from the transaction means we will not be issuing it
// inflight, and that it was not added to the txn and that we need to reset the
// drain index.
func (b *recBatch) removeFromTxn() {
b.owner.addedToTxn.Store(false)
b.owner.resetBatchDrainIdx()
b.decInflight()
}
func (s *sink) issueTxnReq(
req *produceRequest,
txnReq *kmsg.AddPartitionsToTxnRequest,
) (stripped bool, fatalErr error) {
resp, err := txnReq.RequestWith(s.cl.ctx, s.cl)
if err != nil {
return false, err
}
for _, topic := range resp.Topics {
topicBatches, ok := req.batches.bs[topic.Topic]
if !ok {
s.cl.cfg.logger.Log(LogLevelError, "broker replied with topic in AddPartitionsToTxnResponse that was not in request", "topic", topic.Topic)
continue
}
for _, partition := range topic.Partitions {
if err := kerr.ErrorForCode(partition.ErrorCode); err != nil && err != kerr.TransactionAbortable { // see below for txn abortable
// OperationNotAttempted is set for all partitions that are authorized
// if any partition is unauthorized _or_ does not exist. We simply remove
// unattempted partitions and treat them as retryable.
if !kerr.IsRetriable(err) && !errors.Is(err, kerr.OperationNotAttempted) {
fatalErr = err // auth err, etc
continue
}
batch, ok := topicBatches[partition.Partition]
if !ok {
s.cl.cfg.logger.Log(LogLevelError, "broker replied with partition in AddPartitionsToTxnResponse that was not in request", "topic", topic.Topic, "partition", partition.Partition)
continue
}
// We are stripping this retryable-err batch from the request,
// so we must reset that it has been added to the txn.
batch.owner.mu.Lock()
batch.removeFromTxn()
batch.owner.mu.Unlock()
stripped = true
delete(topicBatches, partition.Partition)
}
if len(topicBatches) == 0 {
delete(req.batches.bs, topic.Topic)
}
}
}
return stripped, fatalErr
}
// firstRespCheck is effectively a sink.Once. On the first response, if the
// used request version is at least 4, we upgrade our inflight sem.
//
// Starting on version 4, Kafka allowed five inflight requests while
// maintaining idempotency. Before, only one was allowed.
//
// We go through an atomic because drain can be waiting on the sem (with
// capacity one). We store four here, meaning new drain loops will load the
// higher capacity sem without read/write pointer racing a current loop.
//
// This logic does mean that we will never use the full potential 5 in flight
// outside of a small window during the store, but some pages in the Kafka
// confluence basically show that more than two in flight has marginal benefit
// anyway (although that may be due to their Java API).
//
// https://cwiki.apache.org/confluence/display/KAFKA/An+analysis+of+the+impact+of+max.in.flight.requests.per.connection+and+acks+on+Producer+performance
// https://issues.apache.org/jira/browse/KAFKA-5494
func (s *sink) firstRespCheck(idempotent bool, version int16) {
if s.produceVersion.Load() < 0 {
s.produceVersion.Store(int32(version))
if idempotent && version >= 4 {
s.inflightSem.Store(make(chan struct{}, 4))
}
}
}
// handleReqClientErr is called when the client errors before receiving a
// produce response.
func (s *sink) handleReqClientErr(req *produceRequest, err error) {
switch {
default:
s.cl.cfg.logger.Log(LogLevelWarn, "random error while producing, requeueing unattempted request", "broker", logID(s.nodeID), "err", err)
fallthrough
case errors.Is(err, errUnknownBroker),
isDialNonTimeoutErr(err),
isRetryableBrokerErr(err):
updateMeta := !isRetryableBrokerErr(err)
if updateMeta {
s.cl.cfg.logger.Log(LogLevelInfo, "produce request failed, triggering metadata update", "broker", logID(s.nodeID), "err", err)
}
s.handleRetryBatches(req.batches, nil, req.backoffSeq, updateMeta, false, "failed produce request triggered metadata update")
case errors.Is(err, ErrClientClosed):
s.cl.failBufferedRecords(ErrClientClosed)
}
}
// No acks mean no response. The following block is basically an extremely
// condensed version of the logic in handleReqResp.
func (s *sink) handleReqRespNoack(b *bytes.Buffer, debug bool, req *produceRequest) {
if debug {
fmt.Fprintf(b, "noack ")
}
for topic, partitions := range req.batches.bs {
if debug {
fmt.Fprintf(b, "%s[", topic)
}
for partition, batch := range partitions {
batch.owner.mu.Lock()
if batch.isOwnersFirstBatch() {
if debug {
fmt.Fprintf(b, "%d{0=>%d}, ", partition, len(batch.records))
}
s.cl.finishBatch(batch.recBatch, req.producerID, req.producerEpoch, partition, 0, nil)
} else if debug {
fmt.Fprintf(b, "%d{skipped}, ", partition)
}
batch.owner.mu.Unlock()
}
if debug {
if bytes.HasSuffix(b.Bytes(), []byte(", ")) {
b.Truncate(b.Len() - 2)
}
b.WriteString("], ")
}
}
}
func (s *sink) handleReqResp(br *broker, req *produceRequest, resp kmsg.Response, err error) {
if err != nil {
s.handleReqClientErr(req, err)
return
}
s.firstRespCheck(req.idempotent(), req.version)
s.consecutiveFailures.Store(0)
defer req.metrics.hook(&s.cl.cfg, br) // defer to end so that non-written batches are removed
var b *bytes.Buffer
debug := s.cl.cfg.logger.Level() >= LogLevelDebug
if debug {
b = bytes.NewBuffer(make([]byte, 0, 128))
defer func() {
update := b.String()
update = strings.TrimSuffix(update, ", ")
s.cl.cfg.logger.Log(LogLevelDebug, "produced", "broker", logID(s.nodeID), "to", update)
}()
}
if req.acks == 0 {
s.handleReqRespNoack(b, debug, req)
return
}
var kmove kip951move
var reqRetry seqRecBatches // handled at the end
kresp := resp.(*kmsg.ProduceResponse)
for i := range kresp.Topics {
rt := &kresp.Topics[i]
topic := rt.Topic
tid := rt.TopicID
if req.version >= 13 {
var ok bool
if topic, ok = req.batches.id2t[rt.TopicID]; !ok {
s.cl.cfg.logger.Log(LogLevelError, "broker erroneously replied with topic id in produce request that we did not produce to", "broker", logID(s.nodeID), "topic", topic, "topic_id", strtid(rt.TopicID))
delete(req.metrics, topic)
continue
}
} else {
tid = req.batches.t2id[topic]
}
partitions, ok := req.batches.bs[topic]
if !ok {
s.cl.cfg.logger.Log(LogLevelError, "broker erroneously replied with topic in produce request that we did not produce to", "broker", logID(s.nodeID), "topic", topic)
delete(req.metrics, topic)
continue
}
if debug {
fmt.Fprintf(b, "%s[", topic)
}
tmetrics := req.metrics[topic]
for j := range rt.Partitions {
rp := &rt.Partitions[j]
partition := rp.Partition
batch, ok := partitions[partition]
if !ok {
s.cl.cfg.logger.Log(LogLevelError, "broker erroneously replied with partition in produce request that we did not produce to", "broker", logID(s.nodeID), "topic", rt.Topic, "partition", partition)
delete(tmetrics, partition)
continue // should not hit this
}
delete(partitions, partition)
retry, didProduce := s.handleReqRespBatch(
b,
&kmove,
kresp,
topic,
rp,
batch,
req.producerID,
req.producerEpoch,
)
if retry {
reqRetry.addSeqBatch(topic, tid, partition, batch)
}
if !didProduce {
delete(tmetrics, partition)
}
}
if debug {
if bytes.HasSuffix(b.Bytes(), []byte(", ")) {
b.Truncate(b.Len() - 2)
}
b.WriteString("], ")
}
if len(partitions) == 0 {
delete(req.batches.bs, topic)
}
}
if len(req.batches.bs) > 0 {
s.cl.cfg.logger.Log(LogLevelError, "broker did not reply to all topics / partitions in the produce request! reenqueuing missing partitions", "broker", logID(s.nodeID))
s.handleRetryBatches(req.batches, nil, 0, true, false, "broker did not reply to all topics in produce request")
}
if len(reqRetry.bs) > 0 {
s.handleRetryBatches(reqRetry, &kmove, 0, true, true, "produce request had retry batches")
}
}
func (s *sink) handleReqRespBatch(
b *bytes.Buffer,
kmove *kip951move,
resp *kmsg.ProduceResponse,
topic string,
rp *kmsg.ProduceResponseTopicPartition,
batch seqRecBatch,
producerID int64,
producerEpoch int16,
) (retry, didProduce bool) {
batch.owner.mu.Lock()
defer batch.owner.mu.Unlock()
nrec := len(batch.records)
debug := b != nil
if debug {
fmt.Fprintf(b, "%d{", rp.Partition)
}
// We only ever operate on the first batch in a record buf. Batches
// work sequentially; if this is not the first batch then an error
// happened and this later batch is no longer a part of a seq chain.
if !batch.isOwnersFirstBatch() {
if debug {
if err := kerr.ErrorForCode(rp.ErrorCode); err == nil {
if nrec > 0 {
fmt.Fprintf(b, "skipped@%d=>%d}, ", rp.BaseOffset, rp.BaseOffset+int64(nrec))
} else {
fmt.Fprintf(b, "skipped@%d}, ", rp.BaseOffset)
}
} else {
if nrec > 0 {
fmt.Fprintf(b, "skipped@%d,%d(%s)}, ", rp.BaseOffset, nrec, err)
} else {
fmt.Fprintf(b, "skipped@%d(%s)}, ", rp.BaseOffset, err)
}
}
}
return false, false
}
// Since we have received a response and we are the first batch, we can
// at this point re-enable failing from load errors.
//
// We do not need the mutex lock on the batch. We already have the
// recBuf mu (guarding most concurrency). The only place batch fields
// are accessed & modified without the recBuf mu is when writing a
// batch, and we only ever use a batch in inflight request at a time
// (regardless of the partition being canceled or moving to a
// different sink).
batch.canFailFromLoadErrs = true
// By default, we assume we errored. Non-error updates this back
// to true.
batch.owner.okOnSink = false
if moving := kmove.maybeAddProducePartition(resp, rp, batch.owner); moving {
if debug {
fmt.Fprintf(b, "move:%d:%d@%d,%d}, ", rp.CurrentLeader.LeaderID, rp.CurrentLeader.LeaderEpoch, rp.BaseOffset, nrec)
}
batch.owner.failing = true
return true, false
}
err := kerr.ErrorForCode(rp.ErrorCode)
failUnknown := batch.owner.checkUnknownFailLimit(err)
switch {
case err == kerr.ConcurrentTransactions:
// Occasionally this is bubbled back to the producer as of
// KIP-890; we retry this.
fallthrough //nolint:gocritic // easier to read this way
case kerr.IsRetriable(err) &&
!failUnknown &&
err != kerr.CorruptMessage &&
batch.tries <= s.cl.cfg.recordRetries:
if debug {
fmt.Fprintf(b, "retrying@%d,%d(%s)}, ", rp.BaseOffset, nrec, err)
}
return true, false
case err == kerr.OutOfOrderSequenceNumber,
err == kerr.UnknownProducerID,
err == kerr.InvalidProducerIDMapping,
err == kerr.InvalidProducerEpoch:
// OOOSN always means data loss 1.0+ and is ambiguous prior.
// We assume the worst and only continue if requested.
//
// UnknownProducerID was introduced to allow some form of safe
// handling, but KIP-360 demonstrated that resetting sequence
// numbers is fundamentally unsafe, so we treat it like OOOSN.
//
// KAFKA-5793 specifically mentions for OOOSN "when you get it,
// it should always mean data loss". Sometime after KIP-360,
// Kafka changed the client to remove all places
// UnknownProducerID was returned, and then started referring
// to OOOSN as retryable. KIP-890 definitively says OOOSN is
// retryable. However, the Kafka source as of 24-10-10 still
// only retries OOOSN for batches that are NOT the expected
// next batch (i.e., it's next + 1, for when there are multiple
// in flight). With KIP-890, we still just disregard whatever
// supposedly non-retryable / actually-is-retryable error is
// returned if the LogStartOffset is _after_ what we previously
// produced. Specifically, this is step (4) in wiki link
// within KAFKA-5793.
//
// InvalidMapping is similar to UnknownProducerID, but occurs
// when the txnal coordinator timed out our transaction.
//
// 2.5
// =====
// 2.5 introduced some behavior to potentially safely reset
// the sequence numbers by bumping an epoch (see KIP-360).
//
// For the idempotent producer, the solution is to fail all
// buffered records and then let the client user reset things
// with the understanding that they cannot guard against
// potential dups / reordering at that point. Realistically,
// that's no better than a config knob that allows the user
// to continue (our stopOnDataLoss flag), so for the idempotent
// producer, if stopOnDataLoss is false, we just continue.
//
// For the transactional producer, we always fail the producerID.
// EndTransaction will trigger recovery if possible.
//
// 2.7
// =====
// InvalidProducerEpoch became retryable in 2.7. Prior, it
// was ambiguous (timeout? fenced?). Now, InvalidProducerEpoch
// is only returned on produce, and then we can recover on other
// txn coordinator requests, which have PRODUCER_FENCED vs
// TRANSACTION_TIMED_OUT.
if batch.owner.lastAckedOffset >= 0 && rp.LogStartOffset > batch.owner.lastAckedOffset {
s.cl.cfg.logger.Log(LogLevelInfo, "partition prefix truncation to after our last produce caused the broker to forget us; no loss occurred, bumping producer epoch and resetting sequence numbers",
"broker", logID(s.nodeID),
"topic", topic,
"partition", rp.Partition,
"producer_id", producerID,
"producer_epoch", producerEpoch,
"err", err,
)
s.cl.failProducerID(producerID, producerEpoch, errReloadProducerID)
if debug {
fmt.Fprintf(b, "resetting@%d,%d(%s)}, ", rp.BaseOffset, nrec, err)
}
return true, false
}
if s.cl.cfg.txnID != nil || s.cl.cfg.stopOnDataLoss {
s.cl.cfg.logger.Log(LogLevelInfo, "batch errored, failing the producer ID",
"broker", logID(s.nodeID),
"topic", topic,
"partition", rp.Partition,
"producer_id", producerID,
"producer_epoch", producerEpoch,
"err", err,
)
s.cl.failProducerID(producerID, producerEpoch, err)
s.cl.finishBatch(batch.recBatch, producerID, producerEpoch, rp.Partition, rp.BaseOffset, err)
if debug {
fmt.Fprintf(b, "fatal@%d,%d(%s)}, ", rp.BaseOffset, nrec, err)
}
return false, false
}
if s.cl.cfg.onDataLoss != nil {
s.cl.cfg.onDataLoss(topic, rp.Partition)
}
// For OOOSN, and UnknownProducerID
//
// The only recovery is to fail the producer ID, which ensures
// that all batches reset sequence numbers and use a new producer
// ID on the next batch.
//
// For InvalidProducerIDMapping && InvalidProducerEpoch,
//
// We should not be here, since this error occurs in the
// context of transactions, which are caught above.
s.cl.cfg.logger.Log(LogLevelInfo, fmt.Sprintf("batch errored with %s, failing the producer ID and resetting all sequence numbers", err.(*kerr.Error).Message),
"broker", logID(s.nodeID),
"topic", topic,
"partition", rp.Partition,
"producer_id", producerID,
"producer_epoch", producerEpoch,
"err", err,
)
// After we fail here, any new produce (even new ones
// happening concurrent with this function) will load
// a new epoch-bumped producer ID and all first-batches
// will reset sequence numbers appropriately.
s.cl.failProducerID(producerID, producerEpoch, errReloadProducerID)
if debug {
fmt.Fprintf(b, "resetting@%d,%d(%s)}, ", rp.BaseOffset, nrec, err)
}
return true, false
case err == kerr.DuplicateSequenceNumber: // ignorable, but we should not get
s.cl.cfg.logger.Log(LogLevelInfo, "received unexpected duplicate sequence number, ignoring and treating batch as successful",
"broker", logID(s.nodeID),
"topic", topic,
"partition", rp.Partition,
)
err = nil
fallthrough
default:
if err != nil {
s.cl.cfg.logger.Log(LogLevelInfo, "batch in a produce request failed",
"broker", logID(s.nodeID),
"topic", topic,
"partition", rp.Partition,
"err", err,
"err_is_retryable", kerr.IsRetriable(err),
"max_retries_reached", !failUnknown && batch.tries > s.cl.cfg.recordRetries,
)
} else {
batch.owner.okOnSink = true
batch.owner.lastAckedOffset = rp.BaseOffset + int64(len(batch.records))
if resp.Version >= 12 && s.cl.cfg.txnID != nil {
batch.owner.addedToTxn.Swap(true)
}
}
s.cl.finishBatch(batch.recBatch, producerID, producerEpoch, rp.Partition, rp.BaseOffset, err)
didProduce = err == nil
if debug {
if err != nil {
fmt.Fprintf(b, "err@%d,%d(%s)}, ", rp.BaseOffset, nrec, err)
} else {
fmt.Fprintf(b, "%d=>%d}, ", rp.BaseOffset, rp.BaseOffset+int64(nrec))
}
}
}
return false, didProduce // no retry
}
// finishBatch removes a batch from its owning record buffer and finishes all
// records in the batch.
//
// This is safe even if the owning recBuf migrated sinks, since we are
// finishing based off the status of an inflight req from the original sink.
func (cl *Client) finishBatch(batch *recBatch, producerID int64, producerEpoch int16, partition int32, baseOffset int64, err error) {
recBuf := batch.owner
if err != nil {
// We know that Kafka replied this batch is a failure. We can
// fail this batch and all batches in this partition.
// This will keep sequence numbers correct.
recBuf.failAllRecords(err)
return
}
// We know the batch made it to Kafka successfully without error.
// We remove this batch and finish all records appropriately.
finished := len(batch.records)
recBuf.batch0Seq = incrementSequence(recBuf.batch0Seq, int32(finished))
recBuf.buffered.Add(-int64(finished))
recBuf.batches[0] = nil
recBuf.batches = recBuf.batches[1:]
recBuf.batchDrainIdx--
batch.mu.Lock()
records, attrs := batch.records, batch.attrs
batch.records = nil
batch.mu.Unlock()
cl.producer.promiseBatch(batchPromise{
baseOffset: baseOffset,
pid: producerID,
epoch: producerEpoch,
// A recBuf.attrs is updated when appending to be written. For
// v0 && v1 produce requests, we set bit 8 in the attrs
// corresponding to our own RecordAttr's bit 8 being no
// timestamp type. Thus, we can directly convert the batch
// attrs to our own RecordAttrs.
attrs: RecordAttrs{uint8(attrs)},
partition: partition,
recs: records,
})
}
// handleRetryBatches sets any first-buf-batch to failing and triggers a
// metadata that will eventually clear the failing state and re-drain.
//
// If idempotency is disabled, if a batch is timed out or hit the retry limit,
// we fail it and anything after it.
func (s *sink) handleRetryBatches(
retry seqRecBatches,
kmove *kip951move,
backoffSeq uint32,
updateMeta bool, // if we should maybe update the metadata
canFail bool, // if records can fail if they are at limits
why string,
) {
logger := s.cl.cfg.logger
debug := logger.Level() >= LogLevelDebug
var needsMetaUpdate bool
var shouldBackoff bool
if kmove != nil {
defer kmove.maybeBeginMove(s.cl)
}
var numRetryBatches, numMoveBatches int
retry.eachOwnerLocked(func(batch seqRecBatch) {
numRetryBatches++
if !batch.isOwnersFirstBatch() {
if debug {
logger.Log(LogLevelDebug, "retry batch is not the first batch in the owner, skipping result",
"topic", batch.owner.topic,
"partition", batch.owner.partition,
)
}
return
}
// If the request failed due to a concurrent metadata update
// moving partitions to a different sink (or killing the sink
// this partition was on), we can just reset the drain index
// and trigger draining now the new sink. There is no reason
// to backoff on this sink nor trigger a metadata update.
if batch.owner.sink != s {
if debug {
logger.Log(LogLevelDebug, "transitioned sinks while a request was inflight, retrying immediately on new sink without backoff",
"topic", batch.owner.topic,
"partition", batch.owner.partition,
"old_sink", s.nodeID,
"new_sink", batch.owner.sink.nodeID,
)
}
batch.owner.resetBatchDrainIdx()
return
}
if canFail || s.cl.cfg.disableIdempotency {
if err := batch.maybeFailErr(&s.cl.cfg); err != nil {
batch.owner.failAllRecords(err)
return
}
}
batch.owner.resetBatchDrainIdx()
// Now that the batch drain index is reset, if this retry is
// caused from a moving batch, return early. We do not need
// to backoff nor do we need to trigger a metadata update.
if kmove.hasRecBuf(batch.owner) {
numMoveBatches++
return
}
// If our first batch (seq == 0) fails with unknown topic, we
// retry immediately. Kafka can reply with valid metadata
// immediately after a topic was created, before the leaders
// actually know they are leader.
unknownAndFirstBatch := batch.owner.unknownFailures == 1 && batch.owner.seq == 0
if unknownAndFirstBatch {
shouldBackoff = true
return
}
if updateMeta {
batch.owner.failing = true
needsMetaUpdate = true
}
})
if debug {
logger.Log(LogLevelDebug, "retry batches processed",
"wanted_metadata_update", updateMeta,
"triggering_metadata_update", needsMetaUpdate,
"should_backoff", shouldBackoff,
)
}
// If we do want to metadata update, we only do so if any batch was the
// first batch in its buf / not concurrently failed.
if needsMetaUpdate {
s.cl.triggerUpdateMetadata(true, why)
return
}
// We could not need a metadata update for two reasons:
//
// * our request died when being issued
//
// * we would update metadata, but what failed was the first batch
// produced and the error was unknown topic / partition.
//
// In either of these cases, we should backoff a little bit to avoid
// spin looping.
//
// If neither of these cases are true, then we entered wanting a
// metadata update, but the batches either were not the first batch, or
// the batches were concurrently failed.
//
// If all partitions are moving, we do not need to backoff nor drain.
if shouldBackoff || (!updateMeta && numRetryBatches != numMoveBatches) {
s.maybeTriggerBackoff(backoffSeq)
s.maybeDrain()
}
}
// addRecBuf adds a new record buffer to be drained to a sink and clears the
// buffer's failing state.
func (s *sink) addRecBuf(add *recBuf) {
s.recBufsMu.Lock()
add.recBufsIdx = len(s.recBufs)
s.recBufs = append(s.recBufs, add)
s.recBufsMu.Unlock()
add.clearFailing()
}
// removeRecBuf removes a record buffer from a sink.
func (s *sink) removeRecBuf(rm *recBuf) {
s.recBufsMu.Lock()
defer s.recBufsMu.Unlock()
if rm.recBufsIdx != len(s.recBufs)-1 {
s.recBufs[rm.recBufsIdx], s.recBufs[len(s.recBufs)-1] = s.recBufs[len(s.recBufs)-1], nil
s.recBufs[rm.recBufsIdx].recBufsIdx = rm.recBufsIdx
} else {
s.recBufs[rm.recBufsIdx] = nil // do not let this removal hang around
}
s.recBufs = s.recBufs[:len(s.recBufs)-1]
if s.recBufsStart == len(s.recBufs) {
s.recBufsStart = 0
}
}
// recBuf is a buffer of records being produced to a partition and being
// drained by a sink. This is only not drained if the partition has a load
// error and thus does not a have a sink to be drained into.
type recBuf struct {
cl *Client // for cfg, record finishing
topic string
topicID [16]byte
partition int32
// The number of bytes we can buffer in a batch for this particular
// topic/partition. This may be less than the configured
// maxRecordBatchBytes because of produce request overhead.
maxRecordBatchBytes int32
// addedToTxn, for transactions only, signifies whether this partition
// has been added to the transaction yet or not.
addedToTxn atomic.Bool
// For LoadTopicPartitioner partitioning; atomically tracks the number
// of records buffered in total on this recBuf.
buffered atomic.Int64
mu sync.Mutex // guards r/w access to all fields below
// sink is who is currently draining us. This can be modified
// concurrently during a metadata update.
//
// The first set to a non-nil sink is done without a mutex.
//
// Since only metadata updates can change the sink, metadata updates
// also read this without a mutex.
sink *sink
// recBufsIdx is our index into our current sink's recBufs field.
// This exists to aid in removing the buffer from the sink.
recBufsIdx int
// A concurrent metadata update can move a recBuf from one sink to
// another while requests are inflight on the original sink. We do not
// want to allow new requests to start on the new sink until they all
// finish on the old, because with some pathological request order
// finishing, we would allow requests to finish out of order:
// handleSeqResps works per sink, not across sinks.
inflightOnSink *sink
// We only want to allow more than 1 inflight on a sink *if* we are
// currently receiving successful responses. Unimportantly, this allows
// us to save resources if the broker is having a problem or just
// recovered from one. Importantly, we work around an edge case in
// Kafka. Kafka will accept the first produce request for a pid/epoch
// with *any* sequence number. Say we sent two requests inflight. The
// first request Kafka replies to with NOT_LEADER_FOR_PARTITION, the
// second, the broker finished setting up and accepts. The broker now
// has the second request but not the first, we will retry both
// requests and receive OOOSN, and the broker has logs out of order.
// By only allowing more than one inflight if we have seen an ok
// response, we largely eliminate risk of this problem. See #223 for
// more details.
okOnSink bool
// Inflight tracks the number of requests inflight using batches from
// this recBuf. Every time this hits zero, if the batchDrainIdx is not
// at the end, we clear inflightOnSink and trigger the *current* sink
// to drain.
inflight uint8
lastAckedOffset int64 // last ProduceResponse's BaseOffset + how many records we produced
topicPartitionData // updated in metadata migrateProductionTo (same spot sink is updated)
// seq is used for the seq in each record batch. It is incremented when
// produce requests are made and can be reset on errors to batch0Seq.
//
// If idempotency is disabled, we just use "0" for the first sequence
// when encoding our payload.
//
// This is also used to check the first batch produced (disregarding
// seq resets) -- see handleRetryBatches.
seq int32
// batch0Seq is the seq of the batch at batchDrainIdx 0. If we reset
// the drain index, we reset seq with this number. If we successfully
// finish batch 0, we bump this.
batch0Seq int32
// If we need to reset sequence numbers, we set needSeqReset, and then
// when we use the **first** batch, we reset sequences to 0.
needSeqReset bool
// batches is our list of buffered records. Batches are appended as the
// final batch crosses size thresholds or as drain freezes batches from
// further modification.
//
// Most functions in a sink only operate on a batch if the batch is the
// first batch in a buffer. This is necessary to ensure that all
// records are truly finished without error in order.
batches []*recBatch
// batchDrainIdx is where the next batch will drain from. We only
// remove from the head of batches when a batch is finished.
// This is read while buffering and modified in a few places.
batchDrainIdx int
// If we fail with UNKNOWN_TOPIC_OR_PARTITION, we bump this and fail
// all records once this exceeds the config's unknown topic fail limit.
// If we ever see a different error (or no error), this is reset.
unknownFailures int64
// lingering is a timer that avoids starting maybeDrain until expiry,
// allowing for more records to be buffered in a single batch.
//
// Note that if something else starts a drain, if the first batch of
// this buffer fits into the request, it will be used.
//
// This is on recBuf rather than Sink to avoid some complicated
// interactions of triggering the sink to loop or not. Ideally, with
// the sticky partition hashers, we will only have a few partitions
// lingering and that this is on a RecBuf should not matter.
lingering *time.Timer
// failing is set when we encounter a temporary partition error during
// producing, such as UnknownTopicOrPartition (signifying the partition
// moved to a different broker).
//
// It is always cleared on metadata update.
failing bool
// Only possibly set in PurgeTopics, this is used to fail anything that
// was in the process of being buffered.
purged bool
}
// bufferRecord usually buffers a record, but does not if abortOnNewBatch is
// true and if this function would create a new batch.
//
// This returns whether the promised record was processed or not (buffered or
// immediately errored).
func (recBuf *recBuf) bufferRecord(pr promisedRec, abortOnNewBatch bool) bool {
recBuf.mu.Lock()
defer recBuf.mu.Unlock()
// We truncate to milliseconds to avoid some accumulated rounding error
// problems (see IBM/sarama#1455)
if pr.Timestamp.IsZero() {
pr.Timestamp = time.Now()
}
pr.Timestamp = pr.Timestamp.Truncate(time.Millisecond)
pr.Partition = recBuf.partition // set now, for the hook below
if recBuf.purged {
recBuf.cl.producer.promiseRecord(pr, errPurged)
return true
}
var (
mkNewBatch = true
produceVersion = recBuf.sink.produceVersion.Load()
)
if recBuf.batchDrainIdx != len(recBuf.batches) {
batch := recBuf.batches[len(recBuf.batches)-1]
appended, _ := batch.tryBuffer(pr, produceVersion, recBuf.maxRecordBatchBytes, false)
mkNewBatch = !appended
}
if mkNewBatch {
newBatch := recBuf.newRecordBatch()
appended, aborted := newBatch.tryBuffer(pr, produceVersion, recBuf.maxRecordBatchBytes, abortOnNewBatch)
switch {
case aborted: // not processed
recBuf.cl.prsPool.put(newBatch.records)
return false
case appended: // we return true below
default: // processed as failure
recBuf.cl.prsPool.put(newBatch.records)
recBuf.cl.producer.promiseRecord(pr, kerr.MessageTooLarge)
return true
}
recBuf.batches = append(recBuf.batches, newBatch)
}
recBuf.maybeTriggerDrain()
recBuf.buffered.Add(1)
if recBuf.cl.producer.hooks != nil && len(recBuf.cl.producer.hooks.partitioned) > 0 {
for _, h := range recBuf.cl.producer.hooks.partitioned {
h.OnProduceRecordPartitioned(pr.Record, recBuf.sink.nodeID)
}
}
return true
}
// Maybe drains, maybe starts a linger.
// Must be called while locked.
func (recBuf *recBuf) maybeTriggerDrain() {
if recBuf.checkIfShouldDrainOrStartLinger() {
recBuf.lockedStopLinger()
recBuf.sink.maybeDrain()
}
}
// Checks and returns if we should drain; if not, this potentially starts a
// linger timer.
func (recBuf *recBuf) checkIfShouldDrainOrStartLinger() bool {
nbufBatches := len(recBuf.batches) - recBuf.batchDrainIdx
return recBuf.cl.cfg.linger == 0 && nbufBatches > 0 || // no lingering, and any batch exists? drain
nbufBatches > 1 || // lingering, and more than one batch exists? drain -- one is full
nbufBatches == 1 && !recBuf.lockedMaybeLinger() // only one batch; if we cannot start lingering, we are being flushed or have too much buffered and have to drain
}
// Begins a linger timer unless the producer is being flushed.
func (recBuf *recBuf) lockedMaybeLinger() bool {
if recBuf.cl.producer.flushing.Load() > 0 || recBuf.cl.producer.blocked.Load() > 0 {
return false
}
if recBuf.lingering == nil {
recBuf.lingering = time.AfterFunc(recBuf.cl.cfg.linger, recBuf.unlingerAndManuallyDrain)
}
return true
}
func (recBuf *recBuf) lockedStopLinger() {
if recBuf.lingering != nil {
recBuf.lingering.Stop()
recBuf.lingering = nil
}
}
func (recBuf *recBuf) unlingerAndManuallyDrain() {
recBuf.mu.Lock()
defer recBuf.mu.Unlock()
recBuf.lockedStopLinger()
recBuf.sink.maybeDrain()
}
// bumpRepeatedLoadErr is provided to bump a buffer's number of consecutive
// load errors during metadata updates.
//
// Partition load errors are generally temporary (leader/listener/replica not
// available), and this try bump is not expected to do much. If for some reason
// a partition errors for a long time and we are not idempotent, this function
// drops all buffered records.
func (recBuf *recBuf) bumpRepeatedLoadErr(err error) {
recBuf.mu.Lock()
defer recBuf.mu.Unlock()
if len(recBuf.batches) == 0 {
return
}
batch0 := recBuf.batches[0]
// We need to lock the batch as well because there could be a buffered
// request about to be written. Writing requests only grabs the batch
// mu, not the recBuf mu.
batch0.mu.Lock()
batch0.tries++
var (
canFail = !recBuf.cl.idempotent() || batch0.canFailFromLoadErrs // we can only fail if we are not idempotent or if we have no outstanding requests
batch0Fail = batch0.maybeFailErr(&recBuf.cl.cfg) != nil // timeout, retries, or aborting
netErr = isRetryableBrokerErr(err) || isDialNonTimeoutErr(err) // we can fail if this is *not* a network error
retryableKerr = kerr.IsRetriable(err) // we fail if this is not a retryable kerr,
isUnknownLimit = recBuf.checkUnknownFailLimit(err) // or if it is, but it is UnknownTopicOrPartition and we are at our limit
willFail = canFail && (batch0Fail || !netErr && (!retryableKerr || retryableKerr && isUnknownLimit))
)
batch0.isFailingFromLoadErr = willFail
batch0.mu.Unlock()
recBuf.cl.cfg.logger.Log(LogLevelWarn, "produce partition load error, bumping error count on first stored batch",
"broker", logID(recBuf.sink.nodeID),
"topic", recBuf.topic,
"partition", recBuf.partition,
"err", err,
"can_fail", canFail,
"batch0_should_fail", batch0Fail,
"is_network_err", netErr,
"is_retryable_kerr", retryableKerr,
"is_unknown_limit", isUnknownLimit,
"will_fail", willFail,
)
if willFail {
recBuf.failAllRecords(err)
}
}
// Called locked, if err is an unknown error, bumps our limit, otherwise resets
// it. This returns if we have reached or exceeded the limit.
func (recBuf *recBuf) checkUnknownFailLimit(err error) bool {
if errors.Is(err, kerr.UnknownTopicOrPartition) {
recBuf.unknownFailures++
} else {
recBuf.unknownFailures = 0
}
return recBuf.cl.cfg.maxUnknownFailures >= 0 && recBuf.unknownFailures > recBuf.cl.cfg.maxUnknownFailures
}
// failAllRecords fails all buffered records in this recBuf.
// This is used anywhere where we have to fail and remove an entire batch,
// if we just removed the one batch, the seq num chain would be broken.
//
// - from fatal InitProducerID or AddPartitionsToTxn
// - from client closing
// - if not idempotent && hit retry / timeout limit
// - if batch fails fatally when producing
func (recBuf *recBuf) failAllRecords(err error) {
recBuf.lockedStopLinger()
for _, batch := range recBuf.batches {
// We need to guard our clearing of records against a
// concurrent produceRequest's write, which can have this batch
// buffered wile we are failing.
//
// We do not need to worry about concurrent recBuf
// modifications to this batch because the recBuf is already
// locked.
batch.mu.Lock()
records := batch.records
batch.records = nil
batch.mu.Unlock()
recBuf.cl.producer.promiseBatch(batchPromise{
recs: records,
err: err,
})
}
recBuf.resetBatchDrainIdx()
recBuf.buffered.Store(0)
recBuf.batches = nil
}
// clearFailing clears a buffer's failing state if it is failing.
//
// This is called when a buffer is added to a sink (to clear a failing state
// from migrating buffers between sinks) or when a metadata update sees the
// sink is still on the same source.
func (recBuf *recBuf) clearFailing() {
recBuf.mu.Lock()
defer recBuf.mu.Unlock()
recBuf.failing = false
recBuf.maybeTriggerDrain()
}
func (recBuf *recBuf) resetBatchDrainIdx() {
recBuf.seq = recBuf.batch0Seq
recBuf.batchDrainIdx = 0
}
// promisedRec ties a record with the callback that will be called once
// a batch is finally written and receives a response.
type promisedRec struct {
ctx context.Context
promise func(*Record, error)
*Record
}
func (pr promisedRec) cancelingCtx() context.Context {
if pr.ctx.Done() != nil {
return pr.ctx
}
if pr.Context.Done() != nil {
return pr.Context
}
return nil
}
// recBatch is the type used for buffering records before they are written.
type recBatch struct {
owner *recBuf // who owns us
tries int64 // how many times this batch has been sent, or should have been sent but requests leading up to it failed (metadata, add partitions to txn, etc)
// Once this batch is actually selected to be sent in a produce request,
// we freeze it. No more records can be added.
frozen bool
// We can only fail a batch if we have never issued it, or we have
// issued it and have received a response. If we do not receive a
// response, we cannot know whether we actually wrote bytes that Kafka
// processed or not. So, we set this to false every time we issue a
// request with this batch, and then reset it to true whenever we
// process a response.
canFailFromLoadErrs bool
// If we are going to fail the batch in bumpRepeatedLoadErr, we need to
// set this bool to true. There could be a concurrent request about to
// be written. See more comments below where this is used.
isFailingFromLoadErr bool
wireLength int32 // tracks total size this batch would currently encode as, including length prefix
v1wireLength int32 // same as wireLength, but for message set v1
attrs int16 // updated during apending; read and converted to RecordAttrs on success
firstTimestamp int64 // since unix epoch, in millis
maxTimestampDelta int64
mu sync.Mutex // guards appendTo's reading of records against failAllRecords emptying it
records []promisedRec // record w/ length, ts calculated
}
// Returns an error if the batch should fail.
func (b *recBatch) maybeFailErr(cfg *cfg) error {
if len(b.records) > 0 {
r0 := &b.records[0]
select {
case <-r0.ctx.Done():
return r0.ctx.Err()
case <-r0.Context.Done():
return r0.Context.Err()
default:
}
}
switch {
case b.isTimedOut(cfg.recordTimeout):
return ErrRecordTimeout
case b.tries > cfg.recordRetries:
return ErrRecordRetries
case b.owner.cl.producer.isAborting():
return ErrAborting
}
return nil
}
func (b *recBatch) v0wireLength() int32 { return b.v1wireLength - 8 } // no timestamp
func (b *recBatch) batchLength() int32 { return b.wireLength - 4 } // no length prefix
func (b *recBatch) flexibleWireLength() int32 { // uvarint length prefix
batchLength := b.batchLength()
return int32(kbin.UvarintLen(uvar32(batchLength))) + batchLength
}
// appendRecord saves a new record to a batch.
//
// This is called under the owning recBuf's mu, meaning records cannot be
// concurrently modified by failing. This batch cannot actively be used
// in a request, so we do not need to worry about a concurrent read.
func (b *recBatch) appendRecord(pr promisedRec, nums recordNumbers) {
b.wireLength += nums.wireLength()
b.v1wireLength += messageSet1Length(pr.Record)
if len(b.records) == 0 {
b.firstTimestamp = pr.Timestamp.UnixNano() / 1e6
} else if nums.tsDelta > b.maxTimestampDelta {
b.maxTimestampDelta = nums.tsDelta
}
b.records = append(b.records, pr)
}
// newRecordBatch returns a new record batch for a topic and partition.
func (recBuf *recBuf) newRecordBatch() *recBatch {
const recordBatchOverhead = 4 + // array len
8 + // firstOffset
4 + // batchLength
4 + // partitionLeaderEpoch
1 + // magic
4 + // crc
2 + // attributes
4 + // lastOffsetDelta
8 + // firstTimestamp
8 + // maxTimestamp
8 + // producerID
2 + // producerEpoch
4 + // seq
4 // record array length
return &recBatch{
owner: recBuf,
records: recBuf.cl.prsPool.get()[:0],
wireLength: recordBatchOverhead,
canFailFromLoadErrs: true, // until we send this batch, we can fail it
}
}
// prsPool is the one pool we have internally that is hard to expose an
// interface for. That said, ideally batch size is relatively consistent
// over time and using our own internal pool is fine enough.
type prsPool struct{ p *sync.Pool }
func newPrsPool() prsPool {
return prsPool{
p: &sync.Pool{New: func() any { r := make([]promisedRec, 10); return &r }},
}
}
func (p prsPool) get() []promisedRec { return (*p.p.Get().(*[]promisedRec))[:0] }
func (p prsPool) put(s []promisedRec) { p.p.Put(&s) }
// isOwnersFirstBatch returns if the batch in a recBatch is the first batch in
// a records. We only ever want to update batch / buffer logic if the batch is
// the first in the buffer.
func (b *recBatch) isOwnersFirstBatch() bool {
return len(b.owner.batches) > 0 && b.owner.batches[0] == b
}
// Returns whether the first record in a batch is past the limit.
func (b *recBatch) isTimedOut(limit time.Duration) bool {
if limit == 0 {
return false
}
return time.Since(b.records[0].Timestamp) > limit
}
// Decrements the inflight count for this batch.
//
// If the inflight count hits zero, this potentially re-triggers a drain on the
// *current* sink. A concurrent metadata update could have moved the recBuf to
// a different sink; that sink will not drain this recBuf until all requests on
// the old sink are finished.
//
// This is always called in the produce request path, not anywhere else (i.e.
// not failAllRecords). We want inflight decrementing to be the last thing that
// happens always for every request. It does not matter if the records were
// independently failed: from the request issuing perspective, the batch is
// still inflight.
func (b *recBatch) decInflight() {
recBuf := b.owner
recBuf.inflight--
if recBuf.inflight != 0 {
return
}
recBuf.inflightOnSink = nil
recBuf.maybeTriggerDrain()
}
////////////////////
// produceRequest //
////////////////////
// produceRequest is a kmsg.Request that is used when we want to
// flush our buffered records.
//
// It is the same as kmsg.ProduceRequest, but with a custom AppendTo.
type produceRequest struct {
version int16
can12 bool // we can send v12+ if we aren't using txns OR the broker has feature transaction.version >= 2
backoffSeq uint32
txnID *string
acks int16
timeout int32
batches seqRecBatches
firstCancelingCtx context.Context // of all batches added, the first one with a record that has a canceling context; only used with disableIdempotency
producerID int64
producerEpoch int16
// Initialized in AppendTo, metrics tracks uncompressed & compressed
// sizes (in byteS) of each batch.
//
// We use this in handleReqResp for the OnProduceHook.
metrics produceMetrics
hasHook bool
compressor Compressor
// wireLength is initially the size of sending a produce request,
// including the request header, with no topics. We start with the
// non-flexible size because it is strictly larger than flexible, but
// we use the proper flexible numbers when calculating.
wireLength int32
wireLengthLimit int32
}
type produceMetrics map[string]map[int32]ProduceBatchMetrics
func (p produceMetrics) hook(cfg *cfg, br *broker) {
if len(p) == 0 {
return
}
var hooks []HookProduceBatchWritten
cfg.hooks.each(func(h Hook) {
if h, ok := h.(HookProduceBatchWritten); ok {
hooks = append(hooks, h)
}
})
if len(hooks) == 0 {
return
}
go func() {
for _, h := range hooks {
for topic, partitions := range p {
for partition, metrics := range partitions {
h.OnProduceBatchWritten(br.meta, topic, partition, metrics)
}
}
}
}()
}
func (p *produceRequest) idempotent() bool { return p.producerID >= 0 }
func (p *produceRequest) tryAddBatch(produceVersion int32, recBuf *recBuf, batch *recBatch) bool {
batchWireLength, flexible, topicIDs := batch.wireLengthForProduceVersion(produceVersion)
batchWireLength += 4 // int32 partition prefix
if partitions, exists := p.batches.bs[recBuf.topic]; !exists {
if topicIDs {
batchWireLength += 16 + 1 // topic ID size, compact array len for 1 item (if we are using topic IDs, we are definitely flexible)
} else {
lt := int32(len(recBuf.topic))
if flexible {
batchWireLength += uvarlen(len(recBuf.topic)) + lt + 1 // compact string len, topic, compact array len for 1 item
} else {
batchWireLength += 2 + lt + 4 // string len, topic, partition array len
}
}
} else if flexible {
// If the topic exists and we are flexible, adding this
// partition may increase the length of our size prefix.
lastPartitionsLen := uvarlen(len(partitions))
newPartitionsLen := uvarlen(len(partitions) + 1)
batchWireLength += (newPartitionsLen - lastPartitionsLen)
}
// If we are flexible but do not know it yet, adding partitions may
// increase our length prefix. Since we are pessimistically assuming
// non-flexible, we have 200mil partitions to add before we have to
// worry about hitting 5 bytes vs. the non-flexible 4. We do not worry.
if p.wireLength+batchWireLength > p.wireLengthLimit {
return false
}
if recBuf.batches[0] == batch {
if !p.idempotent() || batch.canFailFromLoadErrs {
if err := batch.maybeFailErr(&batch.owner.cl.cfg); err != nil {
recBuf.failAllRecords(err)
return false
}
}
if recBuf.needSeqReset {
recBuf.needSeqReset = false
recBuf.seq = 0
recBuf.batch0Seq = 0
}
}
batch.frozen = true
p.wireLength += batchWireLength
p.batches.addBatch(
recBuf.topic,
recBuf.topicID,
recBuf.partition,
recBuf.seq,
batch,
)
return true
}
// seqRecBatch: a recBatch with a sequence number.
type seqRecBatch struct {
seq int32
*recBatch
}
type seqRecBatches struct {
bs map[string]map[int32]seqRecBatch
t2id map[string][16]byte
id2t map[[16]byte]string
}
func (rbs *seqRecBatches) addBatch(topic string, topicID [16]byte, part, seq int32, batch *recBatch) {
if rbs.bs == nil {
rbs.bs = make(map[string]map[int32]seqRecBatch)
rbs.t2id = make(map[string][16]byte)
rbs.id2t = make(map[[16]byte]string)
}
topicBatches, exists := rbs.bs[topic]
if !exists {
topicBatches = make(map[int32]seqRecBatch, 1)
rbs.bs[topic] = topicBatches
rbs.t2id[topic] = topicID
rbs.id2t[topicID] = topic
}
topicBatches[part] = seqRecBatch{seq, batch}
}
func (rbs *seqRecBatches) addSeqBatch(topic string, topicID [16]byte, part int32, batch seqRecBatch) {
if rbs.bs == nil {
rbs.bs = make(map[string]map[int32]seqRecBatch)
rbs.t2id = make(map[string][16]byte)
rbs.id2t = make(map[[16]byte]string)
}
topicBatches, exists := rbs.bs[topic]
if !exists {
topicBatches = make(map[int32]seqRecBatch, 1)
rbs.bs[topic] = topicBatches
rbs.t2id[topic] = topicID
rbs.id2t[topicID] = topic
}
topicBatches[part] = batch
}
func (rbs seqRecBatches) each(fn func(seqRecBatch)) {
for _, partitions := range rbs.bs {
for _, batch := range partitions {
fn(batch)
}
}
}
func (rbs seqRecBatches) eachOwnerLocked(fn func(seqRecBatch)) {
rbs.each(func(batch seqRecBatch) {
batch.owner.mu.Lock()
defer batch.owner.mu.Unlock()
fn(batch)
})
}
func (rbs seqRecBatches) sliced() recBatches {
var batches []*recBatch
for _, partitions := range rbs.bs {
for _, batch := range partitions {
batches = append(batches, batch.recBatch)
}
}
return batches
}
type recBatches []*recBatch
func (bs recBatches) eachOwnerLocked(fn func(*recBatch)) {
for _, b := range bs {
b.owner.mu.Lock()
fn(b)
b.owner.mu.Unlock()
}
}
//////////////
// COUNTING // - this section is all about counting how bytes lay out on the wire
//////////////
// Returns the non-flexible base produce request length (the request header and
// the request itself with no topics).
//
// See the large comment on maxRecordBatchBytesForTopic for why we always use
// non-flexible (in short: it is strictly larger).
func (cl *Client) baseProduceRequestLength() int32 {
const messageRequestOverhead int32 = 4 + // int32 length prefix
2 + // int16 key
2 + // int16 version
4 + // int32 correlation ID
2 // int16 client ID len (always non flexible)
// empty tag section skipped; see below
const produceRequestBaseOverhead int32 = 2 + // int16 transactional ID len (flexible or not, since we cap at 16382)
2 + // int16 acks
4 + // int32 timeout
4 // int32 topics non-flexible array length
// empty tag section skipped; see below
baseLength := messageRequestOverhead + produceRequestBaseOverhead
if cl.cfg.id != nil {
baseLength += int32(len(*cl.cfg.id))
}
if cl.cfg.txnID != nil {
baseLength += int32(len(*cl.cfg.txnID))
}
return baseLength
}
// Returns the maximum size a record batch can be for this given topic, such
// that if just a **single partition** is fully stuffed with records and we
// only encode that one partition, we will not overflow our configured limits.
//
// The maximum topic length is 249, which has a 2 byte prefix for flexible or
// non-flexible.
//
// Non-flexible versions will have a 4 byte length topic array prefix, a 4 byte
// length partition array prefix. and a 4 byte records array length prefix.
//
// Flexible versions would have a 1 byte length topic array prefix, a 1 byte
// length partition array prefix, up to 5 bytes for the records array length
// prefix, and three empty tag sections resulting in 3 bytes (produce request
// struct, topic struct, partition struct). As well, for the request header
// itself, we have an additional 1 byte tag section (that we currently keep
// empty).
//
// Thus in the worst case, we have 14 bytes of prefixes for non-flexible vs.
// 11 bytes for flexible. We default to the more limiting size: non-flexible.
func (cl *Client) maxRecordBatchBytesForTopic(topic string) int32 {
topicLen := 16 // topic ID length, if using topic IDs
if len(topic) > topicLen {
topicLen = len(topic)
}
minOnePartitionBatchLength := cl.baseProduceRequestLength() +
2 + // int16 topic string length prefix length
int32(topicLen) +
4 + // int32 partitions array length
4 + // partition int32 encoding length
4 // int32 record bytes array length
wireLengthLimit := cl.cfg.maxBrokerWriteBytes
recordBatchLimit := wireLengthLimit - minOnePartitionBatchLength
if cfgLimit := cl.cfg.maxRecordBatchBytes; cfgLimit < recordBatchLimit {
recordBatchLimit = cfgLimit
}
return recordBatchLimit
}
func messageSet0Length(r *Record) int32 {
const length = 4 + // array len
8 + // offset
4 + // size
4 + // crc
1 + // magic
1 + // attributes
4 + // key array bytes len
4 // value array bytes len
return length + int32(len(r.Key)) + int32(len(r.Value))
}
func messageSet1Length(r *Record) int32 {
return messageSet0Length(r) + 8 // timestamp
}
// Returns the numbers for a record if it were added to the record batch.
func (b *recBatch) calculateRecordNumbers(r *Record) recordNumbers {
tsMillis := r.Timestamp.UnixNano() / 1e6
tsDelta := tsMillis - b.firstTimestamp
// If this is to be the first record in the batch, then our timestamp
// delta is actually 0.
if len(b.records) == 0 {
tsDelta = 0
}
offsetDelta := int32(len(b.records)) // since called before adding record, delta is the current end
l := 1 + // attributes, int8 unused
kbin.VarlongLen(tsDelta) +
kbin.VarintLen(offsetDelta) +
kbin.VarintLen(int32(len(r.Key))) +
len(r.Key) +
kbin.VarintLen(int32(len(r.Value))) +
len(r.Value) +
kbin.VarintLen(int32(len(r.Headers))) // varint array len headers
for _, h := range r.Headers {
l += kbin.VarintLen(int32(len(h.Key))) +
len(h.Key) +
kbin.VarintLen(int32(len(h.Value))) +
len(h.Value)
}
return recordNumbers{
lengthField: int32(l),
tsDelta: tsDelta,
}
}
func uvar32(l int32) uint32 { return 1 + uint32(l) }
func uvarlen(l int) int32 { return int32(kbin.UvarintLen(uvar32(int32(l)))) }
// recordNumbers tracks a few numbers for a record that is buffered.
type recordNumbers struct {
lengthField int32 // the length field prefix of a record encoded on the wire
tsDelta int64 // the ms delta of when the record was added against the first timestamp
}
// wireLength is the wire length of a record including its length field prefix.
func (n recordNumbers) wireLength() int32 {
return int32(kbin.VarintLen(n.lengthField)) + n.lengthField
}
func (b *recBatch) wireLengthForProduceVersion(v int32) (batchWireLength int32, flexible, topicIDs bool) {
batchWireLength = b.wireLength
// If we do not yet know the produce version, we default to the largest
// size. Our request building sizes will always be an overestimate.
if v < 0 {
v1BatchWireLength := b.v1wireLength
if v1BatchWireLength > batchWireLength {
batchWireLength = v1BatchWireLength
}
flexibleBatchWireLength := b.flexibleWireLength()
if flexibleBatchWireLength > batchWireLength {
batchWireLength = flexibleBatchWireLength
}
} else {
switch v {
case 0, 1:
batchWireLength = b.v0wireLength()
case 2:
batchWireLength = b.v1wireLength
case 3, 4, 5, 6, 7, 8:
batchWireLength = b.wireLength
default:
batchWireLength = b.flexibleWireLength()
flexible = true
topicIDs = v >= 13
}
}
return batchWireLength, flexible, topicIDs
}
func (b *recBatch) tryBuffer(pr promisedRec, produceVersion, maxBatchBytes int32, abortOnNewBatch bool) (appended, aborted bool) {
nums := b.calculateRecordNumbers(pr.Record)
batchWireLength, _, _ := b.wireLengthForProduceVersion(produceVersion)
newBatchLength := batchWireLength + nums.wireLength()
if b.frozen || newBatchLength > maxBatchBytes {
return false, false
}
if abortOnNewBatch {
return false, true
}
b.appendRecord(pr, nums)
pr.setLengthAndTimestampDelta(
nums.lengthField,
nums.tsDelta,
)
return true, false
}
//////////////
// ENCODING // - this section is all about actually writing a produce request
//////////////
func (*produceRequest) Key() int16 { return 0 }
func (p *produceRequest) MaxVersion() int16 {
if !p.can12 {
return 11
}
return 13
}
func (p *produceRequest) SetVersion(v int16) { p.version = v }
func (p *produceRequest) GetVersion() int16 { return p.version }
func (p *produceRequest) IsFlexible() bool { return p.version >= 9 }
func (p *produceRequest) AppendTo(dst []byte) []byte {
flexible := p.IsFlexible()
if p.hasHook {
p.metrics = make(map[string]map[int32]ProduceBatchMetrics)
}
if p.version >= 3 {
if flexible {
dst = kbin.AppendCompactNullableString(dst, p.txnID)
} else {
dst = kbin.AppendNullableString(dst, p.txnID)
}
}
dst = kbin.AppendInt16(dst, p.acks)
dst = kbin.AppendInt32(dst, p.timeout)
if flexible {
dst = kbin.AppendCompactArrayLen(dst, len(p.batches.bs))
} else {
dst = kbin.AppendArrayLen(dst, len(p.batches.bs))
}
for topic, partitions := range p.batches.bs {
if p.version >= 13 {
id := p.batches.t2id[topic]
dst = append(dst, id[:]...)
dst = kbin.AppendCompactArrayLen(dst, len(partitions))
} else if flexible {
dst = kbin.AppendCompactString(dst, topic)
dst = kbin.AppendCompactArrayLen(dst, len(partitions))
} else {
dst = kbin.AppendString(dst, topic)
dst = kbin.AppendArrayLen(dst, len(partitions))
}
var tmetrics map[int32]ProduceBatchMetrics
if p.hasHook {
tmetrics = make(map[int32]ProduceBatchMetrics)
p.metrics[topic] = tmetrics
}
for partition, batch := range partitions {
dst = kbin.AppendInt32(dst, partition)
batch.mu.Lock()
if batch.records == nil || batch.isFailingFromLoadErr { // concurrent failAllRecords OR concurrent bumpRepeatedLoadErr
if flexible {
dst = kbin.AppendCompactNullableBytes(dst, nil)
} else {
dst = kbin.AppendNullableBytes(dst, nil)
}
batch.mu.Unlock()
continue
}
batch.canFailFromLoadErrs = false // we are going to write this batch: the response status is now unknown
batch.tries++
var pmetrics ProduceBatchMetrics
if p.version < 3 {
dst, pmetrics = batch.appendToAsMessageSet(dst, uint8(p.version), p.compressor)
} else {
dst, pmetrics = batch.appendTo(dst, p.version, p.producerID, p.producerEpoch, p.txnID != nil, p.compressor)
}
batch.mu.Unlock()
if p.hasHook {
tmetrics[partition] = pmetrics
}
if flexible {
dst = append(dst, 0)
}
}
if flexible {
dst = append(dst, 0)
}
}
if flexible {
dst = append(dst, 0)
}
return dst
}
func (*produceRequest) ReadFrom([]byte) error {
panic("unreachable -- the client never uses ReadFrom on its internal produceRequest")
}
func (p *produceRequest) ResponseKind() kmsg.Response {
r := kmsg.NewPtrProduceResponse()
r.Version = p.version
return r
}
func (b seqRecBatch) appendTo(
in []byte,
version int16,
producerID int64,
producerEpoch int16,
transactional bool,
compressor Compressor,
) (dst []byte, m ProduceBatchMetrics) { // named return so that our defer for flexible versions can modify it
flexible := version >= 9
dst = in
nullableBytesLen := b.wireLength - 4 // NULLABLE_BYTES leading length, minus itself
nullableBytesLenAt := len(dst) // in case compression adjusting
dst = kbin.AppendInt32(dst, nullableBytesLen)
// With flexible versions, the array length prefix can be anywhere from
// 1 byte long to 5 bytes long (covering up to 268MB).
//
// We have to add our initial understanding of the array length as a
// uvarint, but if compressing shrinks what that length would encode
// as, we have to shift everything down.
if flexible {
dst = dst[:nullableBytesLenAt]
batchLength := b.batchLength()
dst = kbin.AppendUvarint(dst, uvar32(batchLength)) // compact array non-null prefix
batchAt := len(dst)
defer func() {
batch := dst[batchAt:]
if int32(len(batch)) == batchLength { // we did not compress: simply return
return
}
// We *only* could have shrunk the batch bytes, so our
// append here will not overwrite anything we need to
// keep.
newDst := kbin.AppendUvarint(dst[:nullableBytesLenAt], uvar32(int32(len(batch))))
// If our append did not shorten the length prefix, we
// can just return the prior dst, otherwise we have to
// shift the batch itself down on newDst.
if len(newDst) != batchAt {
dst = append(newDst, batch...)
}
}()
}
// Below here, we append the actual record batch, which cannot be
// flexible. Everything encodes properly; flexible adjusting is done in
// the defer just above.
dst = kbin.AppendInt64(dst, 0) // firstOffset, defined as zero for producing
batchLen := nullableBytesLen - 8 - 4 // length of what follows this field (so, minus what came before and ourself)
batchLenAt := len(dst) // in case compression adjusting
dst = kbin.AppendInt32(dst, batchLen)
dst = kbin.AppendInt32(dst, -1) // partitionLeaderEpoch, unused in clients
dst = kbin.AppendInt8(dst, 2) // magic, defined as 2 for records v0.11.0+
crcStart := len(dst) // fill at end
dst = kbin.AppendInt32(dst, 0) // reserved crc
attrsAt := len(dst) // in case compression adjusting
b.attrs = 0
if transactional {
b.attrs |= 0x0010 // bit 5 is the "is transactional" bit
}
dst = kbin.AppendInt16(dst, b.attrs)
dst = kbin.AppendInt32(dst, int32(len(b.records)-1)) // lastOffsetDelta
dst = kbin.AppendInt64(dst, b.firstTimestamp)
dst = kbin.AppendInt64(dst, b.firstTimestamp+b.maxTimestampDelta)
seq := b.seq
if producerID < 0 { // a negative producer ID means we are not using idempotence
seq = 0
}
dst = kbin.AppendInt64(dst, producerID)
dst = kbin.AppendInt16(dst, producerEpoch)
dst = kbin.AppendInt32(dst, seq)
dst = kbin.AppendArrayLen(dst, len(b.records))
recordsAt := len(dst)
for i, pr := range b.records {
dst = pr.appendTo(dst, int32(i))
}
toCompress := dst[recordsAt:]
m.NumRecords = len(b.records)
m.UncompressedBytes = len(toCompress)
m.CompressedBytes = m.UncompressedBytes
if compressor != nil {
w := byteBuffers.Get().(*bytes.Buffer)
defer byteBuffers.Put(w)
w.Reset()
compressed, codec := compressor.Compress(w, toCompress, mkCompressFlags(version)...)
if compressed != nil && // nil would be from an error
len(compressed) < len(toCompress) {
// our compressed was shorter: copy over
copy(dst[recordsAt:], compressed)
dst = dst[:recordsAt+len(compressed)]
m.CompressedBytes = len(compressed)
m.CompressionType = uint8(codec)
// update the few record batch fields we already wrote
savings := int32(len(toCompress) - len(compressed))
nullableBytesLen -= savings
batchLen -= savings
b.attrs |= int16(codec)
if !flexible {
kbin.AppendInt32(dst[:nullableBytesLenAt], nullableBytesLen)
}
kbin.AppendInt32(dst[:batchLenAt], batchLen)
kbin.AppendInt16(dst[:attrsAt], b.attrs)
}
}
kbin.AppendInt32(dst[:crcStart], int32(crc32.Checksum(dst[crcStart+4:], crc32c)))
return dst, m
}
func (pr promisedRec) appendTo(dst []byte, offsetDelta int32) []byte {
length, tsDelta := pr.lengthAndTimestampDelta()
dst = kbin.AppendVarint(dst, length)
dst = kbin.AppendInt8(dst, 0) // attributes, currently unused
dst = kbin.AppendVarlong(dst, tsDelta)
dst = kbin.AppendVarint(dst, offsetDelta)
dst = kbin.AppendVarintBytes(dst, pr.Key)
dst = kbin.AppendVarintBytes(dst, pr.Value)
dst = kbin.AppendVarint(dst, int32(len(pr.Headers)))
for _, h := range pr.Headers {
dst = kbin.AppendVarintString(dst, h.Key)
dst = kbin.AppendVarintBytes(dst, h.Value)
}
return dst
}
func (b seqRecBatch) appendToAsMessageSet(dst []byte, version uint8, compressor Compressor) ([]byte, ProduceBatchMetrics) {
var m ProduceBatchMetrics
nullableBytesLenAt := len(dst)
dst = append(dst, 0, 0, 0, 0) // nullable bytes len
for i, pr := range b.records {
_, tsDelta := pr.lengthAndTimestampDelta()
dst = appendMessageTo(
dst,
version,
0,
int64(i),
b.firstTimestamp+tsDelta,
pr.Record,
)
}
b.attrs = 0
// Produce request v0 and v1 uses message set v0, which does not have
// timestamps. We set bit 8 in our attrs which corresponds with our own
// kgo.RecordAttrs's bit. The attrs field is unused in a sink / recBuf
// outside of the appending functions or finishing records; if we use
// more bits in our internal RecordAttrs, the below will need to
// change.
if version == 0 || version == 1 {
b.attrs |= 0b1000_0000
}
toCompress := dst[nullableBytesLenAt+4:] // skip nullable bytes leading prefix
m.NumRecords = len(b.records)
m.UncompressedBytes = len(toCompress)
m.CompressedBytes = m.UncompressedBytes
if compressor != nil {
w := byteBuffers.Get().(*bytes.Buffer)
defer byteBuffers.Put(w)
w.Reset()
compressed, codec := compressor.Compress(w, toCompress, mkCompressFlags(int16(version))...)
inner := &Record{Value: compressed}
wrappedLength := messageSet0Length(inner)
if version == 2 {
wrappedLength += 8 // timestamp
}
if compressed != nil && int(wrappedLength) < len(toCompress) {
m.CompressedBytes = int(wrappedLength)
m.CompressionType = uint8(codec)
b.attrs |= int16(codec)
dst = appendMessageTo(
dst[:nullableBytesLenAt+4],
version,
int8(codec),
int64(len(b.records)-1),
b.firstTimestamp,
inner,
)
}
}
kbin.AppendInt32(dst[:nullableBytesLenAt], int32(len(dst[nullableBytesLenAt+4:])))
return dst, m
}
func appendMessageTo(
dst []byte,
version uint8,
attributes int8,
offset int64,
timestamp int64,
r *Record,
) []byte {
magic := version >> 1
dst = kbin.AppendInt64(dst, offset)
msgSizeStart := len(dst)
dst = append(dst, 0, 0, 0, 0)
crc32Start := len(dst)
dst = append(dst,
0, 0, 0, 0,
magic,
byte(attributes))
if magic == 1 {
dst = kbin.AppendInt64(dst, timestamp)
}
dst = kbin.AppendNullableBytes(dst, r.Key)
dst = kbin.AppendNullableBytes(dst, r.Value)
kbin.AppendInt32(dst[:crc32Start], int32(crc32.ChecksumIEEE(dst[crc32Start+4:])))
kbin.AppendInt32(dst[:msgSizeStart], int32(len(dst[msgSizeStart+4:])))
return dst
}