github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/pkg/election/storage_orm.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 election
    15  
    16  import (
    17  	"context"
    18  	"database/sql"
    19  	"database/sql/driver"
    20  	"encoding/json"
    21  
    22  	"github.com/pingcap/log"
    23  	ormUtil "github.com/pingcap/tiflow/engine/pkg/orm"
    24  	"github.com/pingcap/tiflow/pkg/errors"
    25  	"gorm.io/gorm"
    26  	"gorm.io/gorm/clause"
    27  )
    28  
    29  const (
    30  	recordRowID = 1
    31  	// leaderRowID is not used for node probing, it is only used to lock the leader row.
    32  	leaderRowID = 2
    33  )
    34  
    35  // Value implements the driver.Valuer interface
    36  func (r Record) Value() (driver.Value, error) {
    37  	return json.Marshal(r)
    38  }
    39  
    40  // Scan implements the sql.Scanner interface
    41  func (r *Record) Scan(value interface{}) error {
    42  	b, ok := value.([]byte)
    43  	if !ok {
    44  		return errors.New("type assertion to []byte failed")
    45  	}
    46  
    47  	return json.Unmarshal(b, r)
    48  }
    49  
    50  // TableNameElection is the table name of election.
    51  var TableNameElection = "election"
    52  
    53  // DO mapped from table <election>
    54  type DO struct {
    55  	ID       uint32  `gorm:"column:id;type:int(10) unsigned;primaryKey" json:"id"`
    56  	LeaderID string  `gorm:"column:leader_id;type:text;not null" json:"leader_id"`
    57  	Record   *Record `gorm:"column:record;type:text" json:"record"`
    58  	Version  uint64  `gorm:"column:version;type:bigint(20) unsigned;not null" json:"version"`
    59  }
    60  
    61  // TableName Election's table name
    62  func (*DO) TableName() string {
    63  	return TableNameElection
    64  }
    65  
    66  // ORMStorage is a storage implementation based on SQL database.
    67  type ORMStorage struct {
    68  	db        *gorm.DB
    69  	tableName string
    70  }
    71  
    72  // NewORMStorageFromSQLDB creates a new ORMStorage from sql.DB.
    73  func NewORMStorageFromSQLDB(backendDB *sql.DB, tableName string) (*ORMStorage, error) {
    74  	db, err := ormUtil.NewGormDB(backendDB, "mysql")
    75  	if err != nil {
    76  		return nil, err
    77  	}
    78  	return NewORMStorage(db, tableName)
    79  }
    80  
    81  // NewORMStorage creates a new ORMStorage.
    82  func NewORMStorage(db *gorm.DB, tableName string) (*ORMStorage, error) {
    83  	TableNameElection = tableName
    84  	if err := db.AutoMigrate(&DO{}); err != nil {
    85  		return nil, errors.Trace(err)
    86  	}
    87  
    88  	return &ORMStorage{
    89  		db:        db,
    90  		tableName: tableName,
    91  	}, nil
    92  }
    93  
    94  // Get implements Storage.Get.
    95  func (s *ORMStorage) Get(ctx context.Context) (*Record, error) {
    96  	var do DO
    97  
    98  	ret := s.db.WithContext(ctx).Where("id = ?", recordRowID).Limit(1).Find(&do)
    99  	if ret.Error != nil {
   100  		if ret.Error == gorm.ErrRecordNotFound {
   101  			return &Record{}, nil
   102  		}
   103  		return nil, errors.Trace(ret.Error)
   104  	}
   105  	if ret.RowsAffected == 0 {
   106  		return &Record{}, nil
   107  	}
   108  
   109  	do.Record.Version = int64(do.Version)
   110  	return do.Record, nil
   111  }
   112  
   113  // Update implements Storage.Update.
   114  func (s *ORMStorage) Update(ctx context.Context, record *Record, isLeaderChanged bool) error {
   115  	if record.Version == 0 {
   116  		if !isLeaderChanged {
   117  			log.Panic("invalid operation")
   118  		}
   119  		return s.create(ctx, record)
   120  	}
   121  	return s.update(ctx, record, isLeaderChanged)
   122  }
   123  
   124  func (s *ORMStorage) update(ctx context.Context, record *Record, isLeaderChanged bool) error {
   125  	handleRet := func(ret *gorm.DB) error {
   126  		if ret.Error != nil {
   127  			return errors.Trace(ret.Error)
   128  		}
   129  		if ret.RowsAffected != 1 {
   130  			return errors.ErrElectionRecordConflict.GenWithStackByArgs()
   131  		}
   132  		return nil
   133  	}
   134  	return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
   135  		// TODO: find more efficient way
   136  		ret := tx.Where("id = ? AND version = ?", recordRowID, record.Version).
   137  			Updates(DO{
   138  				LeaderID: record.LeaderID,
   139  				Record:   record,
   140  				Version:  uint64(record.Version) + 1,
   141  			})
   142  		if err := handleRet(ret); err != nil {
   143  			return errors.Trace(err)
   144  		}
   145  
   146  		if isLeaderChanged {
   147  			ret := tx.Where("id = ?", leaderRowID).
   148  				Updates(DO{
   149  					LeaderID: record.LeaderID,
   150  					Record:   nil,
   151  					Version:  uint64(record.Version) + 1,
   152  				})
   153  			return handleRet(ret)
   154  		}
   155  		return nil
   156  	})
   157  }
   158  
   159  func (s *ORMStorage) create(ctx context.Context, record *Record) error {
   160  	rows := []*DO{
   161  		{
   162  			ID:       recordRowID,
   163  			LeaderID: record.LeaderID,
   164  			Record:   record,
   165  			Version:  uint64(record.Version) + 1,
   166  		},
   167  		{
   168  			ID:       leaderRowID,
   169  			LeaderID: record.LeaderID,
   170  			Record:   nil,                        /* record is not saved in leader row */
   171  			Version:  uint64(record.Version) + 1, /* equals to recordRow */
   172  		},
   173  	}
   174  	return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
   175  		ret := tx.WithContext(ctx).Create(rows)
   176  		if ret.Error != nil {
   177  			return errors.Trace(ret.Error)
   178  		}
   179  
   180  		if ret.RowsAffected == 0 {
   181  			return errors.ErrElectionRecordConflict.GenWithStackByArgs()
   182  		} else if ret.RowsAffected != int64(len(rows)) {
   183  			log.Panic("Transaction atomicity is broken when updating election record")
   184  		}
   185  		return nil
   186  	})
   187  }
   188  
   189  // TxnWithLeaderLock execute a transaction with leader row locked.
   190  func (s *ORMStorage) TxnWithLeaderLock(ctx context.Context, leaderID string, fc func(tx *gorm.DB) error) error {
   191  	return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
   192  		var do DO
   193  		ret := tx.Select("leader_id").Where("id = ? and leader_id = ?", leaderRowID, leaderID).
   194  			Clauses(clause.Locking{
   195  				Strength: "SHARE",
   196  				Table:    clause.Table{Name: clause.CurrentTable},
   197  			}).Limit(1).Find(&do)
   198  		if ret.Error != nil {
   199  			if ret.Error == gorm.ErrRecordNotFound {
   200  				return errors.ErrElectorNotLeader.GenWithStackByArgs(leaderID)
   201  			}
   202  			return errors.Trace(ret.Error)
   203  		}
   204  		if ret.RowsAffected != 1 {
   205  			return errors.ErrElectorNotLeader.GenWithStackByArgs(leaderID)
   206  		}
   207  		return fc(tx)
   208  	})
   209  }