From f441b87083910713069e5fa163156844f1807827 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 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: After a one-time AT event executes, compute_next_execution_time() sets status=DISABLED and persists both the DISABLED status and last_executed to mysql.event. When the user reschedules with ALTER EVENT ... ENABLE, the status is updated but last_executed remains stale. On server restart, load_events_from_db() calls compute_next_execution_time() which sees last_executed != 0 and unconditionally disables the event again, even though execute_at was changed to a future time. 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. This ensures the event is treated as not-yet-executed for the new schedule. - 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 | 62 ++++++++++++ .../main/mdev38632_alter_onetime_event.test | 97 +++++++++++++++++++ sql/event_data_objects.cc | 8 +- sql/event_db_repository.cc | 24 +++++ 4 files changed, 189 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..a6033aa823d86 --- /dev/null +++ b/mysql-test/main/mdev38632_alter_onetime_event.result @@ -0,0 +1,62 @@ +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 +# 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 with explicit ENABLE +# This is the customer's scenario from the JIRA report. +ALTER EVENT test.mdev38632 +ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 SECOND +ON COMPLETION PRESERVE ENABLE; +# After ALTER with ENABLE and a new schedule, last_executed should be cleared. +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 +# The event should fire again after being rescheduled. +# Step 3: Verify event fires after server restart (the customer scenario). +ALTER EVENT test.mdev38632 +ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 3 SECOND +ON COMPLETION PRESERVE ENABLE; +# 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 server to trigger full reload from mysql.event. +# restart +SET GLOBAL event_scheduler = ON; +# The event should fire after server restart. +# Step 4: User-disabled event stays DISABLED after reschedule without ENABLE +ALTER EVENT test.mdev38632 DISABLE; +ALTER EVENT test.mdev38632 +ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 2 HOUR; +# Status remains DISABLED — explicit ENABLE is required to reschedule. +SELECT status FROM mysql.event WHERE db = 'test' AND name = 'mdev38632'; +status +DISABLED +# Step 5: ALTER that doesn't change schedule 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 and is DISABLED. ALTER body only (no schedule change). +ALTER EVENT test.mdev38632 +DO INSERT INTO test.event_log(id) VALUES (NULL); +# Status should remain DISABLED (no schedule change) +SELECT status FROM mysql.event WHERE db = 'test' AND name = 'mdev38632'; +status +DISABLED +# Cleanup +DROP EVENT 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..3fcf2dae35737 --- /dev/null +++ b/mysql-test/main/mdev38632_alter_onetime_event.test @@ -0,0 +1,97 @@ +# +# 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, the scheduler sets +# status=DISABLED and last_executed in mysql.event. A subsequent ALTER EVENT +# with explicit ENABLE and a new schedule should allow the event to fire again. +# +# The fix clears last_executed when execute_at changes, and only disables +# in compute_next_execution_time() if last_executed >= execute_at. +# + +--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 # 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); + +--echo # Wait for the event to execute +let $wait_condition = SELECT COUNT(*) = 1 FROM test.event_log; +--source include/wait_condition.inc + +--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 # Step 2: ALTER EVENT to reschedule with explicit ENABLE +--echo # This is the customer's scenario from the JIRA report. +ALTER EVENT test.mdev38632 + ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 SECOND + ON COMPLETION PRESERVE ENABLE; + +--echo # After ALTER with ENABLE and a new schedule, last_executed should be cleared. +SELECT status, last_executed IS NOT NULL AS has_last_exec + 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 + +--echo # Step 3: Verify event fires after server restart (the customer scenario). +ALTER EVENT test.mdev38632 + ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 3 SECOND + ON COMPLETION PRESERVE ENABLE; + +--echo # 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 server to trigger full reload from mysql.event. +--source include/restart_mysqld.inc +SET GLOBAL event_scheduler = ON; +--source include/running_event_scheduler.inc + +--echo # The event should fire after server restart. +--let $wait_timeout = 10 +let $wait_condition = SELECT COUNT(*) = 3 FROM test.event_log; +--source include/wait_condition.inc + +--echo # Step 4: User-disabled event stays DISABLED after reschedule without ENABLE +ALTER EVENT test.mdev38632 DISABLE; + +ALTER EVENT test.mdev38632 + ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 2 HOUR; + +--echo # Status remains DISABLED — explicit ENABLE is required to reschedule. +SELECT status FROM mysql.event WHERE db = 'test' AND name = 'mdev38632'; + +--echo # Step 5: ALTER that doesn't change schedule 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); + +let $wait_condition = SELECT COUNT(*) = 4 FROM test.event_log; +--source include/wait_condition.inc + +--echo # Event executed and is DISABLED. ALTER body only (no schedule change). +ALTER EVENT test.mdev38632 + DO INSERT INTO test.event_log(id) VALUES (NULL); + +--echo # Status should remain DISABLED (no schedule change) +SELECT status FROM mysql.event WHERE db = 'test' AND name = 'mdev38632'; + +--echo # Cleanup +DROP EVENT 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 4fdfad925a760..006716b766376 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..b4adb5994ec72 100644 --- a/sql/event_db_repository.cc +++ b/sql/event_db_repository.cc @@ -319,6 +319,30 @@ 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. + A new execute_at means the event hasn't been executed for this + schedule yet. Without this, compute_next_execution_time() would + see the stale last_executed and disable the event on reload. + + Only clear when execute_at actually changed. Compare the new value + against the stored one before overwriting. + */ + 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) + fields[ET_FIELD_LAST_EXECUTED]->set_null(); + } + fields[ET_FIELD_EXECUTE_AT]->set_notnull(); fields[ET_FIELD_EXECUTE_AT]->store_time(&time); }