vitess.io/vitess@v0.16.2/go/vt/vtctl/workflow/vexec/vexec.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 vexec 18 19 import ( 20 "context" 21 "errors" 22 "fmt" 23 24 "vitess.io/vitess/go/sqltypes" 25 "vitess.io/vitess/go/vt/sqlparser" 26 "vitess.io/vitess/go/vt/topo" 27 "vitess.io/vitess/go/vt/topo/topoproto" 28 "vitess.io/vitess/go/vt/vtgate/evalengine" 29 "vitess.io/vitess/go/vt/vttablet/tmclient" 30 31 querypb "vitess.io/vitess/go/vt/proto/query" 32 ) 33 34 const ( 35 // VExecTableQualifier is the qualifier that all tables supported by vexec 36 // are prefixed by. 37 VExecTableQualifier = "_vt" 38 39 // SchemaMigrationsTableName is the unqualified name of the schema 40 // migrations table supported by vexec. 41 SchemaMigrationsTableName = "schema_migrations" 42 // VReplicationLogTableName is the unqualified name of the vreplication_log 43 // table supported by vexec. 44 VReplicationLogTableName = "vreplication_log" 45 // VReplicationTableName is the unqualified name of the vreplication table 46 // supported by vexec. 47 VReplicationTableName = "vreplication" 48 ) 49 50 var ( // Topo lookup errors. 51 // ErrNoShardPrimary occurs when a shard is found with no serving 52 // primary. 53 ErrNoShardPrimary = errors.New("no primary found for shard") 54 // ErrNoShardsForKeyspace occurs when attempting to run a vexec on an empty 55 // keyspace. 56 ErrNoShardsForKeyspace = errors.New("no shards found in keyspace") 57 ) 58 59 var ( // Query parsing and planning errors. 60 // ErrUnsupportedQuery occurs when attempting to run an unsupported query 61 // through vexec. 62 ErrUnsupportedQuery = errors.New("query not supported by vexec") 63 // ErrUnsupportedTable occurs when attempting to run vexec on an unsupported 64 // table. At the time of writing, this occurs when attempting to query any 65 // table other than _vt.vreplication. 66 ErrUnsupportedTable = errors.New("table not supported by vexec") 67 ) 68 69 // VExec provides the main interface to planning and executing vexec queries 70 // (normally, queries on tables in the `_vt` database). It currently supports 71 // some limited vreplication queries; this set of supported behavior will expand 72 // over time. It may be extended to support schema_migrations queries as well. 73 type VExec struct { 74 ts *topo.Server 75 tmc tmclient.TabletManagerClient 76 77 keyspace string 78 workflow string 79 80 // (TODO:@ajm188) Consider renaming this field to "targets", and then 81 // support different Strategy functions for loading target tablets from a 82 // topo.Server. 83 // 84 // For this, I'm currently thinking: 85 // type TargetStrategy func(ts *topo.Server) ([]*topo.TabletInfo, error) 86 // 87 // We _may_ want this if we ever want a vexec query to target anything other 88 // than "all of the shard primaries in a given keyspace", and I'm not sure 89 // about potential future usages yet. 90 primaries []*topo.TabletInfo 91 // (TODO:@ajm188) Similar to supporting a TargetStrategy for controlling how 92 // a VExec picks which tablets to query, we may also want an 93 // ExecutionStrategy (I'm far less sure about whether we would want this at 94 // all, or what its type definition might look like, than TargetStrategy), 95 // to support running in modes like: 96 // - Execute serially rather than concurrently. 97 // - Only return error if greater than some percentage of the targets fail. 98 } 99 100 // NewVExec returns a new instance suitable for making vexec queries to a given 101 // keyspace (required) and workflow (optional, omit by providing the empty 102 // string). The provided topo server is used to look up target tablets for 103 // queries. A given instance will discover targets exactly once for its 104 // lifetime, so to force a refresh, create another instance. 105 func NewVExec(keyspace string, workflow string, ts *topo.Server, tmc tmclient.TabletManagerClient) *VExec { 106 return &VExec{ 107 ts: ts, 108 tmc: tmc, 109 keyspace: keyspace, 110 workflow: workflow, 111 } 112 } 113 114 // QueryContext executes the given vexec query, returning a mapping of tablet 115 // to querypb.QueryResult. 116 // 117 // On first use, QueryContext will also cause the VExec instance to discover 118 // target tablets from the topo; that target list will be reused for all future 119 // queries made by this instance. 120 // 121 // For details on query parsing and planning, see GetPlanner and the 122 // QueryPlanner interface. 123 func (vx *VExec) QueryContext(ctx context.Context, query string) (map[*topo.TabletInfo]*querypb.QueryResult, error) { 124 if vx.primaries == nil { 125 if err := vx.initialize(ctx); err != nil { 126 return nil, err 127 } 128 } 129 130 stmt, err := sqlparser.Parse(query) 131 if err != nil { 132 return nil, err 133 } 134 135 table, err := extractTableName(stmt) 136 if err != nil { 137 return nil, err 138 } 139 140 planner, err := vx.GetPlanner(ctx, table) 141 if err != nil { 142 return nil, err 143 } 144 145 qp, err := planner.PlanQuery(stmt) 146 if err != nil { 147 return nil, err 148 } 149 150 return qp.ExecuteScatter(ctx, vx.primaries...) 151 } 152 153 func (vx *VExec) initialize(ctx context.Context) error { 154 vx.primaries = nil 155 156 getShardsCtx, getShardsCancel := context.WithTimeout(ctx, topo.RemoteOperationTimeout) 157 defer getShardsCancel() 158 159 shards, err := vx.ts.GetShardNames(getShardsCtx, vx.keyspace) 160 if err != nil { 161 return err 162 } 163 164 if len(shards) == 0 { 165 return fmt.Errorf("%w %s", ErrNoShardsForKeyspace, vx.keyspace) 166 } 167 168 primaries := make([]*topo.TabletInfo, 0, len(shards)) 169 170 for _, shard := range shards { 171 ctx, cancel := context.WithTimeout(ctx, topo.RemoteOperationTimeout) 172 defer cancel() 173 174 si, err := vx.ts.GetShard(ctx, vx.keyspace, shard) 175 if err != nil { 176 return err 177 } 178 179 if si.PrimaryAlias == nil { 180 return fmt.Errorf("%w %s/%s", ErrNoShardPrimary, vx.keyspace, shard) 181 } 182 183 primary, err := vx.ts.GetTablet(ctx, si.PrimaryAlias) 184 if err != nil { 185 return err 186 } 187 188 if primary == nil { 189 return fmt.Errorf("%w %s/%s: tablet %v not found", ErrNoShardPrimary, vx.keyspace, shard, topoproto.TabletAliasString(si.PrimaryAlias)) 190 } 191 192 primaries = append(primaries, primary) 193 } 194 195 vx.primaries = primaries 196 197 return nil 198 } 199 200 // GetPlanner returns an appropriate implementation of a QueryPlanner, depending 201 // on the table being queried. 202 // 203 // On first use, GetPlanner will also cause the VExec instance to discover 204 // target tablets from the topo; that target list will be reused for all future 205 // queries made by this instance. 206 func (vx *VExec) GetPlanner(ctx context.Context, table string) (QueryPlanner, error) { // TODO: private? 207 if vx.primaries == nil { 208 if err := vx.initialize(ctx); err != nil { 209 return nil, fmt.Errorf("error while initializing target list: %w", err) 210 } 211 } 212 213 switch table { 214 case qualifiedTableName(VReplicationTableName): 215 return NewVReplicationQueryPlanner(vx.tmc, vx.workflow, vx.primaries[0].DbName()), nil 216 case qualifiedTableName(VReplicationLogTableName): 217 results, err := vx.QueryContext(ctx, "select id from _vt.vreplication") 218 if err != nil { 219 return nil, err 220 } 221 222 tabletStreamIDMap := make(map[string][]int64, len(results)) 223 224 for tablet, p3qr := range results { 225 qr := sqltypes.Proto3ToResult(p3qr) 226 aliasStr := tablet.AliasString() 227 tabletStreamIDMap[aliasStr] = make([]int64, len(qr.Rows)) 228 229 for i, row := range qr.Rows { 230 id, err := evalengine.ToInt64(row[0]) 231 if err != nil { 232 return nil, err 233 } 234 235 tabletStreamIDMap[aliasStr][i] = id 236 } 237 } 238 239 return NewVReplicationLogQueryPlanner(vx.tmc, tabletStreamIDMap), nil 240 case qualifiedTableName(SchemaMigrationsTableName): 241 return nil, errors.New("Schema Migrations not yet supported in new workflow package") 242 default: 243 return nil, fmt.Errorf("%w: %v", ErrUnsupportedTable, table) 244 } 245 } 246 247 // WithWorkflow returns a copy of VExec with the Workflow field updated. Used so 248 // callers to reuse a VExec's primaries list without needing to initialize a new 249 // VExec instance. 250 func (vx *VExec) WithWorkflow(workflow string) *VExec { 251 return &VExec{ 252 ts: vx.ts, 253 tmc: vx.tmc, 254 primaries: vx.primaries, 255 workflow: workflow, 256 } 257 } 258 259 func extractTableName(stmt sqlparser.Statement) (string, error) { 260 switch stmt := stmt.(type) { 261 case *sqlparser.Update: 262 return sqlparser.String(stmt.TableExprs), nil 263 case *sqlparser.Delete: 264 return sqlparser.String(stmt.TableExprs), nil 265 case *sqlparser.Insert: 266 return sqlparser.String(stmt.Table), nil 267 case *sqlparser.Select: 268 return sqlparser.ToString(stmt.From), nil 269 } 270 271 return "", fmt.Errorf("%w: %+v", ErrUnsupportedQuery, sqlparser.String(stmt)) 272 } 273 274 func qualifiedTableName(name string) string { 275 return fmt.Sprintf("%s.%s", VExecTableQualifier, name) 276 }