vitess.io/vitess@v0.16.2/go/vt/vttablet/tabletserver/schema/tracker.go (about)

     1  /*
     2  Copyright 2020 The Vitess Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package schema
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"fmt"
    23  	"sync"
    24  	"time"
    25  
    26  	"google.golang.org/protobuf/proto"
    27  
    28  	"vitess.io/vitess/go/vt/schema"
    29  
    30  	"vitess.io/vitess/go/mysql"
    31  
    32  	"vitess.io/vitess/go/vt/sqlparser"
    33  
    34  	"vitess.io/vitess/go/sqltypes"
    35  	"vitess.io/vitess/go/vt/log"
    36  	binlogdatapb "vitess.io/vitess/go/vt/proto/binlogdata"
    37  	vtrpcpb "vitess.io/vitess/go/vt/proto/vtrpc"
    38  	"vitess.io/vitess/go/vt/vttablet/tabletserver/tabletenv"
    39  )
    40  
    41  // VStreamer defines  the functions of VStreamer
    42  // that the replicationWatcher needs.
    43  type VStreamer interface {
    44  	Stream(ctx context.Context, startPos string, tablePKs []*binlogdatapb.TableLastPK, filter *binlogdatapb.Filter, send func([]*binlogdatapb.VEvent) error) error
    45  }
    46  
    47  // Tracker watches the replication and saves the latest schema into _vt.schema_version when a DDL is encountered.
    48  type Tracker struct {
    49  	enabled bool
    50  
    51  	mu     sync.Mutex
    52  	cancel context.CancelFunc
    53  	wg     sync.WaitGroup
    54  
    55  	env    tabletenv.Env
    56  	vs     VStreamer
    57  	engine *Engine
    58  }
    59  
    60  // NewTracker creates a Tracker, needs an Open SchemaEngine (which implements the trackerEngine interface)
    61  func NewTracker(env tabletenv.Env, vs VStreamer, engine *Engine) *Tracker {
    62  	return &Tracker{
    63  		enabled: env.Config().TrackSchemaVersions,
    64  		env:     env,
    65  		vs:      vs,
    66  		engine:  engine,
    67  	}
    68  }
    69  
    70  // Open enables the tracker functionality
    71  func (tr *Tracker) Open() {
    72  	if !tr.enabled {
    73  		return
    74  	}
    75  	log.Info("Schema Tracker: opening")
    76  
    77  	tr.mu.Lock()
    78  	defer tr.mu.Unlock()
    79  	if tr.cancel != nil {
    80  		return
    81  	}
    82  
    83  	ctx, cancel := context.WithCancel(tabletenv.LocalContext())
    84  	tr.cancel = cancel
    85  	tr.wg.Add(1)
    86  
    87  	go tr.process(ctx)
    88  }
    89  
    90  // Close disables the tracker functionality
    91  func (tr *Tracker) Close() {
    92  	tr.mu.Lock()
    93  	defer tr.mu.Unlock()
    94  	if tr.cancel == nil {
    95  		return
    96  	}
    97  
    98  	tr.cancel()
    99  	tr.cancel = nil
   100  	tr.wg.Wait()
   101  	log.Info("Schema Tracker: closed")
   102  }
   103  
   104  // Enable forces tracking to be on or off.
   105  // Only used for testing.
   106  func (tr *Tracker) Enable(enabled bool) {
   107  	tr.mu.Lock()
   108  	tr.enabled = enabled
   109  	tr.mu.Unlock()
   110  	if enabled {
   111  		tr.Open()
   112  	} else {
   113  		tr.Close()
   114  	}
   115  }
   116  
   117  func (tr *Tracker) process(ctx context.Context) {
   118  	defer tr.env.LogError()
   119  	defer tr.wg.Done()
   120  	if err := tr.possiblyInsertInitialSchema(ctx); err != nil {
   121  		log.Errorf("error inserting initial schema: %v", err)
   122  		return
   123  	}
   124  
   125  	filter := &binlogdatapb.Filter{
   126  		Rules: []*binlogdatapb.Rule{{
   127  			Match: "/.*",
   128  		}},
   129  	}
   130  
   131  	var gtid string
   132  	for {
   133  		err := tr.vs.Stream(ctx, "current", nil, filter, func(events []*binlogdatapb.VEvent) error {
   134  			for _, event := range events {
   135  				if event.Type == binlogdatapb.VEventType_GTID {
   136  					gtid = event.Gtid
   137  				}
   138  				if event.Type == binlogdatapb.VEventType_DDL &&
   139  					MustReloadSchemaOnDDL(event.Statement, tr.engine.cp.DBName()) {
   140  
   141  					if err := tr.schemaUpdated(gtid, event.Statement, event.Timestamp); err != nil {
   142  						tr.env.Stats().ErrorCounters.Add(vtrpcpb.Code_INTERNAL.String(), 1)
   143  						log.Errorf("Error updating schema: %s for ddl %s, gtid %s",
   144  							sqlparser.TruncateForLog(err.Error()), event.Statement, gtid)
   145  					}
   146  				}
   147  			}
   148  			return nil
   149  		})
   150  		select {
   151  		case <-ctx.Done():
   152  			return
   153  		case <-time.After(5 * time.Second):
   154  		}
   155  		log.Infof("Tracker's vStream ended: %v, retrying in 5 seconds", err)
   156  		time.Sleep(5 * time.Second)
   157  	}
   158  }
   159  
   160  func (tr *Tracker) currentPosition(ctx context.Context) (mysql.Position, error) {
   161  	conn, err := tr.engine.cp.Connect(ctx)
   162  	if err != nil {
   163  		return mysql.Position{}, err
   164  	}
   165  	defer conn.Close()
   166  	return conn.PrimaryPosition()
   167  }
   168  
   169  func (tr *Tracker) isSchemaVersionTableEmpty(ctx context.Context) (bool, error) {
   170  	conn, err := tr.engine.GetConnection(ctx)
   171  	if err != nil {
   172  		return false, err
   173  	}
   174  	defer conn.Recycle()
   175  	result, err := conn.Exec(ctx, "select id from _vt.schema_version limit 1", 1, false)
   176  	if err != nil {
   177  		return false, err
   178  	}
   179  	if len(result.Rows) == 0 {
   180  		return true, nil
   181  	}
   182  	return false, nil
   183  }
   184  
   185  // possiblyInsertInitialSchema stores the latest schema when a tracker starts and the schema_version table is empty
   186  // this enables the right schema to be available between the time the tracker starts first and the first DDL is applied
   187  func (tr *Tracker) possiblyInsertInitialSchema(ctx context.Context) error {
   188  	var err error
   189  	needsWarming, err := tr.isSchemaVersionTableEmpty(ctx)
   190  	if err != nil {
   191  		return err
   192  	}
   193  	if !needsWarming { // _vt.schema_version is not empty, nothing to do here
   194  		return nil
   195  	}
   196  	if err = tr.engine.Reload(ctx); err != nil {
   197  		return err
   198  	}
   199  
   200  	timestamp := time.Now().UnixNano() / 1e9
   201  	ddl := ""
   202  	pos, err := tr.currentPosition(ctx)
   203  	if err != nil {
   204  		return err
   205  	}
   206  	gtid := mysql.EncodePosition(pos)
   207  	log.Infof("Saving initial schema for gtid %s", gtid)
   208  
   209  	return tr.saveCurrentSchemaToDb(ctx, gtid, ddl, timestamp)
   210  }
   211  
   212  func (tr *Tracker) schemaUpdated(gtid string, ddl string, timestamp int64) error {
   213  	log.Infof("Processing schemaUpdated event for gtid %s, ddl %s", gtid, ddl)
   214  	if gtid == "" || ddl == "" {
   215  		return fmt.Errorf("got invalid gtid or ddl in schemaUpdated")
   216  	}
   217  	ctx := context.Background()
   218  	// Engine will have reloaded the schema because vstream will reload it on a DDL
   219  	return tr.saveCurrentSchemaToDb(ctx, gtid, ddl, timestamp)
   220  }
   221  
   222  func (tr *Tracker) saveCurrentSchemaToDb(ctx context.Context, gtid, ddl string, timestamp int64) error {
   223  	tables := tr.engine.GetSchema()
   224  	dbSchema := &binlogdatapb.MinimalSchema{
   225  		Tables: []*binlogdatapb.MinimalTable{},
   226  	}
   227  	for _, table := range tables {
   228  		dbSchema.Tables = append(dbSchema.Tables, newMinimalTable(table))
   229  	}
   230  	blob, _ := proto.Marshal(dbSchema)
   231  
   232  	conn, err := tr.engine.GetConnection(ctx)
   233  	if err != nil {
   234  		return err
   235  	}
   236  	defer conn.Recycle()
   237  
   238  	query := fmt.Sprintf("insert into _vt.schema_version "+
   239  		"(pos, ddl, schemax, time_updated) "+
   240  		"values (%v, %v, %v, %d)", encodeString(gtid), encodeString(ddl), encodeString(string(blob)), timestamp)
   241  	_, err = conn.Exec(ctx, query, 1, false)
   242  	if err != nil {
   243  		return err
   244  	}
   245  	return nil
   246  }
   247  
   248  func newMinimalTable(st *Table) *binlogdatapb.MinimalTable {
   249  	table := &binlogdatapb.MinimalTable{
   250  		Name:   st.Name.String(),
   251  		Fields: st.Fields,
   252  	}
   253  	var pkc []int64
   254  	for _, pk := range st.PKColumns {
   255  		pkc = append(pkc, int64(pk))
   256  	}
   257  	table.PKColumns = pkc
   258  	return table
   259  }
   260  
   261  func encodeString(in string) string {
   262  	buf := bytes.NewBuffer(nil)
   263  	sqltypes.NewVarChar(in).EncodeSQL(buf)
   264  	return buf.String()
   265  }
   266  
   267  // MustReloadSchemaOnDDL returns true if the ddl is for the db which is part of the workflow and is not an online ddl artifact
   268  func MustReloadSchemaOnDDL(sql string, dbname string) bool {
   269  	ast, err := sqlparser.Parse(sql)
   270  	if err != nil {
   271  		return false
   272  	}
   273  	switch stmt := ast.(type) {
   274  	case sqlparser.DBDDLStatement:
   275  		return false
   276  	case sqlparser.DDLStatement:
   277  		tables := []sqlparser.TableName{stmt.GetTable()}
   278  		tables = append(tables, stmt.GetToTables()...)
   279  		for _, table := range tables {
   280  			if table.IsEmpty() {
   281  				continue
   282  			}
   283  			if !table.Qualifier.IsEmpty() && table.Qualifier.String() != dbname {
   284  				continue
   285  			}
   286  			tableName := table.Name.String()
   287  			if schema.IsOnlineDDLTableName(tableName) {
   288  				continue
   289  			}
   290  			return true
   291  		}
   292  	}
   293  	return false
   294  }