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 }