diff options
Diffstat (limited to 'sql/sql_base.cc')
-rw-r--r-- | sql/sql_base.cc | 457 |
1 files changed, 412 insertions, 45 deletions
diff --git a/sql/sql_base.cc b/sql/sql_base.cc index f26d6e5777b..14edd460bc4 100644 --- a/sql/sql_base.cc +++ b/sql/sql_base.cc @@ -97,7 +97,7 @@ static bool open_new_frm(THD *thd, TABLE_SHARE *share, const char *alias, uint db_stat, uint prgflag, uint ha_open_flags, TABLE *outparam, TABLE_LIST *table_desc, MEM_ROOT *mem_root); -static void close_old_data_files(THD *thd, TABLE *table, bool abort_locks, +static void close_old_data_files(THD *thd, TABLE *table, bool morph_locks, bool send_refresh); static bool reopen_table(TABLE *table); static bool @@ -688,6 +688,8 @@ static void close_handle_and_leave_table_as_lock(TABLE *table) MEM_ROOT *mem_root= &table->mem_root; DBUG_ENTER("close_handle_and_leave_table_as_lock"); + DBUG_ASSERT(table->db_stat); + /* Make a local copy of the table share and free the current one. This has to be done to ensure that the table share is removed from @@ -934,8 +936,22 @@ bool close_cached_tables(THD *thd, bool if_wait_for_refresh, for (uint idx=0 ; idx < open_cache.records ; idx++) { TABLE *table=(TABLE*) hash_element(&open_cache,idx); + /* + 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->s->log_table && - ((table->s->version) < refresh_version && table->db_stat)) + (table->needs_reopen_or_name_lock() && table->db_stat)) { found=1; DBUG_PRINT("signal", ("Waiting for COND_refresh")); @@ -1249,10 +1265,10 @@ bool close_thread_table(THD *thd, TABLE **table_ptr) TABLE *table= *table_ptr; DBUG_ENTER("close_thread_table"); DBUG_ASSERT(table->key_read == 0); - DBUG_ASSERT(table->file->inited == handler::NONE); + DBUG_ASSERT(!table->file || table->file->inited == handler::NONE); *table_ptr=table->next; - if (table->s->version != refresh_version || + if (table->needs_reopen_or_name_lock() || thd->version != refresh_version || !table->db_stat) { VOID(hash_delete(&open_cache,(byte*) table)); @@ -1260,6 +1276,12 @@ bool close_thread_table(THD *thd, TABLE **table_ptr) } else { + /* + Open placeholders have TABLE::db_stat set to 0, so they should be + handled by the first alternative. + */ + DBUG_ASSERT(!table->open_placeholder); + /* Free memory and reset for next loop */ table->file->ha_reset(); table->in_use=0; @@ -1716,7 +1738,7 @@ void close_temporary_table(THD *thd, TABLE *table, void close_temporary(TABLE *table, bool free_share, bool delete_table) { - handlerton *table_type= table->s->db_type; + handlerton *table_type= table->s->db_type(); DBUG_ENTER("close_temporary"); free_io_cache(table); @@ -1778,18 +1800,32 @@ static void relink_unused(TABLE *table) } -/* - Remove all instances of table from the current open list - Free all locks on tables that are done with LOCK TABLES - */ +/** + @brief Remove all instances of table from thread's open list and + table cache. + + @param thd Thread context + @param find Table to remove + @param unlock TRUE - free all locks on tables removed that are + done with LOCK TABLES + FALSE - otherwise + + @note When unlock parameter is FALSE or current thread doesn't have + any tables locked with LOCK TABLES tables are assumed to be + not locked (for example already unlocked). +*/ -TABLE *unlink_open_table(THD *thd, TABLE *list, TABLE *find) +void unlink_open_table(THD *thd, TABLE *find, bool unlock) { char key[MAX_DBKEY_LENGTH]; uint key_length= find->s->table_cache_key.length; - TABLE *start=list,**prev,*next; - prev= &start; + TABLE *list, **prev, *next; + DBUG_ENTER("unlink_open_table"); + + safe_mutex_assert_owner(&LOCK_open); + list= thd->open_tables; + prev= &thd->open_tables; memcpy(key, find->s->table_cache_key.str, key_length); for (; list ; list=next) { @@ -1797,7 +1833,7 @@ TABLE *unlink_open_table(THD *thd, TABLE *list, TABLE *find) if (list->s->table_cache_key.length == key_length && !memcmp(list->s->table_cache_key.str, key, key_length)) { - if (thd->locked_tables) + if (unlock && thd->locked_tables) mysql_lock_remove(thd, thd->locked_tables,list); VOID(hash_delete(&open_cache,(byte*) list)); // Close table } @@ -1810,7 +1846,41 @@ TABLE *unlink_open_table(THD *thd, TABLE *list, TABLE *find) *prev=0; // Notify any 'refresh' threads broadcast_refresh(); - return start; + DBUG_VOID_RETURN; +} + + +/** + @brief Auxiliary routine which closes and drops open table. + + @param thd Thread handle + @param table TABLE object for table to be dropped + @param db_name Name of database for this table + @param table_name Name of this table + + @note This routine assumes that table to be closed is open only + by calling thread so we needn't wait until other threads + will close the table. It also assumes that table to be + dropped is already unlocked. +*/ + +void drop_open_table(THD *thd, TABLE *table, const char *db_name, + const char *table_name) +{ + if (table->s->tmp_table) + close_temporary_table(thd, table, 1, 1); + else + { + handlerton *table_type= table->s->db_type(); + VOID(pthread_mutex_lock(&LOCK_open)); + /* + unlink_open_table() also tells threads waiting for refresh or close + that something has happened. + */ + unlink_open_table(thd, table, FALSE); + quick_rm_table(table_type, db_name, table_name, 0); + VOID(pthread_mutex_unlock(&LOCK_open)); + } } @@ -1867,6 +1937,11 @@ void wait_for_condition(THD *thd, pthread_mutex_t *mutex, pthread_cond_t *cond) 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. NOTE This function assumes that its caller already acquired LOCK_open mutex. @@ -1876,7 +1951,7 @@ void wait_for_condition(THD *thd, pthread_mutex_t *mutex, pthread_cond_t *cond) TRUE - Error */ -bool reopen_name_locked_table(THD* thd, TABLE_LIST* table_list) +bool reopen_name_locked_table(THD* thd, TABLE_LIST* table_list, bool link_in) { TABLE *table= table_list->table; TABLE_SHARE *share; @@ -1907,11 +1982,32 @@ bool reopen_name_locked_table(THD* thd, TABLE_LIST* table_list) } share= table->s; + /* + We want to prevent other connections from opening this table until end + of statement as it is likely that modifications of table's metadata are + not yet finished (for example CREATE TRIGGER have to change .TRG file, + or we might want to drop table if CREATE TABLE ... SELECT fails). + This also allows us to assume that no other connection will sneak in + before we will get table-level lock on this table. + */ share->version=0; table->in_use = thd; check_unused(); - table->next = thd->open_tables; - thd->open_tables = table; + + 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->tablenr=thd->current_tablenr++; table->used_fields=0; table->const_table=0; @@ -1921,6 +2017,173 @@ bool reopen_name_locked_table(THD* thd, TABLE_LIST* table_list) } +/** + @brief 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, (byte*)table)) + { + my_free((gptr) table, MYF(0)); + DBUG_RETURN(NULL); + } + + DBUG_RETURN(table); +} + + +/** + @brief 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; + VOID(pthread_mutex_lock(&LOCK_open)); + + if (hash_search(&open_cache, (byte *)key, key_length)) + { + VOID(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))) + { + VOID(pthread_mutex_unlock(&LOCK_open)); + DBUG_RETURN(TRUE); + } + (*table)->open_placeholder= 1; + (*table)->next= thd->open_tables; + thd->open_tables= *table; + VOID(pthread_mutex_unlock(&LOCK_open)); + DBUG_RETURN(FALSE); +} + + +/** + @brief Check that table exists in table definition cache, on disk + or in some storage engine. + + @param thd Thread context + @param table Table list element + @param exists[out] Out parameter which is set to TRUE if table + exists and to FALSE otherwise. + + @note This function assumes that caller owns LOCK_open mutex. + It also assumes that the fact that there are no name-locks + on the table was checked beforehand. + + @note If there is no .FRM file for the table but it exists in one + of engines (e.g. it was created on another node of NDB cluster) + this function will fetch and create proper .FRM file for it. + + @retval TRUE Some error occured + @retval FALSE No error. 'exists' out parameter set accordingly. +*/ + +bool check_if_table_exists(THD *thd, TABLE_LIST *table, bool *exists) +{ + char path[FN_REFLEN]; + int rc; + DBUG_ENTER("check_if_table_exists"); + + safe_mutex_assert_owner(&LOCK_open); + + *exists= TRUE; + + if (get_cached_table_share(table->db, table->table_name)) + DBUG_RETURN(FALSE); + + build_table_filename(path, sizeof(path) - 1, table->db, table->table_name, + reg_ext, 0); + + if (!access(path, F_OK)) + DBUG_RETURN(FALSE); + + /* .FRM file doesn't exist. Check if some engine can provide it. */ + + rc= ha_create_table_from_engine(thd, table->db, table->table_name); + + if (rc < 0) + { + /* Table does not exists in engines as well. */ + *exists= FALSE; + DBUG_RETURN(FALSE); + } + else if (!rc) + { + /* Table exists in some engine and .FRM for it was created. */ + DBUG_RETURN(FALSE); + } + else /* (rc > 0) */ + { + my_printf_error(ER_UNKNOWN_ERROR, "Failed to open '%-.64s', error while " + "unpacking from engine", MYF(0), table->table_name); + DBUG_RETURN(TRUE); + } +} + + /* Open a table. @@ -1936,12 +2199,17 @@ bool reopen_name_locked_table(THD* thd, TABLE_LIST* table_list) MYSQL_LOCK_IGNORE_FLUSH - Open table even if someone has done a flush or namelock on it. No version number checking is done. - MYSQL_OPEN_IGNORE_LOCKED_TABLES - Open table - ignoring set of locked tables and prelocked mode. + MYSQL_OPEN_TEMPORARY_ONLY - Open only temporary + table not the base table or view. IMPLEMENTATION Uses a cache of open tables to find a table not in use. + If table list element for the table to be opened has "create" flag + set and table does not exist, this function will automatically insert + a placeholder for exclusive name lock into the open tables cache and + will return the TABLE instance that corresponds to this placeholder. + RETURN NULL Open failed. If refresh is set then one should close all other tables and retry the open. @@ -2014,6 +2282,12 @@ TABLE *open_table(THD *thd, TABLE_LIST *table_list, MEM_ROOT *mem_root, } } + if (flags & MYSQL_OPEN_TEMPORARY_ONLY) + { + my_error(ER_NO_SUCH_TABLE, MYF(0), table_list->db, table_list->table_name); + DBUG_RETURN(0); + } + /* The table is not temporary - if we're in pre-locked or LOCK TABLES mode, let's try to find the requested table in the list of pre-opened @@ -2021,8 +2295,7 @@ TABLE *open_table(THD *thd, TABLE_LIST *table_list, MEM_ROOT *mem_root, open not pre-opened tables in pre-locked/LOCK TABLES mode. TODO: move this block into a separate function. */ - if (!(flags & MYSQL_OPEN_IGNORE_LOCKED_TABLES) && - (thd->locked_tables || thd->prelocked_mode)) + if (thd->locked_tables || thd->prelocked_mode) { // Using table locks TABLE *best_table= 0; int best_distance= INT_MIN; @@ -2204,7 +2477,7 @@ TABLE *open_table(THD *thd, TABLE_LIST *table_list, MEM_ROOT *mem_root, c1: name lock t2; -- blocks c2: open t1; -- blocks */ - if (table->s->version != refresh_version && !table->s->log_table) + if (table->needs_reopen_or_name_lock() && !table->s->log_table) { DBUG_PRINT("note", ("Found table '%s.%s' with different refresh version", @@ -2217,6 +2490,14 @@ TABLE *open_table(THD *thd, TABLE_LIST *table_list, MEM_ROOT *mem_root, continue; } + /* Avoid self-deadlocks by detecting self-dependencies. */ + if (table->open_placeholder && table->in_use == thd) + { + VOID(pthread_mutex_unlock(&LOCK_open)); + my_error(ER_UPDATE_TABLE_USED, MYF(0), table->s->table_name.str); + DBUG_RETURN(0); + } + /* Back off, part 1: mark the table as "unused" for the purpose of name-locking by setting table->db_stat to 0. Do @@ -2233,6 +2514,14 @@ TABLE *open_table(THD *thd, TABLE_LIST *table_list, MEM_ROOT *mem_root, 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. */ if (table->in_use != thd) { @@ -2273,6 +2562,40 @@ TABLE *open_table(THD *thd, TABLE_LIST *table_list, MEM_ROOT *mem_root, while (open_cache.records > table_cache_size && unused_tables) VOID(hash_delete(&open_cache,(byte*) unused_tables)); /* purecov: tested */ + if (table_list->create) + { + bool exists; + + if (check_if_table_exists(thd, table_list, &exists)) + { + VOID(pthread_mutex_unlock(&LOCK_open)); + DBUG_RETURN(NULL); + } + + if (!exists) + { + /* + Table to be created, so we need to create placeholder in table-cache. + */ + if (!(table= table_cache_insert_placeholder(thd, key, key_length))) + { + VOID(pthread_mutex_unlock(&LOCK_open)); + DBUG_RETURN(NULL); + } + /* + 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; + VOID(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)))) { @@ -2489,9 +2812,24 @@ bool close_data_tables(THD *thd,const char *db, const char *table_name) } -/* - Reopen all tables with closed data files - One should have lock on LOCK_open when calling this +/** + @brief Reopen all tables with closed data files. + + @param thd Thread context + @param get_locks Should we get locks after reopening tables ? + @param in_refresh Are we in FLUSH TABLES ? TODO: It seems that + we can remove this parameter. + + @note Since this function can't properly handle prelocking and + create placeholders it should be used in very special + situations like FLUSH TABLES or ALTER TABLE. In general + case one should just repeat open_tables()/lock_tables() + combination when one needs tables to be reopened (for + example see open_and_lock_tables()). + + @note One should have lock on LOCK_open when calling this. + + @return FALSE in case of success, TRUE - otherwise. */ bool reopen_tables(THD *thd,bool get_locks,bool in_refresh) @@ -2537,7 +2875,7 @@ bool reopen_tables(THD *thd,bool get_locks,bool in_refresh) if (in_refresh) { table->s->version=0; - table->locked_by_flush=0; + table->open_placeholder= 0; } } } @@ -2564,13 +2902,21 @@ bool reopen_tables(THD *thd,bool get_locks,bool in_refresh) } -/* - Close handlers for tables in list, but leave the TABLE structure - intact so that we can re-open these quickly - abort_locks is set if called from flush_tables. +/** + @brief Close handlers for tables in list, but leave the TABLE structure + intact so that we can re-open these quickly. + + @param thd Thread context + @param table Head of the list of TABLE objects + @param morph_locks TRUE - remove locks which we have on tables being closed + but ensure that no DML or DDL will sneak in before + we will re-open the table (i.e. temporarily morph + our table-level locks into name-locks). + FALSE - otherwise + @param send_refresh Should we awake waiters even if we didn't close any tables? */ -void close_old_data_files(THD *thd, TABLE *table, bool abort_locks, +void close_old_data_files(THD *thd, TABLE *table, bool morph_locks, bool send_refresh) { bool found= send_refresh; @@ -2582,19 +2928,41 @@ void close_old_data_files(THD *thd, TABLE *table, bool abort_locks, Reopen marked for flush. But close log tables. They are flushed only explicitly on FLUSH LOGS */ - if (table->s->version != refresh_version && !table->s->log_table) + if (table->needs_reopen_or_name_lock() && !table->s->log_table) { found=1; if (table->db_stat) { - if (abort_locks) + if (morph_locks) { - mysql_lock_abort(thd,table, TRUE); // Close waiting threads - mysql_lock_remove(thd, thd->locked_tables,table); - table->locked_by_flush=1; // Will be reopened with locks + /* + Wake up threads waiting for table-level lock on this table + so they won't sneak in when we will temporarily remove our + lock on it. This will also give them a chance to close their + instances of this table. + */ + mysql_lock_abort(thd, table, TRUE); + mysql_lock_remove(thd, thd->locked_tables, table); + /* + We want to protect the table from concurrent DDL operations + (like RENAME TABLE) until we will re-open and re-lock it. + */ + table->open_placeholder= 1; } close_handle_and_leave_table_as_lock(table); } + else if (table->open_placeholder) + { + /* + We come here only in close-for-back-off scenario. So we have to + "close" create placeholder here to avoid deadlocks (for example, + in case of concurrent execution of CREATE TABLE t1 SELECT * FROM t2 + and RENAME TABLE t2 TO t1). In close-for-re-open scenario we will + probably want to let it stay. + */ + DBUG_ASSERT(!morph_locks); + table->open_placeholder= 0; + } } } if (found) @@ -2630,10 +2998,10 @@ bool table_is_used(TABLE *table, bool wait_for_name_lock) key_length, &state)) { DBUG_PRINT("info", ("share: 0x%lx locked_by_logger: %d " - "locked_by_flush: %d locked_by_name: %d " + "open_placeholder: %d locked_by_name: %d " "db_stat: %u version: %lu", (ulong) search->s, search->locked_by_logger, - search->locked_by_flush, search->locked_by_name, + search->open_placeholder, search->locked_by_name, search->db_stat, search->s->version)); if (search->in_use == table->in_use) @@ -2649,8 +3017,7 @@ bool table_is_used(TABLE *table, bool wait_for_name_lock) */ if (!search->locked_by_logger && (search->locked_by_name && wait_for_name_lock || - search->locked_by_flush || - (search->db_stat && search->s->version < refresh_version))) + (search->is_name_opened() && search->needs_reopen_or_name_lock()))) DBUG_RETURN(1); } } while ((table=table->next)); @@ -3098,7 +3465,7 @@ int open_tables(THD *thd, TABLE_LIST **start, uint *counter, uint flags) */ if (!thd->prelocked_mode && !thd->lex->requires_prelocking() && - thd->lex->sroutines_list.elements) + thd->lex->uses_stored_routines()) { bool first_no_prelocking, need_prelocking; TABLE_LIST **save_query_tables_last= thd->lex->query_tables_last; @@ -3284,7 +3651,7 @@ process_view_routines: */ if (tables->view && !thd->prelocked_mode && !thd->lex->requires_prelocking() && - tables->view->sroutines_list.elements) + tables->view->uses_stored_routines()) { /* We have at least one table in TL here. */ if (!query_tables_last_own) @@ -6512,7 +6879,7 @@ my_bool mysql_rm_tmp_tables(void) init_tmp_table_share(&share, "", 0, "", filePathCopy); if (!open_table_def(thd, &share, 0) && ((handler_file= get_new_handler(&share, thd->mem_root, - share.db_type)))) + share.db_type())))) { handler_file->delete_table(filePathCopy); delete handler_file; @@ -6637,7 +7004,7 @@ bool remove_table_from_cache(THD *thd, const char *db, const char *table_name, { DBUG_PRINT("info", ("Table was in use by other thread")); in_use->some_tables_deleted=1; - if (table->db_stat) + if (table->is_name_opened()) { DBUG_PRINT("info", ("Found another active instance of the table")); result=1; @@ -7012,7 +7379,7 @@ has_two_write_locked_tables_with_auto_increment(TABLE_LIST *tables) for (TABLE_LIST *table= tables; table; table= table->next_global) { /* we must do preliminary checks as table->table may be NULL */ - if (!table->placeholder() && !table->schema_table && + if (!table->placeholder() && table->table->found_next_number_field && (table->lock_type >= TL_WRITE_ALLOW_WRITE)) { |