Skip to content

Commit 556915e

Browse files
committed
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.
1 parent c3f9ddf commit 556915e

4 files changed

Lines changed: 189 additions & 2 deletions

File tree

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
SET GLOBAL event_scheduler = ON;
2+
CREATE TABLE test.event_log (id INT AUTO_INCREMENT PRIMARY KEY, ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP);
3+
# Step 1: Create a one-time AT event with ON COMPLETION PRESERVE
4+
CREATE EVENT test.mdev38632
5+
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 SECOND
6+
ON COMPLETION PRESERVE ENABLE
7+
DO INSERT INTO test.event_log(id) VALUES (NULL);
8+
# Wait for the event to execute
9+
# Verify: event executed, status is now DISABLED (preserved but done)
10+
SELECT status, on_completion, last_executed IS NOT NULL AS has_last_exec
11+
FROM mysql.event WHERE db = 'test' AND name = 'mdev38632';
12+
status on_completion has_last_exec
13+
DISABLED PRESERVE 1
14+
# Step 2: ALTER EVENT to reschedule with explicit ENABLE
15+
# This is the customer's scenario from the JIRA report.
16+
ALTER EVENT test.mdev38632
17+
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 SECOND
18+
ON COMPLETION PRESERVE ENABLE;
19+
# After ALTER with ENABLE and a new schedule, last_executed should be cleared.
20+
SELECT status, last_executed IS NOT NULL AS has_last_exec
21+
FROM mysql.event WHERE db = 'test' AND name = 'mdev38632';
22+
status has_last_exec
23+
ENABLED 0
24+
# The event should fire again after being rescheduled.
25+
# Step 3: Verify event fires after server restart (the customer scenario).
26+
ALTER EVENT test.mdev38632
27+
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 3 SECOND
28+
ON COMPLETION PRESERVE ENABLE;
29+
# last_executed should be cleared when schedule changes.
30+
SELECT status, last_executed IS NOT NULL AS has_last_exec
31+
FROM mysql.event WHERE db = 'test' AND name = 'mdev38632';
32+
status has_last_exec
33+
ENABLED 0
34+
# Restart the server to trigger full reload from mysql.event.
35+
# restart
36+
SET GLOBAL event_scheduler = ON;
37+
# The event should fire after server restart.
38+
# Step 4: User-disabled event stays DISABLED after reschedule without ENABLE
39+
ALTER EVENT test.mdev38632 DISABLE;
40+
ALTER EVENT test.mdev38632
41+
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 2 HOUR;
42+
# Status remains DISABLED — explicit ENABLE is required to reschedule.
43+
SELECT status FROM mysql.event WHERE db = 'test' AND name = 'mdev38632';
44+
status
45+
DISABLED
46+
# Step 5: ALTER that doesn't change schedule should not clear last_executed
47+
DROP EVENT test.mdev38632;
48+
CREATE EVENT test.mdev38632
49+
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 SECOND
50+
ON COMPLETION PRESERVE ENABLE
51+
DO INSERT INTO test.event_log(id) VALUES (NULL);
52+
# Event executed and is DISABLED. ALTER body only (no schedule change).
53+
ALTER EVENT test.mdev38632
54+
DO INSERT INTO test.event_log(id) VALUES (NULL);
55+
# Status should remain DISABLED (no schedule change)
56+
SELECT status FROM mysql.event WHERE db = 'test' AND name = 'mdev38632';
57+
status
58+
DISABLED
59+
# Cleanup
60+
DROP EVENT test.mdev38632;
61+
DROP TABLE test.event_log;
62+
SET GLOBAL event_scheduler = OFF;
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
#
2+
# MDEV-38632: ALTER EVENT doesn't run a one-time (AT) event after its first
3+
# execution when ON COMPLETION PRESERVE is used.
4+
#
5+
# After an AT event with ON COMPLETION PRESERVE executes, the scheduler sets
6+
# status=DISABLED and last_executed in mysql.event. A subsequent ALTER EVENT
7+
# with explicit ENABLE and a new schedule should allow the event to fire again.
8+
#
9+
# The fix clears last_executed when execute_at changes, and only disables
10+
# in compute_next_execution_time() if last_executed >= execute_at.
11+
#
12+
13+
--source include/not_embedded.inc
14+
15+
SET GLOBAL event_scheduler = ON;
16+
--source include/running_event_scheduler.inc
17+
18+
CREATE TABLE test.event_log (id INT AUTO_INCREMENT PRIMARY KEY, ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP);
19+
20+
--echo # Step 1: Create a one-time AT event with ON COMPLETION PRESERVE
21+
CREATE EVENT test.mdev38632
22+
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 SECOND
23+
ON COMPLETION PRESERVE ENABLE
24+
DO INSERT INTO test.event_log(id) VALUES (NULL);
25+
26+
--echo # Wait for the event to execute
27+
let $wait_condition = SELECT COUNT(*) = 1 FROM test.event_log;
28+
--source include/wait_condition.inc
29+
30+
--echo # Verify: event executed, status is now DISABLED (preserved but done)
31+
SELECT status, on_completion, last_executed IS NOT NULL AS has_last_exec
32+
FROM mysql.event WHERE db = 'test' AND name = 'mdev38632';
33+
34+
--echo # Step 2: ALTER EVENT to reschedule with explicit ENABLE
35+
--echo # This is the customer's scenario from the JIRA report.
36+
ALTER EVENT test.mdev38632
37+
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 SECOND
38+
ON COMPLETION PRESERVE ENABLE;
39+
40+
--echo # After ALTER with ENABLE and a new schedule, last_executed should be cleared.
41+
SELECT status, last_executed IS NOT NULL AS has_last_exec
42+
FROM mysql.event WHERE db = 'test' AND name = 'mdev38632';
43+
44+
--echo # The event should fire again after being rescheduled.
45+
let $wait_condition = SELECT COUNT(*) = 2 FROM test.event_log;
46+
--source include/wait_condition.inc
47+
48+
--echo # Step 3: Verify event fires after server restart (the customer scenario).
49+
ALTER EVENT test.mdev38632
50+
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 3 SECOND
51+
ON COMPLETION PRESERVE ENABLE;
52+
53+
--echo # last_executed should be cleared when schedule changes.
54+
SELECT status, last_executed IS NOT NULL AS has_last_exec
55+
FROM mysql.event WHERE db = 'test' AND name = 'mdev38632';
56+
57+
--echo # Restart the server to trigger full reload from mysql.event.
58+
--source include/restart_mysqld.inc
59+
SET GLOBAL event_scheduler = ON;
60+
--source include/running_event_scheduler.inc
61+
62+
--echo # The event should fire after server restart.
63+
--let $wait_timeout = 10
64+
let $wait_condition = SELECT COUNT(*) = 3 FROM test.event_log;
65+
--source include/wait_condition.inc
66+
67+
--echo # Step 4: User-disabled event stays DISABLED after reschedule without ENABLE
68+
ALTER EVENT test.mdev38632 DISABLE;
69+
70+
ALTER EVENT test.mdev38632
71+
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 2 HOUR;
72+
73+
--echo # Status remains DISABLED — explicit ENABLE is required to reschedule.
74+
SELECT status FROM mysql.event WHERE db = 'test' AND name = 'mdev38632';
75+
76+
--echo # Step 5: ALTER that doesn't change schedule should not clear last_executed
77+
DROP EVENT test.mdev38632;
78+
CREATE EVENT test.mdev38632
79+
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 SECOND
80+
ON COMPLETION PRESERVE ENABLE
81+
DO INSERT INTO test.event_log(id) VALUES (NULL);
82+
83+
let $wait_condition = SELECT COUNT(*) = 4 FROM test.event_log;
84+
--source include/wait_condition.inc
85+
86+
--echo # Event executed and is DISABLED. ALTER body only (no schedule change).
87+
ALTER EVENT test.mdev38632
88+
DO INSERT INTO test.event_log(id) VALUES (NULL);
89+
90+
--echo # Status should remain DISABLED (no schedule change)
91+
SELECT status FROM mysql.event WHERE db = 'test' AND name = 'mdev38632';
92+
93+
--echo # Cleanup
94+
DROP EVENT test.mdev38632;
95+
DROP TABLE test.event_log;
96+
SET GLOBAL event_scheduler = OFF;
97+
--source include/check_events_off.inc

sql/event_data_objects.cc

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -948,8 +948,12 @@ Event_queue_element::compute_next_execution_time()
948948
/* If one-time, no need to do computation */
949949
if (!expression)
950950
{
951-
/* Let's check whether it was executed */
952-
if (last_executed)
951+
/*
952+
Check whether the event was already executed for the current schedule.
953+
If execute_at was changed (via ALTER EVENT) to a time after
954+
last_executed, the event should still be considered pending (MDEV-38632).
955+
*/
956+
if (last_executed && last_executed >= execute_at)
953957
{
954958
DBUG_PRINT("info",("One-time event %s.%s of was already executed",
955959
dbname.str, name.str));

sql/event_db_repository.cc

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,30 @@ mysql_event_fill_row(THD *thd,
319319
MYSQL_TIME time;
320320
my_tz_OFFSET0->gmt_sec_to_TIME(&time, et->execute_at);
321321

322+
/*
323+
MDEV-38632: When ALTER EVENT changes execute_at, clear last_executed.
324+
A new execute_at means the event hasn't been executed for this
325+
schedule yet. Without this, compute_next_execution_time() would
326+
see the stale last_executed and disable the event on reload.
327+
328+
Only clear when execute_at actually changed. Compare the new value
329+
against the stored one before overwriting.
330+
*/
331+
if (is_update)
332+
{
333+
bool schedule_changed= true;
334+
MYSQL_TIME old_execute_at;
335+
336+
if (!fields[ET_FIELD_EXECUTE_AT]->is_null() &&
337+
!fields[ET_FIELD_EXECUTE_AT]->get_date(&old_execute_at,
338+
TIME_NO_ZERO_DATE |
339+
thd->temporal_round_mode()))
340+
schedule_changed= my_time_compare(&time, &old_execute_at) != 0;
341+
342+
if (schedule_changed)
343+
fields[ET_FIELD_LAST_EXECUTED]->set_null();
344+
}
345+
322346
fields[ET_FIELD_EXECUTE_AT]->set_notnull();
323347
fields[ET_FIELD_EXECUTE_AT]->store_time(&time);
324348
}

0 commit comments

Comments
 (0)