Commit cd8054d4 authored by unknown's avatar unknown

Making FLUSH TABLES WITH READ LOCK block COMMITs of existing transactions,

in a deadlock-free manner. This splits locking the global read lock in two steps.
This fixes a consequence of this bug, known as:
BUG#4953 'mysqldump --master-data may report incorrect binlog position if using InnoDB'
And a test.


sql/handler.cc:
  making COMMIT wait if FLUSH TABLES WITH READ LOCK happened.
sql/lock.cc:
  an additional stage so that FLUSH TABLES WITH READ LOCK blocks COMMIT:
  make_global_read_lock_block_commit():
  taking the global read lock is TWO steps (2nd step is optional; without
  it, COMMIT of existing transactions will be allowed):
  lock_global_read_lock() THEN make_global_read_lock_block_commit().
sql/mysql_priv.h:
  new argument to wait_if_global_read_lock()
sql/sql_class.h:
  THD::global_read_lock now an uint to reflect the 2 steps of global read lock (does not block COMMIT / does)
sql/sql_db.cc:
  update for new prototype
sql/sql_parse.cc:
  implementing the two steps of global read lock so that FLUSH TABLES WITH READ LOCK can block COMMIT without deadlocking with COMMITs.
parent f758ada4
drop table if exists t1;
create table t1 (a int) type=innodb;
begin;
insert into t1 values(1);
flush tables with read lock;
select * from t1;
a
commit;
select * from t1;
a
unlock tables;
begin;
select * from t1 for update;
a
1
begin;
select * from t1 for update;
flush tables with read lock;
commit;
a
1
unlock tables;
drop table t1;
# Let's see if FLUSH TABLES WITH READ LOCK blocks COMMIT of existing
# transactions.
# We verify that we did not introduce a deadlock.
-- source include/have_innodb.inc
connect (con1,localhost,root,,);
connect (con2,localhost,root,,);
connect (con3,localhost,root,,);
connection con1;
drop table if exists t1;
create table t1 (a int) type=innodb;
# blocks COMMIT ?
begin;
insert into t1 values(1);
connection con2;
flush tables with read lock;
select * from t1;
connection con1;
send commit; # blocked by con2
sleep 1;
connection con2;
select * from t1; # verify con1 was blocked and data did not move
unlock tables;
connection con1;
reap;
# No deadlock ?
connection con1;
begin;
select * from t1 for update;
connection con2;
begin;
send select * from t1 for update; # blocked by con1
sleep 1;
connection con3;
send flush tables with read lock; # blocked by con2
connection con1;
commit; # should not be blocked by con3
connection con2;
reap;
connection con3;
reap;
unlock tables;
connection con1;
drop table t1;
...@@ -342,18 +342,31 @@ int ha_commit_trans(THD *thd, THD_TRANS* trans) ...@@ -342,18 +342,31 @@ int ha_commit_trans(THD *thd, THD_TRANS* trans)
#ifdef USING_TRANSACTIONS #ifdef USING_TRANSACTIONS
if (opt_using_transactions) if (opt_using_transactions)
{ {
bool operation_done= 0; bool operation_done= 0, need_start_waiters= 0;
bool transaction_commited= 0; bool transaction_commited= 0;
/* If transaction has done some updates to tables */
/* Update the binary log if we have cached some queries */ if (trans == &thd->transaction.all &&
if (trans == &thd->transaction.all && mysql_bin_log.is_open() &&
my_b_tell(&thd->transaction.trans_log)) my_b_tell(&thd->transaction.trans_log))
{
if (error= wait_if_global_read_lock(thd, 0, 0))
{
/*
Note that ROLLBACK [TO SAVEPOINT] does not have this test; it's
because ROLLBACK never updates data, so needn't wait on the lock.
*/
my_error(ER_ERROR_DURING_COMMIT, MYF(0), error);
error= 1;
}
else
need_start_waiters= 1;
if (mysql_bin_log.is_open())
{ {
mysql_bin_log.write(thd, &thd->transaction.trans_log, 1); mysql_bin_log.write(thd, &thd->transaction.trans_log, 1);
reinit_io_cache(&thd->transaction.trans_log, reinit_io_cache(&thd->transaction.trans_log,
WRITE_CACHE, (my_off_t) 0, 0, 1); WRITE_CACHE, (my_off_t) 0, 0, 1);
thd->transaction.trans_log.end_of_file= max_binlog_cache_size; thd->transaction.trans_log.end_of_file= max_binlog_cache_size;
} }
}
#ifdef HAVE_BERKELEY_DB #ifdef HAVE_BERKELEY_DB
if (trans->bdb_tid) if (trans->bdb_tid)
{ {
...@@ -393,6 +406,8 @@ int ha_commit_trans(THD *thd, THD_TRANS* trans) ...@@ -393,6 +406,8 @@ int ha_commit_trans(THD *thd, THD_TRANS* trans)
statistic_increment(ha_commit_count,&LOCK_status); statistic_increment(ha_commit_count,&LOCK_status);
thd->transaction.cleanup(); thd->transaction.cleanup();
} }
if (need_start_waiters)
start_waiting_global_read_lock(thd);
} }
#endif // using transactions #endif // using transactions
DBUG_RETURN(error); DBUG_RETURN(error);
......
...@@ -96,7 +96,7 @@ MYSQL_LOCK *mysql_lock_tables(THD *thd,TABLE **tables,uint count) ...@@ -96,7 +96,7 @@ MYSQL_LOCK *mysql_lock_tables(THD *thd,TABLE **tables,uint count)
Someone has issued LOCK ALL TABLES FOR READ and we want a write lock Someone has issued LOCK ALL TABLES FOR READ and we want a write lock
Wait until the lock is gone Wait until the lock is gone
*/ */
if (wait_if_global_read_lock(thd, 1)) if (wait_if_global_read_lock(thd, 1, 1))
{ {
my_free((gptr) sql_lock,MYF(0)); my_free((gptr) sql_lock,MYF(0));
sql_lock=0; sql_lock=0;
...@@ -453,7 +453,7 @@ int lock_and_wait_for_table_name(THD *thd, TABLE_LIST *table_list) ...@@ -453,7 +453,7 @@ int lock_and_wait_for_table_name(THD *thd, TABLE_LIST *table_list)
int error= -1; int error= -1;
DBUG_ENTER("lock_and_wait_for_table_name"); DBUG_ENTER("lock_and_wait_for_table_name");
if (wait_if_global_read_lock(thd,0)) if (wait_if_global_read_lock(thd, 0, 1))
DBUG_RETURN(1); DBUG_RETURN(1);
VOID(pthread_mutex_lock(&LOCK_open)); VOID(pthread_mutex_lock(&LOCK_open));
if ((lock_retcode = lock_table_name(thd, table_list)) < 0) if ((lock_retcode = lock_table_name(thd, table_list)) < 0)
...@@ -667,14 +667,23 @@ static void print_lock_error(int error) ...@@ -667,14 +667,23 @@ static void print_lock_error(int error)
The global locks are handled through the global variables: The global locks are handled through the global variables:
global_read_lock global_read_lock
global_read_lock_blocks_commit
waiting_for_read_lock waiting_for_read_lock
protect_against_global_read_lock protect_against_global_read_lock
Taking the global read lock is TWO steps (2nd step is optional; without
it, COMMIT of existing transactions will be allowed):
lock_global_read_lock() THEN make_global_read_lock_block_commit().
****************************************************************************/ ****************************************************************************/
volatile uint global_read_lock=0; volatile uint global_read_lock=0;
volatile uint global_read_lock_blocks_commit=0;
static volatile uint protect_against_global_read_lock=0; static volatile uint protect_against_global_read_lock=0;
static volatile uint waiting_for_read_lock=0; static volatile uint waiting_for_read_lock=0;
#define GOT_GLOBAL_READ_LOCK 1
#define MADE_GLOBAL_READ_LOCK_BLOCK_COMMIT 2
bool lock_global_read_lock(THD *thd) bool lock_global_read_lock(THD *thd)
{ {
DBUG_ENTER("lock_global_read_lock"); DBUG_ENTER("lock_global_read_lock");
...@@ -697,27 +706,40 @@ bool lock_global_read_lock(THD *thd) ...@@ -697,27 +706,40 @@ bool lock_global_read_lock(THD *thd)
thd->exit_cond(old_message); thd->exit_cond(old_message);
DBUG_RETURN(1); DBUG_RETURN(1);
} }
thd->global_read_lock=1; thd->global_read_lock= GOT_GLOBAL_READ_LOCK;
global_read_lock++; global_read_lock++;
thd->exit_cond(old_message); thd->exit_cond(old_message);
} }
/*
We DON'T set global_read_lock_blocks_commit now, it will be set after
tables are flushed (as the present function serves for FLUSH TABLES WITH
READ LOCK only). Doing things in this order is necessary to avoid
deadlocks (we must allow COMMIT until all tables are closed; we should not
forbid it before, or we can have a 3-thread deadlock if 2 do SELECT FOR
UPDATE and one does FLUSH TABLES WITH READ LOCK).
*/
DBUG_RETURN(0); DBUG_RETURN(0);
} }
void unlock_global_read_lock(THD *thd) void unlock_global_read_lock(THD *thd)
{ {
uint tmp; uint tmp;
thd->global_read_lock=0;
pthread_mutex_lock(&LOCK_open); pthread_mutex_lock(&LOCK_open);
tmp= --global_read_lock; tmp= --global_read_lock;
if (thd->global_read_lock == MADE_GLOBAL_READ_LOCK_BLOCK_COMMIT)
--global_read_lock_blocks_commit;
pthread_mutex_unlock(&LOCK_open); pthread_mutex_unlock(&LOCK_open);
/* Send the signal outside the mutex to avoid a context switch */ /* Send the signal outside the mutex to avoid a context switch */
if (!tmp) if (!tmp)
pthread_cond_broadcast(&COND_refresh); pthread_cond_broadcast(&COND_refresh);
thd->global_read_lock= 0;
} }
#define must_wait (global_read_lock && \
(is_not_commit || \
global_read_lock_blocks_commit))
bool wait_if_global_read_lock(THD *thd, bool abort_on_refresh) bool wait_if_global_read_lock(THD *thd, bool abort_on_refresh, bool is_not_commit)
{ {
const char *old_message; const char *old_message;
bool result= 0, need_exit_cond; bool result= 0, need_exit_cond;
...@@ -725,7 +747,7 @@ bool wait_if_global_read_lock(THD *thd, bool abort_on_refresh) ...@@ -725,7 +747,7 @@ bool wait_if_global_read_lock(THD *thd, bool abort_on_refresh)
LINT_INIT(old_message); LINT_INIT(old_message);
(void) pthread_mutex_lock(&LOCK_open); (void) pthread_mutex_lock(&LOCK_open);
if (need_exit_cond= (bool)global_read_lock) if (need_exit_cond= must_wait)
{ {
if (thd->global_read_lock) // This thread had the read locks if (thd->global_read_lock) // This thread had the read locks
{ {
...@@ -735,7 +757,7 @@ bool wait_if_global_read_lock(THD *thd, bool abort_on_refresh) ...@@ -735,7 +757,7 @@ bool wait_if_global_read_lock(THD *thd, bool abort_on_refresh)
} }
old_message=thd->enter_cond(&COND_refresh, &LOCK_open, old_message=thd->enter_cond(&COND_refresh, &LOCK_open,
"Waiting for release of readlock"); "Waiting for release of readlock");
while (global_read_lock && ! thd->killed && while (must_wait && ! thd->killed &&
(!abort_on_refresh || thd->version == refresh_version)) (!abort_on_refresh || thd->version == refresh_version))
(void) pthread_cond_wait(&COND_refresh,&LOCK_open); (void) pthread_cond_wait(&COND_refresh,&LOCK_open);
if (thd->killed) if (thd->killed)
...@@ -762,3 +784,21 @@ void start_waiting_global_read_lock(THD *thd) ...@@ -762,3 +784,21 @@ void start_waiting_global_read_lock(THD *thd)
pthread_cond_broadcast(&COND_refresh); pthread_cond_broadcast(&COND_refresh);
DBUG_VOID_RETURN; DBUG_VOID_RETURN;
} }
void make_global_read_lock_block_commit(THD *thd)
{
/*
If we didn't succeed lock_global_read_lock(), or if we already suceeded
make_global_read_lock_block_commit(), do nothing.
*/
if (thd->global_read_lock != GOT_GLOBAL_READ_LOCK)
return;
pthread_mutex_lock(&LOCK_open);
/* increment this BEFORE waiting on cond (otherwise race cond) */
global_read_lock_blocks_commit++;
while (protect_against_global_read_lock)
pthread_cond_wait(&COND_refresh, &LOCK_open);
pthread_mutex_unlock(&LOCK_open);
thd->global_read_lock= MADE_GLOBAL_READ_LOCK_BLOCK_COMMIT;
}
...@@ -762,8 +762,9 @@ void mysql_lock_abort_for_thread(THD *thd, TABLE *table); ...@@ -762,8 +762,9 @@ void mysql_lock_abort_for_thread(THD *thd, TABLE *table);
MYSQL_LOCK *mysql_lock_merge(MYSQL_LOCK *a,MYSQL_LOCK *b); MYSQL_LOCK *mysql_lock_merge(MYSQL_LOCK *a,MYSQL_LOCK *b);
bool lock_global_read_lock(THD *thd); bool lock_global_read_lock(THD *thd);
void unlock_global_read_lock(THD *thd); void unlock_global_read_lock(THD *thd);
bool wait_if_global_read_lock(THD *thd, bool abort_on_refresh); bool wait_if_global_read_lock(THD *thd, bool abort_on_refresh, bool is_not_commit);
void start_waiting_global_read_lock(THD *thd); void start_waiting_global_read_lock(THD *thd);
void make_global_read_lock_block_commit(THD *thd);
/* Lock based on name */ /* Lock based on name */
int lock_and_wait_for_table_name(THD *thd, TABLE_LIST *table_list); int lock_and_wait_for_table_name(THD *thd, TABLE_LIST *table_list);
......
...@@ -478,7 +478,7 @@ class THD :public ilink ...@@ -478,7 +478,7 @@ class THD :public ilink
ulong rand_saved_seed1, rand_saved_seed2; ulong rand_saved_seed1, rand_saved_seed2;
long dbug_thread_id; long dbug_thread_id;
pthread_t real_id; pthread_t real_id;
uint current_tablenr,tmp_table,cond_count; uint current_tablenr,tmp_table,cond_count,global_read_lock;
uint server_status,open_options,system_thread; uint server_status,open_options,system_thread;
uint32 query_length; uint32 query_length;
uint32 db_length; uint32 db_length;
...@@ -489,7 +489,7 @@ class THD :public ilink ...@@ -489,7 +489,7 @@ class THD :public ilink
bool set_query_id,locked,count_cuted_fields,some_tables_deleted; bool set_query_id,locked,count_cuted_fields,some_tables_deleted;
bool no_errors, allow_sum_func, password, fatal_error; bool no_errors, allow_sum_func, password, fatal_error;
bool query_start_used,last_insert_id_used,insert_id_used,rand_used; bool query_start_used,last_insert_id_used,insert_id_used,rand_used;
bool in_lock_tables,global_read_lock; bool in_lock_tables;
bool query_error, bootstrap, cleanup_done; bool query_error, bootstrap, cleanup_done;
bool safe_to_cache_query; bool safe_to_cache_query;
bool volatile killed; bool volatile killed;
......
...@@ -42,7 +42,7 @@ int mysql_create_db(THD *thd, char *db, uint create_options, bool silent) ...@@ -42,7 +42,7 @@ int mysql_create_db(THD *thd, char *db, uint create_options, bool silent)
VOID(pthread_mutex_lock(&LOCK_mysql_create_db)); VOID(pthread_mutex_lock(&LOCK_mysql_create_db));
// do not create database if another thread is holding read lock // do not create database if another thread is holding read lock
if (wait_if_global_read_lock(thd,0)) if (wait_if_global_read_lock(thd, 0, 1))
{ {
error= -1; error= -1;
goto exit2; goto exit2;
...@@ -146,7 +146,7 @@ int mysql_rm_db(THD *thd,char *db,bool if_exists, bool silent) ...@@ -146,7 +146,7 @@ int mysql_rm_db(THD *thd,char *db,bool if_exists, bool silent)
VOID(pthread_mutex_lock(&LOCK_mysql_create_db)); VOID(pthread_mutex_lock(&LOCK_mysql_create_db));
// do not drop database if another thread is holding read lock // do not drop database if another thread is holding read lock
if (wait_if_global_read_lock(thd,0)) if (wait_if_global_read_lock(thd, 0, 1))
{ {
error= -1; error= -1;
goto exit2; goto exit2;
......
...@@ -3708,7 +3708,11 @@ bool reload_acl_and_cache(THD *thd, ulong options, TABLE_LIST *tables) ...@@ -3708,7 +3708,11 @@ bool reload_acl_and_cache(THD *thd, ulong options, TABLE_LIST *tables)
{ {
if (lock_global_read_lock(thd)) if (lock_global_read_lock(thd))
return 1; return 1;
result=close_cached_tables(thd,(options & REFRESH_FAST) ? 0 : 1,
tables);
make_global_read_lock_block_commit(thd);
} }
else
result=close_cached_tables(thd,(options & REFRESH_FAST) ? 0 : 1, tables); result=close_cached_tables(thd,(options & REFRESH_FAST) ? 0 : 1, tables);
} }
if (options & REFRESH_HOSTS) if (options & REFRESH_HOSTS)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment