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); }