diff --git a/dm/config/task_converters.go b/dm/config/task_converters.go index 7c3bf04f15..dbbfb20355 100644 --- a/dm/config/task_converters.go +++ b/dm/config/task_converters.go @@ -161,6 +161,9 @@ func OpenAPITaskToSubTaskConfigs(task *openapi.Task, toDBCfg *dbconfig.DBConfig, // set task name and mode subTaskCfg.Name = task.Name subTaskCfg.Mode = string(task.TaskMode) + if task.Timezone != nil { + subTaskCfg.Timezone = *task.Timezone + } // set task meta subTaskCfg.MetaSchema = *task.MetaSchema // add binlog meta @@ -201,7 +204,7 @@ func OpenAPITaskToSubTaskConfigs(task *openapi.Task, toDBCfg *dbconfig.DBConfig, subTaskCfg.CaseSensitive = sourceCfgMap[sourceCfg.SourceName].CaseSensitive // set source db config subTaskCfg.SourceID = sourceCfg.SourceName - subTaskCfg.From = sourceCfgMap[sourceCfg.SourceName].From + subTaskCfg.From = *sourceCfgMap[sourceCfg.SourceName].From.Clone() // set target db config subTaskCfg.To = *toDBCfg.Clone() // TODO ExprFilter @@ -683,6 +686,10 @@ func SubTaskConfigsToOpenAPITask(subTaskConfigList []*SubTaskConfig) *openapi.Ta Password: oneSubtaskConfig.To.Password, }, } + if oneSubtaskConfig.Timezone != "" { + timezone := oneSubtaskConfig.Timezone + task.Timezone = &timezone + } if oneSubtaskConfig.ShardMode != "" { taskShardMode := openapi.TaskShardMode(oneSubtaskConfig.ShardMode) task.ShardMode = &taskShardMode diff --git a/dm/config/task_converters_test.go b/dm/config/task_converters_test.go index 98f06ae943..a97e251fd4 100644 --- a/dm/config/task_converters_test.go +++ b/dm/config/task_converters_test.go @@ -60,12 +60,14 @@ func testNoShardTaskToSubTaskConfigs(c *check.C) { c.Assert(err, check.IsNil) source1Name := task.SourceConfig.SourceConf[0].SourceName sourceCfg1.SourceID = task.SourceConfig.SourceConf[0].SourceName + sourceCfg1.From.Session = map[string]string{"time_zone": "UTC"} sourceCfgMap := map[string]*SourceConfig{source1Name: sourceCfg1} toDBCfg := &dbconfig.DBConfig{ Host: task.TargetConfig.Host, Port: task.TargetConfig.Port, User: task.TargetConfig.User, Password: task.TargetConfig.Password, + Session: map[string]string{"time_zone": "UTC"}, Security: &security.Security{ SSLCABytes: []byte(task.TargetConfig.Security.SslCaContent), SSLCertBytes: []byte(task.TargetConfig.Security.SslCertContent), @@ -82,6 +84,7 @@ func testNoShardTaskToSubTaskConfigs(c *check.C) { subTaskConfig := subTaskConfigList[0] // check task name and mode c.Assert(subTaskConfig.Name, check.Equals, task.Name) + c.Assert(subTaskConfig.Timezone, check.Equals, *task.Timezone) // check task meta c.Assert(subTaskConfig.MetaSchema, check.Equals, *task.MetaSchema) c.Assert(subTaskConfig.Meta, check.IsNil) @@ -93,8 +96,12 @@ func testNoShardTaskToSubTaskConfigs(c *check.C) { c.Assert(subTaskConfig.CaseSensitive, check.Equals, sourceCfg1.CaseSensitive) // check from c.Assert(subTaskConfig.From.Host, check.Equals, sourceCfg1.From.Host) + c.Assert(subTaskConfig.From.Session["time_zone"], check.Equals, *task.Timezone) + c.Assert(sourceCfg1.From.Session["time_zone"], check.Equals, "UTC") // check to c.Assert(subTaskConfig.To.Host, check.Equals, toDBCfg.Host) + c.Assert(subTaskConfig.To.Session["time_zone"], check.Equals, *task.Timezone) + c.Assert(toDBCfg.Session["time_zone"], check.Equals, "UTC") // check dumpling loader syncer config c.Assert(subTaskConfig.MydumperConfig.Threads, check.Equals, *task.SourceConfig.FullMigrateConf.ExportThreads) c.Assert(subTaskConfig.LoaderConfig.Dir, check.Equals, fmt.Sprintf( @@ -464,11 +471,34 @@ func TestConvertBetweenOpenAPITaskAndTaskConfig(t *testing.T) { taskCfg, err := OpenAPITaskToTaskConfig(&task, sourceCfgMap) require.NoError(t, err) require.NotNil(t, taskCfg) + require.Equal(t, *task.Timezone, taskCfg.Timezone) task1, err := TaskConfigToOpenAPITask(taskCfg, sourceCfgMap) require.NoError(t, err) require.NotNil(t, task1) require.EqualValues(t, task1, &task) + configuredTimezone := task.Timezone + emptyTimezone := "" + for _, tc := range []struct { + name string + timezone *string + }{ + {name: "omitted", timezone: nil}, + {name: "empty", timezone: &emptyTimezone}, + } { + t.Run(tc.name+" timezone uses downstream default", func(t *testing.T) { + task.Timezone = tc.timezone + taskCfgWithoutTimezone, convertErr := OpenAPITaskToTaskConfig(&task, sourceCfgMap) + require.NoError(t, convertErr) + require.Empty(t, taskCfgWithoutTimezone.Timezone) + + taskWithoutTimezone, convertErr := TaskConfigToOpenAPITask(taskCfgWithoutTimezone, sourceCfgMap) + require.NoError(t, convertErr) + require.Nil(t, taskWithoutTimezone.Timezone) + }) + } + task.Timezone = configuredTimezone + // test update some fields in task { batch := 1000 diff --git a/dm/master/openapi_controller_test.go b/dm/master/openapi_controller_test.go index 232d66ed80..096311fe20 100644 --- a/dm/master/openapi_controller_test.go +++ b/dm/master/openapi_controller_test.go @@ -298,6 +298,9 @@ func (s *OpenAPIControllerSuite) TestTaskController() { res, err := server.createTask(ctx, createTaskReq) s.Nil(err) s.EqualValues(*s.testTask, res.Task) + subTaskCfgs := server.scheduler.GetSubTaskCfgsByTask(s.testTask.Name) + s.Len(subTaskCfgs, 1) + s.Equal(*s.testTask.Timezone, subTaskCfgs[s.testSource.SourceName].Timezone) } // update @@ -470,6 +473,22 @@ func (s *OpenAPIControllerSuite) TestTaskController() { } } +func (s *OpenAPIControllerSuite) TestCreateTaskRejectsInvalidTimezone() { + ctx, cancel := context.WithCancel(context.Background()) + server := setupTestServer(ctx, s.T()) + defer func() { + cancel() + server.Close() + }() + + task := *s.testTask + invalidTimezone := "invalid/timezone" + task.Timezone = &invalidTimezone + res, err := server.createTask(ctx, openapi.CreateTaskRequest{Task: task}) + s.Nil(res) + s.True(terror.ErrConfigInvalidTimezone.Equal(err)) +} + func (s *OpenAPIControllerSuite) TestTaskStatusSourceErrorFallback() { ctx, cancel := context.WithCancel(context.Background()) server := setupTestServer(ctx, s.T()) diff --git a/dm/master/openapi_view_test.go b/dm/master/openapi_view_test.go index 330ec908f3..fdc4f69f4d 100644 --- a/dm/master/openapi_view_test.go +++ b/dm/master/openapi_view_test.go @@ -41,9 +41,11 @@ import ( "github.com/pingcap/tiflow/dm/pkg/log" "github.com/pingcap/tiflow/dm/pkg/terror" "github.com/pingcap/tiflow/dm/pkg/utils" + "github.com/pingcap/tiflow/dm/syncer" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "github.com/tikv/pd/pkg/utils/tempurl" + "google.golang.org/grpc" ) // some data for test. @@ -549,6 +551,7 @@ func (s *OpenAPIViewSuite) TestTaskTemplatesAPI() { var createTaskResp openapi.Task s.NoError(result.UnmarshalBodyToObject(&createTaskResp)) s.Equal(createTaskResp.Name, task.Name) + s.EqualValues(task.Timezone, createTaskResp.Timezone) // create again will fail result = testutil.NewRequest().Post(url).WithJsonBody(task).GoWithHTTPHandler(s.T(), s1.openapiHandles) @@ -564,6 +567,7 @@ func (s *OpenAPIViewSuite) TestTaskTemplatesAPI() { s.NoError(result.UnmarshalBodyToObject(&resultTaskList)) s.Equal(1, resultTaskList.Total) s.Equal(task.Name, resultTaskList.Data[0].Name) + s.EqualValues(task.Timezone, resultTaskList.Data[0].Timezone) // get detail oneURL := fmt.Sprintf("%s/%s", url, task.Name) @@ -572,6 +576,7 @@ func (s *OpenAPIViewSuite) TestTaskTemplatesAPI() { var respTask openapi.Task s.NoError(result.UnmarshalBodyToObject(&respTask)) s.Equal(task.Name, respTask.Name) + s.EqualValues(task.Timezone, respTask.Timezone) // get not exist notExistURL := fmt.Sprintf("%s/%s", url, "notexist") @@ -1000,9 +1005,11 @@ func (s *OpenAPIViewSuite) TestTaskAPI() { var createTaskResp openapi.OperateTaskResponse s.NoError(result.UnmarshalBodyToObject(&createTaskResp)) s.Equal(createTaskResp.Task.Name, task.Name) + s.EqualValues(task.Timezone, createTaskResp.Task.Timezone) subTaskM := s1.scheduler.GetSubTaskCfgsByTask(task.Name) s.Len(subTaskM, 1) s.Equal(task.Name, subTaskM[source1Name].Name) + s.Equal(*task.Timezone, subTaskM[source1Name].Timezone) // get task task1URL := fmt.Sprintf("%s/%s", taskURL, task.Name) @@ -1011,6 +1018,44 @@ func (s *OpenAPIViewSuite) TestTaskAPI() { s.Equal(http.StatusOK, result.Code()) s.NoError(result.UnmarshalBodyToObject(&task1FromHTTP)) s.Equal(task1FromHTTP.Name, task.Name) + s.EqualValues(task.Timezone, task1FromHTTP.Timezone) + + // changing timezone through PUT is rejected by the bottom update guard. + updatedTimezone := "UTC" + timezoneUpdateTask := task + timezoneUpdateTask.Timezone = &updatedTimezone + oldSubTaskCfg := subTaskM[source1Name] + syncUnit := syncer.NewSyncer(oldSubTaskCfg, nil, nil) + mockUpdateWorkerClient := pbmock.NewMockWorkerClient(ctrl) + mockUpdateWorkerClient.EXPECT().CheckSubtasksCanUpdate(gomock.Any(), gomock.Any()).DoAndReturn( + func(_ context.Context, req *pb.CheckSubtasksCanUpdateRequest, _ ...grpc.CallOption) (*pb.CheckSubtasksCanUpdateResponse, error) { + newSubTaskCfg := config.NewSubTaskConfig() + decodeErr := newSubTaskCfg.Decode(req.SubtaskCfgTomlString, false) + s.NoError(decodeErr) + if decodeErr != nil { + return &pb.CheckSubtasksCanUpdateResponse{Msg: decodeErr.Error()}, nil + } + s.Equal(updatedTimezone, newSubTaskCfg.Timezone) + + guardErr := syncUnit.CheckCanUpdateCfg(newSubTaskCfg) + s.Error(guardErr) + resp := &pb.CheckSubtasksCanUpdateResponse{Success: guardErr == nil} + if guardErr != nil { + resp.Msg = guardErr.Error() + } + return resp, nil + }, + ) + s1.scheduler.SetWorkerClientForTest(workerName1, newMockRPCClient(mockUpdateWorkerClient)) + timezoneUpdateReq := openapi.UpdateTaskRequest{Task: timezoneUpdateTask} + result = testutil.NewRequest().Put(task1URL).WithJsonBody(timezoneUpdateReq).GoWithHTTPHandler(s.T(), s1.openapiHandles) + s.Equal(http.StatusBadRequest, result.Code()) + var timezoneUpdateErr openapi.ErrorWithMessage + s.NoError(result.UnmarshalBodyToObject(&timezoneUpdateErr)) + s.Equal(int(terror.ErrSchedulerSubTaskCfgUpdate.Code()), timezoneUpdateErr.ErrorCode) + s.Contains(timezoneUpdateErr.ErrorMsg, "fields that should not be changed") + storedSubTaskCfgs := s1.scheduler.GetSubTaskCfgsByTask(task.Name) + s.Equal(*task.Timezone, storedSubTaskCfgs[source1Name].Timezone) // update a task s.NoError(failpoint.Enable("github.com/pingcap/tiflow/dm/master/scheduler/operateCheckSubtasksCanUpdate", `return("success")`)) @@ -1032,6 +1077,7 @@ func (s *OpenAPIViewSuite) TestTaskAPI() { s.NoError(result.UnmarshalBodyToObject(&resultTaskList)) s.Equal(1, resultTaskList.Total) s.Equal(task.Name, resultTaskList.Data[0].Name) + s.EqualValues(task.Timezone, resultTaskList.Data[0].Timezone) s.testImportTaskTemplate(&task, s1) diff --git a/dm/openapi/fixtures/task.go b/dm/openapi/fixtures/task.go index 52d3b347b4..60dc51c3d0 100644 --- a/dm/openapi/fixtures/task.go +++ b/dm/openapi/fixtures/task.go @@ -27,6 +27,7 @@ var ( "meta_schema": "dm_meta", "name": "test", "on_duplicate": "replace", + "timezone": "Asia/Shanghai", "source_config": { "full_migrate_conf": { "data_dir": "./exported_data", diff --git a/dm/openapi/gen.server.go b/dm/openapi/gen.server.go index 0f8d8f8945..d5062dc85d 100644 --- a/dm/openapi/gen.server.go +++ b/dm/openapi/gen.server.go @@ -1258,125 +1258,126 @@ func RegisterHandlersWithOptions(router *gin.Engine, si ServerInterface, options // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", + "wbM78tmU6ro7WXxV5ZI3yeGrUcdG3rFdsdnhFhew9HSKfaWedzpvzn+zui7AeyvL3dmm6Tcn2Jv6um3L", + "vQly5R3r75u+M6lpffe9Ib/vJrWfKkW0JVsrGNA1IgBH6iUlwLNF7vaxoqjSw6RbNwD5tSx9cYVQaldX", + "UsWUU4qJ4CqbniGeJerMQz3eRWCsXjtQ8WBVF1JtUv4+sKrB+pl8qBexPz4+K7thDmB8A9fcvMcWjj8T", + "SW1ArewfahFA3WSBMUMwXI9SxFQR49ACT8UU1RukDAmICQr/bmChpKw4E8UZX9nv95cDjD+T8zzNXWFe", + "LvcG4vJlC7VTBenpBw0kogUCCxRRhsrFY6Gx4ZypKGucQypRJNeXcaQ2QL1kpxYOYCQQAzCOwfE7c4uS", + "6xdj0iWDIQqHYIECKHvSOEQMmGv2HJi3MjJyRegNAYpqQMkYYw85FfNXSKpeOuWvndHvDib1sER2RvQ8", + "Qjj+z1CAtTjFga9KZEvevn/3u7L2nzIBbDVR/34x7Om3HsMuEvh7xrAtq8hzBJzXo8xrzfaJOFZq2PKd", + "EWSPnn/jPMbTj6yYxz0GvryaH/uPqN9haR9Qtfnx8dMumtSyc8kX6jjYTuCRtp7mFvMDo5kw1x1x5e76", + "3bmyd7pikaj401ri+jUJ75ak8Y0w5fcEyjb6dmdR3puKN8yqLPIpv5P09zzPneUlZ7LnA7OS7LeI0YZR", + "r0WMzgXLApGx7zz11Hhq6C+G7EN5TgG9ce5+mnb3T4gqnMctEt80OPOdQ75zyOzPcZaqxLf7zlIrG/qj", + "ZEV45jsrbjz5t8KIDx+itIKCdT78a6X7a47bUG22W60CdqZSncs232Dku1j3rl/5Vpt8x+Bzv8tr1rvp", + "Oyjsi6r5u359Y0fvyZmbO5p6NqNOmnYKL5p+k7JLL3v3RRdN/ZJLvW/DrvMdrb5vsKbZOKQJxES9bjCQ", + "qDYDuGXBoOtBhZAGvV9RMM8mTL5kOLgaKQk80pnPo7LwXEXGDFyWmVr2dqG6wWI1ChMLHjVtE5q80HDR", + "Lv/h9rfb/wkAAP//yy9UmfXEAAA=", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/dm/openapi/gen.types.go b/dm/openapi/gen.types.go index 3c83d19ee6..05b251aaa5 100644 --- a/dm/openapi/gen.types.go +++ b/dm/openapi/gen.types.go @@ -566,6 +566,9 @@ type Task struct { // migrate mode TaskMode TaskTaskMode `json:"task_mode"` + + // task time zone. If omitted or empty, use the downstream database time zone + Timezone *string `json:"timezone,omitempty"` } // Task_BinlogFilterRule defines model for Task.BinlogFilterRule. diff --git a/dm/openapi/spec/dm.yaml b/dm/openapi/spec/dm.yaml index 38fd7aa408..dc2ad259a4 100644 --- a/dm/openapi/spec/dm.yaml +++ b/dm/openapi/spec/dm.yaml @@ -1898,6 +1898,10 @@ components: example: "dm-meta" description: "downstream database for storing meta information" default: "dm-meta" + timezone: + type: string + example: "Asia/Shanghai" + description: "task time zone. If omitted or empty, use the downstream database time zone" enhance_online_schema_change: type: boolean example: true diff --git a/dm/openapi/task.go b/dm/openapi/task.go index dfce6ea79b..ba231334b9 100644 --- a/dm/openapi/task.go +++ b/dm/openapi/task.go @@ -15,6 +15,8 @@ package openapi import ( "encoding/json" + + "github.com/pingcap/tiflow/dm/pkg/utils" ) var defaultMetaSchema = "dm_meta" @@ -24,6 +26,11 @@ func (t *Task) Adjust() error { if t.MetaSchema == nil { t.MetaSchema = &defaultMetaSchema } + if t.Timezone != nil && *t.Timezone != "" { + if _, err := utils.ParseTimeZone(*t.Timezone); err != nil { + return err + } + } return nil } diff --git a/dm/openapi/task_test.go b/dm/openapi/task_test.go index 5b208e10bf..60c64b7189 100644 --- a/dm/openapi/task_test.go +++ b/dm/openapi/task_test.go @@ -17,6 +17,7 @@ import ( "testing" "github.com/pingcap/check" + "github.com/pingcap/tiflow/dm/pkg/terror" ) var _ = check.Suite(&taskSuite{}) @@ -29,13 +30,24 @@ func TestTask(t *testing.T) { func (t *taskSuite) TestTaskAdjust(c *check.C) { meta := "test" + timezone := "Asia/Shanghai" // test no error - task1 := &Task{MetaSchema: &meta, OnDuplicate: TaskOnDuplicateError} + task1 := &Task{MetaSchema: &meta, OnDuplicate: TaskOnDuplicateError, Timezone: &timezone} c.Assert(task1.Adjust(), check.IsNil) c.Assert(*task1.MetaSchema, check.Equals, meta) + c.Assert(*task1.Timezone, check.Equals, timezone) // test default meta task3 := &Task{OnDuplicate: TaskOnDuplicateError} c.Assert(task3.Adjust(), check.IsNil) c.Assert(*task3.MetaSchema, check.Equals, defaultMetaSchema) + + // an explicit empty timezone keeps the downstream database default. + emptyTimezone := "" + task4 := &Task{OnDuplicate: TaskOnDuplicateError, Timezone: &emptyTimezone} + c.Assert(task4.Adjust(), check.IsNil) + + invalidTimezone := "invalid/timezone" + task5 := &Task{OnDuplicate: TaskOnDuplicateError, Timezone: &invalidTimezone} + c.Assert(terror.ErrConfigInvalidTimezone.Equal(task5.Adjust()), check.IsTrue) } diff --git a/dm/syncer/syncer_test.go b/dm/syncer/syncer_test.go index 1366cd9bc2..ed6e07ac49 100644 --- a/dm/syncer/syncer_test.go +++ b/dm/syncer/syncer_test.go @@ -2049,6 +2049,18 @@ func TestCheckCanUpdateCfg(t *testing.T) { require.NoError(t, syncer.CheckCanUpdateCfg(cfg)) } +func TestCheckCanUpdateCfgRejectsTimezoneChange(t *testing.T) { + cfg := genDefaultSubTaskConfig4Test() + cfg.Timezone = "UTC" + syncer := NewSyncer(cfg, nil, nil) + + newCfg := cloneSubTaskConfigForSyncerTest(t, cfg) + newCfg.Timezone = "Asia/Shanghai" + err := syncer.CheckCanUpdateCfg(newCfg) + require.Truef(t, terror.ErrWorkerUpdateSubTaskConfig.Equal(err), "err: %v", err) + require.ErrorContains(t, err, "fields that should not be changed") +} + func TestCheckCanUpdateCfgRejectsForeignKeyChecksDMLBoundaryOptions(t *testing.T) { cfg := genDefaultSubTaskConfig4Test() syncer := NewSyncer(cfg, nil, nil)