Bug #119957 Contribution by Tencent: Replica Crash Due to Stale fts_doc_id in HASH_SCAN Replication Path
Submitted: 28 Feb 7:28
Reporter: HUAYI CHAI Email Updates:
Status: Open Impact on me:
None 
Category:MySQL Server: Replication Severity:S2 (Serious)
Version:8.0 OS:Any
Assigned to: CPU Architecture:Any

[28 Feb 7:28] HUAYI CHAI
Description:
Version: Tested on MySQL 8.0.45 and 8.4.8. Likely affects all current versions.

####################
# Trigger Scenario
####################
When applying row-based replication events on a replica, a crash occurs if all of the following conditions are met:

1. The table has no primary key and no NOT NULL unique index
2. The table contains a FULLTEXT index
3. On the source, multiple updates to the same row occur within a single transaction, and the updated columns include the FULLTEXT-indexed column(s)
4. The replica's `slave_rows_search_algorithms` includes HASH_SCAN

Expected behavior: The replica should apply the UPDATE events successfully and remain consistent with the source without crashing.

Actual behavior: The replica crashes with an assertion failure `result != FTS_INVALID` in `fts0fts.cc:2460` due to an illegal FTS state transition (`DELETE→DELETE = FTS_INVALID`).

####################
# Crash Backtrace
####################
2026-02-28T03:50:07.579637Z 13 [ERROR] [MY-013183] [InnoDB] Assertion failure: fts0fts.cc:2460:result != FTS_INVALID thread 140320881686272
InnoDB: We intentionally generate a memory trap.
InnoDB: Submit a detailed bug report to http://bugs.mysql.com.
InnoDB: If you get repeated assertion failures or crashes, even
InnoDB: immediately after the mysqld startup, there may be
InnoDB: corruption in the InnoDB tablespace. Please refer to
InnoDB: http://dev.mysql.com/doc/refman/8.0/en/forcing-innodb-recovery.html
InnoDB: about forcing recovery.
2026-02-28T03:50:07Z UTC - mysqld got signal 6 ;
Most likely, you have hit a bug, but this error can also be caused by malfunctioning hardware.
BuildID[sha1]=1fc9bee9eedc4065e6be6dd35b64645200dfd808
Thread pointer: 0x7f9e84000940
Attempting backtrace. You can use the following information to find out
where mysqld died. If you see no messages after this, something went
terribly wrong...
stack_bottom = 7f9f004ea998 thread_stack 0x100000
 #0 0x36f0f0a _Z18print_fatal_signali at mysql-server/sql/signal_handler.cc:155
 #1 0x36f11bc _Z15my_server_abortv at mysql-server/sql/signal_handler.cc:270
 #2 0x4b0b82e _Z8my_abortv at mysql-server/mysys/my_init.cc:264
 #3 0x4f208dc _Z23ut_dbg_assertion_failedPKcS0_m at mysql-server/storage/innobase/ut/ut0dbg.cc:100
 #4 0x51c4a7e fts_trx_row_get_new_state at mysql-server/storage/innobase/fts/fts0fts.cc:2460
 #5 0x51c4f4d fts_trx_table_add_op at mysql-server/storage/innobase/fts/fts0fts.cc:2622
 #6 0x51c50a2 _Z14fts_trx_add_opP5trx_tP12dict_table_tm13fts_row_stateP11ib_vector_t at mysql-server/storage/innobase/fts/fts0fts.cc:2665
 #7 0x4de62cd row_fts_do_update at mysql-server/storage/innobase/row/row0mysql.cc:1822
 #8 0x4de648d row_fts_update_or_delete at mysql-server/storage/innobase/row/row0mysql.cc:1857
 #9 0x4de7ecc row_update_for_mysql_using_upd_graph at mysql-server/storage/innobase/row/row0mysql.cc:2395
 #10 0x4de814f _Z20row_update_for_mysqlPKhP14row_prebuilt_t at mysql-server/storage/innobase/row/row0mysql.cc:2457
 #11 0x4b8414c _ZN11ha_innobase10update_rowEPKhPh at mysql-server/storage/innobase/handler/ha_innodb.cc:9847
 #12 0x389e74b _ZN7handler13ha_update_rowEPKhPh at mysql-server/sql/handler.cc:8051
 #13 0x46ebeec _ZN21Update_rows_log_event11do_exec_rowEPK14Relay_log_info at mysql-server/sql/log_event.cc:12744
 #14 0x46e0016 _ZN14Rows_log_event12do_apply_rowEPK14Relay_log_info at mysql-server/sql/log_event.cc:8917
 #15 0x46e180d _ZN14Rows_log_event18do_scan_and_updateEPK14Relay_log_info at mysql-server/sql/log_event.cc:9501
 #16 0x46e1c46 _ZN14Rows_log_event23do_hash_scan_and_updateEPK14Relay_log_info at mysql-server/sql/log_event.cc:9590
 #17 0x46e3a81 _ZN14Rows_log_event14do_apply_eventEPK14Relay_log_info at mysql-server/sql/log_event.cc:10211
 #18 0x46f8747 _ZN9Log_event21do_apply_event_workerEP12Slave_worker at mysql-server/sql/log_event.cc:1036
 #19 0x47c362f _ZN12Slave_worker23slave_worker_exec_eventEP9Log_event at mysql-server/sql/rpl_rli_pdb.cc:1739
 #20 0x47c6195 _Z27slave_worker_exec_job_groupP12Slave_workerP14Relay_log_info at mysql-server/sql/rpl_rli_pdb.cc:2504
 #21 0x47e1572 handle_slave_worker at mysql-server/sql/rpl_replica.cc:6105
 #22 0x570f3d1 pfs_spawn_thread at mysql-server/storage/perfschema/pfs.cc:3050

####################
# Workaround
####################

To avoid this issue:

1. Add an explicit primary key or a NOT NULL unique key to the table
2. Ensure the replica's `slave_rows_search_algorithms` includes INDEX_SCAN (this prevents the HASH_SCAN inner loop from being triggered)

How to repeat:
The following is an MTR (MySQL Test Runner) test case that reproduces the issue:

```
--source include/have_binlog_format_row.inc
--source include/master-slave.inc

--source include/rpl_connection_slave.inc

SET @saved_slave_rows_search_algorithms= @@global.slave_rows_search_algorithms;

SET @@global.slave_rows_search_algorithms= 'HASH_SCAN';

--source include/rpl_connection_master.inc

CREATE TABLE test (
    id1 INT NOT NULL,
    id2 INT NULL,
    c VARCHAR(200),
    UNIQUE KEY u_id(id1, id2),
    FULLTEXT (c)
) ENGINE=InnoDB;

INSERT INTO test (id1, id2,  c) VALUES (1, 1, 'Happy Every Day');

--delimiter |
CREATE FUNCTION f1 () RETURNS INT BEGIN
  UPDATE test SET c = 'Joyful Daily' WHERE id1 = 1 AND id2 = 1;
  UPDATE test SET c = 'Bliss All Day' WHERE id1 = 1 AND id2 = 1;
  RETURN 0;
END|
--delimiter ;

SELECT f1();

--source include/sync_slave_sql_with_master.inc

--source include/rpl_connection_slave.inc

SELECT * FROM test;

--source include/rpl_connection_master.inc

DROP FUNCTION f1;
DROP TABLE test;

--source include/sync_slave_sql_with_master.inc

--source include/rpl_connection_slave.inc
SET @@global.slave_rows_search_algorithms= @saved_slave_rows_search_algorithms;

--source include/rpl_end.inc
```

Suggested fix:
####################
# Root Cause Analysis
####################

In the HASH_SCAN replication path, when the same row is updated multiple times within a single `Update_rows_log_event`, the function `do_scan_and_update()` (in `log_event.cc`) contains an inner `do...while` optimization loop. This loop reuses the After Image (AI) from the previous `ha_update_row()` call directly for the next hash lookup, skipping the call to `next_record_scan()` (i.e., `row_search_mvcc()`).

However, `prebuilt->fts_doc_id` in InnoDB is only refreshed during the `row_search_mvcc()` → `row_sel_store_mysql_rec()` code path. By skipping this path, the inner loop's subsequent iterations retain the stale `fts_doc_id` from the previous iteration instead of the current row's actual `doc_id`.

As a result, InnoDB's FTS subsystem attempts to perform a DELETE operation using the incorrect (stale) `old_doc_id`, leading to a duplicate DELETE on the same FTS document ID and triggering the illegal state transition `DELETE→DELETE = FTS_INVALID`, which causes an assertion failure and crash.

####################
# Suggested Fix
####################

Ensure that `prebuilt->fts_doc_id` is refreshed before each `ha_update_row()` call in the `do_scan_and_update()` inner loop, even when `next_record_scan()` is skipped. This could be done by explicitly re-reading the FTS doc ID from the current record before performing the update.