Bug #120561 tablespace marked missing after rollback of copy-style ALTER
| Submitted: | 28 May 11:11 | Modified: | 28 May 11:15 |
|---|---|---|---|
| Reporter: | Huaxiong Song (OCA) | Email Updates: | |
| Status: | Open | Impact on me: | |
| Category: | MySQL Server: DDL | Severity: | S3 (Non-critical) |
| Version: | 9.7/8.4/8.0 | OS: | Any |
| Assigned to: | CPU Architecture: | Any |
[28 May 11:15]
Huaxiong Song
Sorry, the content under “Suggested fix” overlaps with “How to Repeat.” Let me reorganize it and send an updated version.
=========================================================================================================================
[How to repeat]
---------------
A debug-build reproduction is included below. The bug also surfaces in production builds whenever an atomic ALTER reaches err_with_mdl after both renames -- for example via deadlock during update_referencing_views_metadata(), a foreign-key parent invalidation failure, or commit failure -- but a deterministic non-debug repro requires precise concurrency, so the DBUG hook is added in sql/sql_table.cc purely to force the same code path that production failures take.
1. Build MySQL with -DWITH_DEBUG=1.
2. Add a DBUG hook in sql/sql_table.cc, in mysql_alter_table(), inside the
"if (!is_noop)" block immediately before the update_referencing_views_metadata()
call (this is the only post-rename point where err_with_mdl is still
reachable). The hook is added between the existing uncommitted_tables setup
and the existing update_referencing_views_metadata() call:
if (write_bin_log(thd, true, thd->query().str, thd->query().length,
atomic_replace && !is_noop))
goto err_with_mdl;
if (!is_noop) {
Uncommitted_tables_guard uncommitted_tables(thd);
uncommitted_tables.add_table(table_list);
/* >>> ADDED: force copy-style ALTER past both mysql_rename_table
calls to fail into err_with_mdl, triggering the rollback branch
in fil_op_replay_rename. <<< */
DBUG_EXECUTE_IF("force_alter_rollback_after_rename", {
my_error(ER_LOCK_DEADLOCK, MYF(0));
goto err_with_mdl;
});
if (update_referencing_views_metadata(thd, table_list, new_db, new_name,
!atomic_replace, &uncommitted_tables))
goto err_with_mdl;
if (alter_ctx.is_table_renamed())
tdc_remove_table(thd, TDC_RT_REMOVE_ALL, alter_ctx.new_db,
alter_ctx.new_name, false);
}
3. Run:
CREATE DATABASE `mydb-x`;
USE `mydb-x`;
CREATE TABLE t1 (id INT PRIMARY KEY, val VARCHAR(32)) ENGINE=InnoDB;
INSERT INTO t1 VALUES (1, 'a'), (2, 'b'), (3, 'c');
FLUSH TABLES t1;
SET SESSION debug = '+d,force_alter_rollback_after_rename';
ALTER TABLE t1 FORCE, ALGORITHM=COPY;
-- ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
SET SESSION debug = '-d,force_alter_rollback_after_rename';
FLUSH TABLES t1;
SELECT * FROM t1;
-- ERROR 1812 (HY000): Tablespace is missing for table `mydb-x`.`t1`.
The .ibd file is fully present:
$ ls $datadir/mydb@002dx/
t1.ibd
Restarting the server makes the table readable again, which confirms the corruption is purely in the in-memory FIL cache, not on disk.
4. Equivalent reproductions for table-name, partition-name, and sub-partition-name variants by substituting:
- table: CREATE TABLE `t-name` in any database
- partition: PARTITION `p-low` VALUES LESS THAN (50)
- sub-partition: SUBPARTITION `sp-a`
In each case the .ibd on disk uses @002d encoding, while dd_load_tablespace looks up the system-charset form.
5. Tables/partitions whose names contain only ASCII alnum characters (e.g. mydb.t1) do not reproduce the bug -- the filesystem and system charset forms are byte-identical, so the strcmp divergence never arises.
[Suggested fix]
---------------
The bug has two equivalent root causes; either can be patched, but the consumer-side fix is safer because the producer side is shared by many recovery paths.
Producer side -- fil_op_replay_rename() in storage/innobase/fil/fil0fil.cc. The "rollback target file does NOT exist" branch (after the failed df.open_read_only()) reconstructs space->name via plain std::string slicing of the .ibd filepath, with no Fil_path::convert_to_filename_charset() / file_to_table() decoding. Fixing here would require decoding each '/'-separated component back to system charset before passing to fil_rename_tablespace(). Risky to change because this code is shared with crash-recovery replay where the on-disk path is the only source of truth.
Consumer side (recommended) -- dd_load_tablespace() in storage/innobase/dict/dict0dd.cc. In the "if (filepath != nullptr)" branch (the one that currently sets table->ibd_file_missing = true and returns), detect the divergence and reconcile the FIL cache name to the DD name before giving up. The primitive fil_space_update_name() already exists for exactly this purpose -- boot_tablespaces() uses it to reconcile a divergent FIL cache name against the DD on startup. After reconciliation, retry fil_space_exists_in_mem(); on success, the table opens normally, on failure, fall through to the current ibd_file_missing path.
In pseudocode:
if (filepath != nullptr) {
fil_space_t *space = fil_space_get(table->space);
if (space != nullptr && space->name != nullptr &&
strcmp(space->name, space_name) != 0) {
// Log the divergence as an operational signal.
// Reconcile cache name to DD name and retry.
fil_space_update_name(space, space_name);
if (is_already_opened()) {
// ...free filepath, return...
}
}
table->ibd_file_missing = true;
// ...existing cleanup...
}
The consumer-side fix is local, touches only the path that is currently broken, leaves crash-recovery replay untouched, and reuses an existing primitive. A full patch with regression tests covering all four name components (schema / table / partition / sub-partition) and both positive (fix engaged) and negative (fix bypassed -> bug reproduces) halves can be attached as a contribution.
[28 May 11:43]
Huaxiong Song
Fix of bug#120561 (*) I confirm the code being submitted is offered under the terms of the OCA, and that I am authorized to contribute it.
Contribution: 0001-InnoDB-tablespace-marked-missing-after-rollback-of-c.patch (application/octet-stream, text), 20.72 KiB.
[29 May 4:41]
Huaxiong Song
Updated Fix (*) I confirm the code being submitted is offered under the terms of the OCA, and that I am authorized to contribute it.
Contribution: 0001-InnoDB-tablespace-marked-missing-after-rollback-of-c.patch (application/octet-stream, text), 20.36 KiB.
Portions of this website are copyright © 2001, 2002 The PHP Group
Page generated in 0.043 sec. using MySQL 8.4.7-u6-cloud
Timestamp references displayed by the system are UTC.
Content reproduced on this site is the property of the
respective copyright holders.
It is not reviewed in advance
by Oracle and does not necessarily represent the opinion of
Oracle or any other party.
Description: When a copy-style ALTER (ALGORITHM=COPY, e.g. ALTER TABLE t FORCE, ALGORITHM=COPY) fails into err_with_mdl after both mysql_rename_table() calls have already run, the rollback path in Log_DDL::post_ddl -> fil_op_replay_rename() enters its "rollback target file does NOT exist" branch (storage/innobase/fil/fil0fil.cc, around the name.erase(0, datadir_pos + 1) / name.append(...) section). That branch string-slices the .ibd filepath taken from the DDL log without performing any filename-to-tablename charset decoding, so the resulting fil_space_t::name ends up in filesystem charset (e.g. schema@002dx/t1). On the next access to the table, dd_load_tablespace() (storage/innobase/dict/dict0dd.cc) looks the space up under its system-charset name (schema-x/t1, decoded via convert_to_space() -> file_to_table()). fil_space_exists_in_mem()'s strcmp fails, so dd_load_tablespace() falls into the branch: if (filepath != nullptr) { /* If space id is already open with a different space name, then skip loading the space. ... */ table->ibd_file_missing = true; ... return; } The table is then reported as missing (ER_TABLESPACE_MISSING / "Tablespace is missing for table") and subsequent reads return zero rows, even though the .ibd file is fully intact on disk. Conditions to surface the bug ----------------------------- The bug is silent for pure ASCII-alnum names because their filesystem-charset and system-charset forms are byte-identical (no @NNNN encoding). It surfaces only when at least one of the following name components contains '-', non-ASCII characters, or any character that gets @NNNN-encoded on disk: - schema name (e.g. `mydb-x`) - table name (e.g. `t-name`) - partition name (e.g. `p-low`) - sub-partition name (e.g. `sp-a`) The bug is engine-agnostic with respect to the DDL caller -- any storage engine that flags HTON_SUPPORTS_ATOMIC_DDL and reaches err_with_mdl after both mysql_rename_table() calls in mysql_alter_table() triggers it. How to repeat: A debug-build reproduction is included below. The bug also surfaces in production builds whenever an atomic ALTER reaches `err_with_mdl` after both renames — for example via deadlock during `update_referencing_views_metadata()`, a foreign-key parent invalidation failure, or commit failure — but a deterministic non-debug repro requires precise concurrency, so the DBUG hook is added in `sql/sql_table.cc` purely to force the same code path that production failures take. 1. Build MySQL with `-DWITH_DEBUG=1`(mysql version is 9.7). 2. Add a one-line DBUG hook in `sql/sql_table.cc`, in `mysql_alter_table()`, immediately before the `update_referencing_views_metadata()` call inside the `if (!is_noop)` block (this is the only post-rename point where `err_with_mdl` is still reachable): ```cpp DBUG_EXECUTE_IF("force_alter_rollback_after_rename", { my_error(ER_LOCK_DEADLOCK, MYF(0)); goto err_with_mdl; }); ``` 3. Run: ```sql CREATE DATABASE `mydb-x`; USE `mydb-x`; CREATE TABLE t1 (id INT PRIMARY KEY, val VARCHAR(32)) ENGINE=InnoDB; INSERT INTO t1 VALUES (1, 'a'), (2, 'b'), (3, 'c'); FLUSH TABLES t1; SET SESSION debug = '+d,force_alter_rollback_after_rename'; ALTER TABLE t1 FORCE, ALGORITHM=COPY; -- ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction SET SESSION debug = '-d,force_alter_rollback_after_rename'; FLUSH TABLES t1; SELECT * FROM t1; -- ERROR 1812 (HY000): Tablespace is missing for table `mydb-x`.`t1`. ``` The `.ibd` file is fully present: ``` $ ls $datadir/mydb@002dx/ t1.ibd ``` Restarting the server makes the table readable again, which confirms the corruption is purely in the in-memory FIL cache, not on disk. 4. Equivalent reproductions for table-name, partition-name, and sub-partition-name variants by substituting: - table: `CREATE TABLE` `` `t-name` `` in any database - partition: `PARTITION` `` `p-low` `` `VALUES LESS THAN (50)` - sub-partition: `SUBPARTITION` `` `sp-a` `` In each case the `.ibd` on disk uses `@002d` encoding, while `dd_load_tablespace` looks up the system-charset form. 5. Tables/partitions whose names contain only ASCII alnum characters (e.g. `mydb.t1`) do **not** reproduce the bug — the filesystem and system charset forms are byte-identical, so the `strcmp` divergence never arises. Suggested fix: A debug-build reproduction is included below. The bug also surfaces in production builds whenever an atomic ALTER reaches err_with_mdl after both renames -- for example via deadlock during update_referencing_views_metadata(), a foreign-key parent invalidation failure, or commit failure -- but a deterministic non-debug repro requires precise concurrency, so the DBUG hook is added in sql/sql_table.cc purely to force the same code path that production failures take. 1. Build MySQL with -DWITH_DEBUG=1. 2. Add a DBUG hook in sql/sql_table.cc, in mysql_alter_table(), inside the "if (!is_noop)" block immediately before the update_referencing_views_metadata() call (this is the only post-rename point where err_with_mdl is still reachable). The hook is added between the existing uncommitted_tables setup and the existing update_referencing_views_metadata() call: if (write_bin_log(thd, true, thd->query().str, thd->query().length, atomic_replace && !is_noop)) goto err_with_mdl; if (!is_noop) { Uncommitted_tables_guard uncommitted_tables(thd); uncommitted_tables.add_table(table_list); + /* >>> ADDED: force copy-style ALTER past both mysql_rename_table + calls to fail into err_with_mdl, triggering the rollback branch + in fil_op_replay_rename. <<< */ + DBUG_EXECUTE_IF("force_alter_rollback_after_rename", { + my_error(ER_LOCK_DEADLOCK, MYF(0)); + goto err_with_mdl; + }); if (update_referencing_views_metadata(thd, table_list, new_db, new_name, !atomic_replace, &uncommitted_tables)) goto err_with_mdl; if (alter_ctx.is_table_renamed()) tdc_remove_table(thd, TDC_RT_REMOVE_ALL, alter_ctx.new_db, alter_ctx.new_name, false); } 3. Run: CREATE DATABASE `mydb-x`; USE `mydb-x`; CREATE TABLE t1 (id INT PRIMARY KEY, val VARCHAR(32)) ENGINE=InnoDB; INSERT INTO t1 VALUES (1, 'a'), (2, 'b'), (3, 'c'); FLUSH TABLES t1; SET SESSION debug = '+d,force_alter_rollback_after_rename'; ALTER TABLE t1 FORCE, ALGORITHM=COPY; -- ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction SET SESSION debug = '-d,force_alter_rollback_after_rename'; FLUSH TABLES t1; SELECT * FROM t1; -- ERROR 1812 (HY000): Tablespace is missing for table `mydb-x`.`t1`. The .ibd file is fully present: $ ls $datadir/mydb@002dx/ t1.ibd Restarting the server makes the table readable again, which confirms the corruption is purely in the in-memory FIL cache, not on disk. 4. Equivalent reproductions for table-name, partition-name, and sub-partition-name variants by substituting: - table: CREATE TABLE `t-name` in any database - partition: PARTITION `p-low` VALUES LESS THAN (50) - sub-partition: SUBPARTITION `sp-a` In each case the .ibd on disk uses @002d encoding, while dd_load_tablespace looks up the system-charset form. 5. Tables/partitions whose names contain only ASCII alnum characters (e.g. mydb.t1) do not reproduce the bug -- the filesystem and system charset forms are byte-identical, so the strcmp divergence never arises.