mirror of
https://github.com/MariaDB/server.git
synced 2025-07-30 16:24:05 +03:00
Bug#32395 Alter table under a impending global read lock causes a server crash
The problem is that some DDL statements (ALTER TABLE, CREATE TRIGGER, FLUSH TABLES, ...) when under LOCK TABLES need to momentarily drop the lock, reopen the table and grab the write lock again (using reopen_tables). When grabbing the lock again, reopen_tables doesn't pass a flag to mysql_lock_tables in order to ignore the impending global read lock, which causes a assertion because LOCK_open is being hold. Also dropping the lock must not signal to any threads that the table has been relinquished (related to the locking/flushing protocol). The solution is to correct the way the table is reopenned and the locks grabbed. When reopening the table and under LOCK TABLES, the table version should be set to 0 so other threads have to wait for the table. When grabbing the lock, any other flush should be ignored because it's theoretically a atomic operation. The chosen solution also fixes a potential discrepancy between binlog and GRL (global read lock) because table placeholders were being ignored, now a FLUSH TABLES WITH READ LOCK will properly for table with open placeholders. It's also important to mention that this patch doesn't fix a potential deadlock if one uses two GRLs under LOCK TABLES concurrently.
This commit is contained in:
@ -130,7 +130,7 @@ void table_cache_free(void)
|
||||
DBUG_ENTER("table_cache_free");
|
||||
if (table_def_inited)
|
||||
{
|
||||
close_cached_tables((THD*) 0,0,(TABLE_LIST*) 0);
|
||||
close_cached_tables(NULL, NULL, FALSE, FALSE, FALSE);
|
||||
if (!open_cache.records) // Safety first
|
||||
hash_free(&open_cache);
|
||||
}
|
||||
@ -885,16 +885,24 @@ void free_io_cache(TABLE *table)
|
||||
/*
|
||||
Close all tables which aren't in use by any thread
|
||||
|
||||
THD can be NULL, but then if_wait_for_refresh must be FALSE
|
||||
and tables must be NULL.
|
||||
@param thd Thread context
|
||||
@param tables List of tables to remove from the cache
|
||||
@param have_lock If LOCK_open is locked
|
||||
@param wait_for_refresh Wait for a impending flush
|
||||
@param wait_for_placeholders Wait for tables being reopened so that the GRL
|
||||
won't proceed while write-locked tables are being reopened by other
|
||||
threads.
|
||||
|
||||
@remark THD can be NULL, but then wait_for_refresh must be FALSE
|
||||
and tables must be NULL.
|
||||
*/
|
||||
|
||||
bool close_cached_tables(THD *thd, bool if_wait_for_refresh,
|
||||
TABLE_LIST *tables, bool have_lock)
|
||||
bool close_cached_tables(THD *thd, TABLE_LIST *tables, bool have_lock,
|
||||
bool wait_for_refresh, bool wait_for_placeholders)
|
||||
{
|
||||
bool result=0;
|
||||
DBUG_ENTER("close_cached_tables");
|
||||
DBUG_ASSERT(thd || (!if_wait_for_refresh && !tables));
|
||||
DBUG_ASSERT(thd || (!wait_for_refresh && !tables));
|
||||
|
||||
if (!have_lock)
|
||||
VOID(pthread_mutex_lock(&LOCK_open));
|
||||
@ -918,7 +926,7 @@ bool close_cached_tables(THD *thd, bool if_wait_for_refresh,
|
||||
}
|
||||
DBUG_PRINT("tcache", ("incremented global refresh_version to: %lu",
|
||||
refresh_version));
|
||||
if (if_wait_for_refresh)
|
||||
if (wait_for_refresh)
|
||||
{
|
||||
/*
|
||||
Other threads could wait in a loop in open_and_lock_tables(),
|
||||
@ -975,13 +983,13 @@ bool close_cached_tables(THD *thd, bool if_wait_for_refresh,
|
||||
found=1;
|
||||
}
|
||||
if (!found)
|
||||
if_wait_for_refresh=0; // Nothing to wait for
|
||||
wait_for_refresh=0; // Nothing to wait for
|
||||
}
|
||||
#ifndef EMBEDDED_LIBRARY
|
||||
if (!tables)
|
||||
kill_delayed_threads();
|
||||
#endif
|
||||
if (if_wait_for_refresh)
|
||||
if (wait_for_refresh)
|
||||
{
|
||||
/*
|
||||
If there is any table that has a lower refresh_version, wait until
|
||||
@ -1004,6 +1012,9 @@ bool close_cached_tables(THD *thd, bool if_wait_for_refresh,
|
||||
for (uint idx=0 ; idx < open_cache.records ; idx++)
|
||||
{
|
||||
TABLE *table=(TABLE*) hash_element(&open_cache,idx);
|
||||
/* Avoid a self-deadlock. */
|
||||
if (table->in_use == thd)
|
||||
continue;
|
||||
/*
|
||||
Note that we wait here only for tables which are actually open, and
|
||||
not for placeholders with TABLE::open_placeholder set. Waiting for
|
||||
@ -1018,7 +1029,8 @@ bool close_cached_tables(THD *thd, bool if_wait_for_refresh,
|
||||
are employed by CREATE TABLE as in this case table simply does not
|
||||
exist yet.
|
||||
*/
|
||||
if (table->needs_reopen_or_name_lock() && table->db_stat)
|
||||
if (table->needs_reopen_or_name_lock() && (table->db_stat ||
|
||||
(table->open_placeholder && wait_for_placeholders)))
|
||||
{
|
||||
found=1;
|
||||
DBUG_PRINT("signal", ("Waiting for COND_refresh"));
|
||||
@ -1037,11 +1049,18 @@ bool close_cached_tables(THD *thd, bool if_wait_for_refresh,
|
||||
thd->in_lock_tables=0;
|
||||
/* Set version for table */
|
||||
for (TABLE *table=thd->open_tables; table ; table= table->next)
|
||||
table->s->version= refresh_version;
|
||||
{
|
||||
/*
|
||||
Preserve the version (0) of write locked tables so that a impending
|
||||
global read lock won't sneak in.
|
||||
*/
|
||||
if (table->reginfo.lock_type < TL_WRITE_ALLOW_WRITE)
|
||||
table->s->version= refresh_version;
|
||||
}
|
||||
}
|
||||
if (!have_lock)
|
||||
VOID(pthread_mutex_unlock(&LOCK_open));
|
||||
if (if_wait_for_refresh)
|
||||
if (wait_for_refresh)
|
||||
{
|
||||
pthread_mutex_lock(&thd->mysys_var->mutex);
|
||||
thd->mysys_var->current_mutex= 0;
|
||||
@ -1068,10 +1087,10 @@ bool close_cached_connection_tables(THD *thd, bool if_wait_for_refresh,
|
||||
DBUG_ASSERT(thd);
|
||||
|
||||
bzero(&tmp, sizeof(TABLE_LIST));
|
||||
|
||||
|
||||
if (!have_lock)
|
||||
VOID(pthread_mutex_lock(&LOCK_open));
|
||||
|
||||
|
||||
for (idx= 0; idx < table_def_cache.records; idx++)
|
||||
{
|
||||
TABLE_SHARE *share= (TABLE_SHARE *) hash_element(&table_def_cache, idx);
|
||||
@ -1100,11 +1119,11 @@ bool close_cached_connection_tables(THD *thd, bool if_wait_for_refresh,
|
||||
}
|
||||
|
||||
if (tables)
|
||||
result= close_cached_tables(thd, FALSE, tables, TRUE);
|
||||
|
||||
result= close_cached_tables(thd, tables, TRUE, FALSE, FALSE);
|
||||
|
||||
if (!have_lock)
|
||||
VOID(pthread_mutex_unlock(&LOCK_open));
|
||||
|
||||
|
||||
if (if_wait_for_refresh)
|
||||
{
|
||||
pthread_mutex_lock(&thd->mysys_var->mutex);
|
||||
@ -2204,7 +2223,7 @@ void wait_for_condition(THD *thd, pthread_mutex_t *mutex, pthread_cond_t *cond)
|
||||
current thread.
|
||||
|
||||
@param thd current thread context
|
||||
@param tables able list containing one table to open.
|
||||
@param tables table list containing one table to open.
|
||||
|
||||
@return FALSE on success, TRUE otherwise.
|
||||
*/
|
||||
@ -3272,8 +3291,8 @@ static bool reattach_merge(THD *thd, TABLE **err_tables_p)
|
||||
|
||||
@param thd Thread context
|
||||
@param get_locks Should we get locks after reopening tables ?
|
||||
@param in_refresh Are we in FLUSH TABLES ? TODO: It seems that
|
||||
we can remove this parameter.
|
||||
@param mark_share_as_old Mark share as old to protect from a impending
|
||||
global read lock.
|
||||
|
||||
@note Since this function can't properly handle prelocking and
|
||||
create placeholders it should be used in very special
|
||||
@ -3287,13 +3306,17 @@ static bool reattach_merge(THD *thd, TABLE **err_tables_p)
|
||||
@return FALSE in case of success, TRUE - otherwise.
|
||||
*/
|
||||
|
||||
bool reopen_tables(THD *thd,bool get_locks,bool in_refresh)
|
||||
bool reopen_tables(THD *thd, bool get_locks, bool mark_share_as_old)
|
||||
{
|
||||
TABLE *table,*next,**prev;
|
||||
TABLE **tables,**tables_ptr; // For locks
|
||||
TABLE *err_tables= NULL;
|
||||
bool error=0, not_used;
|
||||
bool merge_table_found= FALSE;
|
||||
const uint flags= MYSQL_LOCK_NOTIFY_IF_NEED_REOPEN |
|
||||
MYSQL_LOCK_IGNORE_GLOBAL_READ_LOCK |
|
||||
MYSQL_LOCK_IGNORE_FLUSH;
|
||||
|
||||
DBUG_ENTER("reopen_tables");
|
||||
|
||||
if (!thd->open_tables)
|
||||
@ -3354,7 +3377,7 @@ bool reopen_tables(THD *thd,bool get_locks,bool in_refresh)
|
||||
/* Do not handle locks of MERGE children. */
|
||||
if (get_locks && !db_stat && !table->parent)
|
||||
*tables_ptr++= table; // need new lock on this
|
||||
if (in_refresh)
|
||||
if (mark_share_as_old)
|
||||
{
|
||||
table->s->version=0;
|
||||
table->open_placeholder= 0;
|
||||
@ -3387,7 +3410,7 @@ bool reopen_tables(THD *thd,bool get_locks,bool in_refresh)
|
||||
*/
|
||||
thd->some_tables_deleted=0;
|
||||
if ((lock= mysql_lock_tables(thd, tables, (uint) (tables_ptr - tables),
|
||||
MYSQL_LOCK_NOTIFY_IF_NEED_REOPEN, ¬_used)))
|
||||
flags, ¬_used)))
|
||||
{
|
||||
thd->locked_tables=mysql_lock_merge(thd->locked_tables,lock);
|
||||
}
|
||||
|
Reference in New Issue
Block a user