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

     1  /*
     2  Copyright 2021 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  	"context"
    21  	"sync"
    22  	"time"
    23  
    24  	vtrpcpb "vitess.io/vitess/go/vt/proto/vtrpc"
    25  	"vitess.io/vitess/go/vt/vterrors"
    26  
    27  	"vitess.io/vitess/go/vt/callerid"
    28  
    29  	"vitess.io/vitess/go/vt/vttablet/queryservice"
    30  
    31  	"vitess.io/vitess/go/mysql"
    32  	"vitess.io/vitess/go/sqltypes"
    33  	querypb "vitess.io/vitess/go/vt/proto/query"
    34  
    35  	"vitess.io/vitess/go/vt/discovery"
    36  	"vitess.io/vitess/go/vt/log"
    37  	"vitess.io/vitess/go/vt/sqlparser"
    38  	"vitess.io/vitess/go/vt/vtgate/vindexes"
    39  )
    40  
    41  type (
    42  	keyspaceStr  = string
    43  	tableNameStr = string
    44  	viewNameStr  = string
    45  
    46  	// Tracker contains the required fields to perform schema tracking.
    47  	Tracker struct {
    48  		ch     chan *discovery.TabletHealth
    49  		cancel context.CancelFunc
    50  
    51  		mu     sync.Mutex
    52  		tables *tableMap
    53  		views  *viewMap
    54  		ctx    context.Context
    55  		signal func() // a function that we'll call whenever we have new schema data
    56  
    57  		// map of keyspace currently tracked
    58  		tracked      map[keyspaceStr]*updateController
    59  		consumeDelay time.Duration
    60  	}
    61  )
    62  
    63  // defaultConsumeDelay is the default time, the updateController will wait before checking the schema fetch request queue.
    64  const defaultConsumeDelay = 1 * time.Second
    65  
    66  // aclErrorMessageLog is for logging a warning when an acl error message is received for querying schema tracking table.
    67  const aclErrorMessageLog = "Table ACL might be enabled, --schema_change_signal_user needs to be passed to VTGate for schema tracking to work. Check 'schema tracking' docs on vitess.io"
    68  
    69  // NewTracker creates the tracker object.
    70  func NewTracker(ch chan *discovery.TabletHealth, user string, enableViews bool) *Tracker {
    71  	ctx := context.Background()
    72  	// Set the caller on the context if the user is provided.
    73  	// This user that will be sent down to vttablet calls.
    74  	if user != "" {
    75  		ctx = callerid.NewContext(ctx, nil, callerid.NewImmediateCallerID(user))
    76  	}
    77  
    78  	t := &Tracker{
    79  		ctx:          ctx,
    80  		ch:           ch,
    81  		tables:       &tableMap{m: map[keyspaceStr]map[tableNameStr][]vindexes.Column{}},
    82  		tracked:      map[keyspaceStr]*updateController{},
    83  		consumeDelay: defaultConsumeDelay,
    84  	}
    85  
    86  	if enableViews {
    87  		t.views = &viewMap{m: map[keyspaceStr]map[viewNameStr]sqlparser.SelectStatement{}}
    88  	}
    89  	return t
    90  }
    91  
    92  // LoadKeyspace loads the keyspace schema.
    93  func (t *Tracker) LoadKeyspace(conn queryservice.QueryService, target *querypb.Target) error {
    94  	err := t.loadTables(conn, target)
    95  	if err != nil {
    96  		return err
    97  	}
    98  	err = t.loadViews(conn, target)
    99  	if err != nil {
   100  		return err
   101  	}
   102  
   103  	t.tracked[target.Keyspace].setLoaded(true)
   104  	return nil
   105  }
   106  
   107  func (t *Tracker) loadTables(conn queryservice.QueryService, target *querypb.Target) error {
   108  	if t.tables == nil {
   109  		// this can only happen in testing
   110  		return nil
   111  	}
   112  
   113  	ftRes, err := conn.Execute(t.ctx, target, mysql.FetchTables, nil, 0, 0, nil)
   114  	if err != nil {
   115  		return err
   116  	}
   117  	t.mu.Lock()
   118  	defer t.mu.Unlock()
   119  
   120  	// We must clear out any previous schema before loading it here as this is called
   121  	// whenever a shard's primary tablet starts and sends the initial signal. Without
   122  	// clearing out the previous schema we can end up with duplicate entries when the
   123  	// tablet is simply restarted or potentially when we elect a new primary.
   124  	t.clearKeyspaceTables(target.Keyspace)
   125  	t.updateTables(target.Keyspace, ftRes)
   126  	log.Infof("finished loading schema for keyspace %s. Found %d columns in total across the tables", target.Keyspace, len(ftRes.Rows))
   127  
   128  	return nil
   129  }
   130  
   131  func (t *Tracker) loadViews(conn queryservice.QueryService, target *querypb.Target) error {
   132  	if t.views == nil {
   133  		// This happens only when views are not enabled.
   134  		return nil
   135  	}
   136  
   137  	t.mu.Lock()
   138  	defer t.mu.Unlock()
   139  	// We must clear out any previous view definition before loading it here as this is called
   140  	// whenever a shard's primary tablet starts and sends the initial signal.
   141  	// This is needed clear out any stale view definitions.
   142  	t.clearKeyspaceViews(target.Keyspace)
   143  
   144  	var numViews int
   145  	err := conn.GetSchema(t.ctx, target, querypb.SchemaTableType_VIEWS, nil, func(schemaRes *querypb.GetSchemaResponse) error {
   146  		t.updateViews(target.Keyspace, schemaRes.TableDefinition)
   147  		numViews += len(schemaRes.TableDefinition)
   148  		return nil
   149  	})
   150  	if err != nil {
   151  		return err
   152  	}
   153  	log.Infof("finished loading views for keyspace %s. Found %d views", target.Keyspace, numViews)
   154  	return nil
   155  }
   156  
   157  // Start starts the schema tracking.
   158  func (t *Tracker) Start() {
   159  	log.Info("Starting schema tracking")
   160  	ctx, cancel := context.WithCancel(t.ctx)
   161  	t.cancel = cancel
   162  	go func(ctx context.Context, t *Tracker) {
   163  		for {
   164  			select {
   165  			case th := <-t.ch:
   166  				ksUpdater := t.getKeyspaceUpdateController(th)
   167  				ksUpdater.add(th)
   168  			case <-ctx.Done():
   169  				// closing of the channel happens outside the scope of the tracker. It is the responsibility of the one who created this tracker.
   170  				return
   171  			}
   172  		}
   173  	}(ctx, t)
   174  }
   175  
   176  // getKeyspaceUpdateController returns the updateController for the given keyspace
   177  // the updateController will be created if there was none.
   178  func (t *Tracker) getKeyspaceUpdateController(th *discovery.TabletHealth) *updateController {
   179  	t.mu.Lock()
   180  	defer t.mu.Unlock()
   181  
   182  	ksUpdater, exists := t.tracked[th.Target.Keyspace]
   183  	if !exists {
   184  		ksUpdater = t.newUpdateController()
   185  		t.tracked[th.Target.Keyspace] = ksUpdater
   186  	}
   187  	return ksUpdater
   188  }
   189  
   190  func (t *Tracker) newUpdateController() *updateController {
   191  	return &updateController{update: t.updateSchema, reloadKeyspace: t.initKeyspace, signal: t.signal, consumeDelay: t.consumeDelay}
   192  }
   193  
   194  func (t *Tracker) initKeyspace(th *discovery.TabletHealth) error {
   195  	err := t.LoadKeyspace(th.Conn, th.Target)
   196  	if err != nil {
   197  		log.Warningf("Unable to add the %s keyspace to the schema tracker: %v", th.Target.Keyspace, err)
   198  		code := vterrors.Code(err)
   199  		if code == vtrpcpb.Code_UNAUTHENTICATED || code == vtrpcpb.Code_PERMISSION_DENIED {
   200  			log.Warning(aclErrorMessageLog)
   201  		}
   202  		return err
   203  	}
   204  	return nil
   205  }
   206  
   207  // Stop stops the schema tracking
   208  func (t *Tracker) Stop() {
   209  	log.Info("Stopping schema tracking")
   210  	t.cancel()
   211  }
   212  
   213  // GetColumns returns the column list for table in the given keyspace.
   214  func (t *Tracker) GetColumns(ks string, tbl string) []vindexes.Column {
   215  	t.mu.Lock()
   216  	defer t.mu.Unlock()
   217  
   218  	return t.tables.get(ks, tbl)
   219  }
   220  
   221  // Tables returns a map with the columns for all known tables in the keyspace
   222  func (t *Tracker) Tables(ks string) map[string][]vindexes.Column {
   223  	t.mu.Lock()
   224  	defer t.mu.Unlock()
   225  
   226  	m := t.tables.m[ks]
   227  	if m == nil {
   228  		return map[string][]vindexes.Column{} // we know nothing about this KS, so that is the info we can give out
   229  	}
   230  
   231  	return m
   232  }
   233  
   234  // Views returns all known views in the keyspace with their definition.
   235  func (t *Tracker) Views(ks string) map[string]sqlparser.SelectStatement {
   236  	t.mu.Lock()
   237  	defer t.mu.Unlock()
   238  
   239  	if t.views == nil {
   240  		return nil
   241  	}
   242  	return t.views.m[ks]
   243  }
   244  
   245  func (t *Tracker) updateSchema(th *discovery.TabletHealth) bool {
   246  	success := true
   247  	if th.Stats.TableSchemaChanged != nil {
   248  		success = t.updatedTableSchema(th)
   249  	}
   250  	if !success || th.Stats.ViewSchemaChanged == nil {
   251  		return success
   252  	}
   253  	// there is view definition change in the tablet
   254  	return t.updatedViewSchema(th)
   255  }
   256  
   257  func (t *Tracker) updatedTableSchema(th *discovery.TabletHealth) bool {
   258  	tablesUpdated := th.Stats.TableSchemaChanged
   259  	tables, err := sqltypes.BuildBindVariable(tablesUpdated)
   260  	if err != nil {
   261  		log.Errorf("failed to read updated tables from TabletHealth: %v", err)
   262  		return false
   263  	}
   264  	bv := map[string]*querypb.BindVariable{"tableNames": tables}
   265  	res, err := th.Conn.Execute(t.ctx, th.Target, mysql.FetchUpdatedTables, bv, 0, 0, nil)
   266  	if err != nil {
   267  		t.tracked[th.Target.Keyspace].setLoaded(false)
   268  		// TODO: optimize for the tables that got errored out.
   269  		log.Warningf("error fetching new schema for %v, making them non-authoritative: %v", tablesUpdated, err)
   270  		code := vterrors.Code(err)
   271  		if code == vtrpcpb.Code_UNAUTHENTICATED || code == vtrpcpb.Code_PERMISSION_DENIED {
   272  			log.Warning(aclErrorMessageLog)
   273  		}
   274  		return false
   275  	}
   276  
   277  	t.mu.Lock()
   278  	defer t.mu.Unlock()
   279  
   280  	// first we empty all prior schema. deleted tables will not show up in the result,
   281  	// so this is the only chance to delete
   282  	for _, tbl := range tablesUpdated {
   283  		t.tables.delete(th.Target.Keyspace, tbl)
   284  	}
   285  	t.updateTables(th.Target.Keyspace, res)
   286  	return true
   287  }
   288  
   289  func (t *Tracker) updateTables(keyspace string, res *sqltypes.Result) {
   290  	for _, row := range res.Rows {
   291  		tbl := row[0].ToString()
   292  		colName := row[1].ToString()
   293  		colType := row[2].ToString()
   294  		collation := row[3].ToString()
   295  
   296  		cType := sqlparser.ColumnType{Type: colType}
   297  		col := vindexes.Column{Name: sqlparser.NewIdentifierCI(colName), Type: cType.SQLType(), CollationName: collation}
   298  		cols := t.tables.get(keyspace, tbl)
   299  
   300  		t.tables.set(keyspace, tbl, append(cols, col))
   301  	}
   302  }
   303  
   304  func (t *Tracker) updatedViewSchema(th *discovery.TabletHealth) bool {
   305  	t.mu.Lock()
   306  	defer t.mu.Unlock()
   307  
   308  	viewsUpdated := th.Stats.ViewSchemaChanged
   309  
   310  	// first we empty all prior schema. deleted tables will not show up in the result,
   311  	// so this is the only chance to delete
   312  	for _, view := range viewsUpdated {
   313  		t.views.delete(th.Target.Keyspace, view)
   314  	}
   315  	err := th.Conn.GetSchema(t.ctx, th.Target, querypb.SchemaTableType_VIEWS, viewsUpdated, func(schemaRes *querypb.GetSchemaResponse) error {
   316  		t.updateViews(th.Target.Keyspace, schemaRes.TableDefinition)
   317  		return nil
   318  	})
   319  	if err != nil {
   320  		t.tracked[th.Target.Keyspace].setLoaded(false)
   321  		// TODO: optimize for the views that got errored out.
   322  		log.Warningf("error fetching new views definition for %v", viewsUpdated, err)
   323  		return false
   324  	}
   325  	return true
   326  }
   327  
   328  func (t *Tracker) updateViews(keyspace string, res map[string]string) {
   329  	for viewName, viewDef := range res {
   330  		t.views.set(keyspace, viewName, viewDef)
   331  	}
   332  }
   333  
   334  // RegisterSignalReceiver allows a function to register to be called when new schema is available
   335  func (t *Tracker) RegisterSignalReceiver(f func()) {
   336  	t.mu.Lock()
   337  	defer t.mu.Unlock()
   338  	for _, controller := range t.tracked {
   339  		controller.signal = f
   340  	}
   341  	t.signal = f
   342  }
   343  
   344  // AddNewKeyspace adds keyspace to the tracker.
   345  func (t *Tracker) AddNewKeyspace(conn queryservice.QueryService, target *querypb.Target) error {
   346  	updateController := t.newUpdateController()
   347  	t.tracked[target.Keyspace] = updateController
   348  	err := t.LoadKeyspace(conn, target)
   349  	if err != nil {
   350  		updateController.setIgnore(checkIfWeShouldIgnoreKeyspace(err))
   351  	}
   352  	return err
   353  }
   354  
   355  type tableMap struct {
   356  	m map[keyspaceStr]map[tableNameStr][]vindexes.Column
   357  }
   358  
   359  func (tm *tableMap) set(ks, tbl string, cols []vindexes.Column) {
   360  	m := tm.m[ks]
   361  	if m == nil {
   362  		m = make(map[tableNameStr][]vindexes.Column)
   363  		tm.m[ks] = m
   364  	}
   365  	m[tbl] = cols
   366  }
   367  
   368  func (tm *tableMap) get(ks, tbl string) []vindexes.Column {
   369  	m := tm.m[ks]
   370  	if m == nil {
   371  		return nil
   372  	}
   373  	return m[tbl]
   374  }
   375  
   376  func (tm *tableMap) delete(ks, tbl string) {
   377  	m := tm.m[ks]
   378  	if m == nil {
   379  		return
   380  	}
   381  	delete(m, tbl)
   382  }
   383  
   384  // This empties out any previous schema for all tables in a keyspace.
   385  // You should call this before initializing/loading a keyspace of the same
   386  // name in the cache.
   387  func (t *Tracker) clearKeyspaceTables(ks string) {
   388  	if t.tables != nil && t.tables.m != nil {
   389  		delete(t.tables.m, ks)
   390  	}
   391  }
   392  
   393  type viewMap struct {
   394  	m map[keyspaceStr]map[viewNameStr]sqlparser.SelectStatement
   395  }
   396  
   397  func (vm *viewMap) set(ks, tbl, sql string) {
   398  	m := vm.m[ks]
   399  	if m == nil {
   400  		m = make(map[tableNameStr]sqlparser.SelectStatement)
   401  		vm.m[ks] = m
   402  	}
   403  	stmt, err := sqlparser.Parse(sql)
   404  	if err != nil {
   405  		log.Warningf("ignoring view '%s', parsing error in view definition: '%s'", tbl, sql)
   406  		return
   407  	}
   408  	cv, ok := stmt.(*sqlparser.CreateView)
   409  	if !ok {
   410  		log.Warningf("ignoring view '%s', view definition is not a create view query: %T", tbl, stmt)
   411  		return
   412  	}
   413  	m[tbl] = cv.Select
   414  }
   415  
   416  func (vm *viewMap) get(ks, tbl string) sqlparser.SelectStatement {
   417  	m := vm.m[ks]
   418  	if m == nil {
   419  		return nil
   420  	}
   421  	return m[tbl]
   422  }
   423  
   424  func (vm *viewMap) delete(ks, tbl string) {
   425  	m := vm.m[ks]
   426  	if m == nil {
   427  		return
   428  	}
   429  	delete(m, tbl)
   430  }
   431  
   432  func (t *Tracker) clearKeyspaceViews(ks string) {
   433  	if t.views != nil && t.views.m != nil {
   434  		delete(t.views.m, ks)
   435  	}
   436  }
   437  
   438  // GetViews returns the view statement for the given keyspace and view name.
   439  func (t *Tracker) GetViews(ks string, tbl string) sqlparser.SelectStatement {
   440  	t.mu.Lock()
   441  	defer t.mu.Unlock()
   442  
   443  	return t.views.get(ks, tbl)
   444  }