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
3 changes: 3 additions & 0 deletions api/v2/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,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 @@ -845,6 +846,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 @@ -1531,6 +1533,7 @@ type MySQLConfig struct {
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"`
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 @@ -207,3 +207,23 @@ func TestReplicaConfigConversionRedoBatchField(t *testing.T) {
require.NotNil(t, apiCfgBack.Consistent.EventCollectorBatchCount)
require.Equal(t, 4096, *apiCfgBack.Consistent.EventCollectorBatchCount)
}

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
68 changes: 54 additions & 14 deletions downstreamadapter/sink/mysql/sink.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,12 @@ type Sink struct {
// enableActiveActive is false.
progressTableWriter *mysql.ProgressTableWriter

// 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 @@ -81,12 +82,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 @@ -97,7 +101,7 @@ func New(
sinkURI *url.URL,
keyspaceID uint32,
) (*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 @@ -113,9 +117,10 @@ func New(
metrics.ChangefeedDownstreamIsTiDBGauge.DeleteLabelValues(keyspace, name)
}

return newMySQLSinkWithControlDB(ctx, changefeedID, cfg, dmlDB, controlDB, config.BDRMode, config.EnableActiveActive, config.ActiveActiveProgressInterval, keyspaceID), nil
return newMySQLSinkWithControlAsyncDB(ctx, changefeedID, cfg, dmlDB, controlDB, controlAsyncDB, config.BDRMode, config.EnableActiveActive, config.ActiveActiveProgressInterval, keyspaceID), nil
}

// NewMySQLSink used for test
func NewMySQLSink(
ctx context.Context,
changefeedID common.ChangeFeedID,
Expand All @@ -126,7 +131,11 @@ func NewMySQLSink(
progressInterval time.Duration,
keyspaceID uint32,
) *Sink {
return newMySQLSinkWithDBs(ctx, changefeedID, cfg, db, db, bdrMode, enableActiveActive, progressInterval, keyspaceID)
var controlAsyncDB *sql.DB
if cfg.IsTiDB {
controlAsyncDB = db
}
return newMySQLSinkWithDBs(ctx, changefeedID, cfg, db, db, controlAsyncDB, bdrMode, enableActiveActive, progressInterval, keyspaceID)
}

// newMySQLSinkWithControlDB creates a MySQL sink with separate pools for DML and
Expand All @@ -144,7 +153,26 @@ func newMySQLSinkWithControlDB(
progressInterval time.Duration,
keyspaceID uint32,
) *Sink {
return newMySQLSinkWithDBs(ctx, changefeedID, cfg, dmlDB, controlDB, bdrMode, enableActiveActive, progressInterval, keyspaceID)
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(
Expand All @@ -153,11 +181,18 @@ func newMySQLSinkWithDBs(
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
Expand All @@ -178,11 +213,12 @@ func newMySQLSinkWithDBs(
}

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 @@ -201,6 +237,7 @@ func newMySQLSinkWithDBs(
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)
}
Expand Down Expand Up @@ -453,6 +490,9 @@ func (s *Sink) Close() {
if s.controlDB != s.dmlDB {
s.closeDBPool("control", s.controlDB)
}
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()
}
Expand Down
92 changes: 92 additions & 0 deletions downstreamadapter/sink/mysql/sink_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/pingcap/ticdc/pkg/common"
commonEvent "github.com/pingcap/ticdc/pkg/common/event"
"github.com/pingcap/ticdc/pkg/sink/mysql"
timodel "github.com/pingcap/tidb/pkg/meta/model"
"github.com/pingcap/tidb/pkg/sessionctx/vardef"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -79,6 +80,97 @@ func getMysqlSinkWithSeparateDBs(t *testing.T) (context.Context, *Sink, sqlmock.
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 @@ -721,6 +721,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