Skip to content

Commit 1ed02eb

Browse files
committed
kgo: add support for KIP-951
This was more complicated than I was anticipating -- kgo is not designed to have partition movement *outside* of the metadata loop. Tested against producing to / consuming from kfake, moving a partition every second.
1 parent 95bed97 commit 1ed02eb

4 files changed

Lines changed: 403 additions & 77 deletions

File tree

‎pkg/kgo/metadata.go‎

Lines changed: 5 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -379,31 +379,8 @@ func (cl *Client) updateMetadata() (retryWhy multiUpdateWhy, err error) {
379379
}
380380
}
381381

382-
// Migrating a cursor requires stopping any consumer session. If we
383-
// stop a session, we need to eventually re-start any offset listing or
384-
// epoch loading that was stopped. Thus, we simply merge what we
385-
// stopped into what we will reload.
386-
var (
387-
consumerSessionStopped bool
388-
reloadOffsets listOrEpochLoads
389-
tpsPrior *topicsPartitions
390-
)
391-
stopConsumerSession := func() {
392-
if consumerSessionStopped {
393-
return
394-
}
395-
consumerSessionStopped = true
396-
loads, tps := cl.consumer.stopSession()
397-
reloadOffsets.mergeFrom(loads)
398-
tpsPrior = tps
399-
}
400-
defer func() {
401-
if consumerSessionStopped {
402-
session := cl.consumer.startNewSession(tpsPrior)
403-
defer session.decWorker()
404-
reloadOffsets.loadWithSession(session, "resuming reload offsets after session stopped for cursor migrating in metadata")
405-
}
406-
}()
382+
css := &consumerSessionStopper{cl: cl}
383+
defer css.maybeRestart()
407384

408385
var missingProduceTopics []*topicPartitions
409386
for _, m := range []struct {
@@ -426,8 +403,7 @@ func (cl *Client) updateMetadata() (retryWhy multiUpdateWhy, err error) {
426403
priorParts,
427404
newParts,
428405
m.isProduce,
429-
&reloadOffsets,
430-
stopConsumerSession,
406+
css,
431407
&retryWhy,
432408
)
433409
}
@@ -661,8 +637,7 @@ func (cl *Client) mergeTopicPartitions(
661637
l *topicPartitions,
662638
mt *metadataTopic,
663639
isProduce bool,
664-
reloadOffsets *listOrEpochLoads,
665-
stopConsumerSession func(),
640+
css *consumerSessionStopper,
666641
retryWhy *multiUpdateWhy,
667642
) {
668643
lv := *l.load() // copy so our field writes do not collide with reads
@@ -853,11 +828,7 @@ func (cl *Client) mergeTopicPartitions(
853828
if isProduce {
854829
oldTP.migrateProductionTo(newTP) // migration clears failing state
855830
} else {
856-
oldTP.migrateCursorTo(
857-
newTP,
858-
reloadOffsets,
859-
stopConsumerSession,
860-
)
831+
oldTP.migrateCursorTo(newTP, css)
861832
}
862833
}
863834
}

‎pkg/kgo/sink.go‎

Lines changed: 64 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -538,7 +538,7 @@ func (s *sink) handleReqClientErr(req *produceRequest, err error) {
538538
if updateMeta {
539539
s.cl.cfg.logger.Log(LogLevelInfo, "produce request failed, triggering metadata update", "broker", logID(s.nodeID), "err", err)
540540
}
541-
s.handleRetryBatches(req.batches, req.backoffSeq, updateMeta, false, "failed produce request triggered metadata update")
541+
s.handleRetryBatches(req.batches, nil, req.backoffSeq, updateMeta, false, "failed produce request triggered metadata update")
542542

543543
case errors.Is(err, ErrClientClosed):
544544
s.cl.failBufferedRecords(ErrClientClosed)
@@ -601,11 +601,13 @@ func (s *sink) handleReqResp(br *broker, req *produceRequest, resp kmsg.Response
601601
return
602602
}
603603

604+
var kmove kip951move
604605
var reqRetry seqRecBatches // handled at the end
605606

606-
pr := resp.(*kmsg.ProduceResponse)
607-
for _, rTopic := range pr.Topics {
608-
topic := rTopic.Topic
607+
kresp := resp.(*kmsg.ProduceResponse)
608+
for i := range kresp.Topics {
609+
rt := &kresp.Topics[i]
610+
topic := rt.Topic
609611
partitions, ok := req.batches[topic]
610612
if !ok {
611613
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)
@@ -618,25 +620,26 @@ func (s *sink) handleReqResp(br *broker, req *produceRequest, resp kmsg.Response
618620
}
619621

620622
tmetrics := req.metrics[topic]
621-
for _, rPartition := range rTopic.Partitions {
622-
partition := rPartition.Partition
623+
for j := range rt.Partitions {
624+
rp := &rt.Partitions[j]
625+
partition := rp.Partition
623626
batch, ok := partitions[partition]
624627
if !ok {
625-
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", rTopic.Topic, "partition", partition)
628+
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)
626629
delete(tmetrics, partition)
627630
continue // should not hit this
628631
}
629632
delete(partitions, partition)
630633

631634
retry, didProduce := s.handleReqRespBatch(
632635
b,
636+
&kmove,
637+
kresp,
633638
topic,
634-
partition,
639+
rp,
635640
batch,
636641
req.producerID,
637642
req.producerEpoch,
638-
rPartition.BaseOffset,
639-
rPartition.ErrorCode,
640643
)
641644
if retry {
642645
reqRetry.addSeqBatch(topic, partition, batch)
@@ -660,22 +663,22 @@ func (s *sink) handleReqResp(br *broker, req *produceRequest, resp kmsg.Response
660663

661664
if len(req.batches) > 0 {
662665
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))
663-
s.handleRetryBatches(req.batches, 0, true, false, "broker did not reply to all topics in produce request")
666+
s.handleRetryBatches(req.batches, nil, 0, true, false, "broker did not reply to all topics in produce request")
664667
}
665668
if len(reqRetry) > 0 {
666-
s.handleRetryBatches(reqRetry, 0, true, true, "produce request had retry batches")
669+
s.handleRetryBatches(reqRetry, &kmove, 0, true, true, "produce request had retry batches")
667670
}
668671
}
669672

670673
func (s *sink) handleReqRespBatch(
671674
b *bytes.Buffer,
675+
kmove *kip951move,
676+
resp *kmsg.ProduceResponse,
672677
topic string,
673-
partition int32,
678+
rp *kmsg.ProduceResponseTopicPartition,
674679
batch seqRecBatch,
675680
producerID int64,
676681
producerEpoch int16,
677-
baseOffset int64,
678-
errorCode int16,
679682
) (retry, didProduce bool) {
680683
batch.owner.mu.Lock()
681684
defer batch.owner.mu.Unlock()
@@ -684,25 +687,25 @@ func (s *sink) handleReqRespBatch(
684687

685688
debug := b != nil
686689
if debug {
687-
fmt.Fprintf(b, "%d{", partition)
690+
fmt.Fprintf(b, "%d{", rp.Partition)
688691
}
689692

690693
// We only ever operate on the first batch in a record buf. Batches
691694
// work sequentially; if this is not the first batch then an error
692695
// happened and this later batch is no longer a part of a seq chain.
693696
if !batch.isOwnersFirstBatch() {
694697
if debug {
695-
if err := kerr.ErrorForCode(errorCode); err == nil {
698+
if err := kerr.ErrorForCode(rp.ErrorCode); err == nil {
696699
if nrec > 0 {
697-
fmt.Fprintf(b, "skipped@%d=>%d}, ", baseOffset, baseOffset+int64(nrec))
700+
fmt.Fprintf(b, "skipped@%d=>%d}, ", rp.BaseOffset, rp.BaseOffset+int64(nrec))
698701
} else {
699-
fmt.Fprintf(b, "skipped@%d}, ", baseOffset)
702+
fmt.Fprintf(b, "skipped@%d}, ", rp.BaseOffset)
700703
}
701704
} else {
702705
if nrec > 0 {
703-
fmt.Fprintf(b, "skipped@%d,%d(%s)}, ", baseOffset, nrec, err)
706+
fmt.Fprintf(b, "skipped@%d,%d(%s)}, ", rp.BaseOffset, nrec, err)
704707
} else {
705-
fmt.Fprintf(b, "skipped@%d(%s)}, ", baseOffset, err)
708+
fmt.Fprintf(b, "skipped@%d(%s)}, ", rp.BaseOffset, err)
706709
}
707710
}
708711
}
@@ -713,7 +716,17 @@ func (s *sink) handleReqRespBatch(
713716
// at this point re-enable failing from load errors.
714717
batch.canFailFromLoadErrs = true
715718

716-
err := kerr.ErrorForCode(errorCode)
719+
// By default, we assume we errored. Non-error updates this back
720+
// to true.
721+
batch.owner.okOnSink = false
722+
723+
if moving := kmove.maybeAddProducePartition(resp, rp, batch.owner); moving {
724+
fmt.Fprintf(b, "move:%d:%d@%d,%d}, ", rp.CurrentLeader.LeaderID, rp.CurrentLeader.LeaderEpoch, rp.BaseOffset, nrec)
725+
batch.owner.failing = true
726+
return true, false
727+
}
728+
729+
err := kerr.ErrorForCode(rp.ErrorCode)
717730
failUnknown := batch.owner.checkUnknownFailLimit(err)
718731
switch {
719732
case kerr.IsRetriable(err) &&
@@ -722,7 +735,7 @@ func (s *sink) handleReqRespBatch(
722735
batch.tries < s.cl.cfg.recordRetries:
723736

724737
if debug {
725-
fmt.Fprintf(b, "retrying@%d,%d(%s)}, ", baseOffset, nrec, err)
738+
fmt.Fprintf(b, "retrying@%d,%d(%s)}, ", rp.BaseOffset, nrec, err)
726739
}
727740
return true, false
728741

@@ -769,21 +782,21 @@ func (s *sink) handleReqRespBatch(
769782
s.cl.cfg.logger.Log(LogLevelInfo, "batch errored, failing the producer ID",
770783
"broker", logID(s.nodeID),
771784
"topic", topic,
772-
"partition", partition,
785+
"partition", rp.Partition,
773786
"producer_id", producerID,
774787
"producer_epoch", producerEpoch,
775788
"err", err,
776789
)
777790
s.cl.failProducerID(producerID, producerEpoch, err)
778791

779-
s.cl.finishBatch(batch.recBatch, producerID, producerEpoch, partition, baseOffset, err)
792+
s.cl.finishBatch(batch.recBatch, producerID, producerEpoch, rp.Partition, rp.BaseOffset, err)
780793
if debug {
781-
fmt.Fprintf(b, "fatal@%d,%d(%s)}, ", baseOffset, nrec, err)
794+
fmt.Fprintf(b, "fatal@%d,%d(%s)}, ", rp.BaseOffset, nrec, err)
782795
}
783796
return false, false
784797
}
785798
if s.cl.cfg.onDataLoss != nil {
786-
s.cl.cfg.onDataLoss(topic, partition)
799+
s.cl.cfg.onDataLoss(topic, rp.Partition)
787800
}
788801

789802
// For OOOSN, and UnknownProducerID
@@ -799,7 +812,7 @@ func (s *sink) handleReqRespBatch(
799812
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),
800813
"broker", logID(s.nodeID),
801814
"topic", topic,
802-
"partition", partition,
815+
"partition", rp.Partition,
803816
"producer_id", producerID,
804817
"producer_epoch", producerEpoch,
805818
"err", err,
@@ -811,15 +824,15 @@ func (s *sink) handleReqRespBatch(
811824
// will reset sequence numbers appropriately.
812825
s.cl.failProducerID(producerID, producerEpoch, errReloadProducerID)
813826
if debug {
814-
fmt.Fprintf(b, "resetting@%d,%d(%s)}, ", baseOffset, nrec, err)
827+
fmt.Fprintf(b, "resetting@%d,%d(%s)}, ", rp.BaseOffset, nrec, err)
815828
}
816829
return true, false
817830

818831
case err == kerr.DuplicateSequenceNumber: // ignorable, but we should not get
819832
s.cl.cfg.logger.Log(LogLevelInfo, "received unexpected duplicate sequence number, ignoring and treating batch as successful",
820833
"broker", logID(s.nodeID),
821834
"topic", topic,
822-
"partition", partition,
835+
"partition", rp.Partition,
823836
)
824837
err = nil
825838
fallthrough
@@ -828,22 +841,21 @@ func (s *sink) handleReqRespBatch(
828841
s.cl.cfg.logger.Log(LogLevelInfo, "batch in a produce request failed",
829842
"broker", logID(s.nodeID),
830843
"topic", topic,
831-
"partition", partition,
844+
"partition", rp.Partition,
832845
"err", err,
833846
"err_is_retryable", kerr.IsRetriable(err),
834847
"max_retries_reached", !failUnknown && batch.tries >= s.cl.cfg.recordRetries,
835848
)
836-
batch.owner.okOnSink = false
837849
} else {
838850
batch.owner.okOnSink = true
839851
}
840-
s.cl.finishBatch(batch.recBatch, producerID, producerEpoch, partition, baseOffset, err)
852+
s.cl.finishBatch(batch.recBatch, producerID, producerEpoch, rp.Partition, rp.BaseOffset, err)
841853
didProduce = err == nil
842854
if debug {
843855
if err != nil {
844-
fmt.Fprintf(b, "err@%d,%d(%s)}, ", baseOffset, nrec, err)
856+
fmt.Fprintf(b, "err@%d,%d(%s)}, ", rp.BaseOffset, nrec, err)
845857
} else {
846-
fmt.Fprintf(b, "%d=>%d}, ", baseOffset, baseOffset+int64(nrec))
858+
fmt.Fprintf(b, "%d=>%d}, ", rp.BaseOffset, rp.BaseOffset+int64(nrec))
847859
}
848860
}
849861
}
@@ -902,6 +914,7 @@ func (cl *Client) finishBatch(batch *recBatch, producerID int64, producerEpoch i
902914
// we fail it and anything after it.
903915
func (s *sink) handleRetryBatches(
904916
retry seqRecBatches,
917+
kmove *kip951move,
905918
backoffSeq uint32,
906919
updateMeta bool, // if we should maybe update the metadata
907920
canFail bool, // if records can fail if they are at limits
@@ -911,7 +924,12 @@ func (s *sink) handleRetryBatches(
911924
debug := logger.Level() >= LogLevelDebug
912925
var needsMetaUpdate bool
913926
var shouldBackoff bool
927+
if kmove != nil {
928+
defer kmove.maybeBeginMove(s.cl)
929+
}
930+
var numRetryBatches, numMoveBatches int
914931
retry.eachOwnerLocked(func(batch seqRecBatch) {
932+
numRetryBatches++
915933
if !batch.isOwnersFirstBatch() {
916934
if debug {
917935
logger.Log(LogLevelDebug, "retry batch is not the first batch in the owner, skipping result",
@@ -931,6 +949,14 @@ func (s *sink) handleRetryBatches(
931949

932950
batch.owner.resetBatchDrainIdx()
933951

952+
// Now that the batch drain index is reset, if this retry is
953+
// caused from a moving batch, return early. We do not need
954+
// to backoff nor do we need to trigger a metadata update.
955+
if kmove.hasRecBuf(batch.owner) {
956+
numMoveBatches++
957+
return
958+
}
959+
934960
// If our first batch (seq == 0) fails with unknown topic, we
935961
// retry immediately. Kafka can reply with valid metadata
936962
// immediately after a topic was created, before the leaders
@@ -975,7 +1001,7 @@ func (s *sink) handleRetryBatches(
9751001
// If neither of these cases are true, then we entered wanting a
9761002
// metadata update, but the batches either were not the first batch, or
9771003
// the batches were concurrently failed.
978-
if shouldBackoff || !updateMeta {
1004+
if shouldBackoff || (!updateMeta && numRetryBatches != numMoveBatches) {
9791005
s.maybeTriggerBackoff(backoffSeq)
9801006
s.maybeDrain()
9811007
}
@@ -1047,7 +1073,7 @@ type recBuf struct {
10471073
recBufsIdx int
10481074

10491075
// A concurrent metadata update can move a recBuf from one sink to
1050-
// another wile requests are inflight on the original sink. We do not
1076+
// another while requests are inflight on the original sink. We do not
10511077
// want to allow new requests to start on the new sink until they all
10521078
// finish on the old, because with some pathological request order
10531079
// finishing, we would allow requests to finish out of order:
@@ -1885,7 +1911,7 @@ func (b *recBatch) tryBuffer(pr promisedRec, produceVersion, maxBatchBytes int32
18851911
//////////////
18861912

18871913
func (*produceRequest) Key() int16 { return 0 }
1888-
func (*produceRequest) MaxVersion() int16 { return 9 }
1914+
func (*produceRequest) MaxVersion() int16 { return 10 }
18891915
func (p *produceRequest) SetVersion(v int16) { p.version = v }
18901916
func (p *produceRequest) GetVersion() int16 { return p.version }
18911917
func (p *produceRequest) IsFlexible() bool { return p.version >= 9 }

‎pkg/kgo/source.go‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -811,6 +811,11 @@ func (s *source) fetch(consumerSession *consumerSession, doneFetch chan<- struct
811811
}
812812

813813
// The logic below here should be relatively quick.
814+
//
815+
// Note that fetch runs entirely in the context of a consumer session.
816+
// loopFetch does not return until this function does, meaning we
817+
// cannot concurrently issue a second fetch for partitions that are
818+
// being processed below.
814819

815820
deleteReqUsedOffset := func(topic string, partition int32) {
816821
t := req.usedOffsets[topic]
@@ -937,7 +942,9 @@ func (s *source) handleReqResp(br *broker, req *fetchRequest, resp *kmsg.FetchRe
937942
debugWhyStripped multiUpdateWhy
938943
numErrsStripped int
939944
kip320 = s.cl.supportsOffsetForLeaderEpoch()
945+
kmove kip951move
940946
)
947+
defer kmove.maybeBeginMove(s.cl)
941948

942949
strip := func(t string, p int32, err error) {
943950
numErrsStripped++
@@ -998,6 +1005,10 @@ func (s *source) handleReqResp(br *broker, req *fetchRequest, resp *kmsg.FetchRe
9981005

9991006
fp := partOffset.processRespPartition(br, rp, s.cl.decompressor, s.cl.cfg.hooks)
10001007
if fp.Err != nil {
1008+
if moving := kmove.maybeAddFetchPartition(resp, rp, partOffset.from); moving {
1009+
strip(topic, partition, fp.Err)
1010+
continue
1011+
}
10011012
updateWhy.add(topic, partition, fp.Err)
10021013
}
10031014

@@ -2026,7 +2037,7 @@ func (f *fetchRequest) MaxVersion() int16 {
20262037
if f.disableIDs || f.session.disableIDs {
20272038
return 12
20282039
}
2029-
return 15
2040+
return 16
20302041
}
20312042
func (f *fetchRequest) SetVersion(v int16) { f.version = v }
20322043
func (f *fetchRequest) GetVersion() int16 { return f.version }

0 commit comments

Comments
 (0)