Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
b701889
add the first version franz implementation
3AceShowHand Jan 9, 2026
2d85ff7
fix the franz
3AceShowHand Feb 7, 2026
0945129
fix the franz
3AceShowHand Feb 7, 2026
539dd0c
kafka: default sink client to franz
3AceShowHand Feb 7, 2026
d156ca0
Merge branch 'master' into franz-go-kafka
3AceShowHand Feb 7, 2026
2c341dc
add more code
3AceShowHand Feb 9, 2026
58208ec
add more code
3AceShowHand Feb 9, 2026
5eede91
adjust monitoring
3AceShowHand Feb 10, 2026
91a9263
adjust code
3AceShowHand Feb 10, 2026
eb1ff68
fix require ack configuration
3AceShowHand Feb 10, 2026
aa98943
fix unit test
3AceShowHand Feb 10, 2026
6e39cdd
fix more code
3AceShowHand Feb 11, 2026
a3e5dd5
merge master
3AceShowHand Apr 17, 2026
f9fec57
Merge branch 'master' into franz-go-kafka
3AceShowHand Jun 26, 2026
42a6a6e
remove sarama, add franz-go
3AceShowHand Jun 26, 2026
d238110
remove keyworkd franz and prometheus
3AceShowHand Jun 26, 2026
0dd9a73
make fmt
3AceShowHand Jun 26, 2026
da5dad6
fix the code
3AceShowHand Jun 30, 2026
da278d8
remove the overhead
3AceShowHand Jul 2, 2026
3e04dac
try to fix code
3AceShowHand Jul 3, 2026
840b21d
yagni the code
3AceShowHand Jul 3, 2026
b22e572
adjust the kafka compression case
3AceShowHand Jul 3, 2026
d0e6cbc
fix a lot of code
3AceShowHand Jul 3, 2026
2d559bc
add golangci
3AceShowHand Jul 3, 2026
0751a11
Merge branch 'master' into franz-go-kafka
3AceShowHand Jul 3, 2026
32190aa
remove design doc
3AceShowHand Jul 7, 2026
28de049
adjust the code
3AceShowHand Jul 7, 2026
adc3742
simplify the metrics hook
3AceShowHand Jul 7, 2026
f7a2d22
move sasl to the kafka package
3AceShowHand Jul 7, 2026
92480ab
simplify more code
3AceShowHand Jul 7, 2026
42de85f
fix it
3AceShowHand Jul 7, 2026
3bdb07b
simplify more code
3AceShowHand Jul 7, 2026
f681f09
simplify more code
3AceShowHand Jul 7, 2026
1edbc65
fix code
3AceShowHand Jul 7, 2026
69601f4
simplify more code
3AceShowHand Jul 7, 2026
c7bc559
Merge branch 'master' into franz-go-kafka
3AceShowHand Jul 10, 2026
c61f156
Merge branch 'master' into franz-go-kafka
3AceShowHand Jul 24, 2026
6f4796d
simplify the timeout
3AceShowHand Jul 27, 2026
2f2e965
adjust file layout
3AceShowHand Jul 27, 2026
e6e2e9d
Merge branch 'master' into franz-go-kafka
3AceShowHand Jul 27, 2026
7a2451c
fix more code
3AceShowHand Jul 28, 2026
6f68f46
simplify the code further
3AceShowHand Jul 28, 2026
dd5e1bf
Merge branch 'master' into franz-go-kafka
3AceShowHand Jul 28, 2026
f0b3192
simplify the code further
3AceShowHand Jul 28, 2026
1a2b4f0
revert some changes
3AceShowHand Jul 29, 2026
e5116ba
Merge branch 'master' into franz-go-kafka
3AceShowHand Jul 31, 2026
28d6dc2
Merge branch 'master' into franz-go-kafka
3AceShowHand Aug 11, 2026
0fa34b7
Merge branch 'master' into franz-go-kafka
3AceShowHand Aug 14, 2026
cfc6a3f
restore the sarama
3AceShowHand Aug 14, 2026
9309281
add franz package and monitoring
3AceShowHand Aug 14, 2026
1119fee
add more metrics
3AceShowHand Aug 17, 2026
9e8dc8b
Merge branch 'master' into franz-go-kafka
3AceShowHand Aug 21, 2026
f8e352c
add kafka sasl oauth ca
3AceShowHand Sep 1, 2026
cbe8a31
kafka: simplify OAuth CA handling and tests
3AceShowHand Sep 1, 2026
f601dec
Merge branch 'master' into franz-go-kafka
3AceShowHand Sep 1, 2026
ccdb03e
Merge commit 'cbe8a31aa881a402a3a28c5cbd02bfd936dbcbad' into franz-go…
3AceShowHand Sep 1, 2026
31d4137
kafka: fix franz security authentication
3AceShowHand Sep 1, 2026
62a7abd
Merge branch 'master' into franz-go-kafka
3AceShowHand Sep 2, 2026
5cbe3b3
kafka: classify franz-go admin errors
3AceShowHand Sep 2, 2026
3c74dd9
metrics: split franz-go request and response rates
3AceShowHand Sep 2, 2026
526da70
kafka: simplify franz-go client implementation
3AceShowHand Sep 2, 2026
8bfc31c
kafka: pass context to sync producer
3AceShowHand Sep 2, 2026
ce78340
kafka: simplify franz-go factory configuration
3AceShowHand Sep 2, 2026
122d36f
kafka: inline franz producer construction
3AceShowHand Sep 2, 2026
a645be1
fix
3AceShowHand Sep 2, 2026
1c8dc27
kafka: refine franz producer limits and timeouts
3AceShowHand Sep 3, 2026
7fc73be
add all code
3AceShowHand Sep 3, 2026
da6a1c4
fix more code
3AceShowHand Sep 3, 2026
9046a30
fix more code
3AceShowHand Sep 3, 2026
f6872ae
fix more code
3AceShowHand Sep 3, 2026
034885e
kafka: refine franz-go producer lifecycle
3AceShowHand Sep 3, 2026
01f445e
kafka: reuse kerberos client per franz client
3AceShowHand Sep 3, 2026
45d82da
fix all code
3AceShowHand Sep 3, 2026
5686584
docs: remove branch-local documents
3AceShowHand Sep 3, 2026
1fc9658
kafka: report canceled franz async sends
3AceShowHand Sep 4, 2026
c6ec5d1
fix
3AceShowHand Sep 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion downstreamadapter/sink/kafka/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ func (c components) close() {
if c.claimCheck != nil {
c.claimCheck.Close()
}
if c.factory != nil {
c.factory.Close()
}
}

func newKafkaSinkComponent(
Expand Down Expand Up @@ -85,7 +88,7 @@ func newKafkaSinkComponent(
}
options.Topic = topic

comp.factory, err = kafka.NewSaramaFactory(ctx, options, changefeedID)
comp.factory, err = kafka.NewFactory(ctx, options, changefeedID)
if err != nil {
return comp, protocol, err
}
Expand All @@ -109,6 +112,9 @@ func newKafkaSinkComponent(
if err != nil {
return comp, protocol, err
}
if options.Client == kafka.KafkaClientFranz {
encoderConfig.WithKafkaRecordBatchSize()
}

comp.claimCheck, err = claimcheck.New(ctx, encoderConfig.LargeMessageHandle, changefeedID)
if err != nil {
Expand Down
21 changes: 9 additions & 12 deletions downstreamadapter/sink/kafka/sink.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,6 @@ func (s *sink) SinkType() common.SinkType {
return common.KafkaSinkType
}

var createKafkaFactory = func(createSaramaFactory func() (kafka.Factory, error)) (kafka.Factory, error) {
return createSaramaFactory()
}

func Verify(ctx context.Context, changefeedID common.ChangeFeedID, uri *url.URL, sinkConfig *config.SinkConfig) error {
protocol, err := helper.GetProtocol(util.GetOrZero(sinkConfig.Protocol))
if err != nil {
Expand All @@ -100,6 +96,9 @@ func Verify(ctx context.Context, changefeedID common.ChangeFeedID, uri *url.URL,
if err != nil {
return err
}
if options.Client == kafka.KafkaClientFranz {
encoderConfig.WithKafkaRecordBatchSize()
}

claimCheck, err := claimcheck.New(ctx, encoderConfig.LargeMessageHandle, changefeedID)
if err != nil {
Expand All @@ -116,12 +115,11 @@ func Verify(ctx context.Context, changefeedID common.ChangeFeedID, uri *url.URL,
return err
}

factory, err := createKafkaFactory(func() (kafka.Factory, error) {
return kafka.NewSaramaFactory(ctx, options, changefeedID)
})
factory, err := kafka.NewFactory(ctx, options, changefeedID)
if err != nil {
return err
}
defer factory.Close()

adminClient, err := factory.AdminClient(ctx)
if err != nil {
Expand Down Expand Up @@ -427,7 +425,6 @@ func (s *sink) sendMessages(ctx context.Context) error {
for _, message := range future.Messages {
start := time.Now()
if err = s.statistics.RecordBatchExecution(func() (int, int64, error) {
message.SetPartitionKey(future.Key.PartitionKey)
if err = s.dmlProducer.AsyncSend(
ctx,
future.Key.Topic,
Expand Down Expand Up @@ -471,11 +468,11 @@ func (s *sink) sendDDLEvent(event *commonEvent.DDLEvent) error {
ddlType := e.GetDDLType().String()
if s.partitionRule == helper.PartitionAll {
err = s.statistics.RecordDDLExecution(func() (string, error) {
return ddlType, s.ddlProducer.SendMessages(topic, partitionNum, message)
return ddlType, s.ddlProducer.SendMessages(s.ctx, topic, partitionNum, message)
})
} else {
err = s.statistics.RecordDDLExecution(func() (string, error) {
return ddlType, s.ddlProducer.SendMessage(topic, 0, message)
return ddlType, s.ddlProducer.SendMessage(s.ctx, topic, 0, message)
})
}
if err != nil {
Expand Down Expand Up @@ -541,7 +538,7 @@ func (s *sink) sendCheckpoint(ctx context.Context) error {
if err != nil {
return err
}
err = s.ddlProducer.SendMessages(topic, partitionNum, msg)
err = s.ddlProducer.SendMessages(ctx, topic, partitionNum, msg)
if err != nil {
return err
}
Expand All @@ -552,7 +549,7 @@ func (s *sink) sendCheckpoint(ctx context.Context) error {
if err != nil {
return err
}
err = s.ddlProducer.SendMessages(topic, partitionNum, msg)
err = s.ddlProducer.SendMessages(ctx, topic, partitionNum, msg)
if err != nil {
return err
}
Expand Down
50 changes: 20 additions & 30 deletions downstreamadapter/sink/kafka/sink_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
"github.com/pingcap/ticdc/pkg/sink/kafka"
"github.com/pingcap/tidb/pkg/meta/model"
"github.com/stretchr/testify/require"
"github.com/twmb/franz-go/pkg/kfake"
"go.uber.org/atomic"
)

Expand Down Expand Up @@ -91,6 +92,9 @@ func TestSinkWorkersReturnContextError(t *testing.T) {
}

func TestVerifyInvalidConfig(t *testing.T) {
cluster := kfake.MustCluster(kfake.NumBrokers(1), kfake.SeedTopics(1, kafkaSinkTestTopic))
defer cluster.Close()

schemaRegistry := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
http.Error(w, "invalid response", http.StatusInternalServerError)
}))
Expand All @@ -101,28 +105,10 @@ func TestVerifyInvalidConfig(t *testing.T) {
Protocol: &avroProtocol,
SchemaRegistry: &schemaRegistry.URL,
}
sinkURI, err := url.Parse("kafka://127.0.0.1:9092/" + kafkaSinkTestTopic +
sinkURI, err := url.Parse("kafka://" + cluster.ListenAddrs()[0] + "/" + kafkaSinkTestTopic +
"?required-acks=1&kafka-version=2.4.0")
require.NoError(t, err)

ctrl := gomock.NewController(t)
adminClient := kafka.NewMockAdminClient(ctrl)
factory := kafka.NewMockFactory(ctrl)
gomock.InOrder(
factory.EXPECT().AdminClient(gomock.Any()).Return(adminClient, nil),
adminClient.EXPECT().GetTopicsMeta([]string{kafkaSinkTestTopic}, false).Return(
map[string]kafka.TopicDetail{kafkaSinkTestTopic: {Name: kafkaSinkTestTopic}}, nil),
adminClient.EXPECT().Close(),
)

originalCreateKafkaFactory := createKafkaFactory
createKafkaFactory = func(_ func() (kafka.Factory, error)) (kafka.Factory, error) {
return factory, nil
}
t.Cleanup(func() {
createKafkaFactory = originalCreateKafkaFactory
})

changefeedID := common.NewChangefeedID4Test("test", "verify-invalid-config")
err = Verify(context.Background(), changefeedID, sinkURI, sinkConfig)
require.ErrorContains(t, err, "ErrAvroSchemaAPIError")
Expand Down Expand Up @@ -208,7 +194,7 @@ func TestKafkaSinkBasicFunctionality(t *testing.T) {
}
return nil
}).Times(2)
syncProducer.EXPECT().SendMessages(gomock.Any(), int32(1), gomock.Any()).Return(nil)
syncProducer.EXPECT().SendMessages(gomock.Any(), gomock.Any(), int32(1), gomock.Any()).Return(nil)
defer cancel()
go kafkaSink.Run(ctx)

Expand Down Expand Up @@ -248,6 +234,7 @@ func TestKafkaSinkConstructionAndCleanup(t *testing.T) {
gomock.InOrder(
adminClient.EXPECT().Close(),
topicManager.EXPECT().Close(),
factory.EXPECT().Close(),
)

kafkaSink, err := newWithComponents(
Expand Down Expand Up @@ -276,6 +263,7 @@ func TestKafkaSinkConstructionAndCleanup(t *testing.T) {
asyncProducer.EXPECT().Close(),
adminClient.EXPECT().Close(),
topicManager.EXPECT().Close(),
factory.EXPECT().Close(),
)

kafkaSink, err := newWithComponents(
Expand Down Expand Up @@ -307,6 +295,7 @@ func TestKafkaSinkConstructionAndCleanup(t *testing.T) {
asyncProducer.EXPECT().Close().Do(func() { closeCount.Add(1) }),
adminClient.EXPECT().Close().Do(func() { closeCount.Add(1) }),
topicManager.EXPECT().Close().Do(func() { closeCount.Add(1) }),
factory.EXPECT().Close().Do(func() { closeCount.Add(1) }),
)

kafkaSink, err := newWithComponents(
Expand All @@ -322,7 +311,7 @@ func TestKafkaSinkConstructionAndCleanup(t *testing.T) {
require.True(t, kafkaSink.IsNormal())

kafkaSink.Close()
require.Equal(t, int64(4), closeCount.Load())
require.Equal(t, int64(5), closeCount.Load())
require.False(t, kafkaSink.IsNormal())
kafkaSink.AddDMLEvent(&commonEvent.DMLEvent{})
require.Zero(t, kafkaSink.eventChan.Len())
Expand Down Expand Up @@ -443,8 +432,8 @@ func TestKafkaSinkDDL(t *testing.T) {
kafkaSink, topicManager, _, syncProducer := newKafkaSinkForTest(
t, t.Context(), config.ProtocolOpen, &config.SinkConfig{})
topicManager.EXPECT().GetPartitionNum(gomock.Any(), kafkaSinkTestTopic).Return(int32(4), nil)
syncProducer.EXPECT().SendMessages(kafkaSinkTestTopic, int32(4), gomock.Any()).
DoAndReturn(func(_ string, _ int32, message *codecCommon.Message) error {
syncProducer.EXPECT().SendMessages(gomock.Any(), kafkaSinkTestTopic, int32(4), gomock.Any()).
DoAndReturn(func(_ context.Context, _ string, _ int32, message *codecCommon.Message) error {
require.NotEmpty(t, message.Key)
require.NotEmpty(t, message.Value)
return nil
Expand All @@ -457,8 +446,8 @@ func TestKafkaSinkDDL(t *testing.T) {
kafkaSink, topicManager, _, syncProducer := newKafkaSinkForTest(
t, t.Context(), config.ProtocolCanalJSON, &config.SinkConfig{})
topicManager.EXPECT().GetPartitionNum(gomock.Any(), kafkaSinkTestTopic).Return(int32(4), nil)
syncProducer.EXPECT().SendMessage(kafkaSinkTestTopic, int32(0), gomock.Any()).
DoAndReturn(func(_ string, _ int32, message *codecCommon.Message) error {
syncProducer.EXPECT().SendMessage(gomock.Any(), kafkaSinkTestTopic, int32(0), gomock.Any()).
DoAndReturn(func(_ context.Context, _ string, _ int32, message *codecCommon.Message) error {
require.NotEmpty(t, message.Value)
return nil
})
Expand All @@ -480,7 +469,7 @@ func TestKafkaSinkDDL(t *testing.T) {
t, t.Context(), config.ProtocolOpen, &config.SinkConfig{})
cause := errors.ErrKafkaSendMessage.GenWithStackByArgs()
topicManager.EXPECT().GetPartitionNum(gomock.Any(), kafkaSinkTestTopic).Return(int32(2), nil)
syncProducer.EXPECT().SendMessages(kafkaSinkTestTopic, int32(2), gomock.Any()).Return(cause)
syncProducer.EXPECT().SendMessages(gomock.Any(), kafkaSinkTestTopic, int32(2), gomock.Any()).Return(cause)

require.Equal(t, cause, kafkaSink.WriteBlockEvent(ddlEvent))
require.False(t, kafkaSink.IsNormal())
Expand Down Expand Up @@ -508,8 +497,8 @@ func TestKafkaSinkCheckpoint(t *testing.T) {
kafkaSink, topicManager, _, syncProducer := newKafkaSinkForTest(
t, t.Context(), config.ProtocolOpen, &config.SinkConfig{})
topicManager.EXPECT().GetPartitionNum(gomock.Any(), kafkaSinkTestTopic).Return(int32(3), nil)
syncProducer.EXPECT().SendMessages(kafkaSinkTestTopic, int32(3), gomock.Any()).
DoAndReturn(func(_ string, _ int32, message *codecCommon.Message) error {
syncProducer.EXPECT().SendMessages(gomock.Any(), kafkaSinkTestTopic, int32(3), gomock.Any()).
DoAndReturn(func(_ context.Context, _ string, _ int32, message *codecCommon.Message) error {
require.NotEmpty(t, message.Key)
return nil
})
Expand All @@ -535,7 +524,7 @@ func TestKafkaSinkCheckpoint(t *testing.T) {
partitionCounts := map[string]int32{"topic-a": 2, "topic-b": 3, kafkaSinkTestTopic: 4}
for topic, partitionCount := range partitionCounts {
topicManager.EXPECT().GetPartitionNum(gomock.Any(), topic).Return(partitionCount, nil)
syncProducer.EXPECT().SendMessages(topic, partitionCount, gomock.Any()).Return(nil)
syncProducer.EXPECT().SendMessages(gomock.Any(), topic, partitionCount, gomock.Any()).Return(nil)
}
kafkaSink.checkpointChan <- 100
close(kafkaSink.checkpointChan)
Expand Down Expand Up @@ -567,7 +556,7 @@ func TestKafkaSinkCheckpoint(t *testing.T) {
// return the error and stop, so exactly one GetPartitionNum and one
// SendMessages call are expected regardless of the topic order.
topicManager.EXPECT().GetPartitionNum(gomock.Any(), gomock.Any()).Return(int32(2), nil)
syncProducer.EXPECT().SendMessages(gomock.Any(), int32(2), gomock.Any()).Return(cause)
syncProducer.EXPECT().SendMessages(gomock.Any(), gomock.Any(), int32(2), gomock.Any()).Return(cause)
kafkaSink.checkpointChan <- 100

require.Equal(t, cause, kafkaSink.sendCheckpoint(t.Context()))
Expand Down Expand Up @@ -623,6 +612,7 @@ func newKafkaSinkForTest(
factory.EXPECT().AsyncProducer(gomock.Any()).Return(asyncProducer, nil)
factory.EXPECT().SyncProducer(gomock.Any()).Return(syncProducer, nil)
factory.EXPECT().MetricsCollector(nil).Return(noopMetricsCollector{})
factory.EXPECT().Close().AnyTimes()

kafkaSink, err := newWithComponents(ctx, changefeedID, common.DefaultKeyspaceID, protocol, components{
encoderGroup: encoderGroup,
Expand Down
19 changes: 8 additions & 11 deletions downstreamadapter/sink/topicmanager/kafka_topic_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ func (m *kafkaTopicManager) backgroundRefreshMeta(ctx context.Context) {
case <-ticker.C:
// We ignore the error here, because the error may be caused by the
// network problem, and we can try to get the metadata next time.
topicPartitionNums, _ := m.fetchAllTopicsPartitionsNum()
topicPartitionNums, _ := m.fetchAllTopicsPartitionsNum(ctx)
for topic, partitionNum := range topicPartitionNums {
m.tryUpdatePartitionsAndLogging(topic, partitionNum)
}
Expand Down Expand Up @@ -157,15 +157,15 @@ func (m *kafkaTopicManager) tryUpdatePartitionsAndLogging(topic string, partitio
// The error returned by this method could be a transient error that is fixable by the underlying logic.
// When handling this error, please be cautious.
// If you simply throw the error to the caller, it may impact the robustness of your program.
func (m *kafkaTopicManager) fetchAllTopicsPartitionsNum() (map[string]int32, error) {
func (m *kafkaTopicManager) fetchAllTopicsPartitionsNum(ctx context.Context) (map[string]int32, error) {
var topics []string
m.topics.Range(func(key, _ any) bool {
topics = append(topics, key.(string))
return true
})

start := time.Now()
numPartitions, err := m.admin.GetTopicsPartitionsNum(topics)
numPartitions, err := m.admin.GetTopicsPartitionsNum(ctx, topics)
if err != nil {
log.Warn(
"kafka topic metadata refresh failed",
Expand Down Expand Up @@ -202,7 +202,7 @@ func (m *kafkaTopicManager) waitUntilTopicVisible(
err := retry.Do(ctx, func() error {
// ignoreTopicError is set to false since we just create the topic,
// make sure the topic is visible.
meta, err := m.admin.GetTopicsMeta(topics, false)
meta, err := m.admin.GetTopicsMeta(ctx, topics, false)
if err != nil {
return err
}
Expand Down Expand Up @@ -231,20 +231,17 @@ func (m *kafkaTopicManager) waitUntilTopicVisible(

// createTopic creates a topic with the given name
// and returns the number of partitions.
func (m *kafkaTopicManager) createTopic(
_ context.Context,
topicName string,
) (int32, error) {
func (m *kafkaTopicManager) createTopic(ctx context.Context, topicName string) (int32, error) {
if !m.cfg.AutoCreate {
return 0, errors.ErrKafkaInvalidConfig.GenWithStack("`auto-create-topic` is false, and %s not found", topicName)
}

if err := m.cfg.ValidateReplicationFactor(m.admin); err != nil {
if err := m.cfg.ValidateReplicationFactor(ctx, m.admin); err != nil {
return 0, err
}

start := time.Now()
err := m.admin.CreateTopic(&kafka.TopicDetail{
err := m.admin.CreateTopic(ctx, &kafka.TopicDetail{
Name: topicName,
NumPartitions: m.cfg.PartitionNum,
ReplicationFactor: m.cfg.ReplicationFactor,
Expand Down Expand Up @@ -274,7 +271,7 @@ func (m *kafkaTopicManager) CreateTopicAndWaitUntilVisible(
ctx context.Context, topicName string,
) (int32, error) {
// If the topic is not in the cache, try to get its metadata.
topicDetails, err := m.admin.GetTopicsMeta([]string{topicName}, false)
topicDetails, err := m.admin.GetTopicsMeta(ctx, []string{topicName}, false)
if err == nil {
if numPartition, ok := m.tryStoreTopicMeta(topicName, topicDetails); ok {
return numPartition, nil
Expand Down
Loading
Loading