vitess.io/vitess@v0.16.2/go/vt/vttablet/tabletserver/schema/historian.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  	"context"
    21  	"fmt"
    22  	"sort"
    23  	"sync"
    24  
    25  	"google.golang.org/protobuf/proto"
    26  
    27  	"vitess.io/vitess/go/mysql"
    28  	"vitess.io/vitess/go/sqltypes"
    29  	"vitess.io/vitess/go/vt/log"
    30  	binlogdatapb "vitess.io/vitess/go/vt/proto/binlogdata"
    31  	"vitess.io/vitess/go/vt/vtgate/evalengine"
    32  	"vitess.io/vitess/go/vt/vttablet/tabletserver/connpool"
    33  	"vitess.io/vitess/go/vt/vttablet/tabletserver/tabletenv"
    34  
    35  	"vitess.io/vitess/go/vt/sqlparser"
    36  )
    37  
    38  const getSchemaVersions = "select id, pos, ddl, time_updated, schemax from _vt.schema_version where id > %d order by id asc"
    39  
    40  // vl defines the glog verbosity level for the package
    41  const vl = 10
    42  
    43  // trackedSchema has the snapshot of the table at a given pos (reached by ddl)
    44  type trackedSchema struct {
    45  	schema map[string]*binlogdatapb.MinimalTable
    46  	pos    mysql.Position
    47  	ddl    string
    48  }
    49  
    50  // historian implements the Historian interface by calling schema.Engine for the underlying schema
    51  // and supplying a schema for a specific version by loading the cached values from the schema_version table
    52  // The schema version table is populated by the Tracker
    53  type historian struct {
    54  	conns   *connpool.Pool
    55  	lastID  int64
    56  	schemas []*trackedSchema
    57  	mu      sync.Mutex
    58  	enabled bool
    59  	isOpen  bool
    60  }
    61  
    62  // newHistorian creates a new historian. It expects a schema.Engine instance
    63  func newHistorian(enabled bool, conns *connpool.Pool) *historian {
    64  	sh := historian{
    65  		conns:   conns,
    66  		lastID:  0,
    67  		enabled: enabled,
    68  	}
    69  	return &sh
    70  }
    71  
    72  func (h *historian) Enable(enabled bool) error {
    73  	h.mu.Lock()
    74  	h.enabled = enabled
    75  	h.mu.Unlock()
    76  	if enabled {
    77  		return h.Open()
    78  	}
    79  	h.Close()
    80  	return nil
    81  }
    82  
    83  // Open opens the underlying schema Engine. Called directly by a user purely interested in schema.Engine functionality
    84  func (h *historian) Open() error {
    85  	h.mu.Lock()
    86  	defer h.mu.Unlock()
    87  	if !h.enabled {
    88  		return nil
    89  	}
    90  	if h.isOpen {
    91  		return nil
    92  	}
    93  	log.Info("Historian: opening")
    94  
    95  	ctx := tabletenv.LocalContext()
    96  	if err := h.loadFromDB(ctx); err != nil {
    97  		log.Errorf("Historian failed to open: %v", err)
    98  		return err
    99  	}
   100  
   101  	h.isOpen = true
   102  	return nil
   103  }
   104  
   105  // Close closes the underlying schema engine and empties the version cache
   106  func (h *historian) Close() {
   107  	h.mu.Lock()
   108  	defer h.mu.Unlock()
   109  	if !h.isOpen {
   110  		return
   111  	}
   112  
   113  	h.schemas = nil
   114  	h.isOpen = false
   115  	log.Info("Historian: closed")
   116  }
   117  
   118  // RegisterVersionEvent is called by the vstream when it encounters a version event (an insert into _vt.schema_tracking)
   119  // It triggers the historian to load the newer rows from the database to update its cache
   120  func (h *historian) RegisterVersionEvent() error {
   121  	h.mu.Lock()
   122  	defer h.mu.Unlock()
   123  	if !h.isOpen {
   124  		return nil
   125  	}
   126  	ctx := tabletenv.LocalContext()
   127  	if err := h.loadFromDB(ctx); err != nil {
   128  		return err
   129  	}
   130  	return nil
   131  }
   132  
   133  // GetTableForPos returns a best-effort schema for a specific gtid
   134  func (h *historian) GetTableForPos(tableName sqlparser.IdentifierCS, gtid string) (*binlogdatapb.MinimalTable, error) {
   135  	h.mu.Lock()
   136  	defer h.mu.Unlock()
   137  	if !h.isOpen {
   138  		return nil, nil
   139  	}
   140  
   141  	log.V(2).Infof("GetTableForPos called for %s with pos %s", tableName, gtid)
   142  	if gtid == "" {
   143  		return nil, nil
   144  	}
   145  	pos, err := mysql.DecodePosition(gtid)
   146  	if err != nil {
   147  		return nil, err
   148  	}
   149  	var t *binlogdatapb.MinimalTable
   150  	if len(h.schemas) > 0 {
   151  		t = h.getTableFromHistoryForPos(tableName, pos)
   152  	}
   153  	if t != nil {
   154  		log.V(2).Infof("Returning table %s from history for pos %s, schema %s", tableName, gtid, t)
   155  	}
   156  	return t, nil
   157  }
   158  
   159  // loadFromDB loads all rows from the schema_version table that the historian does not have as yet
   160  // caller should have locked h.mu
   161  func (h *historian) loadFromDB(ctx context.Context) error {
   162  	conn, err := h.conns.Get(ctx, nil)
   163  	if err != nil {
   164  		return err
   165  	}
   166  	defer conn.Recycle()
   167  	tableData, err := conn.Exec(ctx, fmt.Sprintf(getSchemaVersions, h.lastID), 10000, true)
   168  	if err != nil {
   169  		log.Infof("Error reading schema_tracking table %v, will operate with the latest available schema", err)
   170  		return nil
   171  	}
   172  	for _, row := range tableData.Rows {
   173  		trackedSchema, id, err := h.readRow(row)
   174  		if err != nil {
   175  			return err
   176  		}
   177  		h.schemas = append(h.schemas, trackedSchema)
   178  		h.lastID = id
   179  	}
   180  	h.sortSchemas()
   181  	return nil
   182  }
   183  
   184  // readRow converts a row from the schema_version table to a trackedSchema
   185  func (h *historian) readRow(row []sqltypes.Value) (*trackedSchema, int64, error) {
   186  	id, _ := evalengine.ToInt64(row[0])
   187  	rowBytes, err := row[1].ToBytes()
   188  	if err != nil {
   189  		return nil, 0, err
   190  	}
   191  	pos, err := mysql.DecodePosition(string(rowBytes))
   192  	if err != nil {
   193  		return nil, 0, err
   194  	}
   195  	rowBytes, err = row[2].ToBytes()
   196  	if err != nil {
   197  		return nil, 0, err
   198  	}
   199  	ddl := string(rowBytes)
   200  	timeUpdated, err := evalengine.ToInt64(row[3])
   201  	if err != nil {
   202  		return nil, 0, err
   203  	}
   204  	sch := &binlogdatapb.MinimalSchema{}
   205  	rowBytes, err = row[4].ToBytes()
   206  	if err != nil {
   207  		return nil, 0, err
   208  	}
   209  	if err := proto.Unmarshal(rowBytes, sch); err != nil {
   210  		return nil, 0, err
   211  	}
   212  	log.V(vl).Infof("Read tracked schema from db: id %d, pos %v, ddl %s, schema len %d, time_updated %d \n",
   213  		id, mysql.EncodePosition(pos), ddl, len(sch.Tables), timeUpdated)
   214  
   215  	tables := map[string]*binlogdatapb.MinimalTable{}
   216  	for _, t := range sch.Tables {
   217  		tables[t.Name] = t
   218  	}
   219  	tSchema := &trackedSchema{
   220  		schema: tables,
   221  		pos:    pos,
   222  		ddl:    ddl,
   223  	}
   224  	return tSchema, id, nil
   225  }
   226  
   227  // sortSchemas sorts entries in ascending order of gtid, ex: 40,44,48
   228  func (h *historian) sortSchemas() {
   229  	sort.Slice(h.schemas, func(i int, j int) bool {
   230  		return h.schemas[j].pos.AtLeast(h.schemas[i].pos)
   231  	})
   232  }
   233  
   234  // getTableFromHistoryForPos looks in the cache for a schema for a specific gtid
   235  func (h *historian) getTableFromHistoryForPos(tableName sqlparser.IdentifierCS, pos mysql.Position) *binlogdatapb.MinimalTable {
   236  	idx := sort.Search(len(h.schemas), func(i int) bool {
   237  		return pos.Equal(h.schemas[i].pos) || !pos.AtLeast(h.schemas[i].pos)
   238  	})
   239  	if idx >= len(h.schemas) || idx == 0 && !pos.Equal(h.schemas[idx].pos) { // beyond the range of the cache
   240  		log.Infof("Schema not found in cache for %s with pos %s", tableName, pos)
   241  		return nil
   242  	}
   243  	if pos.Equal(h.schemas[idx].pos) { //exact match to a cache entry
   244  		return h.schemas[idx].schema[tableName.String()]
   245  	}
   246  	//not an exact match, so based on our sort algo idx is one less than found: from 40,44,48 : 43 < 44 but we want 40
   247  	return h.schemas[idx-1].schema[tableName.String()]
   248  }