diff options
author | Konstantin Osipov <kostja@sun.com> | 2009-12-08 11:38:45 +0300 |
---|---|---|
committer | Konstantin Osipov <kostja@sun.com> | 2009-12-08 11:38:45 +0300 |
commit | 478e09609c0922c7838c2ae1e25f4a7b2aaa5970 (patch) | |
tree | 33f973047e9a404d5d9b84e63354dddaaea2dd9c /sql | |
parent | 226d3e4f1e18ddd3e065811a7ec1a0daaaf3c2e8 (diff) | |
download | mariadb-git-478e09609c0922c7838c2ae1e25f4a7b2aaa5970.tar.gz |
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().
mysql-test/r/lock.result:
Update results: test case for Bug#45035.
mysql-test/t/lock.test:
Add a test case for Bug#45035.
sql/sql_base.cc:
Take all thr_lock locks at once in Locked_tables_list::reopen_tables().
sql/sql_class.h:
Add a helper array to store tables for mysql_lock_tables()
in reopen_tables().
sql/sql_table.cc:
Update unlink_all_closed_tables() to the new signature.
Diffstat (limited to 'sql')
-rw-r--r-- | sql/sql_base.cc | 103 | ||||
-rw-r--r-- | sql/sql_class.h | 19 | ||||
-rw-r--r-- | sql/sql_table.cc | 4 |
3 files changed, 101 insertions, 25 deletions
diff --git a/sql/sql_base.cc b/sql/sql_base.cc index 4f8c6be8e7b..02871c118ca 100644 --- a/sql/sql_base.cc +++ b/sql/sql_base.cc @@ -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, <_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; } diff --git a/sql/sql_class.h b/sql/sql_class.h index 92b9f9f4611..4b6564fb9da 100644 --- a/sql/sql_class.h +++ b/sql/sql_class.h @@ -1192,7 +1192,7 @@ private: Therefore, we can't allocate metadata locks on execution memory root -- as well as tables, the locks need to stay around till UNLOCK TABLES is called. - The locks are allocated in the memory root encapsulate in this + The locks are allocated in the memory root encapsulated in this class. Some SQL commands, like FLUSH TABLE or ALTER TABLE, demand that @@ -1211,10 +1211,21 @@ private: MEM_ROOT m_locked_tables_root; TABLE_LIST *m_locked_tables; TABLE_LIST **m_locked_tables_last; + /** An auxiliary array used only in reopen_tables(). */ + TABLE **m_reopen_array; + /** + Count the number of tables in m_locked_tables list. We can't + rely on thd->lock->table_count because it excludes + non-transactional temporary tables. We need to know + an exact number of TABLE objects. + */ + size_t m_locked_tables_count; public: Locked_tables_list() :m_locked_tables(NULL), - m_locked_tables_last(&m_locked_tables) + m_locked_tables_last(&m_locked_tables), + m_reopen_array(NULL), + m_locked_tables_count(0) { init_sql_alloc(&m_locked_tables_root, MEM_ROOT_BLOCK_SIZE, 0); } @@ -1228,7 +1239,9 @@ public: MEM_ROOT *locked_tables_root() { return &m_locked_tables_root; } void unlink_from_list(THD *thd, TABLE_LIST *table_list, bool remove_from_locked_tables); - void unlink_all_closed_tables(); + void unlink_all_closed_tables(THD *thd, + MYSQL_LOCK *lock, + size_t reopen_count); bool reopen_tables(THD *thd); }; diff --git a/sql/sql_table.cc b/sql/sql_table.cc index 8fdaa2cd93a..527095f2c88 100644 --- a/sql/sql_table.cc +++ b/sql/sql_table.cc @@ -4515,7 +4515,7 @@ static int prepare_for_repair(THD *thd, TABLE_LIST *table_list, } end: - thd->locked_tables_list.unlink_all_closed_tables(); + thd->locked_tables_list.unlink_all_closed_tables(thd, NULL, 0); if (table == &tmp_table) { pthread_mutex_lock(&LOCK_open); @@ -7597,7 +7597,7 @@ err_with_mdl: remove all references to the altered table from the list of locked tables and release the exclusive metadata lock. */ - thd->locked_tables_list.unlink_all_closed_tables(); + thd->locked_tables_list.unlink_all_closed_tables(thd, NULL, 0); if (target_mdl_request) { thd->mdl_context.release_lock(target_mdl_request->ticket); |