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  }