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  }