vitess.io/vitess@v0.16.2/go/vt/vttablet/tabletmanager/rpc_lock_tables.go (about)

     1  /*
     2  Copyright 2019 The Vitess Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package tabletmanager
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"fmt"
    23  	"strings"
    24  	"time"
    25  
    26  	"github.com/spf13/pflag"
    27  
    28  	"vitess.io/vitess/go/sqlescape"
    29  	"vitess.io/vitess/go/vt/dbconnpool"
    30  	"vitess.io/vitess/go/vt/log"
    31  	"vitess.io/vitess/go/vt/servenv"
    32  )
    33  
    34  var lockTablesTimeout = 1 * time.Minute
    35  
    36  func registerLockTablesFlags(fs *pflag.FlagSet) {
    37  	fs.DurationVar(&lockTablesTimeout, "lock_tables_timeout", lockTablesTimeout, "How long to keep the table locked before timing out")
    38  }
    39  
    40  func init() {
    41  	servenv.OnParseFor("vtcombo", registerLockTablesFlags)
    42  	servenv.OnParseFor("vttablet", registerLockTablesFlags)
    43  }
    44  
    45  // LockTables will lock all tables with read locks, effectively pausing replication while the lock is held (idempotent)
    46  func (tm *TabletManager) LockTables(ctx context.Context) error {
    47  	// get a connection
    48  	tm.mutex.Lock()
    49  	defer tm.mutex.Unlock()
    50  
    51  	if tm._lockTablesConnection != nil {
    52  		// tables are already locked, bail out
    53  		return errors.New("tables already locked on this tablet")
    54  	}
    55  
    56  	conn, err := tm.MysqlDaemon.GetDbaConnection(ctx)
    57  	if err != nil {
    58  		return err
    59  	}
    60  	// We successfully opened a connection. If we return for any reason before
    61  	// storing this connection in the TabletManager object, we need to close it
    62  	// to avoid leaking it.
    63  	defer func() {
    64  		if tm._lockTablesConnection != conn {
    65  			conn.Close()
    66  		}
    67  	}()
    68  
    69  	// FTWRL is preferable, so we'll try that first
    70  	_, err = conn.ExecuteFetch("FLUSH TABLES WITH READ LOCK", 0, false)
    71  	if err != nil {
    72  		// as fall back, we can lock each individual table as well.
    73  		// this requires slightly less privileges but achieves the same effect
    74  		err = tm.lockTablesUsingLockTables(conn)
    75  		if err != nil {
    76  			return err
    77  		}
    78  	}
    79  	log.Infof("[%v] Tables locked", conn.ConnectionID)
    80  
    81  	tm._lockTablesConnection = conn
    82  	tm._lockTablesTimer = time.AfterFunc(lockTablesTimeout, func() {
    83  		// Here we'll sleep until the timeout time has elapsed.
    84  		// If the table locks have not been released yet, we'll release them here
    85  		tm.mutex.Lock()
    86  		defer tm.mutex.Unlock()
    87  
    88  		// We need the mutex locked before we check this field
    89  		if tm._lockTablesConnection == conn {
    90  			log.Errorf("table lock timed out and released the lock - something went wrong")
    91  			err = tm.unlockTablesHoldingMutex()
    92  			if err != nil {
    93  				log.Errorf("failed to unlock tables: %v", err)
    94  			}
    95  		}
    96  	})
    97  
    98  	return nil
    99  }
   100  
   101  func (tm *TabletManager) lockTablesUsingLockTables(conn *dbconnpool.DBConnection) error {
   102  	log.Warningf("failed to lock tables with FTWRL - falling back to LOCK TABLES")
   103  
   104  	// Ensure schema engine is Open. If vttablet came up in a non_serving role,
   105  	// the schema engine may not have been initialized. Open() is idempotent, so this
   106  	// is always safe
   107  	se := tm.QueryServiceControl.SchemaEngine()
   108  	if err := se.Open(); err != nil {
   109  		return err
   110  	}
   111  
   112  	tables := se.GetSchema()
   113  	tableNames := make([]string, 0, len(tables))
   114  	for name := range tables {
   115  		if name == "dual" {
   116  			continue
   117  		}
   118  		tableNames = append(tableNames, fmt.Sprintf("%s READ", sqlescape.EscapeID(name)))
   119  	}
   120  	lockStatement := fmt.Sprintf("LOCK TABLES %v", strings.Join(tableNames, ", "))
   121  	_, err := conn.ExecuteFetch("USE "+sqlescape.EscapeID(tm.DBConfigs.DBName), 0, false)
   122  	if err != nil {
   123  		return err
   124  	}
   125  
   126  	_, err = conn.ExecuteFetch(lockStatement, 0, false)
   127  	if err != nil {
   128  		return err
   129  	}
   130  
   131  	return nil
   132  }
   133  
   134  // UnlockTables will unlock all tables (idempotent)
   135  func (tm *TabletManager) UnlockTables(ctx context.Context) error {
   136  	tm.mutex.Lock()
   137  	defer tm.mutex.Unlock()
   138  
   139  	if tm._lockTablesConnection == nil {
   140  		return fmt.Errorf("tables were not locked")
   141  	}
   142  
   143  	return tm.unlockTablesHoldingMutex()
   144  }
   145  
   146  func (tm *TabletManager) unlockTablesHoldingMutex() error {
   147  	// We are cleaning up manually, let's kill the timer
   148  	tm._lockTablesTimer.Stop()
   149  	_, err := tm._lockTablesConnection.ExecuteFetch("UNLOCK TABLES", 0, false)
   150  	if err != nil {
   151  		return err
   152  	}
   153  	log.Infof("[%v] Tables unlocked", tm._lockTablesConnection.ConnectionID)
   154  	tm._lockTablesConnection.Close()
   155  	tm._lockTablesConnection = nil
   156  	tm._lockTablesTimer = nil
   157  
   158  	return nil
   159  }