From da0669f256bf3e3fcf44943a306ba03a5705286e Mon Sep 17 00:00:00 2001 From: Yann Defretin Date: Sun, 9 Jun 2024 15:53:34 +0200 Subject: [PATCH] Add support for "*/n" interval cronjob syntax --- kvrocks.conf | 6 ++--- src/common/cron.cc | 46 ++++++++++++++++++++++++++++---------- src/common/cron.h | 9 +++++++- tests/cppunit/cron_test.cc | 34 ++++++++++++++++++++++++++++ 4 files changed, 79 insertions(+), 16 deletions(-) diff --git a/kvrocks.conf b/kvrocks.conf index 113344de236..86bbcdde37b 100644 --- a/kvrocks.conf +++ b/kvrocks.conf @@ -492,7 +492,7 @@ profiling-sample-record-threshold-ms 100 ################################## CRON ################################### # Compact Scheduler, auto compact at schedule time -# time expression format is the same as crontab(currently only support * and int) +# Time expression format is the same as crontab (currently only support *, int and */n) # e.g. compact-cron 0 3 * * * 0 4 * * * # would compact the db at 3am and 4am everyday # compact-cron 0 3 * * * @@ -515,14 +515,14 @@ compaction-checker-range 0-7 # force-compact-file-min-deleted-percentage 10 # Bgsave scheduler, auto bgsave at scheduled time -# time expression format is the same as crontab(currently only support * and int) +# Time expression format is the same as crontab (currently only support *, int and */n) # e.g. bgsave-cron 0 3 * * * 0 4 * * * # would bgsave the db at 3am and 4am every day # Kvrocks doesn't store the key number directly. It needs to scan the DB and # then retrieve the key number by using the dbsize scan command. # The Dbsize scan scheduler auto-recalculates the estimated keys at scheduled time. -# Time expression format is the same as crontab (currently only support * and int) +# Time expression format is the same as crontab (currently only support *, int and */n) # e.g. dbsize-scan-cron 0 * * * * # would recalculate the keyspace infos of the db every hour. diff --git a/src/common/cron.cc b/src/common/cron.cc index 2c4a03bae8b..ce21926a875 100644 --- a/src/common/cron.cc +++ b/src/common/cron.cc @@ -20,15 +20,20 @@ #include "cron.h" +#include #include #include #include "parse_util.h" std::string Scheduler::ToString() const { - auto param2string = [](int n) -> std::string { return n == -1 ? "*" : std::to_string(n); }; - return param2string(minute) + " " + param2string(hour) + " " + param2string(mday) + " " + param2string(month) + " " + - param2string(wday); + auto param2string = [](int n, bool is_interval) -> std::string { + if (n == -1) return "*"; + return is_interval ? "*/" + std::to_string(n) : std::to_string(n); + }; + return param2string(minute, minute_interval) + " " + param2string(hour, hour_interval) + " " + + param2string(mday, mday_interval) + " " + param2string(month, month_interval) + " " + + param2string(wday, wday_interval); } Status Cron::SetScheduleTime(const std::vector &args) { @@ -58,9 +63,13 @@ bool Cron::IsTimeMatch(const tm *tm) { return false; } for (const auto &st : schedulers_) { - if ((st.minute == -1 || tm->tm_min == st.minute) && (st.hour == -1 || tm->tm_hour == st.hour) && - (st.mday == -1 || tm->tm_mday == st.mday) && (st.month == -1 || (tm->tm_mon + 1) == st.month) && - (st.wday == -1 || tm->tm_wday == st.wday)) { + bool minuteMatch = (st.minute == -1 || tm->tm_min == st.minute || (st.minute > 0 && tm->tm_min % st.minute == 0)); + bool hourMatch = (st.hour == -1 || tm->tm_hour == st.hour || (st.hour > 0 && tm->tm_hour % st.hour == 0)); + bool mdayMatch = (st.mday == -1 || tm->tm_mday == st.mday); + bool monthMatch = (st.month == -1 || (tm->tm_mon + 1) == st.month); + bool wdayMatch = (st.wday == -1 || tm->tm_wday == st.wday); + + if (minuteMatch && hourMatch && mdayMatch && monthMatch && wdayMatch) { last_tm_ = *tm; return true; } @@ -84,20 +93,33 @@ StatusOr Cron::convertToScheduleTime(const std::string &minute, const const std::string &wday) { Scheduler st; - st.minute = GET_OR_RET(convertParam(minute, 0, 59)); - st.hour = GET_OR_RET(convertParam(hour, 0, 23)); - st.mday = GET_OR_RET(convertParam(mday, 1, 31)); - st.month = GET_OR_RET(convertParam(month, 1, 12)); - st.wday = GET_OR_RET(convertParam(wday, 0, 6)); + st.minute = GET_OR_RET(convertParam(minute, 0, 59, st.minute_interval)); + st.hour = GET_OR_RET(convertParam(hour, 0, 23, st.hour_interval)); + st.mday = GET_OR_RET(convertParam(mday, 1, 31, st.mday_interval)); + st.month = GET_OR_RET(convertParam(month, 1, 12, st.month_interval)); + st.wday = GET_OR_RET(convertParam(wday, 0, 6, st.wday_interval)); return st; } -StatusOr Cron::convertParam(const std::string ¶m, int lower_bound, int upper_bound) { +StatusOr Cron::convertParam(const std::string ¶m, int lower_bound, int upper_bound, bool &is_interval) { if (param == "*") { return -1; } + // Check for interval syntax (*/n) + std::regex interval_regex(R"(\*/(\d+))"); + std::smatch match; + if (std::regex_match(param, match, interval_regex)) { + int interval = std::stoi(match[1].str()); + if (interval >= lower_bound && interval <= upper_bound) { + is_interval = true; + return interval; + } else { + return {Status::NotOK, "interval value out of bounds"}; + } + } + auto s = ParseInt(param, {lower_bound, upper_bound}, 10); if (!s) { return std::move(s).Prefixed(fmt::format("malformed cron token `{}`", param)); diff --git a/src/common/cron.h b/src/common/cron.h index 5385a0efe85..325745e9fcd 100644 --- a/src/common/cron.h +++ b/src/common/cron.h @@ -34,6 +34,13 @@ struct Scheduler { int month; int wday; + // Whether we use */n interval syntax + bool minute_interval = false; + bool hour_interval = false; + bool mday_interval = false; + bool month_interval = false; + bool wday_interval = false; + std::string ToString() const; }; @@ -54,5 +61,5 @@ class Cron { static StatusOr convertToScheduleTime(const std::string &minute, const std::string &hour, const std::string &mday, const std::string &month, const std::string &wday); - static StatusOr convertParam(const std::string ¶m, int lower_bound, int upper_bound); + static StatusOr convertParam(const std::string ¶m, int lower_bound, int upper_bound, bool &is_interval); }; diff --git a/tests/cppunit/cron_test.cc b/tests/cppunit/cron_test.cc index 9322050cec2..b156679e20b 100644 --- a/tests/cppunit/cron_test.cc +++ b/tests/cppunit/cron_test.cc @@ -50,3 +50,37 @@ TEST_F(CronTest, ToString) { std::string got = cron_->ToString(); ASSERT_EQ("* 3 * * *", got); } + +class CronTestInterval : public testing::Test { + protected: + explicit CronTestInterval() { + cron_ = std::make_unique(); + std::vector schedule{"0", "*/4", "*", "*", "*"}; + auto s = cron_->SetScheduleTime(schedule); + EXPECT_TRUE(s.IsOK()); + } + ~CronTestInterval() override = default; + + std::unique_ptr cron_; +}; + +TEST_F(CronTestInterval, IsTimeMatch) { + std::time_t t = std::time(nullptr); + std::tm *now = std::localtime(&t); + now->tm_hour = 0; + now->tm_min = 0; + ASSERT_TRUE(cron_->IsTimeMatch(now)); + now->tm_hour = 4; + ASSERT_TRUE(cron_->IsTimeMatch(now)); + now->tm_hour = 8; + ASSERT_TRUE(cron_->IsTimeMatch(now)); + now->tm_hour = 12; + ASSERT_TRUE(cron_->IsTimeMatch(now)); + now->tm_hour = 3; + ASSERT_FALSE(cron_->IsTimeMatch(now)); +} + +TEST_F(CronTestInterval, ToString) { + std::string got = cron_->ToString(); + ASSERT_EQ("0 */4 * * *", got); +}