360 lines
10 KiB
Go
360 lines
10 KiB
Go
// Copyright 2021 PingCAP, Inc.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package tables
|
|
|
|
import (
|
|
"context"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/pingcap/errors"
|
|
"github.com/pingcap/tidb/parser/terror"
|
|
"github.com/pingcap/tidb/util/chunk"
|
|
"github.com/pingcap/tidb/util/sqlexec"
|
|
"github.com/tikv/client-go/v2/oracle"
|
|
)
|
|
|
|
// CachedTableLockType define the lock type for cached table
|
|
type CachedTableLockType int
|
|
|
|
const (
|
|
// CachedTableLockNone means there is no lock.
|
|
CachedTableLockNone CachedTableLockType = iota
|
|
// CachedTableLockRead is the READ lock type.
|
|
CachedTableLockRead
|
|
// CachedTableLockIntend is the write INTEND, it exists when the changing READ to WRITE, and the READ lock lease is not expired..
|
|
CachedTableLockIntend
|
|
// CachedTableLockWrite is the WRITE lock type.
|
|
CachedTableLockWrite
|
|
)
|
|
|
|
func (l CachedTableLockType) String() string {
|
|
switch l {
|
|
case CachedTableLockNone:
|
|
return "NONE"
|
|
case CachedTableLockRead:
|
|
return "READ"
|
|
case CachedTableLockIntend:
|
|
return "INTEND"
|
|
case CachedTableLockWrite:
|
|
return "WRITE"
|
|
}
|
|
panic("invalid CachedTableLockType value")
|
|
}
|
|
|
|
// StateRemote is the interface to control the remote state of the cached table's lock meta information.
|
|
type StateRemote interface {
|
|
// Load obtain the corresponding lock type and lease value according to the tableID
|
|
Load(ctx context.Context, tid int64) (CachedTableLockType, uint64, error)
|
|
|
|
// LockForRead try to add a read lock to the table with the specified tableID.
|
|
// If this operation succeed, according to the protocol, the TiKV data will not be
|
|
// modified until the lease expire. It's safe for the caller to load the table data,
|
|
// cache and use the data.
|
|
LockForRead(ctx context.Context, tid int64, lease uint64) (bool, error)
|
|
|
|
// LockForWrite try to add a write lock to the table with the specified tableID
|
|
LockForWrite(ctx context.Context, tid int64, leaseDuration time.Duration) (uint64, error)
|
|
|
|
// RenewLease attempt to renew the read / write lock on the table with the specified tableID
|
|
RenewLease(ctx context.Context, tid int64, newTs uint64, op RenewLeaseType) (bool, error)
|
|
}
|
|
|
|
type sqlExec interface {
|
|
ExecuteInternal(context.Context, string, ...interface{}) (sqlexec.RecordSet, error)
|
|
}
|
|
|
|
type stateRemoteHandle struct {
|
|
exec sqlExec
|
|
sync.Mutex
|
|
}
|
|
|
|
// NewStateRemote creates a StateRemote object.
|
|
func NewStateRemote(exec sqlExec) *stateRemoteHandle {
|
|
return &stateRemoteHandle{
|
|
exec: exec,
|
|
}
|
|
}
|
|
|
|
var _ StateRemote = &stateRemoteHandle{}
|
|
|
|
func (h *stateRemoteHandle) Load(ctx context.Context, tid int64) (CachedTableLockType, uint64, error) {
|
|
lockType, lease, _, err := h.loadRow(ctx, tid)
|
|
return lockType, lease, err
|
|
}
|
|
|
|
func (h *stateRemoteHandle) LockForRead(ctx context.Context, tid int64, newLease uint64) ( /*succ*/ bool, error) {
|
|
h.Lock()
|
|
defer h.Unlock()
|
|
succ := false
|
|
err := h.runInTxn(ctx, func(ctx context.Context, now uint64) error {
|
|
lockType, lease, _, err := h.loadRow(ctx, tid)
|
|
if err != nil {
|
|
return errors.Trace(err)
|
|
}
|
|
// The old lock is outdated, clear orphan lock.
|
|
if now > lease {
|
|
succ = true
|
|
if err := h.updateRow(ctx, tid, "READ", newLease); err != nil {
|
|
return errors.Trace(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
switch lockType {
|
|
case CachedTableLockNone:
|
|
case CachedTableLockRead:
|
|
case CachedTableLockWrite, CachedTableLockIntend:
|
|
return nil
|
|
}
|
|
succ = true
|
|
if newLease > lease { // Note the check, don't decrease lease value!
|
|
if err := h.updateRow(ctx, tid, "READ", newLease); err != nil {
|
|
return errors.Trace(err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
return succ, err
|
|
}
|
|
|
|
// LockForWrite try to add a write lock to the table with the specified tableID, return the write lock lease.
|
|
func (h *stateRemoteHandle) LockForWrite(ctx context.Context, tid int64, leaseDuration time.Duration) (uint64, error) {
|
|
h.Lock()
|
|
defer h.Unlock()
|
|
var ret uint64
|
|
for {
|
|
waitAndRetry, lease, err := h.lockForWriteOnce(ctx, tid, leaseDuration)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if waitAndRetry == 0 {
|
|
ret = lease
|
|
break
|
|
}
|
|
time.Sleep(waitAndRetry)
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
func (h *stateRemoteHandle) lockForWriteOnce(ctx context.Context, tid int64, leaseDuration time.Duration) (waitAndRetry time.Duration, ts uint64, err error) {
|
|
err = h.runInTxn(ctx, func(ctx context.Context, now uint64) error {
|
|
lockType, lease, oldReadLease, err := h.loadRow(ctx, tid)
|
|
if err != nil {
|
|
return errors.Trace(err)
|
|
}
|
|
ts = leaseFromTS(now, leaseDuration)
|
|
// The lease is outdated, so lock is invalid, clear orphan lock of any kind.
|
|
if now > lease {
|
|
if err := h.updateRow(ctx, tid, "WRITE", ts); err != nil {
|
|
return errors.Trace(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// The lease is valid.
|
|
switch lockType {
|
|
case CachedTableLockNone:
|
|
if err = h.updateRow(ctx, tid, "WRITE", ts); err != nil {
|
|
return errors.Trace(err)
|
|
}
|
|
case CachedTableLockRead:
|
|
// Change from READ to INTEND
|
|
if _, err = h.execSQL(ctx,
|
|
"update mysql.table_cache_meta set lock_type='INTEND', oldReadLease=%?, lease=%? where tid=%?",
|
|
lease,
|
|
ts,
|
|
tid); err != nil {
|
|
return errors.Trace(err)
|
|
}
|
|
|
|
// Wait for lease to expire, and then retry.
|
|
waitAndRetry = waitForLeaseExpire(lease, now)
|
|
case CachedTableLockIntend:
|
|
// `now` exceed `oldReadLease` means wait for READ lock lease is done, it's safe to read here.
|
|
if now > oldReadLease {
|
|
if lockType == CachedTableLockIntend {
|
|
if err = h.updateRow(ctx, tid, "WRITE", ts); err != nil {
|
|
return errors.Trace(err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
// Otherwise, the WRITE should wait for the READ lease expire.
|
|
// And then retry changing the lock to WRITE
|
|
waitAndRetry = waitForLeaseExpire(oldReadLease, now)
|
|
}
|
|
return nil
|
|
})
|
|
|
|
return
|
|
}
|
|
|
|
func waitForLeaseExpire(oldReadLease, now uint64) time.Duration {
|
|
if oldReadLease >= now {
|
|
t1 := oracle.GetTimeFromTS(oldReadLease)
|
|
t2 := oracle.GetTimeFromTS(now)
|
|
waitDuration := t1.Sub(t2)
|
|
return waitDuration
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (h *stateRemoteHandle) RenewLease(ctx context.Context, tid int64, newLease uint64, op RenewLeaseType) (bool, error) {
|
|
h.Lock()
|
|
defer h.Unlock()
|
|
|
|
switch op {
|
|
case RenewReadLease:
|
|
return h.renewReadLease(ctx, tid, newLease)
|
|
case RenewWriteLease:
|
|
return h.renewWriteLease(ctx, tid, newLease)
|
|
}
|
|
return false, errors.New("wrong renew lease type")
|
|
}
|
|
|
|
func (h *stateRemoteHandle) renewReadLease(ctx context.Context, tid int64, newLease uint64) (bool, error) {
|
|
var succ bool
|
|
err := h.runInTxn(ctx, func(ctx context.Context, now uint64) error {
|
|
lockType, oldLease, _, err := h.loadRow(ctx, tid)
|
|
if err != nil {
|
|
return errors.Trace(err)
|
|
}
|
|
if now >= oldLease {
|
|
// read lock had already expired, fail to renew
|
|
return nil
|
|
}
|
|
if lockType != CachedTableLockRead {
|
|
// Not read lock, fail to renew
|
|
return nil
|
|
}
|
|
|
|
if newLease > oldLease { // lease should never decrease!
|
|
err = h.updateRow(ctx, tid, "READ", newLease)
|
|
if err != nil {
|
|
return errors.Trace(err)
|
|
}
|
|
}
|
|
succ = true
|
|
return nil
|
|
})
|
|
return succ, err
|
|
}
|
|
|
|
func (h *stateRemoteHandle) renewWriteLease(ctx context.Context, tid int64, newLease uint64) (bool, error) {
|
|
var succ bool
|
|
err := h.runInTxn(ctx, func(ctx context.Context, now uint64) error {
|
|
lockType, oldLease, _, err := h.loadRow(ctx, tid)
|
|
if err != nil {
|
|
return errors.Trace(err)
|
|
}
|
|
if now >= oldLease {
|
|
// write lock had already expired, fail to renew
|
|
return nil
|
|
}
|
|
if lockType != CachedTableLockWrite {
|
|
// Not write lock, fail to renew
|
|
return nil
|
|
}
|
|
|
|
if newLease > oldLease { // lease should never decrease!
|
|
err = h.updateRow(ctx, tid, "WRITE", newLease)
|
|
if err != nil {
|
|
return errors.Trace(err)
|
|
}
|
|
}
|
|
succ = true
|
|
return nil
|
|
})
|
|
return succ, err
|
|
}
|
|
|
|
func (h *stateRemoteHandle) beginTxn(ctx context.Context) error {
|
|
_, err := h.execSQL(ctx, "begin")
|
|
return err
|
|
}
|
|
|
|
func (h *stateRemoteHandle) commitTxn(ctx context.Context) error {
|
|
_, err := h.execSQL(ctx, "commit")
|
|
return err
|
|
}
|
|
|
|
func (h *stateRemoteHandle) rollbackTxn(ctx context.Context) error {
|
|
_, err := h.execSQL(ctx, "rollback")
|
|
return err
|
|
}
|
|
|
|
func (h *stateRemoteHandle) runInTxn(ctx context.Context, fn func(ctx context.Context, txnTS uint64) error) error {
|
|
err := h.beginTxn(ctx)
|
|
if err != nil {
|
|
return errors.Trace(err)
|
|
}
|
|
|
|
rows, err := h.execSQL(ctx, "select @@tidb_current_ts")
|
|
if err != nil {
|
|
return errors.Trace(err)
|
|
}
|
|
resultStr := rows[0].GetString(0)
|
|
txnTS, err := strconv.ParseUint(resultStr, 10, 64)
|
|
if err != nil {
|
|
return errors.Trace(err)
|
|
}
|
|
|
|
err = fn(ctx, txnTS)
|
|
if err != nil {
|
|
terror.Log(h.rollbackTxn(ctx))
|
|
return errors.Trace(err)
|
|
}
|
|
|
|
return h.commitTxn(ctx)
|
|
}
|
|
|
|
func (h *stateRemoteHandle) loadRow(ctx context.Context, tid int64) (CachedTableLockType, uint64, uint64, error) {
|
|
chunkRows, err := h.execSQL(ctx, "select lock_type, lease, oldReadLease from mysql.table_cache_meta where tid = %? for update", tid)
|
|
if err != nil {
|
|
return 0, 0, 0, errors.Trace(err)
|
|
}
|
|
if len(chunkRows) != 1 {
|
|
return 0, 0, 0, errors.Errorf("table_cache_meta tid not exist %d", tid)
|
|
}
|
|
col1 := chunkRows[0].GetEnum(0)
|
|
// Note, the MySQL enum value start from 1 rather than 0
|
|
lockType := CachedTableLockType(col1.Value - 1)
|
|
lease := chunkRows[0].GetUint64(1)
|
|
oldReadLease := chunkRows[0].GetUint64(2)
|
|
return lockType, lease, oldReadLease, nil
|
|
}
|
|
|
|
func (h *stateRemoteHandle) updateRow(ctx context.Context, tid int64, lockType string, lease uint64) error {
|
|
_, err := h.execSQL(ctx, "update mysql.table_cache_meta set lock_type = %?, lease = %? where tid = %?", lockType, lease, tid)
|
|
return err
|
|
}
|
|
|
|
func (h *stateRemoteHandle) execSQL(ctx context.Context, sql string, args ...interface{}) ([]chunk.Row, error) {
|
|
rs, err := h.exec.ExecuteInternal(ctx, sql, args...)
|
|
if rs != nil {
|
|
defer rs.Close()
|
|
}
|
|
if err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
if rs != nil {
|
|
return sqlexec.DrainRecordSet(ctx, rs, 1)
|
|
}
|
|
return nil, nil
|
|
}
|