Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Backend for binding sql plan feature #1471

Merged
merged 9 commits into from
Feb 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,7 @@ linters:
- prealloc
- predeclared
- revive
- rowserrcheck
- sqlclosecheck
- unconvert
- wastedassign
- whitespace

linters-settings:
Expand Down
39 changes: 18 additions & 21 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Although TiDB Dashboard can also be integrated into [PD], this form is not conve

### Step 1. Start a TiDB cluster

[TiUP] is the offical component manager for [TiDB]. It can help you set up a local TiDB cluster in a few minutes.
[TiUP](https://docs.pingcap.com/tidb/stable/tiup-overview) is the official component manager for [TiDB]. It can help you set up a local TiDB cluster in a few minutes.

Download and install TiUP:

Expand All @@ -33,12 +33,12 @@ source ~/.bash_profile
Start a local TiDB cluster:

```bash
tiup playground nightly
tiup playground
```

You might notice that there is already a TiDB Dashboard integrated into the PD started by TiUP. For development purpose, it will not be used intentionally.

### Step 2. Prepare Prerequisites
### Step 2. Prepare Dev Prerequisites

The followings are required for developing TiDB Dashboard:

Expand All @@ -51,21 +51,30 @@ The followings are required for developing TiDB Dashboard:

### Step 3. Build and Run TiDB Dashboard

1. Clone the repository:
> Make sure that `tiup playground` is running on the background.

Package frontend and backend into a single binary:

```bash
git clone https://github.com/pingcap/tidb-dashboard.git
cd tidb-dashboard
# Build a binary into `bin/tidb-dashboard`.
make package

# Run.
make run
```

2. Build and run TiDB Dashboard back-end server:
You can access TiDB Dashboard now: [http://127.0.0.1:12333/dashboard](http://127.0.0.1:12333/dashboard)

#### Develop Frontend and Backend Separately

1. Build and run TiDB Dashboard back-end server:

```bash
# In tidb-dashboard directory:
make dev && make run
```

3. Build and run front-end server in a new terminal:
2. Build and run front-end server in a new terminal:

```bash
# In tidb-dashboard directory:
Expand All @@ -74,19 +83,7 @@ The followings are required for developing TiDB Dashboard:
pnpm dev
```

4. That's it! You can access TiDB Dashboard now: [http://127.0.0.1:3001](http://127.0.0.1:3001)

5. (Optional) Package frontend and backend into a single binary:

```bash
# In tidb-dashboard directory:
make package

# Run the binary without separate frontend server:
make run
```

You can access TiDB Dashboard now: [http://127.0.0.1:12333/dashboard](http://127.0.0.1:12333/dashboard)
3. That's it! You can access TiDB Dashboard now: [http://127.0.0.1:3001](http://127.0.0.1:3001)

### Step 4. Run E2E Tests (optional)

Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ PNPM_INSTALL_TAGS ?=

LDFLAGS ?=

FEATURE_VERSION ?= 6.2.0
FEATURE_VERSION ?= 6.6.0

WITHOUT_NGM ?= false

Expand Down
2 changes: 1 addition & 1 deletion cmd/tidb-dashboard/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ func main() {
log.Info(fmt.Sprintf("API: http://%s:%d/dashboard/api/", cliConfig.ListenHost, cliConfig.ListenPort))
log.Info(fmt.Sprintf("Swagger: http://%s:%d/dashboard/api/swagger/", cliConfig.ListenHost, cliConfig.ListenPort))

srv := &http.Server{Handler: mux}
srv := &http.Server{Handler: mux} // nolint:gosec
var wg sync.WaitGroup
wg.Add(1)
go func() {
Expand Down
4 changes: 2 additions & 2 deletions pkg/apiserver/clusterinfo/topology.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"io"
"net/http"

"github.com/pingcap/tidb-dashboard/pkg/httpc"
Expand All @@ -31,7 +31,7 @@ func fetchAlertManagerCounts(ctx context.Context, alertManagerAddr string, httpC
return 0, fmt.Errorf("alert manager API returns non success status code")
}

data, err := ioutil.ReadAll(resp.Body)
data, err := io.ReadAll(resp.Body)
if err != nil {
return 0, err
}
Expand Down
3 changes: 2 additions & 1 deletion pkg/apiserver/debugapi/endpoint/payload.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ func (c HTTPClients) GetHTTPClientByNodeKind(kind topo.Kind) *httpclient.Client
// RequestPayloadResolver resolves the request payload using specified API definitions.
//
// The relationship is below:
// RequestPayload ---(RequestPayloadResolver.ResolvePayload)---> ResolvedRequestPayload
//
// RequestPayload ---(RequestPayloadResolver.ResolvePayload)---> ResolvedRequestPayload
type RequestPayloadResolver struct {
apis []APIDefinition
apiMapByID map[string]*APIDefinition
Expand Down
5 changes: 2 additions & 3 deletions pkg/apiserver/diagnose/diagnose.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ package diagnose
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
Expand Down Expand Up @@ -91,7 +90,7 @@ func (s *Service) generateMetricsRelation(startTime, endTime time.Time, graphTyp
return "", err
}

file, err := ioutil.TempFile("", "metrics*.svg")
file, err := os.CreateTemp("", "metrics*.svg")
if err != nil {
return "", fmt.Errorf("failed to create temp file: %v", err)
}
Expand Down Expand Up @@ -168,7 +167,7 @@ func (s *Service) metricsRelationViewHandler(c *gin.Context) {
return
}

data, err := ioutil.ReadFile(filepath.Clean(path))
data, err := os.ReadFile(filepath.Clean(path))
if err != nil {
rest.Error(c, err)
return
Expand Down
4 changes: 2 additions & 2 deletions pkg/apiserver/logsearch/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ package logsearch

import (
"context"
"io/ioutil"
"net/http"
"os"
"strconv"
"strings"

Expand Down Expand Up @@ -36,7 +36,7 @@ func NewService(lc fx.Lifecycle, config *config.Config, db *dbstore.DB) *Service
dir := config.TempDir
if dir == "" {
var err error
dir, err = ioutil.TempDir("", "dashboard-logs")
dir, err = os.MkdirTemp("", "dashboard-logs")
if err != nil {
log.Fatal("Failed to create directory for storing logs", zap.Error(err))
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/apiserver/metrics/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ package metrics

import (
"fmt"
"io/ioutil"
"io"
"net/http"
"net/url"
"strconv"
Expand Down Expand Up @@ -84,7 +84,7 @@ func (s *Service) queryMetrics(c *gin.Context) {
return
}

body, err := ioutil.ReadAll(promResp.Body)
body, err := io.ReadAll(promResp.Body)
if err != nil {
rest.Error(c, ErrPrometheusQueryFailed.Wrap(err, "failed to read Prometheus query result"))
return
Expand Down
4 changes: 2 additions & 2 deletions pkg/apiserver/profiling/pprof.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ package profiling

import (
"fmt"
"io/ioutil"
"os"
"strconv"

"github.com/pingcap/tidb-dashboard/pkg/apiserver/model"
Expand Down Expand Up @@ -58,7 +58,7 @@ func (f *fetcher) FetchAndWriteToFile(duration uint, fileNameWithoutExt string,
fileExtenstion = "*.txt"
}

tmpfile, err := ioutil.TempFile("", fileNameWithoutExt+"_"+fileExtenstion)
tmpfile, err := os.CreateTemp("", fileNameWithoutExt+"_"+fileExtenstion)
if err != nil {
return "", "", fmt.Errorf("failed to create tmpfile to write profile: %v", err)
}
Expand Down
3 changes: 1 addition & 2 deletions pkg/apiserver/profiling/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"archive/zip"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"path/filepath"
Expand Down Expand Up @@ -399,7 +398,7 @@ func (s *Service) viewSingle(c *gin.Context) {
return
}

content, err := ioutil.ReadFile(task.FilePath)
content, err := os.ReadFile(task.FilePath)
if err != nil {
rest.Error(c, err)
return
Expand Down
8 changes: 8 additions & 0 deletions pkg/apiserver/statement/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,11 @@ func filterFieldsByColumns(fields []Field, columns []string) []Field {

return filteredFields
}

// Binding struct maps to the response of `SHOW BINDINGS` query.
type Binding struct {
Status string `json:"status" example:"enabled" enums:"enabled,using,disabled,deleted,invalid,rejected,pending verify"`
Source string `json:"source" example:"manual" enums:"manual,history,capture,evolve"`
SQLDigest string `json:"-" gorm:"column:Sql_digest"`
PlanDigest string `json:"plan_digest" gorm:"column:Plan_digest"`
}
62 changes: 62 additions & 0 deletions pkg/apiserver/statement/queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,18 @@ import (
"fmt"
"regexp"
"strings"
"time"

"github.com/pingcap/errors"
"gorm.io/gorm"
)

const (
statementsTable = "INFORMATION_SCHEMA.CLUSTER_STATEMENTS_SUMMARY_HISTORY"
)

var injectChecker = regexp.MustCompile(`\s`)

func queryStmtTypes(db *gorm.DB) (result []string, err error) {
// why should put DISTINCT inside the `Pluck()` method, see here:
// https://github.com/jinzhu/gorm/issues/496
Expand Down Expand Up @@ -172,3 +176,61 @@ func (s *Service) queryPlanDetail(
err = query.Scan(&result).Error
return
}

func (s *Service) queryPlanBinding(db *gorm.DB, sqlDigest string, beginTime, endTime int) (bindings []Binding, err error) {
// The binding sql digest is newly generated and different from the original sql digest,
// we have to do one more query here.

// First, get plan digests by sql digest.
q1 := db.
Table(statementsTable).
Select("plan_digest").
Where("digest = ? AND summary_begin_time <= FROM_UNIXTIME(?) AND summary_end_time > FROM_UNIXTIME(?)", sqlDigest, endTime, beginTime)
q1Res := make([]map[string]any, 0)
if err := q1.Find(&q1Res).Error; err != nil {
return nil, err
}
planDigests := make([]string, 0, len(q1Res))
for _, row := range q1Res {
s, ok := row["plan_digest"].(string)
if !ok {
return nil, errors.New("invalid plan digest value")
}
planDigests = append(planDigests, s)
}

// Second, get bindings.
query := db.Raw("SHOW GLOBAL BINDINGS WHERE plan_digest IN (?) AND source = ? AND status IN (?)", planDigests, "history", []string{"enabled", "using"})
return bindings, query.Scan(&bindings).Error
}

func (s *Service) createPlanBinding(db *gorm.DB, planDigest string) (err error) {
// Caution! SQL injection vulnerability!
// We have to interpolate sql string here, since plan binding stmt does not support session level prepare.
// go-sql-driver can enable interpolation globally. Refer to https://github.com/go-sql-driver/mysql#interpolateparams.
if injectChecker.MatchString(planDigest) {
return errors.New("invalid planDigest")
}

query := db.Exec(fmt.Sprintf("CREATE GLOBAL BINDING FROM HISTORY USING PLAN DIGEST '%s'", planDigest))
return query.Error
}

func (s *Service) dropPlanBinding(db *gorm.DB, sqlDigest string) (err error) {
// The binding sql digest is newly generated and different from the original sql digest,
// we have to do one more query here.
bindings, err := s.queryPlanBinding(db, sqlDigest, 0, int(time.Now().Unix()))
if err != nil {
return err
}

for _, binding := range bindings {
// No SQL injection vulnerability here.
query := db.Exec(fmt.Sprintf("DROP GLOBAL BINDING FOR SQL DIGEST '%s'", binding.SQLDigest))
if query.Error != nil {
return query.Error
}
}

return nil
}
Loading