// Copyright 2023 PingCAP, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package importer_test import ( "context" "fmt" "math/rand" "net/http/httptest" "net/url" "testing" "time" "github.com/johannesboyne/gofakes3" "github.com/johannesboyne/gofakes3/backend/s3mem" "github.com/pingcap/tidb/br/pkg/streamhelper" "github.com/pingcap/tidb/pkg/executor/importer" "github.com/pingcap/tidb/pkg/infoschema" "github.com/pingcap/tidb/pkg/kv" "github.com/pingcap/tidb/pkg/parser/model" "github.com/pingcap/tidb/pkg/testkit" "github.com/pingcap/tidb/pkg/util/cdcutil" "github.com/pingcap/tidb/pkg/util/dbterror/exeerrors" "github.com/pingcap/tidb/pkg/util/etcd" "github.com/stretchr/testify/require" "github.com/tikv/client-go/v2/util" clientv3 "go.etcd.io/etcd/client/v3" "go.etcd.io/etcd/server/v3/embed" ) const addrFmt = "http://127.0.0.1:%d" func createMockETCD(t *testing.T) (string, *embed.Etcd) { cfg := embed.NewConfig() cfg.Dir = t.TempDir() // rand port in [20000, 60000) randPort := int(rand.Int31n(40000)) + 20000 clientAddr := fmt.Sprintf(addrFmt, randPort) lcurl, _ := url.Parse(clientAddr) cfg.ListenClientUrls, cfg.AdvertiseClientUrls = []url.URL{*lcurl}, []url.URL{*lcurl} lpurl, _ := url.Parse(fmt.Sprintf(addrFmt, randPort+1)) cfg.ListenPeerUrls, cfg.AdvertisePeerUrls = []url.URL{*lpurl}, []url.URL{*lpurl} cfg.InitialCluster = "default=" + lpurl.String() cfg.Logger = "zap" embedEtcd, err := embed.StartEtcd(cfg) require.NoError(t, err) select { case <-embedEtcd.Server.ReadyNotify(): case <-time.After(5 * time.Second): embedEtcd.Server.Stop() // trigger a shutdown require.False(t, true, "server took too long to start") } return clientAddr, embedEtcd } func TestCheckRequirements(t *testing.T) { store := testkit.CreateMockStore(t) tk := testkit.NewTestKit(t, store) ctx := util.WithInternalSourceType(context.Background(), kv.InternalImportInto) conn := tk.Session().GetSQLExecutor() _, err := conn.Execute(ctx, "create table test.t(id int primary key)") require.NoError(t, err) is := tk.Session().GetDomainInfoSchema().(infoschema.InfoSchema) tableObj, err := is.TableByName(context.Background(), model.NewCIStr("test"), model.NewCIStr("t")) require.NoError(t, err) c := &importer.LoadDataController{ Plan: &importer.Plan{ DBName: "test", DataSourceType: importer.DataSourceTypeFile, TableInfo: tableObj.Meta(), }, Table: tableObj, } // create a dummy job _, err = importer.CreateJob(ctx, conn, "test", "tttt", tableObj.Meta().ID, "root", &importer.ImportParameters{}, 0) require.NoError(t, err) // there is active job on the target table already jobID, err := importer.CreateJob(ctx, conn, "test", "t", tableObj.Meta().ID, "root", &importer.ImportParameters{}, 0) require.NoError(t, err) err = c.CheckRequirements(ctx, conn) require.ErrorIs(t, err, exeerrors.ErrLoadDataPreCheckFailed) require.ErrorContains(t, err, "there is active job on the target table already") // cancel the job require.NoError(t, importer.CancelJob(ctx, conn, jobID)) // source data file size = 0 require.ErrorIs(t, c.CheckRequirements(ctx, conn), exeerrors.ErrLoadDataPreCheckFailed) // make checkTotalFileSize pass c.TotalFileSize = 1 // global sort with thread count < 8 c.ThreadCnt = 7 c.CloudStorageURI = "s3://test" err = c.CheckRequirements(ctx, conn) require.ErrorIs(t, err, exeerrors.ErrLoadDataPreCheckFailed) require.ErrorContains(t, err, "global sort requires at least 8 threads") // reset fields, make global sort thread check pass c.ThreadCnt = 1 c.CloudStorageURI = "" // non-empty table _, err = conn.Execute(ctx, "insert into test.t values(1)") require.NoError(t, err) require.ErrorIs(t, c.CheckRequirements(ctx, conn), exeerrors.ErrLoadDataPreCheckFailed) // table not exists _, err = conn.Execute(ctx, "drop table if exists test.t") require.NoError(t, err) require.ErrorContains(t, c.CheckRequirements(ctx, conn), "doesn't exist") // create table again, now checkTableEmpty pass _, err = conn.Execute(ctx, "create table test.t(id int primary key)") require.NoError(t, err) clientAddr, embedEtcd := createMockETCD(t) require.NotNil(t, embedEtcd) t.Cleanup(func() { embedEtcd.Close() }) backup := importer.GetEtcdClient importer.GetEtcdClient = func() (*etcd.Client, error) { etcdCli, err := clientv3.New(clientv3.Config{ Endpoints: []string{clientAddr}, }) require.NoError(t, err) return etcd.NewClient(etcdCli, ""), nil } t.Cleanup(func() { importer.GetEtcdClient = backup }) // mock a PiTR task etcdCli, err := clientv3.New(clientv3.Config{ Endpoints: []string{clientAddr}, }) t.Cleanup(func() { require.NoError(t, etcdCli.Close()) }) require.NoError(t, err) pitrKey := streamhelper.PrefixOfTask() + "test" _, err = etcdCli.Put(ctx, pitrKey, "") require.NoError(t, err) err = c.CheckRequirements(ctx, conn) require.ErrorIs(t, err, exeerrors.ErrLoadDataPreCheckFailed) require.ErrorContains(t, err, "found PiTR log streaming") // disable precheck, should pass c.DisablePrecheck = true require.NoError(t, c.CheckRequirements(ctx, conn)) c.DisablePrecheck = false // revert back // remove PiTR task, and mock a CDC task _, err = etcdCli.Delete(ctx, pitrKey) require.NoError(t, err) // example: /tidb/cdc///changefeed/info/ cdcKey := cdcutil.CDCPrefix + "testcluster/test_ns/changefeed/info/test_cf" _, err = etcdCli.Put(ctx, cdcKey, `{"state":"normal"}`) require.NoError(t, err) err = c.CheckRequirements(ctx, conn) require.ErrorIs(t, err, exeerrors.ErrLoadDataPreCheckFailed) require.ErrorContains(t, err, "found CDC changefeed") // remove CDC task, pass _, err = etcdCli.Delete(ctx, cdcKey) require.NoError(t, err) require.NoError(t, c.CheckRequirements(ctx, conn)) // with global sort c.Plan.ThreadCnt = 8 c.Plan.CloudStorageURI = ":" require.ErrorIs(t, c.CheckRequirements(ctx, conn), exeerrors.ErrLoadDataInvalidURI) c.Plan.CloudStorageURI = "sdsdsdsd://sdsdsdsd" require.ErrorIs(t, c.CheckRequirements(ctx, conn), exeerrors.ErrLoadDataInvalidURI) c.Plan.CloudStorageURI = "local:///tmp" require.ErrorContains(t, c.CheckRequirements(ctx, conn), "unsupported cloud storage uri scheme: local") // this mock cannot mock credential check, so we just skip it. backend := s3mem.New() faker := gofakes3.New(backend) ts := httptest.NewServer(faker.Server()) defer ts.Close() require.NoError(t, backend.CreateBucket("test-bucket")) c.Plan.CloudStorageURI = fmt.Sprintf("s3://test-bucket/path?region=us-east-1&endpoint=%s&access-key=xxxxxx&secret-access-key=xxxxxx", ts.URL) require.NoError(t, c.CheckRequirements(ctx, conn)) }