Skip to content

Commit e91440e

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: 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.
1 parent f43b705 commit e91440e

4 files changed

Lines changed: 256 additions & 2 deletions

File tree

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
SET GLOBAL event_scheduler = ON;
2+
CREATE TABLE test.event_log (id INT AUTO_INCREMENT PRIMARY KEY, ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP);
3+
#
4+
# Step 1: Create a one-time AT event with ON COMPLETION PRESERVE
5+
#
6+
CREATE EVENT test.mdev38632
7+
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 SECOND
8+
ON COMPLETION PRESERVE ENABLE
9+
DO INSERT INTO test.event_log(id) VALUES (NULL);
10+
# Wait for the event to execute
11+
SELECT COUNT(*) AS exec_count FROM test.event_log;
12+
exec_count
13+
1
14+
# Verify: event executed, status is now DISABLED (preserved but done)
15+
SELECT status, on_completion, last_executed IS NOT NULL AS has_last_exec
16+
FROM mysql.event WHERE db = 'test' AND name = 'mdev38632';
17+
status on_completion has_last_exec
18+
DISABLED PRESERVE 1
19+
#
20+
# Step 2: ALTER EVENT to reschedule — without explicit ENABLE
21+
# This is the customer's scenario from the JIRA report.
22+
#
23+
ALTER EVENT test.mdev38632
24+
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 SECOND;
25+
# After ALTER with a new schedule, status should be re-enabled automatically.
26+
SELECT status FROM mysql.event WHERE db = 'test' AND name = 'mdev38632';
27+
status
28+
ENABLED
29+
# The event should fire again after being rescheduled.
30+
SELECT COUNT(*) AS exec_count FROM test.event_log;
31+
exec_count
32+
2
33+
#
34+
# Step 3: Try again with explicit ENABLE — verify scheduler reload works.
35+
#
36+
ALTER EVENT test.mdev38632
37+
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 SECOND
38+
ON COMPLETION PRESERVE ENABLE;
39+
# After fix, last_executed should be cleared when schedule changes.
40+
SELECT status, last_executed IS NOT NULL AS has_last_exec
41+
FROM mysql.event WHERE db = 'test' AND name = 'mdev38632';
42+
status has_last_exec
43+
ENABLED 0
44+
# Restart the scheduler to trigger reload from mysql.event.
45+
# After fix, the event should remain ENABLED after reload.
46+
SET GLOBAL event_scheduler = OFF;
47+
SET GLOBAL event_scheduler = ON;
48+
# Status should remain ENABLED after scheduler reload.
49+
SELECT status FROM mysql.event WHERE db = 'test' AND name = 'mdev38632';
50+
status
51+
ENABLED
52+
#
53+
# Step 4: User-disabled event should NOT be re-enabled by reschedule
54+
#
55+
ALTER EVENT test.mdev38632
56+
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 HOUR
57+
ON COMPLETION PRESERVE ENABLE;
58+
ALTER EVENT test.mdev38632 DISABLE;
59+
SELECT status FROM mysql.event WHERE db = 'test' AND name = 'mdev38632';
60+
status
61+
DISABLED
62+
# Reschedule without explicit ENABLE — should stay DISABLED
63+
ALTER EVENT test.mdev38632
64+
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 2 HOUR;
65+
SELECT status FROM mysql.event WHERE db = 'test' AND name = 'mdev38632';
66+
status
67+
DISABLED
68+
#
69+
# Step 5: ALTER with same execute_at should not clear last_executed
70+
#
71+
DROP EVENT test.mdev38632;
72+
CREATE EVENT test.mdev38632
73+
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 SECOND
74+
ON COMPLETION PRESERVE ENABLE
75+
DO INSERT INTO test.event_log(id) VALUES (NULL);
76+
# Event executed, now ALTER with same body but no schedule change
77+
ALTER EVENT test.mdev38632
78+
DO INSERT INTO test.event_log(id) VALUES (NULL);
79+
# Status should remain DISABLED (no schedule change, no re-enable)
80+
SELECT status FROM mysql.event WHERE db = 'test' AND name = 'mdev38632';
81+
status
82+
DISABLED
83+
#
84+
# Cleanup
85+
#
86+
DROP EVENT IF EXISTS test.mdev38632;
87+
DROP TABLE test.event_log;
88+
SET GLOBAL event_scheduler = OFF;
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
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:
6+
# 1. compute_next_execution_time() sets status=DISABLED in mysql.event
7+
# 2. ALTER EVENT without explicit ENABLE does not reset the status
8+
# 3. The event queue refuses to queue a DISABLED event
9+
# 4. Even with explicit ENABLE, last_executed causes re-disable on reload
10+
#
11+
# Expected fix: ALTER EVENT with a new schedule should re-enable the event
12+
# and clear last_executed so it is properly queued.
13+
#
14+
15+
--source include/not_embedded.inc
16+
17+
SET GLOBAL event_scheduler = ON;
18+
--source include/running_event_scheduler.inc
19+
20+
CREATE TABLE test.event_log (id INT AUTO_INCREMENT PRIMARY KEY, ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP);
21+
22+
--echo #
23+
--echo # Step 1: Create a one-time AT event with ON COMPLETION PRESERVE
24+
--echo #
25+
CREATE EVENT test.mdev38632
26+
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 SECOND
27+
ON COMPLETION PRESERVE ENABLE
28+
DO INSERT INTO test.event_log(id) VALUES (NULL);
29+
30+
--echo # Wait for the event to execute
31+
let $wait_condition = SELECT COUNT(*) >= 1 FROM test.event_log;
32+
--source include/wait_condition.inc
33+
34+
SELECT COUNT(*) AS exec_count FROM test.event_log;
35+
36+
--echo # Verify: event executed, status is now DISABLED (preserved but done)
37+
SELECT status, on_completion, last_executed IS NOT NULL AS has_last_exec
38+
FROM mysql.event WHERE db = 'test' AND name = 'mdev38632';
39+
40+
--echo #
41+
--echo # Step 2: ALTER EVENT to reschedule — without explicit ENABLE
42+
--echo # This is the customer's scenario from the JIRA report.
43+
--echo #
44+
ALTER EVENT test.mdev38632
45+
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 SECOND;
46+
47+
--echo # After ALTER with a new schedule, status should be re-enabled automatically.
48+
SELECT status FROM mysql.event WHERE db = 'test' AND name = 'mdev38632';
49+
50+
--echo # The event should fire again after being rescheduled.
51+
let $wait_condition = SELECT COUNT(*) >= 2 FROM test.event_log;
52+
--source include/wait_condition.inc
53+
54+
SELECT COUNT(*) AS exec_count FROM test.event_log;
55+
56+
--echo #
57+
--echo # Step 3: Try again with explicit ENABLE — verify scheduler reload works.
58+
--echo #
59+
ALTER EVENT test.mdev38632
60+
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 SECOND
61+
ON COMPLETION PRESERVE ENABLE;
62+
63+
--echo # After fix, last_executed should be cleared when schedule changes.
64+
SELECT status, last_executed IS NOT NULL AS has_last_exec
65+
FROM mysql.event WHERE db = 'test' AND name = 'mdev38632';
66+
67+
--echo # Restart the scheduler to trigger reload from mysql.event.
68+
--echo # After fix, the event should remain ENABLED after reload.
69+
SET GLOBAL event_scheduler = OFF;
70+
--source include/check_events_off.inc
71+
SET GLOBAL event_scheduler = ON;
72+
--source include/running_event_scheduler.inc
73+
74+
--echo # Status should remain ENABLED after scheduler reload.
75+
SELECT status FROM mysql.event WHERE db = 'test' AND name = 'mdev38632';
76+
77+
--echo #
78+
--echo # Step 4: User-disabled event should NOT be re-enabled by reschedule
79+
--echo #
80+
ALTER EVENT test.mdev38632
81+
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 HOUR
82+
ON COMPLETION PRESERVE ENABLE;
83+
84+
ALTER EVENT test.mdev38632 DISABLE;
85+
86+
SELECT status FROM mysql.event WHERE db = 'test' AND name = 'mdev38632';
87+
88+
--echo # Reschedule without explicit ENABLE — should stay DISABLED
89+
ALTER EVENT test.mdev38632
90+
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 2 HOUR;
91+
92+
SELECT status FROM mysql.event WHERE db = 'test' AND name = 'mdev38632';
93+
94+
--echo #
95+
--echo # Step 5: ALTER with same execute_at should not clear last_executed
96+
--echo #
97+
DROP EVENT test.mdev38632;
98+
CREATE EVENT test.mdev38632
99+
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 SECOND
100+
ON COMPLETION PRESERVE ENABLE
101+
DO INSERT INTO test.event_log(id) VALUES (NULL);
102+
103+
let $wait_condition = SELECT COUNT(*) >= 3 FROM test.event_log;
104+
--source include/wait_condition.inc
105+
106+
--echo # Event executed, now ALTER with same body but no schedule change
107+
ALTER EVENT test.mdev38632
108+
DO INSERT INTO test.event_log(id) VALUES (NULL);
109+
110+
--echo # Status should remain DISABLED (no schedule change, no re-enable)
111+
SELECT status FROM mysql.event WHERE db = 'test' AND name = 'mdev38632';
112+
113+
--echo #
114+
--echo # Cleanup
115+
--echo #
116+
DROP EVENT IF EXISTS test.mdev38632;
117+
DROP TABLE test.event_log;
118+
SET GLOBAL event_scheduler = OFF;
119+
--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: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,49 @@ 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+
and re-enable the event if it was auto-disabled by the scheduler.
325+
326+
Only act when execute_at actually changed. Compare the new value
327+
against the stored one before overwriting.
328+
329+
Re-enable only if: the user didn't explicitly set status, the stored
330+
status is DISABLED, and last_executed >= old execute_at (meaning the
331+
scheduler disabled it after running at that time).
332+
333+
Note: this heuristic cannot distinguish "scheduler auto-disabled"
334+
from "user explicitly disabled an already-auto-disabled event"
335+
since both states look identical in mysql.event. In the rare case
336+
where a user explicitly disables an already-disabled event and then
337+
reschedules it, the event would be incorrectly re-enabled.
338+
*/
339+
if (is_update)
340+
{
341+
MYSQL_TIME old_execute_at;
342+
fields[ET_FIELD_EXECUTE_AT]->get_date(&old_execute_at,
343+
TIME_NO_ZERO_DATE |
344+
thd->temporal_round_mode());
345+
bool schedule_changed= my_time_compare(&time, &old_execute_at) != 0;
346+
347+
if (schedule_changed)
348+
{
349+
if (!et->status_changed &&
350+
fields[ET_FIELD_STATUS]->val_int() == Event_parse_data::DISABLED &&
351+
!fields[ET_FIELD_LAST_EXECUTED]->is_null())
352+
{
353+
MYSQL_TIME old_last_executed;
354+
fields[ET_FIELD_LAST_EXECUTED]->get_date(&old_last_executed,
355+
TIME_NO_ZERO_DATE |
356+
thd->temporal_round_mode());
357+
if (my_time_compare(&old_last_executed, &old_execute_at) >= 0)
358+
rs|= fields[ET_FIELD_STATUS]->store(
359+
(longlong)Event_parse_data::ENABLED, TRUE);
360+
}
361+
fields[ET_FIELD_LAST_EXECUTED]->set_null();
362+
}
363+
}
364+
322365
fields[ET_FIELD_EXECUTE_AT]->set_notnull();
323366
fields[ET_FIELD_EXECUTE_AT]->store_time(&time);
324367
}

0 commit comments

Comments
 (0)