Details
-
Bug
-
Status: Stalled (View Workflow)
-
Critical
-
Resolution: Unresolved
-
10.11, 11.8, 12.3, 10.6.28
-
Can result in hang or crash
-
Q4/2026 Replic. Development
Description
DESCRIPTION
===========
On a slave with slave_run_triggers_for_rbr enabled, the per-table trigger
event map is computed from the FIRST row event of a statement group only.
The block that assigns it is guarded by
sql/log_event_server.cc:5001 if (!thd->open_tables)
with the assignment at
sql/log_event_server.cc:5075 tables->trg_event_map= new_trg_event_map;
taken from that event's get_trg_event_map(). Once the tables are open, later
row events of the same group skip the block, so the map still describes the
first event's kind.
Any statement whose group mixes row-event kinds therefore fires triggers of a
kind absent from the map, and
sql/sql_trigger.cc:2840
DBUG_ASSERT(trigger_table->pos_in_table_list->trg_event_map &
trg2bit(event));
fails. In a release build the assert is compiled out and the trigger runs
anyway - but add_tables_and_routines_for_triggers() (sql/sql_trigger.cc:2940)
walked the same stale map at open time, so that trigger's body was never
prelocked. Its referenced tables and routines are not open or locked for the
statement. The assert's own comment states the invariant being violated:
"This trigger must have been processed by the pre-locking algorithm."
Two ways to produce a mixed group, both verified to crash a debug slave:
1. INSERT ... ON DUPLICATE KEY UPDATE on a plain table, where one row updates
and another inserts: Update_rows then Write_rows. Nothing to do with
system versioning.
2. UPDATE of a row in a system-versioned table. Measured event sequence for
such an UPDATE is Update_rows (live row) + Write_rows (archived history
row, from vers_insert_history_row). The master fires only BEFORE/AFTER
UPDATE for the whole statement, so the slave-side INSERT triggers are
also spurious, independently of the assert.
REPRODUCE (case 1, plain table)
===============================
master: create table t (a int primary key, b int) engine=innodb;
create table trg_log (id int auto_increment primary key,
note varchar(64)) engine=innodb;
insert into t values (1,1);
slave: set @@global.slave_run_triggers_for_rbr= YES;
create trigger t_bi before insert on t for each row
insert into trg_log (note) values (concat('B INSERT a=', NEW.a));
create trigger t_bu before update on t for each row
insert into trg_log (note) values (concat('B UPDATE a=', OLD.a));
master: insert into t values (1,10), (2,20)
on duplicate key update b= values(b);
=> slave asserts in Table_triggers_list::process_triggers()
REPRODUCE (case 2, system-versioned table)
==========================================
Same setup, with
create table tv (a int primary key, b int)
with system versioning engine=innodb;
triggers for insert and update on the slave, then
master: insert into tv values (1,1); – ok, INSERT triggers fire
master: update tv set b=2 where a=1; – slave asserts
NOTES
=====
- Predates CDR. Update_rows_log_event::get_trg_event_map() returned only
trg2bit(TRG_EVENT_UPDATE) before the CDR work; the CDR change added
conflict bits only, never TRG_EVENT_INSERT. - Fixing the assert alone leaves the prelocking violation. A correct fix has
to make the map right BEFORE open_and_lock_tables, and the group's later
event kinds are not known at that point - so it needs a static
over-approximation (e.g. every Rows_log_event advertising
INSERT|UPDATE|DELETE while slave_run_triggers_for_rbr is on), at the cost
of prelocking more triggers than a given statement needs. - Not verified on branches other than 12.3, nor on a community build, though
neither reproducer uses an enterprise-only feature.