Repository: pingcap/dumpling (go)

Issue:
--snapshot argument doesn't work
## Bug Report

Please answer these questions before submitting your issue. Thanks!

1. What did you do? If possible, provide a recipe for reproducing the error.
Run dumpling with `--snapshot` argument.

2. What did you expect to see?
Dump tables with given snapshot.

3. What did you see instead?
Dump tables with current snapshot.



Pull request description:
fix snapshot problem
<!--
Thank you for contributing to Dumpling! Please read the [CONTRIBUTING](https://github.com/pingcap/dumpling/blob/master/CONTRIBUTING.md) document **BEFORE** filing this PR.
-->

### What problem does this PR solve? <!--add issue link with summary if exists-->
fix #112
1. Now dumpling use `db.Exec("set tidb_snapshot = ?")` to set `tidb_snapshot`, but this can only influence that connection. The connection pool's `tidb_snapshot` is not set.
2. Now dumpling get tables before we `set snapshot`, which means the fetched tables are not the tables we actually want. But for consistency "lock" it needs all tables to lock before the dump process.

### What is changed and how it works?
1. Add `SessionParams` in `conf`. After we adjust all the `SessionParams` dumpling will reset `sql.DB` with the new dsn. This can make sure all connections' session variable has been set.
2. Delete consistency snapshot. Current process order: 
    a. set snapshot ts into `session params`
    b. fetch all the tables.
    c. set consistency.

### Check List <!--REMOVE the items that are not applicable-->

Tests <!-- At least one of them must be included. -->

 - Unit test
 - Integration test
 - Manual test (add detailed scripts or steps below)

Side effects

 - Increased code complexity

Related changes

 - Need to cherry-pick to the release branch
 - Need to update the documentation
 
### Release note

<!-- bugfixes or new feature need a release note, must in the form of a list, such as

- support -T/--tables-list argument

or if no need to be included in the release note, just add the following line

- No release note
-->
- fix the problem that --snapshot argument can't work

Patch:
diff --git a/cmd/dumpling/main.go b/cmd/dumpling/main.go
index e36cf09..747928a 100644
--- a/cmd/dumpling/main.go
+++ b/cmd/dumpling/main.go
@@ -181,10 +181,10 @@ func main() {
 	conf.CsvNullValue = csvNullValue
 	conf.Sql = sql
 	conf.TableFilter = tableFilter
-	conf.TiDBMemQuotaQuery = tidbMemQuotaQuery
 	conf.Security.CAPath = caPath
 	conf.Security.CertPath = certPath
 	conf.Security.KeyPath = keyPath
+	conf.SessionParams["tidb_mem_quota_query"] = tidbMemQuotaQuery
 
 	err = export.Dump(context.Background(), conf)
 	if err != nil {
diff --git a/v4/export/config.go b/v4/export/config.go
index 61c6fb9..84817b0 100644
--- a/v4/export/config.go
+++ b/v4/export/config.go
@@ -53,7 +53,7 @@ type Config struct {
 	FileType          string
 	EscapeBackslash   bool
 	DumpEmptyDatabase bool
-	TiDBMemQuotaQuery uint64
+	SessionParams     map[string]interface{}
 }
 
 func DefaultConfig() *Config {
@@ -86,15 +86,12 @@ func DefaultConfig() *Config {
 		Sql:               "",
 		TableFilter:       allFilter,
 		DumpEmptyDatabase: true,
-		TiDBMemQuotaQuery: UnspecifiedSize,
+		SessionParams:     make(map[string]interface{}),
 	}
 }
 
 func (conf *Config) getDSN(db string) string {
 	dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4", conf.User, conf.Password, conf.Host, conf.Port, db)
-	if conf.TiDBMemQuotaQuery != UnspecifiedSize {
-		dsn += fmt.Sprintf("&tidb_mem_quota_query=%v", conf.TiDBMemQuotaQuery)
-	}
 	if len(conf.Security.CAPath) > 0 {
 		dsn += "&tls=dumpling-tls-target"
 	}
diff --git a/v4/export/consistency.go b/v4/export/consistency.go
index def1209..0280988 100644
--- a/v4/export/consistency.go
+++ b/v4/export/consistency.go
@@ -20,11 +20,10 @@ func NewConsistencyController(conf *Config, session *sql.DB) (ConsistencyControl
 			allTables: conf.Tables,
 		}, nil
 	case "snapshot":
-		return &ConsistencySnapshot{
-			serverType: conf.ServerInfo.ServerType,
-			snapshot:   conf.Snapshot,
-			db:         session,
-		}, nil
+		if conf.ServerInfo.ServerType != ServerTypeTiDB {
+			return nil, withStack(errors.New("snapshot consistency is not supported for this server"))
+		}
+		return &ConsistencyNone{}, nil
 	case "none":
 		return &ConsistencyNone{}, nil
 	default:
@@ -92,40 +91,9 @@ func (c *ConsistencyLockDumpingTables) TearDown() error {
 	return UnlockTables(c.db)
 }
 
-type ConsistencySnapshot struct {
-	serverType ServerType
-	snapshot   string
-	db         *sql.DB
-}
-
 const showMasterStatusFieldNum = 5
 const snapshotFieldIndex = 1
 
-func (c *ConsistencySnapshot) Setup() error {
-	if c.serverType != ServerTypeTiDB {
-		return withStack(errors.New("snapshot consistency is not supported for this server"))
-	}
-	if c.snapshot == "" {
-		str, err := ShowMasterStatus(c.db, showMasterStatusFieldNum)
-		if err != nil {
-			return err
-		}
-		c.snapshot = str[snapshotFieldIndex]
-	}
-	hasTiKV, err := CheckTiDBWithTiKV(c.db)
-	if err != nil {
-		return err
-	}
-	if !hasTiKV {
-		return nil
-	}
-	return SetTiDBSnapshot(c.db, c.snapshot)
-}
-
-func (c *ConsistencySnapshot) TearDown() error {
-	return nil
-}
-
 func resolveAutoConsistency(conf *Config) {
 	if conf.Consistency != "auto" {
 		return
diff --git a/v4/export/dump.go b/v4/export/dump.go
index 2777659..ca55d13 100644
--- a/v4/export/dump.go
+++ b/v4/export/dump.go
@@ -3,6 +3,7 @@ package export
 import (
 	"context"
 	"database/sql"
+	"errors"
 	"strconv"
 	"strings"
 	"time"
@@ -28,6 +29,7 @@ func Dump(pCtx context.Context, conf *Config) (err error) {
 			}
 		}
 	}()
+
 	pool, err := sql.Open("mysql", conf.getDSN(""))
 	if err != nil {
 		return withStack(err)
@@ -36,41 +38,9 @@ func Dump(pCtx context.Context, conf *Config) (err error) {
 
 	conf.ServerInfo, err = detectServerInfo(pool)
 	if err != nil {
-		if strings.Contains(err.Error(), "tidb_mem_quota_query") {
-			conf.TiDBMemQuotaQuery = UnspecifiedSize
-			pool, err = sql.Open("mysql", conf.getDSN(""))
-			if err != nil {
-				return withStack(err)
-			}
-			conf.ServerInfo, err = detectServerInfo(pool)
-			if err != nil {
-				return withStack(err)
-			}
-		} else {
-			return withStack(err)
-		}
-	}
-
-	databases, err := prepareDumpingDatabases(conf, pool)
-	if err != nil {
-		return err
-	}
-
-	conf.Tables, err = listAllTables(pool, databases)
-	if err != nil {
-		return err
-	}
-
-	if !conf.NoViews {
-		views, err := listAllViews(pool, databases)
-		if err != nil {
-			return err
-		}
-		conf.Tables.Merge(views)
+		return withStack(err)
 	}
 
-	filterTables(conf)
-
 	ctx, cancel := context.WithCancel(pCtx)
 	defer cancel()
 
@@ -95,13 +65,23 @@ func Dump(pCtx context.Context, conf *Config) (err error) {
 		}
 	}
 
-	if conf.Snapshot == "" && (doPdGC || conf.Consistency == "flush") {
-		if conf.Snapshot == "" {
-			str, err := ShowMasterStatus(pool, showMasterStatusFieldNum)
-			if err != nil {
-				return err
-			}
-			conf.Snapshot = str[snapshotFieldIndex]
+	if conf.Snapshot == "" && (doPdGC || conf.Consistency == "snapshot") {
+		conf.Snapshot, err = getSnapshot(pool)
+		if err != nil {
+			return err
+		}
+	}
+
+	if conf.Snapshot != "" {
+		if conf.ServerInfo.ServerType != ServerTypeTiDB {
+			return errors.New("snapshot consistency is not supported for this server")
+		}
+		hasTiKV, err := CheckTiDBWithTiKV(pool)
+		if err != nil {
+			return err
+		}
+		if hasTiKV {
+			conf.SessionParams["tidb_snapshot"] = conf.Snapshot
 		}
 	}
 
@@ -118,6 +98,31 @@ func Dump(pCtx context.Context, conf *Config) (err error) {
 			"After dumping: run sql `update mysql.tidb set VARIABLE_VALUE = '10m' where VARIABLE_NAME = 'tikv_gc_life_time';` in tidb.\n")
 	}
 
+	pool, err = resetDBWithSessionParams(pool, conf.getDSN(""), conf.SessionParams)
+	if err != nil {
+		return err
+	}
+
+	databases, err := prepareDumpingDatabases(conf, pool)
+	if err != nil {
+		return err
+	}
+
+	conf.Tables, err = listAllTables(pool, databases)
+	if err != nil {
+		return err
+	}
+
+	if !conf.NoViews {
+		views, err := listAllViews(pool, databases)
+		if err != nil {
+			return err
+		}
+		conf.Tables.Merge(views)
+	}
+
+	filterTables(conf)
+
 	conCtrl, err := NewConsistencyController(conf, pool)
 	if err != nil {
 		return err
diff --git a/v4/export/prepare.go b/v4/export/prepare.go
index c1a6634..81d1516 100644
--- a/v4/export/prepare.go
+++ b/v4/export/prepare.go
@@ -41,6 +41,10 @@ func adjustConfig(conf *Config) error {
 		// Disable filesize if rows was set
 		conf.FileSize = UnspecifiedSize
 	}
+	if conf.SessionParams == nil {
+		conf.SessionParams = make(map[string]interface{})
+	}
+	resolveAutoConsistency(conf)
 
 	return nil
 }
diff --git a/v4/export/sql.go b/v4/export/sql.go
index 14e0b45..94c7122 100644
--- a/v4/export/sql.go
+++ b/v4/export/sql.go
@@ -3,6 +3,7 @@ package export
 import (
 	"database/sql"
 	"fmt"
+	"net/url"
 	"strconv"
 	"strings"
 
@@ -384,11 +385,6 @@ func GetTiDBDDLIDs(db *sql.DB) ([]string, error) {
 	return GetSpecifiedColumnValue(rows, "DDL_ID")
 }
 
-func SetTiDBSnapshot(db *sql.DB, snapshot string) error {
-	_, err := db.Exec("SET SESSION tidb_snapshot = ?", snapshot)
-	return withStack(err)
-}
-
 func CheckTiDBWithTiKV(db *sql.DB) (bool, error) {
 	var count int
 	handleOneRow := func(rows *sql.Rows) error {
@@ -401,6 +397,54 @@ func CheckTiDBWithTiKV(db *sql.DB) (bool, error) {
 	return count > 0, nil
 }
 
+func getSnapshot(db *sql.DB) (string, error) {
+	str, err := ShowMasterStatus(db, showMasterStatusFieldNum)
+	if err != nil {
+		return "", err
+	}
+	return str[snapshotFieldIndex], nil
+}
+
+func isUnknownSystemVariableErr(err error) bool {
+	return strings.Contains(err.Error(), "Unknown system variable")
+}
+
+func resetDBWithSessionParams(db *sql.DB, dsn string, params map[string]interface{}) (*sql.DB, error) {
+	support := make(map[string]interface{})
+	for k, v := range params {
+		s := fmt.Sprintf("SET SESSION %s = ?", k)
+		_, err := db.Exec(s, v)
+		if err != nil {
+			if isUnknownSystemVariableErr(err) {
+				log.Info("session variable is not supported by db", zap.String("variable", k), zap.Reflect("value", v))
+				continue
+			}
+			return nil, withStack(err)
+		}
+
+		support[k] = v
+	}
+
+	for k, v := range support {
+		var s string
+		if str, ok := v.(string); ok {
+			s = wrapStringWith(str, "'")
+		} else {
+			s = fmt.Sprintf("%v", v)
+		}
+		dsn += fmt.Sprintf("&%s=%s", k, url.QueryEscape(s))
+	}
+
+	newDB, err := sql.Open("mysql", dsn)
+	if err != nil {
+		return nil, withStack(err)
+	}
+
+	db.Close()
+
+	return newDB, nil
+}
+
 func buildSelectField(db *sql.DB, dbName, tableName string) (string, error) {
 	query := `SELECT COLUMN_NAME,EXTRA FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=? AND TABLE_NAME=?;`
 	rows, err := db.Query(query, dbName, tableName)


Test patch:
diff --git a/v4/export/consistency_test.go b/v4/export/consistency_test.go
index d5531ee..0e756f5 100644
--- a/v4/export/consistency_test.go
+++ b/v4/export/consistency_test.go
@@ -49,23 +49,10 @@ func (s *testConsistencySuite) TestConsistencyController(c *C) {
 
 	conf.Consistency = "snapshot"
 	conf.ServerInfo.ServerType = ServerTypeTiDB
-	conf.Snapshot = "" // let dumpling detect the TSO
-	rows := sqlmock.NewRows([]string{"File", "Position", "Binlog_Do_DB", "Binlog_Ignore_DB", "Executed_Gtid_Set"})
-	rows.AddRow("tidb-binlog", "413802961528946688", "", "", "")
-	tidbRows := sqlmock.NewRows([]string{"c"})
-	tidbRows.AddRow(1)
-	mock.ExpectQuery("SHOW MASTER STATUS").WillReturnRows(rows)
-	mock.ExpectQuery("SELECT COUNT\\(1\\) as c FROM MYSQL.TiDB WHERE VARIABLE_NAME='tikv_gc_safe_point'").
-		WillReturnRows(tidbRows)
-	mock.ExpectExec("SET SESSION tidb_snapshot").
-		WillReturnResult(sqlmock.NewResult(0, 1))
 	ctrl, _ = NewConsistencyController(conf, db)
-	_, ok = ctrl.(*ConsistencySnapshot)
+	_, ok = ctrl.(*ConsistencyNone)
 	c.Assert(ok, IsTrue)
 	s.assertLifetimeErrNil(ctrl, c)
-	if err = mock.ExpectationsWereMet(); err != nil {
-		c.Fatalf(err.Error())
-	}
 
 	conf.Consistency = "lock"
 	conf.Tables = NewDatabaseTables().
@@ -119,14 +106,13 @@ func (s *testConsistencySuite) TestConsistencyControllerError(c *C) {
 	// snapshot consistency is only available in TiDB
 	conf.Consistency = "snapshot"
 	conf.ServerInfo.ServerType = ServerTypeUnknown
-	ctrl, _ := NewConsistencyController(conf, db)
-	err = ctrl.Setup()
+	_, err = NewConsistencyController(conf, db)
 	c.Assert(err, NotNil)
 
 	// flush consistency is unavailable in TiDB
 	conf.Consistency = "flush"
 	conf.ServerInfo.ServerType = ServerTypeTiDB
-	ctrl, _ = NewConsistencyController(conf, db)
+	ctrl, _ := NewConsistencyController(conf, db)
 	err = ctrl.Setup()
 	c.Assert(err, NotNil)