diff options
author | Konstantin Osipov <kostja@sun.com> | 2009-11-30 18:55:03 +0300 |
---|---|---|
committer | Konstantin Osipov <kostja@sun.com> | 2009-11-30 18:55:03 +0300 |
commit | eff3780dd8f0e31d06b94f69ae18d2c55240100b (patch) | |
tree | 7bd54aeb6a84931b5374760af2239149d840a0c1 | |
parent | be0add42f53b23d8a5e279cb3041a3fc93e375a0 (diff) | |
download | mariadb-git-eff3780dd8f0e31d06b94f69ae18d2c55240100b.tar.gz |
Initial import of WL#3726 "DDL locking for all metadata objects".
Backport of:
------------------------------------------------------------
revno: 2630.4.1
committer: Dmitry Lenev <dlenev@mysql.com>
branch nick: mysql-6.0-3726-w
timestamp: Fri 2008-05-23 17:54:03 +0400
message:
WL#3726 "DDL locking for all metadata objects".
After review fixes in progress.
------------------------------------------------------------
This is the first patch in series. It transforms the metadata
locking subsystem to use a dedicated module (mdl.h,cc). No
significant changes in the locking protocol.
The import passes the test suite with the exception of
deprecated/removed 6.0 features, and MERGE tables. The latter
are subject to a fix by WL#4144.
Unfortunately, the original changeset comments got lost in a merge,
thus this import has its own (largely insufficient) comments.
This patch fixes Bug#25144 "replication / binlog with view breaks".
Warning: this patch introduces an incompatible change:
Under LOCK TABLES, it's no longer possible to FLUSH a table that
was not locked for WRITE.
Under LOCK TABLES, it's no longer possible to DROP a table or
VIEW that was not locked for WRITE.
******
Backport of:
------------------------------------------------------------
revno: 2630.4.2
committer: Dmitry Lenev <dlenev@mysql.com>
branch nick: mysql-6.0-3726-w
timestamp: Sat 2008-05-24 14:03:45 +0400
message:
WL#3726 "DDL locking for all metadata objects".
After review fixes in progress.
******
Backport of:
------------------------------------------------------------
revno: 2630.4.3
committer: Dmitry Lenev <dlenev@mysql.com>
branch nick: mysql-6.0-3726-w
timestamp: Sat 2008-05-24 14:08:51 +0400
message:
WL#3726 "DDL locking for all metadata objects"
Fixed failing Windows builds by adding mdl.cc to the lists
of files needed to build server/libmysqld on Windows.
******
Backport of:
------------------------------------------------------------
revno: 2630.4.4
committer: Dmitry Lenev <dlenev@mysql.com>
branch nick: mysql-6.0-3726-w
timestamp: Sat 2008-05-24 21:57:58 +0400
message:
WL#3726 "DDL locking for all metadata objects".
Fix for assert failures in kill.test which occured when one
tried to kill ALTER TABLE statement on merge table while it
was waiting in wait_while_table_is_used() for other connections
to close this table.
These assert failures stemmed from the fact that cleanup code
in this case assumed that temporary table representing new
version of table was open with adding to THD::temporary_tables
list while code which were opening this temporary table wasn't
always fulfilling this.
This patch changes code that opens new version of table to
always do this linking in. It also streamlines cleanup process
for cases when error occurs while we have new version of table
open.
******
WL#3726 "DDL locking for all metadata objects"
Add libmysqld/mdl.cc to .bzrignore.
******
Backport of:
------------------------------------------------------------
revno: 2630.4.6
committer: Dmitry Lenev <dlenev@mysql.com>
branch nick: mysql-6.0-3726-w
timestamp: Sun 2008-05-25 00:33:22 +0400
message:
WL#3726 "DDL locking for all metadata objects".
Addition to the fix of assert failures in kill.test caused by
changes for this worklog.
Make sure we close the new table only once.
.bzrignore:
Add libmysqld/mdl.cc
libmysqld/CMakeLists.txt:
Added mdl.cc to the list of files needed for building of libmysqld.
libmysqld/Makefile.am:
Added files implementing new meta-data locking subsystem to the server.
mysql-test/include/handler.inc:
Use separate connection for waiting while threads performing DDL
operations conflicting with open HANDLER tables reach blocked
state. This is required because now we check and close tables open
by HANDLER statements in this connection conflicting with DDL in
another each time open_tables() is called and thus select from I_S
which is used for waiting will unblock DDL operations if issued
from connection with open HANDLERs.
mysql-test/r/create.result:
Adjusted test case after change in implementation of CREATE TABLE
... SELECT. We no longer have special check in open_table() which
catches the case when we select from the table created. Instead we
rely on unique_table() call which happens after opening and
locking all tables.
mysql-test/r/flush.result:
FLUSH TABLES WITH READ LOCK can no longer happen under LOCK
TABLES. Updated test accordingly.
mysql-test/r/flush_table.result:
Under LOCK TABLES we no longer allow to do FLUSH TABLES for tables
locked for read. Updated test accordingly.
mysql-test/r/handler_innodb.result:
Use separate connection for waiting while threads performing DDL
operations conflicting with open HANDLER tables reach blocked
state. This is required because now we check and close tables open
by HANDLER statements in this connection conflicting with DDL in
another each time open_tables() is called and thus select from I_S
which is used for waiting will unblock DDL operations if issued
from connection with open HANDLERs.
mysql-test/r/handler_myisam.result:
Use separate connection for waiting while threads performing DDL
operations conflicting with open HANDLER tables reach blocked
state. This is required because now we check and close tables open
by HANDLER statements in this connection conflicting with DDL in
another each time open_tables() is called and thus select from I_S
which is used for waiting will unblock DDL operations if issued
from connection with open HANDLERs.
mysql-test/r/information_schema.result:
Additional test for WL#3726 "DDL locking for all metadata
objects". Check that we use high-priority metadata lock requests
when filling I_S tables.
Rearrange tests to match 6.0 better (fewer merge conflicts).
mysql-test/r/kill.result:
Added tests checking that DDL and DML statements waiting for
metadata locks can be interrupted by KILL command.
mysql-test/r/lock.result:
One no longer is allowed to do DROP VIEW under LOCK TABLES even if
this view is locked by LOCK TABLES. The problem is that in such
situation write locks on view are not mutually exclusive so
upgrading metadata lock which is required for dropping of view
will lead to deadlock.
mysql-test/r/partition_column_prune.result:
Update results (same results in 6.0), WL#3726
mysql-test/r/partition_pruning.result:
Update results (same results in 6.0), WL#3726
mysql-test/r/ps_ddl.result:
We no longer invalidate prepared CREATE TABLE ... SELECT statement
if target table changes. This is OK since it is not strictly
necessary.
The first change is wrong, is caused by FLUSH TABLE
now flushing all unused tables. This is a regression that
Dmitri fixed in 6.0 in a follow up patch.
mysql-test/r/sp.result:
Under LOCK TABLES we no longer allow accessing views which were
not explicitly locked. To access view we need to obtain metadata
lock on it and doing this under LOCK TABLES may lead to deadlocks.
mysql-test/r/view.result:
One no longer is allowed to do DROP VIEW under LOCK TABLES even if
this view is locked by LOCK TABLES. The problem is that in such
situation even "write locks" on view are not mutually exclusive so
upgrading metadata lock which is required for dropping of view
will lead to deadlock
mysql-test/r/view_grant.result:
ALTER VIEW implementation was changed to open a view only after
checking that user which does alter has appropriate privileges on
it. This means that in case when user's privileges are
insufficient for this we won't check that new view definer is the
same as original one or user performing alter has SUPER privilege.
Adjusted test case accordingly.
mysql-test/r/view_multi.result:
Added test case for bug#25144 "replication / binlog with view
breaks".
mysql-test/suite/rpl/t/disabled.def:
Disable test for deprecated features (they don't work with new MDL).
mysql-test/t/create.test:
Adjusted test case after change in implementation of CREATE TABLE
... SELECT. We no longer have special check in open_table() which
catches the case when we select from the table created. Instead we
rely on unique_table() call which happens after opening and
locking all tables.
mysql-test/t/disabled.def:
Disable merge.test, subject of WL#4144
mysql-test/t/flush.test:
FLUSH TABLES WITH READ LOCK can no longer happen under LOCK
TABLES. Updated test accordingly.
mysql-test/t/flush_table.test:
Under LOCK TABLES we no longer allow to do FLUSH TABLES for tables
locked for read. Updated test accordingly.
mysql-test/t/information_schema.test:
Additional test for WL#3726 "DDL locking for all metadata
objects". Check that we use high-priority metadata lock requests
when filling I_S tables.
Rearrange the results for easier merges with 6.0.
mysql-test/t/kill.test:
Added tests checking that DDL and DML statements waiting for
metadata locks can be interrupted by KILL command.
mysql-test/t/lock.test:
One no longer is allowed to do DROP VIEW under LOCK TABLES even if
this view is locked by LOCK TABLES. The problem is that in such
situation write locks on view are not mutually exclusive so
upgrading metadata lock which is required for dropping of view
will lead to deadlock.
mysql-test/t/lock_multi.test:
Adjusted test case to the changes of status in various places
caused by change in implementation FLUSH TABLES WITH READ LOCK,
which is now takes global metadata lock before flushing tables and
therefore waits on at these places.
mysql-test/t/ps_ddl.test:
We no longer invalidate prepared CREATE TABLE ... SELECT statement
if target table changes. This is OK since it is not strictly
necessary.
The first change is wrong, is caused by FLUSH TABLE
now flushing all unused tables. This is a regression that
Dmitri fixed in 6.0 in a follow up patch.
mysql-test/t/sp.test:
Under LOCK TABLES we no longer allow accessing views which were
not explicitly locked. To access view we need to obtain metadata
lock on it and doing this under LOCK TABLES may lead to deadlocks.
mysql-test/t/trigger_notembedded.test:
Adjusted test case to the changes of status in various places
caused by change in implementation FLUSH TABLES WITH READ LOCK,
which is now takes global metadata lock before flushing tables and
therefore waits on at these places.
mysql-test/t/view.test:
One no longer is allowed to do DROP VIEW under LOCK TABLES even if
this view is locked by LOCK TABLES. The problem is that in such
situation even "write locks" on view are not mutually exclusive so
upgrading metadata lock which is required for dropping of view
will lead to deadlock.
mysql-test/t/view_grant.test:
ALTER VIEW implementation was changed to open a view only after
checking that user which does alter has appropriate privileges on
it. This means that in case when user's privileges are
insufficient for this we won't check that new view definer is the
same as original one or user performing alter has SUPER privilege.
Adjusted test case accordingly.
mysql-test/t/view_multi.test:
Added test case for bug#25144 "replication / binlog with view
breaks".
sql/CMakeLists.txt:
Added mdl.cc to the list of files needed for building of server.
sql/Makefile.am:
Added files implementing new meta-data locking subsystem to the
server.
sql/event_db_repository.cc:
Allocate metadata lock requests objects (MDL_LOCK) on execution
memory root in cases when TABLE_LIST objects is also allocated
there or on stack.
sql/ha_ndbcluster.cc:
Adjusted code to work nicely with new metadata locking subsystem.
close_cached_tables() no longer has wait_for_placeholder argument.
Instead of relying on this parameter and related behavior FLUSH
TABLES WITH READ LOCK now takes global shared metadata lock.
sql/ha_ndbcluster_binlog.cc:
Adjusted code to work with new metadata locking subsystem.
close_cached_tables() no longer has wait_for_placeholder argument.
Instead of relying on this parameter and related behavior FLUSH
TABLES WITH READ LOCK now takes global shared metadata lock.
sql/handler.cc:
update_frm_version():
Directly update TABLE_SHARE::mysql_version member instead of
going through all TABLE instances for this table (old code was a
legacy from pre-table-definition-cache days).
sql/lock.cc:
Use new metadata locking subsystem. Threw away most of functions
related to name locking as now one is supposed to use metadata
locking API instead. In lock_global_read_lock() and
unlock_global_read_lock() in order to avoid problems with global
read lock sneaking in at the moment when we perform FLUSH TABLES
or ALTER TABLE under LOCK TABLES and when tables being reopened
are protected only by metadata locks we also have to take global
shared meta data lock.
sql/log_event.cc:
Adjusted code to work with new metadata locking subsystem. For
tables open by slave thread for applying RBR events allocate
memory for lock request object in the same chunk of memory as
TABLE_LIST objects for them. In order to ensure that we keep these
objects around until tables are open always close tables before
calling Relay_log_info::clear_tables_to_lock(). Use new auxiliary
Relay_log_info::slave_close_thread_tables() method to enforce
this.
sql/log_event_old.cc:
Adjusted code to work with new metadata locking subsystem. Since
for tables open by slave thread for applying RBR events memory for
lock request object is allocated in the same chunk of memory as
TABLE_LIST objects for them we have to ensure that we keep these
objects around until tables are open. To ensure this we always
close tables before calling
Relay_log_info::clear_tables_to_lock(). To enfore this we use
new auxiliary Relay_log_info::slave_close_thread_tables()
method.
sql/mdl.cc:
Implemented new metadata locking subsystem and API described in
WL3726 "DDL locking for all metadata objects".
sql/mdl.h:
Implemented new metadata locking subsystem and API described in
WL3726 "DDL locking for all metadata objects".
sql/mysql_priv.h:
- close_thread_tables()/close_tables_for_reopen() now has one more
argument which indicates that metadata locks should be released
but not removed from the context in order to be used later in
mdl_wait_for_locks() and tdc_wait_for_old_version().
- close_cached_table() routine is no longer public.
- Thread waiting in wait_while_table_is_used() can be now killed
so this function returns boolean to make caller aware of such
situation.
- We no longer have table cache as separate entity instead used
and unused TABLE instances are linked to TABLE_SHARE objects in
table definition cache.
- Now third argument of open_table() is also used for requesting
table repair or auto-discovery of table's new definition. So its
type was changed from bool to enum.
- Added tdc_open_view() function for opening view by getting its
definition from disk (and table cache in future).
- reopen_name_locked_table() no longer needs "link_in" argument as
now we have exclusive metadata locks instead of dummy TABLE
instances when this function is called.
- find_locked_table() now takes head of list of TABLE instances
instead of always scanning through THD::open_tables list. Also
added find_write_locked_table() auxiliary.
- reopen_tables(), close_cached_tables() no longer have
mark_share_as_old and wait_for_placeholder arguments. Instead of
relying on this parameters and related behavior FLUSH TABLES
WITH READ LOCK now takes global shared metadata lock.
- We no longer need drop_locked_tables() and
abort_locked_tables().
- mysql_ha_rm_tables() now always assume that LOCK_open is not
acquired by caller.
- Added notify_thread_having_shared_lock() callback invoked by
metadata locking subsystem when acquiring an exclusive lock, for
each thread that has a conflicting shared metadata lock.
- Introduced expel_table_from_cache() as replacement for
remove_table_from_cache() (the main difference is that this new
function assumes that caller follows metadata locking protocol
and never waits).
- Threw away most of functions related to name locking. One should
use new metadata locking subsystem and API instead.
sql/mysqld.cc:
Got rid of call initializing/deinitializing table cache since now
it is embedded into table definition cache. Added calls for
initializing/ deinitializing metadata locking subsystem.
sql/rpl_rli.cc:
Introduced auxiliary Relay_log_info::slave_close_thread_tables()
method which is used for enforcing that we always close tables
open for RBR before deallocating TABLE_LIST elements and MDL_LOCK
objects for them.
sql/rpl_rli.h:
Introduced auxiliary Relay_log_info::slave_close_thread_tables()
method which is used for enforcing that we always close tables
open for RBR before deallocating TABLE_LIST elements and MDL_LOCK
objects for them.
sql/set_var.cc:
close_cached_tables() no longer has wait_for_placeholder argument.
Instead of relying on this parameter and related behavior FLUSH
TABLES WITH READ LOCK now takes global shared metadata lock.
sql/sp_head.cc:
For tables added to the statement's table list by prelocking
algorithm we allocate these objects either on the same memory as
corresponding table list elements or on THD::locked_tables_root
(if we are building table list for LOCK TABLES).
sql/sql_acl.cc:
Allocate metadata lock requests objects (MDL_LOCK) on execution
memory root in cases when we use stack TABLE_LIST objects to open
tables. Got rid of redundant code by using unlock_locked_tables()
function.
sql/sql_base.cc:
Changed code to use new MDL subsystem. Got rid of separate table
cache. Now used and unused TABLE instances are linked to the
TABLE_SHAREs in table definition cache.
check_unused():
Adjusted code to the fact that we no longer have separate table
cache. Removed dead code.
table_def_free():
Free TABLE instances referenced from TABLE_SHARE objects before
destroying table definition cache.
get_table_share():
Added assert which ensures that noone will be able to access
table (and its share) without acquiring some kind of metadata
lock first.
close_handle_and_leave_table_as_lock():
Adjusted code to the fact that TABLE instances now are linked to
list in TABLE_SHARE.
list_open_tables():
Changed this function to use table definition cache instead of
table cache.
free_cache_entry():
Unlink freed TABLE elements from the list of all TABLE instances
for the table in TABLE_SHARE.
kill_delayed_thread_for_table():
Added auxiliary for killing delayed insert threads for
particular table.
close_cached_tables():
Got rid of wait_for_refresh argument as we now rely on global
shared metadata lock to prevent FLUSH WITH READ LOCK sneaking in
when we are reopening tables. Heavily reworked this function to
use new MDL code and not to rely on separate table cache entity.
close_open_tables():
We no longer have separate table cache.
close_thread_tables():
Release metadata locks after closing all tables. Added skip_mdl
argument which allows us not to remove metadata lock requests
from the context in case when we are going to use this requests
later in mdl_wait_for_locks() and tdc_wait_for_old_versions().
close_thread_table()/close_table_for_reopen():
Since we no longer have separate table cache and all TABLE
instances are linked to TABLE_SHARE objects in table definition
cache we have to link/unlink TABLE object to/from appropriate
lists in the share.
name_lock_locked_table():
Moved redundant code to find_write_locked_table() function and
adjusted code to the fact that wait_while_table_is_used() can
now return with an error if our thread is killed.
reopen_table_entry():
We no longer need "link_in" argument as with MDL we no longer
call this function with dummy TABLE object pre-allocated and
added to the THD::open_tables. Also now we add newly-open TABLE
instance to the list of share's used TABLE instances.
table_cache_insert_placeholder():
Got rid of name-locking legacy.
lock_table_name_if_not_cached():
Moved to sql_table.cc the only place where it is used. It was
also reimplemented using new MDL API.
open_table():
- Reworked this function to use new MDL subsystem.
- Changed code to deal with table definition cache directly
instead of going through separate table cache.
- Now third argument is also used for requesting table repair
or auto-discovery of table's new definition. So its type was
changed from bool to enum.
find_locked_table()/find_write_locked_table():
Accept head of list of TABLE objects as first argument and use
this list instead of always searching in THD::open_tables list.
Also added auxiliary for finding write-locked locked tables.
reopen_table():
Adjusted function to work with new MDL subsystem and to properly
manuipulate with lists of used/unused TABLE instaces in
TABLE_SHARE.
reopen_tables():
Removed mark_share_as_old parameter. Instead of relying on it
and related behavior FLUSH TABLES WITH READ LOCK now takes
global shared metadata lock. Changed code after removing
separate table cache.
drop_locked_tables()/abort_locked_tables():
Got rid of functions which are no longer needed.
unlock_locked_tables():
Moved this function from sql_parse.cc and changed it to release
memory which was used for allocating metadata lock requests for
tables open and locked by LOCK TABLES.
tdc_open_view():
Intoduced function for opening a view by getting its definition
from disk (and table cache in future).
reopen_table_entry():
Introduced function for opening table definitions while holding
exclusive metatadata lock on it.
open_unireg_entry():
Got rid of this function. Most of its functionality is relocated
to open_table() and open_table_fini() functions, and some of it
to reopen_table_entry() and tdc_open_view(). Also code
resposible for auto-repair and auto-discovery of tables was
moved to separate function.
open_table_entry_fini():
Introduced function which contains common actions which finalize
process of TABLE object creation.
auto_repair_table():
Moved code responsible for auto-repair of table being opened
here.
handle_failed_open_table_attempt()
Moved code responsible for handling failing attempt to open
table to one place (retry due to lock conflict/old version,
auto-discovery and repair).
open_tables():
- Flush open HANDLER tables if they have old version of if there
is conflicting metadata lock against them (before this moment
we had this code in open_table()).
- When we open view which should be processed via derived table
on the second execution of prepared statement or stored
routine we still should call open_table() for it in order to
obtain metadata lock on it and prepare its security context.
- In cases when we discover that some special handling of
failure to open table is needed call
handle_failed_open_table_attempt() which handles all such
scenarios.
open_ltable():
Handling of various special scenarios of failure to open a table
was moved to separate handle_failed_open_table_attempt()
function.
remove_db_from_cache():
Removed this function as it is no longer used.
notify_thread_having_shared_lock():
Added callback which is invoked by MDL subsystem when acquiring
an exclusive lock, for each thread that has a conflicting shared
metadata lock.
expel_table_from_cache():
Introduced function for removing unused TABLE instances. Unlike
remove_table_from_cache() it relies on caller following MDL
protocol and having appropriate locks when calling it and thus
does not do any waiting if table is still in use.
tdc_wait_for_old_version():
Added function which allows open_tables() to wait in cases when
we discover that we should back-off due to presence of old
version of table.
abort_and_upgrade_lock():
Use new MDL calls.
mysql_wait_completed_table():
Got rid of unused function.
open_system_tables_for_read/for_update()/performance_schema_table():
Allocate MDL_LOCK objects on execution memory root in cases when
TABLE_LIST objects for corresponding tables is allocated on
stack.
close_performance_schema_table():
Release metadata locks after closing tables.
******
Use I_P_List for free/used tables list in the table share.
sql/sql_binlog.cc:
Use Relay_log_info::slave_close_thread_tables() method to enforce
that we always close tables open for RBR before deallocating
TABLE_LIST elements and MDL_LOCK objects for them.
sql/sql_class.cc:
Added meta-data locking contexts as part of Open_tables_state
context. Also introduced THD::locked_tables_root memory root
which is to be used for allocating MDL_LOCK objects for tables in
LOCK TABLES statement (end of lifetime for such objects is UNLOCK
TABLES so we can't use statement or execution root for them).
sql/sql_class.h:
Added meta-data locking contexts as part of Open_tables_state
context. Also introduced THD::locked_tables_root memory root
which is to be used for allocating MDL_LOCK objects for tables in
LOCK TABLES statement (end of lifetime for such objects is UNLOCK
TABLES so we can't use statement or execution root for them).
Note: handler_mdl_context and locked_tables_root and
mdl_el_root will be removed by subsequent patches.
sql/sql_db.cc:
mysql_rm_db() does not really need to call remove_db_from_cache()
as it drops each table in the database using
mysql_rm_table_part2(), which performs all necessary operations on
table (definition) cache.
sql/sql_delete.cc:
Use the new metadata locking API for TRUNCATE.
sql/sql_handler.cc:
Changed HANDLER implementation to use new metadata locking
subsystem. Note that MDL_LOCK objects for HANDLER tables are
allocated in the same chunk of heap memory as TABLE_LIST object
for those tables.
sql/sql_insert.cc:
mysql_insert():
find_locked_table() now takes head of list of TABLE object as
its argument instead of always scanning through THD::open_tables
list.
handle_delayed_insert():
Allocate metadata lock request object for table open by delayed
insert thread on execution memroot. create_table_from_items():
We no longer allocate dummy TABLE objects for tables being
created if they don't exist. As consequence
reopen_name_locked_table() no longer has link_in argument.
open_table() now has one more argument which is not relevant for
temporary tables.
sql/sql_parse.cc:
- Moved unlock_locked_tables() routine to sql_base.cc and made
available it in other files. Got rid of some redundant code by
using this function.
- Replaced boolean TABLE_LIST::create member with enum
open_table_type member.
- Use special memory root for allocating MDL_LOCK objects for
tables open and locked by LOCK TABLES (these object should live
till UNLOCK TABLES so we can't allocate them on statement nor
execution memory root). Also properly set metadata lock
upgradability attribure for those tables.
- Under LOCK TABLES it is no longer allowed to flush tables which
are not write-locked as this breaks metadata locking protocol
and thus potentially might lead to deadlock.
- Added auxiliary adjust_mdl_locks_upgradability() function.
sql/sql_partition.cc:
Adjusted code to the fact that reopen_tables() no longer has
"mark_share_as_old" argument. Got rid of comments which are no
longer true.
sql/sql_plist.h:
Added I_P_List template class for parametrized intrusive doubly
linked lists and I_P_List_iterator for corresponding iterator.
Unlike for I_List<> list elements of such list can participate in
several lists. Unlike List<> such lists are doubly-linked and
intrusive.
sql/sql_plugin.cc:
Allocate metadata lock requests objects (MDL_LOCK) on execution
memory root in cases when we use stack TABLE_LIST objects to open
tables.
sql/sql_prepare.cc:
Replaced boolean TABLE_LIST::create member with enum
open_table_type member. This allows easily handle situation in
which instead of opening the table we want only to take exclusive
metadata lock on it.
sql/sql_rename.cc:
Use new metadata locking subsystem in implementation of RENAME
TABLE.
sql/sql_servers.cc:
Allocate metadata lock requests objects (MDL_LOCK) on execution
memory root in cases when we use stack TABLE_LIST objects to open
tables. Got rid of redundant code by using unlock_locked_tables()
function.
sql/sql_show.cc:
Acquire shared metadata lock when we are getting information for
I_S table directly from TABLE_SHARE without doing full-blown table
open. We use high priority lock request in this situation in
order to avoid deadlocks.
Also allocate metadata lock requests objects (MDL_LOCK) on
execution memory root in cases when TABLE_LIST objects are also
allocated there
sql/sql_table.cc:
mysql_rm_table():
Removed comment which is no longer relevant.
mysql_rm_table_part2():
Now caller of mysql_ha_rm_tables() should not own LOCK_open.
Adjusted code to use new metadata locking subsystem instead of
name-locks.
lock_table_name_if_not_cached():
Moved this function from sql_base.cc to this file and
reimplemented it using metadata locking API.
mysql_create_table():
Adjusted code to use new MDL API.
wait_while_table_is_used():
Changed function to use new MDL subsystem. Made thread waiting
in it killable (this also led to introduction of return value so
caller can distinguish successful executions from situations
when waiting was aborted).
close_cached_tables():
Thread waiting in this function is killable now. As result it
has return value for distinguishing between succes and failure.
Got rid of redundant boradcast_refresh() call.
prepare_for_repair():
Use MDL subsystem instead of name-locks.
mysql_admin_table():
mysql_ha_rm_tables() now always assumes that caller doesn't own
LOCK_open.
mysql_repair_table():
We should mark all elements of table list as requiring
upgradable metadata locks.
mysql_create_table_like():
Use new MDL subsystem instead of name-locks.
create_temporary_tables():
We don't need to obtain metadata locks when creating temporary
table.
mysql_fast_or_online_alter_table():
Thread waiting in wait_while_table_is_used() is now killable.
mysql_alter_table():
Adjusted code to work with new MDL subsystem and to the fact
that threads waiting in what_while_table_is_used() and
close_cached_table() are now killable.
sql/sql_test.cc:
We no longer have separate table cache. TABLE instances are now
associated with/linked to TABLE_SHARE objects in table definition
cache.
sql/sql_trigger.cc:
Adjusted code to work with new metadata locking subsystem. Also
reopen_tables() no longer has mark_share_as_old argument (Instead
of relying on this parameter and related behavior FLUSH TABLES
WITH READ LOCK now takes global shared metadata lock).
sql/sql_udf.cc:
Allocate metadata lock requests objects (MDL_LOCK) on execution
memory root in cases when we use stack TABLE_LIST objects to open
tables.
sql/sql_update.cc:
Adjusted code to work with new meta-data locking subsystem.
sql/sql_view.cc:
Added proper meta-data locking to implementations of
CREATE/ALTER/DROP VIEW statements. Now we obtain exclusive
meta-data lock on a view before creating/ changing/dropping it.
This ensures that all concurrent statements that use this view
will finish before our statement will proceed and therefore we
will get correct order of statements in the binary log.
Also ensure that TABLE_LIST::mdl_upgradable attribute is properly
propagated for underlying tables of view.
sql/table.cc:
Added auxiliary alloc_mdl_locks() function for allocating metadata
lock request objects for all elements of table list.
sql/table.h:
TABLE_SHARE:
Got rid of unused members. Introduced members for storing lists
of used and unused TABLE objects for this share.
TABLE:
Added members for linking TABLE objects into per-share lists of
used and unused TABLE instances. Added member for holding
pointer to metadata lock for this table.
TABLE_LIST:
Replaced boolean TABLE_LIST::create member with enum
open_table_type member. This allows easily handle situation in
which instead of opening the table we want only to take
exclusive meta-data lock on it (we need this in order to handle
ALTER VIEW and CREATE VIEW statements).
Introduced new mdl_upgradable member for marking elements of
table list for which we need to take upgradable shared metadata
lock instead of plain shared metadata lock. Added pointer for
holding pointer to MDL_LOCK for the table.
Added auxiliary alloc_mdl_locks() function for allocating metadata
lock requests objects for all elements of table list. Added
auxiliary set_all_mdl_upgradable() function for marking all
elements in table list as requiring upgradable metadata locks.
storage/myisammrg/ha_myisammrg.cc:
Allocate MDL_LOCK objects for underlying tables of MERGE table.
To be reworked once Ingo pushes his patch for WL4144.
77 files changed, 4786 insertions, 2117 deletions
diff --git a/.bzrignore b/.bzrignore index 351417a4353..bc3ca7b0c24 100644 --- a/.bzrignore +++ b/.bzrignore @@ -3070,3 +3070,4 @@ libmysqld/rpl_handler.cc libmysqld/debug_sync.cc libmysqld/rpl_handler.cc dbug/tests +libmysqld/mdl.cc diff --git a/libmysqld/CMakeLists.txt b/libmysqld/CMakeLists.txt index 65b8e12bc26..655082f0304 100644 --- a/libmysqld/CMakeLists.txt +++ b/libmysqld/CMakeLists.txt @@ -133,6 +133,7 @@ SET(LIBMYSQLD_SOURCES emb_qcache.cc libmysqld.c lib_sql.cc ../sql/partition_info.cc ../sql/sql_connect.cc ../sql/scheduler.cc ../sql/event_parse_data.cc ../sql/sql_signal.cc ../sql/rpl_handler.cc + ../sql/mdl.cc ${GEN_SOURCES} ${LIB_SOURCES}) diff --git a/libmysqld/Makefile.am b/libmysqld/Makefile.am index ec73741eaaf..3a7fa934778 100644 --- a/libmysqld/Makefile.am +++ b/libmysqld/Makefile.am @@ -79,7 +79,7 @@ sqlsources = derror.cc field.cc field_conv.cc strfunc.cc filesort.cc \ sql_tablespace.cc \ rpl_injector.cc my_user.c partition_info.cc \ sql_servers.cc event_parse_data.cc sql_signal.cc \ - rpl_handler.cc + rpl_handler.cc mdl.cc libmysqld_int_a_SOURCES= $(libmysqld_sources) nodist_libmysqld_int_a_SOURCES= $(libmysqlsources) $(sqlsources) diff --git a/mysql-test/include/handler.inc b/mysql-test/include/handler.inc index 6e7f53ba9b2..8ff38c7e7a1 100644 --- a/mysql-test/include/handler.inc +++ b/mysql-test/include/handler.inc @@ -518,12 +518,15 @@ connect (flush,localhost,root,,); connection flush; --echo connection: flush --send flush tables; -connection default; ---echo connection: default +connect (waiter,localhost,root,,); +connection waiter; +--echo connection: waiter let $wait_condition= select count(*) = 1 from information_schema.processlist where state = "Flushing tables"; --source include/wait_condition.inc +connection default; +--echo connection: default handler t2 open; handler t2 read first; handler t1 read next; @@ -550,12 +553,14 @@ connect (flush,localhost,root,,); connection flush; --echo connection: flush --send rename table t1 to t2; -connection default; ---echo connection: default +connection waiter; +--echo connection: waiter let $wait_condition= select count(*) = 1 from information_schema.processlist where state = "Waiting for table" and info = "rename table t1 to t2"; --source include/wait_condition.inc +connection default; +--echo connection: default handler t2 open; handler t2 read first; --error ER_NO_SUCH_TABLE @@ -566,7 +571,13 @@ connection flush; reap; connection default; drop table t2; +connection flush; disconnect flush; +--source include/wait_until_disconnected.inc +connection waiter; +disconnect waiter; +--source include/wait_until_disconnected.inc +connection default; # # Bug#30882 Dropping a temporary table inside a stored function may cause a server crash @@ -699,19 +710,24 @@ handler t1 read a next; # Bug#41112: crash in mysql_ha_close_table/get_lock_data with alter table # +connect(con1,localhost,root,,); +connect(con2,localhost,root,,); + +connection default; --disable_warnings drop table if exists t1; --enable_warnings create table t1 (a int); insert into t1 values (1); handler t1 open; -connect(con1,localhost,root,,); +connection con1; send alter table t1 engine=memory; -connection default; +connection con2; let $wait_condition= select count(*) = 1 from information_schema.processlist - where state = "rename result table" and info = "alter table t1 engine=memory"; + where state = "Waiting for table" and info = "alter table t1 engine=memory"; --source include/wait_condition.inc +connection default; --error ER_ILLEGAL_HA handler t1 read a next; handler t1 close; @@ -720,6 +736,9 @@ connection con1; drop table t1; disconnect con1; --source include/wait_until_disconnected.inc +connection con2; +disconnect con2; +--source include/wait_until_disconnected.inc connection default; # diff --git a/mysql-test/r/create.result b/mysql-test/r/create.result index 471cc6e9a3d..cf424b8b058 100644 --- a/mysql-test/r/create.result +++ b/mysql-test/r/create.result @@ -785,7 +785,7 @@ drop table t1; create table t1 select * from t2; ERROR 42S02: Table 'test.t2' doesn't exist create table t1 select * from t1; -ERROR HY000: You can't specify target table 't1' for update in FROM clause +ERROR 42S02: Table 'test.t1' doesn't exist create table t1 select coalesce('a' collate latin1_swedish_ci,'b' collate latin1_bin); ERROR HY000: Illegal mix of collations (latin1_swedish_ci,EXPLICIT) and (latin1_bin,EXPLICIT) for operation 'coalesce' create table t1 (primary key(a)) select "b" as b; @@ -805,6 +805,11 @@ Note 1050 Table 't1' already exists select * from t1; i 1 +create table if not exists t1 select * from t1; +ERROR HY000: You can't specify target table 't1' for update in FROM clause +select * from t1; +i +1 create table t1 select coalesce('a' collate latin1_swedish_ci,'b' collate latin1_bin); ERROR HY000: Illegal mix of collations (latin1_swedish_ci,EXPLICIT) and (latin1_bin,EXPLICIT) for operation 'coalesce' select * from t1; diff --git a/mysql-test/r/flush.result b/mysql-test/r/flush.result index b978304f59d..2be426d3a4a 100644 --- a/mysql-test/r/flush.result +++ b/mysql-test/r/flush.result @@ -33,6 +33,9 @@ flush tables with read lock; ERROR HY000: Can't execute the given command because you have active locked tables or an active transaction lock table t1 read; flush tables with read lock; +ERROR HY000: Can't execute the given command because you have active locked tables or an active transaction +unlock tables; +flush tables with read lock; lock table t1 write; ERROR HY000: Can't execute the query because you have a conflicting read lock lock table t1 read; @@ -46,6 +49,7 @@ flush tables with read lock; ERROR HY000: Can't execute the given command because you have active locked tables or an active transaction lock table t1 read, t2 read, t3 read; flush tables with read lock; +ERROR HY000: Can't execute the given command because you have active locked tables or an active transaction unlock tables; drop table t1, t2, t3; create table t1 (c1 int); @@ -69,6 +73,7 @@ ERROR HY000: Can't execute the given command because you have active locked tabl unlock tables; lock tables t1 read; flush tables with read lock; +ERROR HY000: Can't execute the given command because you have active locked tables or an active transaction unlock tables; drop table t1, t2; set session low_priority_updates=default; diff --git a/mysql-test/r/flush_table.result b/mysql-test/r/flush_table.result index 8821bade6b4..2b0ee1cb205 100644 --- a/mysql-test/r/flush_table.result +++ b/mysql-test/r/flush_table.result @@ -3,21 +3,14 @@ create table t1 (a int not null auto_increment primary key); insert into t1 values(0); lock table t1 read; flush table t1; +ERROR HY000: Table 't1' was locked with a READ lock and can't be updated +unlock tables; +lock table t1 write; +flush table t1; check table t1; Table Op Msg_type Msg_text test.t1 check status OK unlock tables; -lock table t1 read; -lock table t1 read; -flush table t1; -select * from t1; -a -1 -unlock tables; -select * from t1; -a -1 -unlock tables; lock table t1 write; lock table t1 read; flush table t1; @@ -26,7 +19,7 @@ a 1 unlock tables; unlock tables; -lock table t1 read; +lock table t1 write; lock table t1 write; flush table t1; select * from t1; diff --git a/mysql-test/r/handler_innodb.result b/mysql-test/r/handler_innodb.result index 957fc30acef..5990b19062b 100644 --- a/mysql-test/r/handler_innodb.result +++ b/mysql-test/r/handler_innodb.result @@ -548,6 +548,7 @@ c1 1 connection: flush flush tables;; +connection: waiter connection: default handler t2 open; handler t2 read first; @@ -567,6 +568,7 @@ handler t1 read first; c1 connection: flush rename table t1 to t2;; +connection: waiter connection: default handler t2 open; handler t2 read first; diff --git a/mysql-test/r/handler_myisam.result b/mysql-test/r/handler_myisam.result index 90a1bdfe6be..f7b0ff2e04e 100644 --- a/mysql-test/r/handler_myisam.result +++ b/mysql-test/r/handler_myisam.result @@ -547,6 +547,7 @@ c1 1 connection: flush flush tables;; +connection: waiter connection: default handler t2 open; handler t2 read first; @@ -566,6 +567,7 @@ handler t1 read first; c1 connection: flush rename table t1 to t2;; +connection: waiter connection: default handler t2 open; handler t2 read first; diff --git a/mysql-test/r/information_schema.result b/mysql-test/r/information_schema.result index 04234eb3cc4..a66e494dcd2 100644 --- a/mysql-test/r/information_schema.result +++ b/mysql-test/r/information_schema.result @@ -1644,6 +1644,57 @@ TEST_RESULT OK SET TIMESTAMP=DEFAULT; End of 5.1 tests. +# +# Additional test for WL#3726 "DDL locking for all metadata objects" +# To avoid possible deadlocks process of filling of I_S tables should +# use high-priority metadata lock requests when opening tables. +# Below we just test that we really use high-priority lock request +# since reproducing a deadlock will require much more complex test. +# +drop tables if exists t1, t2, t3; +create table t1 (i int); +create table t2 (j int primary key auto_increment); +# Switching to connection 'con3726_1' +lock table t2 read; +# Switching to connection 'con3726_2' +# RENAME below will be blocked by 'lock table t2 read' above but +# will add two pending requests for exclusive metadata locks. +rename table t2 to t3; +# Switching to connection 'default' +# These statements should not be blocked by pending lock requests +select table_name, column_name, data_type from information_schema.columns +where table_schema = 'test' and table_name in ('t1', 't2'); +table_name column_name data_type +t1 i int +t2 j int +select table_name, auto_increment from information_schema.tables +where table_schema = 'test' and table_name in ('t1', 't2'); +table_name auto_increment +t1 NULL +t2 1 +# Switching to connection 'con3726_1' +unlock tables; +# Switching to connection 'con3726_2' +# Switching to connection 'default' +drop tables t1, t3; +EXPLAIN SELECT * FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE; +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE KEY_COLUMN_USAGE ALL NULL NULL NULL NULL NULL Open_full_table; Scanned all databases +EXPLAIN SELECT * FROM INFORMATION_SCHEMA.PARTITIONS WHERE TABLE_NAME='t1'; +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE PARTITIONS ALL NULL TABLE_NAME NULL NULL NULL Using where; Open_full_table; Scanned 1 database +EXPLAIN SELECT * FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS +WHERE CONSTRAINT_SCHEMA='test'; +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE REFERENTIAL_CONSTRAINTS ALL NULL CONSTRAINT_SCHEMA NULL NULL NULL Using where; Open_full_table; Scanned 1 database +EXPLAIN SELECT * FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS +WHERE TABLE_NAME='t1' and TABLE_SCHEMA='test'; +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE TABLE_CONSTRAINTS ALL NULL TABLE_SCHEMA,TABLE_NAME NULL NULL NULL Using where; Open_full_table; Scanned 0 databases +EXPLAIN SELECT * FROM INFORMATION_SCHEMA.TRIGGERS +WHERE EVENT_OBJECT_SCHEMA='test'; +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE TRIGGERS ALL NULL EVENT_OBJECT_SCHEMA NULL NULL NULL Using where; Open_full_table; Scanned 1 database create table information_schema.t1 (f1 INT); ERROR 42000: Access denied for user 'root'@'localhost' to database 'information_schema' drop table information_schema.t1; @@ -1682,28 +1733,10 @@ ERROR 42000: Access denied for user 'root'@'localhost' to database 'information_ LOCK TABLES t1 READ, information_schema.tables READ; ERROR 42000: Access denied for user 'root'@'localhost' to database 'information_schema' DROP TABLE t1; -EXPLAIN SELECT * FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE; -id select_type table type possible_keys key key_len ref rows Extra -1 SIMPLE KEY_COLUMN_USAGE ALL NULL NULL NULL NULL NULL Open_full_table; Scanned all databases -EXPLAIN SELECT * FROM INFORMATION_SCHEMA.PARTITIONS WHERE TABLE_NAME='t1'; -id select_type table type possible_keys key key_len ref rows Extra -1 SIMPLE PARTITIONS ALL NULL TABLE_NAME NULL NULL NULL Using where; Open_full_table; Scanned 1 database -EXPLAIN SELECT * FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS -WHERE CONSTRAINT_SCHEMA='test'; -id select_type table type possible_keys key key_len ref rows Extra -1 SIMPLE REFERENTIAL_CONSTRAINTS ALL NULL CONSTRAINT_SCHEMA NULL NULL NULL Using where; Open_full_table; Scanned 1 database -EXPLAIN SELECT * FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS -WHERE TABLE_NAME='t1' and TABLE_SCHEMA='test'; -id select_type table type possible_keys key key_len ref rows Extra -1 SIMPLE TABLE_CONSTRAINTS ALL NULL TABLE_SCHEMA,TABLE_NAME NULL NULL NULL Using where; Open_full_table; Scanned 0 databases -EXPLAIN SELECT * FROM INFORMATION_SCHEMA.TRIGGERS -WHERE EVENT_OBJECT_SCHEMA='test'; -id select_type table type possible_keys key key_len ref rows Extra -1 SIMPLE TRIGGERS ALL NULL EVENT_OBJECT_SCHEMA NULL NULL NULL Using where; Open_full_table; Scanned 1 database SELECT * -FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE -LEFT JOIN INFORMATION_SCHEMA.COLUMNS -USING (TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME) -WHERE COLUMNS.TABLE_SCHEMA = 'test' +FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE +LEFT JOIN INFORMATION_SCHEMA.COLUMNS +USING (TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME) +WHERE COLUMNS.TABLE_SCHEMA = 'test' AND COLUMNS.TABLE_NAME = 't1'; TABLE_SCHEMA TABLE_NAME COLUMN_NAME CONSTRAINT_CATALOG CONSTRAINT_SCHEMA CONSTRAINT_NAME TABLE_CATALOG ORDINAL_POSITION POSITION_IN_UNIQUE_CONSTRAINT REFERENCED_TABLE_SCHEMA REFERENCED_TABLE_NAME REFERENCED_COLUMN_NAME TABLE_CATALOG ORDINAL_POSITION COLUMN_DEFAULT IS_NULLABLE DATA_TYPE CHARACTER_MAXIMUM_LENGTH CHARACTER_OCTET_LENGTH NUMERIC_PRECISION NUMERIC_SCALE CHARACTER_SET_NAME COLLATION_NAME COLUMN_TYPE COLUMN_KEY EXTRA PRIVILEGES COLUMN_COMMENT diff --git a/mysql-test/r/kill.result b/mysql-test/r/kill.result index 8b6830d4798..1f4f4bb32eb 100644 --- a/mysql-test/r/kill.result +++ b/mysql-test/r/kill.result @@ -138,4 +138,107 @@ KILL CONNECTION_ID(); # of close of the connection socket SELECT 1; Got one of the listed errors +# +# Additional test for WL#3726 "DDL locking for all metadata objects" +# Check that DDL and DML statements waiting for metadata locks can +# be killed. Note that we don't cover all situations here since it +# can be tricky to write test case for some of them (e.g. REPAIR or +# ALTER and other statements under LOCK TABLES). +# +drop tables if exists t1, t2, t3; +create table t1 (i int primary key); +# Test for RENAME TABLE +# Switching to connection 'blocker' +lock table t1 read; +# Switching to connection 'ddl' +rename table t1 to t2; +# Switching to connection 'default' +kill query ID; +# Switching to connection 'ddl' +ERROR 70100: Query execution was interrupted +# Test for DROP TABLE +drop table t1; +# Switching to connection 'default' +kill query ID; +# Switching to connection 'ddl' +ERROR 70100: Query execution was interrupted +# Test for CREATE TRIGGER +create trigger t1_bi before insert on t1 for each row set @a:=1; +# Switching to connection 'default' +kill query ID; +# Switching to connection 'ddl' +ERROR 70100: Query execution was interrupted +# +# Tests for various kinds of ALTER TABLE +# +# Full-blown ALTER which should copy table +alter table t1 add column j int; +# Switching to connection 'default' +kill query ID; +# Switching to connection 'ddl' +ERROR 70100: Query execution was interrupted +# Two kinds of simple ALTER +alter table t1 rename to t2; +# Switching to connection 'default' +kill query ID; +# Switching to connection 'ddl' +ERROR 70100: Query execution was interrupted +alter table t1 disable keys; +# Switching to connection 'default' +kill query ID; +# Switching to connection 'ddl' +ERROR 70100: Query execution was interrupted +# Fast ALTER +alter table t1 alter column i set default 100; +# Switching to connection 'default' +kill query ID; +# Switching to connection 'ddl' +ERROR 70100: Query execution was interrupted +# Special case which is triggered only for MERGE tables. +# Switching to connection 'blocker' +unlock tables; +create table t2 (i int primary key) engine=merge union=(t1); +lock tables t2 read; +# Switching to connection 'ddl' +alter table t2 alter column i set default 100; +# Switching to connection 'default' +kill query ID; +# Switching to connection 'ddl' +ERROR 70100: Query execution was interrupted +# Test for DML waiting for meta-data lock +# Switching to connection 'blocker' +unlock tables; +drop table t2; +create table t2 (k int); +lock tables t1 read; +# Switching to connection 'ddl' +rename tables t1 to t3, t2 to t1; +# Switching to connection 'dml' +insert into t2 values (1); +# Switching to connection 'default' +kill query ID2; +# Switching to connection 'dml' +ERROR 70100: Query execution was interrupted +# Switching to connection 'blocker' +unlock tables; +# Switching to connection 'ddl' +# Test for DML waiting for tables to be flushed +# Switching to connection 'blocker' +lock tables t1 read; +# Switching to connection 'ddl' +# Let us mark locked table t1 as old +flush tables; +# Switching to connection 'dml' +select * from t1; +# Switching to connection 'default' +kill query ID2; +# Switching to connection 'dml' +ERROR 70100: Query execution was interrupted +# Switching to connection 'blocker' +unlock tables; +# Switching to connection 'ddl' +# Cleanup. +# Switching to connection 'default' +drop table t3; +drop table t1; set @@global.concurrent_insert= @old_concurrent_insert; diff --git a/mysql-test/r/lock.result b/mysql-test/r/lock.result index 1f8f6aa04ae..a60c5c8383f 100644 --- a/mysql-test/r/lock.result +++ b/mysql-test/r/lock.result @@ -128,13 +128,14 @@ select * from v_bug5719; 1 1 drop view v_bug5719; +ERROR HY000: Can't execute the given command because you have active locked tables or an active transaction sic: did not left LOCK TABLES mode automatically select * from t1; ERROR HY000: Table 't1' was not locked with LOCK TABLES unlock tables; -create view v_bug5719 as select * from t1; +create or replace view v_bug5719 as select * from t1; lock tables v_bug5719 write; select * from v_bug5719; a diff --git a/mysql-test/r/partition_column_prune.result b/mysql-test/r/partition_column_prune.result index 82c49453d43..844429d24a6 100644 --- a/mysql-test/r/partition_column_prune.result +++ b/mysql-test/r/partition_column_prune.result @@ -56,11 +56,11 @@ insert into t1 values (2,5), (2,15), (2,25), insert into t1 select * from t1; explain partitions select * from t1 where a=2; id select_type table partitions type possible_keys key key_len ref rows Extra -1 SIMPLE t1 p01,p02,p03,p11 ALL NULL NULL NULL NULL 13 Using where +1 SIMPLE t1 p01,p02,p03,p11 ALL NULL NULL NULL NULL 8 Using where explain partitions select * from t1 where a=4; id select_type table partitions type possible_keys key key_len ref rows Extra -1 SIMPLE t1 p11,p12,p13,p21 ALL NULL NULL NULL NULL 16 Using where +1 SIMPLE t1 p11,p12,p13,p21 ALL NULL NULL NULL NULL 14 Using where explain partitions select * from t1 where a=2 and b < 22; id select_type table partitions type possible_keys key key_len ref rows Extra -1 SIMPLE t1 p01,p02,p03 ALL NULL NULL NULL NULL 16 Using where +1 SIMPLE t1 p01,p02,p03 ALL NULL NULL NULL NULL 14 Using where drop table t1; diff --git a/mysql-test/r/partition_pruning.result b/mysql-test/r/partition_pruning.result index d7790cd8075..4c22c25ddb9 100644 --- a/mysql-test/r/partition_pruning.result +++ b/mysql-test/r/partition_pruning.result @@ -1960,7 +1960,7 @@ id select_type table partitions type possible_keys key key_len ref rows Extra explain partitions select * from t1 X, t1 Y where X.a = Y.a and (X.a=1 or X.a=2); id select_type table partitions type possible_keys key key_len ref rows Extra -1 SIMPLE X p1,p2 ALL a NULL NULL NULL 4 Using where +1 SIMPLE X p1,p2 ALL a NULL NULL NULL 2 Using where 1 SIMPLE Y p1,p2 ref a a 4 test.X.a 2 drop table t1; create table t1 (a int) partition by hash(a) partitions 20; diff --git a/mysql-test/r/ps_ddl.result b/mysql-test/r/ps_ddl.result index c7e8812320c..af0ffaf4ae5 100644 --- a/mysql-test/r/ps_ddl.result +++ b/mysql-test/r/ps_ddl.result @@ -850,7 +850,7 @@ flush table t1; execute stmt; f1() 6 -call p_verify_reprepare_count(0); +call p_verify_reprepare_count(1); SUCCESS execute stmt; @@ -1706,7 +1706,7 @@ SUCCESS drop temporary table t2; execute stmt; -call p_verify_reprepare_count(1); +call p_verify_reprepare_count(0); SUCCESS drop table t2; diff --git a/mysql-test/r/sp.result b/mysql-test/r/sp.result index a73abf787d8..47d441ee182 100644 --- a/mysql-test/r/sp.result +++ b/mysql-test/r/sp.result @@ -1083,11 +1083,9 @@ select f0()| f0() 100 select * from v0| -f0() -100 +ERROR HY000: Table 'v0' was not locked with LOCK TABLES select *, f0() from v0, (select 123) as d1| -f0() 123 f0() -100 123 100 +ERROR HY000: Table 'v0' was not locked with LOCK TABLES select id, f3() from t1| ERROR HY000: Table 't1' was not locked with LOCK TABLES select f4()| diff --git a/mysql-test/r/view.result b/mysql-test/r/view.result index 2df2b0bafa6..5f16d88a0dc 100644 --- a/mysql-test/r/view.result +++ b/mysql-test/r/view.result @@ -1102,10 +1102,8 @@ select * from v1; a select * from t2; ERROR HY000: Table 't2' was not locked with LOCK TABLES -drop view v1; -drop table t1, t2; -ERROR HY000: Table 't1' was locked with a READ lock and can't be updated unlock tables; +drop view v1; drop table t1, t2; create table t1 (a int); create view v1 as select * from t1 where a < 2 with check option; diff --git a/mysql-test/r/view_grant.result b/mysql-test/r/view_grant.result index 65fbf2d87b6..0c74d8ed91b 100644 --- a/mysql-test/r/view_grant.result +++ b/mysql-test/r/view_grant.result @@ -779,9 +779,9 @@ GRANT CREATE VIEW ON db26813.v2 TO u26813@localhost; GRANT DROP, CREATE VIEW ON db26813.v3 TO u26813@localhost; GRANT SELECT ON db26813.t1 TO u26813@localhost; ALTER VIEW v1 AS SELECT f2 FROM t1; -ERROR 42000: Access denied; you need (at least one of) the SUPER privilege(s) for this operation +ERROR 42000: CREATE VIEW command denied to user 'u26813'@'localhost' for table 'v1' ALTER VIEW v2 AS SELECT f2 FROM t1; -ERROR 42000: Access denied; you need (at least one of) the SUPER privilege(s) for this operation +ERROR 42000: DROP command denied to user 'u26813'@'localhost' for table 'v2' ALTER VIEW v3 AS SELECT f2 FROM t1; ERROR 42000: Access denied; you need (at least one of) the SUPER privilege(s) for this operation SHOW CREATE VIEW v3; diff --git a/mysql-test/r/view_multi.result b/mysql-test/r/view_multi.result new file mode 100644 index 00000000000..95a8d572be4 --- /dev/null +++ b/mysql-test/r/view_multi.result @@ -0,0 +1,48 @@ +reset master; +create table t1 (i int); +create table t2 (i int); +create view v1 as select * from t1; +select get_lock("lock_bg25144", 1); +get_lock("lock_bg25144", 1) +1 +insert into v1 values (get_lock("lock_bg25144", 100));; +drop view v1;; +select release_lock("lock_bg25144"); +release_lock("lock_bg25144") +1 +select release_lock("lock_bg25144"); +release_lock("lock_bg25144") +1 +select * from t1; +i +1 +create view v1 as select * from t1; +select get_lock("lock_bg25144", 1); +get_lock("lock_bg25144", 1) +1 +insert into v1 values (get_lock("lock_bg25144", 100));; +alter view v1 as select * from t2;; +select release_lock("lock_bg25144"); +release_lock("lock_bg25144") +1 +select release_lock("lock_bg25144"); +release_lock("lock_bg25144") +1 +select * from t1; +i +1 +1 +select * from t2; +i +show binlog events in 'master-bin.000001' from 107; +Log_name Pos Event_type Server_id End_log_pos Info +master-bin.000001 # Query 1 # use `test`; create table t1 (i int) +master-bin.000001 # Query 1 # use `test`; create table t2 (i int) +master-bin.000001 # Query 1 # use `test`; CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`localhost` SQL SECURITY DEFINER VIEW `v1` AS select * from t1 +master-bin.000001 # Query 1 # use `test`; insert into v1 values (get_lock("lock_bg25144", 100)) +master-bin.000001 # Query 1 # use `test`; drop view v1 +master-bin.000001 # Query 1 # use `test`; CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`localhost` SQL SECURITY DEFINER VIEW `v1` AS select * from t1 +master-bin.000001 # Query 1 # use `test`; insert into v1 values (get_lock("lock_bg25144", 100)) +master-bin.000001 # Query 1 # use `test`; ALTER ALGORITHM=UNDEFINED DEFINER=`root`@`localhost` SQL SECURITY DEFINER VIEW `v1` AS select * from t2 +drop table t1, t2; +drop view v1; diff --git a/mysql-test/suite/rpl/t/disabled.def b/mysql-test/suite/rpl/t/disabled.def index bed019f9c79..da962a7fb9d 100644 --- a/mysql-test/suite/rpl/t/disabled.def +++ b/mysql-test/suite/rpl/t/disabled.def @@ -13,3 +13,6 @@ rpl_get_master_version_and_clock: # Bug#46931 2009-10-17 joro rpl.rpl_get_master_version_and_clock fails rpl_cross_version : BUG#43913 2009-10-22 luis rpl_cross_version fails with symptom in described in bug report rpl_spec_variables : BUG#47661 2009-10-27 jasonh rpl_spec_variables fails on PB2 hpux +rpl_empty_master_crash : BUG#48048 +rpl_load_from_master : BUG#48048 +rpl_load_table_from_master : BUG#48048 diff --git a/mysql-test/t/create.test b/mysql-test/t/create.test index 9f3c3a88151..c07014bfc19 100644 --- a/mysql-test/t/create.test +++ b/mysql-test/t/create.test @@ -683,8 +683,8 @@ drop table t1; # Error during open_and_lock_tables() of tables --error ER_NO_SUCH_TABLE create table t1 select * from t2; -# Rather special error which also caught during open tables pahse ---error ER_UPDATE_TABLE_USED +# A special case which is also caught during open tables pahse +--error ER_NO_SUCH_TABLE create table t1 select * from t1; # Error which happens before select_create::prepare() --error ER_CANT_AGGREGATE_2COLLATIONS @@ -706,6 +706,10 @@ create table t1 (i int); create table t1 select 1 as i; create table if not exists t1 select 1 as i; select * from t1; +# Error which is detected after successfull table open. +--error ER_UPDATE_TABLE_USED +create table if not exists t1 select * from t1; +select * from t1; # Error before select_create::prepare() --error ER_CANT_AGGREGATE_2COLLATIONS create table t1 select coalesce('a' collate latin1_swedish_ci,'b' collate latin1_bin); diff --git a/mysql-test/t/disabled.def b/mysql-test/t/disabled.def index ad7617b9403..bb2749e902b 100644 --- a/mysql-test/t/disabled.def +++ b/mysql-test/t/disabled.def @@ -15,3 +15,4 @@ partition_innodb_builtin : Bug#32430 2009-09-25 mattiasj Waiting for push of Inn partition_innodb_plugin : Bug#32430 2009-09-25 mattiasj Waiting for push of Innodb changes innodb-autoinc : Bug#48482 2009-11-02 svoj innodb-autoinc.test fails with results difference rpl_killed_ddl : Bug#45520: rpl_killed_ddl fails sporadically in pb2 +merge : WL#4144 diff --git a/mysql-test/t/flush.test b/mysql-test/t/flush.test index f27d4cf2fad..4172230a54d 100644 --- a/mysql-test/t/flush.test +++ b/mysql-test/t/flush.test @@ -68,10 +68,13 @@ drop table t1; create table t1 (c1 int); lock table t1 write; # Cannot get the global read lock with write locked tables. ---error 1192 +--error ER_LOCK_OR_ACTIVE_TRANSACTION flush tables with read lock; lock table t1 read; -# Can get the global read lock with read locked tables. +# Cannot get the global read lock with read locked tables. +--error ER_LOCK_OR_ACTIVE_TRANSACTION +flush tables with read lock; +unlock tables; flush tables with read lock; --error 1223 lock table t1 write; @@ -84,12 +87,12 @@ create table t2 (c1 int); create table t3 (c1 int); lock table t1 read, t2 read, t3 write; # Cannot get the global read lock with write locked tables. ---error 1192 +--error ER_LOCK_OR_ACTIVE_TRANSACTION flush tables with read lock; lock table t1 read, t2 read, t3 read; -# Can get the global read lock with read locked tables. +# Cannot get the global read lock with read locked tables. +--error ER_LOCK_OR_ACTIVE_TRANSACTION flush tables with read lock; -# Release all table locks and the global read lock. unlock tables; drop table t1, t2, t3; @@ -157,6 +160,7 @@ flush tables with read lock; unlock tables; lock tables t1 read; +--error ER_LOCK_OR_ACTIVE_TRANSACTION flush tables with read lock; unlock tables; diff --git a/mysql-test/t/flush_table.test b/mysql-test/t/flush_table.test index 50e7e91419a..e4fc1b0c39f 100644 --- a/mysql-test/t/flush_table.test +++ b/mysql-test/t/flush_table.test @@ -15,30 +15,21 @@ insert into t1 values(0); # Test for with read lock + flush lock table t1 read; +--error ER_TABLE_NOT_LOCKED_FOR_WRITE flush table t1; -check table t1; unlock tables; -# Test for with 2 read lock in different thread + flush +# Test for with write lock + flush -lock table t1 read; -connect (locker,localhost,root,,test); -connection locker; -lock table t1 read; -connection default; -send flush table t1; -connection locker; ---sleep 2 -select * from t1; -unlock tables; -connection default; -reap; -select * from t1; +lock table t1 write; +flush table t1; +check table t1; unlock tables; # Test for with a write lock and a waiting read lock + flush lock table t1 write; +connect (locker,localhost,root,,test); connection locker; send lock table t1 read; connection default; @@ -51,9 +42,9 @@ reap; unlock tables; connection default; -# Test for with a read lock and a waiting write lock + flush +# Test for with a write lock and a waiting write lock + flush -lock table t1 read; +lock table t1 write; connection locker; send lock table t1 write; connection default; diff --git a/mysql-test/t/information_schema.test b/mysql-test/t/information_schema.test index 9da7cc1042d..c6cb28a4a30 100644 --- a/mysql-test/t/information_schema.test +++ b/mysql-test/t/information_schema.test @@ -544,6 +544,7 @@ AND table_name not like 'ndb%' AND table_name not like 'innodb_%' GROUP BY TABLE_SCHEMA; + # # TRIGGERS table test # @@ -914,8 +915,8 @@ DROP PROCEDURE p1; DROP USER mysql_bug20230@localhost; # -# Bug#2123 query with a simple non-correlated subquery over -# INFORMARTION_SCHEMA.TABLES +# Bug#21231 query with a simple non-correlated subquery over +# INFORMARTION_SCHEMA.TABLES # SELECT MAX(table_name) FROM information_schema.tables WHERE table_schema IN ('mysql', 'INFORMATION_SCHEMA', 'test'); @@ -1391,9 +1392,66 @@ SET TIMESTAMP=DEFAULT; --echo End of 5.1 tests. +--echo # +--echo # Additional test for WL#3726 "DDL locking for all metadata objects" +--echo # To avoid possible deadlocks process of filling of I_S tables should +--echo # use high-priority metadata lock requests when opening tables. +--echo # Below we just test that we really use high-priority lock request +--echo # since reproducing a deadlock will require much more complex test. +--echo # +--disable_warnings +drop tables if exists t1, t2, t3; +--enable_warnings +create table t1 (i int); +create table t2 (j int primary key auto_increment); +connect (con3726_1,localhost,root,,test); +--echo # Switching to connection 'con3726_1' +connection con3726_1; +lock table t2 read; +connect (con3726_2,localhost,root,,test); +--echo # Switching to connection 'con3726_2' +connection con3726_2; +--echo # RENAME below will be blocked by 'lock table t2 read' above but +--echo # will add two pending requests for exclusive metadata locks. +--send rename table t2 to t3 +--echo # Switching to connection 'default' +connection default; +let $wait_condition= + select count(*) = 1 from information_schema.processlist + where state = "Waiting for table" and info like "rename table t2 to t3"; +--source include/wait_condition.inc +--echo # These statements should not be blocked by pending lock requests +select table_name, column_name, data_type from information_schema.columns + where table_schema = 'test' and table_name in ('t1', 't2'); +select table_name, auto_increment from information_schema.tables + where table_schema = 'test' and table_name in ('t1', 't2'); +--echo # Switching to connection 'con3726_1' +connection con3726_1; +unlock tables; +--echo # Switching to connection 'con3726_2' +connection con3726_2; +--reap +--echo # Switching to connection 'default' +connection default; +disconnect con3726_1; +disconnect con3726_2; +drop tables t1, t3; + +# +# Bug#39270 I_S optimization algorithm does not work properly in some cases +# +EXPLAIN SELECT * FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE; +EXPLAIN SELECT * FROM INFORMATION_SCHEMA.PARTITIONS WHERE TABLE_NAME='t1'; +EXPLAIN SELECT * FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS + WHERE CONSTRAINT_SCHEMA='test'; +EXPLAIN SELECT * FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS + WHERE TABLE_NAME='t1' and TABLE_SCHEMA='test'; +EXPLAIN SELECT * FROM INFORMATION_SCHEMA.TRIGGERS + WHERE EVENT_OBJECT_SCHEMA='test'; + # # Bug#24062 Incorrect error msg after execute DROP TABLE IF EXISTS on information_schema -# +# --error ER_DBACCESS_DENIED_ERROR create table information_schema.t1 (f1 INT); --error ER_DBACCESS_DENIED_ERROR @@ -1429,27 +1487,18 @@ DROP TABLE t1, information_schema.tables; LOCK TABLES t1 READ, information_schema.tables READ; DROP TABLE t1; -# -# Bug#39270 I_S optimization algorithm does not work properly in some cases -# -EXPLAIN SELECT * FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE; -EXPLAIN SELECT * FROM INFORMATION_SCHEMA.PARTITIONS WHERE TABLE_NAME='t1'; -EXPLAIN SELECT * FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS - WHERE CONSTRAINT_SCHEMA='test'; -EXPLAIN SELECT * FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS - WHERE TABLE_NAME='t1' and TABLE_SCHEMA='test'; -EXPLAIN SELECT * FROM INFORMATION_SCHEMA.TRIGGERS - WHERE EVENT_OBJECT_SCHEMA='test'; +# Wait till all disconnects are completed +--source include/wait_until_count_sessions.inc # -# Bug #43834 Assertion in Natural_join_column::db_name() on an I_S query +# Bug #43834 Assertion in Natural_join_column::db_name() on an I_S query # + SELECT * -FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE -LEFT JOIN INFORMATION_SCHEMA.COLUMNS -USING (TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME) -WHERE COLUMNS.TABLE_SCHEMA = 'test' -AND COLUMNS.TABLE_NAME = 't1'; +FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE +LEFT JOIN INFORMATION_SCHEMA.COLUMNS +USING (TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME) +WHERE COLUMNS.TABLE_SCHEMA = 'test' +AND COLUMNS.TABLE_NAME = 't1'; + -# Wait till all disconnects are completed ---source include/wait_until_count_sessions.inc diff --git a/mysql-test/t/kill.test b/mysql-test/t/kill.test index 02b033df2e5..98cb9f64d98 100644 --- a/mysql-test/t/kill.test +++ b/mysql-test/t/kill.test @@ -329,6 +329,232 @@ KILL CONNECTION_ID(); SELECT 1; --connection default +--echo # +--echo # Additional test for WL#3726 "DDL locking for all metadata objects" +--echo # Check that DDL and DML statements waiting for metadata locks can +--echo # be killed. Note that we don't cover all situations here since it +--echo # can be tricky to write test case for some of them (e.g. REPAIR or +--echo # ALTER and other statements under LOCK TABLES). +--echo # +--disable_warnings +drop tables if exists t1, t2, t3; +--enable_warnings + +create table t1 (i int primary key); +connect (blocker, localhost, root, , ); +connect (dml, localhost, root, , ); +connect (ddl, localhost, root, , ); + +--echo # Test for RENAME TABLE +--echo # Switching to connection 'blocker' +connection blocker; +lock table t1 read; +--echo # Switching to connection 'ddl' +connection ddl; +let $ID= `select connection_id()`; +--send rename table t1 to t2 +--echo # Switching to connection 'default' +connection default; +let $wait_condition= + select count(*) = 1 from information_schema.processlist + where state = "Waiting for table" and info = "rename table t1 to t2"; +--source include/wait_condition.inc +--replace_result $ID ID +eval kill query $ID; +--echo # Switching to connection 'ddl' +connection ddl; +--error ER_QUERY_INTERRUPTED +--reap + +--echo # Test for DROP TABLE +--send drop table t1 +--echo # Switching to connection 'default' +connection default; +let $wait_condition= + select count(*) = 1 from information_schema.processlist + where state = "Waiting for table" and + info = "drop table t1"; +--replace_result $ID ID +eval kill query $ID; +--echo # Switching to connection 'ddl' +connection ddl; +--error ER_QUERY_INTERRUPTED +--reap + +--echo # Test for CREATE TRIGGER +--send create trigger t1_bi before insert on t1 for each row set @a:=1 +--echo # Switching to connection 'default' +connection default; +let $wait_condition= + select count(*) = 1 from information_schema.processlist + where state = "Waiting for table" and + info = "create trigger t1_bi before insert on t1 for each row set @a:=1"; +--replace_result $ID ID +eval kill query $ID; +--echo # Switching to connection 'ddl' +connection ddl; +--error ER_QUERY_INTERRUPTED +--reap + +--echo # +--echo # Tests for various kinds of ALTER TABLE +--echo # +--echo # Full-blown ALTER which should copy table +--send alter table t1 add column j int +--echo # Switching to connection 'default' +connection default; +let $wait_condition= + select count(*) = 1 from information_schema.processlist + where state = "Waiting for table" and + info = "alter table t1 add column j int"; +--replace_result $ID ID +eval kill query $ID; +--echo # Switching to connection 'ddl' +connection ddl; +--error ER_QUERY_INTERRUPTED +--reap + +--echo # Two kinds of simple ALTER +--send alter table t1 rename to t2 +--echo # Switching to connection 'default' +connection default; +let $wait_condition= + select count(*) = 1 from information_schema.processlist + where state = "Waiting for table" and + info = "alter table t1 rename to t2"; +--replace_result $ID ID +eval kill query $ID; +--echo # Switching to connection 'ddl' +connection ddl; +--error ER_QUERY_INTERRUPTED +--reap +--send alter table t1 disable keys +--echo # Switching to connection 'default' +connection default; +let $wait_condition= + select count(*) = 1 from information_schema.processlist + where state = "Waiting for table" and + info = "alter table t1 disable keys"; +--replace_result $ID ID +eval kill query $ID; +--echo # Switching to connection 'ddl' +connection ddl; +--error ER_QUERY_INTERRUPTED +--reap +--echo # Fast ALTER +--send alter table t1 alter column i set default 100 +--echo # Switching to connection 'default' +connection default; +let $wait_condition= + select count(*) = 1 from information_schema.processlist + where state = "Waiting for table" and + info = "alter table t1 alter column i set default 100"; +--replace_result $ID ID +eval kill query $ID; +--echo # Switching to connection 'ddl' +connection ddl; +--error ER_QUERY_INTERRUPTED +--reap +--echo # Special case which is triggered only for MERGE tables. +--echo # Switching to connection 'blocker' +connection blocker; +unlock tables; +create table t2 (i int primary key) engine=merge union=(t1); +lock tables t2 read; +--echo # Switching to connection 'ddl' +connection ddl; +--send alter table t2 alter column i set default 100 +--echo # Switching to connection 'default' +connection default; +let $wait_condition= + select count(*) = 1 from information_schema.processlist + where state = "Waiting for table" and + info = "alter table t2 alter column i set default 100"; +--replace_result $ID ID +eval kill query $ID; +--echo # Switching to connection 'ddl' +connection ddl; +--error ER_QUERY_INTERRUPTED +--reap + +--echo # Test for DML waiting for meta-data lock +--echo # Switching to connection 'blocker' +connection blocker; +unlock tables; +drop table t2; +create table t2 (k int); +lock tables t1 read; +--echo # Switching to connection 'ddl' +connection ddl; +# Let us add pending exclusive metadata lock on t2 +--send rename tables t1 to t3, t2 to t1 +--echo # Switching to connection 'dml' +connection dml; +let $wait_condition= + select count(*) = 1 from information_schema.processlist + where state = "Waiting for table" and + info = "rename tables t1 to t3, t2 to t1"; +let $ID2= `select connection_id()`; +--send insert into t2 values (1) +--echo # Switching to connection 'default' +connection default; +let $wait_condition= + select count(*) = 1 from information_schema.processlist + where state = "Waiting for table" and + info = "insert into t2 values (1)"; +--replace_result $ID2 ID2 +eval kill query $ID2; +--echo # Switching to connection 'dml' +connection dml; +--error ER_QUERY_INTERRUPTED +--reap +--echo # Switching to connection 'blocker' +connection blocker; +unlock tables; +--echo # Switching to connection 'ddl' +connection ddl; +--reap + +--echo # Test for DML waiting for tables to be flushed +--echo # Switching to connection 'blocker' +connection blocker; +lock tables t1 read; +--echo # Switching to connection 'ddl' +connection ddl; +--echo # Let us mark locked table t1 as old +--send flush tables +--echo # Switching to connection 'dml' +connection dml; +let $wait_condition= + select count(*) = 1 from information_schema.processlist + where state = "Flushing tables" and + info = "flush tables"; +--send select * from t1 +--echo # Switching to connection 'default' +connection default; +let $wait_condition= + select count(*) = 1 from information_schema.processlist + where state = "Waiting for table" and + info = "select * from t1"; +--replace_result $ID2 ID2 +eval kill query $ID2; +--echo # Switching to connection 'dml' +connection dml; +--error ER_QUERY_INTERRUPTED +--reap +--echo # Switching to connection 'blocker' +connection blocker; +unlock tables; +--echo # Switching to connection 'ddl' +connection ddl; +--reap + +--echo # Cleanup. +--echo # Switching to connection 'default' +connection default; +drop table t3; +drop table t1; + ########################################################################### # Restore global concurrent_insert value. Keep in the end of the test file. diff --git a/mysql-test/t/lock.test b/mysql-test/t/lock.test index 04994e3e48f..856ae020492 100644 --- a/mysql-test/t/lock.test +++ b/mysql-test/t/lock.test @@ -178,6 +178,7 @@ select * from t2; --error ER_TABLE_NOT_LOCKED select * from t3; select * from v_bug5719; +--error ER_LOCK_OR_ACTIVE_TRANSACTION drop view v_bug5719; --echo --echo sic: did not left LOCK TABLES mode automatically @@ -185,7 +186,7 @@ drop view v_bug5719; --error ER_TABLE_NOT_LOCKED select * from t1; unlock tables; -create view v_bug5719 as select * from t1; +create or replace view v_bug5719 as select * from t1; lock tables v_bug5719 write; select * from v_bug5719; --echo diff --git a/mysql-test/t/lock_multi.test b/mysql-test/t/lock_multi.test index 75ee6d07723..58f46941920 100644 --- a/mysql-test/t/lock_multi.test +++ b/mysql-test/t/lock_multi.test @@ -164,7 +164,7 @@ connection locker; # Sleep a bit till the select of connection reader is in work and hangs let $wait_condition= select count(*) = 1 from information_schema.processlist - where state = "Waiting for table" and info = + where state = "Locked" and info = "SELECT user.Select_priv FROM user, db WHERE user.user = db.user LIMIT 1"; --source include/wait_condition.inc # Make test case independent from earlier grants. @@ -196,7 +196,7 @@ connection writer; # Sleep a bit till the flush of connection locker is in work and hangs let $wait_condition= select count(*) = 1 from information_schema.processlist - where state = "Flushing tables" and info = "FLUSH TABLES WITH READ LOCK"; + where state = "Waiting for table" and info = "FLUSH TABLES WITH READ LOCK"; --source include/wait_condition.inc # This must not block. CREATE TABLE t2 (c1 int); @@ -226,7 +226,7 @@ connection writer; # Sleep a bit till the flush of connection locker is in work and hangs let $wait_condition= select count(*) = 1 from information_schema.processlist - where state = "Flushing tables" and info = "FLUSH TABLES WITH READ LOCK"; + where state = "Waiting for table" and info = "FLUSH TABLES WITH READ LOCK"; --source include/wait_condition.inc --error ER_TABLE_NOT_LOCKED CREATE TABLE t2 AS SELECT * FROM t1; @@ -337,10 +337,10 @@ connection con2; send flush tables with read lock; connection con5; --echo # con5 -let $show_statement= SHOW PROCESSLIST; -let $field= State; -let $condition= = 'Flushing tables'; ---source include/wait_show_condition.inc +let $wait_condition= + select count(*) = 1 from information_schema.processlist + where state = "Waiting for table" and info = "flush tables with read lock"; +--source include/wait_condition.inc --echo # global read lock is taken connection con3; --echo # con3 @@ -528,7 +528,7 @@ connection default; --echo connection: default let $wait_condition= select count(*) = 1 from information_schema.processlist - where state = "Flushing tables"; + where state = "Waiting for table"; --source include/wait_condition.inc alter table t1 add column j int; connect (insert,localhost,root,,test,,); @@ -536,7 +536,7 @@ connection insert; --echo connection: insert let $wait_condition= select count(*) = 1 from information_schema.processlist - where state = "Flushing tables"; + where state = "Waiting for table"; --source include/wait_condition.inc --send insert into t1 values (1,2); --echo connection: default @@ -586,18 +586,14 @@ connection default; --echo connection: default let $wait_condition= select count(*) = 1 from information_schema.processlist - where state = "Flushing tables"; + where state = "Waiting for table"; --source include/wait_condition.inc flush tables; let $wait_condition= select count(*) = 1 from information_schema.processlist - where state = "Flushing tables"; + where state = "Waiting for table"; --source include/wait_condition.inc unlock tables; -let $wait_condition= - select count(*) = 0 from information_schema.processlist - where state = "Flushing tables"; ---source include/wait_condition.inc connection flush; --reap connection default; @@ -656,18 +652,14 @@ connection default; --echo connection: default let $wait_condition= select count(*) = 1 from information_schema.processlist - where state = "Flushing tables"; + where state = "Waiting for table"; --source include/wait_condition.inc flush tables; let $wait_condition= select count(*) = 1 from information_schema.processlist - where state = "Flushing tables"; + where state = "Waiting for table"; --source include/wait_condition.inc drop table t1; -let $wait_condition= - select count(*) = 0 from information_schema.processlist - where state = "Flushing tables"; ---source include/wait_condition.inc connection flush; --reap connection default; diff --git a/mysql-test/t/ps_ddl.test b/mysql-test/t/ps_ddl.test index fee235cd36c..6e771d44b3f 100644 --- a/mysql-test/t/ps_ddl.test +++ b/mysql-test/t/ps_ddl.test @@ -745,7 +745,7 @@ execute stmt; call p_verify_reprepare_count(1); flush table t1; execute stmt; -call p_verify_reprepare_count(0); +call p_verify_reprepare_count(1); execute stmt; --echo # Test 18-c: dependent VIEW has changed @@ -1453,7 +1453,7 @@ execute stmt; call p_verify_reprepare_count(0); drop temporary table t2; execute stmt; -call p_verify_reprepare_count(1); +call p_verify_reprepare_count(0); drop table t2; execute stmt; call p_verify_reprepare_count(0); diff --git a/mysql-test/t/sp.test b/mysql-test/t/sp.test index b0342491a34..b477b5a3e24 100644 --- a/mysql-test/t/sp.test +++ b/mysql-test/t/sp.test @@ -1308,9 +1308,11 @@ select f3()| select id, f3() from t1 as t11 order by id| # Degenerate cases work too :) select f0()| +# But these should not (particularly views should be locked explicitly). +--error ER_TABLE_NOT_LOCKED select * from v0| +--error ER_TABLE_NOT_LOCKED select *, f0() from v0, (select 123) as d1| -# But these should not ! --error ER_TABLE_NOT_LOCKED select id, f3() from t1| --error ER_TABLE_NOT_LOCKED diff --git a/mysql-test/t/trigger_notembedded.test b/mysql-test/t/trigger_notembedded.test index 7a7e6c6bc85..8a570a7e87d 100644 --- a/mysql-test/t/trigger_notembedded.test +++ b/mysql-test/t/trigger_notembedded.test @@ -896,7 +896,7 @@ connection default; --echo connection: default let $wait_condition= select count(*) = 1 from information_schema.processlist - where state = "Flushing tables"; + where state = "Waiting for table"; --source include/wait_condition.inc create trigger t1_bi before insert on t1 for each row begin end; unlock tables; diff --git a/mysql-test/t/view.test b/mysql-test/t/view.test index abf8dac2870..c3ff58880c9 100644 --- a/mysql-test/t/view.test +++ b/mysql-test/t/view.test @@ -1016,10 +1016,8 @@ lock tables t1 read, v1 read; select * from v1; -- error ER_TABLE_NOT_LOCKED select * from t2; -drop view v1; ---error ER_TABLE_NOT_LOCKED_FOR_WRITE -drop table t1, t2; unlock tables; +drop view v1; drop table t1, t2; # diff --git a/mysql-test/t/view_grant.test b/mysql-test/t/view_grant.test index f01edb1e499..d94bcfc29ed 100644 --- a/mysql-test/t/view_grant.test +++ b/mysql-test/t/view_grant.test @@ -1047,9 +1047,9 @@ GRANT SELECT ON db26813.t1 TO u26813@localhost; connect (u1,localhost,u26813,,db26813); connection u1; ---error ER_SPECIFIC_ACCESS_DENIED_ERROR +--error ER_TABLEACCESS_DENIED_ERROR ALTER VIEW v1 AS SELECT f2 FROM t1; ---error ER_SPECIFIC_ACCESS_DENIED_ERROR +--error ER_TABLEACCESS_DENIED_ERROR ALTER VIEW v2 AS SELECT f2 FROM t1; --error ER_SPECIFIC_ACCESS_DENIED_ERROR ALTER VIEW v3 AS SELECT f2 FROM t1; diff --git a/mysql-test/t/view_multi.test b/mysql-test/t/view_multi.test new file mode 100644 index 00000000000..a61e7738095 --- /dev/null +++ b/mysql-test/t/view_multi.test @@ -0,0 +1,110 @@ +# +# QQ: Should we find a better place for this test? +# May be binlog or rpl suites ? +# +--source include/have_log_bin.inc +--source include/have_binlog_format_mixed_or_statement.inc + +# +# Bug #25144 "replication / binlog with view breaks". +# Statements that used views didn't ensure that view were not modified +# during their execution. Indeed this led to incorrect binary log with +# statement based logging. +# +--disable_parsing +drop table if not exists t1, t2; +drop view if exists v1; +--enable_parsing + +# We are going to use binary log later to check that statements are +# logged in proper order, so it is good idea to reset it here. +reset master; + +connect (addconn1,localhost,root,,); +connect (addconn2,localhost,root,,); +connection default; + +create table t1 (i int); +create table t2 (i int); +create view v1 as select * from t1; + +# First we try to concurrently execute statement that uses view +# and statement that drops it. We use "user" locks as means to +# suspend execution of first statement once it opens our view. +select get_lock("lock_bg25144", 1); + +connection addconn1; +--send insert into v1 values (get_lock("lock_bg25144", 100)); + +connection addconn2; +let $wait_condition= + select count(*) = 1 from information_schema.processlist + where state = "User lock" and info like "insert into v1 %lock_bg25144%"; +--source include/wait_condition.inc +--send drop view v1; + +connection default; +let $wait_condition= + select count(*) = 1 from information_schema.processlist + where state = "Waiting for table" and info = "drop view v1"; +--source include/wait_condition.inc + +select release_lock("lock_bg25144"); + +connection addconn1; +--reap +select release_lock("lock_bg25144"); + +connection addconn2; +--reap + +connection default; +# Check that insertion through view did happen. +select * from t1; +# At the end of test we will check that statements were +# logged in proper order. + +# Now we will repeat the test by trying concurrently execute +# statement that uses a view and statement that alters it. +create view v1 as select * from t1; + +select get_lock("lock_bg25144", 1); + +connection addconn1; +--send insert into v1 values (get_lock("lock_bg25144", 100)); + +connection addconn2; +let $wait_condition= + select count(*) = 1 from information_schema.processlist + where state = "User lock" and info like "insert into v1 %lock_bg25144%"; +--source include/wait_condition.inc +--send alter view v1 as select * from t2; + +connection default; +let $wait_condition= + select count(*) = 1 from information_schema.processlist + where state = "Waiting for table" and + info = "alter view v1 as select * from t2"; +--source include/wait_condition.inc + +select release_lock("lock_bg25144"); + +connection addconn1; +--reap +select release_lock("lock_bg25144"); + +connection addconn2; +--reap + +connection default; + +# Second insertion should go to t1 as well. +select * from t1; +select * from t2; + +# Now let us check that statements were logged in proper order +--replace_column 2 # 5 # +show binlog events in 'master-bin.000001' from 107; + +drop table t1, t2; +drop view v1; diff --git a/sql/CMakeLists.txt b/sql/CMakeLists.txt index 15c2d950ff9..a40cf04f37c 100755 --- a/sql/CMakeLists.txt +++ b/sql/CMakeLists.txt @@ -76,7 +76,7 @@ SET (SQL_SOURCE rpl_rli.cc rpl_mi.cc sql_servers.cc sql_connect.cc scheduler.cc sql_profile.cc event_parse_data.cc - sql_signal.cc rpl_handler.cc + sql_signal.cc rpl_handler.cc mdl.cc ${PROJECT_SOURCE_DIR}/sql/sql_yacc.cc ${PROJECT_SOURCE_DIR}/sql/sql_yacc.h ${PROJECT_SOURCE_DIR}/include/mysqld_error.h diff --git a/sql/Makefile.am b/sql/Makefile.am index 15ee0d588c4..95864dfbb1b 100644 --- a/sql/Makefile.am +++ b/sql/Makefile.am @@ -112,7 +112,8 @@ noinst_HEADERS = item.h item_func.h item_sum.h item_cmpfunc.h \ event_data_objects.h event_scheduler.h \ sql_partition.h partition_info.h partition_element.h \ contributors.h sql_servers.h sql_signal.h records.h \ - sql_prepare.h rpl_handler.h replication.h + sql_prepare.h rpl_handler.h replication.h mdl.h \ + sql_plist.h mysqld_SOURCES = sql_lex.cc sql_handler.cc sql_partition.cc \ item.cc item_sum.cc item_buff.cc item_func.cc \ @@ -158,7 +159,7 @@ mysqld_SOURCES = sql_lex.cc sql_handler.cc sql_partition.cc \ sql_plugin.cc sql_binlog.cc \ sql_builtin.cc sql_tablespace.cc partition_info.cc \ sql_servers.cc event_parse_data.cc sql_signal.cc \ - rpl_handler.cc + rpl_handler.cc mdl.cc nodist_mysqld_SOURCES = mini_client_errors.c pack.c client.c my_time.c my_user.c diff --git a/sql/event_db_repository.cc b/sql/event_db_repository.cc index 8faab5023da..0cf16e3a8a4 100644 --- a/sql/event_db_repository.cc +++ b/sql/event_db_repository.cc @@ -555,6 +555,7 @@ Event_db_repository::open_event_table(THD *thd, enum thr_lock_type lock_type, DBUG_ENTER("Event_db_repository::open_event_table"); tables.init_one_table("mysql", "event", lock_type); + alloc_mdl_locks(&tables, thd->mem_root); if (simple_open_n_lock_tables(thd, &tables)) { @@ -1109,6 +1110,7 @@ Event_db_repository::check_system_tables(THD *thd) /* Check mysql.db */ tables.init_one_table("mysql", "db", TL_READ); + alloc_mdl_locks(&tables, thd->mem_root); if (simple_open_n_lock_tables(thd, &tables)) { @@ -1126,6 +1128,7 @@ Event_db_repository::check_system_tables(THD *thd) } /* Check mysql.user */ tables.init_one_table("mysql", "user", TL_READ); + alloc_mdl_locks(&tables, thd->mem_root); if (simple_open_n_lock_tables(thd, &tables)) { @@ -1146,6 +1149,7 @@ Event_db_repository::check_system_tables(THD *thd) } /* Check mysql.event */ tables.init_one_table("mysql", "event", TL_READ); + alloc_mdl_locks(&tables, thd->mem_root); if (simple_open_n_lock_tables(thd, &tables)) { diff --git a/sql/ha_ndbcluster.cc b/sql/ha_ndbcluster.cc index b71a7f602ab..a7c4cbf5d20 100644 --- a/sql/ha_ndbcluster.cc +++ b/sql/ha_ndbcluster.cc @@ -565,7 +565,7 @@ int ha_ndbcluster::ndb_err(NdbTransaction *trans) bzero((char*) &table_list,sizeof(table_list)); table_list.db= m_dbname; table_list.alias= table_list.table_name= m_tabname; - close_cached_tables(thd, &table_list, FALSE, FALSE, FALSE); + close_cached_tables(thd, &table_list, FALSE, FALSE); break; } default: @@ -7276,6 +7276,20 @@ int ndbcluster_find_files(handlerton *hton, THD *thd, } } + /* + ndbcluster_find_files() may be called from I_S code and ndbcluster_binlog + thread in situations when some tables are already open. This means that + code below will try to obtain exclusive metadata lock on some table + while holding shared meta-data lock on other tables. This might lead to + a deadlock, and therefore is disallowed by assertions of the metadata + locking subsystem. In order to temporarily make the code work, we must + reset and backup the open tables state, thus hide the existing locks + from MDL asserts. But in the essence this is violation of metadata + locking protocol which has to be closed ASAP. + */ + Open_tables_state open_tables_state_backup; + thd->reset_n_backup_open_tables_state(&open_tables_state_backup); + if (!global_read_lock) { // Delete old files @@ -7299,8 +7313,11 @@ int ndbcluster_find_files(handlerton *hton, THD *thd, } } + thd->restore_backup_open_tables_state(&open_tables_state_backup); + + /* Lock mutex before creating .FRM files. */ pthread_mutex_lock(&LOCK_open); - // Create new files + /* Create new files. */ List_iterator_fast<char> it2(create_list); while ((file_name_str=it2++)) { @@ -8215,7 +8232,7 @@ int handle_trailing_share(NDB_SHARE *share) table_list.db= share->db; table_list.alias= table_list.table_name= share->table_name; safe_mutex_assert_owner(&LOCK_open); - close_cached_tables(thd, &table_list, TRUE, FALSE, FALSE); + close_cached_tables(thd, &table_list, TRUE, FALSE); pthread_mutex_lock(&ndbcluster_mutex); /* ndb_share reference temporary free */ diff --git a/sql/ha_ndbcluster_binlog.cc b/sql/ha_ndbcluster_binlog.cc index e34a22cf9f4..bf76960201d 100644 --- a/sql/ha_ndbcluster_binlog.cc +++ b/sql/ha_ndbcluster_binlog.cc @@ -140,6 +140,8 @@ static Uint64 *p_latest_trans_gci= 0; */ static TABLE *ndb_binlog_index= 0; static TABLE_LIST binlog_tables; +static MDL_LOCK binlog_mdl_lock; +static char binlog_mdlkey[MAX_DBKEY_LENGTH]; /* Helper functions @@ -282,7 +284,13 @@ static void run_query(THD *thd, char *buf, char *end, thd_ndb->m_error_code, (int) thd->is_error(), thd->is_slave_error); } + + /* + After executing statement we should unlock and close tables open + by it as well as release meta-data locks obtained by it. + */ close_thread_tables(thd); + /* XXX: this code is broken. mysql_parse()/mysql_reset_thd_for_next_command() can not be called from within a statement, and @@ -921,7 +929,7 @@ int ndbcluster_setup_binlog_table_shares(THD *thd) ndb_binlog_tables_inited= TRUE; if (ndb_extra_logging) sql_print_information("NDB Binlog: ndb tables writable"); - close_cached_tables(NULL, NULL, TRUE, FALSE, FALSE); + close_cached_tables(NULL, NULL, TRUE, FALSE); pthread_mutex_unlock(&LOCK_open); /* Signal injector thread that all is setup */ pthread_cond_signal(&injector_cond); @@ -1735,7 +1743,7 @@ ndb_handle_schema_change(THD *thd, Ndb *ndb, NdbEventOperation *pOp, bzero((char*) &table_list,sizeof(table_list)); table_list.db= (char *)dbname; table_list.alias= table_list.table_name= (char *)tabname; - close_cached_tables(thd, &table_list, TRUE, FALSE, FALSE); + close_cached_tables(thd, &table_list, TRUE, FALSE); if ((error= ndbcluster_binlog_open_table(thd, share, table_share, table, 1))) @@ -1841,7 +1849,7 @@ ndb_handle_schema_change(THD *thd, Ndb *ndb, NdbEventOperation *pOp, bzero((char*) &table_list,sizeof(table_list)); table_list.db= (char *)dbname; table_list.alias= table_list.table_name= (char *)tabname; - close_cached_tables(thd, &table_list, FALSE, FALSE, FALSE); + close_cached_tables(thd, &table_list, FALSE, FALSE); /* ndb_share reference create free */ DBUG_PRINT("NDB_SHARE", ("%s create free use_count: %u", share->key, share->use_count)); @@ -1962,7 +1970,7 @@ ndb_binlog_thread_handle_schema_event(THD *thd, Ndb *ndb, bzero((char*) &table_list,sizeof(table_list)); table_list.db= schema->db; table_list.alias= table_list.table_name= schema->name; - close_cached_tables(thd, &table_list, FALSE, FALSE, FALSE); + close_cached_tables(thd, &table_list, FALSE, FALSE); } /* ndb_share reference temporary free */ if (share) @@ -2079,7 +2087,7 @@ ndb_binlog_thread_handle_schema_event(THD *thd, Ndb *ndb, pthread_mutex_unlock(&ndb_schema_share_mutex); /* end protect ndb_schema_share */ - close_cached_tables(NULL, NULL, FALSE, FALSE, FALSE); + close_cached_tables(NULL, NULL, FALSE, FALSE); // fall through case NDBEVENT::TE_ALTER: ndb_handle_schema_change(thd, ndb, pOp, tmp_share); @@ -2236,7 +2244,7 @@ ndb_binlog_thread_handle_schema_event_post_epoch(THD *thd, bzero((char*) &table_list,sizeof(table_list)); table_list.db= schema->db; table_list.alias= table_list.table_name= schema->name; - close_cached_tables(thd, &table_list, FALSE, FALSE, FALSE); + close_cached_tables(thd, &table_list, FALSE, FALSE); } if (schema_type != SOT_ALTER_TABLE) break; @@ -2323,18 +2331,21 @@ struct ndb_binlog_index_row { /* Open the ndb_binlog_index table */ -static int open_ndb_binlog_index(THD *thd, TABLE_LIST *tables, - TABLE **ndb_binlog_index) +static int open_ndb_binlog_index(THD *thd, TABLE **ndb_binlog_index) { static char repdb[]= NDB_REP_DB; static char reptable[]= NDB_REP_TABLE; const char *save_proc_info= thd->proc_info; + TABLE_LIST *tables= &binlog_tables; bzero((char*) tables, sizeof(*tables)); tables->db= repdb; tables->alias= tables->table_name= reptable; tables->lock_type= TL_WRITE; thd->proc_info= "Opening " NDB_REP_DB "." NDB_REP_TABLE; + mdl_init_lock(&binlog_mdl_lock, binlog_mdlkey, 0, tables->db, + tables->table_name); + tables->mdl_lock= &binlog_mdl_lock; tables->required_type= FRMTYPE_TABLE; uint counter; thd->clear_error(); @@ -2374,7 +2385,7 @@ int ndb_add_ndb_binlog_index(THD *thd, void *_row) for ( ; ; ) /* loop for need_reopen */ { - if (!ndb_binlog_index && open_ndb_binlog_index(thd, &binlog_tables, &ndb_binlog_index)) + if (!ndb_binlog_index && open_ndb_binlog_index(thd, &ndb_binlog_index)) { error= -1; goto add_ndb_binlog_index_err; @@ -2385,7 +2396,7 @@ int ndb_add_ndb_binlog_index(THD *thd, void *_row) if (need_reopen) { TABLE_LIST *p_binlog_tables= &binlog_tables; - close_tables_for_reopen(thd, &p_binlog_tables); + close_tables_for_reopen(thd, &p_binlog_tables, FALSE); ndb_binlog_index= 0; continue; } @@ -3893,7 +3904,7 @@ restart: static char db[]= ""; thd->db= db; if (ndb_binlog_running) - open_ndb_binlog_index(thd, &binlog_tables, &ndb_binlog_index); + open_ndb_binlog_index(thd, &ndb_binlog_index); thd->db= db; } do_ndbcluster_binlog_close_connection= BCCC_running; diff --git a/sql/handler.cc b/sql/handler.cc index 17a92b00b4f..9c32171eefd 100644 --- a/sql/handler.cc +++ b/sql/handler.cc @@ -2997,20 +2997,13 @@ static bool update_frm_version(TABLE *table) if ((file= my_open(path, O_RDWR|O_BINARY, MYF(MY_WME))) >= 0) { uchar version[4]; - char *key= table->s->table_cache_key.str; - uint key_length= table->s->table_cache_key.length; - TABLE *entry; - HASH_SEARCH_STATE state; int4store(version, MYSQL_VERSION_ID); if ((result= my_pwrite(file,(uchar*) version,4,51L,MYF_RW))) goto err; - for (entry=(TABLE*) my_hash_first(&open_cache,(uchar*) key,key_length, &state); - entry; - entry= (TABLE*) my_hash_next(&open_cache,(uchar*) key,key_length, &state)) - entry->s->mysql_version= MYSQL_VERSION_ID; + table->s->mysql_version= MYSQL_VERSION_ID; } err: if (file >= 0) diff --git a/sql/lock.cc b/sql/lock.cc index 56ae94ddc39..0c8c3095844 100644 --- a/sql/lock.cc +++ b/sql/lock.cc @@ -955,361 +955,56 @@ static MYSQL_LOCK *get_lock_data(THD *thd, TABLE **table_ptr, uint count, *****************************************************************************/ /** - Lock and wait for the named lock. + Obtain exclusive metadata locks on the list of tables. - @param thd Thread handler - @param table_list Lock first table in this list + @param thd Thread handle + @param table_list List of tables to lock + @note This function assumes that no metadata locks were acquired + before calling it. Also it cannot be called while holding + LOCK_open mutex. Both these invariants are enforced by asserts + in mdl_acquire_exclusive_locks() functions. - @note - Works together with global read lock. - - @retval - 0 ok - @retval - 1 error -*/ - -int lock_and_wait_for_table_name(THD *thd, TABLE_LIST *table_list) -{ - int lock_retcode; - int error= -1; - DBUG_ENTER("lock_and_wait_for_table_name"); - - if (wait_if_global_read_lock(thd, 0, 1)) - DBUG_RETURN(1); - pthread_mutex_lock(&LOCK_open); - if ((lock_retcode = lock_table_name(thd, table_list, TRUE)) < 0) - goto end; - if (lock_retcode && wait_for_locked_table_names(thd, table_list)) - { - unlock_table_name(thd, table_list); - goto end; - } - error=0; - -end: - pthread_mutex_unlock(&LOCK_open); - start_waiting_global_read_lock(thd); - DBUG_RETURN(error); -} - - -/** - Put a not open table with an old refresh version in the table cache. - - @param thd Thread handler - @param table_list Lock first table in this list - @param check_in_use Do we need to check if table already in use by us - - @note - One must have a lock on LOCK_open! - - @warning - If you are going to update the table, you should use - lock_and_wait_for_table_name instead of this function as this works - together with 'FLUSH TABLES WITH READ LOCK' - - @note - This will force any other threads that uses the table to release it - as soon as possible. - - @return - < 0 error - @return - == 0 table locked - @return - > 0 table locked, but someone is using it -*/ - -int lock_table_name(THD *thd, TABLE_LIST *table_list, bool check_in_use) -{ - TABLE *table; - char key[MAX_DBKEY_LENGTH]; - char *db= table_list->db; - uint key_length; - bool found_locked_table= FALSE; - HASH_SEARCH_STATE state; - DBUG_ENTER("lock_table_name"); - DBUG_PRINT("enter",("db: %s name: %s", db, table_list->table_name)); - - key_length= create_table_def_key(thd, key, table_list, 0); - - if (check_in_use) - { - /* Only insert the table if we haven't insert it already */ - for (table=(TABLE*) my_hash_first(&open_cache, (uchar*)key, - key_length, &state); - table ; - table = (TABLE*) my_hash_next(&open_cache,(uchar*) key, - key_length, &state)) - { - if (table->reginfo.lock_type < TL_WRITE) - { - if (table->in_use == thd) - found_locked_table= TRUE; - continue; - } - - if (table->in_use == thd) - { - DBUG_PRINT("info", ("Table is in use")); - table->s->version= 0; // Ensure no one can use this - table->locked_by_name= 1; - DBUG_RETURN(0); - } - } - } - - if (thd->locked_tables && thd->locked_tables->table_count && - ! find_temporary_table(thd, table_list->db, table_list->table_name)) - { - if (found_locked_table) - my_error(ER_TABLE_NOT_LOCKED_FOR_WRITE, MYF(0), table_list->alias); - else - my_error(ER_TABLE_NOT_LOCKED, MYF(0), table_list->alias); - - DBUG_RETURN(-1); - } - - if (!(table= table_cache_insert_placeholder(thd, key, key_length))) - DBUG_RETURN(-1); - - table_list->table=table; - - /* Return 1 if table is in use */ - DBUG_RETURN(test(remove_table_from_cache(thd, db, table_list->table_name, - check_in_use ? RTFC_NO_FLAG : RTFC_WAIT_OTHER_THREAD_FLAG))); -} - - -void unlock_table_name(THD *thd, TABLE_LIST *table_list) -{ - if (table_list->table) - { - my_hash_delete(&open_cache, (uchar*) table_list->table); - broadcast_refresh(); - } -} - - -static bool locked_named_table(THD *thd, TABLE_LIST *table_list) -{ - for (; table_list ; table_list=table_list->next_local) - { - TABLE *table= table_list->table; - if (table) - { - TABLE *save_next= table->next; - bool result; - table->next= 0; - result= table_is_used(table_list->table, 0); - table->next= save_next; - if (result) - return 1; - } - } - return 0; // All tables are locked -} - - -bool wait_for_locked_table_names(THD *thd, TABLE_LIST *table_list) -{ - bool result=0; - DBUG_ENTER("wait_for_locked_table_names"); - - safe_mutex_assert_owner(&LOCK_open); - - while (locked_named_table(thd,table_list)) - { - if (thd->killed) - { - result=1; - break; - } - wait_for_condition(thd, &LOCK_open, &COND_refresh); - pthread_mutex_lock(&LOCK_open); - } - DBUG_RETURN(result); -} - - -/** - Lock all tables in list with a name lock. - - REQUIREMENTS - - One must have a lock on LOCK_open when calling this - - @param thd Thread handle - @param table_list Names of tables to lock - - @note - If you are just locking one table, you should use - lock_and_wait_for_table_name(). - - @retval - 0 ok - @retval - 1 Fatal error (end of memory ?) + @retval FALSE Success. + @retval TRUE Failure (OOM or thread was killed). */ bool lock_table_names(THD *thd, TABLE_LIST *table_list) { - bool got_all_locks=1; TABLE_LIST *lock_table; + MDL_LOCK *mdl_lock; for (lock_table= table_list; lock_table; lock_table= lock_table->next_local) { - int got_lock; - if ((got_lock=lock_table_name(thd,lock_table, TRUE)) < 0) - goto end; // Fatal error - if (got_lock) - got_all_locks=0; // Someone is using table + if (!(mdl_lock= mdl_alloc_lock(0, lock_table->db, lock_table->table_name, + thd->mem_root))) + goto end; + mdl_set_lock_type(mdl_lock, MDL_EXCLUSIVE); + mdl_add_lock(&thd->mdl_context, mdl_lock); } - - /* If some table was in use, wait until we got the lock */ - if (!got_all_locks && wait_for_locked_table_names(thd, table_list)) - goto end; + if (mdl_acquire_exclusive_locks(&thd->mdl_context)) + return 1; return 0; end: - unlock_table_names(thd, table_list, lock_table); + mdl_remove_all_locks(&thd->mdl_context); return 1; } /** - Unlock all tables in list with a name lock. - - @param thd Thread handle. - @param table_list Names of tables to lock. - - @note - This function needs to be protected by LOCK_open. If we're - under LOCK TABLES, this function does not work as advertised. Namely, - it does not exclude other threads from using this table and does not - put an exclusive name lock on this table into the table cache. - - @see lock_table_names - @see unlock_table_names - - @retval TRUE An error occured. - @retval FALSE Name lock successfully acquired. -*/ - -bool lock_table_names_exclusively(THD *thd, TABLE_LIST *table_list) -{ - if (lock_table_names(thd, table_list)) - return TRUE; - - /* - Upgrade the table name locks from semi-exclusive to exclusive locks. - */ - for (TABLE_LIST *table= table_list; table; table= table->next_global) - { - if (table->table) - table->table->open_placeholder= 1; - } - return FALSE; -} - - -/** - Test is 'table' is protected by an exclusive name lock. - - @param[in] thd The current thread handler - @param[in] table_list Table container containing the single table to be - tested - - @note Needs to be protected by LOCK_open mutex. - - @return Error status code - @retval TRUE Table is protected - @retval FALSE Table is not protected -*/ - -bool -is_table_name_exclusively_locked_by_this_thread(THD *thd, - TABLE_LIST *table_list) -{ - char key[MAX_DBKEY_LENGTH]; - uint key_length; - - key_length= create_table_def_key(thd, key, table_list, 0); - - return is_table_name_exclusively_locked_by_this_thread(thd, (uchar *)key, - key_length); -} - - -/** - Test is 'table key' is protected by an exclusive name lock. - - @param[in] thd The current thread handler. - @param[in] key - @param[in] key_length - - @note Needs to be protected by LOCK_open mutex + Release all metadata locks previously obtained by lock_table_names(). - @retval TRUE Table is protected - @retval FALSE Table is not protected - */ - -bool -is_table_name_exclusively_locked_by_this_thread(THD *thd, uchar *key, - int key_length) -{ - HASH_SEARCH_STATE state; - TABLE *table; - - for (table= (TABLE*) my_hash_first(&open_cache, key, - key_length, &state); - table ; - table= (TABLE*) my_hash_next(&open_cache, key, - key_length, &state)) - { - if (table->in_use == thd && - table->open_placeholder == 1 && - table->s->version == 0) - return TRUE; - } - - return FALSE; -} + @param thd Thread handle. -/** - Unlock all tables in list with a name lock. - - @param - thd Thread handle - @param - table_list Names of tables to unlock - @param - last_table Don't unlock any tables after this one. - (default 0, which will unlock all tables) - - @note - One must have a lock on LOCK_open when calling this. - - @note - This function will broadcast refresh signals to inform other threads - that the name locks are removed. - - @retval - 0 ok - @retval - 1 Fatal error (end of memory ?) + @note Cannot be called while holding LOCK_open mutex. */ -void unlock_table_names(THD *thd, TABLE_LIST *table_list, - TABLE_LIST *last_table) +void unlock_table_names(THD *thd) { DBUG_ENTER("unlock_table_names"); - for (TABLE_LIST *table= table_list; - table != last_table; - table= table->next_local) - unlock_table_name(thd,table); - broadcast_refresh(); + mdl_release_locks(&thd->mdl_context); + mdl_remove_all_locks(&thd->mdl_context); DBUG_VOID_RETURN; } @@ -1455,6 +1150,33 @@ bool lock_global_read_lock(THD *thd) thd->global_read_lock= GOT_GLOBAL_READ_LOCK; global_read_lock++; thd->exit_cond(old_message); // this unlocks LOCK_global_read_lock + /* + When we perform FLUSH TABLES or ALTER TABLE under LOCK TABLES, + tables being reopened are protected only by meta-data locks at + some point. To avoid sneaking in with our global read lock at + this moment we have to take global shared meta data lock. + + TODO: We should change this code to acquire global shared metadata + lock before acquiring global read lock. But in order to do + this we have to get rid of all those places in which + wait_if_global_read_lock() is called before acquiring + metadata locks first. Also long-term we should get rid of + redundancy between metadata locks, global read lock and DDL + blocker (see WL#4399 and WL#4400). + */ + if (mdl_acquire_global_shared_lock(&thd->mdl_context)) + { + /* Our thread was killed -- return back to initial state. */ + pthread_mutex_lock(&LOCK_global_read_lock); + if (!(--global_read_lock)) + { + DBUG_PRINT("signal", ("Broadcasting COND_global_read_lock")); + pthread_cond_broadcast(&COND_global_read_lock); + } + pthread_mutex_unlock(&LOCK_global_read_lock); + thd->global_read_lock= 0; + DBUG_RETURN(1); + } } /* We DON'T set global_read_lock_blocks_commit now, it will be set after @@ -1476,6 +1198,8 @@ void unlock_global_read_lock(THD *thd) ("global_read_lock: %u global_read_lock_blocks_commit: %u", global_read_lock, global_read_lock_blocks_commit)); + mdl_release_global_shared_lock(&thd->mdl_context); + pthread_mutex_lock(&LOCK_global_read_lock); tmp= --global_read_lock; if (thd->global_read_lock == MADE_GLOBAL_READ_LOCK_BLOCK_COMMIT) diff --git a/sql/log_event.cc b/sql/log_event.cc index 3d35dec4fb0..fd0e20d690d 100644 --- a/sql/log_event.cc +++ b/sql/log_event.cc @@ -3036,7 +3036,7 @@ int Query_log_event::do_apply_event(Relay_log_info const *rli, } else { - const_cast<Relay_log_info*>(rli)->clear_tables_to_lock(); + const_cast<Relay_log_info*>(rli)->slave_close_thread_tables(thd); } /* @@ -7238,8 +7238,7 @@ int Rows_log_event::do_apply_event(Relay_log_info const *rli) */ DBUG_ASSERT(get_flags(STMT_END_F)); - const_cast<Relay_log_info*>(rli)->clear_tables_to_lock(); - close_thread_tables(thd); + const_cast<Relay_log_info*>(rli)->slave_close_thread_tables(thd); thd->clear_error(); DBUG_RETURN(0); } @@ -7322,7 +7321,7 @@ int Rows_log_event::do_apply_event(Relay_log_info const *rli) "unexpected success or fatal error")); thd->is_slave_error= 1; } - const_cast<Relay_log_info*>(rli)->clear_tables_to_lock(); + const_cast<Relay_log_info*>(rli)->slave_close_thread_tables(thd); DBUG_RETURN(actual_error); } @@ -7347,7 +7346,7 @@ int Rows_log_event::do_apply_event(Relay_log_info const *rli) mysql_unlock_tables(thd, thd->lock); thd->lock= 0; thd->is_slave_error= 1; - const_cast<Relay_log_info*>(rli)->clear_tables_to_lock(); + const_cast<Relay_log_info*>(rli)->slave_close_thread_tables(thd); DBUG_RETURN(ERR_BAD_TABLE_DEF); } } @@ -7530,12 +7529,6 @@ int Rows_log_event::do_apply_event(Relay_log_info const *rli) } } // if (table) - /* - We need to delay this clear until here bacause unpack_current_row() uses - master-side table definitions stored in rli. - */ - if (rli->tables_to_lock && get_flags(STMT_END_F)) - const_cast<Relay_log_info*>(rli)->clear_tables_to_lock(); if (error) { @@ -8064,7 +8057,8 @@ Table_map_log_event::~Table_map_log_event() int Table_map_log_event::do_apply_event(Relay_log_info const *rli) { RPL_TABLE_LIST *table_list; - char *db_mem, *tname_mem; + char *db_mem, *tname_mem, *mdlkey; + MDL_LOCK *mdl_lock; size_t dummy_len; void *memory; DBUG_ENTER("Table_map_log_event::do_apply_event(Relay_log_info*)"); @@ -8079,6 +8073,8 @@ int Table_map_log_event::do_apply_event(Relay_log_info const *rli) &table_list, (uint) sizeof(RPL_TABLE_LIST), &db_mem, (uint) NAME_LEN + 1, &tname_mem, (uint) NAME_LEN + 1, + &mdl_lock, sizeof(MDL_LOCK), + &mdlkey, MAX_DBKEY_LENGTH, NullS))) DBUG_RETURN(HA_ERR_OUT_OF_MEM); @@ -8091,6 +8087,8 @@ int Table_map_log_event::do_apply_event(Relay_log_info const *rli) table_list->updating= 1; strmov(table_list->db, rpl_filter->get_rewrite_db(m_dbnam, &dummy_len)); strmov(table_list->table_name, m_tblnam); + mdl_init_lock(mdl_lock, mdlkey, 0, table_list->db, table_list->table_name); + table_list->mdl_lock= mdl_lock; int error= 0; diff --git a/sql/log_event_old.cc b/sql/log_event_old.cc index fbcbb388236..030d51b3618 100644 --- a/sql/log_event_old.cc +++ b/sql/log_event_old.cc @@ -32,8 +32,7 @@ Old_rows_log_event::do_apply_event(Old_rows_log_event *ev, const Relay_log_info */ DBUG_ASSERT(ev->get_flags(Old_rows_log_event::STMT_END_F)); - const_cast<Relay_log_info*>(rli)->clear_tables_to_lock(); - close_thread_tables(thd); + const_cast<Relay_log_info*>(rli)->slave_close_thread_tables(thd); thd->clear_error(); DBUG_RETURN(0); } @@ -91,7 +90,7 @@ Old_rows_log_event::do_apply_event(Old_rows_log_event *ev, const Relay_log_info "unexpected success or fatal error")); thd->is_slave_error= 1; } - const_cast<Relay_log_info*>(rli)->clear_tables_to_lock(); + const_cast<Relay_log_info*>(rli)->slave_close_thread_tables(thd); DBUG_RETURN(actual_error); } @@ -109,10 +108,8 @@ Old_rows_log_event::do_apply_event(Old_rows_log_event *ev, const Relay_log_info { if (ptr->m_tabledef.compatible_with(rli, ptr->table)) { - mysql_unlock_tables(thd, thd->lock); - thd->lock= 0; thd->is_slave_error= 1; - const_cast<Relay_log_info*>(rli)->clear_tables_to_lock(); + const_cast<Relay_log_info*>(rli)->slave_close_thread_tables(thd); DBUG_RETURN(Old_rows_log_event::ERR_BAD_TABLE_DEF); } } @@ -236,13 +233,6 @@ Old_rows_log_event::do_apply_event(Old_rows_log_event *ev, const Relay_log_info } } - /* - We need to delay this clear until the table def is no longer needed. - The table def is needed in unpack_row(). - */ - if (rli->tables_to_lock && ev->get_flags(Old_rows_log_event::STMT_END_F)) - const_cast<Relay_log_info*>(rli)->clear_tables_to_lock(); - if (error) { /* error has occured during the transaction */ rli->report(ERROR_LEVEL, thd->stmt_da->sql_errno(), @@ -1440,8 +1430,7 @@ int Old_rows_log_event::do_apply_event(Relay_log_info const *rli) */ DBUG_ASSERT(get_flags(STMT_END_F)); - const_cast<Relay_log_info*>(rli)->clear_tables_to_lock(); - close_thread_tables(thd); + const_cast<Relay_log_info*>(rli)->slave_close_thread_tables(thd); thd->clear_error(); DBUG_RETURN(0); } @@ -1496,7 +1485,7 @@ int Old_rows_log_event::do_apply_event(Relay_log_info const *rli) "Error in %s event: when locking tables", get_type_str()); } - const_cast<Relay_log_info*>(rli)->clear_tables_to_lock(); + const_cast<Relay_log_info*>(rli)->slave_close_thread_tables(thd); DBUG_RETURN(error); } @@ -1515,7 +1504,7 @@ int Old_rows_log_event::do_apply_event(Relay_log_info const *rli) */ thd->binlog_flush_pending_rows_event(false); TABLE_LIST *tables= rli->tables_to_lock; - close_tables_for_reopen(thd, &tables); + close_tables_for_reopen(thd, &tables, FALSE); uint tables_count= rli->tables_to_lock_count; if ((error= open_tables(thd, &tables, &tables_count, 0))) @@ -1533,7 +1522,7 @@ int Old_rows_log_event::do_apply_event(Relay_log_info const *rli) "unexpected success or fatal error")); thd->is_slave_error= 1; } - const_cast<Relay_log_info*>(rli)->clear_tables_to_lock(); + const_cast<Relay_log_info*>(rli)->slave_close_thread_tables(thd); DBUG_RETURN(error); } } @@ -1552,10 +1541,8 @@ int Old_rows_log_event::do_apply_event(Relay_log_info const *rli) { if (ptr->m_tabledef.compatible_with(rli, ptr->table)) { - mysql_unlock_tables(thd, thd->lock); - thd->lock= 0; thd->is_slave_error= 1; - const_cast<Relay_log_info*>(rli)->clear_tables_to_lock(); + const_cast<Relay_log_info*>(rli)->slave_close_thread_tables(thd); DBUG_RETURN(ERR_BAD_TABLE_DEF); } } @@ -1726,13 +1713,6 @@ int Old_rows_log_event::do_apply_event(Relay_log_info const *rli) } } // if (table) - /* - We need to delay this clear until here bacause unpack_current_row() uses - master-side table definitions stored in rli. - */ - if (rli->tables_to_lock && get_flags(STMT_END_F)) - const_cast<Relay_log_info*>(rli)->clear_tables_to_lock(); - if (error) { /* error has occured during the transaction */ rli->report(ERROR_LEVEL, thd->net.last_errno, diff --git a/sql/mdl.cc b/sql/mdl.cc new file mode 100644 index 00000000000..00dcc12cdc8 --- /dev/null +++ b/sql/mdl.cc @@ -0,0 +1,1342 @@ +/* Copyright (C) 2007-2008 MySQL AB + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; version 2 of the License. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ + + + +/* + TODO: Remove this dependency on mysql_priv.h. It's not + trivial step at the moment since currently we access to + some of THD members and use some of its methods here. +*/ +#include "mysql_priv.h" +#include "mdl.h" + + +/** + The lock context. Created internally for an acquired lock. + For a given name, there exists only one MDL_LOCK_DATA instance, + and it exists only when the lock has been granted. + Can be seen as an MDL subsystem's version of TABLE_SHARE. +*/ + +struct MDL_LOCK_DATA +{ + I_P_List<MDL_LOCK, MDL_LOCK_lock> active_shared; + /* + There can be several upgraders and active exclusive + belonging to the same context. + */ + I_P_List<MDL_LOCK, MDL_LOCK_lock> active_shared_waiting_upgrade; + I_P_List<MDL_LOCK, MDL_LOCK_lock> active_exclusive; + I_P_List<MDL_LOCK, MDL_LOCK_lock> waiting_exclusive; + uint users; + void *cached_object; + mdl_cached_object_release_hook cached_object_release_hook; + + MDL_LOCK_DATA() : cached_object(0), cached_object_release_hook(0) {} + + MDL_LOCK *get_key_owner() + { + return !active_shared.is_empty() ? + active_shared.head() : + (!active_shared_waiting_upgrade.is_empty() ? + active_shared_waiting_upgrade.head() : + (!active_exclusive.is_empty() ? + active_exclusive.head() : waiting_exclusive.head())); + } + + bool has_no_other_users() + { + return (users == 1); + } +}; + + +pthread_mutex_t LOCK_mdl; +pthread_cond_t COND_mdl; +HASH mdl_locks; +uint global_shared_locks_pending; +uint global_shared_locks_acquired; +uint global_intention_exclusive_locks_acquired; + + + +extern "C" uchar *mdl_locks_key(const uchar *record, size_t *length, + my_bool not_used __attribute__((unused))) +{ + MDL_LOCK_DATA *entry=(MDL_LOCK_DATA*) record; + *length= entry->get_key_owner()->key_length; + return (uchar*) entry->get_key_owner()->key; +} + + +/** + Initialize the metadata locking subsystem. + + This function is called at server startup. + + In particular, initializes the new global mutex and + the associated condition variable: LOCK_mdl and COND_mdl. + These locking primitives are implementation details of the MDL + subsystem and are private to it. + + Note, that even though the new implementation adds acquisition + of a new global mutex to the execution flow of almost every SQL + statement, the design capitalizes on that to later save on + look ups in the table definition cache. This leads to reduced + contention overall and on LOCK_open in particular. + Please see the description of mdl_acquire_shared_lock() for details. +*/ + +void mdl_init() +{ + pthread_mutex_init(&LOCK_mdl, NULL); + pthread_cond_init(&COND_mdl, NULL); + my_hash_init(&mdl_locks, &my_charset_bin, 16 /* FIXME */, 0, 0, + mdl_locks_key, 0, 0); + global_shared_locks_pending= global_shared_locks_acquired= 0; + global_intention_exclusive_locks_acquired= 0; +} + + +/** + Release resources of metadata locking subsystem. + + Destroys the global mutex and the condition variable. + Called at server shutdown. +*/ + +void mdl_destroy() +{ + DBUG_ASSERT(!mdl_locks.records); + pthread_mutex_destroy(&LOCK_mdl); + pthread_cond_destroy(&COND_mdl); + my_hash_free(&mdl_locks); +} + + +/** + Initialize a metadata locking context. + + This is to be called when a new server connection is created. +*/ + +void mdl_context_init(MDL_CONTEXT *context, THD *thd) +{ + context->locks.empty(); + context->thd= thd; + context->has_global_shared_lock= FALSE; +} + + +/** + Destroy metadata locking context. + + Assumes and asserts that there are no active or pending locks + associated with this context at the time of the destruction. + + Currently does nothing. Asserts that there are no pending + or satisfied lock requests. The pending locks must be released + prior to destruction. This is a new way to express the assertion + that all tables are closed before a connection is destroyed. +*/ + +void mdl_context_destroy(MDL_CONTEXT *context) +{ + DBUG_ASSERT(context->locks.is_empty()); + DBUG_ASSERT(!context->has_global_shared_lock); +} + + +/** + Backup and reset state of meta-data locking context. + + mdl_context_backup_and_reset(), mdl_context_restore() and + mdl_context_merge() are used by HANDLER implementation which + needs to open table for new HANDLER independently of already + open HANDLERs and add this table/metadata lock to the set of + tables open/metadata locks for HANDLERs afterwards. +*/ + +void mdl_context_backup_and_reset(MDL_CONTEXT *ctx, MDL_CONTEXT *backup) +{ + backup->locks.empty(); + ctx->locks.swap(backup->locks); +} + + +/** + Restore state of meta-data locking context from backup. +*/ + +void mdl_context_restore(MDL_CONTEXT *ctx, MDL_CONTEXT *backup) +{ + DBUG_ASSERT(ctx->locks.is_empty()); + ctx->locks.swap(backup->locks); +} + + +/** + Merge meta-data locks from one context into another. +*/ + +void mdl_context_merge(MDL_CONTEXT *dst, MDL_CONTEXT *src) +{ + MDL_LOCK *l; + + DBUG_ASSERT(dst->thd == src->thd); + + if (!src->locks.is_empty()) + { + I_P_List_iterator<MDL_LOCK, MDL_LOCK_context> it(src->locks); + while ((l= it++)) + { + DBUG_ASSERT(l->ctx); + l->ctx= dst; + dst->locks.push_front(l); + } + src->locks.empty(); + } +} + + +/** + Initialize a lock request. + + This is to be used for every lock request. + + Note that initialization and allocation are split + into two calls. This is to allow flexible memory management + of lock requests. Normally a lock request is stored + in statement memory (e.g. is a member of struct TABLE_LIST), + but we would also like to allow allocation of lock + requests in other memory roots, for example in the grant + subsystem, to lock privilege tables. + + The MDL subsystem does not own or manage memory of lock + requests. Instead it assumes that the life time of every lock + request encloses calls to mdl_acquire_shared_lock() and + mdl_release_locks(). + + @param mdl Pointer to an MDL_LOCK object to initialize + @param key_buff Pointer to the buffer for key for the lock request + (should be at least strlen(db) + strlen(name) + + 2 bytes, or, if the lengths are not known, MAX_DBNAME_LENGTH) + @param type Id of type of object to be locked + @param db Name of database to which the object belongs + @param name Name of of the object + + Stores the database name, object name and the type in the key + buffer. Initializes mdl_el to point to the key. + We can't simply initialize mdl_el with type, db and name + by-pointer because of the underlying HASH implementation + requires the key to be a contiguous buffer. + + The initialized lock request will have MDL_SHARED type and + normal priority. + + Suggested lock types: TABLE - 0 PROCEDURE - 1 FUNCTION - 2 + Note that tables and views have the same lock type, since + they share the same name space in the SQL standard. +*/ + +void mdl_init_lock(MDL_LOCK *mdl, char *key, int type, const char *db, + const char *name) +{ + int4store(key, type); + mdl->key_length= (uint) (strmov(strmov(key+4, db)+1, name)-key)+1; + mdl->key= key; + mdl->type= MDL_SHARED; + mdl->state= MDL_PENDING; + mdl->prio= MDL_NORMAL_PRIO; + mdl->upgradable= FALSE; +#ifndef DBUG_OFF + mdl->ctx= 0; + mdl->lock_data= 0; +#endif +} + + +/** + Allocate and initialize one lock request. + + Same as mdl_init_lock(), but allocates the lock and the key buffer + on a memory root. Necessary to lock ad-hoc tables, e.g. + mysql.* tables of grant and data dictionary subsystems. + + @param type Id of type of object to be locked + @param db Name of database to which object belongs + @param name Name of of object + @param root MEM_ROOT on which object should be allocated + + @note The allocated lock request will have MDL_SHARED type and + normal priority. + + @retval 0 Error + @retval non-0 Pointer to an object representing a lock request +*/ + +MDL_LOCK *mdl_alloc_lock(int type, const char *db, const char *name, + MEM_ROOT *root) +{ + MDL_LOCK *lock; + char *key; + + if (!multi_alloc_root(root, &lock, sizeof(MDL_LOCK), &key, + MAX_DBKEY_LENGTH, NULL)) + return NULL; + + mdl_init_lock(lock, key, type, db, name); + + return lock; +} + + +/** + Add a lock request to the list of lock requests of the context. + + The procedure to acquire metadata locks is: + - allocate and initialize lock requests (mdl_alloc_lock()) + - associate them with a context (mdl_add_lock()) + - call mdl_acquire_shared_lock()/mdl_release_lock() (maybe repeatedly). + + Associates a lock request with the given context. + + @param context The MDL context to associate the lock with. + There should be no more than one context per + connection, to avoid deadlocks. + @param lock The lock request to be added. +*/ + +void mdl_add_lock(MDL_CONTEXT *context, MDL_LOCK *lock) +{ + DBUG_ENTER("mdl_add_lock"); + DBUG_ASSERT(lock->state == MDL_PENDING); + DBUG_ASSERT(!lock->ctx); + lock->ctx= context; + context->locks.push_front(lock); + DBUG_VOID_RETURN; +} + + +/** + Clear all lock requests in the context (clear the context). + + Disassociates lock requests from the context. + All granted locks must be released prior to calling this + function. + + In other words, the expected procedure to release locks is: + - mdl_release_locks(); + - mdl_remove_all_locks(); + + We could possibly merge mdl_remove_all_locks() and mdl_release_locks(), + but this function comes in handy when we need to back off: in that case + we release all the locks acquired so-far but do not free them, since + we know that the respective lock requests will be used again. + + Also resets lock requests back to their initial state (i.e. + sets type and priority to MDL_SHARED and MDL_NORMAL_PRIO). + + @param context Context to be cleared. +*/ + +void mdl_remove_all_locks(MDL_CONTEXT *context) +{ + MDL_LOCK *l; + I_P_List_iterator<MDL_LOCK, MDL_LOCK_context> it(context->locks); + while ((l= it++)) + { + /* Reset lock request back to its initial state. */ + l->type= MDL_SHARED; + l->prio= MDL_NORMAL_PRIO; + l->upgradable= FALSE; +#ifndef DBUG_OFF + l->ctx= 0; +#endif + } + context->locks.empty(); +} + + +/** + Auxiliary functions needed for creation/destruction of MDL_LOCK_DATA + objects. + + @todo This naive implementation should be replaced with one that saves + on memory allocation by reusing released objects. +*/ + +static MDL_LOCK_DATA* get_lock_data_object(void) +{ + return new MDL_LOCK_DATA(); +} + + +static void release_lock_data_object(MDL_LOCK_DATA *lock) +{ + delete lock; +} + + +/** + Try to acquire one shared lock. + + Unlike exclusive locks, shared locks are acquired one by + one. This is interface is chosen to simplify introduction of + the new locking API to the system. mdl_acquire_shared_lock() + is currently used from open_table(), and there we have only one + table to work with. + + In future we may consider allocating multiple shared locks at once. + + This function must be called after the lock is added to a context. + + @param lock [in] Lock request object for lock to be acquired + @param retry [out] Indicates that conflicting lock exists and another + attempt should be made after releasing all current + locks and waiting for conflicting lock go away + (using mdl_wait_for_locks()). + + @retval FALSE Success. + @retval TRUE Failure. Either error occured or conflicting lock exists. + In the latter case "retry" parameter is set to TRUE. +*/ + +bool mdl_acquire_shared_lock(MDL_LOCK *l, bool *retry) +{ + MDL_LOCK_DATA *lock_data; + *retry= FALSE; + + DBUG_ASSERT(l->type == MDL_SHARED && l->state == MDL_PENDING); + + safe_mutex_assert_not_owner(&LOCK_open); + + if (l->ctx->has_global_shared_lock && l->upgradable) + { + my_error(ER_CANT_UPDATE_WITH_READLOCK, MYF(0)); + return TRUE; + } + + pthread_mutex_lock(&LOCK_mdl); + + if (l->upgradable && + (global_shared_locks_acquired || global_shared_locks_pending)) + { + pthread_mutex_unlock(&LOCK_mdl); + *retry= TRUE; + return TRUE; + } + + if (!(lock_data= (MDL_LOCK_DATA *)my_hash_search(&mdl_locks, (uchar*)l->key, + l->key_length))) + { + lock_data= get_lock_data_object(); + lock_data->active_shared.push_front(l); + lock_data->users= 1; + my_hash_insert(&mdl_locks, (uchar*)lock_data); + l->state= MDL_ACQUIRED; + l->lock_data= lock_data; + if (l->upgradable) + global_intention_exclusive_locks_acquired++; + } + else + { + if ((lock_data->active_exclusive.is_empty() && + (l->prio == MDL_HIGH_PRIO || + lock_data->waiting_exclusive.is_empty() && + lock_data->active_shared_waiting_upgrade.is_empty())) || + (!lock_data->active_exclusive.is_empty() && + lock_data->active_exclusive.head()->ctx == l->ctx)) + { + /* + When exclusive lock comes from the same context we can satisfy our + shared lock. This is required for CREATE TABLE ... SELECT ... and + ALTER VIEW ... AS .... + */ + lock_data->active_shared.push_front(l); + lock_data->users++; + l->state= MDL_ACQUIRED; + l->lock_data= lock_data; + if (l->upgradable) + global_intention_exclusive_locks_acquired++; + } + else + *retry= TRUE; + } + pthread_mutex_unlock(&LOCK_mdl); + + return *retry; +} + + +static void release_lock(MDL_LOCK *l); + + +/** + Acquire exclusive locks. The context must contain the list of + locks to be acquired. There must be no granted locks in the + context. + + This is a replacement of lock_table_names(). It is used in + RENAME, DROP and other DDL SQL statements. + + @param context A context containing requests for exclusive locks + The context may not have other lock requests. + + @note In case of failure (for example, if our thread was killed) + resets lock requests back to their initial state (MDL_SHARED + and MDL_NORMAL_PRIO). + + @retval FALSE Success + @retval TRUE Failure +*/ + +bool mdl_acquire_exclusive_locks(MDL_CONTEXT *context) +{ + MDL_LOCK *l, *lh; + MDL_LOCK_DATA *lock_data; + bool signalled= FALSE; + const char *old_msg; + I_P_List_iterator<MDL_LOCK, MDL_LOCK_context> it(context->locks); + THD *thd= context->thd; + + DBUG_ASSERT(thd == current_thd); + + safe_mutex_assert_not_owner(&LOCK_open); + + if (context->has_global_shared_lock) + { + my_error(ER_CANT_UPDATE_WITH_READLOCK, MYF(0)); + return TRUE; + } + + pthread_mutex_lock(&LOCK_mdl); + + old_msg= thd->enter_cond(&COND_mdl, &LOCK_mdl, "Waiting for table"); + + while ((l= it++)) + { + DBUG_ASSERT(l->type == MDL_EXCLUSIVE && l->state == MDL_PENDING); + if (!(lock_data= (MDL_LOCK_DATA *)my_hash_search(&mdl_locks, (uchar*)l->key, + l->key_length))) + { + lock_data= get_lock_data_object(); + lock_data->waiting_exclusive.push_front(l); + lock_data->users= 1; + my_hash_insert(&mdl_locks, (uchar*)lock_data); + l->lock_data= lock_data; + } + else + { + lock_data->waiting_exclusive.push_front(l); + lock_data->users++; + l->lock_data= lock_data; + } + } + + while (1) + { + it.rewind(); + while ((l= it++)) + { + lock_data= l->lock_data; + + if (global_shared_locks_acquired || global_shared_locks_pending) + { + /* + There is active or pending global shared lock we have + to wait until it goes away. + */ + signalled= TRUE; + break; + } + else if (!lock_data->active_exclusive.is_empty() || + !lock_data->active_shared_waiting_upgrade.is_empty()) + { + /* + Exclusive MDL owner won't wait on table-level lock the same + applies to shared lock waiting upgrade (in this cases we already + have some table-level lock). + */ + signalled= TRUE; + break; + } + else if ((lh= lock_data->active_shared.head())) + { + signalled= notify_thread_having_shared_lock(thd, lh->ctx->thd); + break; + } + } + if (!l) + break; + if (signalled) + pthread_cond_wait(&COND_mdl, &LOCK_mdl); + else + { + /* + Another thread obtained shared MDL-lock on some table but + has not yet opened it and/or tried to obtain data lock on + it. In this case we need to wait until this happens and try + to abort this thread once again. + */ + struct timespec abstime; + set_timespec(abstime, 10); + pthread_cond_timedwait(&COND_mdl, &LOCK_mdl, &abstime); + } + if (thd->killed) + { + /* Remove our pending lock requests from the locks. */ + it.rewind(); + while ((l= it++)) + { + DBUG_ASSERT(l->type == MDL_EXCLUSIVE && l->state == MDL_PENDING); + release_lock(l); + /* Return lock request to its initial state. */ + l->type= MDL_SHARED; + l->prio= MDL_NORMAL_PRIO; + l->upgradable= FALSE; + context->locks.remove(l); + } + /* Pending requests for shared locks can be satisfied now. */ + pthread_cond_broadcast(&COND_mdl); + thd->exit_cond(old_msg); + return TRUE; + } + } + it.rewind(); + while ((l= it++)) + { + global_intention_exclusive_locks_acquired++; + lock_data= l->lock_data; + lock_data->waiting_exclusive.remove(l); + lock_data->active_exclusive.push_front(l); + l->state= MDL_ACQUIRED; + if (lock_data->cached_object) + (*lock_data->cached_object_release_hook)(lock_data->cached_object); + lock_data->cached_object= NULL; + } + /* As a side-effect THD::exit_cond() unlocks LOCK_mdl. */ + thd->exit_cond(old_msg); + return FALSE; +} + + +/** + Upgrade a shared metadata lock to exclusive. + + Used in ALTER TABLE, when a copy of the table with the + new definition has been constructed. + + @param context Context to which shared long belongs + @param type Id of object type + @param db Name of the database + @param name Name of the object + + @note In case of failure to upgrade locks (e.g. because upgrader + was killed) leaves locks in their original state (locked + in shared mode). + + @retval FALSE Success + @retval TRUE Failure (thread was killed) +*/ + +bool mdl_upgrade_shared_lock_to_exclusive(MDL_CONTEXT *context, int type, + const char *db, const char *name) +{ + char key[MAX_DBKEY_LENGTH]; + uint key_length; + bool signalled= FALSE; + MDL_LOCK *l, *lh; + MDL_LOCK_DATA *lock_data; + I_P_List_iterator<MDL_LOCK, MDL_LOCK_context> it(context->locks); + const char *old_msg; + THD *thd= context->thd; + + DBUG_ENTER("mdl_upgrade_shared_lock_to_exclusive"); + DBUG_PRINT("enter", ("db=%s name=%s", db, name)); + + DBUG_ASSERT(thd == current_thd); + + int4store(key, type); + key_length= (uint) (strmov(strmov(key+4, db)+1, name)-key)+1; + + safe_mutex_assert_not_owner(&LOCK_open); + + pthread_mutex_lock(&LOCK_mdl); + + old_msg= thd->enter_cond(&COND_mdl, &LOCK_mdl, "Waiting for table"); + + while ((l= it++)) + if (l->key_length == key_length && !memcmp(l->key, key, key_length) && + l->type == MDL_SHARED) + { + DBUG_PRINT("info", ("found shared lock for upgrade")); + DBUG_ASSERT(l->state == MDL_ACQUIRED); + DBUG_ASSERT(l->upgradable); + l->state= MDL_PENDING_UPGRADE; + lock_data= l->lock_data; + lock_data->active_shared.remove(l); + lock_data->active_shared_waiting_upgrade.push_front(l); + } + + while (1) + { + DBUG_PRINT("info", ("looking at conflicting locks")); + it.rewind(); + while ((l= it++)) + { + if (l->state == MDL_PENDING_UPGRADE) + { + DBUG_ASSERT(l->type == MDL_SHARED); + + lock_data= l->lock_data; + + DBUG_ASSERT(global_shared_locks_acquired == 0 && + global_intention_exclusive_locks_acquired); + + if ((lh= lock_data->active_shared.head())) + { + DBUG_PRINT("info", ("found active shared locks")); + signalled= notify_thread_having_shared_lock(thd, lh->ctx->thd); + break; + } + else if (!lock_data->active_exclusive.is_empty()) + { + DBUG_PRINT("info", ("found active exclusive locks")); + signalled= TRUE; + break; + } + } + } + if (!l) + break; + if (signalled) + pthread_cond_wait(&COND_mdl, &LOCK_mdl); + else + { + /* + Another thread obtained shared MDL-lock on some table but + has not yet opened it and/or tried to obtain data lock on + it. In this case we need to wait until this happens and try + to abort this thread once again. + */ + struct timespec abstime; + set_timespec(abstime, 10); + DBUG_PRINT("info", ("Failed to wake-up from table-level lock ... sleeping")); + pthread_cond_timedwait(&COND_mdl, &LOCK_mdl, &abstime); + } + if (thd->killed) + { + it.rewind(); + while ((l= it++)) + if (l->state == MDL_PENDING_UPGRADE) + { + DBUG_ASSERT(l->type == MDL_SHARED); + l->state= MDL_ACQUIRED; + lock_data= l->lock_data; + lock_data->active_shared_waiting_upgrade.remove(l); + lock_data->active_shared.push_front(l); + } + /* Pending requests for shared locks can be satisfied now. */ + pthread_cond_broadcast(&COND_mdl); + thd->exit_cond(old_msg); + DBUG_RETURN(TRUE); + } + } + + it.rewind(); + while ((l= it++)) + if (l->state == MDL_PENDING_UPGRADE) + { + DBUG_ASSERT(l->type == MDL_SHARED); + lock_data= l->lock_data; + lock_data->active_shared_waiting_upgrade.remove(l); + lock_data->active_exclusive.push_front(l); + l->type= MDL_EXCLUSIVE; + l->state= MDL_ACQUIRED; + if (lock_data->cached_object) + (*lock_data->cached_object_release_hook)(lock_data->cached_object); + lock_data->cached_object= 0; + } + + /* As a side-effect THD::exit_cond() unlocks LOCK_mdl. */ + thd->exit_cond(old_msg); + DBUG_RETURN(FALSE); +} + + +/** + Try to acquire an exclusive lock on the object if there are + no conflicting locks. + + Similar to the previous function, but returns + immediately without any side effect if encounters a lock + conflict. Otherwise takes the lock. + + This function is used in CREATE TABLE ... LIKE to acquire a lock + on the table to be created. In this statement we don't want to + block and wait for the lock if the table already exists. + + @param context [in] The context containing the lock request + @param lock [in] The lock request + + @retval FALSE the lock was granted + @retval TRUE there were conflicting locks. + + FIXME: Compared to lock_table_name_if_not_cached() + it gives sligthly more false negatives. +*/ + +bool mdl_try_acquire_exclusive_lock(MDL_CONTEXT *context, MDL_LOCK *l) +{ + MDL_LOCK_DATA *lock_data; + + DBUG_ASSERT(l->type == MDL_EXCLUSIVE && l->state == MDL_PENDING); + + safe_mutex_assert_not_owner(&LOCK_open); + + pthread_mutex_lock(&LOCK_mdl); + + if (!(lock_data= (MDL_LOCK_DATA *)my_hash_search(&mdl_locks, (uchar*)l->key, + l->key_length))) + { + lock_data= get_lock_data_object(); + lock_data->active_exclusive.push_front(l); + lock_data->users= 1; + my_hash_insert(&mdl_locks, (uchar*)lock_data); + l->state= MDL_ACQUIRED; + l->lock_data= lock_data; + lock_data= 0; + global_intention_exclusive_locks_acquired++; + } + pthread_mutex_unlock(&LOCK_mdl); + + /* + FIXME: We can't leave pending MDL_EXCLUSIVE lock request in the list since + for such locks we assume that they have MDL_LOCK::lock properly set. + Long term we should clearly define relation between lock types, + presence in the context lists and MDL_LOCK::lock values. + */ + if (lock_data) + context->locks.remove(l); + + return lock_data; +} + + +/** + Acquire global shared metadata lock. + + Holding this lock will block all requests for exclusive locks + and shared locks which can be potentially upgraded to exclusive + (see MDL_LOCK::upgradable). + + @param context Current metadata locking context. + + @retval FALSE Success -- the lock was granted. + @retval TRUE Failure -- our thread was killed. +*/ + +bool mdl_acquire_global_shared_lock(MDL_CONTEXT *context) +{ + THD *thd= context->thd; + const char *old_msg; + + safe_mutex_assert_not_owner(&LOCK_open); + DBUG_ASSERT(thd == current_thd); + DBUG_ASSERT(!context->has_global_shared_lock); + + pthread_mutex_lock(&LOCK_mdl); + + global_shared_locks_pending++; + old_msg= thd->enter_cond(&COND_mdl, &LOCK_mdl, "Waiting for table"); + + while (!thd->killed && global_intention_exclusive_locks_acquired) + pthread_cond_wait(&COND_mdl, &LOCK_mdl); + + global_shared_locks_pending--; + if (thd->killed) + { + /* As a side-effect THD::exit_cond() unlocks LOCK_mdl. */ + thd->exit_cond(old_msg); + return TRUE; + } + global_shared_locks_acquired++; + context->has_global_shared_lock= TRUE; + /* As a side-effect THD::exit_cond() unlocks LOCK_mdl. */ + thd->exit_cond(old_msg); + return FALSE; +} + + +/** + Wait until there will be no locks that conflict with lock requests + in the context. + + This is a part of the locking protocol and must be used by the + acquirer of shared locks after a back-off. + + Does not acquire the locks! + + @param context Context with which lock requests are associated. + + @retval FALSE Success. One can try to obtain metadata locks. + @retval TRUE Failure (thread was killed) +*/ + +bool mdl_wait_for_locks(MDL_CONTEXT *context) +{ + MDL_LOCK *l; + MDL_LOCK_DATA *lock_data; + I_P_List_iterator<MDL_LOCK, MDL_LOCK_context> it(context->locks); + const char *old_msg; + THD *thd= context->thd; + + safe_mutex_assert_not_owner(&LOCK_open); + DBUG_ASSERT(thd == current_thd); + + while (!thd->killed) + { + /* + We have to check if there are some HANDLERs open by this thread + which conflict with some pending exclusive locks. Otherwise we + might have a deadlock in situations when we are waiting for + pending writer to go away, which in its turn waits for HANDLER + open by our thread. + + TODO: investigate situations in which we need to broadcast on + COND_mdl because of above scenario. + */ + mysql_ha_flush(context->thd); + pthread_mutex_lock(&LOCK_mdl); + old_msg= thd->enter_cond(&COND_mdl, &LOCK_mdl, "Waiting for table"); + it.rewind(); + while ((l= it++)) + { + DBUG_ASSERT(l->state == MDL_PENDING); + if ((l->upgradable || l->type == MDL_EXCLUSIVE) && + (global_shared_locks_acquired || global_shared_locks_pending)) + break; + /* + To avoid starvation we don't wait if we have pending MDL_EXCLUSIVE lock. + */ + if (l->type == MDL_SHARED && + (lock_data= (MDL_LOCK_DATA *)my_hash_search(&mdl_locks, (uchar*)l->key, + l->key_length)) && + !(lock_data->active_exclusive.is_empty() && + lock_data->active_shared_waiting_upgrade.is_empty() && + lock_data->waiting_exclusive.is_empty())) + break; + } + if (!l) + { + pthread_mutex_unlock(&LOCK_mdl); + break; + } + pthread_cond_wait(&COND_mdl, &LOCK_mdl); + /* As a side-effect THD::exit_cond() unlocks LOCK_mdl. */ + thd->exit_cond(old_msg); + } + return thd->killed; +} + + +/** + Auxiliary function which allows to release particular lock + ownership of which is represented by lock request object. +*/ + +static void release_lock(MDL_LOCK *l) +{ + MDL_LOCK_DATA *lock_data; + + DBUG_ENTER("release_lock"); + DBUG_PRINT("enter", ("db=%s name=%s", l->key + 4, + l->key + 4 + strlen(l->key + 4) + 1)); + + lock_data= l->lock_data; + if (lock_data->has_no_other_users()) + { + my_hash_delete(&mdl_locks, (uchar *)lock_data); + DBUG_PRINT("info", ("releasing cached_object cached_object=%p", + lock_data->cached_object)); + if (lock_data->cached_object) + (*lock_data->cached_object_release_hook)(lock_data->cached_object); + release_lock_data_object(lock_data); + if (l->type == MDL_EXCLUSIVE && l->state == MDL_ACQUIRED || + l->type == MDL_SHARED && l->state == MDL_ACQUIRED && l->upgradable) + global_intention_exclusive_locks_acquired--; + } + else + { + switch (l->type) + { + case MDL_SHARED: + lock_data->active_shared.remove(l); + if (l->upgradable) + global_intention_exclusive_locks_acquired--; + break; + case MDL_EXCLUSIVE: + if (l->state == MDL_PENDING) + lock_data->waiting_exclusive.remove(l); + else + { + lock_data->active_exclusive.remove(l); + global_intention_exclusive_locks_acquired--; + } + break; + default: + /* TODO Really? How about problems during lock upgrade ? */ + DBUG_ASSERT(0); + } + lock_data->users--; + } + + DBUG_VOID_RETURN; +} + + +/** + Release all locks associated with the context, but leave them + in the context as lock requests. + + This function is used to back off in case of a lock conflict. + It is also used to release shared locks in the end of an SQL + statement. + + @param context The context with which the locks to be released + are associated. +*/ + +void mdl_release_locks(MDL_CONTEXT *context) +{ + MDL_LOCK *l; + I_P_List_iterator<MDL_LOCK, MDL_LOCK_context> it(context->locks); + DBUG_ENTER("mdl_release_locks"); + + safe_mutex_assert_not_owner(&LOCK_open); + + pthread_mutex_lock(&LOCK_mdl); + while ((l= it++)) + { + DBUG_PRINT("info", ("found lock to release l=%p", l)); + /* + We should not release locks which pending shared locks as these + are not associated with lock object and don't present in its + lists. Allows us to avoid problems in open_tables() in case of + back-off + */ + if (!(l->type == MDL_SHARED && l->state == MDL_PENDING)) + { + release_lock(l); + l->state= MDL_PENDING; +#ifndef DBUG_OFF + l->lock_data= 0; +#endif + } + /* + We will return lock request to its initial state only in + mdl_remove_all_locks() since we need to know type of lock + request and if it is upgradable in mdl_wait_for_locks(). + */ + } + /* Inefficient but will do for a while */ + pthread_cond_broadcast(&COND_mdl); + pthread_mutex_unlock(&LOCK_mdl); + DBUG_VOID_RETURN; +} + + +/** + Release all exclusive locks associated with context. + Removes the locks from the context. + + @param context Context with exclusive locks. + + @note Shared locks are left intact. + @note Resets lock requests for locks released back to their + initial state (i.e.sets type and priority to MDL_SHARED + and MDL_NORMAL_PRIO). +*/ + +void mdl_release_exclusive_locks(MDL_CONTEXT *context) +{ + MDL_LOCK *l; + I_P_List_iterator<MDL_LOCK, MDL_LOCK_context> it(context->locks); + + safe_mutex_assert_not_owner(&LOCK_open); + + pthread_mutex_lock(&LOCK_mdl); + while ((l= it++)) + { + if (l->type == MDL_EXCLUSIVE) + { + DBUG_ASSERT(l->state == MDL_ACQUIRED); + release_lock(l); +#ifndef DBUG_OFF + l->ctx= 0; + l->lock_data= 0; +#endif + l->state= MDL_PENDING; + /* Return lock request to its initial state. */ + l->type= MDL_SHARED; + l->prio= MDL_NORMAL_PRIO; + l->upgradable= FALSE; + context->locks.remove(l); + } + } + pthread_cond_broadcast(&COND_mdl); + pthread_mutex_unlock(&LOCK_mdl); +} + + +/** + Release a lock. + Removes the lock from the context. + + @param context Context containing lock in question + @param lock Lock to be released + + @note Resets lock request for lock released back to its initial state + (i.e.sets type and priority to MDL_SHARED and MDL_NORMAL_PRIO). +*/ + +void mdl_release_lock(MDL_CONTEXT *context, MDL_LOCK *lr) +{ + safe_mutex_assert_not_owner(&LOCK_open); + + pthread_mutex_lock(&LOCK_mdl); + release_lock(lr); +#ifndef DBUG_OFF + lr->ctx= 0; + lr->lock_data= 0; +#endif + lr->state= MDL_PENDING; + /* Return lock request to its initial state. */ + lr->type= MDL_SHARED; + lr->prio= MDL_NORMAL_PRIO; + lr->upgradable= FALSE; + context->locks.remove(lr); + pthread_cond_broadcast(&COND_mdl); + pthread_mutex_unlock(&LOCK_mdl); +} + + +/** + Downgrade all exclusive locks in the context to + shared. + + @param context A context with exclusive locks. +*/ + +void mdl_downgrade_exclusive_locks(MDL_CONTEXT *context) +{ + MDL_LOCK *l; + MDL_LOCK_DATA *lock_data; + I_P_List_iterator<MDL_LOCK, MDL_LOCK_context> it(context->locks); + + safe_mutex_assert_not_owner(&LOCK_open); + + pthread_mutex_lock(&LOCK_mdl); + while ((l= it++)) + if (l->type == MDL_EXCLUSIVE) + { + DBUG_ASSERT(l->state == MDL_ACQUIRED); + if (!l->upgradable) + global_intention_exclusive_locks_acquired--; + lock_data= l->lock_data; + lock_data->active_exclusive.remove(l); + l->type= MDL_SHARED; + lock_data->active_shared.push_front(l); + } + pthread_cond_broadcast(&COND_mdl); + pthread_mutex_unlock(&LOCK_mdl); +} + + +/** + Release global shared metadata lock. + + @param context Current context +*/ + +void mdl_release_global_shared_lock(MDL_CONTEXT *context) +{ + safe_mutex_assert_not_owner(&LOCK_open); + DBUG_ASSERT(context->has_global_shared_lock); + + pthread_mutex_lock(&LOCK_mdl); + global_shared_locks_acquired--; + context->has_global_shared_lock= FALSE; + pthread_cond_broadcast(&COND_mdl); + pthread_mutex_unlock(&LOCK_mdl); +} + + +/** + Auxiliary function which allows to check if we have exclusive lock + on the object. + + @param context Current context + @param type Id of object type + @param db Name of the database + @param name Name of the object + + @return TRUE if current context contains exclusive lock for the object, + FALSE otherwise. +*/ + +bool mdl_is_exclusive_lock_owner(MDL_CONTEXT *context, int type, + const char *db, const char *name) +{ + char key[MAX_DBKEY_LENGTH]; + uint key_length; + MDL_LOCK *l; + I_P_List_iterator<MDL_LOCK, MDL_LOCK_context> it(context->locks); + + int4store(key, type); + key_length= (uint) (strmov(strmov(key+4, db)+1, name)-key)+1; + + while ((l= it++) && (l->key_length != key_length || memcmp(l->key, key, key_length))) + continue; + return (l && l->type == MDL_EXCLUSIVE && l->state == MDL_ACQUIRED); +} + + +/** + Auxiliary function which allows to check if we some kind of lock on + the object. + + @param context Current context + @param type Id of object type + @param db Name of the database + @param name Name of the object + + @return TRUE if current context contains satisfied lock for the object, + FALSE otherwise. +*/ + +bool mdl_is_lock_owner(MDL_CONTEXT *context, int type, const char *db, + const char *name) +{ + char key[MAX_DBKEY_LENGTH]; + uint key_length; + MDL_LOCK *l; + I_P_List_iterator<MDL_LOCK, MDL_LOCK_context> it(context->locks); + + int4store(key, type); + key_length= (uint) (strmov(strmov(key+4, db)+1, name)-key)+1; + + while ((l= it++) && (l->key_length != key_length || + memcmp(l->key, key, key_length) || + l->state == MDL_PENDING)) + continue; + + return l; +} + + +/** + Check if we have any pending exclusive locks which conflict with + existing shared lock. + + @param l Shared lock against which check should be performed. + + @return TRUE if there are any conflicting locks, FALSE otherwise. +*/ + +bool mdl_has_pending_conflicting_lock(MDL_LOCK *l) +{ + bool result; + + DBUG_ASSERT(l->type == MDL_SHARED && l->state == MDL_ACQUIRED); + safe_mutex_assert_not_owner(&LOCK_open); + + pthread_mutex_lock(&LOCK_mdl); + result= !(l->lock_data->waiting_exclusive.is_empty() && + l->lock_data->active_shared_waiting_upgrade.is_empty()); + pthread_mutex_unlock(&LOCK_mdl); + return result; +} + + +/** + Associate pointer to an opaque object with a lock. + + @param l Lock request for the lock with which the + object should be associated. + @param cached_object Pointer to the object + @param release_hook Cleanup function to be called when MDL subsystem + decides to remove lock or associate another object. + + This is used to cache a pointer to TABLE_SHARE in the lock + structure. Such caching can save one acquisition of LOCK_open + and one table definition cache lookup for every table. + + Since the pointer may be stored only inside an acquired lock, + the caching is only effective when there is more than one lock + granted on a given table. + + This function has the following usage pattern: + - try to acquire an MDL lock + - when done, call for mdl_get_cached_object(). If it returns NULL, our + thread has the only lock on this table. + - look up TABLE_SHARE in the table definition cache + - call mdl_set_cache_object() to assign the share to the opaque pointer. + + The release hook is invoked when the last shared metadata + lock on this name is released. +*/ + +void mdl_set_cached_object(MDL_LOCK *l, void *cached_object, + mdl_cached_object_release_hook release_hook) +{ + DBUG_ENTER("mdl_set_cached_object"); + DBUG_PRINT("enter", ("db=%s name=%s cached_object=%p", l->key + 4, + l->key + 4 + strlen(l->key + 4) + 1, + cached_object)); + + DBUG_ASSERT(l->state == MDL_ACQUIRED || l->state == MDL_PENDING_UPGRADE); + + /* + TODO: This assumption works now since we do mdl_get_cached_object() + and mdl_set_cached_object() in the same critical section. Once + this becomes false we will have to call release_hook here and + use additional mutex protecting 'cached_object' member. + */ + DBUG_ASSERT(!l->lock_data->cached_object); + + l->lock_data->cached_object= cached_object; + l->lock_data->cached_object_release_hook= release_hook; + + DBUG_VOID_RETURN; +} + + +/** + Get a pointer to an opaque object that associated with the lock. + + @param l Lock request for the lock with which the object is + associated. + + @return Pointer to an opaque object associated with the lock. +*/ + +void* mdl_get_cached_object(MDL_LOCK *l) +{ + DBUG_ASSERT(l->state == MDL_ACQUIRED || l->state == MDL_PENDING_UPGRADE); + return l->lock_data->cached_object; +} diff --git a/sql/mdl.h b/sql/mdl.h new file mode 100644 index 00000000000..ca3d8d0a784 --- /dev/null +++ b/sql/mdl.h @@ -0,0 +1,260 @@ +#ifndef MDL_H +#define MDL_H +/* Copyright (C) 2007-2008 MySQL AB + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; version 2 of the License. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ + + +#include "sql_plist.h" +#include <my_sys.h> +#include <m_string.h> + +class THD; + +struct MDL_LOCK; +struct MDL_LOCK_DATA; +struct MDL_CONTEXT; + +/** Type of metadata lock request. */ + +enum enum_mdl_type {MDL_SHARED=0, MDL_EXCLUSIVE}; + + +/** States which metadata lock request can have. */ + +enum enum_mdl_state {MDL_PENDING=0, MDL_ACQUIRED, MDL_PENDING_UPGRADE}; + + +/** + Priority of metadata lock requests. High priority attribute is + applicable only to requests for shared locks and indicates that + such request should ignore pending requests for exclusive locks + and for upgrading of shared locks to exclusive. +*/ + +enum enum_mdl_prio {MDL_NORMAL_PRIO=0, MDL_HIGH_PRIO}; + + +/** + A pending lock request or a granted metadata lock. A lock is requested + or granted based on a fully qualified name and type. E.g. for a table + the key consists of <0 (=table)>+<database name>+<table name>. + Later in this document this triple will be referred to simply as + "key" or "name". +*/ + +struct MDL_LOCK +{ + char *key; + uint key_length; + enum enum_mdl_type type; + enum enum_mdl_state state; + enum enum_mdl_prio prio; + /** + TRUE -- if shared lock corresponding to this lock request at some + point might be upgraded to an exclusive lock and therefore conflicts + with global shared lock, FALSE -- otherwise. + */ + bool upgradable; + +private: + /** + Pointers for participating in the list of lock requests for this context. + */ + MDL_LOCK *next_context; + MDL_LOCK **prev_context; + /** + Pointers for participating in the list of satisfied/pending requests + for the lock. + */ + MDL_LOCK *next_lock; + MDL_LOCK **prev_lock; + + friend struct MDL_LOCK_context; + friend struct MDL_LOCK_lock; + +public: + /* + Pointer to the lock object for this lock request. Valid only if this lock + request is satisified or is present in the list of pending lock requests + for particular lock. + */ + MDL_LOCK_DATA *lock_data; + MDL_CONTEXT *ctx; +}; + + +/** + Helper class which specifies which members of MDL_LOCK are used for + participation in the list lock requests belonging to one context. +*/ + +struct MDL_LOCK_context +{ + static inline MDL_LOCK **next_ptr(MDL_LOCK *l) + { + return &l->next_context; + } + static inline MDL_LOCK ***prev_ptr(MDL_LOCK *l) + { + return &l->prev_context; + } +}; + + +/** + Helper class which specifies which members of MDL_LOCK are used for + participation in the list of satisfied/pending requests for the lock. +*/ + +struct MDL_LOCK_lock +{ + static inline MDL_LOCK **next_ptr(MDL_LOCK *l) + { + return &l->next_lock; + } + static inline MDL_LOCK ***prev_ptr(MDL_LOCK *l) + { + return &l->prev_lock; + } +}; + + +/** + Context of the owner of metadata locks. I.e. each server + connection has such a context. +*/ + +struct MDL_CONTEXT +{ + I_P_List <MDL_LOCK, MDL_LOCK_context> locks; + bool has_global_shared_lock; + THD *thd; +}; + + +void mdl_init(); +void mdl_destroy(); + +void mdl_context_init(MDL_CONTEXT *context, THD *thd); +void mdl_context_destroy(MDL_CONTEXT *context); +void mdl_context_backup_and_reset(MDL_CONTEXT *ctx, MDL_CONTEXT *backup); +void mdl_context_restore(MDL_CONTEXT *ctx, MDL_CONTEXT *backup); +void mdl_context_merge(MDL_CONTEXT *target, MDL_CONTEXT *source); + +void mdl_init_lock(MDL_LOCK *mdl, char *key, int type, const char *db, + const char *name); +MDL_LOCK *mdl_alloc_lock(int type, const char *db, const char *name, + MEM_ROOT *root); +void mdl_add_lock(MDL_CONTEXT *context, MDL_LOCK *lock); +void mdl_remove_all_locks(MDL_CONTEXT *context); + +/** + Set type of lock request. Can be only applied to pending locks. +*/ + +inline void mdl_set_lock_type(MDL_LOCK *lock, enum_mdl_type lock_type) +{ + DBUG_ASSERT(lock->state == MDL_PENDING); + lock->type= lock_type; +} + +/** + Set priority for lock request. High priority can be only set + for shared locks. +*/ + +inline void mdl_set_lock_priority(MDL_LOCK *lock, enum_mdl_prio prio) +{ + DBUG_ASSERT(lock->type == MDL_SHARED && lock->state == MDL_PENDING); + lock->prio= prio; +} + +/** + Mark request for shared lock as upgradable. Can be only applied + to pending locks. +*/ + +inline void mdl_set_upgradable(MDL_LOCK *lock) +{ + DBUG_ASSERT(lock->type == MDL_SHARED && lock->state == MDL_PENDING); + lock->upgradable= TRUE; +} + +bool mdl_acquire_shared_lock(MDL_LOCK *l, bool *retry); +bool mdl_acquire_exclusive_locks(MDL_CONTEXT *context); +bool mdl_upgrade_shared_lock_to_exclusive(MDL_CONTEXT *context, int type, + const char *db, const char *name); +bool mdl_try_acquire_exclusive_lock(MDL_CONTEXT *context, MDL_LOCK *lock); +bool mdl_acquire_global_shared_lock(MDL_CONTEXT *context); + +bool mdl_wait_for_locks(MDL_CONTEXT *context); + +void mdl_release_locks(MDL_CONTEXT *context); +void mdl_release_exclusive_locks(MDL_CONTEXT *context); +void mdl_release_lock(MDL_CONTEXT *context, MDL_LOCK *lock); +void mdl_downgrade_exclusive_locks(MDL_CONTEXT *context); +void mdl_release_global_shared_lock(MDL_CONTEXT *context); + +bool mdl_is_exclusive_lock_owner(MDL_CONTEXT *context, int type, const char *db, + const char *name); +bool mdl_is_lock_owner(MDL_CONTEXT *context, int type, const char *db, + const char *name); + +bool mdl_has_pending_conflicting_lock(MDL_LOCK *l); + +inline bool mdl_has_locks(MDL_CONTEXT *context) +{ + return !context->locks.is_empty(); +} + + +/** + Get iterator for walking through all lock requests in the context. +*/ + +inline I_P_List_iterator<MDL_LOCK, MDL_LOCK_context> mdl_get_locks(MDL_CONTEXT *ctx) +{ + I_P_List_iterator<MDL_LOCK, MDL_LOCK_context> result(ctx->locks); + return result; +} + +/** + Give metadata lock request object for the table get table definition + cache key corresponding to it. + + @param l [in] Lock request object for the table. + @param key [out] LEX_STRING object where table definition cache key + should be put. + + @note This key will have the same life-time as this lock request object. + + @note This is yet another place where border between MDL subsystem and the + rest of the server is broken. OTOH it allows to save some CPU cycles + and memory by avoiding generating these TDC keys from table list. +*/ + +inline void mdl_get_tdc_key(MDL_LOCK *l, LEX_STRING *key) +{ + key->str= l->key + 4; + key->length= l->key_length - 4; +} + + +typedef void (* mdl_cached_object_release_hook)(void *); +void* mdl_get_cached_object(MDL_LOCK *l); +void mdl_set_cached_object(MDL_LOCK *l, void *cached_object, + mdl_cached_object_release_hook release_hook); + +#endif diff --git a/sql/mysql_priv.h b/sql/mysql_priv.h index c6454679e28..be03759b383 100644 --- a/sql/mysql_priv.h +++ b/sql/mysql_priv.h @@ -805,7 +805,7 @@ extern my_decimal decimal_zero; void free_items(Item *item); void cleanup_items(Item *item); class THD; -void close_thread_tables(THD *thd); +void close_thread_tables(THD *thd, bool skip_mdl= 0); #ifndef NO_EMBEDDED_ACCESS_CHECKS bool check_one_table_access(THD *thd, ulong privilege, TABLE_LIST *tables); @@ -993,7 +993,6 @@ int mysql_rm_table_part2(THD *thd, TABLE_LIST *tables, bool if_exists, bool drop_temporary, bool drop_view, bool log_query); bool quick_rm_table(handlerton *base,const char *db, const char *table_name, uint flags); -void close_cached_table(THD *thd, TABLE *table); bool mysql_rename_tables(THD *thd, TABLE_LIST *table_list, bool silent); bool do_rename(THD *thd, TABLE_LIST *ren_table, char *new_db, char *new_table_name, char *new_table_alias, @@ -1034,10 +1033,8 @@ bool check_dup(const char *db, const char *name, TABLE_LIST *tables); bool compare_record(TABLE *table); bool append_file_to_dir(THD *thd, const char **filename_ptr, const char *table_name); -void wait_while_table_is_used(THD *thd, TABLE *table, +bool wait_while_table_is_used(THD *thd, TABLE *table, enum ha_extra_function function); -bool table_cache_init(void); -void table_cache_free(void); bool table_def_init(void); void table_def_free(void); void assign_new_table_id(TABLE_SHARE *share); @@ -1223,28 +1220,30 @@ void release_table_share(TABLE_SHARE *share, enum release_type type); TABLE_SHARE *get_cached_table_share(const char *db, const char *table_name); TABLE *open_ltable(THD *thd, TABLE_LIST *table_list, thr_lock_type update, uint lock_flags); +enum enum_open_table_action {OT_NO_ACTION= 0, OT_BACK_OFF_AND_RETRY, + OT_DISCOVER, OT_REPAIR}; TABLE *open_table(THD *thd, TABLE_LIST *table_list, MEM_ROOT* mem, - bool *refresh, uint flags); + enum_open_table_action *action, uint flags); +bool tdc_open_view(THD *thd, TABLE_LIST *table_list, const char *alias, + char *cache_key, uint cache_key_length, + MEM_ROOT *mem_root, uint flags); bool name_lock_locked_table(THD *thd, TABLE_LIST *tables); -bool reopen_name_locked_table(THD* thd, TABLE_LIST* table_list, bool link_in); -TABLE *table_cache_insert_placeholder(THD *thd, const char *key, - uint key_length); -bool lock_table_name_if_not_cached(THD *thd, const char *db, - const char *table_name, TABLE **table); -TABLE *find_locked_table(THD *thd, const char *db,const char *table_name); +bool reopen_name_locked_table(THD* thd, TABLE_LIST* table_list); +TABLE *find_locked_table(TABLE *list, const char *db, const char *table_name); +TABLE *find_write_locked_table(TABLE *list, const char *db, + const char *table_name); void detach_merge_children(TABLE *table, bool clear_refs); bool fix_merge_after_open(TABLE_LIST *old_child_list, TABLE_LIST **old_last, TABLE_LIST *new_child_list, TABLE_LIST **new_last); bool reopen_table(TABLE *table); -bool reopen_tables(THD *thd,bool get_locks,bool in_refresh); +bool reopen_tables(THD *thd, bool get_locks); thr_lock_type read_lock_type_for_table(THD *thd, TABLE *table); void close_data_files_and_morph_locks(THD *thd, const char *db, const char *table_name); void close_handle_and_leave_table_as_lock(TABLE *table); bool wait_for_tables(THD *thd); bool table_is_used(TABLE *table, bool wait_for_name_lock); -TABLE *drop_locked_tables(THD *thd,const char *db, const char *table_name); -void abort_locked_tables(THD *thd,const char *db, const char *table_name); +void unlock_locked_tables(THD *thd); void execute_init_command(THD *thd, sys_var_str *init_command_var, rw_lock_t *var_mutex); extern Field *not_found_field; @@ -1364,7 +1363,7 @@ bool mysql_ha_close(THD *thd, TABLE_LIST *tables); bool mysql_ha_read(THD *, TABLE_LIST *,enum enum_ha_read_modes,char *, List<Item> *,enum ha_rkey_function,Item *,ha_rows,ha_rows); void mysql_ha_flush(THD *thd); -void mysql_ha_rm_tables(THD *thd, TABLE_LIST *tables, bool is_locked); +void mysql_ha_rm_tables(THD *thd, TABLE_LIST *tables); void mysql_ha_cleanup(THD *thd); /* sql_base.cc */ @@ -1501,7 +1500,7 @@ void free_io_cache(TABLE *entry); void intern_close_table(TABLE *entry); bool close_thread_table(THD *thd, TABLE **table_ptr); void close_temporary_tables(THD *thd); -void close_tables_for_reopen(THD *thd, TABLE_LIST **tables); +void close_tables_for_reopen(THD *thd, TABLE_LIST **tables, bool skip_mdl); TABLE_LIST *find_table_in_list(TABLE_LIST *table, TABLE_LIST *TABLE_LIST::*link, const char *db_name, @@ -1550,6 +1549,9 @@ char *generate_partition_syntax(partition_info *part_info, #define RTFC_CHECK_KILLED_FLAG 0x0004 bool remove_table_from_cache(THD *thd, const char *db, const char *table, uint flags); +bool notify_thread_having_shared_lock(THD *thd, THD *in_use); +void expel_table_from_cache(THD *leave_thd, const char *db, + const char *table_name); #define NORMAL_PART_NAME 0 #define TEMP_PART_NAME 1 @@ -1681,7 +1683,7 @@ TABLE *open_performance_schema_table(THD *thd, TABLE_LIST *one_table, void close_performance_schema_table(THD *thd, Open_tables_state *backup); bool close_cached_tables(THD *thd, TABLE_LIST *tables, bool have_lock, - bool wait_for_refresh, bool wait_for_placeholders); + bool wait_for_refresh); bool close_cached_connection_tables(THD *thd, bool wait_for_refresh, LEX_STRING *connect_string, bool have_lock = FALSE); @@ -2001,8 +2003,9 @@ extern const char *opt_date_time_formats[]; extern KNOWN_DATE_TIME_FORMAT known_date_time_formats[]; extern String null_string; -extern HASH open_cache, lock_db_cache; +extern HASH table_def_cache, lock_db_cache; extern TABLE *unused_tables; +extern uint table_cache_count; extern const char* any_db; extern struct my_option my_long_options[]; extern const LEX_STRING view_type; @@ -2070,18 +2073,8 @@ void unset_protect_against_global_read_lock(void); void broadcast_refresh(void); /* Lock based on name */ -int lock_and_wait_for_table_name(THD *thd, TABLE_LIST *table_list); -int lock_table_name(THD *thd, TABLE_LIST *table_list, bool check_in_use); -void unlock_table_name(THD *thd, TABLE_LIST *table_list); -bool wait_for_locked_table_names(THD *thd, TABLE_LIST *table_list); bool lock_table_names(THD *thd, TABLE_LIST *table_list); -void unlock_table_names(THD *thd, TABLE_LIST *table_list, - TABLE_LIST *last_table); -bool lock_table_names_exclusively(THD *thd, TABLE_LIST *table_list); -bool is_table_name_exclusively_locked_by_this_thread(THD *thd, - TABLE_LIST *table_list); -bool is_table_name_exclusively_locked_by_this_thread(THD *thd, uchar *key, - int key_length); +void unlock_table_names(THD *thd); /* old unireg functions */ diff --git a/sql/mysqld.cc b/sql/mysqld.cc index 1cab245b317..90924b5b662 100644 --- a/sql/mysqld.cc +++ b/sql/mysqld.cc @@ -1301,9 +1301,9 @@ void clean_up(bool print_message) grant_free(); #endif query_cache_destroy(); - table_cache_free(); table_def_free(); hostname_cache_free(); + mdl_destroy(); item_user_lock_free(); lex_free(); /* Free some memory */ item_create_cleanup(); @@ -3755,7 +3755,8 @@ static int init_server_components() We need to call each of these following functions to ensure that all things are initialized so that unireg_abort() doesn't fail */ - if (table_cache_init() | table_def_init() | hostname_cache_init()) + mdl_init(); + if (table_def_init() | hostname_cache_init()) unireg_abort(1); query_cache_result_size_limit(query_cache_limit); diff --git a/sql/rpl_rli.cc b/sql/rpl_rli.cc index ae3cbf789a5..42cd12bd66c 100644 --- a/sql/rpl_rli.cc +++ b/sql/rpl_rli.cc @@ -1187,8 +1187,7 @@ void Relay_log_info::cleanup_context(THD *thd, bool error) end_trans(thd, ROLLBACK); // if a "real transaction" } m_table_map.clear_tables(); - close_thread_tables(thd); - clear_tables_to_lock(); + slave_close_thread_tables(thd); clear_flag(IN_STMT); /* Cleanup for the flags that have been set at do_apply_event. @@ -1200,6 +1199,13 @@ void Relay_log_info::cleanup_context(THD *thd, bool error) void Relay_log_info::clear_tables_to_lock() { + /* + Deallocating elements of table list below will also free memory where + meta-data locks are stored. So we want to be sure that we don't have + any references to this memory left. + */ + DBUG_ASSERT(!mdl_has_locks(&(current_thd->mdl_context))); + while (tables_to_lock) { uchar* to_free= reinterpret_cast<uchar*>(tables_to_lock); @@ -1216,4 +1222,15 @@ void Relay_log_info::clear_tables_to_lock() DBUG_ASSERT(tables_to_lock == NULL && tables_to_lock_count == 0); } +void Relay_log_info::slave_close_thread_tables(THD *thd) +{ + /* + Since we use same memory chunks for allocation of metadata lock + objects for tables as we use for allocating corresponding elements + of 'tables_to_lock' list, we have to release metadata locks by + closing tables before calling clear_tables_to_lock(). + */ + close_thread_tables(thd); + clear_tables_to_lock(); +} #endif diff --git a/sql/rpl_rli.h b/sql/rpl_rli.h index fd36d18adae..332bc4e53d0 100644 --- a/sql/rpl_rli.h +++ b/sql/rpl_rli.h @@ -346,6 +346,7 @@ public: bool cached_charset_compare(char *charset) const; void cleanup_context(THD *, bool); + void slave_close_thread_tables(THD *); void clear_tables_to_lock(); /* diff --git a/sql/set_var.cc b/sql/set_var.cc index 1028e5441ae..1490ffd9598 100644 --- a/sql/set_var.cc +++ b/sql/set_var.cc @@ -4333,7 +4333,7 @@ bool sys_var_opt_readonly::update(THD *thd, set_var *var) can cause to wait on a read lock, it's required for the client application to unlock everything, and acceptable for the server to wait on all locks. */ - if ((result= close_cached_tables(thd, NULL, FALSE, TRUE, TRUE))) + if ((result= close_cached_tables(thd, NULL, FALSE, TRUE))) goto end_with_read_lock; if ((result= make_global_read_lock_block_commit(thd))) diff --git a/sql/sp_head.cc b/sql/sp_head.cc index f90aefc2a3f..14573cd6884 100644 --- a/sql/sp_head.cc +++ b/sql/sp_head.cc @@ -3984,6 +3984,9 @@ sp_head::add_used_tables_to_table_list(THD *thd, table->prelocking_placeholder= 1; table->belong_to_view= belong_to_view; table->trg_event_map= stab->trg_event_map; + table->mdl_lock= mdl_alloc_lock(0, table->db, table->table_name, + thd->mdl_el_root ? thd->mdl_el_root : + thd->mem_root); /* Everyting else should be zeroed */ @@ -4025,7 +4028,9 @@ sp_add_to_query_tables(THD *thd, LEX *lex, table->lock_type= locktype; table->select_lex= lex->current_select; table->cacheable_table= 1; - + table->mdl_lock= mdl_alloc_lock(0, table->db, table->table_name, + thd->mdl_el_root ? thd->mdl_el_root : + thd->mem_root); lex->add_to_query_tables(table); return table; } diff --git a/sql/sql_acl.cc b/sql/sql_acl.cc index 641423605aa..b01e7b1049d 100644 --- a/sql/sql_acl.cc +++ b/sql/sql_acl.cc @@ -676,12 +676,7 @@ my_bool acl_reload(THD *thd) my_bool return_val= 1; DBUG_ENTER("acl_reload"); - if (thd->locked_tables) - { // Can't have locked tables here - thd->lock=thd->locked_tables; - thd->locked_tables=0; - close_thread_tables(thd); - } + unlock_locked_tables(thd); // Can't have locked tables here /* To avoid deadlocks we should obtain table locks before @@ -697,6 +692,7 @@ my_bool acl_reload(THD *thd) tables[0].lock_type=tables[1].lock_type=tables[2].lock_type=TL_READ; tables[0].skip_temporary= tables[1].skip_temporary= tables[2].skip_temporary= TRUE; + alloc_mdl_locks(tables, thd->mem_root); if (simple_open_n_lock_tables(thd, tables)) { @@ -1601,6 +1597,7 @@ bool change_password(THD *thd, const char *host, const char *user, bzero((char*) &tables, sizeof(tables)); tables.alias= tables.table_name= (char*) "user"; tables.db= (char*) "mysql"; + alloc_mdl_locks(&tables, thd->mem_root); #ifdef HAVE_REPLICATION /* @@ -3053,6 +3050,7 @@ int mysql_table_grant(THD *thd, TABLE_LIST *table_list, ? tables+2 : 0); tables[0].lock_type=tables[1].lock_type=tables[2].lock_type=TL_WRITE; tables[0].db=tables[1].db=tables[2].db=(char*) "mysql"; + alloc_mdl_locks(tables, thd->mem_root); /* This statement will be replicated as a statement, even when using @@ -3270,6 +3268,7 @@ bool mysql_routine_grant(THD *thd, TABLE_LIST *table_list, bool is_proc, tables[0].next_local= tables[0].next_global= tables+1; tables[0].lock_type=tables[1].lock_type=TL_WRITE; tables[0].db=tables[1].db=(char*) "mysql"; + alloc_mdl_locks(tables, thd->mem_root); /* This statement will be replicated as a statement, even when using @@ -3408,6 +3407,7 @@ bool mysql_grant(THD *thd, const char *db, List <LEX_USER> &list, tables[0].next_local= tables[0].next_global= tables+1; tables[0].lock_type=tables[1].lock_type=TL_WRITE; tables[0].db=tables[1].db=(char*) "mysql"; + alloc_mdl_locks(tables, thd->mem_root); /* This statement will be replicated as a statement, even when using @@ -3741,6 +3741,7 @@ static my_bool grant_reload_procs_priv(THD *thd) table.db= (char *) "mysql"; table.lock_type= TL_READ; table.skip_temporary= 1; + alloc_mdl_locks(&table, thd->mem_root); if (simple_open_n_lock_tables(thd, &table)) { @@ -3807,6 +3808,8 @@ my_bool grant_reload(THD *thd) tables[0].next_local= tables[0].next_global= tables+1; tables[0].lock_type= tables[1].lock_type= TL_READ; tables[0].skip_temporary= tables[1].skip_temporary= TRUE; + alloc_mdl_locks(tables, thd->mem_root); + /* To avoid deadlocks we should obtain table locks before obtaining LOCK_grant rwlock. @@ -5152,6 +5155,7 @@ int open_grant_tables(THD *thd, TABLE_LIST *tables) (tables+4)->lock_type= TL_WRITE; tables->db= (tables+1)->db= (tables+2)->db= (tables+3)->db= (tables+4)->db= (char*) "mysql"; + alloc_mdl_locks(tables, thd->mem_root); #ifdef HAVE_REPLICATION /* diff --git a/sql/sql_base.cc b/sql/sql_base.cc index 8f31ef6999a..de4aaac633e 100644 --- a/sql/sql_base.cc +++ b/sql/sql_base.cc @@ -96,17 +96,32 @@ bool Prelock_error_handler::safely_trapped_errors() @defgroup Data_Dictionary Data Dictionary @{ */ -TABLE *unused_tables; /* Used by mysql_test */ -HASH open_cache; /* Used by mysql_test */ -static HASH table_def_cache; + +/** + Total number of TABLE instances for tables in the table definition cache + (both in use by threads and not in use). This value is accessible to user + as "Open_tables" status variable. +*/ +uint table_cache_count= 0; +/** + List that contains all TABLE instances for tables in the table definition + cache that are not in use by any thread. Recently used TABLE instances are + appended to the end of the list. Thus the beginning of the list contains + tables which have been least recently used. +*/ +TABLE *unused_tables; +HASH table_def_cache; static TABLE_SHARE *oldest_unused_share, end_of_unused_share; static pthread_mutex_t LOCK_table_share; static bool table_def_inited= 0; -static int open_unireg_entry(THD *thd, TABLE *entry, TABLE_LIST *table_list, - const char *alias, - char *cache_key, uint cache_key_length, - MEM_ROOT *mem_root, uint flags); +static bool check_and_update_table_version(THD *thd, TABLE_LIST *tables, + TABLE_SHARE *table_share); +static bool reopen_table_entry(THD *thd, TABLE *entry, TABLE_LIST *table_list, + const char *alias, char *cache_key, + uint cache_key_length); +static bool open_table_entry_fini(THD *thd, TABLE_SHARE *share, TABLE *entry); +static bool auto_repair_table(THD *thd, TABLE_LIST *table_list); static void free_cache_entry(TABLE *entry); static bool open_new_frm(THD *thd, TABLE_SHARE *share, const char *alias, uint db_stat, uint prgflag, @@ -114,41 +129,14 @@ static bool open_new_frm(THD *thd, TABLE_SHARE *share, const char *alias, TABLE_LIST *table_desc, MEM_ROOT *mem_root); static void close_old_data_files(THD *thd, TABLE *table, bool morph_locks, bool send_refresh); +static bool tdc_wait_for_old_versions(THD *thd, MDL_CONTEXT *context); static bool has_write_table_with_auto_increment(TABLE_LIST *tables); -extern "C" uchar *table_cache_key(const uchar *record, size_t *length, - my_bool not_used __attribute__((unused))) -{ - TABLE *entry=(TABLE*) record; - *length= entry->s->table_cache_key.length; - return (uchar*) entry->s->table_cache_key.str; -} - - -bool table_cache_init(void) -{ - return my_hash_init(&open_cache, &my_charset_bin, table_cache_size+16, - 0, 0, table_cache_key, - (my_hash_free_key) free_cache_entry, 0) != 0; -} - -void table_cache_free(void) -{ - DBUG_ENTER("table_cache_free"); - if (table_def_inited) - { - close_cached_tables(NULL, NULL, FALSE, FALSE, FALSE); - if (!open_cache.records) // Safety first - my_hash_free(&open_cache); - } - DBUG_VOID_RETURN; -} - uint cached_open_tables(void) { - return open_cache.records; + return table_cache_count; } @@ -156,7 +144,8 @@ uint cached_open_tables(void) static void check_unused(void) { uint count= 0, open_files= 0, idx= 0; - TABLE *cur_link,*start_link; + TABLE *cur_link, *start_link, *entry; + TABLE_SHARE *share; if ((start_link=cur_link=unused_tables)) { @@ -167,45 +156,42 @@ static void check_unused(void) DBUG_PRINT("error",("Unused_links aren't linked properly")); /* purecov: inspected */ return; /* purecov: inspected */ } - } while (count++ < open_cache.records && + } while (count++ < table_cache_count && (cur_link=cur_link->next) != start_link); if (cur_link != start_link) { DBUG_PRINT("error",("Unused_links aren't connected")); /* purecov: inspected */ } } - for (idx=0 ; idx < open_cache.records ; idx++) + for (idx=0 ; idx < table_def_cache.records ; idx++) { - TABLE *entry=(TABLE*) my_hash_element(&open_cache,idx); - if (!entry->in_use) + share= (TABLE_SHARE*) my_hash_element(&table_def_cache, idx); + + I_P_List_iterator<TABLE, TABLE_share> it(share->free_tables); + while ((entry= it++)) + { + if (entry->in_use) + { + DBUG_PRINT("error",("Used table is in share's list of unused tables")); /* purecov: inspected */ + } count--; - if (entry->file) open_files++; + } + it.init(share->used_tables); + while ((entry= it++)) + { + if (!entry->in_use) + { + DBUG_PRINT("error",("Unused table is in share's list of used tables")); /* purecov: inspected */ + } + open_files++; + } } if (count != 0) { DBUG_PRINT("error",("Unused_links doesn't match open_cache: diff: %d", /* purecov: inspected */ count)); /* purecov: inspected */ } - -#ifdef NOT_SAFE_FOR_REPAIR - /* - check that open cache and table definition cache has same number of - aktive tables - */ - count= 0; - for (idx=0 ; idx < table_def_cache.records ; idx++) - { - TABLE_SHARE *entry= (TABLE_SHARE*) hash_element(&table_def_cache,idx); - count+= entry->ref_count; - } - if (count != open_files) - { - DBUG_PRINT("error", ("table_def ref_count: %u open_cache: %u", - count, open_files)); - DBUG_ASSERT(count == open_files); - } -#endif } #else #define check_unused() @@ -300,8 +286,11 @@ void table_def_free(void) DBUG_ENTER("table_def_free"); if (table_def_inited) { + /* Free all open TABLEs first. */ + close_cached_tables(NULL, NULL, FALSE, FALSE); table_def_inited= 0; pthread_mutex_destroy(&LOCK_table_share); + /* Free table definitions. */ my_hash_free(&table_def_cache); } DBUG_VOID_RETURN; @@ -315,6 +304,128 @@ uint cached_table_definitions(void) /* + Auxiliary routines for manipulating with per-share used/unused and + global unused lists of TABLE objects and table_cache_count counter. + Responsible for preserving invariants between those lists, counter + and TABLE::in_use member. + In fact those routines implement sort of implicit table cache as + part of table definition cache. +*/ + + +/** + Add newly created TABLE object for table share which is going + to be used right away. +*/ + +static void table_def_add_used_table(THD *thd, TABLE *table) +{ + DBUG_ASSERT(table->in_use == thd); + table->s->used_tables.push_front(table); + table_cache_count++; +} + + +/** + Prepare used or unused TABLE instance for destruction by removing + it from share's and global list. +*/ + +static void table_def_remove_table(TABLE *table) +{ + if (table->in_use) + { + /* Remove from per-share chain of used TABLE objects. */ + table->s->used_tables.remove(table); + } + else + { + /* Remove from per-share chain of unused TABLE objects. */ + table->s->free_tables.remove(table); + + /* And global unused chain. */ + table->next->prev=table->prev; + table->prev->next=table->next; + if (table == unused_tables) + { + unused_tables=unused_tables->next; + if (table == unused_tables) + unused_tables=0; + } + check_unused(); + } + table_cache_count--; +} + + +/** + Mark already existing TABLE instance as used. +*/ + +static void table_def_use_table(THD *thd, TABLE *table) +{ + DBUG_ASSERT(!table->in_use); + + /* Unlink table from list of unused tables for this share. */ + table->s->free_tables.remove(table); + /* Unlink able from global unused tables list. */ + if (table == unused_tables) + { // First unused + unused_tables=unused_tables->next; // Remove from link + if (table == unused_tables) + unused_tables=0; + } + table->prev->next=table->next; /* Remove from unused list */ + table->next->prev=table->prev; + check_unused(); + /* Add table to list of used tables for this share. */ + table->s->used_tables.push_front(table); + table->in_use= thd; +} + + +/** + Mark already existing used TABLE instance as unused. +*/ + +static void table_def_unuse_table(TABLE *table) +{ + DBUG_ASSERT(table->in_use); + + table->in_use= 0; + /* Remove table from the list of tables used in this share. */ + table->s->used_tables.remove(table); + /* Add table to the list of unused TABLE objects for this share. */ + table->s->free_tables.push_front(table); + /* Also link it last in the global list of unused TABLE objects. */ + if (unused_tables) + { + table->next=unused_tables; + table->prev=unused_tables->prev; + unused_tables->prev=table; + table->prev->next=table; + } + else + unused_tables=table->next=table->prev=table; + check_unused(); +} + + +/** + Bind used TABLE instance to another table share. + + @note Will go away once we refactor code responsible + for reopening tables under lock tables. +*/ + +static void table_def_change_share(TABLE *table, TABLE_SHARE *new_share) +{ + table->s->used_tables.remove(table); + new_share->used_tables.push_front(table); +} + + +/* Get TABLE_SHARE for a table. get_table_share() @@ -347,6 +458,13 @@ TABLE_SHARE *get_table_share(THD *thd, TABLE_LIST *table_list, char *key, *error= 0; + /* + To be able perform any operation on table we should own + some kind of metadata lock on it. + */ + DBUG_ASSERT(mdl_is_lock_owner(&thd->mdl_context, 0, table_list->db, + table_list->table_name)); + /* Read table definition from cache */ if ((share= (TABLE_SHARE*) my_hash_search(&table_def_cache,(uchar*) key, key_length))) @@ -569,6 +687,7 @@ void release_table_share(TABLE_SHARE *share, enum release_type type) safe_mutex_assert_owner(&LOCK_open); pthread_mutex_lock(&share->mutex); + DBUG_ASSERT(share->ref_count); if (!--share->ref_count) { if (share->version != refresh_version) @@ -629,6 +748,26 @@ TABLE_SHARE *get_cached_table_share(const char *db, const char *table_name) } +/** + @brief Mark table share as having one more user (increase its reference + count). + + @param share Table share for which reference count should be increased. +*/ + +static void reference_table_share(TABLE_SHARE *share) +{ + DBUG_ENTER("reference_table_share"); + DBUG_ASSERT(share->ref_count); + pthread_mutex_lock(&share->mutex); + share->ref_count++; + pthread_mutex_unlock(&share->mutex); + DBUG_PRINT("exit", ("share: 0x%lx ref_count: %u", + (ulong) share, share->ref_count)); + DBUG_VOID_RETURN; +} + + /* Close file handle, but leave the table in the table cache @@ -680,6 +819,7 @@ void close_handle_and_leave_table_as_lock(TABLE *table) detach_merge_children(table, FALSE); table->file->close(); table->db_stat= 0; // Mark file closed + table_def_change_share(table, share); release_table_share(table->s, RELEASE_NORMAL); table->s= share; table->file->change_table_ptr(table, table->s); @@ -719,11 +859,9 @@ OPEN_TABLE_LIST *list_open_tables(THD *thd, const char *db, const char *wild) start_list= &open_list; open_list=0; - for (uint idx=0 ; result == 0 && idx < open_cache.records; idx++) + for (uint idx=0 ; result == 0 && idx < table_def_cache.records; idx++) { - OPEN_TABLE_LIST *table; - TABLE *entry=(TABLE*) my_hash_element(&open_cache,idx); - TABLE_SHARE *share= entry->s; + TABLE_SHARE *share= (TABLE_SHARE *)my_hash_element(&table_def_cache, idx); if (db && my_strcasecmp(system_charset_info, db, share->db.str)) continue; @@ -737,21 +875,7 @@ OPEN_TABLE_LIST *list_open_tables(THD *thd, const char *db, const char *wild) if (check_table_access(thd,SELECT_ACL,&table_list, TRUE, 1, TRUE)) continue; - /* need to check if we haven't already listed it */ - for (table= open_list ; table ; table=table->next) - { - if (!strcmp(table->table, share->table_name.str) && - !strcmp(table->db, share->db.str)) - { - if (entry->in_use) - table->in_use++; - if (entry->locked_by_name) - table->locked++; - break; - } - } - if (table) - continue; + if (!(*start_list = (OPEN_TABLE_LIST *) sql_alloc(sizeof(**start_list)+share->table_cache_key.length))) { @@ -762,8 +886,11 @@ OPEN_TABLE_LIST *list_open_tables(THD *thd, const char *db, const char *wild) strmov(((*start_list)->db= (char*) ((*start_list)+1)), share->db.str)+1, share->table_name.str); - (*start_list)->in_use= entry->in_use ? 1 : 0; - (*start_list)->locked= entry->locked_by_name ? 1 : 0; + (*start_list)->in_use= 0; + I_P_List_iterator<TABLE, TABLE_share> it(share->used_tables); + while (it++) + ++(*start_list)->in_use; + (*start_list)->locked= (share->version == 0) ? 1 : 0; start_list= &(*start_list)->next; *start_list=0; } @@ -809,19 +936,11 @@ static void free_cache_entry(TABLE *table) /* Assert that MERGE children are not attached before final close. */ DBUG_ASSERT(!table->is_children_attached()); + /* This should be done before releasing table share. */ + table_def_remove_table(table); + intern_close_table(table); - if (!table->in_use) - { - table->next->prev=table->prev; /* remove from used chain */ - table->prev->next=table->next; - if (table == unused_tables) - { - unused_tables=unused_tables->next; - if (table == unused_tables) - unused_tables=0; - } - check_unused(); // consisty check - } + my_free((uchar*) table,MYF(0)); DBUG_VOID_RETURN; } @@ -841,6 +960,38 @@ void free_io_cache(TABLE *table) } +/** + Auxiliary function which allows to kill delayed threads for + particular table identified by its share. + + @param share Table share. +*/ + +static void kill_delayed_threads_for_table(TABLE_SHARE *share) +{ + I_P_List_iterator<TABLE, TABLE_share> it(share->used_tables); + TABLE *tab; + while ((tab= it++)) + { + THD *in_use= tab->in_use; + + if ((in_use->system_thread & SYSTEM_THREAD_DELAYED_INSERT) && + ! in_use->killed) + { + in_use->killed= THD::KILL_CONNECTION; + pthread_mutex_lock(&in_use->mysys_var->mutex); + if (in_use->mysys_var->current_cond) + { + pthread_mutex_lock(in_use->mysys_var->current_mutex); + pthread_cond_broadcast(in_use->mysys_var->current_cond); + pthread_mutex_unlock(in_use->mysys_var->current_mutex); + } + pthread_mutex_unlock(&in_use->mysys_var->mutex); + } + } +} + + /* Close all tables which aren't in use by any thread @@ -848,18 +999,23 @@ void free_io_cache(TABLE *table) @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. + @note THD can be NULL, but then wait_for_refresh must be FALSE + and tables must be NULL. + + @note When called as part of FLUSH TABLES WITH READ LOCK this function + ignores metadata locks held by other threads. In order to avoid + situation when FLUSH TABLES WITH READ LOCK sneaks in at the moment + when some write-locked table is being reopened (by FLUSH TABLES or + ALTER TABLE) we have to rely on additional global shared metadata + lock taken by thread trying to obtain global read lock. */ bool close_cached_tables(THD *thd, TABLE_LIST *tables, bool have_lock, - bool wait_for_refresh, bool wait_for_placeholders) + bool wait_for_refresh) { - bool result=0; + bool result= FALSE; + bool found= TRUE; DBUG_ENTER("close_cached_tables"); DBUG_ASSERT(thd || (!wait_for_refresh && !tables)); @@ -868,165 +1024,181 @@ bool close_cached_tables(THD *thd, TABLE_LIST *tables, bool have_lock, if (!tables) { refresh_version++; // Force close of open tables - while (unused_tables) - { -#ifdef EXTRA_DEBUG - if (my_hash_delete(&open_cache,(uchar*) unused_tables)) - printf("Warning: Couldn't delete open table from hash\n"); -#else - (void) my_hash_delete(&open_cache,(uchar*) unused_tables); -#endif - } - /* Free table shares */ - while (oldest_unused_share->next) - { - pthread_mutex_lock(&oldest_unused_share->mutex); - (void) my_hash_delete(&table_def_cache, (uchar*) oldest_unused_share); - } DBUG_PRINT("tcache", ("incremented global refresh_version to: %lu", refresh_version)); - if (wait_for_refresh) - { - /* - Other threads could wait in a loop in open_and_lock_tables(), - trying to lock one or more of our tables. - - If they wait for the locks in thr_multi_lock(), their lock - request is aborted. They loop in open_and_lock_tables() and - enter open_table(). Here they notice the table is refreshed and - wait for COND_refresh. Then they loop again in - open_and_lock_tables() and this time open_table() succeeds. At - this moment, if we (the FLUSH TABLES thread) are scheduled and - on another FLUSH TABLES enter close_cached_tables(), they could - awake while we sleep below, waiting for others threads (us) to - close their open tables. If this happens, the other threads - would find the tables unlocked. They would get the locks, one - after the other, and could do their destructive work. This is an - issue if we have LOCK TABLES in effect. - - The problem is that the other threads passed all checks in - open_table() before we refresh the table. - - The fix for this problem is to set some_tables_deleted for all - threads with open tables. These threads can still get their - locks, but will immediately release them again after checking - this variable. They will then loop in open_and_lock_tables() - again. There they will wait until we update all tables version - below. - - Setting some_tables_deleted is done by remove_table_from_cache() - in the other branch. - - In other words (reviewer suggestion): You need this setting of - some_tables_deleted for the case when table was opened and all - related checks were passed before incrementing refresh_version - (which you already have) but attempt to lock the table happened - after the call to close_old_data_files() i.e. after removal of - current thread locks. - */ - for (uint idx=0 ; idx < open_cache.records ; idx++) - { - TABLE *table=(TABLE*) my_hash_element(&open_cache,idx); - if (table->in_use) - table->in_use->some_tables_deleted= 1; - } - } + kill_delayed_threads(); } else { bool found=0; for (TABLE_LIST *table= tables; table; table= table->next_local) { - if (remove_table_from_cache(thd, table->db, table->table_name, - RTFC_OWNED_BY_THD_FLAG)) + TABLE_SHARE *share= get_cached_table_share(table->db, table->table_name); + + if (share) + { + share->version= 0; + kill_delayed_threads_for_table(share); found=1; + } } if (!found) wait_for_refresh=0; // Nothing to wait for } -#ifndef EMBEDDED_LIBRARY - if (!tables) - kill_delayed_threads(); -#endif - if (wait_for_refresh) + + /* + Get rid of all unused TABLE and TABLE_SHARE instances. By doing + this we automatically close all tables which were marked as "old". + + FIXME: Do not close all unused TABLE instances when flushing + particular table. + */ + while (unused_tables) + free_cache_entry(unused_tables); + /* Free table shares */ + while (oldest_unused_share->next) + { + pthread_mutex_lock(&oldest_unused_share->mutex); + (void) my_hash_delete(&table_def_cache, (uchar*) oldest_unused_share); + } + + if (!wait_for_refresh) + { + if (!have_lock) + pthread_mutex_unlock(&LOCK_open); + DBUG_RETURN(result); + } + + DBUG_ASSERT(!have_lock); + pthread_mutex_unlock(&LOCK_open); + + if (thd->locked_tables) { /* - If there is any table that has a lower refresh_version, wait until - this is closed (or this thread is killed) before returning + If we are under LOCK TABLES we need to reopen tables without + opening a door for any concurrent threads to sneak in and get + lock on our tables. To achieve this we use exclusive metadata + locks. */ - thd->mysys_var->current_mutex= &LOCK_open; - thd->mysys_var->current_cond= &COND_refresh; - thd_proc_info(thd, "Flushing tables"); + if (!tables) + { + for (TABLE *tab= thd->open_tables; tab; tab= tab->next) + { + /* + Checking TABLE::db_stat is essential in case when we have + several instances of the table open and locked. + */ + if (tab->db_stat) + { + char dbname[NAME_LEN+1]; + char tname[NAME_LEN+1]; + /* + Since close_data_files_and_morph_locks() frees share's memroot + we need to make copies of database and table names. + */ + strmov(dbname, tab->s->db.str); + strmov(tname, tab->s->table_name.str); + if (wait_while_table_is_used(thd, tab, HA_EXTRA_FORCE_REOPEN)) + { + result= TRUE; + goto err_with_reopen; + } + pthread_mutex_lock(&LOCK_open); + close_data_files_and_morph_locks(thd, dbname, tname); + pthread_mutex_unlock(&LOCK_open); + } + } + } + else + { + for (TABLE_LIST *table= tables; table; table= table->next_local) + { + TABLE *tab= find_locked_table(thd->open_tables, table->db, + table->table_name); + /* + Checking TABLE::db_stat is essential in case when we have + several instances of the table open and locked. + */ + if (tab->db_stat) + { + if (wait_while_table_is_used(thd, tab, HA_EXTRA_FORCE_REOPEN)) + { + result= TRUE; + goto err_with_reopen; + } + pthread_mutex_lock(&LOCK_open); + close_data_files_and_morph_locks(thd, table->db, table->table_name); + pthread_mutex_unlock(&LOCK_open); + } + } + } + } - close_old_data_files(thd,thd->open_tables,1,1); + /* Wait until all threads have closed all the tables we are flushing. */ + DBUG_PRINT("info", ("Waiting for other threads to close their open tables")); + + while (found && ! thd->killed) + { + found= FALSE; + /* + To avoid self and other kinds of deadlock we have to flush open HANDLERs. + */ mysql_ha_flush(thd); DEBUG_SYNC(thd, "after_flush_unlock"); - bool found=1; - /* Wait until all threads has closed all the tables we had locked */ - DBUG_PRINT("info", - ("Waiting for other threads to close their open tables")); - while (found && ! thd->killed) + pthread_mutex_lock(&LOCK_open); + + thd->enter_cond(&COND_refresh, &LOCK_open, "Flushing tables"); + + if (!tables) { - found=0; - for (uint idx=0 ; idx < open_cache.records ; idx++) + for (uint idx=0 ; idx < table_def_cache.records ; idx++) { - TABLE *table=(TABLE*) my_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 - latter will cause deadlock in the following scenario, for example: - - conn1: lock table t1 write; - conn2: lock table t2 write; - conn1: flush tables; - conn2: flush tables; - - It also does not make sense to wait for those of placeholders that - 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 || - (table->open_placeholder && wait_for_placeholders))) - { - found=1; - DBUG_PRINT("signal", ("Waiting for COND_refresh")); - pthread_cond_wait(&COND_refresh,&LOCK_open); - break; - } + TABLE_SHARE *share=(TABLE_SHARE*) my_hash_element(&table_def_cache, + idx); + if (share->version != refresh_version) + { + found= TRUE; + break; + } + } + } + else + { + for (TABLE_LIST *table= tables; table; table= table->next_local) + { + TABLE_SHARE *share= get_cached_table_share(table->db, table->table_name); + if (share && share->version != refresh_version) + { + found= TRUE; + break; + } } } + + if (found) + { + DBUG_PRINT("signal", ("Waiting for COND_refresh")); + pthread_cond_wait(&COND_refresh,&LOCK_open); + } + + thd->exit_cond(NULL); + } + +err_with_reopen: + if (thd->locked_tables) + { + pthread_mutex_lock(&LOCK_open); /* No other thread has the locked tables open; reopen them and get the old locks. This should always succeed (unless some external process has removed the tables) */ thd->in_lock_tables=1; - result=reopen_tables(thd,1,1); + result|= reopen_tables(thd, 1); thd->in_lock_tables=0; - /* Set version for table */ - for (TABLE *table=thd->open_tables; table ; table= table->next) - { - /* - 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) pthread_mutex_unlock(&LOCK_open); - if (wait_for_refresh) - { - pthread_mutex_lock(&thd->mysys_var->mutex); - thd->mysys_var->current_mutex= 0; - thd->mysys_var->current_cond= 0; - thd_proc_info(thd, 0); - pthread_mutex_unlock(&thd->mysys_var->mutex); + mdl_downgrade_exclusive_locks(&thd->mdl_context); } DBUG_RETURN(result); } @@ -1079,7 +1251,7 @@ bool close_cached_connection_tables(THD *thd, bool if_wait_for_refresh, } if (tables) - result= close_cached_tables(thd, tables, TRUE, FALSE, FALSE); + result= close_cached_tables(thd, tables, TRUE, FALSE); if (!have_lock) pthread_mutex_unlock(&LOCK_open); @@ -1208,9 +1380,8 @@ static void close_open_tables(THD *thd) thd->some_tables_deleted= 0; /* Free tables to hold down open files */ - while (open_cache.records > table_cache_size && unused_tables) - my_hash_delete(&open_cache,(uchar*) unused_tables); /* purecov: tested */ - check_unused(); + while (table_cache_count > table_cache_size && unused_tables) + free_cache_entry(unused_tables); if (found_old_table) { /* Tell threads waiting for refresh that something has happened */ @@ -1239,7 +1410,8 @@ static void close_open_tables(THD *thd) leave prelocked mode if needed. */ -void close_thread_tables(THD *thd) +void close_thread_tables(THD *thd, + bool skip_mdl) { TABLE *table; prelocked_mode_type prelocked_mode= thd->prelocked_mode; @@ -1328,6 +1500,10 @@ void close_thread_tables(THD *thd) if (prelocked_mode == PRELOCKED_UNDER_LOCK_TABLES) DBUG_VOID_RETURN; + /* + Note that we are leaving prelocked mode so we don't need + to care about THD::locked_tables_root. + */ thd->lock= thd->locked_tables; thd->locked_tables= 0; /* Fallthrough */ @@ -1358,6 +1534,12 @@ void close_thread_tables(THD *thd) if (thd->open_tables) close_open_tables(thd); + mdl_release_locks(&thd->mdl_context); + if (!skip_mdl) + { + mdl_remove_all_locks(&thd->mdl_context); + } + if (prelocked_mode == PRELOCKED) { /* @@ -1381,8 +1563,7 @@ bool close_thread_table(THD *thd, TABLE **table_ptr) DBUG_ENTER("close_thread_table"); DBUG_ASSERT(table->key_read == 0); DBUG_ASSERT(!table->file || table->file->inited == handler::NONE); - DBUG_PRINT("tcache", ("table: '%s'.'%s' 0x%lx", table->s->db.str, - table->s->table_name.str, (long) table)); + safe_mutex_assert_owner(&LOCK_open); *table_ptr=table->next; /* @@ -1392,10 +1573,11 @@ bool close_thread_table(THD *thd, TABLE **table_ptr) if (table->child_l || table->parent) detach_merge_children(table, TRUE); + table->mdl_lock= 0; if (table->needs_reopen_or_name_lock() || thd->version != refresh_version || !table->db_stat) { - my_hash_delete(&open_cache,(uchar*) table); + free_cache_entry(table); found_old_table=1; } else @@ -1413,16 +1595,7 @@ bool close_thread_table(THD *thd, TABLE **table_ptr) free_field_buffers_larger_than(table,MAX_TDC_BLOB_SIZE); table->file->ha_reset(); - table->in_use=0; - if (unused_tables) - { - table->next=unused_tables; /* Link in last */ - table->prev=unused_tables->prev; - unused_tables->prev=table; - table->prev->next=table; - } - else - unused_tables=table->next=table->prev=table; + table_def_unuse_table(table); } DBUG_RETURN(found_old_table); } @@ -2121,7 +2294,7 @@ void unlink_open_table(THD *thd, TABLE *find, bool unlock) /* Remove table from open_tables list. */ *prev= list->next; /* Close table. */ - my_hash_delete(&open_cache,(uchar*) list); // Close table + free_cache_entry(list); } else { @@ -2231,43 +2404,34 @@ void wait_for_condition(THD *thd, pthread_mutex_t *mutex, pthread_cond_t *cond) bool name_lock_locked_table(THD *thd, TABLE_LIST *tables) { + bool result= TRUE; + DBUG_ENTER("name_lock_locked_table"); /* Under LOCK TABLES we must only accept write locked tables. */ - tables->table= find_locked_table(thd, tables->db, tables->table_name); + tables->table= find_write_locked_table(thd->open_tables, tables->db, + tables->table_name); - if (!tables->table) - my_error(ER_TABLE_NOT_LOCKED, MYF(0), tables->alias); - else if (tables->table->reginfo.lock_type < TL_WRITE_LOW_PRIORITY) - my_error(ER_TABLE_NOT_LOCKED_FOR_WRITE, MYF(0), tables->alias); - else + if (tables->table) { /* Ensures that table is opened only by this thread and that no other statement will open this table. */ - wait_while_table_is_used(thd, tables->table, HA_EXTRA_FORCE_REOPEN); - DBUG_RETURN(FALSE); + result= wait_while_table_is_used(thd, tables->table, HA_EXTRA_FORCE_REOPEN); } - DBUG_RETURN(TRUE); + DBUG_RETURN(result); } /* - Open table which is already name-locked by this thread. + Open table for which this thread has exclusive meta-data lock. SYNOPSIS reopen_name_locked_table() thd Thread handle - table_list TABLE_LIST object for table to be open, TABLE_LIST::table - member should point to TABLE object which was used for - name-locking. - link_in TRUE - if TABLE object for table to be opened should be - linked into THD::open_tables list. - FALSE - placeholder used for name-locking is already in - this list so we only need to preserve TABLE::next - pointer. + table_list TABLE_LIST object for table to be open. NOTE This function assumes that its caller already acquired LOCK_open mutex. @@ -2277,33 +2441,32 @@ bool name_lock_locked_table(THD *thd, TABLE_LIST *tables) TRUE - Error */ -bool reopen_name_locked_table(THD* thd, TABLE_LIST* table_list, bool link_in) +bool reopen_name_locked_table(THD* thd, TABLE_LIST* table_list) { - TABLE *table= table_list->table; + TABLE *table; TABLE_SHARE *share; + char key[MAX_DBKEY_LENGTH]; + uint key_length; char *table_name= table_list->table_name; - TABLE orig_table; DBUG_ENTER("reopen_name_locked_table"); - safe_mutex_assert_owner(&LOCK_open); - - if (thd->killed || !table) + if (thd->killed) DBUG_RETURN(TRUE); - orig_table= *table; + key_length= create_table_def_key(thd, key, table_list, 0); + + if (!(table=(TABLE*) my_malloc(sizeof(*table),MYF(MY_WME)))) + DBUG_RETURN(TRUE); - if (open_unireg_entry(thd, table, table_list, table_name, - table->s->table_cache_key.str, - table->s->table_cache_key.length, thd->mem_root, 0)) + if (reopen_table_entry(thd, table, table_list, table_name, key, key_length)) { - intern_close_table(table); /* If there was an error during opening of table (for example if it does not exist) '*table' object can be wiped out. To be able properly release name-lock in this case we should restore this object to its original state. */ - *table= orig_table; + my_free((uchar*)table, MYF(0)); DBUG_RETURN(TRUE); } @@ -2318,21 +2481,11 @@ bool reopen_name_locked_table(THD* thd, TABLE_LIST* table_list, bool link_in) */ share->version=0; table->in_use = thd; - check_unused(); - if (link_in) - { - table->next= thd->open_tables; - thd->open_tables= table; - } - else - { - /* - TABLE object should be already in THD::open_tables list so we just - need to set TABLE::next correctly. - */ - table->next= orig_table.next; - } + table_def_add_used_table(thd, table); + + table->next= thd->open_tables; + thd->open_tables= table; table->tablenr=thd->current_tablenr++; table->used_fields=0; @@ -2340,109 +2493,7 @@ bool reopen_name_locked_table(THD* thd, TABLE_LIST* table_list, bool link_in) table->null_row= table->maybe_null= 0; table->force_index= table->force_index_order= table->force_index_group= 0; table->status=STATUS_NO_RECORD; - DBUG_RETURN(FALSE); -} - - -/** - Create and insert into table cache placeholder for table - which will prevent its opening (or creation) (a.k.a lock - table name). - - @param thd Thread context - @param key Table cache key for name to be locked - @param key_length Table cache key length - - @return Pointer to TABLE object used for name locking or 0 in - case of failure. -*/ - -TABLE *table_cache_insert_placeholder(THD *thd, const char *key, - uint key_length) -{ - TABLE *table; - TABLE_SHARE *share; - char *key_buff; - DBUG_ENTER("table_cache_insert_placeholder"); - - safe_mutex_assert_owner(&LOCK_open); - - /* - Create a table entry with the right key and with an old refresh version - Note that we must use my_multi_malloc() here as this is freed by the - table cache - */ - if (!my_multi_malloc(MYF(MY_WME | MY_ZEROFILL), - &table, sizeof(*table), - &share, sizeof(*share), - &key_buff, key_length, - NULL)) - DBUG_RETURN(NULL); - - table->s= share; - share->set_table_cache_key(key_buff, key, key_length); - share->tmp_table= INTERNAL_TMP_TABLE; // for intern_close_table - table->in_use= thd; - table->locked_by_name=1; - - if (my_hash_insert(&open_cache, (uchar*)table)) - { - my_free((uchar*) table, MYF(0)); - DBUG_RETURN(NULL); - } - - DBUG_RETURN(table); -} - - -/** - Obtain an exclusive name lock on the table if it is not cached - in the table cache. - - @param thd Thread context - @param db Name of database - @param table_name Name of table - @param[out] table Out parameter which is either: - - set to NULL if table cache contains record for - the table or - - set to point to the TABLE instance used for - name-locking. - - @note This function takes into account all records for table in table - cache, even placeholders used for name-locking. This means that - 'table' parameter can be set to NULL for some situations when - table does not really exist. - - @retval TRUE Error occured (OOM) - @retval FALSE Success. 'table' parameter set according to above rules. -*/ - -bool lock_table_name_if_not_cached(THD *thd, const char *db, - const char *table_name, TABLE **table) -{ - char key[MAX_DBKEY_LENGTH]; - uint key_length; - DBUG_ENTER("lock_table_name_if_not_cached"); - - key_length= (uint)(strmov(strmov(key, db) + 1, table_name) - key) + 1; - pthread_mutex_lock(&LOCK_open); - - if (my_hash_search(&open_cache, (uchar *)key, key_length)) - { - pthread_mutex_unlock(&LOCK_open); - DBUG_PRINT("info", ("Table is cached, name-lock is not obtained")); - *table= 0; - DBUG_RETURN(FALSE); - } - if (!(*table= table_cache_insert_placeholder(thd, key, key_length))) - { - pthread_mutex_unlock(&LOCK_open); - DBUG_RETURN(TRUE); - } - (*table)->open_placeholder= 1; - (*table)->next= thd->open_tables; - thd->open_tables= *table; - pthread_mutex_unlock(&LOCK_open); + table_list->table= table; DBUG_RETURN(FALSE); } @@ -2511,6 +2562,20 @@ bool check_if_table_exists(THD *thd, TABLE_LIST *table, bool *exists) } +/** + @brief Helper function used by MDL subsystem for releasing TABLE_SHARE + objects in cases when it no longer wants to cache reference to it. +*/ + +void table_share_release_hook(void *share) +{ + pthread_mutex_lock(&LOCK_open); + release_table_share((TABLE_SHARE*)share, RELEASE_NORMAL); + broadcast_refresh(); + pthread_mutex_unlock(&LOCK_open); +} + + /* Open a table. @@ -2518,13 +2583,17 @@ bool check_if_table_exists(THD *thd, TABLE_LIST *table, bool *exists) open_table() thd Thread context. table_list Open first table in list. - refresh INOUT Pointer to memory that will be set to 1 if - we need to close all tables and reopen them. + action INOUT Pointer to variable of enum_open_table_action type + which will be set according to action which is + required to remedy problem appeared during attempt + to open table. If this is a NULL pointer, then the table is not put in the thread-open-list. flags Bitmap of flags to modify how open works: MYSQL_LOCK_IGNORE_FLUSH - Open table even if - someone has done a flush or namelock on it. + someone has done a flush or there is a pending + exclusive metadata lock requests against it + (i.e. request high priority metadata lock). No version number checking is done. MYSQL_OPEN_TEMPORARY_ONLY - Open only temporary table not the base table or view. @@ -2545,21 +2614,23 @@ bool check_if_table_exists(THD *thd, TABLE_LIST *table, bool *exists) TABLE *open_table(THD *thd, TABLE_LIST *table_list, MEM_ROOT *mem_root, - bool *refresh, uint flags) + enum_open_table_action *action, uint flags) { reg1 TABLE *table; char key[MAX_DBKEY_LENGTH]; uint key_length; char *alias= table_list->alias; - HASH_SEARCH_STATE state; + MDL_LOCK *mdl_lock; + int error; + TABLE_SHARE *share; DBUG_ENTER("open_table"); /* Parsing of partitioning information from .frm needs thd->lex set up. */ DBUG_ASSERT(thd->lex->is_lex_started); /* find a unused table in the open table cache */ - if (refresh) - *refresh=0; + if (action) + *action= OT_NO_ACTION; /* an open table operation needs a lot of the stack space */ if (check_stack_overrun(thd, STACK_MIN_SIZE_FOR_OPEN, (uchar *)&alias)) @@ -2685,7 +2756,13 @@ TABLE *open_table(THD *thd, TABLE_LIST *table_list, MEM_ROOT *mem_root, Is this table a view and not a base table? (it is work around to allow to open view with locked tables, real fix will be made after definition cache will be made) + + Since opening of view which was not explicitly locked by LOCK + TABLES breaks metadata locking protocol (potentially can lead + to deadlocks) it should be disallowed. */ + if (mdl_is_lock_owner(&thd->mdl_context, 0, table_list->db, + table_list->table_name)) { char path[FN_REFLEN + 1]; enum legacy_db_type not_used; @@ -2693,21 +2770,12 @@ TABLE *open_table(THD *thd, TABLE_LIST *table_list, MEM_ROOT *mem_root, table_list->db, table_list->table_name, reg_ext, 0); if (mysql_frm_type(thd, path, ¬_used) == FRMTYPE_VIEW) { - /* - Will not be used (because it's VIEW) but has to be passed. - Also we will not free it (because it is a stack variable). - */ - TABLE tab; - table= &tab; - pthread_mutex_lock(&LOCK_open); - if (!open_unireg_entry(thd, table, table_list, alias, - key, key_length, mem_root, 0)) + if (!tdc_open_view(thd, table_list, alias, key, key_length, + mem_root, 0)) { DBUG_ASSERT(table_list->view != 0); - pthread_mutex_unlock(&LOCK_open); DBUG_RETURN(0); // VIEW } - pthread_mutex_unlock(&LOCK_open); } } /* @@ -2740,6 +2808,32 @@ TABLE *open_table(THD *thd, TABLE_LIST *table_list, MEM_ROOT *mem_root, on disk. */ + mdl_lock= table_list->mdl_lock; + mdl_add_lock(&thd->mdl_context, mdl_lock); + + if (table_list->open_table_type) + { + mdl_set_lock_type(mdl_lock, MDL_EXCLUSIVE); + /* TODO: This case can be significantly optimized. */ + if (mdl_acquire_exclusive_locks(&thd->mdl_context)) + DBUG_RETURN(0); + } + else + { + bool retry; + + if (table_list->mdl_upgradable) + mdl_set_upgradable(mdl_lock); + mdl_set_lock_priority(mdl_lock, (flags & MYSQL_LOCK_IGNORE_FLUSH) ? + MDL_HIGH_PRIO : MDL_NORMAL_PRIO); + if (mdl_acquire_shared_lock(mdl_lock, &retry)) + { + if (action && retry) + *action= OT_BACK_OFF_AND_RETRY; + DBUG_RETURN(0); + } + } + pthread_mutex_lock(&LOCK_open); /* @@ -2756,222 +2850,210 @@ TABLE *open_table(THD *thd, TABLE_LIST *table_list, MEM_ROOT *mem_root, ! (flags & MYSQL_LOCK_IGNORE_FLUSH)) { /* Someone did a refresh while thread was opening tables */ - if (refresh) - *refresh=1; + if (action) + *action= OT_BACK_OFF_AND_RETRY; pthread_mutex_unlock(&LOCK_open); DBUG_RETURN(0); } - /* - In order for the back off and re-start process to work properly, - handler tables having old versions (due to FLUSH TABLES or pending - name-lock) MUST be closed. This is specially important if a name-lock - is pending for any table of the handler_tables list, otherwise a - deadlock may occur. - */ - if (thd->handler_tables) - mysql_ha_flush(thd); - - /* - Actually try to find the table in the open_cache. - The cache may contain several "TABLE" instances for the same - physical table. The instances that are currently "in use" by - some thread have their "in_use" member != NULL. - There is no good reason for having more than one entry in the - hash for the same physical table, except that we use this as - an implicit "pending locks queue" - see - wait_for_locked_table_names for details. - */ - for (table= (TABLE*) my_hash_first(&open_cache, (uchar*) key, key_length, - &state); - table && table->in_use ; - table= (TABLE*) my_hash_next(&open_cache, (uchar*) key, key_length, - &state)) + if (table_list->open_table_type == TABLE_LIST::OPEN_OR_CREATE) { - DBUG_PRINT("tcache", ("in_use table: '%s'.'%s' 0x%lx", table->s->db.str, - table->s->table_name.str, (long) table)); - /* - Here we flush tables marked for flush. - Normally, table->s->version contains the value of - refresh_version from the moment when this table was - (re-)opened and added to the cache. - If since then we did (or just started) FLUSH TABLES - statement, refresh_version has been increased. - For "name-locked" TABLE instances, table->s->version is set - to 0 (see lock_table_name for details). - In case there is a pending FLUSH TABLES or a name lock, we - need to back off and re-start opening tables. - If we do not back off now, we may dead lock in case of lock - order mismatch with some other thread: - c1: name lock t1; -- sort of exclusive lock - c2: open t2; -- sort of shared lock - c1: name lock t2; -- blocks - c2: open t1; -- blocks - */ - if (table->needs_reopen_or_name_lock()) - { - DBUG_PRINT("note", - ("Found table '%s.%s' with different refresh version", - table_list->db, table_list->table_name)); + bool exists; - if (flags & MYSQL_LOCK_IGNORE_FLUSH) - { - /* Force close at once after usage */ - thd->version= table->s->version; - continue; - } + if (check_if_table_exists(thd, table_list, &exists)) + goto err_unlock2; - /* Avoid self-deadlocks by detecting self-dependencies. */ - if (table->open_placeholder && table->in_use == thd) - { - pthread_mutex_unlock(&LOCK_open); - my_error(ER_UPDATE_TABLE_USED, MYF(0), table->s->table_name.str); - DBUG_RETURN(0); - } + if (!exists) + { + pthread_mutex_unlock(&LOCK_open); + DBUG_RETURN(0); + } + /* Table exists. Let us try to open it. */ + } + else if (table_list->open_table_type == TABLE_LIST::TAKE_EXCLUSIVE_MDL) + { + pthread_mutex_unlock(&LOCK_open); + DBUG_RETURN(0); + } + if (!(share= (TABLE_SHARE *)mdl_get_cached_object(mdl_lock))) + { + if (!(share= get_table_share_with_create(thd, table_list, key, + key_length, OPEN_VIEW, + &error))) + goto err_unlock2; + + if (share->is_view) + { /* - Back off, part 1: mark the table as "unused" for the - purpose of name-locking by setting table->db_stat to 0. Do - that only for the tables in this thread that have an old - table->s->version (this is an optimization (?)). - table->db_stat == 0 signals wait_for_locked_table_names - that the tables in question are not used any more. See - table_is_used call for details. - - Notice that HANDLER tables were already taken care of by - the earlier call to mysql_ha_flush() in this same critical - section. - */ - close_old_data_files(thd,thd->open_tables,0,0); - /* - Back-off part 2: try to avoid "busy waiting" on the table: - if the table is in use by some other thread, we suspend - and wait till the operation is complete: when any - operation that juggles with table->s->version completes, - it broadcasts COND_refresh condition variable. - If 'old' table we met is in use by current thread we return - without waiting since in this situation it's this thread - which is responsible for broadcasting on COND_refresh - (and this was done already in close_old_data_files()). - Good example of such situation is when we have statement - that needs two instances of table and FLUSH TABLES comes - after we open first instance but before we open second - instance. + This table is a view. Validate its metadata version: in particular, + that it was a view when the statement was prepared. */ - if (table->in_use != thd) + if (check_and_update_table_version(thd, table_list, share)) + goto err_unlock; + if (table_list->i_s_requested_object & OPEN_TABLE_ONLY) + goto err_unlock; + + /* Open view */ + if (open_new_frm(thd, share, alias, + (uint) (HA_OPEN_KEYFILE | HA_OPEN_RNDFILE | + HA_GET_INDEX | HA_TRY_READ_ONLY), + READ_KEYINFO | COMPUTE_TYPES | EXTRA_RECORD | + (flags & OPEN_VIEW_NO_PARSE), thd->open_options, + 0, table_list, mem_root)) + goto err_unlock; + + /* TODO: Don't free this */ + release_table_share(share, RELEASE_NORMAL); + + if (flags & OPEN_VIEW_NO_PARSE) { - /* wait_for_conditionwill unlock LOCK_open for us */ - wait_for_condition(thd, &LOCK_open, &COND_refresh); + /* + VIEW not really opened, only frm were read. + Set 1 as a flag here + */ + table_list->view= (LEX*)1; } else { - pthread_mutex_unlock(&LOCK_open); + DBUG_ASSERT(table_list->view); } + + pthread_mutex_unlock(&LOCK_open); + DBUG_RETURN(0); + } + else if (table_list->view) + { /* - There is a refresh in progress for this table. - Signal the caller that it has to try again. + We're trying to open a table for what was a view. + This can only happen during (re-)execution. + At prepared statement prepare the view has been opened and + merged into the statement parse tree. After that, someone + performed a DDL and replaced the view with a base table. + Don't try to open the table inside a prepared statement, + invalidate it instead. + + Note, the assert below is known to fail inside stored + procedures (Bug#27011). */ - if (refresh) - *refresh=1; - DBUG_RETURN(0); + DBUG_ASSERT(thd->m_reprepare_observer); + check_and_update_table_version(thd, table_list, share); + /* Always an error. */ + DBUG_ASSERT(thd->is_error()); + goto err_unlock; } + + if (table_list->i_s_requested_object & OPEN_VIEW_ONLY) + goto err_unlock; + + /* + We are going to to store extra reference to the share in MDL-subsystem + so we need to increase reference counter; + */ + reference_table_share(share); + mdl_set_cached_object(mdl_lock, share, table_share_release_hook); } - if (table) + else { - DBUG_PRINT("tcache", ("unused table: '%s'.'%s' 0x%lx", table->s->db.str, - table->s->table_name.str, (long) table)); - /* Unlink the table from "unused_tables" list. */ - if (table == unused_tables) - { // First unused - unused_tables=unused_tables->next; // Remove from link - if (table == unused_tables) - unused_tables=0; + if (table_list->view) + { + DBUG_ASSERT(thd->m_reprepare_observer); + check_and_update_table_version(thd, table_list, share); + /* Always an error. */ + DBUG_ASSERT(thd->is_error()); + goto err_unlock; } - table->prev->next=table->next; /* Remove from unused list */ - table->next->prev=table->prev; - table->in_use= thd; + /* When we have cached TABLE_SHARE we know that is not a view. */ + if (table_list->i_s_requested_object & OPEN_VIEW_ONLY) + goto err_unlock; + + /* + We are going to use this share for construction of new TABLE object + so reference counter should be increased. + */ + reference_table_share(share); + } + + if (share->version != refresh_version) + { + if (!(flags & MYSQL_LOCK_IGNORE_FLUSH)) + { + if (action) + *action= OT_BACK_OFF_AND_RETRY; + release_table_share(share, RELEASE_NORMAL); + pthread_mutex_unlock(&LOCK_open); + DBUG_RETURN(0); + } + /* Force close at once after usage */ + thd->version= share->version; + } + + if (!share->free_tables.is_empty()) + { + table= share->free_tables.head(); + table_def_use_table(thd, table); + /* We need to release share as we have EXTRA reference to it in our hands. */ + release_table_share(share, RELEASE_NORMAL); } else { - /* Insert a new TABLE instance into the open cache */ - int error; - DBUG_PRINT("tcache", ("opening new table")); - /* Free cache if too big */ - while (open_cache.records > table_cache_size && unused_tables) - my_hash_delete(&open_cache,(uchar*) unused_tables); /* purecov: tested */ + /* We have too many TABLE instances around let us try to get rid of them. */ + while (table_cache_count > table_cache_size && unused_tables) + free_cache_entry(unused_tables); - if (table_list->create) - { - bool exists; + /* make a new table */ + if (!(table=(TABLE*) my_malloc(sizeof(*table),MYF(MY_WME)))) + goto err_unlock; - if (check_if_table_exists(thd, table_list, &exists)) - { - pthread_mutex_unlock(&LOCK_open); - DBUG_RETURN(NULL); - } + error= open_table_from_share(thd, share, alias, + (uint) (HA_OPEN_KEYFILE | + HA_OPEN_RNDFILE | + HA_GET_INDEX | + HA_TRY_READ_ONLY), + (READ_KEYINFO | COMPUTE_TYPES | + EXTRA_RECORD), + thd->open_options, table, FALSE); + + if (error) + { + my_free(table, MYF(0)); - if (!exists) + if (action) { - /* - Table to be created, so we need to create placeholder in table-cache. - */ - if (!(table= table_cache_insert_placeholder(thd, key, key_length))) + if (error == 7) { - pthread_mutex_unlock(&LOCK_open); - DBUG_RETURN(NULL); + share->version= 0; + *action= OT_DISCOVER; + } + else if (share->crashed) + { + share->version= 0; + *action= OT_REPAIR; } - /* - Link placeholder to the open tables list so it will be automatically - removed once tables are closed. Also mark it so it won't be ignored - by other trying to take name-lock. - */ - table->open_placeholder= 1; - table->next= thd->open_tables; - thd->open_tables= table; - pthread_mutex_unlock(&LOCK_open); - DBUG_RETURN(table); } - /* Table exists. Let us try to open it. */ - } - /* make a new table */ - if (!(table=(TABLE*) my_malloc(sizeof(*table),MYF(MY_WME)))) - { - pthread_mutex_unlock(&LOCK_open); - DBUG_RETURN(NULL); + goto err_unlock; } - error= open_unireg_entry(thd, table, table_list, alias, key, key_length, - mem_root, (flags & OPEN_VIEW_NO_PARSE)); - if (error > 0) + if (open_table_entry_fini(thd, share, table)) { + closefrm(table, 0); my_free((uchar*)table, MYF(0)); - pthread_mutex_unlock(&LOCK_open); - DBUG_RETURN(NULL); + goto err_unlock; } - if (table_list->view || error < 0) - { - /* - VIEW not really opened, only frm were read. - Set 1 as a flag here - */ - if (error < 0) - table_list->view= (LEX*)1; - my_free((uchar*)table, MYF(0)); - pthread_mutex_unlock(&LOCK_open); - DBUG_RETURN(0); // VIEW - } - DBUG_PRINT("info", ("inserting table '%s'.'%s' 0x%lx into the cache", - table->s->db.str, table->s->table_name.str, - (long) table)); - (void) my_hash_insert(&open_cache,(uchar*) table); + /* Add table to the share's used tables list. */ + table_def_add_used_table(thd, table); } - check_unused(); // Debugging call - pthread_mutex_unlock(&LOCK_open); - if (refresh) + + // Table existed + if (table_list->open_table_type == TABLE_LIST::OPEN_OR_CREATE) + mdl_downgrade_exclusive_locks(&thd->mdl_context); + + table->mdl_lock= mdl_lock; + if (action) { table->next=thd->open_tables; /* Link into simple list */ thd->open_tables=table; @@ -3013,15 +3095,32 @@ TABLE *open_table(THD *thd, TABLE_LIST *table_list, MEM_ROOT *mem_root, table->clear_column_bitmaps(); DBUG_ASSERT(table->key_read == 0); DBUG_RETURN(table); + +err_unlock: + release_table_share(share, RELEASE_NORMAL); +err_unlock2: + pthread_mutex_unlock(&LOCK_open); + mdl_release_lock(&thd->mdl_context, mdl_lock); + DBUG_RETURN(0); } -TABLE *find_locked_table(THD *thd, const char *db,const char *table_name) +/** + Find table in the list of open tables. + + @param list List of TABLE objects to be inspected. + @param db Database name + @param table_name Table name + + @return Pointer to the TABLE object found, 0 if no table found. +*/ + +TABLE *find_locked_table(TABLE *list, const char *db, const char *table_name) { char key[MAX_DBKEY_LENGTH]; uint key_length=(uint) (strmov(strmov(key,db)+1,table_name)-key)+1; - for (TABLE *table=thd->open_tables; table ; table=table->next) + for (TABLE *table= list; table ; table=table->next) { if (table->s->table_cache_key.length == key_length && !memcmp(table->s->table_cache_key.str, key, key_length)) @@ -3031,6 +3130,41 @@ TABLE *find_locked_table(THD *thd, const char *db,const char *table_name) } +/** + Find write locked instance of table in the list of open tables, + emit error if no such instance found. + + @param thd List of TABLE objects to be searched + @param db Database name. + @param table_name Name of table. + + @return Pointer to write-locked TABLE instance, 0 - otherwise. +*/ + +TABLE *find_write_locked_table(TABLE *list, const char *db, const char *table_name) +{ + TABLE *tab= find_locked_table(list, db, table_name); + + if (!tab) + { + my_error(ER_TABLE_NOT_LOCKED, MYF(0), table_name); + return 0; + } + else + { + while (tab->reginfo.lock_type < TL_WRITE_LOW_PRIORITY && + (tab= find_locked_table(tab->next, db, table_name))) + continue; + if (!tab) + { + my_error(ER_TABLE_NOT_LOCKED_FOR_WRITE, MYF(0), table_name); + return 0; + } + } + return tab; +} + + /* Reopen an table because the definition has changed. @@ -3073,14 +3207,10 @@ bool reopen_table(TABLE *table) table_list.table_name= table->s->table_name.str; table_list.table= table; - if (wait_for_locked_table_names(thd, &table_list)) - DBUG_RETURN(1); // Thread was killed - - if (open_unireg_entry(thd, &tmp, &table_list, - table->alias, - table->s->table_cache_key.str, - table->s->table_cache_key.length, - thd->mem_root, 0)) + if (reopen_table_entry(thd, &tmp, &table_list, + table->alias, + table->s->table_cache_key.str, + table->s->table_cache_key.length)) goto end; /* This list copies variables set by open_table */ @@ -3112,6 +3242,12 @@ bool reopen_table(TABLE *table) (void) closefrm(&tmp, 1); // close file, free everything goto end; } + tmp.mdl_lock= table->mdl_lock; + + table_def_change_share(table, tmp.s); + /* Avoid wiping out TABLE's position in new share's used tables list. */ + tmp.share_next= table->share_next; + tmp.share_prev= table->share_prev; delete table->triggers; if (table->file) @@ -3214,6 +3350,7 @@ void close_data_files_and_morph_locks(THD *thd, const char *db, mysql_lock_remove(thd, thd->locked_tables, table, TRUE); } table->open_placeholder= 1; + table->s->version= 0; close_handle_and_leave_table_as_lock(table); } } @@ -3282,8 +3419,6 @@ 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 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 @@ -3297,11 +3432,11 @@ 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 mark_share_as_old) +bool reopen_tables(THD *thd, bool get_locks) { TABLE *table,*next,**prev; TABLE **tables,**tables_ptr; // For locks - TABLE *err_tables= NULL; + TABLE *err_tables= NULL, *err_tab_tmp; bool error=0, not_used; bool merge_table_found= FALSE; const uint flags= MYSQL_LOCK_NOTIFY_IF_NEED_REOPEN | @@ -3356,7 +3491,7 @@ bool reopen_tables(THD *thd, bool get_locks, bool mark_share_as_old) */ if (table->child_l || table->parent) detach_merge_children(table, TRUE); - my_hash_delete(&open_cache,(uchar*) table); + free_cache_entry(table); error=1; } else @@ -3367,11 +3502,15 @@ bool reopen_tables(THD *thd, bool get_locks, bool mark_share_as_old) prev= &table->next; /* Do not handle locks of MERGE children. */ if (get_locks && !db_stat && !table->parent) - *tables_ptr++= table; // need new lock on this - if (mark_share_as_old) { - table->s->version=0; - table->open_placeholder= 0; + *tables_ptr++= table; // need new lock on this + /* + We rely on having exclusive metadata lock on the table to be + able safely re-acquire table locks on it. + */ + DBUG_ASSERT(mdl_is_exclusive_lock_owner(&thd->mdl_context, 0, + table->s->db.str, + table->s->table_name.str)); } } } @@ -3385,8 +3524,9 @@ bool reopen_tables(THD *thd, bool get_locks, bool mark_share_as_old) { while (err_tables) { - my_hash_delete(&open_cache, (uchar*) err_tables); - err_tables= err_tables->next; + err_tab_tmp= err_tables->next; + free_cache_entry(err_tables); + err_tables= err_tab_tmp; } } DBUG_PRINT("tcache", ("open tables to lock: %u", @@ -3534,41 +3674,24 @@ bool table_is_used(TABLE *table, bool wait_for_name_lock) { char *key= table->s->table_cache_key.str; uint key_length= table->s->table_cache_key.length; - - DBUG_PRINT("loop", ("table_name: %s", table->alias)); - HASH_SEARCH_STATE state; - for (TABLE *search= (TABLE*) my_hash_first(&open_cache, (uchar*) key, - key_length, &state); - search ; - search= (TABLE*) my_hash_next(&open_cache, (uchar*) key, - key_length, &state)) - { - DBUG_PRINT("info", ("share: 0x%lx " - "open_placeholder: %d locked_by_name: %d " - "db_stat: %u version: %lu", - (ulong) search->s, - search->open_placeholder, search->locked_by_name, - search->db_stat, - search->s->version)); - if (search->in_use == table->in_use) - continue; // Name locked by this thread - /* - We can't use the table under any of the following conditions: - - There is an name lock on it (Table is to be deleted or altered) - - If we are in flush table and we didn't execute the flush - - If the table engine is open and it's an old version - (We must wait until all engines are shut down to use the table) - */ - if ( (search->locked_by_name && wait_for_name_lock) || - (search->is_name_opened() && search->needs_reopen_or_name_lock())) - DBUG_RETURN(1); - } + /* Note that 'table' can use artificial TABLE_SHARE object. */ + TABLE_SHARE *share= (TABLE_SHARE*)my_hash_search(&table_def_cache, + (uchar*) key, key_length); + if (share && !share->used_tables.is_empty() && + share->version != refresh_version) + DBUG_RETURN(1); } while ((table=table->next)); DBUG_RETURN(0); } -/* Wait until all used tables are refreshed */ +/* + Wait until all used tables are refreshed. + + FIXME We should remove this function since for several functions which + are invoked by it new scenarios of usage are introduced, while + this function implements optimization useful only in rare cases. +*/ bool wait_for_tables(THD *thd) { @@ -3593,7 +3716,7 @@ bool wait_for_tables(THD *thd) /* Now we can open all tables without any interference */ thd_proc_info(thd, "Reopen tables"); thd->version= refresh_version; - result=reopen_tables(thd,0,0); + result=reopen_tables(thd, 0); } pthread_mutex_unlock(&LOCK_open); thd_proc_info(thd, 0); @@ -3601,111 +3724,27 @@ bool wait_for_tables(THD *thd) } -/* - drop tables from locked list - - SYNOPSIS - drop_locked_tables() - thd Thread thandler - db Database - table_name Table name - - INFORMATION - This is only called on drop tables - - The TABLE object for the dropped table is unlocked but still kept around - as a name lock, which means that the table will be available for other - thread as soon as we call unlock_table_names(). - If there is multiple copies of the table locked, all copies except - the first, which acts as a name lock, is removed. +/** + Unlock and close tables open and locked by LOCK TABLES statement. - RETURN - # If table existed, return table - 0 Table was not locked + @param thd Current thread's context. */ - -TABLE *drop_locked_tables(THD *thd,const char *db, const char *table_name) +void unlock_locked_tables(THD *thd) { - TABLE *table,*next,**prev, *found= 0; - prev= &thd->open_tables; - DBUG_ENTER("drop_locked_tables"); - - /* - Note that we need to hold LOCK_open while changing the - open_tables list. Another thread may work on it. - (See: remove_table_from_cache(), mysql_wait_completed_table()) - Closing a MERGE child before the parent would be fatal if the - other thread tries to abort the MERGE lock in between. - */ - for (table= thd->open_tables; table ; table=next) - { - next=table->next; - if (!strcmp(table->s->table_name.str, table_name) && - !strcmp(table->s->db.str, db)) - { - /* If MERGE child, forward lock handling to parent. */ - mysql_lock_remove(thd, thd->locked_tables, - table->parent ? table->parent : table, TRUE); - /* - When closing a MERGE parent or child table, detach the children first. - Clear child table references in case this object is opened again. - */ - if (table->child_l || table->parent) - detach_merge_children(table, TRUE); + DBUG_ASSERT(!thd->in_sub_stmt && + !(thd->state_flags & Open_tables_state::BACKUPS_AVAIL)); - if (!found) - { - found= table; - /* Close engine table, but keep object around as a name lock */ - if (table->db_stat) - { - table->db_stat= 0; - table->file->close(); - } - } - else - { - /* We already have a name lock, remove copy */ - my_hash_delete(&open_cache,(uchar*) table); - } - } - else - { - *prev=table; - prev= &table->next; - } - } - *prev=0; - if (found) - broadcast_refresh(); - if (thd->locked_tables && thd->locked_tables->table_count == 0) + if (thd->locked_tables) { - my_free((uchar*) thd->locked_tables,MYF(0)); + thd->lock= thd->locked_tables; thd->locked_tables=0; - } - DBUG_RETURN(found); -} - - -/* - If we have the table open, which only happens when a LOCK TABLE has been - done on the table, change the lock type to a lock that will abort all - other threads trying to get the lock. -*/ - -void abort_locked_tables(THD *thd,const char *db, const char *table_name) -{ - TABLE *table; - for (table= thd->open_tables; table ; table= table->next) - { - if (!strcmp(table->s->table_name.str, table_name) && - !strcmp(table->s->db.str, db)) - { - /* If MERGE child, forward lock handling to parent. */ - mysql_lock_abort(thd, table->parent ? table->parent : table, TRUE); - break; - } + close_thread_tables(thd); + /* + After closing tables we can free memory used for storing lock + request objects for metadata locks + */ + free_root(&thd->locked_tables_root, MYF(MY_MARK_BLOCKS_FREE)); } } @@ -3812,7 +3851,7 @@ static bool inject_reprepare(THD *thd) @retval FALSE success, version in TABLE_LIST has been updated */ -bool +static bool check_and_update_table_version(THD *thd, TABLE_LIST *tables, TABLE_SHARE *table_share) { @@ -3838,41 +3877,98 @@ check_and_update_table_version(THD *thd, return FALSE; } -/* - Load a table definition from file and open unireg table - SYNOPSIS - open_unireg_entry() - thd Thread handle - entry Store open table definition here - table_list TABLE_LIST with db, table_name & belong_to_view - alias Alias name - cache_key Key for share_cache - cache_key_length length of cache_key - mem_root temporary mem_root for parsing - flags the OPEN_VIEW_NO_PARSE flag to be passed to - openfrm()/open_new_frm() +/** + Open view by getting its definition from disk (and table cache in future). - NOTES - Extra argument for open is taken from thd->open_options - One must have a lock on LOCK_open when calling this function + @param thd Thread handle + @param table_list TABLE_LIST with db, table_name & belong_to_view + @param alias Alias name + @param cache_key Key for table definition cache + @param cache_key_length Length of cache_key + @param mem_root Memory to be used for .frm parsing. + @param flags Flags which modify how we open the view - RETURN - 0 ok - # Error + @todo This function is needed for special handling of views under + LOCK TABLES. We probably should get rid of it in long term. + + @return FALSE if success, TRUE - otherwise. */ -static int open_unireg_entry(THD *thd, TABLE *entry, TABLE_LIST *table_list, - const char *alias, - char *cache_key, uint cache_key_length, - MEM_ROOT *mem_root, uint flags) +bool tdc_open_view(THD *thd, TABLE_LIST *table_list, const char *alias, + char *cache_key, uint cache_key_length, + MEM_ROOT *mem_root, uint flags) +{ + TABLE not_used; + int error; + TABLE_SHARE *share; + + pthread_mutex_lock(&LOCK_open); + + if (!(share= get_table_share_with_create(thd, table_list, cache_key, + cache_key_length, + OPEN_VIEW, &error))) + goto err; + + if (share->is_view && + !open_new_frm(thd, share, alias, + (uint) (HA_OPEN_KEYFILE | HA_OPEN_RNDFILE | + HA_GET_INDEX | HA_TRY_READ_ONLY), + READ_KEYINFO | COMPUTE_TYPES | EXTRA_RECORD | + flags, thd->open_options, ¬_used, table_list, + mem_root)) + { + release_table_share(share, RELEASE_NORMAL); + pthread_mutex_unlock(&LOCK_open); + return FALSE; + } + + my_error(ER_WRONG_OBJECT, MYF(0), share->db.str, share->table_name.str, "VIEW"); + release_table_share(share, RELEASE_NORMAL); +err: + pthread_mutex_unlock(&LOCK_open); + return TRUE; +} + + +/** + Load table definition from file and open table while holding exclusive + meta-data lock on it. + + @param thd Thread handle + @param entry Memory for TABLE object to be created + @param table_list TABLE_LIST with db, table_name & belong_to_view + @param alias Alias name + @param cache_key Key for table definition cache + @param cache_key_length Length of cache_key + + @note This auxiliary function is mostly inteded for re-opening table + in situations when we hold exclusive meta-data lock. It is not + intended for normal case in which we have only shared meta-data + lock on the table to be open. + + @note Extra argument for open is taken from thd->open_options. + + @note One must have a lock on LOCK_open as well as exclusive meta-data + lock on the table when calling this function. + + @return FALSE in case of success, TRUE otherwise. +*/ + +static bool reopen_table_entry(THD *thd, TABLE *entry, TABLE_LIST *table_list, + const char *alias, char *cache_key, + uint cache_key_length) { int error; TABLE_SHARE *share; uint discover_retry_count= 0; - DBUG_ENTER("open_unireg_entry"); + DBUG_ENTER("reopen_table_entry"); safe_mutex_assert_owner(&LOCK_open); + DBUG_ASSERT(mdl_is_exclusive_lock_owner(&thd->mdl_context, 0, + table_list->db, + table_list->table_name)); + retry: if (!(share= get_table_share_with_create(thd, table_list, cache_key, cache_key_length, @@ -3901,40 +3997,11 @@ retry: goto err; if (table_list->i_s_requested_object & OPEN_TABLE_ONLY) goto err; - - /* Open view */ - error= (int) open_new_frm(thd, share, alias, - (uint) (HA_OPEN_KEYFILE | HA_OPEN_RNDFILE | - HA_GET_INDEX | HA_TRY_READ_ONLY), - READ_KEYINFO | COMPUTE_TYPES | EXTRA_RECORD | - (flags & OPEN_VIEW_NO_PARSE), - thd->open_options, entry, table_list, - mem_root); - if (error) - goto err; - /* TODO: Don't free this */ + /* Attempt to reopen view will bring havoc to upper layers anyway. */ release_table_share(share, RELEASE_NORMAL); - DBUG_RETURN((flags & OPEN_VIEW_NO_PARSE)? -1 : 0); - } - else if (table_list->view) - { - /* - We're trying to open a table for what was a view. - This can only happen during (re-)execution. - At prepared statement prepare the view has been opened and - merged into the statement parse tree. After that, someone - performed a DDL and replaced the view with a base table. - Don't try to open the table inside a prepared statement, - invalidate it instead. - - Note, the assert below is known to fail inside stored - procedures (Bug#27011). - */ - DBUG_ASSERT(thd->m_reprepare_observer); - check_and_update_table_version(thd, table_list, share); - /* Always an error. */ - DBUG_ASSERT(thd->is_error()); - goto err; + my_error(ER_WRONG_OBJECT, MYF(0), share->db.str, share->table_name.str, + "BASE TABLE"); + DBUG_RETURN(1); } if (table_list->i_s_requested_object & OPEN_VIEW_ONLY) @@ -3956,89 +4023,67 @@ retry: goto err; /* - TODO: - Here we should wait until all threads has released the table. - For now we do one retry. This may cause a deadlock if there - is other threads waiting for other tables used by this thread. - - Proper fix would be to if the second retry failed: - - Mark that table def changed - - Return from open table - - Close all tables used by this thread - - Start waiting that the share is released - - Retry by opening all tables again + Since we have exclusive metadata lock on the table here the only + practical case when share->ref_count != 1 is when we have several + instances of the table opened by this thread (i.e we are under LOCK + TABLES). */ + if (share->ref_count != 1) + goto err; + + release_table_share(share, RELEASE_NORMAL); + if (ha_create_table_from_engine(thd, table_list->db, table_list->table_name)) goto err; - /* - TO BE FIXED - To avoid deadlock, only wait for release if no one else is - using the share. - */ - if (share->ref_count != 1) - goto err; - /* Free share and wait until it's released by all threads */ - release_table_share(share, RELEASE_WAIT_FOR_DROP); - if (!thd->killed) - { - thd->warning_info->clear_warning_info(thd->query_id); - thd->clear_error(); // Clear error message - goto retry; - } - DBUG_RETURN(1); + + thd->warning_info->clear_warning_info(thd->query_id); + thd->clear_error(); // Clear error message + goto retry; } if (!entry->s || !entry->s->crashed) goto err; - // Code below is for repairing a crashed file - if ((error= lock_table_name(thd, table_list, TRUE))) - { - if (error < 0) - goto err; - if (wait_for_locked_table_names(thd, table_list)) - { - unlock_table_name(thd, table_list); - goto err; - } - } - pthread_mutex_unlock(&LOCK_open); - thd->clear_error(); // Clear error message - error= 0; - if (open_table_from_share(thd, share, alias, - (uint) (HA_OPEN_KEYFILE | HA_OPEN_RNDFILE | - HA_GET_INDEX | - HA_TRY_READ_ONLY), - READ_KEYINFO | COMPUTE_TYPES | EXTRA_RECORD, - ha_open_options | HA_OPEN_FOR_REPAIR, - entry, FALSE) || ! entry->file || - (entry->file->is_crashed() && entry->file->ha_check_and_repair(thd))) - { - /* Give right error message */ - thd->clear_error(); - my_error(ER_NOT_KEYFILE, MYF(0), share->table_name.str, my_errno); - sql_print_error("Couldn't repair table: %s.%s", share->db.str, - share->table_name.str); - if (entry->file) - closefrm(entry, 0); - error=1; - } - else - thd->clear_error(); // Clear error message - pthread_mutex_lock(&LOCK_open); - unlock_table_name(thd, table_list); - - if (error) - goto err; - break; - } - if (Table_triggers_list::check_n_load(thd, share->db.str, - share->table_name.str, entry, 0)) + entry->s->version= 0; + + /* TODO: We don't need to release share here. */ + release_table_share(share, RELEASE_NORMAL); + pthread_mutex_unlock(&LOCK_open); + error= (int)auto_repair_table(thd, table_list); + pthread_mutex_lock(&LOCK_open); + + if (error) + goto err; + + goto retry; + } + + if (open_table_entry_fini(thd, share, entry)) { closefrm(entry, 0); goto err; } + DBUG_RETURN(0); + +err: + release_table_share(share, RELEASE_NORMAL); + DBUG_RETURN(1); +} + + +/** + Auxiliary routine which finalizes process of TABLE object creation + by loading triggers and handling implicitly emptied tables. +*/ + +static bool open_table_entry_fini(THD *thd, TABLE_SHARE *share, TABLE *entry) +{ + + if (Table_triggers_list::check_n_load(thd, share->db.str, + share->table_name.str, entry, 0)) + return TRUE; + /* If we are here, there was no fatal error (but error may be still unitialized). @@ -4070,18 +4115,141 @@ retry: */ sql_print_error("When opening HEAP table, could not allocate memory " "to write 'DELETE FROM `%s`.`%s`' to the binary log", - table_list->db, table_list->table_name); + share->db.str, share->table_name.str); delete entry->triggers; - closefrm(entry, 0); - goto err; + return TRUE; } } } - DBUG_RETURN(0); + return FALSE; +} -err: + +/** + Auxiliary routine which is used for performing automatical table repair. +*/ + +static bool auto_repair_table(THD *thd, TABLE_LIST *table_list) +{ + char cache_key[MAX_DBKEY_LENGTH]; + uint cache_key_length; + TABLE_SHARE *share; + TABLE *entry; + int not_used; + bool result= FALSE; + + cache_key_length= create_table_def_key(thd, cache_key, table_list, 0); + + thd->clear_error(); + + pthread_mutex_lock(&LOCK_open); + + if (!(share= get_table_share_with_create(thd, table_list, cache_key, + cache_key_length, + OPEN_VIEW, ¬_used))) + { + pthread_mutex_unlock(&LOCK_open); + return TRUE; + } + + if (share->is_view) + goto end_with_lock_open; + + if (!(entry= (TABLE*)my_malloc(sizeof(TABLE), MYF(MY_WME)))) + { + result= TRUE; + goto end_with_lock_open; + } + share->version= 0; + pthread_mutex_unlock(&LOCK_open); + + if (open_table_from_share(thd, share, table_list->alias, + (uint) (HA_OPEN_KEYFILE | HA_OPEN_RNDFILE | + HA_GET_INDEX | + HA_TRY_READ_ONLY), + READ_KEYINFO | COMPUTE_TYPES | EXTRA_RECORD, + ha_open_options | HA_OPEN_FOR_REPAIR, + entry, FALSE) || ! entry->file || + (entry->file->is_crashed() && entry->file->ha_check_and_repair(thd))) + { + /* Give right error message */ + thd->clear_error(); + my_error(ER_NOT_KEYFILE, MYF(0), share->table_name.str, my_errno); + sql_print_error("Couldn't repair table: %s.%s", share->db.str, + share->table_name.str); + if (entry->file) + closefrm(entry, 0); + result= TRUE; + } + else + { + thd->clear_error(); // Clear error message + closefrm(entry, 0); + } + my_free(entry, MYF(0)); + + pthread_mutex_lock(&LOCK_open); + +end_with_lock_open: release_table_share(share, RELEASE_NORMAL); - DBUG_RETURN(1); + pthread_mutex_unlock(&LOCK_open); + return result; +} + + +/** + Handle failed attempt ot open table by performing requested action. + + @param thd Thread context + @param table Table list element for table that caused problem + @param action Type of action requested by failed open_table() call + + @retval FALSE - Success. One should try to open tables once again. + @retval TRUE - Error +*/ + +static bool handle_failed_open_table_attempt(THD *thd, TABLE_LIST *table, + enum_open_table_action action) +{ + bool result= FALSE; + + switch (action) + { + case OT_BACK_OFF_AND_RETRY: + result= (mdl_wait_for_locks(&thd->mdl_context) || + tdc_wait_for_old_versions(thd, &thd->mdl_context)); + mdl_remove_all_locks(&thd->mdl_context); + break; + case OT_DISCOVER: + mdl_set_lock_type(table->mdl_lock, MDL_EXCLUSIVE); + mdl_add_lock(&thd->mdl_context, table->mdl_lock); + if (mdl_acquire_exclusive_locks(&thd->mdl_context)) + return TRUE; + pthread_mutex_lock(&LOCK_open); + expel_table_from_cache(0, table->db, table->table_name); + ha_create_table_from_engine(thd, table->db, table->table_name); + pthread_mutex_unlock(&LOCK_open); + + thd->warning_info->clear_warning_info(thd->query_id); + thd->clear_error(); // Clear error message + mdl_release_exclusive_locks(&thd->mdl_context); + break; + case OT_REPAIR: + mdl_set_lock_type(table->mdl_lock, MDL_EXCLUSIVE); + mdl_add_lock(&thd->mdl_context, table->mdl_lock); + if (mdl_acquire_exclusive_locks(&thd->mdl_context)) + return TRUE; + pthread_mutex_lock(&LOCK_open); + expel_table_from_cache(0, table->db, table->table_name); + pthread_mutex_unlock(&LOCK_open); + + result= auto_repair_table(thd, table); + mdl_release_exclusive_locks(&thd->mdl_context); + break; + default: + DBUG_ASSERT(0); + } + return result; } @@ -4132,6 +4300,7 @@ static int add_merge_table_list(TABLE_LIST *tlist) /* Set lock type. */ child_l->lock_type= tlist->lock_type; + child_l->mdl_upgradable= tlist->mdl_upgradable; /* Set parent reference. */ child_l->parent_l= tlist; @@ -4487,7 +4656,7 @@ thr_lock_type read_lock_type_for_table(THD *thd, TABLE *table) int open_tables(THD *thd, TABLE_LIST **start, uint *counter, uint flags) { TABLE_LIST *tables= NULL; - bool refresh; + enum_open_table_action action; int result=0; MEM_ROOT new_frm_mem; /* Also used for indicating that prelocking is need */ @@ -4508,6 +4677,18 @@ int open_tables(THD *thd, TABLE_LIST **start, uint *counter, uint flags) thd_proc_info(thd, "Opening tables"); /* + Close HANDLER tables which are marked for flush or against which there + are pending exclusive metadata locks. Note that we do this not to avoid + deadlocks (calls to mysql_ha_flush() in mdl_wait_for_locks() and + tdc_wait_for_old_version() are enough for this) but in order to have + a point during statement execution at which such HANDLERs are closed + even if they don't create problems for current thread (i.e. to avoid + having DDL blocked by HANDLERs opened for long time). + */ + if (thd->handler_tables) + mysql_ha_flush(thd); + + /* If we are not already executing prelocked statement and don't have statement for which table list for prelocking is already built, let us cache routines and try to build such table list. @@ -4556,9 +4737,18 @@ int open_tables(THD *thd, TABLE_LIST **start, uint *counter, uint flags) */ if (tables->derived) { - if (tables->view) - goto process_view_routines; - continue; + if (!tables->view) + continue; + /* + We restore view's name and database wiped out by derived tables + processing and fall back to standard open process in order to + obtain proper metadata locks and do other necessary steps like + stored routine processing. + */ + tables->db= tables->view_db.str; + tables->db_length= tables->view_db.length; + tables->table_name= tables->view_name.str; + tables->table_name_length= tables->view_name.length; } /* If this TABLE_LIST object is a placeholder for an information_schema @@ -4602,12 +4792,12 @@ int open_tables(THD *thd, TABLE_LIST **start, uint *counter, uint flags) */ Prelock_error_handler prelock_handler; thd->push_internal_handler(& prelock_handler); - tables->table= open_table(thd, tables, &new_frm_mem, &refresh, flags); + tables->table= open_table(thd, tables, &new_frm_mem, &action, flags); thd->pop_internal_handler(); safe_to_ignore_table= prelock_handler.safely_trapped_errors(); } else - tables->table= open_table(thd, tables, &new_frm_mem, &refresh, flags); + tables->table= open_table(thd, tables, &new_frm_mem, &action, flags); } else DBUG_PRINT("tcache", ("referenced table: '%s'.'%s' 0x%lx", @@ -4657,7 +4847,16 @@ int open_tables(THD *thd, TABLE_LIST **start, uint *counter, uint flags) parent_l->next_global= *parent_l->table->child_last_l; } - if (refresh) // Refresh in progress + /* + FIXME This is a temporary hack. Actually we need check that will + allow us to differentiate between error while opening/creating + table and successful table creation. + ... + */ + if (tables->open_table_type) + continue; + + if (action) { /* We have met name-locked or old version of table. Now we have @@ -4675,7 +4874,17 @@ int open_tables(THD *thd, TABLE_LIST **start, uint *counter, uint flags) */ if (query_tables_last_own) thd->lex->mark_as_requiring_prelocking(query_tables_last_own); - close_tables_for_reopen(thd, start); + close_tables_for_reopen(thd, start, (action == OT_BACK_OFF_AND_RETRY)); + /* + Here we rely on the fact that 'tables' still points to the valid + TABLE_LIST element. Altough currently this assumption is valid + it may change in future. + */ + if (handle_failed_open_table_attempt(thd, tables, action)) + { + result= -1; + goto err; + } goto restart; } @@ -4929,6 +5138,7 @@ TABLE *open_ltable(THD *thd, TABLE_LIST *table_list, thr_lock_type lock_type, uint lock_flags) { TABLE *table; + enum_open_table_action action; bool refresh; DBUG_ENTER("open_ltable"); @@ -4939,9 +5149,20 @@ TABLE *open_ltable(THD *thd, TABLE_LIST *table_list, thr_lock_type lock_type, thd->current_tablenr= 0; /* open_ltable can be used only for BASIC TABLEs */ table_list->required_type= FRMTYPE_TABLE; - while (!(table= open_table(thd, table_list, thd->mem_root, &refresh, 0)) && - refresh) - ; + +retry: + while (!(table= open_table(thd, table_list, thd->mem_root, &action, 0)) && + action) + { + /* + Even altough we have failed to open table we still need to + call close_thread_tables() to release metadata locks which + might have been acquired successfully. + */ + close_thread_tables(thd, (action == OT_BACK_OFF_AND_RETRY)); + if (handle_failed_open_table_attempt(thd, table_list, action)) + break; + } if (table) { @@ -4969,8 +5190,22 @@ TABLE *open_ltable(THD *thd, TABLE_LIST *table_list, thr_lock_type lock_type, DBUG_ASSERT(thd->lock == 0); // You must lock everything at once if ((table->reginfo.lock_type= lock_type) != TL_UNLOCK) if (! (thd->lock= mysql_lock_tables(thd, &table_list->table, 1, - lock_flags, &refresh))) - table= 0; + (lock_flags | + MYSQL_LOCK_NOTIFY_IF_NEED_REOPEN), + &refresh))) + { + /* + FIXME: Actually we should get rid of MYSQL_LOCK_NOTIFY_IF_NEED_REOPEN option + as all reopening should happen outside of mysql_lock_tables() code. + */ + if (refresh) + { + close_thread_tables(thd); + goto retry; + } + else + table= 0; + } } } @@ -5026,7 +5261,7 @@ int open_and_lock_tables_derived(THD *thd, TABLE_LIST *tables, bool derived) break; if (!need_reopen) DBUG_RETURN(-1); - close_tables_for_reopen(thd, &tables); + close_tables_for_reopen(thd, &tables, FALSE); } if (derived && (mysql_handle_derived(thd->lex, &mysql_derived_prepare) || @@ -5383,6 +5618,10 @@ int lock_tables(THD *thd, TABLE_LIST *tables, uint count, bool *need_reopen) table->table->query_id= thd->query_id; if (check_lock_and_start_stmt(thd, table->table, table->lock_type)) { + /* + This was an attempt to enter prelocked mode so there is no + need to care about THD::locked_tables_root here. + */ mysql_unlock_tables(thd, thd->locked_tables); thd->locked_tables= 0; thd->options&= ~(OPTION_TABLE_LOCK); @@ -5469,7 +5708,7 @@ int lock_tables(THD *thd, TABLE_LIST *tables, uint count, bool *need_reopen) */ -void close_tables_for_reopen(THD *thd, TABLE_LIST **tables) +void close_tables_for_reopen(THD *thd, TABLE_LIST **tables, bool skip_mdl) { /* If table list consists only from tables from prelocking set, table list @@ -5481,7 +5720,7 @@ void close_tables_for_reopen(THD *thd, TABLE_LIST **tables) sp_remove_not_own_routines(thd->lex); for (TABLE_LIST *tmp= *tables; tmp; tmp= tmp->next_global) tmp->table= 0; - close_thread_tables(thd); + close_thread_tables(thd, skip_mdl); } @@ -8393,36 +8632,6 @@ my_bool mysql_rm_tmp_tables(void) *****************************************************************************/ /* - Invalidate any cache entries that are for some DB - - SYNOPSIS - remove_db_from_cache() - db Database name. This will be in lower case if - lower_case_table_name is set - - NOTE: - We can't use hash_delete when looping hash_elements. We mark them first - and afterwards delete those marked unused. -*/ - -void remove_db_from_cache(const char *db) -{ - for (uint idx=0 ; idx < open_cache.records ; idx++) - { - TABLE *table=(TABLE*) my_hash_element(&open_cache,idx); - if (!strcmp(table->s->db.str, db)) - { - table->s->version= 0L; /* Free when thread is ready */ - if (!table->in_use) - relink_unused(table); - } - } - while (unused_tables && !unused_tables->s->version) - my_hash_delete(&open_cache,(uchar*) unused_tables); -} - - -/* free all unused tables NOTE @@ -8434,7 +8643,7 @@ void flush_tables() { (void) pthread_mutex_lock(&LOCK_open); while (unused_tables) - my_hash_delete(&open_cache,(uchar*) unused_tables); + free_cache_entry(unused_tables); (void) pthread_mutex_unlock(&LOCK_open); } @@ -8468,90 +8677,82 @@ bool remove_table_from_cache(THD *thd, const char *db, const char *table_name, key_length=(uint) (strmov(strmov(key,db)+1,table_name)-key)+1; for (;;) { - HASH_SEARCH_STATE state; result= signalled= 0; - for (table= (TABLE*) my_hash_first(&open_cache, (uchar*) key, key_length, - &state); - table; - table= (TABLE*) my_hash_next(&open_cache, (uchar*) key, key_length, - &state)) + if ((share= (TABLE_SHARE*) my_hash_search(&table_def_cache, (uchar*) key, + key_length))) { - THD *in_use; - DBUG_PRINT("tcache", ("found table: '%s'.'%s' 0x%lx", table->s->db.str, - table->s->table_name.str, (long) table)); - - table->s->version=0L; /* Free when thread is ready */ - if (!(in_use=table->in_use)) - { - DBUG_PRINT("info",("Table was not in use")); + I_P_List_iterator<TABLE, TABLE_share> it(share->free_tables); + share->version= 0; + while ((table= it++)) relink_unused(table); - } - else if (in_use != thd) + + it.init(share->used_tables); + while ((table= it++)) { - DBUG_PRINT("info", ("Table was in use by other thread")); - /* - Mark that table is going to be deleted from cache. This will - force threads that are in mysql_lock_tables() (but not yet - in thr_multi_lock()) to abort it's locks, close all tables and retry - */ - in_use->some_tables_deleted= 1; - if (table->is_name_opened()) - { - DBUG_PRINT("info", ("Found another active instance of the table")); - result=1; - } - /* Kill delayed insert threads */ - if ((in_use->system_thread & SYSTEM_THREAD_DELAYED_INSERT) && - ! in_use->killed) + THD *in_use= table->in_use; + DBUG_ASSERT(in_use); + if (in_use != thd) { - in_use->killed= THD::KILL_CONNECTION; - pthread_mutex_lock(&in_use->mysys_var->mutex); - if (in_use->mysys_var->current_cond) - { - pthread_mutex_lock(in_use->mysys_var->current_mutex); - signalled= 1; - pthread_cond_broadcast(in_use->mysys_var->current_cond); - pthread_mutex_unlock(in_use->mysys_var->current_mutex); - } - pthread_mutex_unlock(&in_use->mysys_var->mutex); + DBUG_PRINT("info", ("Table was in use by other thread")); + /* + Mark that table is going to be deleted from cache. This will + force threads that are in mysql_lock_tables() (but not yet + in thr_multi_lock()) to abort it's locks, close all tables and retry + */ + in_use->some_tables_deleted= 1; + + if (table->is_name_opened()) + { + DBUG_PRINT("info", ("Found another active instance of the table")); + result=1; + } + /* Kill delayed insert threads */ + if ((in_use->system_thread & SYSTEM_THREAD_DELAYED_INSERT) && + ! in_use->killed) + { + in_use->killed= THD::KILL_CONNECTION; + pthread_mutex_lock(&in_use->mysys_var->mutex); + if (in_use->mysys_var->current_cond) + { + pthread_mutex_lock(in_use->mysys_var->current_mutex); + signalled= 1; + pthread_cond_broadcast(in_use->mysys_var->current_cond); + pthread_mutex_unlock(in_use->mysys_var->current_mutex); + } + pthread_mutex_unlock(&in_use->mysys_var->mutex); + } + /* + Now we must abort all tables locks used by this thread + as the thread may be waiting to get a lock for another table. + Note that we need to hold LOCK_open while going through the + list. So that the other thread cannot change it. The other + thread must also hold LOCK_open whenever changing the + open_tables list. Aborting the MERGE lock after a child was + closed and before the parent is closed would be fatal. + */ + for (TABLE *thd_table= in_use->open_tables; + thd_table ; + thd_table= thd_table->next) + { + /* Do not handle locks of MERGE children. */ + if (thd_table->db_stat && !thd_table->parent) // If table is open + signalled|= mysql_lock_abort_for_thread(thd, thd_table); + } } - /* - Now we must abort all tables locks used by this thread - as the thread may be waiting to get a lock for another table. - Note that we need to hold LOCK_open while going through the - list. So that the other thread cannot change it. The other - thread must also hold LOCK_open whenever changing the - open_tables list. Aborting the MERGE lock after a child was - closed and before the parent is closed would be fatal. - */ - for (TABLE *thd_table= in_use->open_tables; - thd_table ; - thd_table= thd_table->next) + else { - /* Do not handle locks of MERGE children. */ - if (thd_table->db_stat && !thd_table->parent) // If table is open - signalled|= mysql_lock_abort_for_thread(thd, thd_table); + DBUG_PRINT("info", ("Table was in use by current thread. db_stat: %u", + table->db_stat)); + result= result || (flags & RTFC_OWNED_BY_THD_FLAG); } } - else - { - DBUG_PRINT("info", ("Table was in use by current thread. db_stat: %u", - table->db_stat)); - result= result || (flags & RTFC_OWNED_BY_THD_FLAG); - } - } - while (unused_tables && !unused_tables->s->version) - my_hash_delete(&open_cache,(uchar*) unused_tables); - DBUG_PRINT("info", ("Removing table from table_def_cache")); - /* Remove table from table definition cache if it's not in use */ - if ((share= (TABLE_SHARE*) my_hash_search(&table_def_cache,(uchar*) key, - key_length))) - { + while (unused_tables && !unused_tables->s->version) + free_cache_entry(unused_tables); + DBUG_PRINT("info", ("share version: %lu ref_count: %u", share->version, share->ref_count)); - share->version= 0; // Mark for delete if (share->ref_count == 0) { pthread_mutex_lock(&share->mutex); @@ -8598,6 +8799,160 @@ bool remove_table_from_cache(THD *thd, const char *db, const char *table_name, } +/** + A callback to the server internals that is used to address + special cases of the locking protocol. + Invoked when acquiring an exclusive lock, for each thread that + has a conflicting shared metadata lock. + + This function: + - aborts waiting of the thread on a data lock, to make it notice + the pending exclusive lock and back off. + - if the thread is an INSERT DELAYED thread, sends it a KILL + signal to terminate it. + + @note This function does not wait for the thread to give away its + locks. Waiting is done outside for all threads at once. + + @param thd Current thread context + @param in_use The thread to wake up + + @retval TRUE if the thread was woken up + @retval FALSE otherwise (e.g. it was not waiting for a table-level lock). + + @note It is one of two places where border between MDL and the + rest of the server is broken. +*/ + +bool notify_thread_having_shared_lock(THD *thd, THD *in_use) +{ + bool signalled= FALSE; + if ((in_use->system_thread & SYSTEM_THREAD_DELAYED_INSERT) && + !in_use->killed) + { + in_use->killed= THD::KILL_CONNECTION; + pthread_mutex_lock(&in_use->mysys_var->mutex); + if (in_use->mysys_var->current_cond) + pthread_cond_broadcast(in_use->mysys_var->current_cond); + pthread_mutex_unlock(&in_use->mysys_var->mutex); + signalled= TRUE; + } + pthread_mutex_lock(&LOCK_open); + for (TABLE *thd_table= in_use->open_tables; + thd_table ; + thd_table= thd_table->next) + { + /* TODO With new MDL check for db_stat is probably a legacy */ + if (thd_table->db_stat) + signalled|= mysql_lock_abort_for_thread(thd, thd_table); + } + pthread_mutex_unlock(&LOCK_open); + return signalled; +} + + +/** + Remove all instances of the table from cache assuming that current thread + has exclusive meta-data lock on it (optionally leave instances belonging + to the current thread in cache). + + @param leave_thd 0 If we should remove all instances + non-0 Pointer to current thread context if we should + leave instances belonging to this thread. + @param db Name of database + @param table_name Name of table + + @note Unlike remove_table_from_cache() it assumes that table instances + are already not used by any (other) thread (this should be achieved + by using meta-data locks). +*/ + +void expel_table_from_cache(THD *leave_thd, const char *db, const char *table_name) +{ + char key[MAX_DBKEY_LENGTH]; + uint key_length; + TABLE *table; + TABLE_SHARE *share; + + key_length=(uint) (strmov(strmov(key,db)+1,table_name)-key)+1; + + if ((share= (TABLE_SHARE*) my_hash_search(&table_def_cache,(uchar*) key, + key_length))) + { + I_P_List_iterator<TABLE, TABLE_share> it(share->free_tables); + share->version= 0; + + while ((table= it++)) + relink_unused(table); + } + + /* This may destroy share so we have to do new look-up later. */ + while (unused_tables && !unused_tables->s->version) + free_cache_entry(unused_tables); + + if ((share= (TABLE_SHARE*) my_hash_search(&table_def_cache,(uchar*) key, + key_length))) + { + DBUG_ASSERT(leave_thd || share->ref_count == 0); + if (share->ref_count == 0) + { + pthread_mutex_lock(&share->mutex); + my_hash_delete(&table_def_cache, (uchar*) share); + } + } +} + + +/** + Wait until there are no old versions of tables in the table + definition cache for the metadata locks that we try to acquire. + + @param thd Thread context + @param context Metadata locking context with locks. +*/ + +static bool tdc_wait_for_old_versions(THD *thd, MDL_CONTEXT *context) +{ + MDL_LOCK *l; + TABLE_SHARE *share; + const char *old_msg; + LEX_STRING key; + + while (!thd->killed) + { + /* + Here we have situation as in mdl_wait_for_locks() we need to + get rid of offending HANDLERs to avoid deadlock. + TODO: We should also investigate in which situations we have + to broadcast on COND_refresh because of this. + */ + mysql_ha_flush(thd); + pthread_mutex_lock(&LOCK_open); + + I_P_List_iterator<MDL_LOCK, MDL_LOCK_context> it= mdl_get_locks(context); + while ((l= it++)) + { + mdl_get_tdc_key(l, &key); + if ((share= (TABLE_SHARE*) my_hash_search(&table_def_cache, (uchar*) key.str, + key.length)) && + share->version != refresh_version && + !share->used_tables.is_empty()) + break; + } + if (!l) + { + pthread_mutex_unlock(&LOCK_open); + break; + } + old_msg= thd->enter_cond(&COND_refresh, &LOCK_open, "Waiting for table"); + pthread_cond_wait(&COND_refresh, &LOCK_open); + /* LOCK_open mutex is unlocked by THD::exit_cond() as side-effect. */ + thd->exit_cond(old_msg); + } + return thd->killed; +} + + int setup_ftfuncs(SELECT_LEX *select_lex) { List_iterator<Item_func_match> li(*(select_lex->ftfunc_list)), @@ -8695,7 +9050,6 @@ open_new_frm(THD *thd, TABLE_SHARE *share, const char *alias, } err: - bzero(outparam, sizeof(TABLE)); // do not run repair DBUG_RETURN(1); } @@ -8728,15 +9082,23 @@ bool is_equal(const LEX_STRING *a, const LEX_STRING *b) int abort_and_upgrade_lock(ALTER_PARTITION_PARAM_TYPE *lpt) { - uint flags= RTFC_WAIT_OTHER_THREAD_FLAG | RTFC_CHECK_KILLED_FLAG; DBUG_ENTER("abort_and_upgrade_locks"); lpt->old_lock_type= lpt->table->reginfo.lock_type; - pthread_mutex_lock(&LOCK_open); /* If MERGE child, forward lock handling to parent. */ mysql_lock_abort(lpt->thd, lpt->table->parent ? lpt->table->parent : lpt->table, TRUE); - (void) remove_table_from_cache(lpt->thd, lpt->db, lpt->table_name, flags); + if (mdl_upgrade_shared_lock_to_exclusive(&lpt->thd->mdl_context, 0, + lpt->db, lpt->table_name)) + { + mysql_lock_downgrade_write(lpt->thd, + lpt->table->parent ? lpt->table->parent : + lpt->table, + lpt->old_lock_type); + DBUG_RETURN(1); + } + pthread_mutex_lock(&LOCK_open); + expel_table_from_cache(lpt->thd, lpt->db, lpt->table_name); pthread_mutex_unlock(&LOCK_open); DBUG_RETURN(0); } @@ -8771,106 +9133,12 @@ void close_open_tables_and_downgrade(ALTER_PARTITION_PARAM_TYPE *lpt) /* - SYNOPSIS - mysql_wait_completed_table() - lpt Parameter passing struct - my_table My table object - All parameters passed through the ALTER_PARTITION_PARAM object - RETURN VALUES - TRUE Failure - FALSE Success - DESCRIPTION - We have changed the frm file and now we want to wait for all users of - the old frm to complete before proceeding to ensure that no one - remains that uses the old frm definition. - Start by ensuring that all users of the table will be removed from cache - once they are done. Then abort all that have stumbled on locks and - haven't been started yet. - - thd Thread object - table Table object - db Database name - table_name Table name -*/ + Tells if two (or more) tables have auto_increment columns and we want to + lock those tables with a write lock. -void mysql_wait_completed_table(ALTER_PARTITION_PARAM_TYPE *lpt, TABLE *my_table) -{ - char key[MAX_DBKEY_LENGTH]; - uint key_length; - TABLE *table; - DBUG_ENTER("mysql_wait_completed_table"); - - key_length=(uint) (strmov(strmov(key,lpt->db)+1,lpt->table_name)-key)+1; - pthread_mutex_lock(&LOCK_open); - HASH_SEARCH_STATE state; - for (table= (TABLE*) my_hash_first(&open_cache,(uchar*) key,key_length, - &state) ; - table; - table= (TABLE*) my_hash_next(&open_cache,(uchar*) key,key_length, - &state)) - { - THD *in_use= table->in_use; - table->s->version= 0L; - if (!in_use) - { - relink_unused(table); - } - else - { - /* Kill delayed insert threads */ - if ((in_use->system_thread & SYSTEM_THREAD_DELAYED_INSERT) && - ! in_use->killed) - { - in_use->killed= THD::KILL_CONNECTION; - pthread_mutex_lock(&in_use->mysys_var->mutex); - if (in_use->mysys_var->current_cond) - { - pthread_mutex_lock(in_use->mysys_var->current_mutex); - pthread_cond_broadcast(in_use->mysys_var->current_cond); - pthread_mutex_unlock(in_use->mysys_var->current_mutex); - } - pthread_mutex_unlock(&in_use->mysys_var->mutex); - } - /* - Now we must abort all tables locks used by this thread - as the thread may be waiting to get a lock for another table. - Note that we need to hold LOCK_open while going through the - list. So that the other thread cannot change it. The other - thread must also hold LOCK_open whenever changing the - open_tables list. Aborting the MERGE lock after a child was - closed and before the parent is closed would be fatal. - */ - for (TABLE *thd_table= in_use->open_tables; - thd_table ; - thd_table= thd_table->next) - { - /* Do not handle locks of MERGE children. */ - if (thd_table->db_stat && !thd_table->parent) // If table is open - mysql_lock_abort_for_thread(lpt->thd, thd_table); - } - } - } - /* - We start by removing all unused objects from the cache and marking - those in use for removal after completion. Now we also need to abort - all that are locked and are not progressing due to being locked - by our lock. We don't upgrade our lock here. - If MERGE child, forward lock handling to parent. - */ - mysql_lock_abort(lpt->thd, my_table->parent ? my_table->parent : my_table, - FALSE); - pthread_mutex_unlock(&LOCK_open); - DBUG_VOID_RETURN; -} - - -/* - Check if one (or more) write tables have auto_increment columns. - - @param[in] tables Table list - - @retval 0 if at least one write tables has an auto_increment column - @retval 1 otherwise + SYNOPSIS + has_two_write_locked_tables_with_auto_increment + tables Table list NOTES: Call this function only when you have established the list of all tables @@ -8924,10 +9192,13 @@ open_system_tables_for_read(THD *thd, TABLE_LIST *table_list, { DBUG_ENTER("open_system_tables_for_read"); + alloc_mdl_locks(table_list, thd->mem_root); + thd->reset_n_backup_open_tables_state(backup); uint count= 0; - bool not_used; + enum_open_table_action not_used; + bool not_used_2; for (TABLE_LIST *tables= table_list; tables; tables= tables->next_global) { TABLE *table= open_table(thd, tables, thd->mem_root, ¬_used, @@ -8950,7 +9221,7 @@ open_system_tables_for_read(THD *thd, TABLE_LIST *table_list, *(ptr++)= tables->table; thd->lock= mysql_lock_tables(thd, list, count, - MYSQL_LOCK_IGNORE_FLUSH, ¬_used); + MYSQL_LOCK_IGNORE_FLUSH, ¬_used_2); } if (thd->lock) DBUG_RETURN(FALSE); @@ -9002,6 +9273,8 @@ open_system_table_for_update(THD *thd, TABLE_LIST *one_table) { DBUG_ENTER("open_system_table_for_update"); + alloc_mdl_locks(one_table, thd->mem_root); + TABLE *table= open_ltable(thd, one_table, one_table->lock_type, 0); if (table) { @@ -9038,6 +9311,7 @@ open_performance_schema_table(THD *thd, TABLE_LIST *one_table, thd->reset_n_backup_open_tables_state(backup); + alloc_mdl_locks(one_table, thd->mem_root); if ((table= open_ltable(thd, one_table, one_table->lock_type, flags))) { DBUG_ASSERT(table->s->table_category == TABLE_CATEGORY_PERFORMANCE); @@ -9116,6 +9390,9 @@ void close_performance_schema_table(THD *thd, Open_tables_state *backup) pthread_mutex_unlock(&LOCK_open); + mdl_release_locks(&thd->mdl_context); + mdl_remove_all_locks(&thd->mdl_context); + thd->restore_backup_open_tables_state(backup); } diff --git a/sql/sql_binlog.cc b/sql/sql_binlog.cc index 58c309ef57b..31d4430cbe6 100644 --- a/sql/sql_binlog.cc +++ b/sql/sql_binlog.cc @@ -241,7 +241,7 @@ void mysql_client_binlog_statement(THD* thd) my_ok(thd); end: - rli->clear_tables_to_lock(); + rli->slave_close_thread_tables(thd); my_free(buf, MYF(MY_ALLOW_ZERO_PTR)); DBUG_VOID_RETURN; } diff --git a/sql/sql_class.cc b/sql/sql_class.cc index b7d88eca89a..9445f092546 100644 --- a/sql/sql_class.cc +++ b/sql/sql_class.cc @@ -202,10 +202,10 @@ bool foreign_key_prefix(Key *a, Key *b) ** Thread specific functions ****************************************************************************/ -Open_tables_state::Open_tables_state(ulong version_arg) +Open_tables_state::Open_tables_state(THD *thd, ulong version_arg) :version(version_arg), state_flags(0U) { - reset_open_tables_state(); + reset_open_tables_state(thd); } /* @@ -440,7 +440,7 @@ bool Drop_table_error_handler::handle_condition(THD *thd, THD::THD() :Statement(&main_lex, &main_mem_root, CONVENTIONAL_EXECUTION, /* statement id */ 0), - Open_tables_state(refresh_version), rli_fake(0), + Open_tables_state(this, refresh_version), rli_fake(0), lock_id(&main_lock_id), user_time(0), in_sub_stmt(0), sql_log_bin_toplevel(false), @@ -468,7 +468,8 @@ THD::THD() #if defined(ENABLED_DEBUG_SYNC) debug_sync_control(0), #endif /* defined(ENABLED_DEBUG_SYNC) */ - main_warning_info(0) + main_warning_info(0), + mdl_el_root(NULL) { ulong tmp; @@ -573,6 +574,8 @@ THD::THD() thr_lock_owner_init(&main_lock_id, &lock_info); m_internal_handler= NULL; + + init_sql_alloc(&locked_tables_root, ALLOC_ROOT_MIN_BLOCK_SIZE, 0); } @@ -1051,6 +1054,9 @@ THD::~THD() if (!cleanup_done) cleanup(); + mdl_context_destroy(&mdl_context); + mdl_context_destroy(&handler_mdl_context); + ha_close_connection(this); plugin_thdvar_cleanup(this); @@ -1072,6 +1078,7 @@ THD::~THD() #endif free_root(&main_mem_root, MYF(0)); + free_root(&locked_tables_root, MYF(0)); DBUG_VOID_RETURN; } @@ -3014,7 +3021,7 @@ void THD::reset_n_backup_open_tables_state(Open_tables_state *backup) { DBUG_ENTER("reset_n_backup_open_tables_state"); backup->set_open_tables_state(this); - reset_open_tables_state(); + reset_open_tables_state(this); state_flags|= Open_tables_state::BACKUPS_AVAIL; DBUG_VOID_RETURN; } @@ -3032,6 +3039,9 @@ void THD::restore_backup_open_tables_state(Open_tables_state *backup) lock == 0 && locked_tables == 0 && prelocked_mode == NON_PRELOCKED && m_reprepare_observer == NULL); + mdl_context_destroy(&mdl_context); + mdl_context_destroy(&handler_mdl_context); + set_open_tables_state(backup); DBUG_VOID_RETURN; } diff --git a/sql/sql_class.h b/sql/sql_class.h index 03a92c1a685..0f7d9d9a8d5 100644 --- a/sql/sql_class.h +++ b/sql/sql_class.h @@ -25,6 +25,7 @@ #include "log.h" #include "rpl_tblmap.h" +#include "mdl.h" class Reprepare_observer; @@ -976,26 +977,31 @@ public: */ uint state_flags; + MDL_CONTEXT mdl_context; + MDL_CONTEXT handler_mdl_context; + /* This constructor serves for creation of Open_tables_state instances which are used as backup storage. */ Open_tables_state() : state_flags(0U) { } - Open_tables_state(ulong version_arg); + Open_tables_state(THD *thd, ulong version_arg); void set_open_tables_state(Open_tables_state *state) { *this= *state; } - void reset_open_tables_state() + void reset_open_tables_state(THD *thd) { open_tables= temporary_tables= handler_tables= derived_tables= 0; extra_lock= lock= locked_tables= 0; prelocked_mode= NON_PRELOCKED; state_flags= 0U; m_reprepare_observer= NULL; + mdl_context_init(&mdl_context, thd); + mdl_context_init(&handler_mdl_context, thd); } }; @@ -1809,6 +1815,9 @@ public: struct st_debug_sync_control *debug_sync_control; #endif /* defined(ENABLED_DEBUG_SYNC) */ + MEM_ROOT *mdl_el_root; + MEM_ROOT locked_tables_root; + THD(); ~THD(); diff --git a/sql/sql_db.cc b/sql/sql_db.cc index 17626f05aa1..44909880da0 100644 --- a/sql/sql_db.cc +++ b/sql/sql_db.cc @@ -904,10 +904,6 @@ bool mysql_rm_db(THD *thd,char *db,bool if_exists, bool silent) } else { - pthread_mutex_lock(&LOCK_open); - remove_db_from_cache(db); - pthread_mutex_unlock(&LOCK_open); - Drop_table_error_handler err_handler(thd->get_internal_handler()); thd->push_internal_handler(&err_handler); diff --git a/sql/sql_delete.cc b/sql/sql_delete.cc index d8aa27c9695..fb48f32660b 100644 --- a/sql/sql_delete.cc +++ b/sql/sql_delete.cc @@ -1089,12 +1089,13 @@ bool mysql_truncate(THD *thd, TABLE_LIST *table_list, bool dont_send_ok) TABLE *table; bool error; uint path_length; + MDL_LOCK *mdl_lock= 0; DBUG_ENTER("mysql_truncate"); bzero((char*) &create_info,sizeof(create_info)); /* Remove tables from the HANDLER's hash. */ - mysql_ha_rm_tables(thd, table_list, FALSE); + mysql_ha_rm_tables(thd, table_list); /* If it is a temporary table, close and regenerate it */ if (!dont_send_ok && (table= find_temporary_table(thd, table_list))) @@ -1158,8 +1159,20 @@ bool mysql_truncate(THD *thd, TABLE_LIST *table_list, bool dont_send_ok) thd->lex->alter_info.flags & ALTER_ADMIN_PARTITION) goto trunc_by_del; - if (lock_and_wait_for_table_name(thd, table_list)) + /* + FIXME: Actually code of TRUNCATE breaks meta-data locking protocol since + tries to get table enging and therefore accesses table in some way + without holding any kind of meta-data lock. + */ + mdl_lock= mdl_alloc_lock(0, table_list->db, table_list->table_name, + thd->mem_root); + mdl_set_lock_type(mdl_lock, MDL_EXCLUSIVE); + mdl_add_lock(&thd->mdl_context, mdl_lock); + if (mdl_acquire_exclusive_locks(&thd->mdl_context)) DBUG_RETURN(TRUE); + pthread_mutex_lock(&LOCK_open); + expel_table_from_cache(0, table_list->db, table_list->table_name); + pthread_mutex_unlock(&LOCK_open); } // Remove the .frm extension AIX 5.2 64-bit compiler bug (BUG#16155): this @@ -1184,15 +1197,13 @@ end: write_bin_log(thd, TRUE, thd->query(), thd->query_length()); my_ok(thd); // This should return record count } - pthread_mutex_lock(&LOCK_open); - unlock_table_name(thd, table_list); - pthread_mutex_unlock(&LOCK_open); + if (mdl_lock) + mdl_release_lock(&thd->mdl_context, mdl_lock); } else if (error) { - pthread_mutex_lock(&LOCK_open); - unlock_table_name(thd, table_list); - pthread_mutex_unlock(&LOCK_open); + if (mdl_lock) + mdl_release_lock(&thd->mdl_context, mdl_lock); } DBUG_RETURN(error); diff --git a/sql/sql_handler.cc b/sql/sql_handler.cc index da5ee93fcb9..c8a66073a67 100644 --- a/sql/sql_handler.cc +++ b/sql/sql_handler.cc @@ -116,17 +116,16 @@ static void mysql_ha_hash_free(TABLE_LIST *tables) @param thd Thread identifier. @param tables A list of tables with the first entry to close. - @param is_locked If LOCK_open is locked. @note Though this function takes a list of tables, only the first list entry will be closed. @note Broadcasts refresh if it closed a table with old version. */ -static void mysql_ha_close_table(THD *thd, TABLE_LIST *tables, - bool is_locked) +static void mysql_ha_close_table(THD *thd, TABLE_LIST *tables) { TABLE **table_ptr; + MDL_LOCK *mdl_lock; /* Though we could take the table pointer from hash_tables->table, @@ -142,15 +141,15 @@ static void mysql_ha_close_table(THD *thd, TABLE_LIST *tables, if (*table_ptr) { (*table_ptr)->file->ha_index_or_rnd_end(); - if (! is_locked) - pthread_mutex_lock(&LOCK_open); + mdl_lock= (*table_ptr)->mdl_lock; + pthread_mutex_lock(&LOCK_open); if (close_thread_table(thd, table_ptr)) { /* Tell threads waiting for refresh that something has happened */ broadcast_refresh(); } - if (! is_locked) - pthread_mutex_unlock(&LOCK_open); + pthread_mutex_unlock(&LOCK_open); + mdl_release_lock(&thd->handler_mdl_context, mdl_lock); } else if (tables->table) { @@ -190,10 +189,12 @@ static void mysql_ha_close_table(THD *thd, TABLE_LIST *tables, bool mysql_ha_open(THD *thd, TABLE_LIST *tables, bool reopen) { TABLE_LIST *hash_tables = NULL; - char *db, *name, *alias; + MDL_LOCK *mdl_lock; + char *db, *name, *alias, *mdlkey; uint dblen, namelen, aliaslen, counter; int error; TABLE *backup_open_tables; + MDL_CONTEXT backup_mdl_context; DBUG_ENTER("mysql_ha_open"); DBUG_PRINT("enter",("'%s'.'%s' as '%s' reopen: %d", tables->db, tables->table_name, tables->alias, @@ -216,7 +217,10 @@ bool mysql_ha_open(THD *thd, TABLE_LIST *tables, bool reopen) HANDLER_TABLES_HASH_SIZE, 0, 0, (my_hash_get_key) mysql_ha_hash_get_key, (my_hash_free_key) mysql_ha_hash_free, 0)) - goto err; + { + DBUG_PRINT("exit",("ERROR")); + DBUG_RETURN(TRUE); + } } else if (! reopen) /* Otherwise we have 'tables' already. */ { @@ -224,10 +228,51 @@ bool mysql_ha_open(THD *thd, TABLE_LIST *tables, bool reopen) strlen(tables->alias) + 1)) { DBUG_PRINT("info",("duplicate '%s'", tables->alias)); + DBUG_PRINT("exit",("ERROR")); my_error(ER_NONUNIQ_TABLE, MYF(0), tables->alias); - goto err; + DBUG_RETURN(TRUE); + } + } + + if (! reopen) + { + /* copy the TABLE_LIST struct */ + dblen= strlen(tables->db) + 1; + namelen= strlen(tables->table_name) + 1; + aliaslen= strlen(tables->alias) + 1; + if (!(my_multi_malloc(MYF(MY_WME), + &hash_tables, (uint) sizeof(*hash_tables), + &db, (uint) dblen, + &name, (uint) namelen, + &alias, (uint) aliaslen, + &mdl_lock, sizeof(MDL_LOCK), + &mdlkey, MAX_DBKEY_LENGTH, + NullS))) + { + DBUG_PRINT("exit",("ERROR")); + DBUG_RETURN(TRUE); + } + /* structure copy */ + *hash_tables= *tables; + hash_tables->db= db; + hash_tables->table_name= name; + hash_tables->alias= alias; + memcpy(hash_tables->db, tables->db, dblen); + memcpy(hash_tables->table_name, tables->table_name, namelen); + memcpy(hash_tables->alias, tables->alias, aliaslen); + mdl_init_lock(mdl_lock, mdlkey, 0, db, name); + hash_tables->mdl_lock= mdl_lock; + + /* add to hash */ + if (my_hash_insert(&thd->handler_tables_hash, (uchar*) hash_tables)) + { + my_free((char*) hash_tables, MYF(0)); + DBUG_PRINT("exit",("ERROR")); + DBUG_RETURN(TRUE); } } + else + hash_tables= tables; /* Save and reset the open_tables list so that open_tables() won't @@ -243,21 +288,22 @@ bool mysql_ha_open(THD *thd, TABLE_LIST *tables, bool reopen) */ backup_open_tables= thd->open_tables; thd->open_tables= NULL; + mdl_context_backup_and_reset(&thd->mdl_context, &backup_mdl_context); /* - open_tables() will set 'tables->table' if successful. + open_tables() will set 'hash_tables->table' if successful. It must be NULL for a real open when calling open_tables(). */ - DBUG_ASSERT(! tables->table); + DBUG_ASSERT(! hash_tables->table); /* for now HANDLER can be used only for real TABLES */ - tables->required_type= FRMTYPE_TABLE; + hash_tables->required_type= FRMTYPE_TABLE; /* We use open_tables() here, rather than, say, open_ltable() or open_table() because we would like to be able to open a temporary table. */ - error= open_tables(thd, &tables, &counter, 0); + error= open_tables(thd, &hash_tables, &counter, 0); if (thd->open_tables) { if (thd->open_tables->next) @@ -281,52 +327,26 @@ bool mysql_ha_open(THD *thd, TABLE_LIST *tables, bool reopen) thd->handler_tables= thd->open_tables; } } + mdl_context_merge(&thd->handler_mdl_context, &thd->mdl_context); - /* Restore the state. */ thd->open_tables= backup_open_tables; + mdl_context_restore(&thd->mdl_context, &backup_mdl_context); if (error) goto err; /* There can be only one table in '*tables'. */ - if (! (tables->table->file->ha_table_flags() & HA_CAN_SQL_HANDLER)) + if (! (hash_tables->table->file->ha_table_flags() & HA_CAN_SQL_HANDLER)) { my_error(ER_ILLEGAL_HA, MYF(0), tables->alias); goto err; } - if (! reopen) - { - /* copy the TABLE_LIST struct */ - dblen= strlen(tables->db) + 1; - namelen= strlen(tables->table_name) + 1; - aliaslen= strlen(tables->alias) + 1; - if (!(my_multi_malloc(MYF(MY_WME), - &hash_tables, (uint) sizeof(*hash_tables), - &db, (uint) dblen, - &name, (uint) namelen, - &alias, (uint) aliaslen, - NullS))) - goto err; - /* structure copy */ - *hash_tables= *tables; - hash_tables->db= db; - hash_tables->table_name= name; - hash_tables->alias= alias; - memcpy(hash_tables->db, tables->db, dblen); - memcpy(hash_tables->table_name, tables->table_name, namelen); - memcpy(hash_tables->alias, tables->alias, aliaslen); - - /* add to hash */ - if (my_hash_insert(&thd->handler_tables_hash, (uchar*) hash_tables)) - goto err; - } - /* If it's a temp table, don't reset table->query_id as the table is being used by this handler. Otherwise, no meaning at all. */ - tables->table->open_by_handler= 1; + hash_tables->table->open_by_handler= 1; if (! reopen) my_ok(thd); @@ -334,10 +354,10 @@ bool mysql_ha_open(THD *thd, TABLE_LIST *tables, bool reopen) DBUG_RETURN(FALSE); err: - if (hash_tables) - my_free((char*) hash_tables, MYF(0)); - if (tables->table) - mysql_ha_close_table(thd, tables, FALSE); + if (hash_tables->table) + mysql_ha_close_table(thd, hash_tables); + if (!reopen) + my_hash_delete(&thd->handler_tables_hash, (uchar*) hash_tables); DBUG_PRINT("exit",("ERROR")); DBUG_RETURN(TRUE); } @@ -371,7 +391,7 @@ bool mysql_ha_close(THD *thd, TABLE_LIST *tables) (uchar*) tables->alias, strlen(tables->alias) + 1))) { - mysql_ha_close_table(thd, hash_tables, FALSE); + mysql_ha_close_table(thd, hash_tables); my_hash_delete(&thd->handler_tables_hash, (uchar*) hash_tables); } else @@ -507,7 +527,7 @@ retry: if (need_reopen) { - mysql_ha_close_table(thd, hash_tables, FALSE); + mysql_ha_close_table(thd, hash_tables); /* The lock might have been aborted, we need to manually reset thd->some_tables_deleted because handler's tables are closed @@ -734,12 +754,11 @@ static TABLE_LIST *mysql_ha_find(THD *thd, TABLE_LIST *tables) @param thd Thread identifier. @param tables The list of tables to remove. - @param is_locked If LOCK_open is locked. @note Broadcasts refresh if it closed a table with old version. */ -void mysql_ha_rm_tables(THD *thd, TABLE_LIST *tables, bool is_locked) +void mysql_ha_rm_tables(THD *thd, TABLE_LIST *tables) { TABLE_LIST *hash_tables, *next; DBUG_ENTER("mysql_ha_rm_tables"); @@ -752,7 +771,7 @@ void mysql_ha_rm_tables(THD *thd, TABLE_LIST *tables, bool is_locked) { next= hash_tables->next_local; if (hash_tables->table) - mysql_ha_close_table(thd, hash_tables, is_locked); + mysql_ha_close_table(thd, hash_tables); my_hash_delete(&thd->handler_tables_hash, (uchar*) hash_tables); hash_tables= next; } @@ -775,13 +794,16 @@ void mysql_ha_flush(THD *thd) TABLE_LIST *hash_tables; DBUG_ENTER("mysql_ha_flush"); - safe_mutex_assert_owner(&LOCK_open); + safe_mutex_assert_not_owner(&LOCK_open); for (uint i= 0; i < thd->handler_tables_hash.records; i++) { hash_tables= (TABLE_LIST*) my_hash_element(&thd->handler_tables_hash, i); - if (hash_tables->table && hash_tables->table->needs_reopen_or_name_lock()) - mysql_ha_close_table(thd, hash_tables, TRUE); + if (hash_tables->table && + (hash_tables->table->mdl_lock && + mdl_has_pending_conflicting_lock(hash_tables->table->mdl_lock) || + hash_tables->table->needs_reopen_or_name_lock())) + mysql_ha_close_table(thd, hash_tables); } DBUG_VOID_RETURN; @@ -805,7 +827,7 @@ void mysql_ha_cleanup(THD *thd) { hash_tables= (TABLE_LIST*) my_hash_element(&thd->handler_tables_hash, i); if (hash_tables->table) - mysql_ha_close_table(thd, hash_tables, FALSE); + mysql_ha_close_table(thd, hash_tables); } my_hash_free(&thd->handler_tables_hash); diff --git a/sql/sql_insert.cc b/sql/sql_insert.cc index efdc8caa3e5..eb4eee9abb5 100644 --- a/sql/sql_insert.cc +++ b/sql/sql_insert.cc @@ -612,7 +612,8 @@ bool mysql_insert(THD *thd,TABLE_LIST *table_list, upgrade the lock here instead? */ if (table_list->lock_type == TL_WRITE_DELAYED && thd->locked_tables && - find_locked_table(thd, table_list->db, table_list->table_name)) + find_locked_table(thd->open_tables, table_list->db, + table_list->table_name)) { my_error(ER_DELAYED_INSERT_TABLE_LOCKED, MYF(0), table_list->table_name); @@ -2351,6 +2352,8 @@ pthread_handler_t handle_delayed_insert(void *arg) thd->lex->set_stmt_unsafe(); thd->set_current_stmt_binlog_row_based_if_mixed(); + alloc_mdl_locks(&di->table_list, thd->mem_root); + /* Open table */ if (!(di->table= open_n_lock_single_table(thd, &di->table_list, TL_WRITE_DELAYED))) @@ -3495,7 +3498,7 @@ static TABLE *create_table_from_items(THD *thd, HA_CREATE_INFO *create_info, if (!(create_info->options & HA_LEX_CREATE_TMP_TABLE)) { pthread_mutex_lock(&LOCK_open); - if (reopen_name_locked_table(thd, create_table, FALSE)) + if (reopen_name_locked_table(thd, create_table)) { quick_rm_table(create_info->db_type, create_table->db, table_case_name(create_info, create_table->table_name), @@ -3507,7 +3510,8 @@ static TABLE *create_table_from_items(THD *thd, HA_CREATE_INFO *create_info, } else { - if (!(table= open_table(thd, create_table, thd->mem_root, (bool*) 0, + if (!(table= open_table(thd, create_table, thd->mem_root, + (enum_open_table_action*) 0, MYSQL_OPEN_TEMPORARY_ONLY)) && !create_info->table_existed) { @@ -3621,8 +3625,7 @@ select_create::prepare(List<Item> &values, SELECT_LEX_UNIT *u) DBUG_EXECUTE_IF("sleep_create_select_before_check_if_exists", my_sleep(6000000);); - if (!(create_info->options & HA_LEX_CREATE_TMP_TABLE) && - (create_table->table && create_table->table->db_stat)) + if (!(create_info->options & HA_LEX_CREATE_TMP_TABLE) && create_table->table) { /* Table already exists and was open at open_and_lock_tables() stage. */ if (create_info->options & HA_LEX_CREATE_IF_NOT_EXISTS) diff --git a/sql/sql_parse.cc b/sql/sql_parse.cc index 1271c3112ff..2b2c736fd9e 100644 --- a/sql/sql_parse.cc +++ b/sql/sql_parse.cc @@ -47,6 +47,7 @@ "FUNCTION" : "PROCEDURE") static bool execute_sqlcom_select(THD *thd, TABLE_LIST *all_tables); +static void adjust_mdl_locks_upgradability(TABLE_LIST *tables); const char *any_db="*any*"; // Special symbol for check_access @@ -143,17 +144,6 @@ static bool xa_trans_rollback(THD *thd) return status; } -static void unlock_locked_tables(THD *thd) -{ - if (thd->locked_tables) - { - thd->lock=thd->locked_tables; - thd->locked_tables=0; // Will be automatically closed - close_thread_tables(thd); // Free tables - } -} - - bool end_active_trans(THD *thd) { int error=0; @@ -194,12 +184,9 @@ bool begin_trans(THD *thd) my_error(ER_COMMIT_NOT_ALLOWED_IN_SF_OR_TRG, MYF(0)); return 1; } - if (thd->locked_tables) - { - thd->lock=thd->locked_tables; - thd->locked_tables=0; // Will be automatically closed - close_thread_tables(thd); // Free tables - } + + unlock_locked_tables(thd); + if (end_active_trans(thd)) error= -1; else @@ -1342,6 +1329,7 @@ bool dispatch_command(enum enum_server_command command, THD *thd, select_lex.table_list.link_in_list((uchar*) &table_list, (uchar**) &table_list.next_local); thd->lex->add_to_query_tables(&table_list); + alloc_mdl_locks(&table_list, thd->mem_root); /* switch on VIEW optimisation: do not fill temporary tables */ thd->lex->sql_command= SQLCOM_SHOW_FIELDS; @@ -2643,7 +2631,7 @@ case SQLCOM_PREPARE: if (!(create_info.options & HA_LEX_CREATE_TMP_TABLE)) { lex->link_first_table_back(create_table, link_to_local); - create_table->create= TRUE; + create_table->open_table_type= TABLE_LIST::OPEN_OR_CREATE; } if (!(res= open_and_lock_tables(thd, lex->query_tables))) @@ -3618,6 +3606,9 @@ end_with_restore_list: goto error; thd->in_lock_tables=1; thd->options|= OPTION_TABLE_LOCK; + alloc_mdl_locks(all_tables, &thd->locked_tables_root); + thd->mdl_el_root= &thd->locked_tables_root; + adjust_mdl_locks_upgradability(all_tables); if (!(res= simple_open_n_lock_tables(thd, all_tables))) { @@ -3641,6 +3632,7 @@ end_with_restore_list: thd->options&= ~(OPTION_TABLE_LOCK); } thd->in_lock_tables=0; + thd->mdl_el_root= 0; break; case SQLCOM_CREATE_DB: { @@ -6542,6 +6534,9 @@ TABLE_LIST *st_select_lex::add_table_to_list(THD *thd, ptr->next_name_resolution_table= NULL; /* Link table in global list (all used tables) */ lex->add_to_query_tables(ptr); + ptr->mdl_lock= mdl_alloc_lock(0 , ptr->db, ptr->table_name, + thd->mdl_el_root ? thd->mdl_el_root : + thd->mem_root); DBUG_RETURN(ptr); } @@ -7079,23 +7074,15 @@ bool reload_acl_and_cache(THD *thd, ulong options, TABLE_LIST *tables, if ((options & REFRESH_READ_LOCK) && thd) { /* - We must not try to aspire a global read lock if we have a write - locked table. This would lead to a deadlock when trying to - reopen (and re-lock) the table after the flush. + On the first hand we need write lock on the tables to be flushed, + on the other hand we must not try to aspire a global read lock + if we have a write locked table as this would lead to a deadlock + when trying to reopen (and re-lock) the table after the flush. */ if (thd->locked_tables) { - THR_LOCK_DATA **lock_p= thd->locked_tables->locks; - THR_LOCK_DATA **end_p= lock_p + thd->locked_tables->lock_count; - - for (; lock_p < end_p; lock_p++) - { - if ((*lock_p)->type >= TL_WRITE_ALLOW_WRITE) - { - my_error(ER_LOCK_OR_ACTIVE_TRANSACTION, MYF(0)); - return 1; - } - } + my_error(ER_LOCK_OR_ACTIVE_TRANSACTION, MYF(0)); + return 1; } /* Writing to the binlog could cause deadlocks, as we don't log @@ -7105,7 +7092,7 @@ bool reload_acl_and_cache(THD *thd, ulong options, TABLE_LIST *tables, if (lock_global_read_lock(thd)) return 1; // Killed if (close_cached_tables(thd, tables, FALSE, (options & REFRESH_FAST) ? - FALSE : TRUE, TRUE)) + FALSE : TRUE)) result= 1; if (make_global_read_lock_block_commit(thd)) // Killed @@ -7117,8 +7104,35 @@ bool reload_acl_and_cache(THD *thd, ulong options, TABLE_LIST *tables, } else { + if (thd && thd->locked_tables) + { + /* + If we are under LOCK TABLES we should have a write + lock on tables which we are going to flush. + */ + if (tables) + { + for (TABLE_LIST *t= tables; t; t= t->next_local) + if (!find_write_locked_table(thd->open_tables, t->db, + t->table_name)) + return 1; + } + else + { + for (TABLE *tab= thd->open_tables; tab; tab= tab->next) + { + if (tab->reginfo.lock_type < TL_WRITE_ALLOW_WRITE) + { + my_error(ER_TABLE_NOT_LOCKED_FOR_WRITE, MYF(0), + tab->s->table_name.str); + return 1; + } + } + } + } + if (close_cached_tables(thd, tables, FALSE, (options & REFRESH_FAST) ? - FALSE : TRUE, FALSE)) + FALSE : TRUE)) result= 1; } my_dbopt_cleanup(); @@ -8092,6 +8106,42 @@ bool parse_sql(THD *thd, return ret_value; } + +/** + Auxiliary function which marks metadata locks for all tables + on which we plan to take write lock as upgradable. +*/ + +static void adjust_mdl_locks_upgradability(TABLE_LIST *tables) +{ + TABLE_LIST *tab, *otab; + + for (tab= tables; tab; tab= tab->next_global) + { + if (tab->lock_type >= TL_WRITE_ALLOW_WRITE) + tab->mdl_upgradable= TRUE; + else + { + /* + TODO: To get rid of this loop we need to change our code to do + metadata lock upgrade only for those instances of tables + which are write locked instead of doing such upgrade for + all instances of tables. + */ + for (otab= tables; otab; otab= otab->next_global) + if (otab->lock_type >= TL_WRITE_ALLOW_WRITE && + otab->db_length == tab->db_length && + otab->table_name_length == tab->table_name_length && + !strcmp(otab->db, tab->db) && + !strcmp(otab->table_name, tab->table_name)) + { + tab->mdl_upgradable= TRUE; + break; + } + } + } +} + /** @} (end of group Runtime_Environment) */ diff --git a/sql/sql_partition.cc b/sql/sql_partition.cc index 868dfc3e968..48f33bf3295 100644 --- a/sql/sql_partition.cc +++ b/sql/sql_partition.cc @@ -6225,7 +6225,7 @@ static void alter_partition_lock_handling(ALTER_PARTITION_PARAM_TYPE *lpt) */ pthread_mutex_lock(&LOCK_open); lpt->thd->in_lock_tables= 1; - err= reopen_tables(lpt->thd, 1, 1); + err= reopen_tables(lpt->thd, 1); lpt->thd->in_lock_tables= 0; if (err) { @@ -6564,7 +6564,7 @@ uint fast_alter_partition_table(THD *thd, TABLE *table, write_log_drop_partition(lpt) || ERROR_INJECT_CRASH("crash_drop_partition_3") || (not_completed= FALSE) || - abort_and_upgrade_lock(lpt) || /* Always returns 0 */ + abort_and_upgrade_lock(lpt) || ERROR_INJECT_CRASH("crash_drop_partition_4") || alter_close_tables(lpt) || ERROR_INJECT_CRASH("crash_drop_partition_5") || @@ -6631,7 +6631,7 @@ uint fast_alter_partition_table(THD *thd, TABLE *table, ERROR_INJECT_CRASH("crash_add_partition_2") || mysql_change_partitions(lpt) || ERROR_INJECT_CRASH("crash_add_partition_3") || - abort_and_upgrade_lock(lpt) || /* Always returns 0 */ + abort_and_upgrade_lock(lpt) || ERROR_INJECT_CRASH("crash_add_partition_4") || alter_close_tables(lpt) || ERROR_INJECT_CRASH("crash_add_partition_5") || @@ -6647,7 +6647,7 @@ uint fast_alter_partition_table(THD *thd, TABLE *table, ERROR_INJECT_CRASH("crash_add_partition_8") || (write_log_completed(lpt, FALSE), FALSE) || ERROR_INJECT_CRASH("crash_add_partition_9") || - (alter_partition_lock_handling(lpt), FALSE)) + (alter_partition_lock_handling(lpt), FALSE)) { handle_alter_part_error(lpt, not_completed, FALSE, frm_install); goto err; @@ -6721,7 +6721,7 @@ uint fast_alter_partition_table(THD *thd, TABLE *table, write_log_final_change_partition(lpt) || ERROR_INJECT_CRASH("crash_change_partition_4") || (not_completed= FALSE) || - abort_and_upgrade_lock(lpt) || /* Always returns 0 */ + abort_and_upgrade_lock(lpt) || ERROR_INJECT_CRASH("crash_change_partition_5") || alter_close_tables(lpt) || ERROR_INJECT_CRASH("crash_change_partition_6") || diff --git a/sql/sql_plist.h b/sql/sql_plist.h new file mode 100644 index 00000000000..af2ed227ea1 --- /dev/null +++ b/sql/sql_plist.h @@ -0,0 +1,125 @@ +#ifndef SQL_PLIST_H +#define SQL_PLIST_H +/* Copyright (C) 2008 MySQL AB + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; version 2 of the License. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ + + +#include <my_global.h> + +template <typename T, typename B> class I_P_List_iterator; + + +/** + Intrusive parameterized list. + + Unlike I_List does not require its elements to be descendant of ilink + class and therefore allows them to participate in several such lists + simultaneously. + + Unlike List is doubly-linked list and thus supports efficient deletion + of element without iterator. + + @param T Type of elements which will belong to list. + @param B Class which via its methods specifies which members + of T should be used for participating in this list. + Here is typical layout of such class: + + struct B + { + static inline T **next_ptr(T *el) + { + return &el->next; + } + static inline T ***prev_ptr(T *el) + { + return &el->prev; + } + }; +*/ + +template <typename T, typename B> +class I_P_List +{ + T *first; + + /* + Do not prohibit copying of I_P_List object to simplify their usage in + backup/restore scenarios. Note that performing any operations on such + is a bad idea. + */ +public: + I_P_List() : first(NULL) { }; + inline void empty() { first= NULL; } + inline bool is_empty() { return (first == NULL); } + inline void push_front(T* a) + { + *B::next_ptr(a)= first; + if (first) + *B::prev_ptr(first)= B::next_ptr(a); + first= a; + *B::prev_ptr(a)= &first; + } + inline void remove(T *a) + { + T *next= *B::next_ptr(a); + if (next) + *B::prev_ptr(next)= *B::prev_ptr(a); + **B::prev_ptr(a)= next; + } + inline T* head() { return first; } + void swap(I_P_List<T,B> &rhs) + { + swap_variables(T *, first, rhs.first); + if (first) + *B::prev_ptr(first)= &first; + if (rhs.first) + *B::prev_ptr(rhs.first)= &rhs.first; + } +#ifndef _lint + friend class I_P_List_iterator<T, B>; +#endif +}; + + +/** + Iterator for I_P_List. +*/ + +template <typename T, typename B> +class I_P_List_iterator +{ + I_P_List<T, B> *list; + T *current; +public: + I_P_List_iterator(I_P_List<T, B> &a) : list(&a), current(a.first) {} + inline void init(I_P_List<T, B> &a) + { + list= &a; + current= a.first; + } + inline T* operator++(int) + { + T *result= current; + if (result) + current= *B::next_ptr(current); + return result; + } + inline void rewind() + { + current= list->first; + } +}; + +#endif diff --git a/sql/sql_plugin.cc b/sql/sql_plugin.cc index 936c9ae8866..e9f4152f861 100644 --- a/sql/sql_plugin.cc +++ b/sql/sql_plugin.cc @@ -1366,6 +1366,7 @@ static void plugin_load(MEM_ROOT *tmp_root, int *argc, char **argv) tables.alias= tables.table_name= (char*)"plugin"; tables.lock_type= TL_READ; tables.db= new_thd->db; + alloc_mdl_locks(&tables, tmp_root); #ifdef EMBEDDED_LIBRARY /* @@ -1659,6 +1660,8 @@ bool mysql_install_plugin(THD *thd, const LEX_STRING *name, const LEX_STRING *dl if (check_table_access(thd, INSERT_ACL, &tables, FALSE, 1, FALSE)) DBUG_RETURN(TRUE); + alloc_mdl_locks(&tables, thd->mem_root); + /* need to open before acquiring LOCK_plugin or it will deadlock */ if (! (table = open_ltable(thd, &tables, TL_WRITE, 0))) DBUG_RETURN(TRUE); @@ -1732,6 +1735,7 @@ bool mysql_uninstall_plugin(THD *thd, const LEX_STRING *name) bzero(&tables, sizeof(tables)); tables.db= (char *)"mysql"; tables.table_name= tables.alias= (char *)"plugin"; + alloc_mdl_locks(&tables, thd->mem_root); /* need to open before acquiring LOCK_plugin or it will deadlock */ if (! (table= open_ltable(thd, &tables, TL_WRITE, 0))) diff --git a/sql/sql_prepare.cc b/sql/sql_prepare.cc index d624c22f43a..582e18a3abf 100644 --- a/sql/sql_prepare.cc +++ b/sql/sql_prepare.cc @@ -1673,7 +1673,7 @@ static bool mysql_test_create_table(Prepared_statement *stmt) if (!(lex->create_info.options & HA_LEX_CREATE_TMP_TABLE)) { lex->link_first_table_back(create_table, link_to_local); - create_table->create= TRUE; + create_table->open_table_type= TABLE_LIST::OPEN_OR_CREATE; } if (open_normal_and_derived_tables(stmt->thd, lex->query_tables, 0)) diff --git a/sql/sql_rename.cc b/sql/sql_rename.cc index dac96f2e9c4..857cccde50f 100644 --- a/sql/sql_rename.cc +++ b/sql/sql_rename.cc @@ -51,7 +51,7 @@ bool mysql_rename_tables(THD *thd, TABLE_LIST *table_list, bool silent) DBUG_RETURN(1); } - mysql_ha_rm_tables(thd, table_list, FALSE); + mysql_ha_rm_tables(thd, table_list); if (wait_if_global_read_lock(thd,0,1)) DBUG_RETURN(1); @@ -133,12 +133,13 @@ bool mysql_rename_tables(THD *thd, TABLE_LIST *table_list, bool silent) } } - pthread_mutex_lock(&LOCK_open); - if (lock_table_names_exclusively(thd, table_list)) - { - pthread_mutex_unlock(&LOCK_open); + if (lock_table_names(thd, table_list)) goto err; - } + + pthread_mutex_lock(&LOCK_open); + + for (ren_table= table_list; ren_table; ren_table= ren_table->next_local) + expel_table_from_cache(0, ren_table->db, ren_table->table_name); error=0; if ((ren_table=rename_tables(thd,table_list,0))) @@ -184,9 +185,7 @@ bool mysql_rename_tables(THD *thd, TABLE_LIST *table_list, bool silent) if (!error) query_cache_invalidate3(thd, table_list, 0); - pthread_mutex_lock(&LOCK_open); - unlock_table_names(thd, table_list, (TABLE_LIST*) 0); - pthread_mutex_unlock(&LOCK_open); + unlock_table_names(thd); err: start_waiting_global_read_lock(thd); diff --git a/sql/sql_servers.cc b/sql/sql_servers.cc index e5fe06ce39b..5251a50cab9 100644 --- a/sql/sql_servers.cc +++ b/sql/sql_servers.cc @@ -224,12 +224,7 @@ bool servers_reload(THD *thd) bool return_val= TRUE; DBUG_ENTER("servers_reload"); - if (thd->locked_tables) - { // Can't have locked tables here - thd->lock=thd->locked_tables; - thd->locked_tables=0; - close_thread_tables(thd); - } + unlock_locked_tables(thd); // Can't have locked tables here DBUG_PRINT("info", ("locking servers_cache")); rw_wrlock(&THR_LOCK_servers); @@ -238,6 +233,7 @@ bool servers_reload(THD *thd) tables[0].alias= tables[0].table_name= (char*) "servers"; tables[0].db= (char*) "mysql"; tables[0].lock_type= TL_READ; + alloc_mdl_locks(tables, thd->mem_root); if (simple_open_n_lock_tables(thd, tables)) { @@ -368,6 +364,7 @@ insert_server(THD *thd, FOREIGN_SERVER *server) bzero((char*) &tables, sizeof(tables)); tables.db= (char*) "mysql"; tables.alias= tables.table_name= (char*) "servers"; + alloc_mdl_locks(&tables, thd->mem_root); /* need to open before acquiring THR_LOCK_plugin or it will deadlock */ if (! (table= open_ltable(thd, &tables, TL_WRITE, 0))) @@ -586,6 +583,7 @@ int drop_server(THD *thd, LEX_SERVER_OPTIONS *server_options) bzero((char*) &tables, sizeof(tables)); tables.db= (char*) "mysql"; tables.alias= tables.table_name= (char*) "servers"; + alloc_mdl_locks(&tables, thd->mem_root); rw_wrlock(&THR_LOCK_servers); @@ -710,6 +708,7 @@ int update_server(THD *thd, FOREIGN_SERVER *existing, FOREIGN_SERVER *altered) bzero((char*) &tables, sizeof(tables)); tables.db= (char*)"mysql"; tables.alias= tables.table_name= (char*)"servers"; + alloc_mdl_locks(&tables, thd->mem_root); if (!(table= open_ltable(thd, &tables, TL_WRITE, 0))) { diff --git a/sql/sql_show.cc b/sql/sql_show.cc index babadc34842..c83a6981166 100644 --- a/sql/sql_show.cc +++ b/sql/sql_show.cc @@ -2963,7 +2963,7 @@ fill_schema_show_cols_or_idxs(THD *thd, TABLE_LIST *tables, table, res, db_name, table_name)); thd->temporary_tables= 0; - close_tables_for_reopen(thd, &show_table_list); + close_tables_for_reopen(thd, &show_table_list, FALSE); DBUG_RETURN(error); } @@ -3106,6 +3106,9 @@ static int fill_schema_table_from_frm(THD *thd,TABLE *table, char key[MAX_DBKEY_LENGTH]; uint key_length; char db_name_buff[NAME_LEN + 1], table_name_buff[NAME_LEN + 1]; + MDL_LOCK mdl_lock; + char mdlkey[MAX_DBKEY_LENGTH]; + bool retry; bzero((char*) &table_list, sizeof(TABLE_LIST)); bzero((char*) &tbl, sizeof(TABLE)); @@ -3130,6 +3133,34 @@ static int fill_schema_table_from_frm(THD *thd,TABLE *table, table_list.db= db_name->str; } + mdl_init_lock(&mdl_lock, mdlkey, 0, db_name->str, table_name->str); + table_list.mdl_lock= &mdl_lock; + mdl_add_lock(&thd->mdl_context, &mdl_lock); + mdl_set_lock_priority(&mdl_lock, MDL_HIGH_PRIO); + + /* + TODO: investigate if in this particular situation we can get by + simply obtaining internal lock of data-dictionary (ATM it + is LOCK_open) instead of obtaning full-blown metadata lock. + */ + while (1) + { + if (mdl_acquire_shared_lock(&mdl_lock, &retry)) + { + if (!retry || mdl_wait_for_locks(&thd->mdl_context)) + { + /* + Some error occured or we have been killed while waiting + for conflicting locks to go away, let the caller to handle + the situation. + */ + return 1; + } + continue; + } + break; + } + key_length= create_table_def_key(thd, key, &table_list, 0); pthread_mutex_lock(&LOCK_open); share= get_table_share(thd, &table_list, key, @@ -3137,7 +3168,7 @@ static int fill_schema_table_from_frm(THD *thd,TABLE *table, if (!share) { res= 0; - goto err; + goto err_unlock; } if (share->is_view) @@ -3146,7 +3177,7 @@ static int fill_schema_table_from_frm(THD *thd,TABLE *table, { /* skip view processing */ res= 0; - goto err1; + goto err_share; } else if (schema_table->i_s_requested_object & OPEN_VIEW_FULL) { @@ -3155,7 +3186,7 @@ static int fill_schema_table_from_frm(THD *thd,TABLE *table, open_normal_and_derived_tables() */ res= 1; - goto err1; + goto err_share; } } @@ -3171,14 +3202,17 @@ static int fill_schema_table_from_frm(THD *thd,TABLE *table, res= schema_table->process_table(thd, &table_list, table, res, db_name, table_name); closefrm(&tbl, true); - goto err; + goto err_unlock; } -err1: +err_share: release_table_share(share, RELEASE_NORMAL); -err: +err_unlock: pthread_mutex_unlock(&LOCK_open); + +err: + mdl_release_lock(&thd->mdl_context, &mdl_lock); thd->clear_error(); return res; } @@ -3425,7 +3459,7 @@ int get_all_tables(THD *thd, TABLE_LIST *tables, COND *cond) res= schema_table->process_table(thd, show_table_list, table, res, &orig_db_name, &tmp_lex_string); - close_tables_for_reopen(thd, &show_table_list); + close_tables_for_reopen(thd, &show_table_list, FALSE); } DBUG_ASSERT(!lex->query_tables_own_last); if (res) @@ -7273,6 +7307,8 @@ bool show_create_trigger(THD *thd, const sp_name *trg_name) uint num_tables; /* NOTE: unused, only to pass to open_tables(). */ + alloc_mdl_locks(lst, thd->mem_root); + if (open_tables(thd, &lst, &num_tables, 0)) { my_error(ER_TRG_CANT_OPEN_TABLE, MYF(0), diff --git a/sql/sql_table.cc b/sql/sql_table.cc index dc0c876e882..f91bae8b76c 100644 --- a/sql/sql_table.cc +++ b/sql/sql_table.cc @@ -53,6 +53,7 @@ static bool mysql_prepare_alter_table(THD *thd, TABLE *table, HA_CREATE_INFO *create_info, Alter_info *alter_info); +static bool close_cached_table(THD *thd, TABLE *table); #ifndef DBUG_OFF @@ -1791,11 +1792,6 @@ bool mysql_rm_table(THD *thd,TABLE_LIST *tables, my_bool if_exists, DBUG_RETURN(TRUE); } - /* - Acquire LOCK_open after wait_if_global_read_lock(). If we would hold - LOCK_open during wait_if_global_read_lock(), other threads could not - close their tables. This would make a pretty deadlock. - */ thd->push_internal_handler(&err_handler); error= mysql_rm_table_part2(thd, tables, if_exists, drop_temporary, 0, 0); thd->pop_internal_handler(); @@ -1867,16 +1863,14 @@ int mysql_rm_table_part2(THD *thd, TABLE_LIST *tables, bool if_exists, built_query.append("DROP TABLE "); } - mysql_ha_rm_tables(thd, tables, FALSE); - - pthread_mutex_lock(&LOCK_open); + mysql_ha_rm_tables(thd, tables); /* If we have the table in the definition cache, we don't have to check the .frm file to find if the table is a normal table (not view) and what engine to use. */ - + pthread_mutex_lock(&LOCK_open); for (table= tables; table; table= table->next_local) { TABLE_SHARE *share; @@ -1889,16 +1883,32 @@ int mysql_rm_table_part2(THD *thd, TABLE_LIST *tables, bool if_exists, check_if_log_table(table->db_length, table->db, table->table_name_length, table->table_name, 1)) { - my_error(ER_BAD_LOG_STATEMENT, MYF(0), "DROP"); pthread_mutex_unlock(&LOCK_open); + my_error(ER_BAD_LOG_STATEMENT, MYF(0), "DROP"); DBUG_RETURN(1); } } + pthread_mutex_unlock(&LOCK_open); - if (!drop_temporary && lock_table_names_exclusively(thd, tables)) + if (!drop_temporary) { - pthread_mutex_unlock(&LOCK_open); - DBUG_RETURN(1); + if (!thd->locked_tables) + { + if (lock_table_names(thd, tables)) + DBUG_RETURN(1); + pthread_mutex_lock(&LOCK_open); + for (table= tables; table; table= table->next_local) + expel_table_from_cache(0, table->db, table->table_name); + pthread_mutex_unlock(&LOCK_open); + } + else if (thd->locked_tables) + { + for (table= tables; table; table= table->next_local) + if (!find_temporary_table(thd, table->db, table->table_name) && + !find_write_locked_table(thd->open_tables, table->db, + table->table_name)) + DBUG_RETURN(1); + } } for (table= tables; table; table= table->next_local) @@ -1973,17 +1983,21 @@ int mysql_rm_table_part2(THD *thd, TABLE_LIST *tables, bool if_exists, table_type= table->db_type; if (!drop_temporary) { - TABLE *locked_table; - abort_locked_tables(thd, db, table->table_name); - remove_table_from_cache(thd, db, table->table_name, - RTFC_WAIT_OTHER_THREAD_FLAG | - RTFC_CHECK_KILLED_FLAG); - /* - If the table was used in lock tables, remember it so that - unlock_table_names can free it - */ - if ((locked_table= drop_locked_tables(thd, db, table->table_name))) - table->table= locked_table; + if (thd->locked_tables) + { + TABLE *tab= find_locked_table(thd->open_tables, db, table->table_name); + if (close_cached_table(thd, tab)) + { + error= -1; + goto err_with_placeholders; + } + /* + Leave LOCK TABLES mode if we managed to drop all tables + which were locked. + */ + if (thd->locked_tables->table_count == 0) + unlock_locked_tables(thd); + } if (thd->killed) { @@ -1997,6 +2011,11 @@ int mysql_rm_table_part2(THD *thd, TABLE_LIST *tables, bool if_exists, table->internal_tmp_table ? FN_IS_TMP : 0); } + /* + TODO: Investigate what should be done to remove this lock completely. + Is exclusive meta-data lock enough ? + */ + pthread_mutex_lock(&LOCK_open); if (drop_temporary || ((table_type == NULL && access(path, F_OK) && @@ -2049,6 +2068,7 @@ int mysql_rm_table_part2(THD *thd, TABLE_LIST *tables, bool if_exists, error|= new_error; } } + pthread_mutex_unlock(&LOCK_open); if (error) { if (wrong_tables.length()) @@ -2064,11 +2084,6 @@ int mysql_rm_table_part2(THD *thd, TABLE_LIST *tables, bool if_exists, table->table_name);); } - /* - It's safe to unlock LOCK_open: we have an exclusive lock - on the table name. - */ - pthread_mutex_unlock(&LOCK_open); thd->thread_specific_used|= tmp_table_deleted; error= 0; if (wrong_tables.length()) @@ -2151,10 +2166,19 @@ int mysql_rm_table_part2(THD *thd, TABLE_LIST *tables, bool if_exists, */ } } - pthread_mutex_lock(&LOCK_open); err_with_placeholders: - unlock_table_names(thd, tables, (TABLE_LIST*) 0); - pthread_mutex_unlock(&LOCK_open); + if (!drop_temporary) + { + /* + Under LOCK TABLES we should release meta-data locks on the tables + which were dropped. Otherwise we can rely on close_thread_tables() + doing this. Unfortunately in this case we are likely to get more + false positives in lock_table_name_if_not_cached() function. So + it makes sense to remove exclusive meta-data locks in all cases. + */ + mdl_release_exclusive_locks(&thd->mdl_context); + } + DBUG_RETURN(error); } @@ -4005,6 +4029,34 @@ warn: } +/** + Auxiliary function which obtains exclusive meta-data lock on the + table if there are no shared or exclusive on it already. + + See mdl_try_acquire_exclusive_lock() function for more info. + + TODO: This function is here mostly to simplify current patch + and probably should be removed. + TODO: Investigate if it is kosher to leave lock request in the + context in the case when we fail to obtain the lock. +*/ + +static bool lock_table_name_if_not_cached(THD *thd, const char *db, + const char *table_name, + MDL_LOCK **lock) +{ + if (!(*lock= mdl_alloc_lock(0, db, table_name, thd->mem_root))) + return TRUE; + mdl_set_lock_type(*lock, MDL_EXCLUSIVE); + mdl_add_lock(&thd->mdl_context, *lock); + if (mdl_try_acquire_exclusive_lock(&thd->mdl_context, *lock)) + { + *lock= 0; + } + return FALSE; +} + + /* Database and name-locking aware wrapper for mysql_create_table_no_lock(), */ @@ -4015,7 +4067,7 @@ bool mysql_create_table(THD *thd, const char *db, const char *table_name, bool internal_tmp_table, uint select_field_count) { - TABLE *name_lock= 0; + MDL_LOCK *target_lock= 0; bool result; DBUG_ENTER("mysql_create_table"); @@ -4038,12 +4090,12 @@ bool mysql_create_table(THD *thd, const char *db, const char *table_name, if (!(create_info->options & HA_LEX_CREATE_TMP_TABLE)) { - if (lock_table_name_if_not_cached(thd, db, table_name, &name_lock)) + if (lock_table_name_if_not_cached(thd, db, table_name, &target_lock)) { result= TRUE; goto unlock; } - if (!name_lock) + if (!target_lock) { if (create_info->options & HA_LEX_CREATE_IF_NOT_EXISTS) { @@ -4069,12 +4121,8 @@ bool mysql_create_table(THD *thd, const char *db, const char *table_name, select_field_count); unlock: - if (name_lock) - { - pthread_mutex_lock(&LOCK_open); - unlink_open_table(thd, name_lock, FALSE); - pthread_mutex_unlock(&LOCK_open); - } + if (target_lock) + mdl_release_exclusive_locks(&thd->mdl_context); pthread_mutex_lock(&LOCK_lock_db); if (!--creating_table && creating_database) pthread_cond_signal(&COND_refresh); @@ -4212,80 +4260,83 @@ mysql_rename_table(handlerton *base, const char *old_db, } -/* - Force all other threads to stop using the table +/** + Force all other threads to stop using the table by upgrading + metadata lock on it and remove unused TABLE instances from cache. - SYNOPSIS - wait_while_table_is_used() - thd Thread handler - table Table to remove from cache - function HA_EXTRA_PREPARE_FOR_DROP if table is to be deleted - HA_EXTRA_FORCE_REOPEN if table is not be used - HA_EXTRA_PREPARE_FOR_RENAME if table is to be renamed - NOTES - When returning, the table will be unusable for other threads until - the table is closed. + @param thd Thread handler + @param table Table to remove from cache + @param function HA_EXTRA_PREPARE_FOR_DROP if table is to be deleted + HA_EXTRA_FORCE_REOPEN if table is not be used + HA_EXTRA_PREPARE_FOR_RENAME if table is to be renamed - PREREQUISITES - Lock on LOCK_open - Win32 clients must also have a WRITE LOCK on the table ! + @note When returning, the table will be unusable for other threads + until metadata lock is downgraded. + + @retval FALSE Success. + @retval TRUE Failure (e.g. because thread was killed). */ -void wait_while_table_is_used(THD *thd, TABLE *table, +bool wait_while_table_is_used(THD *thd, TABLE *table, enum ha_extra_function function) { + enum thr_lock_type old_lock_type; + DBUG_ENTER("wait_while_table_is_used"); DBUG_PRINT("enter", ("table: '%s' share: 0x%lx db_stat: %u version: %lu", table->s->table_name.str, (ulong) table->s, table->db_stat, table->s->version)); - safe_mutex_assert_owner(&LOCK_open); - (void) table->file->extra(function); - /* Mark all tables that are in use as 'old' */ + + old_lock_type= table->reginfo.lock_type; mysql_lock_abort(thd, table, TRUE); /* end threads waiting on lock */ - /* Wait until all there are no other threads that has this table open */ - remove_table_from_cache(thd, table->s->db.str, - table->s->table_name.str, - RTFC_WAIT_OTHER_THREAD_FLAG); - DBUG_VOID_RETURN; + if (mdl_upgrade_shared_lock_to_exclusive(&thd->mdl_context, 0, + table->s->db.str, + table->s->table_name.str)) + { + mysql_lock_downgrade_write(thd, table, old_lock_type); + DBUG_RETURN(TRUE); + } + + pthread_mutex_lock(&LOCK_open); + expel_table_from_cache(thd, table->s->db.str, table->s->table_name.str); + pthread_mutex_unlock(&LOCK_open); + DBUG_RETURN(FALSE); } -/* - Close a cached table - SYNOPSIS - close_cached_table() - thd Thread handler - table Table to remove from cache +/** + Upgrade metadata lock on the table and close all its instances. - NOTES - Function ends by signaling threads waiting for the table to try to - reopen the table. + @param thd Thread handler + @param table Table to remove from cache - PREREQUISITES - Lock on LOCK_open - Win32 clients must also have a WRITE LOCK on the table ! + @retval FALSE Success. + @retval TRUE Failure (e.g. because thread was killed). */ -void close_cached_table(THD *thd, TABLE *table) +static bool close_cached_table(THD *thd, TABLE *table) { DBUG_ENTER("close_cached_table"); - wait_while_table_is_used(thd, table, HA_EXTRA_FORCE_REOPEN); + /* FIXME: check if we pass proper parameters everywhere. */ + if (wait_while_table_is_used(thd, table, HA_EXTRA_FORCE_REOPEN)) + DBUG_RETURN(TRUE); + /* Close lock if this is not got with LOCK TABLES */ if (thd->lock) { mysql_unlock_tables(thd, thd->lock); thd->lock=0; // Start locked threads } + + pthread_mutex_lock(&LOCK_open); /* Close all copies of 'table'. This also frees all LOCK TABLES lock */ unlink_open_table(thd, table, TRUE); - - /* When lock on LOCK_open is freed other threads can continue */ - broadcast_refresh(); - DBUG_VOID_RETURN; + pthread_mutex_unlock(&LOCK_open); + DBUG_RETURN(FALSE); } static int send_check_errmsg(THD *thd, TABLE_LIST* table, @@ -4308,6 +4359,7 @@ static int send_check_errmsg(THD *thd, TABLE_LIST* table, static int prepare_for_restore(THD* thd, TABLE_LIST* table, HA_CHECK_OPT *check_opt) { + MDL_LOCK *mdl_lock= 0; DBUG_ENTER("prepare_for_restore"); if (table->table) // do not overwrite existing tables on restore @@ -4331,22 +4383,25 @@ static int prepare_for_restore(THD* thd, TABLE_LIST* table, build_table_filename(dst_path, sizeof(dst_path) - 1, db, table_name, reg_ext, 0); - if (lock_and_wait_for_table_name(thd,table)) - DBUG_RETURN(-1); + mdl_lock= mdl_alloc_lock(0, table->db, table->table_name, + thd->mem_root); + mdl_set_lock_type(mdl_lock, MDL_EXCLUSIVE); + mdl_add_lock(&thd->mdl_context, mdl_lock); + if (mdl_acquire_exclusive_locks(&thd->mdl_context)) + DBUG_RETURN(TRUE); + pthread_mutex_lock(&LOCK_open); + expel_table_from_cache(0, table->db, table->table_name); + pthread_mutex_unlock(&LOCK_open); if (my_copy(src_path, dst_path, MYF(MY_WME))) { - pthread_mutex_lock(&LOCK_open); - unlock_table_name(thd, table); - pthread_mutex_unlock(&LOCK_open); + mdl_release_lock(&thd->mdl_context, mdl_lock); DBUG_RETURN(send_check_errmsg(thd, table, "restore", "Failed copying .frm file")); } if (mysql_truncate(thd, table, 1)) { - pthread_mutex_lock(&LOCK_open); - unlock_table_name(thd, table); - pthread_mutex_unlock(&LOCK_open); + mdl_release_lock(&thd->mdl_context, mdl_lock); DBUG_RETURN(send_check_errmsg(thd, table, "restore", "Failed generating table from .frm file")); } @@ -4357,10 +4412,11 @@ static int prepare_for_restore(THD* thd, TABLE_LIST* table, to finish the restore in the handler later on */ pthread_mutex_lock(&LOCK_open); - if (reopen_name_locked_table(thd, table, TRUE)) + if (reopen_name_locked_table(thd, table)) { - unlock_table_name(thd, table); pthread_mutex_unlock(&LOCK_open); + if (mdl_lock) + mdl_release_lock(&thd->mdl_context, mdl_lock); DBUG_RETURN(send_check_errmsg(thd, table, "restore", "Failed to open partially restored table")); } @@ -4380,6 +4436,7 @@ static int prepare_for_repair(THD *thd, TABLE_LIST *table_list, char from[FN_REFLEN],tmp[FN_REFLEN+32]; const char **ext; MY_STAT stat_info; + MDL_LOCK *mdl_lock; DBUG_ENTER("prepare_for_repair"); if (!(check_opt->sql_flags & TT_USEFRM)) @@ -4391,6 +4448,17 @@ static int prepare_for_repair(THD *thd, TABLE_LIST *table_list, uint key_length; key_length= create_table_def_key(thd, key, table_list, 0); + /* + TODO: Check that REPAIR's code also conforms to meta-data + locking protocol. Fix if it is not. + */ + mdl_lock= mdl_alloc_lock(0, table_list->db, table_list->table_name, + thd->mem_root); + mdl_set_lock_type(mdl_lock, MDL_EXCLUSIVE); + mdl_add_lock(&thd->mdl_context, mdl_lock); + if (mdl_acquire_exclusive_locks(&thd->mdl_context)) + DBUG_RETURN(0); + pthread_mutex_lock(&LOCK_open); if (!(share= (get_table_share(thd, table_list, key, key_length, 0, &error)))) @@ -4457,41 +4525,29 @@ static int prepare_for_repair(THD *thd, TABLE_LIST *table_list, my_snprintf(tmp, sizeof(tmp), "%s-%lx_%lx", from, current_pid, thd->thread_id); - /* If we could open the table, close it */ if (table_list->table) { - pthread_mutex_lock(&LOCK_open); - close_cached_table(thd, table); - pthread_mutex_unlock(&LOCK_open); - } - if (lock_and_wait_for_table_name(thd,table_list)) - { - error= -1; - goto end; + /* If we could open the table, close it */ + if (close_cached_table(thd, table)) + goto end; + table_list->table= 0; } + // After this point we have X mdl lock in both cases + if (my_rename(from, tmp, MYF(MY_WME))) { - pthread_mutex_lock(&LOCK_open); - unlock_table_name(thd, table_list); - pthread_mutex_unlock(&LOCK_open); error= send_check_errmsg(thd, table_list, "repair", "Failed renaming data file"); goto end; } if (mysql_truncate(thd, table_list, 1)) { - pthread_mutex_lock(&LOCK_open); - unlock_table_name(thd, table_list); - pthread_mutex_unlock(&LOCK_open); error= send_check_errmsg(thd, table_list, "repair", "Failed generating table from .frm file"); goto end; } if (my_rename(tmp, from, MYF(MY_WME))) { - pthread_mutex_lock(&LOCK_open); - unlock_table_name(thd, table_list); - pthread_mutex_unlock(&LOCK_open); error= send_check_errmsg(thd, table_list, "repair", "Failed restoring .MYD file"); goto end; @@ -4502,9 +4558,8 @@ static int prepare_for_repair(THD *thd, TABLE_LIST *table_list, to finish the repair in the handler later on. */ pthread_mutex_lock(&LOCK_open); - if (reopen_name_locked_table(thd, table_list, TRUE)) + if (reopen_name_locked_table(thd, table_list)) { - unlock_table_name(thd, table_list); pthread_mutex_unlock(&LOCK_open); error= send_check_errmsg(thd, table_list, "repair", "Failed to open partially repaired table"); @@ -4519,6 +4574,8 @@ end: closefrm(table, 1); // Free allocated memory pthread_mutex_unlock(&LOCK_open); } + if (error) + mdl_release_exclusive_locks(&thd->mdl_context); DBUG_RETURN(error); } @@ -4566,7 +4623,7 @@ static bool mysql_admin_table(THD* thd, TABLE_LIST* tables, Protocol::SEND_NUM_ROWS | Protocol::SEND_EOF)) DBUG_RETURN(TRUE); - mysql_ha_rm_tables(thd, tables, FALSE); + mysql_ha_rm_tables(thd, tables); for (table= tables; table; table= table->next_local) { @@ -5063,6 +5120,7 @@ bool mysql_restore_table(THD* thd, TABLE_LIST* table_list) bool mysql_repair_table(THD* thd, TABLE_LIST* tables, HA_CHECK_OPT* check_opt) { DBUG_ENTER("mysql_repair_table"); + set_all_mdl_upgradable(tables); DBUG_RETURN(mysql_admin_table(thd, tables, check_opt, "repair", TL_WRITE, 1, test(check_opt->sql_flags & TT_USEFRM), @@ -5250,7 +5308,7 @@ bool mysql_create_like_schema_frm(THD* thd, TABLE_LIST* schema_table, bool mysql_create_like_table(THD* thd, TABLE_LIST* table, TABLE_LIST* src_table, HA_CREATE_INFO *create_info) { - TABLE *name_lock= 0; + MDL_LOCK *target_lock= 0; char src_path[FN_REFLEN], dst_path[FN_REFLEN + 1]; uint dst_path_length; char *db= table->db; @@ -5307,9 +5365,9 @@ bool mysql_create_like_table(THD* thd, TABLE_LIST* table, TABLE_LIST* src_table, } else { - if (lock_table_name_if_not_cached(thd, db, table_name, &name_lock)) + if (lock_table_name_if_not_cached(thd, db, table_name, &target_lock)) goto err; - if (!name_lock) + if (!target_lock) goto table_exists; dst_path_length= build_table_filename(dst_path, sizeof(dst_path) - 1, db, table_name, reg_ext, 0); @@ -5453,9 +5511,8 @@ binlog: The table will be closed by unlink_open_table() at the end of this function. */ - table->table= name_lock; pthread_mutex_lock(&LOCK_open); - if (reopen_name_locked_table(thd, table, FALSE)) + if (reopen_name_locked_table(thd, table)) { pthread_mutex_unlock(&LOCK_open); goto err; @@ -5468,6 +5525,10 @@ binlog: DBUG_ASSERT(result == 0); // store_create_info() always return 0 write_bin_log(thd, TRUE, query.ptr(), query.length()); + + pthread_mutex_lock(&LOCK_open); + unlink_open_table(thd, table->table, FALSE); + pthread_mutex_unlock(&LOCK_open); } else // Case 1 write_bin_log(thd, TRUE, thd->query(), thd->query_length()); @@ -5482,12 +5543,8 @@ binlog: res= FALSE; err: - if (name_lock) - { - pthread_mutex_lock(&LOCK_open); - unlink_open_table(thd, name_lock, FALSE); - pthread_mutex_unlock(&LOCK_open); - } + if (target_lock) + mdl_release_exclusive_locks(&thd->mdl_context); DBUG_RETURN(res); } @@ -5570,7 +5627,7 @@ mysql_discard_or_import_tablespace(THD *thd, err: ha_autocommit_or_rollback(thd, error); thd->tablespace_op=FALSE; - + if (error == 0) { my_ok(thd); @@ -5578,7 +5635,7 @@ err: } table->file->print_error(error, MYF(0)); - + DBUG_RETURN(-1); } @@ -6424,7 +6481,8 @@ bool mysql_alter_table(THD *thd,char *new_db, char *new_name, Alter_info *alter_info, uint order_num, ORDER *order, bool ignore) { - TABLE *table, *new_table= 0, *name_lock= 0; + TABLE *table, *new_table= 0; + MDL_LOCK *target_lock= 0; int error= 0; char tmp_name[80],old_name[32],new_name_buff[FN_REFLEN + 1]; char new_alias_buff[FN_REFLEN], *table_name, *db, *new_alias, *alias; @@ -6507,7 +6565,7 @@ bool mysql_alter_table(THD *thd,char *new_db, char *new_name, build_table_filename(reg_path, sizeof(reg_path) - 1, db, table_name, reg_ext, 0); build_table_filename(path, sizeof(path) - 1, db, table_name, "", 0); - mysql_ha_rm_tables(thd, table_list, FALSE); + mysql_ha_rm_tables(thd, table_list); /* DISCARD/IMPORT TABLESPACE is always alone in an ALTER TABLE */ if (alter_info->tablespace_op != NO_TABLESPACE_OP) @@ -6563,13 +6621,14 @@ bool mysql_alter_table(THD *thd,char *new_db, char *new_name, if (wait_if_global_read_lock(thd,0,1)) DBUG_RETURN(TRUE); - pthread_mutex_lock(&LOCK_open); if (lock_table_names(thd, table_list)) { error= 1; goto view_err; } - + + pthread_mutex_lock(&LOCK_open); + if (!do_rename(thd, table_list, new_db, new_name, new_name, 1)) { if (mysql_bin_log.is_open()) @@ -6581,15 +6640,17 @@ bool mysql_alter_table(THD *thd,char *new_db, char *new_name, } my_ok(thd); } + pthread_mutex_unlock(&LOCK_open); - unlock_table_names(thd, table_list, (TABLE_LIST*) 0); + unlock_table_names(thd); view_err: - pthread_mutex_unlock(&LOCK_open); start_waiting_global_read_lock(thd); DBUG_RETURN(error); } + table_list->mdl_upgradable= TRUE; + if (!(table= open_n_lock_single_table(thd, table_list, TL_WRITE_ALLOW_READ))) DBUG_RETURN(TRUE); table->use_all_columns(); @@ -6644,9 +6705,9 @@ view_err: } else { - if (lock_table_name_if_not_cached(thd, new_db, new_name, &name_lock)) + if (lock_table_name_if_not_cached(thd, new_db, new_name, &target_lock)) DBUG_RETURN(TRUE); - if (!name_lock) + if (!target_lock) { my_error(ER_TABLE_EXISTS_ERROR, MYF(0), new_alias); DBUG_RETURN(TRUE); @@ -6737,26 +6798,15 @@ view_err: case LEAVE_AS_IS: break; case ENABLE: - /* - wait_while_table_is_used() ensures that table being altered is - opened only by this thread and that TABLE::TABLE_SHARE::version - of TABLE object corresponding to this table is 0. - The latter guarantees that no DML statement will open this table - until ALTER TABLE finishes (i.e. until close_thread_tables()) - while the fact that the table is still open gives us protection - from concurrent DDL statements. - */ - pthread_mutex_lock(&LOCK_open); - wait_while_table_is_used(thd, table, HA_EXTRA_FORCE_REOPEN); - pthread_mutex_unlock(&LOCK_open); + if (wait_while_table_is_used(thd, table, HA_EXTRA_FORCE_REOPEN)) + goto err; DBUG_EXECUTE_IF("sleep_alter_enable_indexes", my_sleep(6000000);); error= table->file->ha_enable_indexes(HA_KEY_SWITCH_NONUNIQ_SAVE); /* COND_refresh will be signaled in close_thread_tables() */ break; case DISABLE: - pthread_mutex_lock(&LOCK_open); - wait_while_table_is_used(thd, table, HA_EXTRA_FORCE_REOPEN); - pthread_mutex_unlock(&LOCK_open); + if (wait_while_table_is_used(thd, table, HA_EXTRA_FORCE_REOPEN)) + goto err; error=table->file->ha_disable_indexes(HA_KEY_SWITCH_NONUNIQ_SAVE); /* COND_refresh will be signaled in close_thread_tables() */ break; @@ -6773,24 +6823,19 @@ view_err: table->alias); } - pthread_mutex_lock(&LOCK_open); - /* - Unlike to the above case close_cached_table() below will remove ALL - instances of TABLE from table cache (it will also remove table lock - held by this thread). So to make actual table renaming and writing - to binlog atomic we have to put them into the same critical section - protected by LOCK_open mutex. This also removes gap for races between - access() and mysql_rename_table() calls. - */ - if (!error && (new_name != table_name || new_db != db)) { thd_proc_info(thd, "rename"); /* Then do a 'simple' rename of the table. First we need to close all instances of 'source' table. + Note that if close_cached_table() returns error here (i.e. if + this thread was killed) then it must be that previous step of + simple rename did nothing and therefore we can safely reture + without additional clean-up. */ - close_cached_table(thd, table); + if (close_cached_table(thd, table)) + goto err; /* Then, we want check once again that target table does not exist. Actually the order of these two steps does not matter since @@ -6807,6 +6852,7 @@ view_err: else { *fn_ext(new_name)=0; + pthread_mutex_lock(&LOCK_open); if (mysql_rename_table(old_db_type,db,table_name,new_db,new_alias, 0)) error= -1; else if (Table_triggers_list::change_table_name(thd, db, table_name, @@ -6816,6 +6862,7 @@ view_err: table_name, 0); error= -1; } + pthread_mutex_unlock(&LOCK_open); } } @@ -6837,11 +6884,24 @@ view_err: table->file->print_error(error, MYF(0)); error= -1; } - if (name_lock) - unlink_open_table(thd, name_lock, FALSE); - pthread_mutex_unlock(&LOCK_open); table_list->table= NULL; // For query cache query_cache_invalidate3(thd, table_list, 0); + + if (thd->locked_tables) + { + /* + Under LOCK TABLES we should adjust meta-data locks before finishing + statement. Otherwise we can rely on close_thread_tables() releasing + them. + + TODO: Investigate what should be done with upgraded table-level + lock here... + */ + if (new_name != table_name || new_db != db) + mdl_release_exclusive_locks(&thd->mdl_context); + else + mdl_downgrade_exclusive_locks(&thd->mdl_context); + } DBUG_RETURN(error); } @@ -7073,7 +7133,7 @@ view_err: #ifdef WITH_PARTITION_STORAGE_ENGINE if (fast_alter_partition) { - DBUG_ASSERT(!name_lock); + DBUG_ASSERT(!target_lock); DBUG_RETURN(fast_alter_partition_table(thd, table, alter_info, create_info, table_list, db, table_name, @@ -7158,7 +7218,7 @@ view_err: tbl.db= new_db; tbl.table_name= tbl.alias= tmp_name; /* Table is in thd->temporary_tables */ - new_table= open_table(thd, &tbl, thd->mem_root, (bool*) 0, + new_table= open_table(thd, &tbl, thd->mem_root, (enum_open_table_action*) 0, MYSQL_LOCK_IGNORE_FLUSH); } else @@ -7168,10 +7228,10 @@ view_err: build_table_filename(path, sizeof(path) - 1, new_db, tmp_name, "", FN_IS_TMP); /* Open our intermediate table */ - new_table=open_temporary_table(thd, path, new_db, tmp_name,0); + new_table= open_temporary_table(thd, path, new_db, tmp_name, 1); } if (!new_table) - goto err1; + goto err_new_table_cleanup; /* Note: In case of MERGE table, we do not attach children. We do not copy data for MERGE tables. Only the children have data. @@ -7200,9 +7260,8 @@ view_err: } else { - pthread_mutex_lock(&LOCK_open); - wait_while_table_is_used(thd, table, HA_EXTRA_FORCE_REOPEN); - pthread_mutex_unlock(&LOCK_open); + if (wait_while_table_is_used(thd, table, HA_EXTRA_FORCE_REOPEN)) + goto err_new_table_cleanup; thd_proc_info(thd, "manage keys"); alter_table_manage_keys(table, table->file->indexes_are_disabled(), alter_info->keys_onoff); @@ -7253,7 +7312,7 @@ view_err: table->key_info= key_info; table->file->print_error(error, MYF(0)); table->key_info= save_key_info; - goto err1; + goto err_new_table_cleanup; } } /*end of if (index_add_count)*/ @@ -7276,14 +7335,14 @@ view_err: index_drop_count))) { table->file->print_error(error, MYF(0)); - goto err1; + goto err_new_table_cleanup; } /* Tell the handler to finally drop the indexes. */ if ((error= table->file->final_drop_index(table))) { table->file->print_error(error, MYF(0)); - goto err1; + goto err_new_table_cleanup; } } /*end of if (index_drop_count)*/ @@ -7296,16 +7355,16 @@ view_err: /* Need to commit before a table is unlocked (NDB requirement). */ DBUG_PRINT("info", ("Committing before unlocking table")); if (ha_autocommit_or_rollback(thd, 0) || end_active_trans(thd)) - goto err1; + goto err_new_table_cleanup; committed= 1; } /*end of if (! new_table) for add/drop index*/ + if (error) + goto err_new_table_cleanup; + if (table->s->tmp_table != NO_TMP_TABLE) { - /* We changed a temporary table */ - if (error) - goto err1; /* Close lock if this is a transactional table */ if (thd->lock) { @@ -7316,28 +7375,22 @@ view_err: close_temporary_table(thd, table, 1, 1); /* Should pass the 'new_name' as we store table name in the cache */ if (rename_temporary_table(thd, new_table, new_db, new_name)) - goto err1; + goto err_new_table_cleanup; /* We don't replicate alter table statement on temporary tables */ if (!thd->current_stmt_binlog_row_based) write_bin_log(thd, TRUE, thd->query(), thd->query_length()); goto end_temporary; } + /* + Close the intermediate table that will be the new table, but do + not delete it! Even altough MERGE tables do not have their children + attached here it is safe to call close_temporary_table(). + */ if (new_table) { - /* - Close the intermediate table that will be the new table. - Note that MERGE tables do not have their children attached here. - */ - intern_close_table(new_table); - my_free(new_table,MYF(0)); - } - pthread_mutex_lock(&LOCK_open); - if (error) - { - (void) quick_rm_table(new_db_type, new_db, tmp_name, FN_IS_TMP); - pthread_mutex_unlock(&LOCK_open); - goto err; + close_temporary_table(thd, new_table, 1, 0); + new_table= 0; } /* @@ -7362,7 +7415,11 @@ view_err: if (lower_case_table_names) my_casedn_str(files_charset_info, old_name); - wait_while_table_is_used(thd, table, HA_EXTRA_PREPARE_FOR_RENAME); + if (wait_while_table_is_used(thd, table, HA_EXTRA_PREPARE_FOR_RENAME)) + goto err_new_table_cleanup; + + pthread_mutex_lock(&LOCK_open); + close_data_files_and_morph_locks(thd, db, table_name); error=0; @@ -7431,8 +7488,7 @@ view_err: table_list->table_name_length= strlen(new_name); table_list->db= new_db; table_list->db_length= strlen(new_db); - table_list->table= name_lock; - if (reopen_name_locked_table(thd, table_list, FALSE)) + if (reopen_name_locked_table(thd, table_list)) goto err_with_placeholders; t_table= table_list->table; } @@ -7446,16 +7502,24 @@ view_err: if (t_table->file->ha_create_handler_files(path, NULL, CHF_INDEX_FLAG, create_info)) goto err_with_placeholders; - if (thd->locked_tables && new_name == table_name && new_db == db) + if (thd->locked_tables) { - /* - We are going to reopen table down on the road, so we have to restore - state of the TABLE object which we used for obtaining of handler - object to make it suitable for reopening. - */ - DBUG_ASSERT(t_table == table); - table->open_placeholder= 1; - close_handle_and_leave_table_as_lock(table); + if (new_name == table_name && new_db == db) + { + /* + We are going to reopen table down on the road, so we have to restore + state of the TABLE object which we used for obtaining of handler + object to make it suitable for reopening. + */ + DBUG_ASSERT(t_table == table); + table->open_placeholder= 1; + close_handle_and_leave_table_as_lock(table); + } + else + { + /* Unlink the new name from the list of locked tables. */ + unlink_open_table(thd, t_table, FALSE); + } } } @@ -7464,7 +7528,7 @@ view_err: if (thd->locked_tables && new_name == table_name && new_db == db) { thd->in_lock_tables= 1; - error= reopen_tables(thd, 1, 1); + error= reopen_tables(thd, 1); thd->in_lock_tables= 0; if (error) goto err_with_placeholders; @@ -7508,18 +7572,17 @@ view_err: table_list->table=0; // For query cache query_cache_invalidate3(thd, table_list, 0); - if (thd->locked_tables && (new_name != table_name || new_db != db)) + if (thd->locked_tables) { - /* - If are we under LOCK TABLES and did ALTER TABLE with RENAME we need - to remove placeholders for the old table and for the target table - from the list of open tables and table cache. If we are not under - LOCK TABLES we can rely on close_thread_tables() doing this job. - */ - pthread_mutex_lock(&LOCK_open); - unlink_open_table(thd, table, FALSE); - unlink_open_table(thd, name_lock, FALSE); - pthread_mutex_unlock(&LOCK_open); + if ((new_name != table_name || new_db != db)) + { + pthread_mutex_lock(&LOCK_open); + unlink_open_table(thd, table, FALSE); + pthread_mutex_unlock(&LOCK_open); + mdl_release_exclusive_locks(&thd->mdl_context); + } + else + mdl_downgrade_exclusive_locks(&thd->mdl_context); } end_temporary: @@ -7530,7 +7593,7 @@ end_temporary: thd->some_tables_deleted=0; DBUG_RETURN(FALSE); -err1: +err_new_table_cleanup: if (new_table) { /* close_temporary_table() frees the new_table pointer. */ @@ -7574,12 +7637,8 @@ err: alter_info->datetime_field->field_name); thd->abort_on_warning= save_abort_on_warning; } - if (name_lock) - { - pthread_mutex_lock(&LOCK_open); - unlink_open_table(thd, name_lock, FALSE); - pthread_mutex_unlock(&LOCK_open); - } + if (target_lock) + mdl_release_exclusive_locks(&thd->mdl_context); DBUG_RETURN(TRUE); err_with_placeholders: @@ -7589,9 +7648,8 @@ err_with_placeholders: from list of open tables list and table cache. */ unlink_open_table(thd, table, FALSE); - if (name_lock) - unlink_open_table(thd, name_lock, FALSE); pthread_mutex_unlock(&LOCK_open); + mdl_release_exclusive_locks(&thd->mdl_context); DBUG_RETURN(TRUE); } /* mysql_alter_table */ diff --git a/sql/sql_test.cc b/sql/sql_test.cc index d9beb77f546..3907aa6a5ae 100644 --- a/sql/sql_test.cc +++ b/sql/sql_test.cc @@ -75,7 +75,8 @@ print_where(COND *cond,const char *info, enum_query_type query_type) void print_cached_tables(void) { uint idx,count,unused; - TABLE *start_link,*lnk; + TABLE_SHARE *share; + TABLE *start_link, *lnk, *entry; compile_time_assert(TL_WRITE_ONLY+1 == array_elements(lock_descriptions)); @@ -83,16 +84,26 @@ void print_cached_tables(void) pthread_mutex_lock(&LOCK_open); puts("DB Table Version Thread Open Lock"); - for (idx=unused=0 ; idx < open_cache.records ; idx++) + for (idx=unused=0 ; idx < table_def_cache.records ; idx++) { - TABLE *entry=(TABLE*) my_hash_element(&open_cache,idx); - printf("%-14.14s %-32s%6ld%8ld%6d %s\n", - entry->s->db.str, entry->s->table_name.str, entry->s->version, - entry->in_use ? entry->in_use->thread_id : 0L, - entry->db_stat ? 1 : 0, - entry->in_use ? lock_descriptions[(int)entry->reginfo.lock_type] : "Not in use"); - if (!entry->in_use) + share= (TABLE_SHARE*) my_hash_element(&table_def_cache, idx); + + I_P_List_iterator<TABLE, TABLE_share> it(share->used_tables); + while ((entry= it++)) + { + printf("%-14.14s %-32s%6ld%8ld%6d %s\n", + entry->s->db.str, entry->s->table_name.str, entry->s->version, + entry->in_use->thread_id, entry->db_stat ? 1 : 0, + lock_descriptions[(int)entry->reginfo.lock_type]); + } + it.init(share->free_tables); + while ((entry= it++)) + { unused++; + printf("%-14.14s %-32s%6ld%8ld%6d %s\n", + entry->s->db.str, entry->s->table_name.str, entry->s->version, + 0L, entry->db_stat ? 1 : 0, "Not in use"); + } } count=0; if ((start_link=lnk=unused_tables)) @@ -104,17 +115,18 @@ void print_cached_tables(void) printf("unused_links isn't linked properly\n"); return; } - } while (count++ < open_cache.records && (lnk=lnk->next) != start_link); + } while (count++ < table_cache_count && (lnk=lnk->next) != start_link); if (lnk != start_link) { printf("Unused_links aren't connected\n"); } } if (count != unused) - printf("Unused_links (%d) doesn't match open_cache: %d\n", count,unused); + printf("Unused_links (%d) doesn't match table_def_cache: %d\n", count, + unused); printf("\nCurrent refresh version: %ld\n",refresh_version); - if (my_hash_check(&open_cache)) - printf("Error: File hash table is corrupted\n"); + if (my_hash_check(&table_def_cache)) + printf("Error: Table definition hash table is corrupted\n"); fflush(stdout); pthread_mutex_unlock(&LOCK_open); /* purecov: end */ @@ -380,7 +392,7 @@ static void display_table_locks(void) LIST *list; DYNAMIC_ARRAY saved_table_locks; - (void) my_init_dynamic_array(&saved_table_locks,sizeof(TABLE_LOCK_INFO),open_cache.records + 20,50); + (void) my_init_dynamic_array(&saved_table_locks,sizeof(TABLE_LOCK_INFO), table_cache_count + 20,50); pthread_mutex_lock(&THR_LOCK_lock); for (list= thr_lock_thread_list; list; list= list_rest(list)) { diff --git a/sql/sql_trigger.cc b/sql/sql_trigger.cc index ecbb6473ec4..a623b3c80f3 100644 --- a/sql/sql_trigger.cc +++ b/sql/sql_trigger.cc @@ -387,8 +387,6 @@ bool mysql_create_or_drop_trigger(THD *thd, TABLE_LIST *tables, bool create) !(need_start_waiting= !wait_if_global_read_lock(thd, 0, 1))) DBUG_RETURN(TRUE); - pthread_mutex_lock(&LOCK_open); - if (!create) { bool if_exists= thd->lex->drop_if_exists; @@ -444,26 +442,28 @@ bool mysql_create_or_drop_trigger(THD *thd, TABLE_LIST *tables, bool create) tables->required_type= FRMTYPE_TABLE; /* Keep consistent with respect to other DDL statements */ - mysql_ha_rm_tables(thd, tables, TRUE); + mysql_ha_rm_tables(thd, tables); if (thd->locked_tables) { - /* Table must be write locked */ if (name_lock_locked_table(thd, tables)) goto end; + pthread_mutex_lock(&LOCK_open); } else { - /* Grab the name lock and insert the placeholder*/ + /* + Obtain exlusive meta-data lock on the table and remove TABLE + instances from cache. + */ if (lock_table_names(thd, tables)) goto end; - /* Convert the placeholder to a real table */ - if (reopen_name_locked_table(thd, tables, TRUE)) - { - unlock_table_name(thd, tables); - goto end; - } + pthread_mutex_lock(&LOCK_open); + expel_table_from_cache(0, tables->db, tables->table_name); + + if (reopen_name_locked_table(thd, tables)) + goto end_unlock; } table= tables->table; @@ -472,11 +472,11 @@ bool mysql_create_or_drop_trigger(THD *thd, TABLE_LIST *tables, bool create) if (!create) { my_error(ER_TRG_DOES_NOT_EXIST, MYF(0)); - goto end; + goto end_unlock; } if (!(table->triggers= new (&table->mem_root) Table_triggers_list(table))) - goto end; + goto end_unlock; } result= (create ? @@ -489,7 +489,7 @@ bool mysql_create_or_drop_trigger(THD *thd, TABLE_LIST *tables, bool create) /* Make table suitable for reopening */ close_data_files_and_morph_locks(thd, tables->db, tables->table_name); thd->in_lock_tables= 1; - if (reopen_tables(thd, 1, 1)) + if (reopen_tables(thd, 1)) { /* To be safe remove this table from the set of LOCKED TABLES */ unlink_open_table(thd, tables->table, FALSE); @@ -503,14 +503,22 @@ bool mysql_create_or_drop_trigger(THD *thd, TABLE_LIST *tables, bool create) thd->in_lock_tables= 0; } -end: +end_unlock: + pthread_mutex_unlock(&LOCK_open); +end: if (!result) { write_bin_log(thd, TRUE, stmt_query.ptr(), stmt_query.length()); } - pthread_mutex_unlock(&LOCK_open); + /* + If we are under LOCK TABLES we should restore original state of meta-data + locks. Otherwise call to close_thread_tables() will take care about both + TABLE instance created by reopen_name_locked_table() and meta-data lock. + */ + if (thd->locked_tables) + mdl_downgrade_exclusive_locks(&thd->mdl_context); if (need_start_waiting) start_waiting_global_read_lock(thd); @@ -1879,11 +1887,7 @@ bool Table_triggers_list::change_table_name(THD *thd, const char *db, In the future, only an exclusive table name lock will be enough. */ #ifndef DBUG_OFF - uchar key[MAX_DBKEY_LENGTH]; - uint key_length= (uint) (strmov(strmov((char*)&key[0], db)+1, - old_table)-(char*)&key[0])+1; - - if (!is_table_name_exclusively_locked_by_this_thread(thd, key, key_length)) + if (mdl_is_exclusive_lock_owner(&thd->mdl_context, 0, db, old_table)) safe_mutex_assert_owner(&LOCK_open); #endif diff --git a/sql/sql_udf.cc b/sql/sql_udf.cc index a1a0d9633b7..25a0db2fbb8 100644 --- a/sql/sql_udf.cc +++ b/sql/sql_udf.cc @@ -142,6 +142,7 @@ void udf_init() tables.alias= tables.table_name= (char*) "func"; tables.lock_type = TL_READ; tables.db= db; + alloc_mdl_locks(&tables, new_thd->mem_root); if (simple_open_n_lock_tables(new_thd, &tables)) { @@ -485,6 +486,7 @@ int mysql_create_function(THD *thd,udf_func *udf) bzero((char*) &tables,sizeof(tables)); tables.db= (char*) "mysql"; tables.table_name= tables.alias= (char*) "func"; + alloc_mdl_locks(&tables, thd->mem_root); /* Allow creation of functions even if we can't open func table */ if (!(table = open_ltable(thd, &tables, TL_WRITE, 0))) goto err; @@ -563,6 +565,7 @@ int mysql_drop_function(THD *thd,const LEX_STRING *udf_name) bzero((char*) &tables,sizeof(tables)); tables.db=(char*) "mysql"; tables.table_name= tables.alias= (char*) "func"; + alloc_mdl_locks(&tables, thd->mem_root); if (!(table = open_ltable(thd, &tables, TL_WRITE, 0))) goto err; table->use_all_columns(); diff --git a/sql/sql_update.cc b/sql/sql_update.cc index da8b2d046bb..1ad39a2f838 100644 --- a/sql/sql_update.cc +++ b/sql/sql_update.cc @@ -226,7 +226,7 @@ int mysql_update(THD *thd, break; if (!need_reopen) DBUG_RETURN(1); - close_tables_for_reopen(thd, &table_list); + close_tables_for_reopen(thd, &table_list, FALSE); } if (mysql_handle_derived(thd->lex, &mysql_derived_prepare) || @@ -1149,7 +1149,7 @@ reopen_tables: */ cleanup_items(thd->free_list); - close_tables_for_reopen(thd, &table_list); + close_tables_for_reopen(thd, &table_list, FALSE); goto reopen_tables; } diff --git a/sql/sql_view.cc b/sql/sql_view.cc index 83341a53c3e..a0acde600c7 100644 --- a/sql/sql_view.cc +++ b/sql/sql_view.cc @@ -178,40 +178,17 @@ err: static bool fill_defined_view_parts (THD *thd, TABLE_LIST *view) { + char key[MAX_DBKEY_LENGTH]; + uint key_length; LEX *lex= thd->lex; - bool not_used; TABLE_LIST decoy; memcpy (&decoy, view, sizeof (TABLE_LIST)); + key_length= create_table_def_key(thd, key, view, 0); - /* - Let's reset decoy.view before calling open_table(): when we start - supporting ALTER VIEW in PS/SP that may save us from a crash. - */ - - decoy.view= NULL; - - /* - open_table() will return NULL if 'decoy' is idenitifying a view *and* - there is no TABLE object for that view in the table cache. However, - decoy.view will be set to 1. - - If there is a TABLE-instance for the oject identified by 'decoy', - open_table() will return that instance no matter if it is a table or - a view. - - Thus, there is no need to check for the return value of open_table(), - since the return value itself does not mean anything. - */ - - open_table(thd, &decoy, thd->mem_root, ¬_used, OPEN_VIEW_NO_PARSE); - - if (!decoy.view) - { - /* It's a table. */ - my_error(ER_WRONG_OBJECT, MYF(0), view->db, view->table_name, "VIEW"); + if (tdc_open_view(thd, &decoy, decoy.alias, key, key_length, + thd->mem_root, OPEN_VIEW_NO_PARSE)) return TRUE; - } if (!lex->definer) { @@ -400,6 +377,35 @@ bool mysql_create_view(THD *thd, TABLE_LIST *views, DBUG_ASSERT(!lex->proc_list.first && !lex->result && !lex->param_list.elements); + /* + We can't allow taking exclusive meta-data locks of unlocked view under + LOCK TABLES since this might lead to deadlock. Since at the moment we + can't really lock view with LOCK TABLES we simply prohibit creation/ + alteration of views under LOCK TABLES. + */ + + if (thd->locked_tables) + { + my_error(ER_LOCK_OR_ACTIVE_TRANSACTION, MYF(0)); + res= TRUE; + goto err; + } + + if ((res= create_view_precheck(thd, tables, view, mode))) + goto err; + + lex->link_first_table_back(view, link_to_local); + view->open_table_type= TABLE_LIST::TAKE_EXCLUSIVE_MDL; + + if (open_and_lock_tables(thd, lex->query_tables)) + { + view= lex->unlink_first_table(&link_to_local); + res= TRUE; + goto err; + } + + view= lex->unlink_first_table(&link_to_local); + if (mode != VIEW_CREATE_NEW) { if (mode == VIEW_ALTER && @@ -464,16 +470,6 @@ bool mysql_create_view(THD *thd, TABLE_LIST *views, } } #endif - - if ((res= create_view_precheck(thd, tables, view, mode))) - goto err; - - if (open_and_lock_tables(thd, tables)) - { - res= TRUE; - goto err; - } - /* check that tables are not temporary and this VIEW do not used in query (it is possible with ALTERing VIEW). @@ -615,11 +611,13 @@ bool mysql_create_view(THD *thd, TABLE_LIST *views, } #endif + if (wait_if_global_read_lock(thd, 0, 0)) { res= TRUE; goto err; } + pthread_mutex_lock(&LOCK_open); res= mysql_register_view(thd, view, mode); @@ -1331,7 +1329,10 @@ bool mysql_make_view(THD *thd, File_parser *parser, TABLE_LIST *table, anyway. */ for (tbl= view_main_select_tables; tbl; tbl= tbl->next_local) + { tbl->lock_type= table->lock_type; + tbl->mdl_upgradable= table->mdl_upgradable; + } /* If the view is mergeable, we might want to INSERT/UPDATE/DELETE into tables of this view. Preserve the @@ -1579,6 +1580,21 @@ bool mysql_drop_view(THD *thd, TABLE_LIST *views, enum_drop_mode drop_mode) bool something_wrong= FALSE; DBUG_ENTER("mysql_drop_view"); + /* + We can't allow dropping of unlocked view under LOCK TABLES since this + might lead to deadlock. But since we can't really lock view with LOCK + TABLES we have to simply prohibit dropping of views. + */ + + if (thd->locked_tables) + { + my_error(ER_LOCK_OR_ACTIVE_TRANSACTION, MYF(0)); + DBUG_RETURN(TRUE); + } + + if (lock_table_names(thd, views)) + DBUG_RETURN(TRUE); + pthread_mutex_lock(&LOCK_open); for (view= views; view; view= view->next_local) { diff --git a/sql/table.cc b/sql/table.cc index 7ea04ed3e15..181014df9f4 100644 --- a/sql/table.cc +++ b/sql/table.cc @@ -316,6 +316,9 @@ TABLE_SHARE *alloc_table_share(TABLE_LIST *table_list, char *key, share->table_map_id= ~0UL; share->cached_row_logging_check= -1; + share->used_tables.empty(); + share->free_tables.empty(); + memcpy((char*) &share->mem_root, (char*) &mem_root, sizeof(mem_root)); pthread_mutex_init(&share->mutex, MY_MUTEX_INIT_FAST); pthread_cond_init(&share->cond, NULL); @@ -382,6 +385,9 @@ void init_tmp_table_share(THD *thd, TABLE_SHARE *share, const char *key, */ share->table_map_id= (ulong) thd->query_id; + share->used_tables.empty(); + share->free_tables.empty(); + DBUG_VOID_RETURN; } @@ -4832,6 +4838,20 @@ size_t max_row_length(TABLE *table, const uchar *data) return length; } + +/** + Helper function which allows to allocate metadata lock request + objects for all elements of table list. +*/ + +void alloc_mdl_locks(TABLE_LIST *table_list, MEM_ROOT *root) +{ + for ( ; table_list ; table_list= table_list->next_global) + table_list->mdl_lock= mdl_alloc_lock(0, table_list->db, + table_list->table_name, root); +} + + /***************************************************************************** ** Instansiate templates *****************************************************************************/ diff --git a/sql/table.h b/sql/table.h index 76bebd3fdaa..b0598e07299 100644 --- a/sql/table.h +++ b/sql/table.h @@ -17,6 +17,8 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ +#include "sql_plist.h" + /* Structs that defines the TABLE */ class Item; /* Needed by ORDER */ @@ -28,6 +30,7 @@ class st_select_lex; class partition_info; class COND_EQUAL; class Security_context; +struct MDL_LOCK; /*************************************************************************/ @@ -288,6 +291,9 @@ typedef enum enum_table_category TABLE_CATEGORY; TABLE_CATEGORY get_table_category(const LEX_STRING *db, const LEX_STRING *name); + +struct TABLE_share; + /* This structure is shared between different table objects. There is one instance of table share per one table in the database. @@ -310,6 +316,13 @@ struct TABLE_SHARE pthread_cond_t cond; /* To signal that share is ready */ TABLE_SHARE *next, **prev; /* Link to unused shares */ + /* + Doubly-linked (back-linked) lists of used and unused TABLE objects + for this share. + */ + I_P_List <TABLE, TABLE_share> used_tables; + I_P_List <TABLE, TABLE_share> free_tables; + /* The following is copied to each TABLE on OPEN */ Field **field; Field **found_next_number_field; @@ -613,6 +626,19 @@ struct TABLE handler *file; TABLE *next, *prev; +private: + /** + Links for the lists of used/unused TABLE objects for this share. + Declared as private to avoid direct manipulation with those objects. + One should use methods of I_P_List template instead. + */ + TABLE *share_next, **share_prev; + + friend struct TABLE_share; + friend bool reopen_table(TABLE *table); + +public: + /* For the below MERGE related members see top comment in ha_myisammrg.cc */ TABLE *parent; /* Set in MERGE child. Ptr to parent */ TABLE_LIST *child_l; /* Set in MERGE parent. List of children */ @@ -814,6 +840,7 @@ struct TABLE partition_info *part_info; /* Partition related information */ bool no_partitions_used; /* If true, all partitions have been pruned away */ #endif + MDL_LOCK *mdl_lock; bool fill_item_list(List<Item> *item_list) const; void reset_item_list(List<Item> *item_list) const; @@ -859,6 +886,25 @@ struct TABLE bool is_children_attached(void); }; + +/** + Helper class which specifies which members of TABLE are used for + participation in the list of used/unused TABLE objects for the share. +*/ + +struct TABLE_share +{ + static inline TABLE **next_ptr(TABLE *l) + { + return &l->share_next; + } + static inline TABLE ***prev_ptr(TABLE *l) + { + return &l->share_prev; + } +}; + + enum enum_schema_table_state { NOT_PROCESSED= 0, @@ -1326,11 +1372,18 @@ struct TABLE_LIST */ bool prelocking_placeholder; /* - This TABLE_LIST object corresponds to the table to be created - so it is possible that it does not exist (used in CREATE TABLE - ... SELECT implementation). + This TABLE_LIST object corresponds to the table/view which requires + special handling/meta-data locking. For example this is a target + table in CREATE TABLE ... SELECT so it is possible that it does not + exist and we should take exclusive meta-data lock on it in this + case. + */ + enum {NORMAL_OPEN= 0, OPEN_OR_CREATE, TAKE_EXCLUSIVE_MDL} open_table_type; + /** + Indicates that for this table/view we need to take shared metadata + lock which should be upgradable to exclusive metadata lock. */ - bool create; + bool mdl_upgradable; bool internal_tmp_table; /** TRUE if an alias for this table was specified in the SQL. */ bool is_alias; @@ -1379,6 +1432,9 @@ struct TABLE_LIST bool has_table_lookup_value; uint table_open_method; enum enum_schema_table_state schema_table_state; + + MDL_LOCK *mdl_lock; + void calc_md5(char *buffer); void set_underlying_merge(); int view_check_option(THD *thd, bool ignore_failure); @@ -1386,8 +1442,7 @@ struct TABLE_LIST void cleanup_items(); bool placeholder() { - return derived || view || schema_table || (create && !table->db_stat) || - !table; + return derived || view || schema_table || !table; } void print(THD *thd, String *str, enum_query_type query_type); bool check_single_table(TABLE_LIST **table, table_map map, @@ -1746,4 +1801,17 @@ static inline void dbug_tmp_restore_column_maps(MY_BITMAP *read_set, size_t max_row_length(TABLE *table, const uchar *data); + +void alloc_mdl_locks(TABLE_LIST *table_list, MEM_ROOT *root); + +/** + Helper function which allows to mark all elements in table list + as requiring upgradable metadata locks. +*/ + +inline void set_all_mdl_upgradable(TABLE_LIST *tables) +{ + for (; tables; tables= tables->next_global) + tables->mdl_upgradable= TRUE; +} #endif /* TABLE_INCLUDED */ diff --git a/storage/myisammrg/ha_myisammrg.cc b/storage/myisammrg/ha_myisammrg.cc index 471e2243aac..b2181636492 100644 --- a/storage/myisammrg/ha_myisammrg.cc +++ b/storage/myisammrg/ha_myisammrg.cc @@ -270,6 +270,12 @@ static int myisammrg_parent_open_callback(void *callback_param, /* Set alias. */ child_l->alias= child_l->table_name; + /* + FIXME: Actually we should use some other mem-root here. + To be fixed once Ingo pushes his patch for WL4144. + */ + alloc_mdl_locks(child_l, &parent->mem_root); + /* Initialize table map to 'undefined'. */ child_l->init_child_def_version(); |