github.com/pingcap/br@v5.3.0-alpha.0.20220125034240-ec59c7b6ce30+incompatible/pkg/lightning/lightning.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  package lightning
    15  
    16  import (
    17  	"compress/gzip"
    18  	"context"
    19  	"encoding/json"
    20  	"fmt"
    21  	"io"
    22  	"net"
    23  	"net/http"
    24  	"net/http/pprof"
    25  	"os"
    26  	"sort"
    27  	"strconv"
    28  	"strings"
    29  	"sync"
    30  	"time"
    31  
    32  	"github.com/pingcap/errors"
    33  	"github.com/pingcap/failpoint"
    34  	"github.com/prometheus/client_golang/prometheus/promhttp"
    35  	"github.com/shurcooL/httpgzip"
    36  	"go.uber.org/zap"
    37  	"go.uber.org/zap/zapcore"
    38  
    39  	"github.com/pingcap/br/pkg/lightning/backend/local"
    40  	"github.com/pingcap/br/pkg/lightning/checkpoints"
    41  	"github.com/pingcap/br/pkg/lightning/common"
    42  	"github.com/pingcap/br/pkg/lightning/config"
    43  	"github.com/pingcap/br/pkg/lightning/glue"
    44  	"github.com/pingcap/br/pkg/lightning/log"
    45  	"github.com/pingcap/br/pkg/lightning/mydump"
    46  	"github.com/pingcap/br/pkg/lightning/restore"
    47  	"github.com/pingcap/br/pkg/lightning/web"
    48  	"github.com/pingcap/br/pkg/redact"
    49  	"github.com/pingcap/br/pkg/storage"
    50  	"github.com/pingcap/br/pkg/utils"
    51  	"github.com/pingcap/br/pkg/version/build"
    52  )
    53  
    54  type Lightning struct {
    55  	globalCfg *config.GlobalConfig
    56  	globalTLS *common.TLS
    57  	// taskCfgs is the list of task configurations enqueued in the server mode
    58  	taskCfgs   *config.List
    59  	ctx        context.Context
    60  	shutdown   context.CancelFunc // for whole lightning context
    61  	server     http.Server
    62  	serverAddr net.Addr
    63  	serverLock sync.Mutex
    64  
    65  	cancelLock sync.Mutex
    66  	curTask    *config.Config
    67  	cancel     context.CancelFunc // for per task context, which maybe different from lightning context
    68  }
    69  
    70  func initEnv(cfg *config.GlobalConfig) error {
    71  	return log.InitLogger(&cfg.App.Config, cfg.TiDB.LogLevel)
    72  }
    73  
    74  func New(globalCfg *config.GlobalConfig) *Lightning {
    75  	if err := initEnv(globalCfg); err != nil {
    76  		fmt.Println("Failed to initialize environment:", err)
    77  		os.Exit(1)
    78  	}
    79  
    80  	tls, err := common.NewTLS(globalCfg.Security.CAPath, globalCfg.Security.CertPath, globalCfg.Security.KeyPath, globalCfg.App.StatusAddr)
    81  	if err != nil {
    82  		log.L().Fatal("failed to load TLS certificates", zap.Error(err))
    83  	}
    84  
    85  	redact.InitRedact(globalCfg.Security.RedactInfoLog)
    86  
    87  	ctx, shutdown := context.WithCancel(context.Background())
    88  	return &Lightning{
    89  		globalCfg: globalCfg,
    90  		globalTLS: tls,
    91  		ctx:       ctx,
    92  		shutdown:  shutdown,
    93  	}
    94  }
    95  
    96  func (l *Lightning) GoServe() error {
    97  	handleSigUsr1(func() {
    98  		l.serverLock.Lock()
    99  		statusAddr := l.globalCfg.App.StatusAddr
   100  		shouldStartServer := len(statusAddr) == 0
   101  		if shouldStartServer {
   102  			l.globalCfg.App.StatusAddr = ":"
   103  		}
   104  		l.serverLock.Unlock()
   105  
   106  		if shouldStartServer {
   107  			// open a random port and start the server if SIGUSR1 is received.
   108  			if err := l.goServe(":", os.Stderr); err != nil {
   109  				log.L().Warn("failed to start HTTP server", log.ShortError(err))
   110  			}
   111  		} else {
   112  			// just prints the server address if it is already started.
   113  			log.L().Info("already started HTTP server", zap.Stringer("address", l.serverAddr))
   114  		}
   115  	})
   116  
   117  	l.serverLock.Lock()
   118  	statusAddr := l.globalCfg.App.StatusAddr
   119  	l.serverLock.Unlock()
   120  
   121  	if len(statusAddr) == 0 {
   122  		return nil
   123  	}
   124  	return l.goServe(statusAddr, io.Discard)
   125  }
   126  
   127  func (l *Lightning) goServe(statusAddr string, realAddrWriter io.Writer) error {
   128  	mux := http.NewServeMux()
   129  	mux.Handle("/", http.RedirectHandler("/web/", http.StatusFound))
   130  	mux.Handle("/metrics", promhttp.Handler())
   131  
   132  	mux.HandleFunc("/debug/pprof/", pprof.Index)
   133  	mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
   134  	mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
   135  	mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
   136  	mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
   137  
   138  	handleTasks := http.StripPrefix("/tasks", http.HandlerFunc(l.handleTask))
   139  	mux.Handle("/tasks", handleTasks)
   140  	mux.Handle("/tasks/", handleTasks)
   141  	mux.HandleFunc("/progress/task", handleProgressTask)
   142  	mux.HandleFunc("/progress/table", handleProgressTable)
   143  	mux.HandleFunc("/pause", handlePause)
   144  	mux.HandleFunc("/resume", handleResume)
   145  	mux.HandleFunc("/loglevel", handleLogLevel)
   146  
   147  	mux.Handle("/web/", http.StripPrefix("/web", httpgzip.FileServer(web.Res, httpgzip.FileServerOptions{
   148  		IndexHTML: true,
   149  		ServeError: func(w http.ResponseWriter, req *http.Request, err error) {
   150  			if os.IsNotExist(err) && !strings.Contains(req.URL.Path, ".") {
   151  				http.Redirect(w, req, "/web/", http.StatusFound)
   152  			} else {
   153  				httpgzip.NonSpecific(w, req, err)
   154  			}
   155  		},
   156  	})))
   157  
   158  	listener, err := net.Listen("tcp", statusAddr)
   159  	if err != nil {
   160  		return err
   161  	}
   162  	l.serverAddr = listener.Addr()
   163  	log.L().Info("starting HTTP server", zap.Stringer("address", l.serverAddr))
   164  	fmt.Fprintln(realAddrWriter, "started HTTP server on", l.serverAddr)
   165  	l.server.Handler = mux
   166  	listener = l.globalTLS.WrapListener(listener)
   167  
   168  	go func() {
   169  		err := l.server.Serve(listener)
   170  		log.L().Info("stopped HTTP server", log.ShortError(err))
   171  	}()
   172  	return nil
   173  }
   174  
   175  // RunOnce is used by binary lightning and host when using lightning as a library.
   176  // - for binary lightning, taskCtx could be context.Background which means taskCtx wouldn't be canceled directly by its
   177  //   cancel function, but only by Lightning.Stop or HTTP DELETE using l.cancel. and glue could be nil to let lightning
   178  //   use a default glue later.
   179  // - for lightning as a library, taskCtx could be a meaningful context that get canceled outside, and glue could be a
   180  //   caller implemented glue.
   181  func (l *Lightning) RunOnce(taskCtx context.Context, taskCfg *config.Config, glue glue.Glue) error {
   182  	if err := taskCfg.Adjust(taskCtx); err != nil {
   183  		return err
   184  	}
   185  
   186  	taskCfg.TaskID = time.Now().UnixNano()
   187  	failpoint.Inject("SetTaskID", func(val failpoint.Value) {
   188  		taskCfg.TaskID = int64(val.(int))
   189  	})
   190  
   191  	return l.run(taskCtx, taskCfg, glue)
   192  }
   193  
   194  func (l *Lightning) RunServer() error {
   195  	l.taskCfgs = config.NewConfigList()
   196  	log.L().Info(
   197  		"Lightning server is running, post to /tasks to start an import task",
   198  		zap.Stringer("address", l.serverAddr),
   199  	)
   200  
   201  	for {
   202  		task, err := l.taskCfgs.Pop(l.ctx)
   203  		if err != nil {
   204  			return err
   205  		}
   206  		err = l.run(context.Background(), task, nil)
   207  		if err != nil {
   208  			restore.DeliverPauser.Pause() // force pause the progress on error
   209  			log.L().Error("tidb lightning encountered error", zap.Error(err))
   210  		}
   211  	}
   212  }
   213  
   214  var taskCfgRecorderKey struct{}
   215  
   216  func (l *Lightning) run(taskCtx context.Context, taskCfg *config.Config, g glue.Glue) (err error) {
   217  	build.LogInfo(build.Lightning)
   218  	log.L().Info("cfg", zap.Stringer("cfg", taskCfg))
   219  
   220  	utils.LogEnvVariables()
   221  
   222  	ctx, cancel := context.WithCancel(taskCtx)
   223  	l.cancelLock.Lock()
   224  	l.cancel = cancel
   225  	l.curTask = taskCfg
   226  	l.cancelLock.Unlock()
   227  	web.BroadcastStartTask()
   228  
   229  	defer func() {
   230  		cancel()
   231  		l.cancelLock.Lock()
   232  		l.cancel = nil
   233  		l.cancelLock.Unlock()
   234  		web.BroadcastEndTask(err)
   235  	}()
   236  
   237  	failpoint.Inject("SkipRunTask", func() {
   238  		if recorder, ok := l.ctx.Value(&taskCfgRecorderKey).(chan *config.Config); ok {
   239  			select {
   240  			case recorder <- taskCfg:
   241  			case <-ctx.Done():
   242  				failpoint.Return(ctx.Err())
   243  			}
   244  		}
   245  		failpoint.Return(nil)
   246  	})
   247  
   248  	if err := taskCfg.TiDB.Security.RegisterMySQL(); err != nil {
   249  		return err
   250  	}
   251  	defer func() {
   252  		// deregister TLS config with name "cluster"
   253  		if taskCfg.TiDB.Security == nil {
   254  			return
   255  		}
   256  		taskCfg.TiDB.Security.CAPath = ""
   257  		if err := taskCfg.TiDB.Security.RegisterMySQL(); err != nil {
   258  			log.L().Warn("failed to deregister TLS config", log.ShortError(err))
   259  		}
   260  	}()
   261  
   262  	// initiation of default glue should be after RegisterMySQL, which is ready to be called after taskCfg.Adjust
   263  	// and also put it here could avoid injecting another two SkipRunTask failpoint to caller
   264  	if g == nil {
   265  		db, err := restore.DBFromConfig(taskCfg.TiDB)
   266  		if err != nil {
   267  			return err
   268  		}
   269  		g = glue.NewExternalTiDBGlue(db, taskCfg.TiDB.SQLMode)
   270  	}
   271  
   272  	u, err := storage.ParseBackend(taskCfg.Mydumper.SourceDir, nil)
   273  	if err != nil {
   274  		return errors.Annotate(err, "parse backend failed")
   275  	}
   276  	s, err := storage.New(ctx, u, &storage.ExternalStorageOptions{})
   277  	if err != nil {
   278  		return errors.Annotate(err, "create storage failed")
   279  	}
   280  
   281  	loadTask := log.L().Begin(zap.InfoLevel, "load data source")
   282  	var mdl *mydump.MDLoader
   283  	mdl, err = mydump.NewMyDumpLoaderWithStore(ctx, taskCfg, s)
   284  	loadTask.End(zap.ErrorLevel, err)
   285  	if err != nil {
   286  		return errors.Trace(err)
   287  	}
   288  	err = checkSystemRequirement(taskCfg, mdl.GetDatabases())
   289  	if err != nil {
   290  		log.L().Error("check system requirements failed", zap.Error(err))
   291  		return errors.Trace(err)
   292  	}
   293  	// check table schema conflicts
   294  	err = checkSchemaConflict(taskCfg, mdl.GetDatabases())
   295  	if err != nil {
   296  		log.L().Error("checkpoint schema conflicts with data files", zap.Error(err))
   297  		return errors.Trace(err)
   298  	}
   299  
   300  	dbMetas := mdl.GetDatabases()
   301  	web.BroadcastInitProgress(dbMetas)
   302  
   303  	var procedure *restore.Controller
   304  	procedure, err = restore.NewRestoreController(ctx, dbMetas, taskCfg, s, g)
   305  	if err != nil {
   306  		log.L().Error("restore failed", log.ShortError(err))
   307  		return errors.Trace(err)
   308  	}
   309  	defer procedure.Close()
   310  
   311  	err = procedure.Run(ctx)
   312  	return errors.Trace(err)
   313  }
   314  
   315  func (l *Lightning) Stop() {
   316  	l.cancelLock.Lock()
   317  	if l.cancel != nil {
   318  		l.cancel()
   319  	}
   320  	l.cancelLock.Unlock()
   321  	if err := l.server.Shutdown(l.ctx); err != nil {
   322  		log.L().Warn("failed to shutdown HTTP server", log.ShortError(err))
   323  	}
   324  	l.shutdown()
   325  }
   326  
   327  func writeJSONError(w http.ResponseWriter, code int, prefix string, err error) {
   328  	type errorResponse struct {
   329  		Error string `json:"error"`
   330  	}
   331  
   332  	w.WriteHeader(code)
   333  
   334  	if err != nil {
   335  		prefix += ": " + err.Error()
   336  	}
   337  	_ = json.NewEncoder(w).Encode(errorResponse{Error: prefix})
   338  }
   339  
   340  func parseTaskID(req *http.Request) (int64, string, error) {
   341  	path := strings.TrimPrefix(req.URL.Path, "/")
   342  	taskIDString := path
   343  	verb := ""
   344  	if i := strings.IndexByte(path, '/'); i >= 0 {
   345  		taskIDString = path[:i]
   346  		verb = path[i+1:]
   347  	}
   348  
   349  	taskID, err := strconv.ParseInt(taskIDString, 10, 64)
   350  	if err != nil {
   351  		return 0, "", err
   352  	}
   353  
   354  	return taskID, verb, nil
   355  }
   356  
   357  func (l *Lightning) handleTask(w http.ResponseWriter, req *http.Request) {
   358  	w.Header().Set("Content-Type", "application/json")
   359  
   360  	switch req.Method {
   361  	case http.MethodGet:
   362  		taskID, _, err := parseTaskID(req)
   363  		// golint tells us to refactor this with switch stmt.
   364  		// However switch stmt doesn't support init-statements,
   365  		// hence if we follow it things might be worse.
   366  		// Anyway, this chain of if-else isn't unacceptable.
   367  		//nolint:gocritic
   368  		if e, ok := err.(*strconv.NumError); ok && e.Num == "" {
   369  			l.handleGetTask(w)
   370  		} else if err == nil {
   371  			l.handleGetOneTask(w, req, taskID)
   372  		} else {
   373  			writeJSONError(w, http.StatusBadRequest, "invalid task ID", err)
   374  		}
   375  	case http.MethodPost:
   376  		l.handlePostTask(w, req)
   377  	case http.MethodDelete:
   378  		l.handleDeleteOneTask(w, req)
   379  	case http.MethodPatch:
   380  		l.handlePatchOneTask(w, req)
   381  	default:
   382  		w.Header().Set("Allow", http.MethodGet+", "+http.MethodPost+", "+http.MethodDelete+", "+http.MethodPatch)
   383  		writeJSONError(w, http.StatusMethodNotAllowed, "only GET, POST, DELETE and PATCH are allowed", nil)
   384  	}
   385  }
   386  
   387  func (l *Lightning) handleGetTask(w http.ResponseWriter) {
   388  	var response struct {
   389  		Current   *int64  `json:"current"`
   390  		QueuedIDs []int64 `json:"queue"`
   391  	}
   392  
   393  	if l.taskCfgs != nil {
   394  		response.QueuedIDs = l.taskCfgs.AllIDs()
   395  	} else {
   396  		response.QueuedIDs = []int64{}
   397  	}
   398  
   399  	l.cancelLock.Lock()
   400  	if l.cancel != nil && l.curTask != nil {
   401  		response.Current = new(int64)
   402  		*response.Current = l.curTask.TaskID
   403  	}
   404  	l.cancelLock.Unlock()
   405  
   406  	w.WriteHeader(http.StatusOK)
   407  	_ = json.NewEncoder(w).Encode(response)
   408  }
   409  
   410  func (l *Lightning) handleGetOneTask(w http.ResponseWriter, req *http.Request, taskID int64) {
   411  	var task *config.Config
   412  
   413  	l.cancelLock.Lock()
   414  	if l.curTask != nil && l.curTask.TaskID == taskID {
   415  		task = l.curTask
   416  	}
   417  	l.cancelLock.Unlock()
   418  
   419  	if task == nil && l.taskCfgs != nil {
   420  		task, _ = l.taskCfgs.Get(taskID)
   421  	}
   422  
   423  	if task == nil {
   424  		writeJSONError(w, http.StatusNotFound, "task ID not found", nil)
   425  		return
   426  	}
   427  
   428  	json, err := json.Marshal(task)
   429  	if err != nil {
   430  		writeJSONError(w, http.StatusInternalServerError, "unable to serialize task", err)
   431  		return
   432  	}
   433  
   434  	writeBytesCompressed(w, req, json)
   435  }
   436  
   437  func (l *Lightning) handlePostTask(w http.ResponseWriter, req *http.Request) {
   438  	w.Header().Set("Cache-Control", "no-store")
   439  
   440  	if l.taskCfgs == nil {
   441  		// l.taskCfgs is non-nil only if Lightning is started with RunServer().
   442  		// Without the server mode this pointer is default to be nil.
   443  		writeJSONError(w, http.StatusNotImplemented, "server-mode not enabled", nil)
   444  		return
   445  	}
   446  
   447  	type taskResponse struct {
   448  		ID int64 `json:"id"`
   449  	}
   450  
   451  	data, err := io.ReadAll(req.Body)
   452  	if err != nil {
   453  		writeJSONError(w, http.StatusBadRequest, "cannot read request", err)
   454  		return
   455  	}
   456  	log.L().Debug("received task config", zap.ByteString("content", data))
   457  
   458  	cfg := config.NewConfig()
   459  	if err = cfg.LoadFromGlobal(l.globalCfg); err != nil {
   460  		writeJSONError(w, http.StatusInternalServerError, "cannot restore from global config", err)
   461  		return
   462  	}
   463  	if err = cfg.LoadFromTOML(data); err != nil {
   464  		writeJSONError(w, http.StatusBadRequest, "cannot parse task (must be TOML)", err)
   465  		return
   466  	}
   467  	if err = cfg.Adjust(l.ctx); err != nil {
   468  		writeJSONError(w, http.StatusBadRequest, "invalid task configuration", err)
   469  		return
   470  	}
   471  
   472  	l.taskCfgs.Push(cfg)
   473  	w.WriteHeader(http.StatusOK)
   474  	_ = json.NewEncoder(w).Encode(taskResponse{ID: cfg.TaskID})
   475  }
   476  
   477  func (l *Lightning) handleDeleteOneTask(w http.ResponseWriter, req *http.Request) {
   478  	w.Header().Set("Content-Type", "application/json")
   479  
   480  	taskID, _, err := parseTaskID(req)
   481  	if err != nil {
   482  		writeJSONError(w, http.StatusBadRequest, "invalid task ID", err)
   483  		return
   484  	}
   485  
   486  	var cancel context.CancelFunc
   487  	cancelSuccess := false
   488  
   489  	l.cancelLock.Lock()
   490  	if l.cancel != nil && l.curTask != nil && l.curTask.TaskID == taskID {
   491  		cancel = l.cancel
   492  		l.cancel = nil
   493  	}
   494  	l.cancelLock.Unlock()
   495  
   496  	if cancel != nil {
   497  		cancel()
   498  		cancelSuccess = true
   499  	} else if l.taskCfgs != nil {
   500  		cancelSuccess = l.taskCfgs.Remove(taskID)
   501  	}
   502  
   503  	log.L().Info("canceled task", zap.Int64("taskID", taskID), zap.Bool("success", cancelSuccess))
   504  
   505  	if cancelSuccess {
   506  		w.WriteHeader(http.StatusOK)
   507  		_, _ = w.Write([]byte("{}"))
   508  	} else {
   509  		writeJSONError(w, http.StatusNotFound, "task ID not found", nil)
   510  	}
   511  }
   512  
   513  func (l *Lightning) handlePatchOneTask(w http.ResponseWriter, req *http.Request) {
   514  	if l.taskCfgs == nil {
   515  		writeJSONError(w, http.StatusNotImplemented, "server-mode not enabled", nil)
   516  		return
   517  	}
   518  
   519  	taskID, verb, err := parseTaskID(req)
   520  	if err != nil {
   521  		writeJSONError(w, http.StatusBadRequest, "invalid task ID", err)
   522  		return
   523  	}
   524  
   525  	moveSuccess := false
   526  	switch verb {
   527  	case "front":
   528  		moveSuccess = l.taskCfgs.MoveToFront(taskID)
   529  	case "back":
   530  		moveSuccess = l.taskCfgs.MoveToBack(taskID)
   531  	default:
   532  		writeJSONError(w, http.StatusBadRequest, "unknown patch action", nil)
   533  		return
   534  	}
   535  
   536  	if moveSuccess {
   537  		w.WriteHeader(http.StatusOK)
   538  		_, _ = w.Write([]byte("{}"))
   539  	} else {
   540  		writeJSONError(w, http.StatusNotFound, "task ID not found", nil)
   541  	}
   542  }
   543  
   544  func writeBytesCompressed(w http.ResponseWriter, req *http.Request, b []byte) {
   545  	if !strings.Contains(req.Header.Get("Accept-Encoding"), "gzip") {
   546  		_, _ = w.Write(b)
   547  		return
   548  	}
   549  
   550  	w.Header().Set("Content-Encoding", "gzip")
   551  	w.WriteHeader(http.StatusOK)
   552  	gw, _ := gzip.NewWriterLevel(w, gzip.BestSpeed)
   553  	_, _ = gw.Write(b)
   554  	_ = gw.Close()
   555  }
   556  
   557  func handleProgressTask(w http.ResponseWriter, req *http.Request) {
   558  	w.Header().Set("Content-Type", "application/json")
   559  	res, err := web.MarshalTaskProgress()
   560  	if err == nil {
   561  		writeBytesCompressed(w, req, res)
   562  	} else {
   563  		w.WriteHeader(http.StatusInternalServerError)
   564  		_ = json.NewEncoder(w).Encode(err.Error())
   565  	}
   566  }
   567  
   568  func handleProgressTable(w http.ResponseWriter, req *http.Request) {
   569  	w.Header().Set("Content-Type", "application/json")
   570  	tableName := req.URL.Query().Get("t")
   571  	res, err := web.MarshalTableCheckpoints(tableName)
   572  	if err == nil {
   573  		writeBytesCompressed(w, req, res)
   574  	} else {
   575  		if errors.IsNotFound(err) {
   576  			w.WriteHeader(http.StatusNotFound)
   577  		} else {
   578  			w.WriteHeader(http.StatusInternalServerError)
   579  		}
   580  		_ = json.NewEncoder(w).Encode(err.Error())
   581  	}
   582  }
   583  
   584  func handlePause(w http.ResponseWriter, req *http.Request) {
   585  	w.Header().Set("Content-Type", "application/json")
   586  
   587  	switch req.Method {
   588  	case http.MethodGet:
   589  		w.WriteHeader(http.StatusOK)
   590  		fmt.Fprintf(w, `{"paused":%v}`, restore.DeliverPauser.IsPaused())
   591  
   592  	case http.MethodPut:
   593  		w.WriteHeader(http.StatusOK)
   594  		restore.DeliverPauser.Pause()
   595  		log.L().Info("progress paused")
   596  		_, _ = w.Write([]byte("{}"))
   597  
   598  	default:
   599  		w.Header().Set("Allow", http.MethodGet+", "+http.MethodPut)
   600  		writeJSONError(w, http.StatusMethodNotAllowed, "only GET and PUT are allowed", nil)
   601  	}
   602  }
   603  
   604  func handleResume(w http.ResponseWriter, req *http.Request) {
   605  	w.Header().Set("Content-Type", "application/json")
   606  
   607  	switch req.Method {
   608  	case http.MethodPut:
   609  		w.WriteHeader(http.StatusOK)
   610  		restore.DeliverPauser.Resume()
   611  		log.L().Info("progress resumed")
   612  		_, _ = w.Write([]byte("{}"))
   613  
   614  	default:
   615  		w.Header().Set("Allow", http.MethodPut)
   616  		writeJSONError(w, http.StatusMethodNotAllowed, "only PUT is allowed", nil)
   617  	}
   618  }
   619  
   620  func handleLogLevel(w http.ResponseWriter, req *http.Request) {
   621  	w.Header().Set("Content-Type", "application/json")
   622  
   623  	var logLevel struct {
   624  		Level zapcore.Level `json:"level"`
   625  	}
   626  
   627  	switch req.Method {
   628  	case http.MethodGet:
   629  		logLevel.Level = log.Level()
   630  		w.WriteHeader(http.StatusOK)
   631  		_ = json.NewEncoder(w).Encode(logLevel)
   632  
   633  	case http.MethodPut, http.MethodPost:
   634  		if err := json.NewDecoder(req.Body).Decode(&logLevel); err != nil {
   635  			writeJSONError(w, http.StatusBadRequest, "invalid log level", err)
   636  			return
   637  		}
   638  		oldLevel := log.SetLevel(zapcore.InfoLevel)
   639  		log.L().Info("changed log level", zap.Stringer("old", oldLevel), zap.Stringer("new", logLevel.Level))
   640  		log.SetLevel(logLevel.Level)
   641  		w.WriteHeader(http.StatusOK)
   642  		_, _ = w.Write([]byte("{}"))
   643  
   644  	default:
   645  		w.Header().Set("Allow", http.MethodGet+", "+http.MethodPut+", "+http.MethodPost)
   646  		writeJSONError(w, http.StatusMethodNotAllowed, "only GET, PUT and POST are allowed", nil)
   647  	}
   648  }
   649  
   650  func checkSystemRequirement(cfg *config.Config, dbsMeta []*mydump.MDDatabaseMeta) error {
   651  	// in local mode, we need to read&write a lot of L0 sst files, so we need to check system max open files limit
   652  	if cfg.TikvImporter.Backend == config.BackendLocal {
   653  		// estimate max open files = {top N(TableConcurrency) table sizes} / {MemoryTableSize}
   654  		tableTotalSizes := make([]int64, 0)
   655  		for _, dbs := range dbsMeta {
   656  			for _, tb := range dbs.Tables {
   657  				tableTotalSizes = append(tableTotalSizes, tb.TotalSize)
   658  			}
   659  		}
   660  		sort.Slice(tableTotalSizes, func(i, j int) bool {
   661  			return tableTotalSizes[i] > tableTotalSizes[j]
   662  		})
   663  		topNTotalSize := int64(0)
   664  		for i := 0; i < len(tableTotalSizes) && i < cfg.App.TableConcurrency; i++ {
   665  			topNTotalSize += tableTotalSizes[i]
   666  		}
   667  
   668  		// region-concurrency: number of LocalWriters writing SST files.
   669  		// 2*totalSize/memCacheSize: number of Pebble MemCache files.
   670  		maxDBFiles := topNTotalSize / int64(cfg.TikvImporter.LocalWriterMemCacheSize) * 2
   671  		// the pebble db and all import routine need upto maxDBFiles fds for read and write.
   672  		maxOpenDBFiles := maxDBFiles * (1 + int64(cfg.TikvImporter.RangeConcurrency))
   673  		estimateMaxFiles := local.Rlim_t(cfg.App.RegionConcurrency) + local.Rlim_t(maxOpenDBFiles)
   674  		if err := local.VerifyRLimit(estimateMaxFiles); err != nil {
   675  			return err
   676  		}
   677  	}
   678  
   679  	return nil
   680  }
   681  
   682  // checkSchemaConflict return error if checkpoint table scheme is conflict with data files
   683  func checkSchemaConflict(cfg *config.Config, dbsMeta []*mydump.MDDatabaseMeta) error {
   684  	if cfg.Checkpoint.Enable && cfg.Checkpoint.Driver == config.CheckpointDriverMySQL {
   685  		for _, db := range dbsMeta {
   686  			if db.Name == cfg.Checkpoint.Schema {
   687  				for _, tb := range db.Tables {
   688  					if checkpoints.IsCheckpointTable(tb.Name) {
   689  						return errors.Errorf("checkpoint table `%s`.`%s` conflict with data files. Please change the `checkpoint.schema` config or set `checkpoint.driver` to \"file\" instead", db.Name, tb.Name)
   690  					}
   691  				}
   692  			}
   693  		}
   694  	}
   695  	return nil
   696  }