github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/engine/pkg/meta/internal/sqlkv/sql_impl.go (about)

     1  // Copyright 2022 PingCAP, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package sqlkv
    15  
    16  import (
    17  	"context"
    18  	"database/sql"
    19  	"sync"
    20  	"time"
    21  
    22  	"github.com/VividCortex/mysqlerr"
    23  	"github.com/go-sql-driver/mysql"
    24  	"github.com/pingcap/log"
    25  	sqlkvModel "github.com/pingcap/tiflow/engine/pkg/meta/internal/sqlkv/model"
    26  	metaModel "github.com/pingcap/tiflow/engine/pkg/meta/model"
    27  	"github.com/pingcap/tiflow/engine/pkg/orm"
    28  	ormModel "github.com/pingcap/tiflow/engine/pkg/orm/model"
    29  	"github.com/pingcap/tiflow/pkg/errors"
    30  	"go.uber.org/zap"
    31  	"gorm.io/gorm"
    32  	"gorm.io/gorm/clause"
    33  )
    34  
    35  // Where clause for meta kv option
    36  // NOTE: 'job_id' and 'meta_key' MUST be same as backend table
    37  const (
    38  	WhereClauseWithJobID     = "job_id = ?"
    39  	WhereClauseWithKeyRange  = "meta_key >= ? AND meta_key < ?"
    40  	WhereClauseWithKeyPrefix = "meta_key like ?"
    41  	WhereClauseWithFromKey   = "meta_key >= ?"
    42  	WhereClauseWithKey       = "meta_key = ?"
    43  )
    44  
    45  // sqlKVClientImpl is the mysql-compatible implement for KVClient
    46  type sqlKVClientImpl struct {
    47  	// db is the original gorm.DB without table scope
    48  	db    *gorm.DB
    49  	jobID metaModel.JobID
    50  	// tableScopeDB is with project-specific metakv table scope
    51  	// we use it in all methods except GenEpoch
    52  	// since GenEpoch uses a different backend table
    53  	tableScopeDB *gorm.DB
    54  
    55  	// meta kv table name
    56  	table string
    57  
    58  	// for GenEpoch
    59  	epochClient ormModel.EpochClient
    60  }
    61  
    62  // NewSQLKVClientImpl new a sql implement for kvclient
    63  func NewSQLKVClientImpl(sqlDB *sql.DB, storeType metaModel.StoreType, table string,
    64  	jobID metaModel.JobID,
    65  ) (*sqlKVClientImpl, error) {
    66  	if sqlDB == nil {
    67  		return nil, errors.ErrMetaParamsInvalid.GenWithStackByArgs("input db is nil")
    68  	}
    69  
    70  	db, err := orm.NewGormDB(sqlDB, storeType)
    71  	if err != nil {
    72  		return nil, err
    73  	}
    74  
    75  	tableScopeDB := db
    76  	if table != "" {
    77  		tableScopeDB = db.Table(table)
    78  	}
    79  
    80  	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    81  	defer cancel()
    82  	impl := &sqlKVClientImpl{
    83  		db:           db,
    84  		jobID:        jobID,
    85  		tableScopeDB: tableScopeDB,
    86  		table:        table,
    87  	}
    88  	if err := impl.initialize(ctx); err != nil {
    89  		return nil, err
    90  	}
    91  
    92  	return impl, nil
    93  }
    94  
    95  // initialize initializes metakv table
    96  // NOTE: Make Sure to call InitializeEpochModel before initializing any KVClient
    97  func (c *sqlKVClientImpl) initialize(ctx context.Context) error {
    98  	if err := c.tableScopeDB.
    99  		WithContext(ctx).
   100  		AutoMigrate(&sqlkvModel.MetaKV{}); err != nil {
   101  		// since meta kv table is project-isolated and needs to be created dynamically,
   102  		// 'table exists' error will be raised if multi-jobs create meta kv table concurrently.
   103  		// Ignore the specific mysql error code: 1050
   104  		if errMySQL, ok := err.(*mysql.MySQLError); !ok || errMySQL.Number != mysqlerr.ER_TABLE_EXISTS_ERROR {
   105  			return err
   106  		}
   107  		log.Info("meet 'table exists' error when creating meta kv table, but can be ignored",
   108  			zap.String("table", c.table))
   109  	}
   110  
   111  	epCli, err := ormModel.NewEpochClient(c.jobID, c.db)
   112  	if err != nil {
   113  		return err
   114  	}
   115  	c.epochClient = epCli
   116  	return nil
   117  }
   118  
   119  // Close implements Close interface of Client
   120  func (c *sqlKVClientImpl) Close() error {
   121  	return nil
   122  }
   123  
   124  // GetEpoch implements GenEpoch interface of Client
   125  // Guarantee to be thread-safe
   126  func (c *sqlKVClientImpl) GenEpoch(ctx context.Context) (int64, error) {
   127  	return c.epochClient.GenEpoch(ctx)
   128  }
   129  
   130  // Put implements Put interface of KV
   131  // Guarantee to be thread-safe
   132  func (c *sqlKVClientImpl) Put(ctx context.Context, key, val string) (*metaModel.PutResponse, metaModel.Error) {
   133  	op := metaModel.OpPut(key, val)
   134  	return c.doPut(ctx, c.tableScopeDB, &op)
   135  }
   136  
   137  func (c *sqlKVClientImpl) doPut(ctx context.Context, db *gorm.DB, op *metaModel.Op) (*metaModel.PutResponse, metaModel.Error) {
   138  	if err := db.WithContext(ctx).
   139  		Clauses(clause.OnConflict{
   140  			UpdateAll: true,
   141  		}).Create(&sqlkvModel.MetaKV{
   142  		JobID: c.jobID,
   143  		KeyValue: metaModel.KeyValue{
   144  			Key:   op.KeyBytes(),
   145  			Value: op.ValueBytes(),
   146  		},
   147  	}).Error; err != nil {
   148  		return nil, sqlErrorFromOpFail(err)
   149  	}
   150  
   151  	return &metaModel.PutResponse{
   152  		Header: &metaModel.ResponseHeader{},
   153  	}, nil
   154  }
   155  
   156  // Get implements Get interface of KV
   157  // Guarantee to be thread-safe
   158  func (c *sqlKVClientImpl) Get(ctx context.Context, key string, opts ...metaModel.OpOption) (*metaModel.GetResponse, metaModel.Error) {
   159  	op := metaModel.OpGet(key, opts...)
   160  	return c.doGet(ctx, c.tableScopeDB, &op)
   161  }
   162  
   163  func (c *sqlKVClientImpl) doGet(ctx context.Context, db *gorm.DB, op *metaModel.Op) (*metaModel.GetResponse, metaModel.Error) {
   164  	if err := op.CheckValidOp(); err != nil {
   165  		return nil, &sqlError{
   166  			displayed: errors.ErrMetaOptionInvalid.Wrap(err),
   167  		}
   168  	}
   169  
   170  	var (
   171  		metaKvs    []*sqlkvModel.MetaKV
   172  		metaKv     sqlkvModel.MetaKV
   173  		err        error
   174  		isPointGet bool
   175  		key        = op.KeyBytes()
   176  	)
   177  
   178  	db = db.WithContext(ctx).Where(WhereClauseWithJobID, c.jobID)
   179  	switch {
   180  	case op.IsOptsWithRange():
   181  		err = db.Where(WhereClauseWithKeyRange, key, op.RangeBytes()).Find(&metaKvs).Error
   182  	case op.IsOptsWithPrefix():
   183  		keyPrefix := make([]byte, len(key)+1)
   184  		copy(keyPrefix, key)
   185  		keyPrefix[len(key)] = '%'
   186  		err = db.Where(WhereClauseWithKeyPrefix, keyPrefix).Find(&metaKvs).Error
   187  	case op.IsOptsWithFromKey():
   188  		err = db.Where(WhereClauseWithFromKey, key).Find(&metaKvs).Error
   189  	default:
   190  		err = db.Where(WhereClauseWithKey, key).First(&metaKv).Error
   191  		isPointGet = true
   192  	}
   193  	if err != nil {
   194  		// for Get method, `record not found` error should be translated to empty resp
   195  		if err == gorm.ErrRecordNotFound {
   196  			return &metaModel.GetResponse{
   197  				Header: &metaModel.ResponseHeader{},
   198  				Kvs:    []*metaModel.KeyValue{},
   199  			}, nil
   200  		}
   201  
   202  		return nil, sqlErrorFromOpFail(err)
   203  	}
   204  
   205  	var kvs []*metaModel.KeyValue
   206  	if isPointGet {
   207  		kvs = make([]*metaModel.KeyValue, 0, 1)
   208  		kvs = append(kvs, &metaModel.KeyValue{Key: metaKv.KeyValue.Key, Value: metaKv.KeyValue.Value})
   209  	} else {
   210  		kvs = make([]*metaModel.KeyValue, 0, len(metaKvs))
   211  		for _, metaKv := range metaKvs {
   212  			kvs = append(kvs, &metaModel.KeyValue{Key: metaKv.KeyValue.Key, Value: metaKv.KeyValue.Value})
   213  		}
   214  	}
   215  
   216  	return &metaModel.GetResponse{
   217  		Header: &metaModel.ResponseHeader{},
   218  		Kvs:    kvs,
   219  	}, nil
   220  }
   221  
   222  // Delete implements Delete interface of KV
   223  // Guarantee to be thread-safe
   224  func (c *sqlKVClientImpl) Delete(ctx context.Context, key string, opts ...metaModel.OpOption) (*metaModel.DeleteResponse, metaModel.Error) {
   225  	op := metaModel.OpDelete(key, opts...)
   226  	return c.doDelete(ctx, c.tableScopeDB, &op)
   227  }
   228  
   229  func (c *sqlKVClientImpl) doDelete(ctx context.Context, db *gorm.DB, op *metaModel.Op) (*metaModel.DeleteResponse, metaModel.Error) {
   230  	if err := op.CheckValidOp(); err != nil {
   231  		return nil, &sqlError{
   232  			displayed: errors.ErrMetaOptionInvalid.Wrap(err),
   233  		}
   234  	}
   235  
   236  	var (
   237  		err error
   238  		key = op.KeyBytes()
   239  	)
   240  
   241  	db = db.WithContext(ctx).Where(WhereClauseWithJobID, c.jobID)
   242  	switch {
   243  	case op.IsOptsWithRange():
   244  		err = db.Where(WhereClauseWithKeyRange, key,
   245  			op.RangeBytes()).Delete(&sqlkvModel.MetaKV{}).Error
   246  	case op.IsOptsWithPrefix():
   247  		keyPrefix := make([]byte, len(key)+1)
   248  		copy(keyPrefix, key)
   249  		keyPrefix[len(key)] = '%'
   250  		err = db.Where(WhereClauseWithKeyPrefix, keyPrefix).Delete(&sqlkvModel.MetaKV{}).Error
   251  	case op.IsOptsWithFromKey():
   252  		err = db.Where(WhereClauseWithFromKey, key).Delete(&sqlkvModel.MetaKV{}).Error
   253  	default:
   254  		err = db.Where(WhereClauseWithKey, key).Delete(&sqlkvModel.MetaKV{}).Error
   255  	}
   256  	if err != nil {
   257  		return nil, sqlErrorFromOpFail(err)
   258  	}
   259  
   260  	return &metaModel.DeleteResponse{
   261  		Header: &metaModel.ResponseHeader{},
   262  	}, nil
   263  }
   264  
   265  type sqlTxn struct {
   266  	mu sync.Mutex
   267  
   268  	ctx  context.Context
   269  	impl *sqlKVClientImpl
   270  	ops  []metaModel.Op
   271  	// cache error to make chain operation work
   272  	Err       *sqlError
   273  	committed bool
   274  }
   275  
   276  // Txn implements Txn interface of KV
   277  func (c *sqlKVClientImpl) Txn(ctx context.Context) metaModel.Txn {
   278  	return &sqlTxn{
   279  		ctx:  ctx,
   280  		impl: c,
   281  		ops:  make([]metaModel.Op, 0, 2),
   282  	}
   283  }
   284  
   285  // Do implements Do interface of Txn
   286  // Guarantee to be thread-safe
   287  func (t *sqlTxn) Do(ops ...metaModel.Op) metaModel.Txn {
   288  	t.mu.Lock()
   289  	defer t.mu.Unlock()
   290  
   291  	if t.Err != nil {
   292  		return t
   293  	}
   294  
   295  	if t.committed {
   296  		t.Err = &sqlError{
   297  			displayed: errors.ErrMetaCommittedTxn.GenWithStackByArgs("txn had been committed"),
   298  		}
   299  		return t
   300  	}
   301  
   302  	t.ops = append(t.ops, ops...)
   303  	return t
   304  }
   305  
   306  // Commit implements Commit interface of Txn
   307  // Guarantee to be thread-safe
   308  func (t *sqlTxn) Commit() (*metaModel.TxnResponse, metaModel.Error) {
   309  	t.mu.Lock()
   310  	if t.Err != nil {
   311  		t.mu.Unlock()
   312  		return nil, t.Err
   313  	}
   314  	if t.committed {
   315  		t.Err = &sqlError{
   316  			displayed: errors.ErrMetaCommittedTxn.GenWithStackByArgs("txn had been committed"),
   317  		}
   318  		t.mu.Unlock()
   319  		return nil, t.Err
   320  	}
   321  	t.committed = true
   322  	t.mu.Unlock()
   323  
   324  	var txnRsp metaModel.TxnResponse
   325  	txnRsp.Responses = make([]metaModel.ResponseOp, 0, len(t.ops))
   326  	err := t.impl.tableScopeDB.Transaction(func(tx *gorm.DB) error {
   327  		for _, op := range t.ops {
   328  			switch {
   329  			case op.IsGet():
   330  				rsp, err := t.impl.doGet(t.ctx, tx, &op)
   331  				if err != nil {
   332  					return err // rollback
   333  				}
   334  				txnRsp.Responses = append(txnRsp.Responses, makeGetResponseOp(rsp))
   335  			case op.IsPut():
   336  				rsp, err := t.impl.doPut(t.ctx, tx, &op)
   337  				if err != nil {
   338  					return err
   339  				}
   340  				txnRsp.Responses = append(txnRsp.Responses, makePutResponseOp(rsp))
   341  			case op.IsDelete():
   342  				rsp, err := t.impl.doDelete(t.ctx, tx, &op)
   343  				if err != nil {
   344  					return err
   345  				}
   346  				txnRsp.Responses = append(txnRsp.Responses, makeDelResponseOp(rsp))
   347  			case op.IsTxn():
   348  				return &sqlError{
   349  					displayed: errors.ErrMetaNestedTxn.GenWithStackByArgs("unsupported nested txn"),
   350  				}
   351  			default:
   352  				return &sqlError{
   353  					displayed: errors.ErrMetaOpFail.GenWithStackByArgs("unknown op type"),
   354  				}
   355  			}
   356  		}
   357  
   358  		return nil // commit
   359  	})
   360  	if err != nil {
   361  		err2, ok := err.(*sqlError)
   362  		if ok {
   363  			return nil, err2
   364  		}
   365  
   366  		return nil, sqlErrorFromOpFail(err2)
   367  	}
   368  
   369  	return &txnRsp, nil
   370  }