From cb709bab8b4c556f3f6226797436d6b688a6d0b0 Mon Sep 17 00:00:00 2001 From: Meng-Hsiu Chiang Date: Wed, 22 Apr 2026 19:36:40 +0000 Subject: [PATCH] MDEV-38632 ALTER EVENT doesn't re-run one-time AT event after first execution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A one-time event (ON SCHEDULE AT ... ON COMPLETION PRESERVE) could not be rescheduled via ALTER EVENT after its first execution. The event would silently fail to fire again. Root cause: two bugs in the event scheduler. 1. After a one-time AT event executes, compute_next_execution_time() sets status=DISABLED and update_timing_fields_for_event() persists both DISABLED status and last_executed to mysql.event. When ALTER EVENT changes the schedule without explicit ENABLE, status_changed is false, so the DISABLED status is never overwritten in the table. Event_queue::update_event() loads the element as DISABLED and discards it without queuing. 2. Even with explicit ENABLE, compute_next_execution_time() checks only `if (last_executed)` for AT events and unconditionally sets status=DISABLED. Since last_executed is never cleared on reschedule, the event gets re-disabled on every scheduler reload or queue insertion. Fix: - event_db_repository.cc (mysql_event_fill_row): When ALTER EVENT changes execute_at to a new value, clear last_executed in mysql.event. Also re-enable the event if it was auto-disabled by the scheduler after execution — detected by: status_changed is false (user didn't explicitly set ENABLE/DISABLE), stored status is DISABLED, and last_executed >= old execute_at (the scheduler disabled it after running at the previously scheduled time). This avoids incorrectly re-enabling events the user explicitly disabled. - event_data_objects.cc (compute_next_execution_time): For one-time events, only disable if last_executed >= execute_at, meaning the event was actually executed for the current schedule. If execute_at is after last_executed (rescheduled), treat it as pending. This serves as a safety net for server restarts where last_executed may persist from an older schedule. All new code of the whole pull request, including one or several files that are either new files or modified ones, are contributed under the BSD-new license. I am contributing on behalf of my employer Amazon Web Services, Inc. --- .../main/mdev38632_alter_onetime_event.result | 88 +++++++++++++ .../main/mdev38632_alter_onetime_event.test | 119 ++++++++++++++++++ sql/event_data_objects.cc | 8 +- sql/event_db_repository.cc | 48 +++++++ 4 files changed, 261 insertions(+), 2 deletions(-) create mode 100644 mysql-test/main/mdev38632_alter_onetime_event.result create mode 100644 mysql-test/main/mdev38632_alter_onetime_event.test diff --git a/mysql-test/main/mdev38632_alter_onetime_event.result b/mysql-test/main/mdev38632_alter_onetime_event.result new file mode 100644 index 0000000000000..381c677dbe165 --- /dev/null +++ b/mysql-test/main/mdev38632_alter_onetime_event.result @@ -0,0 +1,88 @@ +SET GLOBAL event_scheduler = ON; +CREATE TABLE test.event_log (id INT AUTO_INCREMENT PRIMARY KEY, ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP); +# +# Step 1: Create a one-time AT event with ON COMPLETION PRESERVE +# +CREATE EVENT test.mdev38632 +ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 SECOND +ON COMPLETION PRESERVE ENABLE +DO INSERT INTO test.event_log(id) VALUES (NULL); +# Wait for the event to execute +SELECT COUNT(*) AS exec_count FROM test.event_log; +exec_count +1 +# Verify: event executed, status is now DISABLED (preserved but done) +SELECT status, on_completion, last_executed IS NOT NULL AS has_last_exec +FROM mysql.event WHERE db = 'test' AND name = 'mdev38632'; +status on_completion has_last_exec +DISABLED PRESERVE 1 +# +# Step 2: ALTER EVENT to reschedule — without explicit ENABLE +# This is the customer's scenario from the JIRA report. +# +ALTER EVENT test.mdev38632 +ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 SECOND; +# After ALTER with a new schedule, status should be re-enabled automatically. +SELECT status FROM mysql.event WHERE db = 'test' AND name = 'mdev38632'; +status +ENABLED +# The event should fire again after being rescheduled. +SELECT COUNT(*) AS exec_count FROM test.event_log; +exec_count +2 +# +# Step 3: Try again with explicit ENABLE — verify scheduler reload works. +# +ALTER EVENT test.mdev38632 +ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 SECOND +ON COMPLETION PRESERVE ENABLE; +# After fix, last_executed should be cleared when schedule changes. +SELECT status, last_executed IS NOT NULL AS has_last_exec +FROM mysql.event WHERE db = 'test' AND name = 'mdev38632'; +status has_last_exec +ENABLED 0 +# Restart the scheduler to trigger reload from mysql.event. +# After fix, the event should remain ENABLED after reload. +SET GLOBAL event_scheduler = OFF; +SET GLOBAL event_scheduler = ON; +# Status should remain ENABLED after scheduler reload. +SELECT status FROM mysql.event WHERE db = 'test' AND name = 'mdev38632'; +status +ENABLED +# +# Step 4: User-disabled event should NOT be re-enabled by reschedule +# +ALTER EVENT test.mdev38632 +ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 HOUR +ON COMPLETION PRESERVE ENABLE; +ALTER EVENT test.mdev38632 DISABLE; +SELECT status FROM mysql.event WHERE db = 'test' AND name = 'mdev38632'; +status +DISABLED +# Reschedule without explicit ENABLE — should stay DISABLED +ALTER EVENT test.mdev38632 +ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 2 HOUR; +SELECT status FROM mysql.event WHERE db = 'test' AND name = 'mdev38632'; +status +DISABLED +# +# Step 5: ALTER with same execute_at should not clear last_executed +# +DROP EVENT test.mdev38632; +CREATE EVENT test.mdev38632 +ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 SECOND +ON COMPLETION PRESERVE ENABLE +DO INSERT INTO test.event_log(id) VALUES (NULL); +# Event executed, now ALTER with same body but no schedule change +ALTER EVENT test.mdev38632 +DO INSERT INTO test.event_log(id) VALUES (NULL); +# Status should remain DISABLED (no schedule change, no re-enable) +SELECT status FROM mysql.event WHERE db = 'test' AND name = 'mdev38632'; +status +DISABLED +# +# Cleanup +# +DROP EVENT IF EXISTS test.mdev38632; +DROP TABLE test.event_log; +SET GLOBAL event_scheduler = OFF; diff --git a/mysql-test/main/mdev38632_alter_onetime_event.test b/mysql-test/main/mdev38632_alter_onetime_event.test new file mode 100644 index 0000000000000..a1792ef99e149 --- /dev/null +++ b/mysql-test/main/mdev38632_alter_onetime_event.test @@ -0,0 +1,119 @@ +# +# MDEV-38632: ALTER EVENT doesn't run a one-time (AT) event after its first +# execution when ON COMPLETION PRESERVE is used. +# +# After an AT event with ON COMPLETION PRESERVE executes: +# 1. compute_next_execution_time() sets status=DISABLED in mysql.event +# 2. ALTER EVENT without explicit ENABLE does not reset the status +# 3. The event queue refuses to queue a DISABLED event +# 4. Even with explicit ENABLE, last_executed causes re-disable on reload +# +# Expected fix: ALTER EVENT with a new schedule should re-enable the event +# and clear last_executed so it is properly queued. +# + +--source include/not_embedded.inc + +SET GLOBAL event_scheduler = ON; +--source include/running_event_scheduler.inc + +CREATE TABLE test.event_log (id INT AUTO_INCREMENT PRIMARY KEY, ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP); + +--echo # +--echo # Step 1: Create a one-time AT event with ON COMPLETION PRESERVE +--echo # +CREATE EVENT test.mdev38632 + ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 SECOND + ON COMPLETION PRESERVE ENABLE + DO INSERT INTO test.event_log(id) VALUES (NULL); + +--echo # Wait for the event to execute +let $wait_condition = SELECT COUNT(*) >= 1 FROM test.event_log; +--source include/wait_condition.inc + +SELECT COUNT(*) AS exec_count FROM test.event_log; + +--echo # Verify: event executed, status is now DISABLED (preserved but done) +SELECT status, on_completion, last_executed IS NOT NULL AS has_last_exec + FROM mysql.event WHERE db = 'test' AND name = 'mdev38632'; + +--echo # +--echo # Step 2: ALTER EVENT to reschedule — without explicit ENABLE +--echo # This is the customer's scenario from the JIRA report. +--echo # +ALTER EVENT test.mdev38632 + ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 SECOND; + +--echo # After ALTER with a new schedule, status should be re-enabled automatically. +SELECT status FROM mysql.event WHERE db = 'test' AND name = 'mdev38632'; + +--echo # The event should fire again after being rescheduled. +let $wait_condition = SELECT COUNT(*) >= 2 FROM test.event_log; +--source include/wait_condition.inc + +SELECT COUNT(*) AS exec_count FROM test.event_log; + +--echo # +--echo # Step 3: Try again with explicit ENABLE — verify scheduler reload works. +--echo # +ALTER EVENT test.mdev38632 + ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 SECOND + ON COMPLETION PRESERVE ENABLE; + +--echo # After fix, last_executed should be cleared when schedule changes. +SELECT status, last_executed IS NOT NULL AS has_last_exec + FROM mysql.event WHERE db = 'test' AND name = 'mdev38632'; + +--echo # Restart the scheduler to trigger reload from mysql.event. +--echo # After fix, the event should remain ENABLED after reload. +SET GLOBAL event_scheduler = OFF; +--source include/check_events_off.inc +SET GLOBAL event_scheduler = ON; +--source include/running_event_scheduler.inc + +--echo # Status should remain ENABLED after scheduler reload. +SELECT status FROM mysql.event WHERE db = 'test' AND name = 'mdev38632'; + +--echo # +--echo # Step 4: User-disabled event should NOT be re-enabled by reschedule +--echo # +ALTER EVENT test.mdev38632 + ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 HOUR + ON COMPLETION PRESERVE ENABLE; + +ALTER EVENT test.mdev38632 DISABLE; + +SELECT status FROM mysql.event WHERE db = 'test' AND name = 'mdev38632'; + +--echo # Reschedule without explicit ENABLE — should stay DISABLED +ALTER EVENT test.mdev38632 + ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 2 HOUR; + +SELECT status FROM mysql.event WHERE db = 'test' AND name = 'mdev38632'; + +--echo # +--echo # Step 5: ALTER with same execute_at should not clear last_executed +--echo # +DROP EVENT test.mdev38632; +CREATE EVENT test.mdev38632 + ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 SECOND + ON COMPLETION PRESERVE ENABLE + DO INSERT INTO test.event_log(id) VALUES (NULL); + +let $wait_condition = SELECT COUNT(*) >= 3 FROM test.event_log; +--source include/wait_condition.inc + +--echo # Event executed, now ALTER with same body but no schedule change +ALTER EVENT test.mdev38632 + DO INSERT INTO test.event_log(id) VALUES (NULL); + +--echo # Status should remain DISABLED (no schedule change, no re-enable) +SELECT status FROM mysql.event WHERE db = 'test' AND name = 'mdev38632'; + +--echo # +--echo # Cleanup +--echo # +DROP EVENT IF EXISTS test.mdev38632; +DROP TABLE test.event_log; +SET GLOBAL event_scheduler = OFF; +--source include/check_events_off.inc diff --git a/sql/event_data_objects.cc b/sql/event_data_objects.cc index 72bc2d38d0779..53b595e9247f9 100644 --- a/sql/event_data_objects.cc +++ b/sql/event_data_objects.cc @@ -948,8 +948,12 @@ Event_queue_element::compute_next_execution_time() /* If one-time, no need to do computation */ if (!expression) { - /* Let's check whether it was executed */ - if (last_executed) + /* + Check whether the event was already executed for the current schedule. + If execute_at was changed (via ALTER EVENT) to a time after + last_executed, the event should still be considered pending (MDEV-38632). + */ + if (last_executed && last_executed >= execute_at) { DBUG_PRINT("info",("One-time event %s.%s of was already executed", dbname.str, name.str)); diff --git a/sql/event_db_repository.cc b/sql/event_db_repository.cc index ad9f1c2cb4ea6..54087dae3ae05 100644 --- a/sql/event_db_repository.cc +++ b/sql/event_db_repository.cc @@ -319,6 +319,54 @@ mysql_event_fill_row(THD *thd, MYSQL_TIME time; my_tz_OFFSET0->gmt_sec_to_TIME(&time, et->execute_at); + /* + MDEV-38632: When ALTER EVENT changes execute_at, clear last_executed + and re-enable the event if it was auto-disabled by the scheduler. + + Only act when execute_at actually changed. Compare the new value + against the stored one before overwriting. + + Re-enable only if: the user didn't explicitly set status, the stored + status is DISABLED, and last_executed >= old execute_at (meaning the + scheduler disabled it after running at that time). + + Note: this heuristic cannot distinguish "scheduler auto-disabled" + from "user explicitly disabled an already-auto-disabled event" + since both states look identical in mysql.event. In the rare case + where a user explicitly disables an already-disabled event and then + reschedules it, the event would be incorrectly re-enabled. + */ + if (is_update) + { + bool schedule_changed= true; + MYSQL_TIME old_execute_at; + + if (!fields[ET_FIELD_EXECUTE_AT]->is_null() && + !fields[ET_FIELD_EXECUTE_AT]->get_date(&old_execute_at, + TIME_NO_ZERO_DATE | + thd->temporal_round_mode())) + schedule_changed= my_time_compare(&time, &old_execute_at) != 0; + + if (schedule_changed) + { + if (!et->status_changed && + fields[ET_FIELD_STATUS]->val_int() == Event_parse_data::DISABLED && + !fields[ET_FIELD_LAST_EXECUTED]->is_null()) + { + MYSQL_TIME old_last_executed; + if (!fields[ET_FIELD_LAST_EXECUTED]->get_date(&old_last_executed, + TIME_NO_ZERO_DATE | + thd->temporal_round_mode())) + { + if (my_time_compare(&old_last_executed, &old_execute_at) >= 0) + rs|= fields[ET_FIELD_STATUS]->store( + (longlong)Event_parse_data::ENABLED, TRUE); + } + } + fields[ET_FIELD_LAST_EXECUTED]->set_null(); + } + } + fields[ET_FIELD_EXECUTE_AT]->set_notnull(); fields[ET_FIELD_EXECUTE_AT]->store_time(&time); }