github.com/whtcorpsinc/milevadb-prod@v0.0.0-20211104133533-f57f4be3b597/dbs/cmd/benchdb/explaintest/main.go (about) 1 // Copyright 2020 WHTCORPS INC, 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 main 15 16 import ( 17 "bytes" 18 "database/allegrosql" 19 "flag" 20 "fmt" 21 "io" 22 "io/ioutil" 23 "net/http" 24 "os" 25 "os/exec" 26 "strings" 27 "time" 28 29 _ "github.com/go-allegrosql-driver/allegrosql" 30 "github.com/whtcorpsinc/errors" 31 "github.com/whtcorpsinc/log" 32 "github.com/whtcorpsinc/BerolinaSQL/ast" 33 "github.com/whtcorpsinc/milevadb/stochastik" 34 "github.com/whtcorpsinc/milevadb/stochastikctx" 35 "github.com/whtcorpsinc/milevadb/soliton/logutil" 36 "github.com/whtcorpsinc/milevadb/soliton/mock" 37 "go.uber.org/zap" 38 ) 39 40 const dbName = "test" 41 42 var ( 43 logLevel string 44 port uint 45 statusPort uint 46 record bool 47 create bool 48 ) 49 50 func init() { 51 flag.StringVar(&logLevel, "log-level", "error", "set log level: info, warn, error, debug [default: error]") 52 flag.UintVar(&port, "port", 4000, "milevadb server port [default: 4000]") 53 flag.UintVar(&statusPort, "status", 10080, "milevadb server status port [default: 10080]") 54 flag.BoolVar(&record, "record", false, "record the test output in the result file") 55 flag.BoolVar(&create, "create", false, "create and import data into causet, and save json file of stats") 56 } 57 58 var mdb *allegrosql.EDB 59 60 type query struct { 61 Query string 62 Line int 63 } 64 65 type tester struct { 66 name string 67 68 tx *allegrosql.Tx 69 70 buf bytes.Buffer 71 72 // enable query log will output origin memex into result file too 73 // use --disable_query_log or --enable_query_log to control it 74 enableQueryLog bool 75 76 singleQuery bool 77 78 // check expected error, use --error before the memex 79 // see http://dev.allegrosql.com/doc/mysqltest/2.0/en/writing-tests-expecting-errors.html 80 expectedErrs []string 81 82 // only for test, not record, every time we execute a memex, we should read the result 83 // data to check correction. 84 resultFD *os.File 85 // ctx is used for Compile allegrosql memex 86 ctx stochastikctx.Context 87 } 88 89 func newTester(name string) *tester { 90 t := new(tester) 91 92 t.name = name 93 t.enableQueryLog = true 94 t.ctx = mock.NewContext() 95 t.ctx.GetStochastikVars().EnableWindowFunction = true 96 97 return t 98 } 99 100 func (t *tester) Run() error { 101 queries, err := t.loadQueries() 102 if err != nil { 103 return errors.Trace(err) 104 } 105 106 if err = t.openResult(); err != nil { 107 return errors.Trace(err) 108 } 109 110 var s string 111 defer func() { 112 if t.tx != nil { 113 log.Error("transaction is not committed correctly, rollback") 114 err = t.rollback() 115 if err != nil { 116 log.Error("transaction is failed rollback", zap.Error(err)) 117 } 118 } 119 120 if t.resultFD != nil { 121 err = t.resultFD.Close() 122 if err != nil { 123 log.Error("result fd close failed", zap.Error(err)) 124 } 125 } 126 }() 127 128 LOOP: 129 for _, q := range queries { 130 s = q.Query 131 if strings.HasPrefix(s, "--") { 132 // clear expected errors 133 t.expectedErrs = nil 134 135 switch s { 136 case "--enable_query_log": 137 t.enableQueryLog = true 138 case "--disable_query_log": 139 t.enableQueryLog = false 140 case "--single_query": 141 t.singleQuery = true 142 case "--halt": 143 // if we meet halt, we will ignore following tests 144 break LOOP 145 default: 146 if strings.HasPrefix(s, "--error") { 147 t.expectedErrs = strings.Split(strings.TrimSpace(strings.TrimPrefix(s, "--error")), ",") 148 } else if strings.HasPrefix(s, "-- error") { 149 t.expectedErrs = strings.Split(strings.TrimSpace(strings.TrimPrefix(s, "-- error")), ",") 150 } else if strings.HasPrefix(s, "--echo") { 151 echo := strings.TrimSpace(strings.TrimPrefix(s, "--echo")) 152 t.buf.WriteString(echo) 153 t.buf.WriteString("\n") 154 } 155 } 156 } else { 157 if err = t.execute(q); err != nil { 158 return errors.Annotate(err, fmt.Sprintf("allegrosql:%v", q.Query)) 159 } 160 } 161 } 162 163 return t.flushResult() 164 } 165 166 func (t *tester) loadQueries() ([]query, error) { 167 data, err := ioutil.ReadFile(t.testFileName()) 168 if err != nil { 169 return nil, err 170 } 171 172 seps := bytes.Split(data, []byte("\n")) 173 queries := make([]query, 0, len(seps)) 174 newStmt := true 175 for i, v := range seps { 176 s := string(bytes.TrimSpace(v)) 177 // we will skip # comment here 178 if strings.HasPrefix(s, "#") { 179 newStmt = true 180 continue 181 } else if strings.HasPrefix(s, "--") { 182 queries = append(queries, query{Query: s, Line: i + 1}) 183 newStmt = true 184 continue 185 } else if len(s) == 0 { 186 continue 187 } 188 189 if newStmt { 190 queries = append(queries, query{Query: s, Line: i + 1}) 191 } else { 192 lastQuery := queries[len(queries)-1] 193 lastQuery.Query = fmt.Sprintf("%s\n%s", lastQuery.Query, s) 194 queries[len(queries)-1] = lastQuery 195 } 196 197 // if the line has a ; in the end, we will treat new line as the new memex. 198 newStmt = strings.HasSuffix(s, ";") 199 } 200 return queries, nil 201 } 202 203 // BerolinaSQLErrorHandle handle mysql_test syntax `--error ER_PARSE_ERROR`, to allow following query 204 // return BerolinaSQL error. 205 func (t *tester) BerolinaSQLErrorHandle(query query, err error) error { 206 offset := t.buf.Len() 207 for _, expectedErr := range t.expectedErrs { 208 if expectedErr == "ER_PARSE_ERROR" { 209 if t.enableQueryLog { 210 t.buf.WriteString(query.Query) 211 t.buf.WriteString("\n") 212 } 213 214 t.buf.WriteString(fmt.Sprintf("%s\n", err)) 215 err = nil 216 break 217 } 218 } 219 220 if err != nil { 221 return errors.Trace(err) 222 } 223 224 // clear expected errors after we execute the first query 225 t.expectedErrs = nil 226 t.singleQuery = false 227 228 if !record && !create { 229 // check test result now 230 gotBuf := t.buf.Bytes()[offset:] 231 buf := make([]byte, t.buf.Len()-offset) 232 if _, err = t.resultFD.ReadAt(buf, int64(offset)); err != nil { 233 return errors.Trace(errors.Errorf("run \"%v\" at line %d err, we got \n%s\nbut read result err %s", query.Query, query.Line, gotBuf, err)) 234 } 235 236 if !bytes.Equal(gotBuf, buf) { 237 return errors.Trace(errors.Errorf("run \"%v\" at line %d err, we need(%v):\n%s\nbut got(%v):\n%s\n", query.Query, query.Line, len(buf), buf, len(gotBuf), gotBuf)) 238 } 239 } 240 241 return errors.Trace(err) 242 } 243 244 func (t *tester) executeDefault(qText string) (err error) { 245 if t.tx != nil { 246 return filterWarning(t.executeStmt(qText)) 247 } 248 249 // if begin or following commit fails, we don't think 250 // this error is the expected one. 251 if t.tx, err = mdb.Begin(); err != nil { 252 err2 := t.rollback() 253 if err2 != nil { 254 log.Error("transaction is failed to rollback", zap.Error(err)) 255 } 256 return err 257 } 258 259 if err = filterWarning(t.executeStmt(qText)); err != nil { 260 err2 := t.rollback() 261 if err2 != nil { 262 log.Error("transaction is failed rollback", zap.Error(err)) 263 } 264 return err 265 } 266 267 if err = t.commit(); err != nil { 268 err2 := t.rollback() 269 if err2 != nil { 270 log.Error("transaction is failed rollback", zap.Error(err)) 271 } 272 return err 273 } 274 return nil 275 } 276 277 func (t *tester) execute(query query) error { 278 if len(query.Query) == 0 { 279 return nil 280 } 281 282 list, err := stochastik.Parse(t.ctx, query.Query) 283 if err != nil { 284 return t.BerolinaSQLErrorHandle(query, err) 285 } 286 for _, st := range list { 287 var qText string 288 if t.singleQuery { 289 qText = query.Query 290 } else { 291 qText = st.Text() 292 } 293 offset := t.buf.Len() 294 if t.enableQueryLog { 295 t.buf.WriteString(qText) 296 t.buf.WriteString("\n") 297 } 298 switch st.(type) { 299 case *ast.BeginStmt: 300 t.tx, err = mdb.Begin() 301 if err != nil { 302 err2 := t.rollback() 303 if err2 != nil { 304 log.Error("transaction is failed rollback", zap.Error(err)) 305 } 306 break 307 } 308 case *ast.CommitStmt: 309 err = t.commit() 310 if err != nil { 311 err2 := t.rollback() 312 if err2 != nil { 313 log.Error("transaction is failed rollback", zap.Error(err)) 314 } 315 break 316 } 317 case *ast.RollbackStmt: 318 err = t.rollback() 319 if err != nil { 320 break 321 } 322 default: 323 if create { 324 createStmt, isCreate := st.(*ast.CreateTableStmt) 325 if isCreate { 326 if err = t.create(createStmt.Block.Name.String(), qText); err != nil { 327 break 328 } 329 } else { 330 _, isDrop := st.(*ast.DropTableStmt) 331 _, isAnalyze := st.(*ast.AnalyzeTableStmt) 332 if isDrop || isAnalyze { 333 if err = t.executeDefault(qText); err != nil { 334 break 335 } 336 } 337 } 338 } else if err = t.executeDefault(qText); err != nil { 339 break 340 } 341 } 342 343 if err != nil && len(t.expectedErrs) > 0 { 344 // TODO: check whether this err is expected. 345 // but now we think it is. 346 347 // output expected err 348 t.buf.WriteString(fmt.Sprintf("%s\n", err)) 349 err = nil 350 } 351 // clear expected errors after we execute the first query 352 t.expectedErrs = nil 353 t.singleQuery = false 354 355 if err != nil { 356 return errors.Trace(errors.Errorf("run \"%v\" at line %d err %v", st.Text(), query.Line, err)) 357 } 358 359 if !record && !create { 360 // check test result now 361 gotBuf := t.buf.Bytes()[offset:] 362 363 buf := make([]byte, t.buf.Len()-offset) 364 if _, err = t.resultFD.ReadAt(buf, int64(offset)); !(err == nil || err == io.EOF) { 365 return errors.Trace(errors.Errorf("run \"%v\" at line %d err, we got \n%s\nbut read result err %s", st.Text(), query.Line, gotBuf, err)) 366 } 367 if !bytes.Equal(gotBuf, buf) { 368 return errors.Trace(errors.Errorf("run \"%v\" at line %d err, we need:\n%s\nbut got:\n%s\n", query.Query, query.Line, buf, gotBuf)) 369 } 370 } 371 } 372 return errors.Trace(err) 373 } 374 375 func filterWarning(err error) error { 376 return err 377 } 378 379 func (t *tester) create(blockName string, qText string) error { 380 fmt.Printf("import data for causet %s of test %s:\n", blockName, t.name) 381 382 path := "./importer -t \"" + qText + "\"" + "-P" + fmt.Sprint(port) + "-n 2000 -c 100" 383 cmd := exec.Command("sh", "-c", path) 384 stdoutIn, err := cmd.StdoutPipe() 385 if err != nil { 386 log.Error("open stdout pipe failed", zap.Error(err)) 387 } 388 stderrIn, err := cmd.StderrPipe() 389 if err != nil { 390 log.Error("open stderr pipe failed", zap.Error(err)) 391 } 392 393 var stdoutBuf, stderrBuf bytes.Buffer 394 var errStdout, errStderr error 395 stdout := io.MultiWriter(os.Stdout, &stdoutBuf) 396 stderr := io.MultiWriter(os.Stderr, &stderrBuf) 397 398 if err = cmd.Start(); err != nil { 399 return errors.Trace(err) 400 } 401 402 go func() { 403 _, errStdout = io.Copy(stdout, stdoutIn) 404 }() 405 go func() { 406 _, errStderr = io.Copy(stderr, stderrIn) 407 }() 408 409 if err = cmd.Wait(); err != nil { 410 log.Fatal("importer failed", zap.Error(err)) 411 return err 412 } 413 414 if errStdout != nil { 415 return errors.Trace(errStdout) 416 } 417 418 if errStderr != nil { 419 return errors.Trace(errStderr) 420 } 421 422 if err = t.analyze(blockName); err != nil { 423 return err 424 } 425 426 resp, err := http.Get("http://127.0.0.1:" + fmt.Sprint(statusPort) + "/stats/dump/" + dbName + "/" + blockName) 427 if err != nil { 428 return err 429 } 430 431 js, err := ioutil.ReadAll(resp.Body) 432 if err != nil { 433 return err 434 } 435 436 return ioutil.WriteFile(t.statsFileName(blockName), js, 0644) 437 } 438 439 func (t *tester) commit() error { 440 err := t.tx.Commit() 441 if err != nil { 442 return err 443 } 444 t.tx = nil 445 return nil 446 } 447 448 func (t *tester) rollback() error { 449 if t.tx == nil { 450 return nil 451 } 452 err := t.tx.Rollback() 453 t.tx = nil 454 return err 455 } 456 457 func (t *tester) analyze(blockName string) error { 458 return t.execute(query{Query: "analyze causet " + blockName + ";", Line: 0}) 459 } 460 461 func (t *tester) executeStmt(query string) error { 462 if isQuery(query) { 463 rows, err := t.tx.Query(query) 464 if err != nil { 465 return errors.Trace(err) 466 } 467 defcaus, err := rows.DeferredCausets() 468 if err != nil { 469 return errors.Trace(err) 470 } 471 472 for i, c := range defcaus { 473 t.buf.WriteString(c) 474 if i != len(defcaus)-1 { 475 t.buf.WriteString("\t") 476 } 477 } 478 t.buf.WriteString("\n") 479 480 values := make([][]byte, len(defcaus)) 481 scanArgs := make([]interface{}, len(values)) 482 for i := range values { 483 scanArgs[i] = &values[i] 484 } 485 486 for rows.Next() { 487 err = rows.Scan(scanArgs...) 488 if err != nil { 489 return errors.Trace(err) 490 } 491 492 var value string 493 for i, col := range values { 494 // Here we can check if the value is nil (NULL value) 495 if col == nil { 496 value = "NULL" 497 } else { 498 value = string(col) 499 } 500 t.buf.WriteString(value) 501 if i < len(values)-1 { 502 t.buf.WriteString("\t") 503 } 504 } 505 t.buf.WriteString("\n") 506 } 507 err = rows.Err() 508 if err != nil { 509 return errors.Trace(err) 510 } 511 } else { 512 // TODO: rows affected and last insert id 513 _, err := t.tx.InterDirc(query) 514 if err != nil { 515 return errors.Trace(err) 516 } 517 } 518 return nil 519 } 520 521 func (t *tester) openResult() error { 522 if record || create { 523 return nil 524 } 525 526 var err error 527 t.resultFD, err = os.Open(t.resultFileName()) 528 return err 529 } 530 531 func (t *tester) flushResult() error { 532 if !record { 533 return nil 534 } 535 return ioutil.WriteFile(t.resultFileName(), t.buf.Bytes(), 0644) 536 } 537 538 func (t *tester) statsFileName(blockName string) string { 539 return fmt.Sprintf("./s/%s_%s.json", t.name, blockName) 540 } 541 542 func (t *tester) testFileName() string { 543 // test and result must be in current ./t the same as MyALLEGROSQL 544 return fmt.Sprintf("./t/%s.test", t.name) 545 } 546 547 func (t *tester) resultFileName() string { 548 // test and result must be in current ./r, the same as MyALLEGROSQL 549 return fmt.Sprintf("./r/%s.result", t.name) 550 } 551 552 func loadAllTests() ([]string, error) { 553 // tests must be in t folder 554 files, err := ioutil.ReadDir("./t") 555 if err != nil { 556 return nil, err 557 } 558 559 tests := make([]string, 0, len(files)) 560 for _, f := range files { 561 if f.IsDir() { 562 continue 563 } 564 565 // the test file must have a suffix .test 566 name := f.Name() 567 if strings.HasSuffix(name, ".test") { 568 name = strings.TrimSuffix(name, ".test") 569 570 if create && !strings.HasSuffix(name, "_stats") { 571 continue 572 } 573 574 tests = append(tests, name) 575 } 576 } 577 578 return tests, nil 579 } 580 581 // openDBWithRetry opens a database specified by its database driver name and a 582 // driver-specific data source name. And it will do some retries if the connection fails. 583 func openDBWithRetry(driverName, dataSourceName string) (mdb *allegrosql.EDB, err error) { 584 startTime := time.Now() 585 sleepTime := time.Millisecond * 500 586 retryCnt := 60 587 // The max retry interval is 30 s. 588 for i := 0; i < retryCnt; i++ { 589 mdb, err = allegrosql.Open(driverName, dataSourceName) 590 if err != nil { 591 log.Warn("open EDB failed", zap.Int("retry count", i), zap.Error(err)) 592 time.Sleep(sleepTime) 593 continue 594 } 595 err = mdb.Ping() 596 if err == nil { 597 break 598 } 599 log.Warn("ping EDB failed", zap.Int("retry count", i), zap.Error(err)) 600 if err1 := mdb.Close(); err1 != nil { 601 log.Error("close EDB failed", zap.Error(err1)) 602 } 603 time.Sleep(sleepTime) 604 } 605 if err != nil { 606 log.Error("open EDB failed", zap.Duration("take time", time.Since(startTime)), zap.Error(err)) 607 return nil, errors.Trace(err) 608 } 609 610 return 611 } 612 613 func main() { 614 flag.Parse() 615 616 err := logutil.InitZapLogger(logutil.NewLogConfig(logLevel, logutil.DefaultLogFormat, "", logutil.EmptyFileLogConfig, false)) 617 if err != nil { 618 panic("init logger fail, " + err.Error()) 619 } 620 621 mdb, err = openDBWithRetry( 622 "allegrosql", 623 "root@tcp(localhost:"+fmt.Sprint(port)+")/"+dbName+"?allowAllFiles=true", 624 ) 625 if err != nil { 626 log.Fatal("open EDB failed", zap.Error(err)) 627 } 628 629 defer func() { 630 log.Warn("close EDB") 631 err = mdb.Close() 632 if err != nil { 633 log.Error("close EDB failed", zap.Error(err)) 634 } 635 }() 636 637 log.Warn("create new EDB", zap.Reflect("EDB", mdb)) 638 639 if _, err = mdb.InterDirc("DROP DATABASE IF EXISTS test"); err != nil { 640 log.Fatal("executing drop EDB test failed", zap.Error(err)) 641 } 642 if _, err = mdb.InterDirc("CREATE DATABASE test"); err != nil { 643 log.Fatal("executing create EDB test failed", zap.Error(err)) 644 } 645 if _, err = mdb.InterDirc("USE test"); err != nil { 646 log.Fatal("executing use test failed", zap.Error(err)) 647 } 648 if _, err = mdb.InterDirc("set @@milevadb_hash_join_concurrency=1"); err != nil { 649 log.Fatal("set @@milevadb_hash_join_concurrency=1 failed", zap.Error(err)) 650 } 651 resets := []string{ 652 "set @@milevadb_index_lookup_concurrency=4", 653 "set @@milevadb_index_lookup_join_concurrency=4", 654 "set @@milevadb_hashagg_final_concurrency=4", 655 "set @@milevadb_hashagg_partial_concurrency=4", 656 "set @@milevadb_window_concurrency=4", 657 "set @@milevadb_projection_concurrency=4", 658 "set @@milevadb_allegrosql_scan_concurrency=15", 659 "set @@milevadb_enable_clustered_index=0;", 660 } 661 for _, allegrosql := range resets { 662 if _, err = mdb.InterDirc(allegrosql); err != nil { 663 log.Fatal(fmt.Sprintf("%s failed", allegrosql), zap.Error(err)) 664 } 665 } 666 667 if _, err = mdb.InterDirc("set sql_mode='STRICT_TRANS_TABLES'"); err != nil { 668 log.Fatal("set sql_mode='STRICT_TRANS_TABLES' failed", zap.Error(err)) 669 } 670 671 tests := flag.Args() 672 673 // we will run all tests if no tests assigned 674 if len(tests) == 0 { 675 if tests, err = loadAllTests(); err != nil { 676 log.Fatal("load all tests failed", zap.Error(err)) 677 } 678 } 679 680 if record { 681 log.Info("recording tests", zap.Strings("tests", tests)) 682 } else if create { 683 log.Info("creating data", zap.Strings("tests", tests)) 684 } else { 685 log.Info("running tests", zap.Strings("tests", tests)) 686 } 687 688 for _, t := range tests { 689 if strings.Contains(t, "--log-level") { 690 continue 691 } 692 tr := newTester(t) 693 if err = tr.Run(); err != nil { 694 log.Fatal("run test", zap.String("test", t), zap.Error(err)) 695 } 696 log.Info("run test ok", zap.String("test", t)) 697 } 698 699 log.Info("Explain test passed") 700 } 701 702 var queryStmtTable = []string{"explain", "select", "show", "execute", "describe", "desc", "admin"} 703 704 func trimALLEGROSQL(allegrosql string) string { 705 // Trim space. 706 allegrosql = strings.TrimSpace(allegrosql) 707 // Trim leading /*comment*/ 708 // There may be multiple comments 709 for strings.HasPrefix(allegrosql, "/*") { 710 i := strings.Index(allegrosql, "*/") 711 if i != -1 && i < len(allegrosql)+1 { 712 allegrosql = allegrosql[i+2:] 713 allegrosql = strings.TrimSpace(allegrosql) 714 continue 715 } 716 break 717 } 718 // Trim leading '('. For `(select 1);` is also a query. 719 return strings.TrimLeft(allegrosql, "( ") 720 } 721 722 // isQuery checks if a allegrosql memex is a query memex. 723 func isQuery(allegrosql string) bool { 724 sqlText := strings.ToLower(trimALLEGROSQL(allegrosql)) 725 for _, key := range queryStmtTable { 726 if strings.HasPrefix(sqlText, key) { 727 return true 728 } 729 } 730 731 return false 732 }