diff options
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(); |