github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/engine/pkg/orm/model/logic_epoch.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 model 15 16 import ( 17 "context" 18 19 "github.com/pingcap/failpoint" 20 "github.com/pingcap/tiflow/pkg/errors" 21 "go.uber.org/atomic" 22 "gorm.io/gorm" 23 "gorm.io/gorm/clause" 24 ) 25 26 const ( 27 defaultMinEpoch = 1 28 ) 29 30 // LogicEpoch is used to generate increasing epoch 31 // We use union columns <JobID, Epoch> as uk to achieve job-level isolation 32 type LogicEpoch struct { 33 Model 34 JobID string `gorm:"type:varchar(128) not null;uniqueIndex:uidx_jk"` 35 Epoch int64 `gorm:"type:bigint not null default 1"` 36 } 37 38 // TODO: after we split the orm model, move this client out of the file 39 40 // EpochClient defines the client to generate epoch 41 type EpochClient interface { 42 // GenEpoch increases the backend epoch by 1 and return the new epoch 43 // Guarantee to be thread-safe 44 GenEpoch(ctx context.Context) (int64, error) 45 46 // Close releases some inner resources 47 Close() error 48 } 49 50 // NewEpochClient news a EpochClient 51 // Make Sure to call 'InitEpochModel' to create backend table before 52 // calling 'NewEpochClient' 53 func NewEpochClient(jobID string, db *gorm.DB) (*epochClient, error) { 54 if db == nil { 55 return nil, errors.ErrMetaParamsInvalid.GenWithStackByArgs("input db is nil") 56 } 57 58 return &epochClient{ 59 jobID: jobID, 60 db: db, 61 }, nil 62 } 63 64 type epochClient struct { 65 // isInitialized is for lazy initialization 66 isInitialized atomic.Bool 67 jobID string 68 db *gorm.DB 69 } 70 71 func (e *epochClient) initialize(ctx context.Context) error { 72 // Do nothing on conflict 73 if err := e.db.WithContext(ctx).Clauses(clause.OnConflict{DoNothing: true}). 74 Create(&LogicEpoch{ 75 JobID: e.jobID, 76 Epoch: defaultMinEpoch, 77 }).Error; err != nil { 78 return errors.ErrMetaOpFail.Wrap(err) 79 } 80 81 return nil 82 } 83 84 // GenEpoch implements GenEpoch of EpochClient 85 // Guarantee to be thread-safe 86 func (e *epochClient) GenEpoch(ctx context.Context) (int64, error) { 87 // we make lazy initialization for two reasons: 88 // 1. Not all kinds of client need calling GenEpoch 89 // 2. Some components depend on framework meta client before initializing the backend meta table 90 if !e.isInitialized.Load() { 91 if err := e.initialize(ctx); err != nil { 92 return int64(0), err 93 } 94 e.isInitialized.Store(true) 95 } 96 97 failpoint.InjectContext(ctx, "genEpochDelay", nil) 98 if e.db == nil { 99 return int64(0), errors.ErrMetaParamsInvalid.GenWithStackByArgs("inner db is nil") 100 } 101 102 var epoch int64 103 // every job owns its logic epoch 104 err := e.db.WithContext(ctx). 105 Where("job_id = ?", e.jobID). 106 Transaction(func(tx *gorm.DB) error { 107 //(1)update epoch = epoch + 1 108 if err := tx.Model(&LogicEpoch{}). 109 Update("epoch", gorm.Expr("epoch + ?", 1)).Error; err != nil { 110 // return any error will rollback 111 return err 112 } 113 114 //(2)select epoch 115 var logicEp LogicEpoch 116 if err := tx.First(&logicEp).Error; err != nil { 117 return err 118 } 119 epoch = logicEp.Epoch 120 121 // return nil will commit the whole transaction 122 return nil 123 }) 124 if err != nil { 125 return int64(0), err 126 } 127 128 return epoch, nil 129 } 130 131 // Close implements Close of EpochClient 132 func (e *epochClient) Close() error { 133 return nil 134 }