Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 21 additions & 0 deletions api/v2/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,7 @@ func (c *ReplicaConfig) toInternalReplicaConfigWithOriginConfig(
WriteTimeout: c.Sink.MySQLConfig.WriteTimeout,
ReadTimeout: c.Sink.MySQLConfig.ReadTimeout,
Timeout: c.Sink.MySQLConfig.Timeout,
AsyncDDLTimeout: c.Sink.MySQLConfig.AsyncDDLTimeout,
EnableBatchDML: c.Sink.MySQLConfig.EnableBatchDML,
EnableMultiStatement: c.Sink.MySQLConfig.EnableMultiStatement,
EnableCachePreparedStatement: c.Sink.MySQLConfig.EnableCachePreparedStatement,
Expand Down Expand Up @@ -794,6 +795,7 @@ func ToAPIReplicaConfig(c *config.ReplicaConfig) *ReplicaConfig {
WriteTimeout: cloned.Sink.MySQLConfig.WriteTimeout,
ReadTimeout: cloned.Sink.MySQLConfig.ReadTimeout,
Timeout: cloned.Sink.MySQLConfig.Timeout,
AsyncDDLTimeout: cloned.Sink.MySQLConfig.AsyncDDLTimeout,
EnableBatchDML: cloned.Sink.MySQLConfig.EnableBatchDML,
EnableMultiStatement: cloned.Sink.MySQLConfig.EnableMultiStatement,
EnableCachePreparedStatement: cloned.Sink.MySQLConfig.EnableCachePreparedStatement,
Expand Down Expand Up @@ -1473,6 +1475,7 @@ type KafkaConfig struct {

// MySQLConfig represents a MySQL sink configuration
type MySQLConfig struct {
<<<<<<< HEAD
WorkerCount *int `json:"worker_count,omitempty"`
MaxTxnRow *int `json:"max_txn_row,omitempty"`
MaxMultiUpdateRowSize *int `json:"max_multi_update_row_size,omitempty"`
Expand All @@ -1488,6 +1491,24 @@ type MySQLConfig struct {
EnableBatchDML *bool `json:"enable_batch_dml,omitempty"`
EnableMultiStatement *bool `json:"enable_multi_statement,omitempty"`
EnableCachePreparedStatement *bool `json:"enable_cache_prepared_statement,omitempty"`
=======
WorkerCount *int `json:"worker_count,omitempty" toml:"worker-count,omitempty"`
MaxTxnRow *int `json:"max_txn_row,omitempty" toml:"max-txn-row,omitempty"`
MaxMultiUpdateRowSize *int `json:"max_multi_update_row_size,omitempty" toml:"max-multi-update-row-size,omitempty"`
MaxMultiUpdateRowCount *int `json:"max_multi_update_row_count,omitempty" toml:"max-multi-update-row-count,omitempty"`
TiDBTxnMode *string `json:"tidb_txn_mode,omitempty" toml:"tidb-txn-mode,omitempty"`
SSLCa *string `json:"ssl_ca,omitempty" toml:"ssl-ca,omitempty"`
SSLCert *string `json:"ssl_cert,omitempty" toml:"ssl-cert,omitempty"`
SSLKey *string `json:"ssl_key,omitempty" toml:"ssl-key,omitempty"`
TimeZone *string `json:"time_zone,omitempty" toml:"time-zone,omitempty"`
WriteTimeout *string `json:"write_timeout,omitempty" toml:"write-timeout,omitempty"`
ReadTimeout *string `json:"read_timeout,omitempty" toml:"read-timeout,omitempty"`
Timeout *string `json:"timeout,omitempty" toml:"timeout,omitempty"`
AsyncDDLTimeout *string `json:"async_ddl_timeout,omitempty" toml:"async-ddl-timeout,omitempty"`
EnableBatchDML *bool `json:"enable_batch_dml,omitempty" toml:"enable-batch-dml,omitempty"`
EnableMultiStatement *bool `json:"enable_multi_statement,omitempty" toml:"enable-multi-statement,omitempty"`
EnableCachePreparedStatement *bool `json:"enable_cache_prepared_statement,omitempty" toml:"enable-cache-prepared-statement,omitempty"`
>>>>>>> 430b0a8cc (sink: add async ddl timeout for add index (#5836))
}

// CloudStorageConfig represents a cloud storage sink configuration
Expand Down
20 changes: 20 additions & 0 deletions api/v2/model_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,23 @@ func TestReplicaConfigConversion(t *testing.T) {
require.Equal(t, "correctness", *apiCfgBack.Integrity.IntegrityCheckLevel)
require.Equal(t, "eventual", *apiCfgBack.Consistent.Level)
}

func TestReplicaConfigConversionMySQLAsyncDDLTimeout(t *testing.T) {
t.Parallel()

apiCfg := &ReplicaConfig{
Sink: &SinkConfig{
MySQLConfig: &MySQLConfig{
AsyncDDLTimeout: util.AddressOf("45m"),
},
},
}

internalCfg := apiCfg.ToInternalReplicaConfig()
require.NotNil(t, internalCfg.Sink.MySQLConfig)
require.Equal(t, "45m", util.GetOrZero(internalCfg.Sink.MySQLConfig.AsyncDDLTimeout))

apiCfgBack := ToAPIReplicaConfig(internalCfg)
require.NotNil(t, apiCfgBack.Sink.MySQLConfig)
require.Equal(t, "45m", util.GetOrZero(apiCfgBack.Sink.MySQLConfig.AsyncDDLTimeout))
}
3 changes: 3 additions & 0 deletions cmd/cdc/cli/cli_changefeed_create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ func TestTomlFileToApiModel(t *testing.T) {
content := `
[filter]
rules = ['*.*', '!test.*']

[sink.mysql-config]
async-ddl-timeout = "45m"
`
err := os.WriteFile(path, []byte(content), 0o644)
require.Nil(t, err)
Expand Down
120 changes: 109 additions & 11 deletions downstreamadapter/sink/mysql/sink.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,12 @@ type Sink struct {
dmlWriter []*mysql.Writer
ddlWriter *mysql.Writer

// dmlDB and controlDB are the DB pools this sink is responsible for closing.
// dmlDB, controlDB, and controlAsyncDB are the DB pools this sink is responsible for closing.
// Compatibility callers built through NewMySQLSink use one shared pool.
dmlDB *sql.DB
controlDB *sql.DB
statistics *metrics.Statistics
dmlDB *sql.DB
controlDB *sql.DB
controlAsyncDB *sql.DB
statistics *metrics.Statistics

conflictDetector *causality.ConflictDetector

Expand All @@ -71,12 +72,15 @@ func Verify(
config *config.ChangefeedConfig,
) error {
testID := common.NewChangefeedID4Test("test", "mysql_create_sink_test")
_, dmlDB, controlDB, err := mysql.NewMysqlConfigAndDBs(ctx, testID, uri, config)
_, dmlDB, controlDB, controlAsyncDB, err := mysql.NewMysqlConfigAndDBs(ctx, testID, uri, config)
if err != nil {
return err
}
_ = dmlDB.Close()
_ = controlDB.Close()
if controlAsyncDB != nil {
_ = controlAsyncDB.Close()
}
return nil
}

Expand All @@ -86,7 +90,7 @@ func New(
config *config.ChangefeedConfig,
sinkURI *url.URL,
) (*Sink, error) {
cfg, dmlDB, controlDB, err := mysql.NewMysqlConfigAndDBs(ctx, changefeedID, sinkURI, config)
cfg, dmlDB, controlDB, controlAsyncDB, err := mysql.NewMysqlConfigAndDBs(ctx, changefeedID, sinkURI, config)
if err != nil {
return nil, err
}
Expand All @@ -102,17 +106,30 @@ func New(
metrics.ChangefeedDownstreamIsTiDBGauge.DeleteLabelValues(keyspace, name)
}

<<<<<<< HEAD
return newMySQLSinkWithControlDB(ctx, changefeedID, cfg, dmlDB, controlDB, config.BDRMode), nil
=======
return newMySQLSinkWithControlAsyncDB(ctx, changefeedID, cfg, dmlDB, controlDB, controlAsyncDB, config.BDRMode, config.EnableActiveActive, config.ActiveActiveProgressInterval, keyspaceID), nil
>>>>>>> 430b0a8cc (sink: add async ddl timeout for add index (#5836))
}

// NewMySQLSink used for test
func NewMySQLSink(
ctx context.Context,
changefeedID common.ChangeFeedID,
cfg *mysql.Config,
db *sql.DB,
bdrMode bool,
) *Sink {
<<<<<<< HEAD
return newMySQLSinkWithControlDB(ctx, changefeedID, cfg, db, db, bdrMode)
=======
var controlAsyncDB *sql.DB
if cfg.IsTiDB {
controlAsyncDB = db
}
return newMySQLSinkWithDBs(ctx, changefeedID, cfg, db, db, controlAsyncDB, bdrMode, enableActiveActive, progressInterval, keyspaceID)
>>>>>>> 430b0a8cc (sink: add async ddl timeout for add index (#5836))
}

// newMySQLSinkWithControlDB creates a MySQL sink with separate pools for DML and
Expand All @@ -126,13 +143,76 @@ func newMySQLSinkWithControlDB(
controlDB *sql.DB,
bdrMode bool,
) *Sink {
<<<<<<< HEAD
stat := metrics.NewStatistics(changefeedID, "TxnSink")
=======
var controlAsyncDB *sql.DB
if cfg.IsTiDB {
controlAsyncDB = controlDB
}
return newMySQLSinkWithDBs(ctx, changefeedID, cfg, dmlDB, controlDB, controlAsyncDB, bdrMode, enableActiveActive, progressInterval, keyspaceID)
}

func newMySQLSinkWithControlAsyncDB(
ctx context.Context,
changefeedID common.ChangeFeedID,
cfg *mysql.Config,
dmlDB *sql.DB,
controlDB *sql.DB,
controlAsyncDB *sql.DB,
bdrMode bool,
enableActiveActive bool,
progressInterval time.Duration,
keyspaceID uint32,
) *Sink {
return newMySQLSinkWithDBs(ctx, changefeedID, cfg, dmlDB, controlDB, controlAsyncDB, bdrMode, enableActiveActive, progressInterval, keyspaceID)
}

func newMySQLSinkWithDBs(
ctx context.Context,
changefeedID common.ChangeFeedID,
cfg *mysql.Config,
dmlDB *sql.DB,
controlDB *sql.DB,
controlAsyncDB *sql.DB,
bdrMode bool,
enableActiveActive bool,
progressInterval time.Duration,
keyspaceID uint32,
) *Sink {
if !cfg.IsTiDB {
controlAsyncDB = nil
} else if controlAsyncDB == nil {
controlAsyncDB = controlDB
}

stat := metrics.NewStatistics(changefeedID, keyspaceID, "TxnSink")

var activeActiveSyncStatsCollector *mysql.ActiveActiveSyncStatsCollector
if enableActiveActive && cfg.IsTiDB && cfg.ActiveActiveSyncStatsInterval > 0 {
supported, err := mysql.CheckActiveActiveSyncStatsSupported(ctx, dmlDB)
if err != nil {
log.Info("failed to check tidb_cdc_active_active_sync_stats support, disable metric collection",
zap.String("keyspace", changefeedID.Keyspace()),
zap.Stringer("changefeed", changefeedID),
zap.Error(err))
} else if supported {
activeActiveSyncStatsCollector = mysql.NewActiveActiveSyncStatsCollector(changefeedID)
} else {
log.Info("downstream does not support tidb_cdc_active_active_sync_stats, disable metric collection",
zap.String("keyspace", changefeedID.Keyspace()),
zap.Stringer("changefeed", changefeedID))
}
}

>>>>>>> 430b0a8cc (sink: add async ddl timeout for add index (#5836))
result := &Sink{
changefeedID: changefeedID,
dmlDB: dmlDB,
controlDB: controlDB,
dmlWriter: make([]*mysql.Writer, cfg.WorkerCount),
statistics: stat,
changefeedID: changefeedID,
dmlDB: dmlDB,
controlDB: controlDB,
controlAsyncDB: controlAsyncDB,
dmlWriter: make([]*mysql.Writer, cfg.WorkerCount),
statistics: stat,
conflictDetector: causality.New(defaultConflictDetectorSlots,
causality.TxnCacheOption{
Count: cfg.WorkerCount,
Expand All @@ -146,7 +226,16 @@ func newMySQLSinkWithControlDB(
bdrMode: bdrMode,
}
for i := 0; i < len(result.dmlWriter); i++ {
<<<<<<< HEAD
result.dmlWriter[i] = mysql.NewWriter(ctx, i, dmlDB, cfg, changefeedID, stat)
=======
result.dmlWriter[i] = mysql.NewWriter(ctx, i, dmlDB, cfg, changefeedID, stat, activeActiveSyncStatsCollector)
}
result.ddlWriter = mysql.NewWriter(ctx, len(result.dmlWriter), controlDB, cfg, changefeedID, stat, nil)
result.ddlWriter.SetControlAsyncDB(controlAsyncDB)
if enableActiveActive {
result.progressTableWriter = mysql.NewProgressTableWriter(ctx, controlDB, changefeedID, cfg.MaxTxnRow, progressInterval)
>>>>>>> 430b0a8cc (sink: add async ddl timeout for add index (#5836))
}
result.ddlWriter = mysql.NewWriter(ctx, len(result.dmlWriter), controlDB, cfg, changefeedID, stat)
return result
Expand Down Expand Up @@ -369,6 +458,15 @@ func (s *Sink) Close() {
if s.controlDB != s.dmlDB {
s.closeDBPool("control", s.controlDB)
}
<<<<<<< HEAD
=======
if s.controlAsyncDB != nil && s.controlAsyncDB != s.dmlDB && s.controlAsyncDB != s.controlDB {
s.closeDBPool("control async", s.controlAsyncDB)
}
if s.activeActiveSyncStatsCollector != nil {
s.activeActiveSyncStatsCollector.Close()
}
>>>>>>> 430b0a8cc (sink: add async ddl timeout for add index (#5836))
s.statistics.Close()

metrics.ChangefeedDownstreamIsTiDBGauge.DeleteLabelValues(s.changefeedID.Keyspace(), s.changefeedID.Name())
Expand Down
96 changes: 96 additions & 0 deletions downstreamadapter/sink/mysql/sink_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@
"github.com/pingcap/ticdc/pkg/common"
commonEvent "github.com/pingcap/ticdc/pkg/common/event"
"github.com/pingcap/ticdc/pkg/sink/mysql"
<<<<<<< HEAD

Check failure on line 28 in downstreamadapter/sink/mysql/sink_test.go

View workflow job for this annotation

GitHub Actions / Build Classic CDC

missing import path

Check failure on line 28 in downstreamadapter/sink/mysql/sink_test.go

View workflow job for this annotation

GitHub Actions / Classic Unit Tests

missing import path
"github.com/pingcap/tidb/pkg/sessionctx/variable"
=======
timodel "github.com/pingcap/tidb/pkg/meta/model"
"github.com/pingcap/tidb/pkg/sessionctx/vardef"
>>>>>>> 430b0a8cc (sink: add async ddl timeout for add index (#5836))
"github.com/stretchr/testify/require"
)

Expand Down Expand Up @@ -79,6 +84,97 @@
return ctx, sink, dmlMock, controlMock
}

func TestMysqlSinkControlAsyncDBOnlyForTiDB(t *testing.T) {
ctx := context.Background()
changefeedID := common.NewChangefeedID4Test("test", "test")

t.Run("mysql downstream has no control async db", func(t *testing.T) {
db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
require.NoError(t, err)

cfg := mysql.New()
cfg.WorkerCount = 1
cfg.MaxAllowedPacket = int64(vardef.DefMaxAllowedPacket)
cfg.CachePrepStmts = false
cfg.IsTiDB = false

sink := NewMySQLSink(ctx, changefeedID, cfg, db, false, false, time.Minute, common.DefaultKeyspaceID)
require.Nil(t, sink.controlAsyncDB)

mock.ExpectClose()
sink.Close()
require.NoError(t, mock.ExpectationsWereMet())
})

t.Run("tidb downstream has control async db", func(t *testing.T) {
db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
require.NoError(t, err)

cfg := mysql.New()
cfg.WorkerCount = 1
cfg.MaxAllowedPacket = int64(vardef.DefMaxAllowedPacket)
cfg.CachePrepStmts = false
cfg.IsTiDB = true

sink := NewMySQLSink(ctx, changefeedID, cfg, db, false, false, time.Minute, common.DefaultKeyspaceID)
require.Same(t, db, sink.controlAsyncDB)

mock.ExpectClose()
sink.Close()
require.NoError(t, mock.ExpectationsWereMet())
})
}

func TestMysqlSinkUsesControlAsyncDBForTiDBAddIndex(t *testing.T) {
dmlDB, dmlMock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
require.NoError(t, err)
controlDB, controlMock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
require.NoError(t, err)
controlAsyncDB, controlAsyncMock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
require.NoError(t, err)

ctx := context.Background()
changefeedID := common.NewChangefeedID4Test("test", "test")
cfg := mysql.New()
cfg.WorkerCount = 1
cfg.MaxAllowedPacket = int64(vardef.DefMaxAllowedPacket)
cfg.CachePrepStmts = false
cfg.EnableDDLTs = false
cfg.IsTiDB = true

sink := newMySQLSinkWithControlAsyncDB(ctx, changefeedID, cfg, dmlDB, controlDB, controlAsyncDB, false, false, time.Minute, common.DefaultKeyspaceID)

ddl := &commonEvent.DDLEvent{
Type: byte(timodel.ActionAddIndex),
Query: "alter table t add index idx_name(name);",
SchemaName: "test",
TableName: "t",
BlockedTables: &commonEvent.InfluencedTables{
InfluenceType: commonEvent.InfluenceTypeNormal,
TableIDs: []int64{1},
},
}

controlMock.ExpectQuery("BEGIN; SET @ticdc_ts := TIDB_PARSE_TSO(@@tidb_current_ts); ROLLBACK; SELECT @ticdc_ts; SET @ticdc_ts=NULL;").
WillReturnRows(sqlmock.NewRows([]string{"@ticdc_ts"}).AddRow("2021-05-26 11:33:37.776000"))
controlAsyncMock.ExpectBegin()
controlAsyncMock.ExpectExec("USE `test`;").WillReturnResult(sqlmock.NewResult(1, 1))
controlAsyncMock.ExpectExec("SET TIMESTAMP = DEFAULT").WillReturnResult(sqlmock.NewResult(1, 1))
controlAsyncMock.ExpectExec("alter table t add index idx_name(name);").WillReturnResult(sqlmock.NewResult(1, 1))
controlAsyncMock.ExpectCommit()

require.NoError(t, sink.WriteBlockEvent(ddl))

dmlMock.ExpectClose()
controlMock.ExpectClose()
controlAsyncMock.ExpectClose()
sink.Close()

require.NoError(t, dmlMock.ExpectationsWereMet())
require.NoError(t, controlMock.ExpectationsWereMet())
require.NoError(t, controlAsyncMock.ExpectationsWereMet())
}

func MysqlSinkForTest() (*Sink, sqlmock.Sqlmock) {
ctx, sink, mock := getMysqlSink()
go sink.Run(ctx)
Expand Down
1 change: 1 addition & 0 deletions pkg/config/sink.go
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,7 @@ type MySQLConfig struct {
WriteTimeout *string `toml:"write-timeout" json:"write-timeout,omitempty"`
ReadTimeout *string `toml:"read-timeout" json:"read-timeout,omitempty"`
Timeout *string `toml:"timeout" json:"timeout,omitempty"`
AsyncDDLTimeout *string `toml:"async-ddl-timeout" json:"async-ddl-timeout,omitempty"`
EnableBatchDML *bool `toml:"enable-batch-dml" json:"enable-batch-dml,omitempty"`
EnableMultiStatement *bool `toml:"enable-multi-statement" json:"enable-multi-statement,omitempty"`
EnableCachePreparedStatement *bool `toml:"enable-cache-prepared-statement" json:"enable-cache-prepared-statement,omitempty"`
Expand Down
Loading
Loading