github.com/pingcap/br@v5.3.0-alpha.0.20220125034240-ec59c7b6ce30+incompatible/pkg/lightning/lightning_test.go (about) 1 // Copyright 2019 PingCAP, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 14 // Contexts for HTTP requests communicating with a real HTTP server are essential, 15 // however, when the subject is a mocked server, it would probably be redundant. 16 //nolint:noctx 17 18 // lightningSuite.SetUpTest sets up global logger but the gocheck framework calls this method 19 // multi times, hence data race may happen. However, the operation setting up the global logger is idempotent. 20 // Hence in real life the race is harmless. Disable this when race enabled till this get fixed. 21 // +build !race 22 23 package lightning 24 25 import ( 26 "context" 27 "encoding/json" 28 "fmt" 29 "net/http" 30 "path/filepath" 31 "runtime" 32 "strings" 33 "testing" 34 "time" 35 36 "github.com/docker/go-units" 37 . "github.com/pingcap/check" 38 "github.com/pingcap/failpoint" 39 40 "github.com/pingcap/br/pkg/lightning/checkpoints" 41 "github.com/pingcap/br/pkg/lightning/config" 42 "github.com/pingcap/br/pkg/lightning/glue" 43 "github.com/pingcap/br/pkg/lightning/mydump" 44 ) 45 46 type lightningSuite struct{} 47 48 var _ = Suite(&lightningSuite{}) 49 50 func TestLightning(t *testing.T) { 51 TestingT(t) 52 } 53 54 func (s *lightningSuite) TestInitEnv(c *C) { 55 cfg := &config.GlobalConfig{ 56 App: config.GlobalLightning{StatusAddr: ":45678"}, 57 } 58 err := initEnv(cfg) 59 c.Assert(err, IsNil) 60 cfg.App.StatusAddr = "" 61 cfg.App.Config.File = "." 62 err = initEnv(cfg) 63 c.Assert(err, ErrorMatches, "can't use directory as log file name") 64 } 65 66 func (s *lightningSuite) TestRun(c *C) { 67 globalConfig := config.NewGlobalConfig() 68 globalConfig.TiDB.Host = "test.invalid" 69 globalConfig.TiDB.Port = 4000 70 globalConfig.TiDB.PdAddr = "test.invalid:2379" 71 globalConfig.Mydumper.SourceDir = "not-exists" 72 globalConfig.TikvImporter.Backend = config.BackendLocal 73 globalConfig.TikvImporter.SortedKVDir = c.MkDir() 74 lightning := New(globalConfig) 75 cfg := config.NewConfig() 76 err := cfg.LoadFromGlobal(globalConfig) 77 c.Assert(err, IsNil) 78 err = lightning.RunOnce(context.Background(), cfg, nil) 79 c.Assert(err, ErrorMatches, ".*mydumper dir does not exist") 80 81 path, _ := filepath.Abs(".") 82 ctx := context.Background() 83 invalidGlue := glue.NewExternalTiDBGlue(nil, 0) 84 err = lightning.run(ctx, &config.Config{ 85 Mydumper: config.MydumperRuntime{ 86 SourceDir: "file://" + filepath.ToSlash(path), 87 Filter: []string{"*.*"}, 88 DefaultFileRules: true, 89 }, 90 Checkpoint: config.Checkpoint{ 91 Enable: true, 92 Driver: "invalid", 93 }, 94 }, invalidGlue) 95 c.Assert(err, ErrorMatches, "open checkpoint db failed: Unknown checkpoint driver invalid") 96 97 err = lightning.run(ctx, &config.Config{ 98 Mydumper: config.MydumperRuntime{ 99 SourceDir: ".", 100 Filter: []string{"*.*"}, 101 }, 102 Checkpoint: config.Checkpoint{ 103 Enable: true, 104 Driver: "file", 105 DSN: "any-file", 106 }, 107 }, invalidGlue) 108 c.Assert(err, NotNil) 109 } 110 111 var _ = Suite(&lightningServerSuite{}) 112 113 type lightningServerSuite struct { 114 lightning *Lightning 115 taskCfgCh chan *config.Config 116 } 117 118 func (s *lightningServerSuite) SetUpTest(c *C) { 119 cfg := config.NewGlobalConfig() 120 cfg.TiDB.Host = "test.invalid" 121 cfg.TiDB.Port = 4000 122 cfg.TiDB.PdAddr = "test.invalid:2379" 123 cfg.App.ServerMode = true 124 cfg.App.StatusAddr = "127.0.0.1:0" 125 cfg.Mydumper.SourceDir = "file://." 126 cfg.TikvImporter.Backend = config.BackendLocal 127 cfg.TikvImporter.SortedKVDir = c.MkDir() 128 129 s.lightning = New(cfg) 130 s.taskCfgCh = make(chan *config.Config) 131 s.lightning.ctx = context.WithValue(s.lightning.ctx, &taskCfgRecorderKey, s.taskCfgCh) 132 _ = s.lightning.GoServe() 133 134 _ = failpoint.Enable("github.com/pingcap/br/pkg/lightning/SkipRunTask", "return") 135 } 136 137 func (s *lightningServerSuite) TearDownTest(c *C) { 138 _ = failpoint.Disable("github.com/pingcap/br/pkg/lightning/SkipRunTask") 139 s.lightning.Stop() 140 } 141 142 func (s *lightningServerSuite) TestRunServer(c *C) { 143 url := "http://" + s.lightning.serverAddr.String() + "/tasks" 144 145 resp, err := http.Post(url, "application/toml", strings.NewReader("????")) 146 c.Assert(err, IsNil) 147 c.Assert(resp.StatusCode, Equals, http.StatusNotImplemented) 148 var data map[string]string 149 err = json.NewDecoder(resp.Body).Decode(&data) 150 c.Assert(err, IsNil) 151 c.Assert(data, HasKey, "error") 152 c.Assert(data["error"], Equals, "server-mode not enabled") 153 resp.Body.Close() 154 155 go func() { 156 _ = s.lightning.RunServer() 157 }() 158 time.Sleep(100 * time.Millisecond) 159 160 req, err := http.NewRequest(http.MethodPut, url, nil) 161 c.Assert(err, IsNil) 162 resp, err = http.DefaultClient.Do(req) 163 c.Assert(err, IsNil) 164 c.Assert(resp.StatusCode, Equals, http.StatusMethodNotAllowed) 165 c.Assert(resp.Header.Get("Allow"), Matches, ".*"+http.MethodPost+".*") 166 resp.Body.Close() 167 168 resp, err = http.Post(url, "application/toml", strings.NewReader("????")) 169 c.Assert(err, IsNil) 170 c.Assert(resp.StatusCode, Equals, http.StatusBadRequest) 171 err = json.NewDecoder(resp.Body).Decode(&data) 172 c.Assert(err, IsNil) 173 c.Assert(data, HasKey, "error") 174 c.Assert(data["error"], Matches, "cannot parse task.*") 175 resp.Body.Close() 176 177 resp, err = http.Post(url, "application/toml", strings.NewReader("[mydumper.csv]\nseparator = 'fooo'\ndelimiter= 'foo'")) 178 c.Assert(err, IsNil) 179 c.Assert(resp.StatusCode, Equals, http.StatusBadRequest) 180 err = json.NewDecoder(resp.Body).Decode(&data) 181 c.Assert(err, IsNil) 182 c.Assert(data, HasKey, "error") 183 c.Assert(data["error"], Matches, "invalid task configuration:.*") 184 resp.Body.Close() 185 186 for i := 0; i < 20; i++ { 187 resp, err = http.Post(url, "application/toml", strings.NewReader(fmt.Sprintf(` 188 [mydumper] 189 data-source-dir = 'file://demo-path-%d' 190 [mydumper.csv] 191 separator = '/' 192 `, i))) 193 c.Assert(err, IsNil) 194 c.Assert(resp.StatusCode, Equals, http.StatusOK) 195 var result map[string]int 196 err = json.NewDecoder(resp.Body).Decode(&result) 197 resp.Body.Close() 198 c.Assert(err, IsNil) 199 c.Assert(result, HasKey, "id") 200 201 select { 202 case taskCfg := <-s.taskCfgCh: 203 c.Assert(taskCfg.TiDB.Host, Equals, "test.invalid") 204 c.Assert(taskCfg.Mydumper.SourceDir, Equals, fmt.Sprintf("file://demo-path-%d", i)) 205 c.Assert(taskCfg.Mydumper.CSV.Separator, Equals, "/") 206 case <-time.After(5 * time.Second): 207 c.Fatalf("task is not queued after 5 seconds (i = %d)", i) 208 } 209 } 210 } 211 212 func (s *lightningServerSuite) TestGetDeleteTask(c *C) { 213 url := "http://" + s.lightning.serverAddr.String() + "/tasks" 214 215 type getAllResultType struct { 216 Current int64 217 Queue []int64 218 } 219 220 getAllTasks := func() (result getAllResultType) { 221 resp, err := http.Get(url) 222 c.Assert(err, IsNil) 223 c.Assert(resp.StatusCode, Equals, http.StatusOK) 224 err = json.NewDecoder(resp.Body).Decode(&result) 225 resp.Body.Close() 226 c.Assert(err, IsNil) 227 return 228 } 229 230 postTask := func(i int) int64 { 231 resp, err := http.Post(url, "application/toml", strings.NewReader(fmt.Sprintf(` 232 [mydumper] 233 data-source-dir = 'file://demo-path-%d' 234 `, i))) 235 c.Assert(err, IsNil) 236 c.Assert(resp.StatusCode, Equals, http.StatusOK) 237 var result struct{ ID int64 } 238 err = json.NewDecoder(resp.Body).Decode(&result) 239 resp.Body.Close() 240 c.Assert(err, IsNil) 241 return result.ID 242 } 243 244 go func() { 245 _ = s.lightning.RunServer() 246 }() 247 time.Sleep(500 * time.Millisecond) 248 249 // Check `GET /tasks` without any active tasks 250 251 c.Assert(getAllTasks(), DeepEquals, getAllResultType{ 252 Current: 0, 253 Queue: []int64{}, 254 }) 255 256 first := postTask(1) 257 second := postTask(2) 258 third := postTask(3) 259 260 c.Assert(first, Not(Equals), 123456) 261 c.Assert(second, Not(Equals), 123456) 262 c.Assert(third, Not(Equals), 123456) 263 264 // Check `GET /tasks` returns all tasks currently running 265 266 time.Sleep(100 * time.Millisecond) 267 c.Assert(getAllTasks(), DeepEquals, getAllResultType{ 268 Current: first, 269 Queue: []int64{second, third}, 270 }) 271 272 // Check `GET /tasks/abcdef` returns error 273 274 resp, err := http.Get(url + "/abcdef") 275 c.Assert(err, IsNil) 276 c.Assert(resp.StatusCode, Equals, http.StatusBadRequest) 277 resp.Body.Close() 278 279 // Check `GET /tasks/123456` returns not found 280 281 resp, err = http.Get(url + "/123456") 282 c.Assert(err, IsNil) 283 c.Assert(resp.StatusCode, Equals, http.StatusNotFound) 284 resp.Body.Close() 285 286 // Check `GET /tasks/1` returns the desired cfg 287 288 var resCfg config.Config 289 290 resp, err = http.Get(fmt.Sprintf("%s/%d", url, second)) 291 c.Assert(err, IsNil) 292 c.Assert(resp.StatusCode, Equals, http.StatusOK) 293 err = json.NewDecoder(resp.Body).Decode(&resCfg) 294 resp.Body.Close() 295 c.Assert(err, IsNil) 296 c.Assert(resCfg.Mydumper.SourceDir, Equals, "file://demo-path-2") 297 298 resp, err = http.Get(fmt.Sprintf("%s/%d", url, first)) 299 c.Assert(err, IsNil) 300 c.Assert(resp.StatusCode, Equals, http.StatusOK) 301 err = json.NewDecoder(resp.Body).Decode(&resCfg) 302 resp.Body.Close() 303 c.Assert(err, IsNil) 304 c.Assert(resCfg.Mydumper.SourceDir, Equals, "file://demo-path-1") 305 306 // Check `DELETE /tasks` returns error. 307 308 req, err := http.NewRequest(http.MethodDelete, url, nil) 309 c.Assert(err, IsNil) 310 resp, err = http.DefaultClient.Do(req) 311 c.Assert(err, IsNil) 312 c.Assert(resp.StatusCode, Equals, http.StatusBadRequest) 313 resp.Body.Close() 314 315 // Check `DELETE /tasks/` returns error. 316 317 req.URL.Path = "/tasks/" 318 resp, err = http.DefaultClient.Do(req) 319 c.Assert(err, IsNil) 320 c.Assert(resp.StatusCode, Equals, http.StatusBadRequest) 321 resp.Body.Close() 322 323 // Check `DELETE /tasks/(not a number)` returns error. 324 325 req.URL.Path = "/tasks/abcdef" 326 resp, err = http.DefaultClient.Do(req) 327 c.Assert(err, IsNil) 328 c.Assert(resp.StatusCode, Equals, http.StatusBadRequest) 329 resp.Body.Close() 330 331 // Check `DELETE /tasks/123456` returns not found 332 333 req.URL.Path = "/tasks/123456" 334 resp, err = http.DefaultClient.Do(req) 335 c.Assert(err, IsNil) 336 c.Assert(resp.StatusCode, Equals, http.StatusNotFound) 337 resp.Body.Close() 338 339 // Cancel a queued task, then verify the task list. 340 341 req.URL.Path = fmt.Sprintf("/tasks/%d", second) 342 resp, err = http.DefaultClient.Do(req) 343 c.Assert(err, IsNil) 344 c.Assert(resp.StatusCode, Equals, http.StatusOK) 345 resp.Body.Close() 346 347 c.Assert(getAllTasks(), DeepEquals, getAllResultType{ 348 Current: first, 349 Queue: []int64{third}, 350 }) 351 352 // Cancel a running task, then verify the task list. 353 354 req.URL.Path = fmt.Sprintf("/tasks/%d", first) 355 resp, err = http.DefaultClient.Do(req) 356 c.Assert(err, IsNil) 357 c.Assert(resp.StatusCode, Equals, http.StatusOK) 358 resp.Body.Close() 359 360 time.Sleep(100 * time.Millisecond) 361 c.Assert(getAllTasks(), DeepEquals, getAllResultType{ 362 Current: third, 363 Queue: []int64{}, 364 }) 365 } 366 367 func (s *lightningServerSuite) TestHTTPAPIOutsideServerMode(c *C) { 368 s.lightning.globalCfg.App.ServerMode = false 369 370 url := "http://" + s.lightning.serverAddr.String() + "/tasks" 371 372 errCh := make(chan error) 373 cfg := config.NewConfig() 374 err := cfg.LoadFromGlobal(s.lightning.globalCfg) 375 c.Assert(err, IsNil) 376 go func() { 377 errCh <- s.lightning.RunOnce(s.lightning.ctx, cfg, nil) 378 }() 379 time.Sleep(600 * time.Millisecond) 380 381 var curTask struct { 382 Current int64 383 Queue []int64 384 } 385 386 // `GET /tasks` should work fine. 387 resp, err := http.Get(url) 388 c.Assert(err, IsNil) 389 c.Assert(resp.StatusCode, Equals, http.StatusOK) 390 err = json.NewDecoder(resp.Body).Decode(&curTask) 391 resp.Body.Close() 392 c.Assert(err, IsNil) 393 c.Assert(curTask.Current, Not(Equals), int64(0)) 394 c.Assert(curTask.Queue, HasLen, 0) 395 396 // `POST /tasks` should return 501 397 resp, err = http.Post(url, "application/toml", strings.NewReader("??????")) 398 c.Assert(err, IsNil) 399 c.Assert(resp.StatusCode, Equals, http.StatusNotImplemented) 400 resp.Body.Close() 401 402 // `GET /tasks/(current)` should work fine. 403 resp, err = http.Get(fmt.Sprintf("%s/%d", url, curTask.Current)) 404 c.Assert(err, IsNil) 405 c.Assert(resp.StatusCode, Equals, http.StatusOK) 406 resp.Body.Close() 407 408 // `GET /tasks/123456` should return 404 409 resp, err = http.Get(url + "/123456") 410 c.Assert(err, IsNil) 411 c.Assert(resp.StatusCode, Equals, http.StatusNotFound) 412 resp.Body.Close() 413 414 // `PATCH /tasks/(current)/front` should return 501 415 req, err := http.NewRequest(http.MethodPatch, fmt.Sprintf("%s/%d/front", url, curTask.Current), nil) 416 c.Assert(err, IsNil) 417 resp, err = http.DefaultClient.Do(req) 418 c.Assert(err, IsNil) 419 c.Assert(resp.StatusCode, Equals, http.StatusNotImplemented) 420 resp.Body.Close() 421 422 // `DELETE /tasks/123456` should return 404 423 req.Method = http.MethodDelete 424 req.URL.Path = "/tasks/123456" 425 resp, err = http.DefaultClient.Do(req) 426 c.Assert(err, IsNil) 427 c.Assert(resp.StatusCode, Equals, http.StatusNotFound) 428 resp.Body.Close() 429 430 // `DELETE /tasks/(current)` should return 200 431 req.URL.Path = fmt.Sprintf("/tasks/%d", curTask.Current) 432 resp, err = http.DefaultClient.Do(req) 433 c.Assert(err, IsNil) 434 c.Assert(resp.StatusCode, Equals, http.StatusOK) 435 c.Assert(resp.Body.Close(), IsNil) 436 // ... and the task should be canceled now. 437 c.Assert(<-errCh, Equals, context.Canceled) 438 } 439 440 func (s *lightningServerSuite) TestCheckSystemRequirement(c *C) { 441 if runtime.GOOS == "windows" { 442 c.Skip("Local-backend is not supported on Windows") 443 return 444 } 445 446 cfg := config.NewConfig() 447 cfg.App.CheckRequirements = true 448 cfg.App.TableConcurrency = 4 449 cfg.TikvImporter.Backend = config.BackendLocal 450 cfg.TikvImporter.LocalWriterMemCacheSize = 128 * units.MiB 451 cfg.TikvImporter.RangeConcurrency = 16 452 453 dbMetas := []*mydump.MDDatabaseMeta{ 454 { 455 Tables: []*mydump.MDTableMeta{ 456 { 457 TotalSize: 500 << 20, 458 }, 459 { 460 TotalSize: 150_000 << 20, 461 }, 462 }, 463 }, 464 { 465 Tables: []*mydump.MDTableMeta{ 466 { 467 TotalSize: 150_800 << 20, 468 }, 469 { 470 TotalSize: 35 << 20, 471 }, 472 { 473 TotalSize: 100_000 << 20, 474 }, 475 }, 476 }, 477 { 478 Tables: []*mydump.MDTableMeta{ 479 { 480 TotalSize: 240 << 20, 481 }, 482 { 483 TotalSize: 124_000 << 20, 484 }, 485 }, 486 }, 487 } 488 489 // with max open files 1024, the max table size will be: 65536MB 490 err := failpoint.Enable("github.com/pingcap/br/pkg/lightning/backend/local/GetRlimitValue", "return(139439)") 491 c.Assert(err, IsNil) 492 err = failpoint.Enable("github.com/pingcap/br/pkg/lightning/backend/local/SetRlimitError", "return(true)") 493 c.Assert(err, IsNil) 494 defer func() { 495 _ = failpoint.Disable("github.com/pingcap/br/pkg/lightning/backend/local/SetRlimitError") 496 }() 497 // with this dbMetas, the estimated fds will be 139440, so should return error 498 err = checkSystemRequirement(cfg, dbMetas) 499 c.Assert(err, NotNil) 500 501 err = failpoint.Disable("github.com/pingcap/br/pkg/lightning/backend/local/GetRlimitValue") 502 c.Assert(err, IsNil) 503 504 // the min rlimit should be bigger than the default min value (16384) 505 err = failpoint.Enable("github.com/pingcap/br/pkg/lightning/backend/local/GetRlimitValue", "return(139440)") 506 defer func() { 507 _ = failpoint.Disable("github.com/pingcap/br/pkg/lightning/backend/local/GetRlimitValue") 508 }() 509 c.Assert(err, IsNil) 510 err = checkSystemRequirement(cfg, dbMetas) 511 c.Assert(err, IsNil) 512 } 513 514 func (s *lightningServerSuite) TestCheckSchemaConflict(c *C) { 515 cfg := config.NewConfig() 516 cfg.Checkpoint.Schema = "cp" 517 cfg.Checkpoint.Driver = config.CheckpointDriverMySQL 518 519 dbMetas := []*mydump.MDDatabaseMeta{ 520 { 521 Name: "test", 522 Tables: []*mydump.MDTableMeta{ 523 { 524 Name: checkpoints.CheckpointTableNameTable, 525 }, 526 { 527 Name: checkpoints.CheckpointTableNameEngine, 528 }, 529 }, 530 }, 531 { 532 Name: "cp", 533 Tables: []*mydump.MDTableMeta{ 534 { 535 Name: "test", 536 }, 537 }, 538 }, 539 } 540 err := checkSchemaConflict(cfg, dbMetas) 541 c.Assert(err, IsNil) 542 543 dbMetas = append(dbMetas, &mydump.MDDatabaseMeta{ 544 Name: "cp", 545 Tables: []*mydump.MDTableMeta{ 546 { 547 Name: checkpoints.CheckpointTableNameChunk, 548 }, 549 { 550 Name: "test123", 551 }, 552 }, 553 }) 554 err = checkSchemaConflict(cfg, dbMetas) 555 c.Assert(err, NotNil) 556 557 cfg.Checkpoint.Enable = false 558 err = checkSchemaConflict(cfg, dbMetas) 559 c.Assert(err, IsNil) 560 561 cfg.Checkpoint.Enable = true 562 cfg.Checkpoint.Driver = config.CheckpointDriverFile 563 err = checkSchemaConflict(cfg, dbMetas) 564 c.Assert(err, IsNil) 565 }