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 }