Skip to content

Commit 01805bf

Browse files
authored
fix(cron): resolve task execution conflicts and timing precision issues
Fixed critical bugs in cron system causing scheduled tasks to fail after server restarts.
1 parent e214dd5 commit 01805bf

File tree

1 file changed

+43
-20
lines changed

1 file changed

+43
-20
lines changed

[core]/cron/server/main.lua

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,98 @@
11
---@class CronJob
2-
---@field h number
3-
---@field m number
4-
---@field cb function|table
2+
---@field h number Hour (0-23)
3+
---@field m number Minute (0-59)
4+
---@field cb function|table Callback function to execute
5+
---@field lastRun number|nil Timestamp of last execution (prevents duplicates)
56

67
---@type CronJob[]
78
local cronJobs = {}
9+
810
---@type number|false
911
local lastTimestamp = false
1012

11-
---@param h number
12-
---@param m number
13-
---@param cb function|table
13+
---Registers a new cron job to run at specified time daily
14+
---@param h number Hour (0-23)
15+
---@param m number Minute (0-59)
16+
---@param cb function|table Callback function to execute
1417
function RunAt(h, m, cb)
1518
cronJobs[#cronJobs + 1] = {
1619
h = h,
1720
m = m,
1821
cb = cb,
22+
lastRun = nil
1923
}
2024
end
2125

22-
---@return number
26+
---Gets current Unix timestamp
27+
---@return number Current timestamp
2328
function GetUnixTimestamp()
2429
return os.time()
2530
end
2631

27-
---@param timestamp number
32+
---Checks and executes due cron jobs for the current timestamp
33+
---@param timestamp number Current Unix timestamp
2834
function OnTime(timestamp)
2935
for i = 1, #cronJobs, 1 do
36+
-- Calculate today's scheduled timestamp for this job
3037
local scheduledTimestamp = os.time({
3138
hour = cronJobs[i].h,
3239
min = cronJobs[i].m,
33-
sec = 0, -- Assuming tasks run at the start of the minute
40+
sec = 0
3441
day = os.date("%d", timestamp),
3542
month = os.date("%m", timestamp),
3643
year = os.date("%Y", timestamp),
3744
})
3845

39-
if timestamp >= scheduledTimestamp and (not lastTimestamp or lastTimestamp < scheduledTimestamp) then
40-
local d = os.date('*t', scheduledTimestamp).wday
41-
cronJobs[i].cb(d, cronJobs[i].h, cronJobs[i].m)
46+
-- Execute if current time >= scheduled time and hasn't run today
47+
if timestamp >= scheduledTimestamp and (not cronJobs[i].lastRun or cronJobs[i].lastRun < scheduledTimestamp) then
48+
local dayOfWeek = os.date('*t', scheduledTimestamp).wday
49+
50+
-- Execute the callback with day, hour, minute parameters
51+
cronJobs[i].cb(dayOfWeek, cronJobs[i].h, cronJobs[i].m)
52+
53+
-- Mark this job as executed for today
54+
cronJobs[i].lastRun = scheduledTimestamp
4255
end
4356
end
4457
end
4558

46-
---@return nil
59+
---Main tick function that checks for minute changes and processes jobs
60+
---Automatically reschedules itself for precise minute-boundary timing
4761
function Tick()
4862
local timestamp = GetUnixTimestamp()
4963

64+
-- Only process jobs when minute changes to avoid duplicate checks
5065
if not lastTimestamp or os.date("%M", timestamp) ~= os.date("%M", lastTimestamp) then
5166
OnTime(timestamp)
5267
lastTimestamp = timestamp
5368
end
5469

55-
SetTimeout(60000, Tick)
70+
-- Schedule next check at the start of the next minute for precision
71+
local currentSeconds = tonumber(os.date("%S", timestamp))
72+
local msToNextMinute = (60 - currentSeconds) * 1000
73+
SetTimeout(msToNextMinute, Tick)
5674
end
5775

5876
lastTimestamp = GetUnixTimestamp()
5977
Tick()
6078

61-
---@param h number
62-
---@param m number
63-
---@param cb function|table
79+
---Event handler for external resources to register cron jobs
80+
---Usage: TriggerEvent('cron:runAt', hour, minute, callback)
6481
AddEventHandler("cron:runAt", function(h, m, cb)
6582
local invokingResource = GetInvokingResource() or "Unknown"
83+
84+
-- Validate parameters with detailed error messages
6685
local typeH = type(h)
6786
local typeM = type(m)
6887
local typeCb = type(cb)
6988

70-
assert(typeH == "number", ("Expected number for h, got %s. Invoking Resource: '%s'"):format(typeH, invokingResource))
71-
assert(typeM == "number", ("Expected number for m, got %s. Invoking Resource: '%s'"):format(typeM, invokingResource))
72-
assert(typeCb == "function" or (typeCb == "table" and type(getmetatable(cb)?.__call) == "function"), ("Expected function for cb, got %s. Invoking Resource: '%s'"):format(typeCb, invokingResource))
89+
assert(typeH == "number", ("Expected number for hour, got %s. Invoking Resource: '%s'"):format(typeH, invokingResource))
90+
assert(typeM == "number", ("Expected number for minute, got %s. Invoking Resource: '%s'"):format(typeM, invokingResource))
91+
assert(typeCb == "function" or (typeCb == "table" and type(getmetatable(cb)?.__call) == "function"), ("Expected function for callback, got %s. Invoking Resource: '%s'"):format(typeCb, invokingResource))
92+
93+
-- Validate time ranges
94+
assert(h >= 0 and h <= 23, ("Hour must be between 0-23, got %d. Invoking Resource: '%s'"):format(h, invokingResource))
95+
assert(m >= 0 and m <= 59, ("Minute must be between 0-59, got %d. Invoking Resource: '%s'"):format(m, invokingResource))
7396

7497
RunAt(h, m, cb)
7598
end)

0 commit comments

Comments
 (0)