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 }