1
0
mirror of https://github.com/MariaDB/server.git synced 2025-08-01 03:47:19 +03:00

Backport of:

----------------------------------------------------------
revno: 2617.69.2
committer: Konstantin Osipov <kostja@sun.com>
branch nick: 5.4-azalea-bugfixing
timestamp: Mon 2009-08-03 19:26:04 +0400
message:
  A fix and a test case for Bug#45035 "Altering table under LOCK TABLES
  results in "Error 1213 Deadlock found...".

  If a user had a table locked with LOCK TABLES
  for READ and for WRITE in the same connection, ALTER TABLE
  could fail.

  Root cause analysis:

  If a connection issues

  LOCK TABLE t1 write, t1 a read, t1 b read;

  the new LOCK TABLES code in 6.0 (part of WL 3726) will create
  the following list of TABLE_LIST objects
  (thd->locked_tables_list->m_locked_tables):

  {"t1" "b" tl_read_no_insert}, {"t1" "a" tl_read_no_insert},
  {"t1" "t1" tl_write }

  Later on, when we try to ALTER table t1, mysql_alter_table()
  closes all TABLE instances and releases its thr_lock locks,
  keeping only an exclusive metadata lock on t1.

  But when ALTER is finished, Locked_table_list::reopen_tables()
  tries to restore the original list of open and locked tables.

  Before this patch, it used to do so one by one:
  Open t1 b, get TL_READ_NO_INSERT lock,
  Open t1 a, get TL_READ_NO_INSERT lock
  Open t1, try to get TL_WRITE lock, deadlock.

  The cause of the deadlock is that thr_lock.c doesn't
  resolve the situation when the read list only consists
  of locks taken by the same thread, followed by this very
  thread trying to take a WRITE lock. Indeed, since
  thr_lock_multi always gets a sorted list of locks,
  WRITE locks always precede READ locks in the list
  to lock.

  Don't try to fix thr_lock.c deficiency, keep this
  code simple.
  Instead, try to take all thr_lock locks at once
  in ::reopen_tables().
This commit is contained in:
Konstantin Osipov
2009-12-08 11:38:45 +03:00
parent b4677ef084
commit 4a8a1c568d
5 changed files with 178 additions and 25 deletions

View File

@ -2980,8 +2980,11 @@ Locked_tables_list::init_locked_tables(THD *thd)
{
DBUG_ASSERT(thd->locked_tables_mode == LTM_NONE);
DBUG_ASSERT(m_locked_tables == NULL);
DBUG_ASSERT(m_reopen_array == NULL);
DBUG_ASSERT(m_locked_tables_count == 0);
for (TABLE *table= thd->open_tables; table; table= table->next)
for (TABLE *table= thd->open_tables; table;
table= table->next, m_locked_tables_count++)
{
TABLE_LIST *src_table_list= table->pos_in_table_list;
char *db, *table_name, *alias;
@ -3021,7 +3024,24 @@ Locked_tables_list::init_locked_tables(THD *thd)
m_locked_tables_last= &dst_table_list->next_global;
table->pos_in_locked_tables= dst_table_list;
}
if (m_locked_tables_count)
{
/**
Allocate an auxiliary array to pass to mysql_lock_tables()
in reopen_tables(). reopen_tables() is a critical
path and we don't want to complicate it with extra allocations.
*/
m_reopen_array= (TABLE**)alloc_root(&m_locked_tables_root,
sizeof(TABLE*) *
(m_locked_tables_count+1));
if (m_reopen_array == NULL)
{
unlock_locked_tables(0);
return TRUE;
}
}
thd->locked_tables_mode= LTM_LOCK_TABLES;
return FALSE;
}
@ -3070,6 +3090,8 @@ Locked_tables_list::unlock_locked_tables(THD *thd)
free_root(&m_locked_tables_root, MYF(0));
m_locked_tables= NULL;
m_locked_tables_last= &m_locked_tables;
m_reopen_array= NULL;
m_locked_tables_count= 0;
}
@ -3141,8 +3163,39 @@ void Locked_tables_list::unlink_from_list(THD *thd,
@note This function is a no-op if we're not under LOCK TABLES.
*/
void Locked_tables_list::unlink_all_closed_tables()
void Locked_tables_list::
unlink_all_closed_tables(THD *thd, MYSQL_LOCK *lock, size_t reopen_count)
{
/* If we managed to take a lock, unlock tables and free the lock. */
if (lock)
mysql_unlock_tables(thd, lock);
/*
If a failure happened in reopen_tables(), we may have succeeded
reopening some tables, but not all.
This works when the connection was killed in mysql_lock_tables().
*/
if (reopen_count)
{
pthread_mutex_lock(&LOCK_open);
while (reopen_count--)
{
/*
When closing the table, we must remove it
from thd->open_tables list.
We rely on the fact that open_table() that was used
in reopen_tables() always links the opened table
to the beginning of the open_tables list.
*/
DBUG_ASSERT(thd->open_tables == m_reopen_array[reopen_count]);
thd->open_tables->pos_in_locked_tables->table= NULL;
close_thread_table(thd, &thd->open_tables);
}
broadcast_refresh();
pthread_mutex_unlock(&LOCK_open);
}
/* Exclude all closed tables from the LOCK TABLES list. */
for (TABLE_LIST *table_list= m_locked_tables; table_list; table_list=
table_list->next_global)
{
@ -3176,12 +3229,13 @@ Locked_tables_list::reopen_tables(THD *thd)
{
enum enum_open_table_action ot_action_unused;
bool lt_refresh_unused;
size_t reopen_count= 0;
MYSQL_LOCK *lock;
MYSQL_LOCK *merged_lock;
for (TABLE_LIST *table_list= m_locked_tables;
table_list; table_list= table_list->next_global)
{
MYSQL_LOCK *lock;
if (table_list->table) /* The table was not closed */
continue;
@ -3189,33 +3243,42 @@ Locked_tables_list::reopen_tables(THD *thd)
if (open_table(thd, table_list, thd->mem_root, &ot_action_unused,
MYSQL_OPEN_REOPEN))
{
unlink_all_closed_tables();
unlink_all_closed_tables(thd, 0, reopen_count);
return TRUE;
}
table_list->table->pos_in_locked_tables= table_list;
/* See also the comment on lock type in init_locked_tables(). */
table_list->table->reginfo.lock_type= table_list->lock_type;
DBUG_ASSERT(reopen_count < m_locked_tables_count);
m_reopen_array[reopen_count++]= table_list->table;
}
if (reopen_count)
{
thd->in_lock_tables= 1;
lock= mysql_lock_tables(thd, &table_list->table, 1,
/*
We re-lock all tables with mysql_lock_tables() at once rather
than locking one table at a time because of the case
reported in Bug#45035: when the same table is present
in the list many times, thr_lock.c fails to grant READ lock
on a table that is already locked by WRITE lock, even if
WRITE lock is taken by the same thread. If READ and WRITE
lock are passed to thr_lock.c in the same list, everything
works fine. Patching legacy code of thr_lock.c is risking to
break something else.
*/
lock= mysql_lock_tables(thd, m_reopen_array, reopen_count,
MYSQL_OPEN_REOPEN, &lt_refresh_unused);
thd->in_lock_tables= 0;
if (lock)
lock= mysql_lock_merge(thd->lock, lock);
if (lock == NULL)
if (lock == NULL || (merged_lock=
mysql_lock_merge(thd->lock, lock)) == NULL)
{
/*
No one's seen this branch work. Recover and report an
error just in case.
*/
pthread_mutex_lock(&LOCK_open);
close_thread_table(thd, &thd->open_tables);
pthread_mutex_unlock(&LOCK_open);
table_list->table= 0;
unlink_all_closed_tables();
my_error(ER_LOCK_DEADLOCK, MYF(0));
unlink_all_closed_tables(thd, lock, reopen_count);
if (! thd->killed)
my_error(ER_LOCK_DEADLOCK, MYF(0));
return TRUE;
}
thd->lock= lock;
thd->lock= merged_lock;
}
return FALSE;
}