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  }