From 7662212996d7c5bdb2559a7c251cb1602eea56df Mon Sep 17 00:00:00 2001 From: Brandon Nesterenko Date: Wed, 27 May 2026 08:18:02 -0600 Subject: [PATCH 1/5] MDEV-38796 Test updates for forced row logging Test binlog_spurious_ddl_errors is updated here because now decide_logging_format doesn't error out when it can't log in statement format, but just switches to row (aa4b22cbea472e23cd32b47b15308cafae919a2f). @@ -7259,7 +7262,15 @@ int THD::decide_logging_format(TABLE_LIST *tables) wsrep_cs().mode() == wsrep::client_state::m_local),1)) { - my_error((error= ER_BINLOG_STMT_MODE_AND_ROW_ENGINE), MYF(0), ""); + if (is_current_stmt_binlog_format_stmt()) + { + my_printf_error(ER_BINLOG_STMT_MODE_AND_ROW_ENGINE, + "Statement cannot be logged as STATEMENT as at " + "least one table is limited to row-based logging." + " Switching temporarly to row format", + MYF(ME_NOTE)); + set_current_stmt_binlog_format_row(); + } } --- .../r/binlog_spurious_ddl_errors.result | 23 ++++++----- .../binlog/t/binlog_spurious_ddl_errors.test | 38 ++++++++++++++----- 2 files changed, 41 insertions(+), 20 deletions(-) diff --git a/mysql-test/suite/binlog/r/binlog_spurious_ddl_errors.result b/mysql-test/suite/binlog/r/binlog_spurious_ddl_errors.result index 39c0bd52fac06..392ddac3d4b14 100644 --- a/mysql-test/suite/binlog/r/binlog_spurious_ddl_errors.result +++ b/mysql-test/suite/binlog/r/binlog_spurious_ddl_errors.result @@ -1,25 +1,28 @@ SET @old_binlog_format= @@global.binlog_format; INSTALL PLUGIN example SONAME 'ha_example'; ################################################################################ -# Verifies if ER_BINLOG_STMT_MODE_AND_ROW_ENGINE happens by setting the binlog -# format to STATEMENT and the transaction isolation level to READ COMMITTED as -# such changes force Innodb to accept changes in the row format. -# -# When CREATE TABLE, ALTER TABLE, CREATE INDEX and CREATE TRIGGER are executed -# any error should be triggered. -# -# In contrast, CREATE TABLE ... SELECT should trigger the following error: -# ER_BINLOG_STMT_MODE_AND_ROW_ENGINE. +# If the binlog format is set to STATEMENT and the transaction isolation +# level to READ COMMITTED, InnoDB must override the STATEMENT format +# configuration and write using ROW format. ################################################################################ SET binlog_format = STATEMENT; SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; CREATE TABLE t_row (a VARCHAR(100)) ENGINE = InnoDB; +SET statement binlog_format = row for insert into t_row values ('a'), ('b'), ('c'); ALTER TABLE t_row ADD COLUMN b INT; CREATE TRIGGER trig_row BEFORE INSERT ON t_row FOR EACH ROW INSERT INTO t_stmt VALUES (1); CREATE INDEX i ON t_row(a); +# Flush binlogs to ensure the next statement can be analyzed alone +FLUSH BINARY LOGS; CREATE TABLE t_row_new ENGINE = InnoDB SELECT * FROM t_row; -ERROR HY000: Cannot execute statement: impossible to write to binary log since BINLOG_FORMAT = STATEMENT and at least one table uses a storage engine limited to row-based logging. +Warnings: +Note 1665 Statement cannot be logged as STATEMENT as at least one table is limited to row-based logging. Switching temporarly to row format +FLUSH BINARY LOGS; +# MYSQL_BINLOG binlog_file --result-file=binlog_out --base64-output=never --verbose +include/assert_grep.inc [Ensure CREATE TABLE ... SELECT in STATEMENT uses ROW binlogging when InnoDB cannot log using STATEMENT format] +include/assert_grep.inc [Ensure CREATE TABLE ... SELECT in STATEMENT writes all rows when InnoDB cannot log using STATEMENT format] DROP TABLE t_row; +DROP TABLE t_row_new; ################################################################################ diff --git a/mysql-test/suite/binlog/t/binlog_spurious_ddl_errors.test b/mysql-test/suite/binlog/t/binlog_spurious_ddl_errors.test index 5a015c2b8eb98..b2d2f489b9617 100644 --- a/mysql-test/suite/binlog/t/binlog_spurious_ddl_errors.test +++ b/mysql-test/suite/binlog/t/binlog_spurious_ddl_errors.test @@ -29,19 +29,14 @@ SET @old_binlog_format= @@global.binlog_format; INSTALL PLUGIN example SONAME 'ha_example'; --echo ################################################################################ ---echo # Verifies if ER_BINLOG_STMT_MODE_AND_ROW_ENGINE happens by setting the binlog ---echo # format to STATEMENT and the transaction isolation level to READ COMMITTED as ---echo # such changes force Innodb to accept changes in the row format. ---echo # ---echo # When CREATE TABLE, ALTER TABLE, CREATE INDEX and CREATE TRIGGER are executed ---echo # any error should be triggered. ---echo # ---echo # In contrast, CREATE TABLE ... SELECT should trigger the following error: ---echo # ER_BINLOG_STMT_MODE_AND_ROW_ENGINE. +--echo # If the binlog format is set to STATEMENT and the transaction isolation +--echo # level to READ COMMITTED, InnoDB must override the STATEMENT format +--echo # configuration and write using ROW format. --echo ################################################################################ SET binlog_format = STATEMENT; SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; CREATE TABLE t_row (a VARCHAR(100)) ENGINE = InnoDB; +SET statement binlog_format = row for insert into t_row values ('a'), ('b'), ('c'); ALTER TABLE t_row ADD COLUMN b INT; @@ -49,10 +44,33 @@ CREATE TRIGGER trig_row BEFORE INSERT ON t_row FOR EACH ROW INSERT INTO t_stmt V CREATE INDEX i ON t_row(a); ---error ER_BINLOG_STMT_MODE_AND_ROW_ENGINE +--echo # Flush binlogs to ensure the next statement can be analyzed alone +FLUSH BINARY LOGS; + CREATE TABLE t_row_new ENGINE = InnoDB SELECT * FROM t_row; +--let $mysqld_datadir= `SELECT @@datadir` +--let $binlog_filename= query_get_value(SHOW MASTER STATUS, File, 1) +--let $binlog_file= $mysqld_datadir/$binlog_filename +FLUSH BINARY LOGS; + +--let $binlog_out= $MYSQLTEST_VARDIR/tmp/rpl_table_map_metadata_str_len_overflow.out +--echo # MYSQL_BINLOG binlog_file --result-file=binlog_out --base64-output=never --verbose +--exec $MYSQL_BINLOG $binlog_file --result-file=$binlog_out --base64-output=never --verbose + +--let $assert_file= $binlog_out +--let $assert_text= Ensure CREATE TABLE ... SELECT in STATEMENT uses ROW binlogging when InnoDB cannot log using STATEMENT format +--let $assert_select= Write_rows +--let $assert_count= 1 +--source include/assert_grep.inc + +--let $assert_text= Ensure CREATE TABLE ... SELECT in STATEMENT writes all rows when InnoDB cannot log using STATEMENT format +--let $assert_select= INSERT INTO +--let $assert_count= 3 +--source include/assert_grep.inc + DROP TABLE t_row; +DROP TABLE t_row_new; --echo --echo From b122852e426e90ffbcd78f399ffbcf3dbf9207e7 Mon Sep 17 00:00:00 2001 From: Brandon Nesterenko Date: Wed, 27 May 2026 08:18:42 -0600 Subject: [PATCH 2/5] MDEV-38796 Update BINLOG_STATE_FILTER at DB change time Addresses TODO - Update binlog_state with BINLOG_STATE_FILTER only when database or filter is changed. This means we do not have to call binlog_filter->db_ok() for every statement in decide_binlog_format(). --- sql/log.h | 6 +++++- sql/sql_class.cc | 19 ++++++------------- sql/sql_db.cc | 7 +++++++ 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/sql/log.h b/sql/log.h index f2333abdbf37c..e8892081863df 100644 --- a/sql/log.h +++ b/sql/log.h @@ -1408,7 +1408,11 @@ enum enum_binlog_state of the following bits set */ BINLOG_STATE_BYPASS= (1<<4), - // Database filtering used. Note that this does not set the BYPASS bit + /* + Database filtering is active for the thread's the current database. This is + only set when the database changes because the filtering outcome for each + statement won't change as long as the database doesn't change. + */ BINLOG_STATE_FILTER= (1<<5), // set by tmp_disable_binlog(). Previously using OPTION_BIN_TMP_LOG_OFF BINLOG_STATE_TMP_DISABLED= (1<<6), diff --git a/sql/sql_class.cc b/sql/sql_class.cc index 0a9ebbd7308ea..19cc3bdf6e5c7 100644 --- a/sql/sql_class.cc +++ b/sql/sql_class.cc @@ -6006,11 +6006,7 @@ extern "C" void thd_mark_transaction_to_rollback(MYSQL_THD thd, bool all) extern "C" bool thd_binlog_filter_ok(const MYSQL_THD thd) { - /* - TODO: Replace by binlog_state & BINLOG_FILTER) when BINLOG_FILTER - is set when thd->db.str changes value. - */ - return binlog_filter->db_ok(thd->db.str); + return !(thd->binlog_state & BINLOG_STATE_FILTER); } /* @@ -6934,7 +6930,7 @@ int THD::decide_logging_format(TABLE_LIST *tables) Strip the binlogging restrictions from the state that are calculated later in this function */ - binlog_state= (binlog_state & ~(BINLOG_STATE_FILTER | BINLOG_STATE_READONLY)); + binlog_state= (binlog_state & ~BINLOG_STATE_READONLY); /* We should have either binlog or wsrep_emulate_binlog active */ DBUG_ASSERT((binlog_state & BINLOG_STATE_ACTIVE) && binlog_state & (BINLOG_STATE_OPEN | BINLOG_STATE_WSREP)); @@ -7004,9 +7000,6 @@ int THD::decide_logging_format(TABLE_LIST *tables) (BINLOG_STATE_WSREP | BINLOG_STATE_ACTIVE)); #endif /* WITH_WSREP */ - if (binlog_format == BINLOG_FORMAT_STMT && !binlog_filter->db_ok(db.str)) - binlog_state= binlog_state | BINLOG_STATE_FILTER; - if (WSREP_EMULATE_BINLOG_NNULL(this) || (binlog_ready_no_wsrep() && !(binlog_state & BINLOG_STATE_FILTER))) { @@ -7499,7 +7492,7 @@ int THD::decide_logging_format(TABLE_LIST *tables) mysql_bin_log.is_open(), (variables.option_bits & OPTION_BIN_LOG), (uint) binlog_format, - binlog_filter->db_ok(db.str), + binlog_state & BINLOG_STATE_FILTER, binlog_state)); /* TODO: Check if the following is really needed */ @@ -7605,9 +7598,9 @@ exit:; bool THD::binlog_table_should_be_logged(const LEX_CSTRING *db) { - return (binlog_ready_no_wsrep() && - (wsrep_binlog_format(variables.binlog_format) != BINLOG_FORMAT_STMT || - binlog_filter->db_ok(db->str))); + return binlog_ready_no_wsrep() && + (wsrep_binlog_format(variables.binlog_format) != BINLOG_FORMAT_STMT || + !(binlog_state & BINLOG_STATE_FILTER)); } /* Declare in unnamed namespace. */ diff --git a/sql/sql_db.cc b/sql/sql_db.cc index 5568046a971c6..02b27ddc4bc19 100644 --- a/sql/sql_db.cc +++ b/sql/sql_db.cc @@ -1616,6 +1616,13 @@ static void mysql_change_db_impl(THD *thd, thd->db_charset= new_db_charset; thd->variables.collation_database= new_db_charset; + + /* 4. Update binlog_state. */ + if (thd->binlog_ready_precheck() && binlog_filter && + !binlog_filter->db_ok(thd->db.str)) + thd->binlog_state|= BINLOG_STATE_FILTER; + else + thd->binlog_state= (thd->binlog_state & ~BINLOG_STATE_FILTER); } From fe705ac88f4c4d04a9636ff19b3a690d0f84c190 Mon Sep 17 00:00:00 2001 From: Brandon Nesterenko Date: Wed, 27 May 2026 12:29:11 -0600 Subject: [PATCH 3/5] MDEV-38796 Remove *_binlog_local_stmt_filter() TODO Addressed: - Remove clear/reset xxx_binlog_local_stmt_filter() as this is now handled by binlog_state. --- sql/sql_class.cc | 25 +------------------------ sql/sql_class.h | 31 +------------------------------ 2 files changed, 2 insertions(+), 54 deletions(-) diff --git a/sql/sql_class.cc b/sql/sql_class.cc index 19cc3bdf6e5c7..8bcc371aefbec 100644 --- a/sql/sql_class.cc +++ b/sql/sql_class.cc @@ -1384,7 +1384,6 @@ void THD::init() tx_isolation= (enum_tx_isolation) variables.tx_isolation; tx_read_only= variables.tx_read_only; update_charset(); // plugin_thd_var() changed character sets - reset_binlog_local_stmt_filter(); binlog_state= BINLOG_STATE_NONE; sync_binlog_state_with_binlog_open_force(); @@ -2528,13 +2527,12 @@ void THD::cleanup_after_query() #endif } /* - Forget the binlog stmt filter for the next query. + Forget READONLY for the next query. There are some code paths that: - do not call THD::decide_logging_format() - do call THD::binlog_query(), making this reset necessary. */ - reset_binlog_local_stmt_filter(); binlog_state= binlog_state & ~BINLOG_STATE_READONLY; if (first_successful_insert_id_in_cur_stmt > 0) { @@ -7013,7 +7011,6 @@ int THD::decide_logging_format(TABLE_LIST *tables) DBUG_RETURN(-1); } } - reset_binlog_local_stmt_filter(); /* Compute one bit field with the union of all the engine @@ -7413,7 +7410,6 @@ int THD::decide_logging_format(TABLE_LIST *tables) if ((replicated_tables_count == 0) || ! is_write) { DBUG_PRINT("info", ("decision: no logging, no replicated table affected")); - set_binlog_local_stmt_filter(); binlog_state|= BINLOG_STATE_READONLY; } else @@ -7422,16 +7418,8 @@ int THD::decide_logging_format(TABLE_LIST *tables) { my_error((error= ER_BINLOG_STMT_MODE_AND_NO_REPL_TABLES), MYF(0)); } - else - { - clear_binlog_local_stmt_filter(); - } } } - else - { - clear_binlog_local_stmt_filter(); - } if (unlikely(error)) { @@ -8254,15 +8242,6 @@ int THD::binlog_query(THD::enum_binlog_query_type qtype, char const *query_arg, DBUG_ASSERT(query_arg); DBUG_ASSERT(binlog_state & BINLOG_STATE_ACTIVE_WSREP); - if (get_binlog_local_stmt_filter() == BINLOG_FILTER_SET) - { - /* - The current statement is to be ignored, and not written to - the binlog. Do not call issue_unsafe_warnings(). - */ - DBUG_RETURN(-1); - } - /* If this is withing a BEGIN ... COMMIT group, don't log it */ if (variables.option_bits & OPTION_GTID_BEGIN) { @@ -8403,8 +8382,6 @@ bool THD::binlog_current_query_unfiltered() if (!mysql_bin_log.is_open()) return 0; - reset_binlog_local_stmt_filter(); - clear_binlog_local_stmt_filter(); binlog_state= binlog_state & ~BINLOG_STATE_READONLY; return binlog_query(THD::STMT_QUERY_TYPE, query(), query_length(), /* is_trans */ FALSE, diff --git a/sql/sql_class.h b/sql/sql_class.h index 64ecda28406f4..c3a7eeabb50e1 100644 --- a/sql/sql_class.h +++ b/sql/sql_class.h @@ -3548,35 +3548,6 @@ class THD: public THD_count, /* this must be first */ return !binlog_ready_no_wsrep(); } - enum binlog_filter_state - { - BINLOG_FILTER_UNKNOWN, - BINLOG_FILTER_CLEAR, - BINLOG_FILTER_SET - }; - - inline void reset_binlog_local_stmt_filter() - { - m_binlog_filter_state= BINLOG_FILTER_UNKNOWN; - } - - inline void clear_binlog_local_stmt_filter() - { - DBUG_ASSERT(m_binlog_filter_state == BINLOG_FILTER_UNKNOWN); - m_binlog_filter_state= BINLOG_FILTER_CLEAR; - } - - inline void set_binlog_local_stmt_filter() - { - DBUG_ASSERT(m_binlog_filter_state == BINLOG_FILTER_UNKNOWN); - m_binlog_filter_state= BINLOG_FILTER_SET; - } - - inline binlog_filter_state get_binlog_local_stmt_filter() - { - return m_binlog_filter_state; - } - bool binlog_renamed_tmp_tables(TABLE_LIST *table_list); /** @@ -3600,7 +3571,7 @@ class THD: public THD_count, /* this must be first */ DML or DDL that affects only 'local' (non replicated) tables, such as performance_schema.* */ - binlog_filter_state m_binlog_filter_state; + //binlog_filter_state m_binlog_filter_state; /** Indicates the format in which the current statement will be From 2247f9d138d571573c3a119d4bd838c5c3035cb6 Mon Sep 17 00:00:00 2001 From: Brandon Nesterenko Date: Thu, 28 May 2026 08:35:08 -0600 Subject: [PATCH 4/5] MDEV-38796 Remove silent option from mysql_create_db_internal TODO addressed - Remove 'silent' option from mysql_create_db_internal and instead use tmp_disable_binlog() / reenable_binlog() --- sql/sql_db.cc | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/sql/sql_db.cc b/sql/sql_db.cc index 02b27ddc4bc19..ed6ee32064862 100644 --- a/sql/sql_db.cc +++ b/sql/sql_db.cc @@ -730,14 +730,13 @@ CHARSET_INFO *get_default_db_collation(THD *thd, const char *db_name) Function assumes that this is already validated. options DDL options, e.g. IF NOT EXISTS create_info Database create options (like character set) - silent Used by replication when internally creating a database. - In this case the entry should not be logged. SIDE-EFFECTS 1. Report back to client that command succeeded (my_ok) 2. Report errors to client 3. Log event to binary log - (The 'silent' flags turns off 1 and 3.) + (If THD->binlog_state has BINLOG_STATE_TMP_DISABLED flag, 1 and 3 won't + happen) RETURN VALUES FALSE ok @@ -748,8 +747,7 @@ CHARSET_INFO *get_default_db_collation(THD *thd, const char *db_name) static int mysql_create_db_internal(THD *thd, const Lex_ident_db &db, const DDL_options_st &options, - Schema_specification_st *create_info, - bool silent) + Schema_specification_st *create_info) { char path[FN_REFLEN+16]; MY_STAT stat_info; @@ -842,7 +840,7 @@ mysql_create_db_internal(THD *thd, const Lex_ident_db &db, backup_log_ddl(&ddl_log); not_silent: - if (!silent) + if (!(thd->binlog_state & BINLOG_STATE_TMP_DISABLED)) { char *query; uint query_length; @@ -974,7 +972,7 @@ int mysql_create_db(THD *thd, const Lex_ident_db &db, DDL_options_st options, if (thd->slave_thread && slave_ddl_exec_mode_options == SLAVE_EXEC_MODE_IDEMPOTENT) options.add(DDL_options::OPT_IF_NOT_EXISTS); - return mysql_create_db_internal(thd, db, options, &tmp, false); + return mysql_create_db_internal(thd, db, options, &tmp); } @@ -1939,6 +1937,7 @@ bool mysql_upgrade_db(THD *thd, const Lex_ident_db &old_db) TABLE_LIST *table_list; SELECT_LEX *sl= thd->lex->current_select; Lex_ident_db new_db; + BINLOG_STATE binlog_state_save; DBUG_ENTER("mysql_upgrade_db"); if ((old_db.length <= MYSQL50_TABLE_NAME_PREFIX_LENGTH) || @@ -1985,9 +1984,10 @@ bool mysql_upgrade_db(THD *thd, const Lex_ident_db &old_db) } /* Step1: Create the new database */ - if (unlikely((error= mysql_create_db_internal(thd, new_db, - DDL_options(), &create_info, - 1)))) + thd->tmp_disable_binlog(&binlog_state_save, BINLOG_STATE_TMP_DISABLED); + error= mysql_create_db_internal(thd, new_db, DDL_options(), &create_info); + thd->reenable_binlog(&binlog_state_save); + if (unlikely((error))) goto exit; /* Step2: Move tables to the new database */ From e983ad20aec6dfd1a2ea19ad17716bf5965a3430 Mon Sep 17 00:00:00 2001 From: Brandon Nesterenko Date: Thu, 28 May 2026 11:56:06 -0600 Subject: [PATCH 5/5] MDEV-38796 Replace mysql_bin_log.is_open with binlog_ready() Addresses TODO - Remove all testing of mysql_bin_log.is_open(). Instead test for binlog_ready() in main code and add testing of is_open() when trying to commit the binary log. This is needed as currently mysql_bin_log.is_open() is tested without a mutex which makes it unreliable. And also adds tests for the binlog closing for various types of statements in-between a thd->binlog_ready() check and the commit-time acquiring of mysql_bin_log.LOCK_log. Still TODO from the above request: * WSREP related mysql_bin_log.is_open() changes as a part of MDEV-38865 --- .../binlog/t/binlog_close_during_commit.test | 313 ++++++++++++++++++ sql/backup.cc | 2 +- sql/ddl_log.cc | 2 +- sql/item_create.cc | 2 +- sql/log.cc | 8 + sql/log_event_server.cc | 1 + sql/sp.cc | 4 +- sql/sql_acl.cc | 5 +- sql/sql_class.cc | 2 +- sql/sql_insert.cc | 4 +- sql/sql_load.cc | 14 +- sql/sql_parse.cc | 5 +- sql/sql_prepare.cc | 3 +- sql/sql_rename.cc | 2 +- sql/sql_repl.cc | 4 +- sql/sql_table.cc | 5 +- sql/temporary_tables.cc | 2 +- storage/maria/ha_maria.cc | 2 +- 18 files changed, 355 insertions(+), 25 deletions(-) create mode 100644 mysql-test/suite/binlog/t/binlog_close_during_commit.test diff --git a/mysql-test/suite/binlog/t/binlog_close_during_commit.test b/mysql-test/suite/binlog/t/binlog_close_during_commit.test new file mode 100644 index 0000000000000..bf0b1b5b12bd5 --- /dev/null +++ b/mysql-test/suite/binlog/t/binlog_close_during_commit.test @@ -0,0 +1,313 @@ +# +# Purpose: +# Exercise the race where a statement passes the per-THD +# thd->binlog_ready() check, then the global binary log is closed by +# another connection before the statement acquires LOCK_log to commit +# into the binlog. The statement must still commit in the storage +# engine, and the error log must contain exactly one ER_ERROR_ON_WRITE +# entry per binlog-close event (not one per affected transaction). +# After the close, the binary log remains closed and subsequent +# transactions continue to be processed in engines but are not +# binlogged, with no further ER_ERROR_ON_WRITE entries logged. +# +# Methodology: +# DEBUG_SYNC point 'commit_after_binlog_ready_before_LOCK_log' +# pauses the statement in MYSQL_BIN_LOG::write_transaction_to_binlog +# immediately after the thd->binlog_ready() check, but before the +# transaction is queued for group commit (and before LOCK_log is +# taken). From a second connection, the binlog is closed by setting +# DBUG injection 'fault_injection_new_file_rotate_event' and running +# FLUSH LOGS. The fault makes the rotation fail, which leaves the +# binlog in a closed state (see the "Binlog could be closed if +# rotation fails" comment in trx_group_commit_with_engines). The +# first connection, still paused at the sync point, is then resumed +# and reaches LOCK_log to find the binlog closed. +# +# Five statement types are covered: +# 1) DML on a transactional table (InnoDB) +# 2) DML on a non-transactional table (MyISAM) +# 3) Multi-table UPDATE targeting both a trans and a non-trans +# table +# 4) DML selecting from a temporary table +# 5) DDL: CREATE TABLE (atomic DDL, InnoDB) +# +# References: +# MDEV-38796: branch context for the binlog_ready() refactor +# + +--source include/have_log_bin.inc +--source include/have_debug.inc +--source include/have_debug_sync.inc +--source include/have_innodb.inc +--source include/have_binlog_format_row.inc + +call mtr.add_suppression("\\[ERROR\\] Error writing file '.*master-bin.*' .*errno: 9"); +call mtr.add_suppression("\\[ERROR\\] Error writing file"); + +# Search the per-test section of the primary's error log for the +# expected ER_ERROR_ON_WRITE entry. +--let $error_log= $MYSQLTEST_VARDIR/log/mysqld.1.err +--let SEARCH_FILE= $error_log + + +--echo # +--echo # Sub-case 1: DML on a transactional table +--echo # +create table t_trans (id int primary key) engine=innodb; +create table t2 (id int primary key) engine=innodb; + +--connect (con1,localhost,root,,test) +--connect (con3,localhost,root,,test) + +--connection con1 +SET DEBUG_SYNC= 'commit_after_binlog_ready_before_LOCK_log SIGNAL paused WAIT_FOR go'; +SET DEBUG_DBUG= '+d,commit_after_binlog_ready_before_LOCK_log'; +--send insert into t_trans values (1) + +--connection default +SET DEBUG_SYNC= 'now WAIT_FOR paused'; + +--echo # Trigger the binlog error and permanent closure via a failed rotation +SET DEBUG_DBUG= '+d,fault_injection_new_file_rotate_event'; +--error ER_ERROR_ON_WRITE +FLUSH LOGS; + +SET DEBUG_DBUG= ''; +SET DEBUG_SYNC= 'now SIGNAL go'; + +--connection con1 +--reap + +--connection default +--echo # con1's statement must have committed in the engine +select * from t_trans; +--echo # con2's (default conn's) statement must have committed in the engine +select * from t2; + +--echo # A follow-up commit on a third connection must also succeed without +--echo # logging another ER_ERROR_ON_WRITE +--connection con3 +insert into t2 values (102); +select * from t2 order by id; + +--connection default +--let $SEARCH_PATTERN= Error writing file +--source include/search_pattern_in_file.inc + +--disconnect con1 +--disconnect con3 +drop table t_trans, t2; + +--echo # Reopen the binlog for the next sub-case +--source include/restart_mysqld.inc + + +--echo # +--echo # Sub-case 2: DML on a non-transactional table +--echo # +create table t_myisam (id int primary key) engine=myisam; +create table t2 (id int primary key) engine=innodb; + +--connect (con1,localhost,root,,test) +--connect (con3,localhost,root,,test) + +--connection con1 +SET DEBUG_SYNC= 'commit_after_binlog_ready_before_LOCK_log SIGNAL paused WAIT_FOR go'; +--send insert into t_myisam values (1) + +--connection default +SET DEBUG_SYNC= 'now WAIT_FOR paused'; + +--echo # Trigger the binlog error and permanent closure via a failed rotation +SET DEBUG_DBUG= '+d,fault_injection_new_file_rotate_event'; +--error ER_ERROR_ON_WRITE +FLUSH LOGS; + +SET DEBUG_DBUG= ''; +SET DEBUG_SYNC= 'now SIGNAL go'; + +--connection con1 +--reap + +--connection default +--echo # con1's statement must have committed in the engine +select * from t_myisam; +--echo # con2's (default conn's) statement must have committed in the engine +select * from t2; + +--connection con3 +insert into t2 values (202); +select * from t2 order by id; + +--connection default +--let $SEARCH_PATTERN= Error writing file +--source include/search_pattern_in_file.inc + +--disconnect con1 +--disconnect con3 +drop table t_myisam, t2; + +--source include/restart_mysqld.inc + + +--echo # +--echo # Sub-case 3: Multi-table UPDATE on trans + non-trans +--echo # +create table t_trans (id int primary key, v int) engine=innodb; +create table t_myisam (id int primary key, v int) engine=myisam; +create table t2 (id int primary key) engine=innodb; +insert into t_trans values (1, 0), (2, 0); +insert into t_myisam values (1, 0), (2, 0); + +--connect (con1,localhost,root,,test) +--connect (con3,localhost,root,,test) + +--connection con1 +SET DEBUG_SYNC= 'commit_after_binlog_ready_before_LOCK_log SIGNAL paused WAIT_FOR go'; +--send update t_trans, t_myisam set t_trans.v = 1, t_myisam.v = 1 where t_trans.id = t_myisam.id + +--connection default +SET DEBUG_SYNC= 'now WAIT_FOR paused'; + +--echo # Trigger the binlog error and permanent closure via a failed rotation +SET DEBUG_DBUG= '+d,fault_injection_new_file_rotate_event'; +--error ER_ERROR_ON_WRITE +FLUSH LOGS; + +SET DEBUG_DBUG= ''; +SET DEBUG_SYNC= 'now SIGNAL go'; + +--connection con1 +--reap + +--connection default +--echo # con1's statement must have committed in the engine +select * from t_trans order by id; +select * from t_myisam order by id; +--echo # con2's (default conn's) statement must have committed in the engine +select * from t2; + +--connection con3 +insert into t2 values (302); +select * from t2 order by id; + +--connection default +--let $SEARCH_PATTERN= Error writing file +--source include/search_pattern_in_file.inc + +--disconnect con1 +--disconnect con3 +drop table t_trans, t_myisam, t2; + +--source include/restart_mysqld.inc + + +--echo # +--echo # Sub-case 4: DML selecting from a temporary table +--echo # +# Temporary table DML still funnels through the same commit path as +# regular DML; the binlog cache is statement-cached but the commit +# goes through write_transaction_to_binlog. +create table t_trans (id int primary key) engine=innodb; +create table t2 (id int primary key) engine=innodb; + +--connect (con1,localhost,root,,test) +--connect (con3,localhost,root,,test) + +--connection con1 +create temporary table t_tmp (id int primary key) engine=innodb; +insert into t_tmp values (1); +SET DEBUG_SYNC= 'commit_after_binlog_ready_before_LOCK_log SIGNAL paused WAIT_FOR go'; +--send insert into t_trans (id) select id from t_tmp + +--connection default +SET DEBUG_SYNC= 'now WAIT_FOR paused'; + +--echo # Trigger the binlog error and permanent closure via a failed rotation +SET DEBUG_DBUG= '+d,fault_injection_new_file_rotate_event'; +--error ER_ERROR_ON_WRITE +FLUSH LOGS; + +SET DEBUG_DBUG= ''; +SET DEBUG_SYNC= 'now SIGNAL go'; + +--connection con1 +--reap +--echo # con1's statement must have committed in the engine +select * from t_tmp; +select * from t_trans; +drop temporary table t_tmp; + +--connection default +--echo # con2's (default conn's) statement must have committed in the engine +select * from t2; + +--connection con3 +insert into t2 values (402); +select * from t2 order by id; + +--connection default +--let $SEARCH_PATTERN= Error writing file +--source include/search_pattern_in_file.inc + +--disconnect con1 +--disconnect con3 +drop table t_trans, t2; + +--source include/restart_mysqld.inc + + +--echo # +--echo # Sub-case 5: DDL (CREATE TABLE) +--echo # +create table t2 (id int primary key) engine=innodb; + +--connect (con1,localhost,root,,test) +--connect (con3,localhost,root,,test) + +--connection con1 +SET DEBUG_SYNC= 'commit_after_binlog_ready_before_LOCK_log SIGNAL paused WAIT_FOR go'; +--send create table t_ddl (id int primary key) engine=innodb + +--connection default +SET DEBUG_SYNC= 'now WAIT_FOR paused'; + +--echo # Trigger the binlog error and permanent closure via a failed rotation +SET DEBUG_DBUG= '+d,fault_injection_new_file_rotate_event'; +--error ER_ERROR_ON_WRITE +FLUSH LOGS; + +SET DEBUG_DBUG= ''; +SET DEBUG_SYNC= 'now SIGNAL go'; + +--connection con1 +--reap + +--connection default +--echo # DDL must have taken effect in the data dictionary +--replace_column 6 # 7 # 8 # 9 # +show create table t_ddl; +--echo # con2's (default conn's) statement must have committed in the engine +select * from t2; + +--connection con3 +insert into t2 values (502); +select * from t2 order by id; + +--connection default +--let $SEARCH_PATTERN= Error writing file +--source include/search_pattern_in_file.inc + +--disconnect con1 +--disconnect con3 +drop table t_ddl, t2; + +--source include/restart_mysqld.inc + + +--echo # +--echo # Cleanup +--echo # +SET DEBUG_SYNC= 'RESET'; + +--echo # End of binlog_close_during_commit.test diff --git a/sql/backup.cc b/sql/backup.cc index 8184c6c3f8a01..d5269bbaddc1b 100644 --- a/sql/backup.cc +++ b/sql/backup.cc @@ -406,7 +406,7 @@ static bool backup_block_commit(THD *thd) /* We can ignore errors from flush_tables () */ (void) flush_tables(thd, FLUSH_SYS_TABLES); - if (mysql_bin_log.is_open()) + if (thd->binlog_ready_no_wsrep()) { mysql_mutex_lock(mysql_bin_log.get_log_lock()); mysql_file_sync(mysql_bin_log.get_log_file()->file, MYF(MY_WME)); diff --git a/sql/ddl_log.cc b/sql/ddl_log.cc index 156fd2d831f02..e3423242392ca 100644 --- a/sql/ddl_log.cc +++ b/sql/ddl_log.cc @@ -70,7 +70,7 @@ After the final recovery phase is done, the file is truncated. Note that in this file we test for mysql_bin_log.is_open() instead of - using thd->binlog_is_read(). This is because during recovery binlog + using thd->binlog_ready(). This is because during recovery binlog cannot be disabled for some particular commands. History: diff --git a/sql/item_create.cc b/sql/item_create.cc index 1e8ae9b187add..a5c6df66063f2 100644 --- a/sql/item_create.cc +++ b/sql/item_create.cc @@ -3343,7 +3343,7 @@ Item* Create_func_binlog_gtid_pos::create_2_arg(THD *thd, Item *arg1, Item *arg2) { #ifdef HAVE_REPLICATION - if (unlikely(!mysql_bin_log.is_open())) + if (unlikely(!(thd->binlog_state & BINLOG_STATE_OPEN))) #endif { my_error(ER_NO_BINARY_LOGGING, MYF(0)); diff --git a/sql/log.cc b/sql/log.cc index 02edf42fb8004..dfec7ed3788be 100644 --- a/sql/log.cc +++ b/sql/log.cc @@ -8539,6 +8539,14 @@ MYSQL_BIN_LOG::write_transaction_to_binlog(THD *thd, DBUG_RETURN(0); } + /* + Pause point between the per-THD binlog_ready() check and acquisition of + LOCK_log. Used by binlog.binlog_close_during_commit to inject a global + binlog close from a second connection and exercise the race where the + cached state says "ready" but the global binlog is no longer open. + */ + DEBUG_SYNC(thd, "commit_after_binlog_ready_before_LOCK_log"); + entry.thd= thd; entry.cache_mngr= cache_mngr; entry.error= 0; diff --git a/sql/log_event_server.cc b/sql/log_event_server.cc index e04a76f3583ea..4aed2b5219444 100644 --- a/sql/log_event_server.cc +++ b/sql/log_event_server.cc @@ -693,6 +693,7 @@ int Log_event_writer::write_internal(const uchar *pos, size_t len) DBUG_ASSERT(!ctx || encrypt_or_write == &Log_event_writer::encrypt_and_write); if (cache_data && #ifdef WITH_WSREP + /* TODO: MDEV-38865 */ mysql_bin_log.is_open() && #endif cache_data->write_prepare(len)) diff --git a/sql/sp.cc b/sql/sp.cc index ae51063128d31..8f92917b33a69 100644 --- a/sql/sp.cc +++ b/sql/sp.cc @@ -1467,7 +1467,7 @@ Sp_handler::sp_create_routine(THD *thd, const sp_head *sp) const } if (type() == SP_TYPE_FUNCTION && - !trust_function_creators && mysql_bin_log.is_open()) + !trust_function_creators && thd->binlog_ready_no_wsrep()) { if (!sp->detistic()) { @@ -1715,7 +1715,7 @@ Sp_handler::sp_update_routine(THD *thd, const Database_qualified_name *name, if ((ret= db_find_routine_aux(thd, name, table)) == SP_OK) { if (type() == SP_TYPE_FUNCTION && ! trust_function_creators && - mysql_bin_log.is_open() && + thd->binlog_ready_no_wsrep() && (chistics->daccess == SP_CONTAINS_SQL || chistics->daccess == SP_MODIFIES_SQL_DATA)) { diff --git a/sql/sql_acl.cc b/sql/sql_acl.cc index 44c274fd12257..d979e0a501e51 100644 --- a/sql/sql_acl.cc +++ b/sql/sql_acl.cc @@ -4423,6 +4423,9 @@ int acl_set_default_role(THD *thd, if (!strcasecmp(rolename.str, none.str)) clear_role= TRUE; + /* + TODO: MDEV-38865 + */ if (mysql_bin_log.is_open() || (WSREP(thd) && !IF_WSREP(thd->wsrep_applier, 0))) { @@ -11664,7 +11667,7 @@ bool mysql_rename_user(THD *thd, List &list) if (result) my_error(ER_CANNOT_USER, MYF(0), "RENAME USER", wrong_users.c_ptr_safe()); - if (some_users_renamed && mysql_bin_log.is_open()) + if (some_users_renamed && thd->binlog_ready_no_wsrep()) result |= write_bin_log(thd, FALSE, thd->query(), thd->query_length()); mysql_rwlock_unlock(&LOCK_grant); diff --git a/sql/sql_class.cc b/sql/sql_class.cc index 8bcc371aefbec..513c0bdf5638e 100644 --- a/sql/sql_class.cc +++ b/sql/sql_class.cc @@ -8379,7 +8379,7 @@ int THD::binlog_query(THD::enum_binlog_query_type qtype, char const *query_arg, bool THD::binlog_current_query_unfiltered() { - if (!mysql_bin_log.is_open()) + if (!binlog_ready_no_wsrep()) return 0; binlog_state= binlog_state & ~BINLOG_STATE_READONLY; diff --git a/sql/sql_insert.cc b/sql/sql_insert.cc index 0b7b5142c1698..6ca79100d7f9e 100644 --- a/sql/sql_insert.cc +++ b/sql/sql_insert.cc @@ -5150,7 +5150,7 @@ select_create::prepare(List &_values, SELECT_LEX_UNIT *u) */ if (!thd->lex->tmp_table() && thd->is_current_stmt_binlog_format_row() && - mysql_bin_log.is_open()) + thd->binlog_ready_no_wsrep()) { thd->binlog_start_trans_and_stmt(); } @@ -5688,7 +5688,7 @@ void select_create::abort_result_set() table=0; // Safety if (thd->log_current_statement()) { - if (mysql_bin_log.is_open()) + if (thd->binlog_ready_no_wsrep()) { /* Remove logging of drop, create + insert rows */ binlog_reset_cache(thd); diff --git a/sql/sql_load.cc b/sql/sql_load.cc index 97f5f0011ba6e..94594cc16b32a 100644 --- a/sql/sql_load.cc +++ b/sql/sql_load.cc @@ -163,6 +163,7 @@ class READ_INFO: public Load_data_param int *stack,*stack_pos; bool found_end_of_line,start_of_line,eof; int level; /* for load xml */ + bool use_binlog; bool getbyte(char *to) { @@ -271,7 +272,7 @@ class READ_INFO: public Load_data_param void skip_data_till_eof() { #ifndef EMBEDDED_LIBRARY - if (mysql_bin_log.is_open()) + if (use_binlog) cache.read_function= cache.real_read_function; #endif while (GET != my_b_EOF) @@ -641,7 +642,7 @@ int mysql_load(THD *thd, const sql_exchange *ex, TABLE_LIST *table_list, } #ifndef EMBEDDED_LIBRARY - if (mysql_bin_log.is_open()) + if (thd->binlog_ready_no_wsrep()) { read_info.cache.thd = thd; read_info.cache.wrote_create_file = 0; @@ -766,7 +767,7 @@ int mysql_load(THD *thd, const sql_exchange *ex, TABLE_LIST *table_list, read_info.skip_data_till_eof(); #ifndef EMBEDDED_LIBRARY - if (mysql_bin_log.is_open()) + if (thd->binlog_ready_no_wsrep()) { { /* @@ -811,7 +812,7 @@ int mysql_load(THD *thd, const sql_exchange *ex, TABLE_LIST *table_list, thd->transaction->all.m_unsafe_rollback_flags|= (thd->transaction->stmt.m_unsafe_rollback_flags & THD_TRANS::DID_WAIT); #ifndef EMBEDDED_LIBRARY - if (mysql_bin_log.is_open()) + if (thd->binlog_ready_no_wsrep()) { /* We need to do the job that is normally done inside @@ -1454,7 +1455,8 @@ READ_INFO::READ_INFO(THD *thd, File file_par, file(file_par), m_field_term(field_term), m_line_term(line_term), m_line_start(line_start), escape_char(escape), found_end_of_line(false), eof(false), - error(false), line_cuted(false), found_null(false) + use_binlog(thd->binlog_ready_no_wsrep()), error(false), line_cuted(false), + found_null(false) { data.set_thread_specific(); /* @@ -1494,7 +1496,7 @@ READ_INFO::READ_INFO(THD *thd, File file_par, if (get_it_from_net) cache.read_function = _my_b_net_read; - if (mysql_bin_log.is_open()) + if (use_binlog) { cache.real_read_function= cache.read_function; cache.read_function= log_loaded_block; diff --git a/sql/sql_parse.cc b/sql/sql_parse.cc index 6f018805605d9..3828ccefde8f1 100644 --- a/sql/sql_parse.cc +++ b/sql/sql_parse.cc @@ -6374,7 +6374,7 @@ check_rename_table(THD *thd, TABLE_LIST *first_table, #ifndef DBUG_OFF static bool __attribute__ ((noinline)) generate_incident_event(THD *thd) { - if (mysql_bin_log.is_open()) + if (thd->binlog_ready_no_wsrep()) { Incident incident= INCIDENT_NONE; @@ -7501,6 +7501,9 @@ void THD::reset_for_next_command(bool do_clear_error) save_prep_leaf_list= false; #if defined(WITH_WSREP) && !defined(DBUG_OFF) +/* + TODO: MDEV-38865 +*/ if (mysql_bin_log.is_open()) DBUG_PRINT("info", ("is_current_stmt_binlog_format_row(): %d", diff --git a/sql/sql_prepare.cc b/sql/sql_prepare.cc index b94763fd1e0bd..9b07bd0c2fa5b 100644 --- a/sql/sql_prepare.cc +++ b/sql/sql_prepare.cc @@ -3947,7 +3947,8 @@ void Prepared_statement::setup_set_params() */ bool replace_params_with_values= false; // binlog - replace_params_with_values|= mysql_bin_log.is_open() && is_update_query(lex->sql_command); + replace_params_with_values|= + thd->binlog_ready_no_wsrep() && is_update_query(lex->sql_command); // general or slow log replace_params_with_values|= opt_log || thd->variables.sql_log_slow; // query cache diff --git a/sql/sql_rename.cc b/sql/sql_rename.cc index 8658c23d37a37..8b67883d04ca2 100644 --- a/sql/sql_rename.cc +++ b/sql/sql_rename.cc @@ -183,7 +183,7 @@ bool mysql_rename_tables(THD *thd, TABLE_LIST *table_list, bool silent, */ thd->binlog_xid= thd->query_id; ddl_log_update_xid(&ddl_log_state, thd->binlog_xid); - if (mysql_bin_log.is_open()) + if (thd->binlog_ready_no_wsrep()) { if (not_logged_temporary_tables) binlog_error= thd->binlog_renamed_tmp_tables(table_list); diff --git a/sql/sql_repl.cc b/sql/sql_repl.cc index 483067e913465..546988000d1c6 100644 --- a/sql/sql_repl.cc +++ b/sql/sql_repl.cc @@ -655,7 +655,7 @@ bool purge_error_message(THD* thd, int res) bool purge_master_logs(THD* thd, const char* to_log) { char search_file_name[FN_REFLEN]; - if (!mysql_bin_log.is_open()) + if (!thd->binlog_ready_no_wsrep()) { my_ok(thd); return FALSE; @@ -681,7 +681,7 @@ bool purge_master_logs(THD* thd, const char* to_log) */ bool purge_master_logs_before_date(THD* thd, time_t purge_time) { - if (!mysql_bin_log.is_open()) + if (!thd->binlog_ready_no_wsrep()) { my_ok(thd); return 0; diff --git a/sql/sql_table.cc b/sql/sql_table.cc index b8c55350b98a5..2b1bb83c79329 100644 --- a/sql/sql_table.cc +++ b/sql/sql_table.cc @@ -12526,9 +12526,8 @@ do_continue:; THD_STAGE_INFO(thd, stage_end); DEBUG_SYNC(thd, "alter_table_before_main_binlog"); - DBUG_ASSERT(!(mysql_bin_log.is_open() && - thd->is_binlog_format_row() && - (create_info->tmp_table()))); + DBUG_ASSERT(!((mysql_bin_log.is_open() && thd->binlog_ready_no_wsrep()) && + thd->is_binlog_format_row() && (create_info->tmp_table()))); if (start_alter_id) { diff --git a/sql/temporary_tables.cc b/sql/temporary_tables.cc index a884f330c9a3f..dc6c0e10c80e7 100644 --- a/sql/temporary_tables.cc +++ b/sql/temporary_tables.cc @@ -663,7 +663,7 @@ bool THD::close_temporary_tables() } // Write DROP TEMPORARY TABLE query log events to binary log. - if (mysql_bin_log.is_open()) + if (binlog_ready_no_wsrep()) { error= log_events_and_free_tmp_shares(); } diff --git a/storage/maria/ha_maria.cc b/storage/maria/ha_maria.cc index 78200ce130000..09620dc712081 100644 --- a/storage/maria/ha_maria.cc +++ b/storage/maria/ha_maria.cc @@ -3234,7 +3234,7 @@ THR_LOCK_DATA **ha_maria::store_lock(THD *thd, sql_command != SQLCOM_ALTER_TABLE && sql_command != SQLCOM_LOCK_TABLES) && (thd->variables.option_bits & OPTION_BIN_LOG) && - mysql_bin_log.is_open()) + thd->binlog_ready_no_wsrep()) lock_type= TL_READ_NO_INSERT; else if (lock_type == TL_WRITE_CONCURRENT_INSERT) {