diff --git a/kvrocks.conf b/kvrocks.conf index 113344de236..a5f20f09908 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 */int) # 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 */int) # 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 */int) # 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..9bac0ca550f 100644 --- a/src/common/cron.cc +++ b/src/common/cron.cc @@ -24,11 +24,16 @@ #include #include "parse_util.h" +#include "string_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) { @@ -57,10 +62,21 @@ bool Cron::IsTimeMatch(const tm *tm) { tm->tm_mon == last_tm_.tm_mon && tm->tm_wday == last_tm_.tm_wday) { return false; } + + auto match = [](int current, int val, bool interval, int interval_offset) { + if (val == -1) return true; + if (interval) return (current - interval_offset) % val == 0; + return val == current; + }; + 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 minute_match = match(tm->tm_min, st.minute, st.minute_interval, 0); + bool hour_match = match(tm->tm_hour, st.hour, st.hour_interval, 0); + bool mday_match = match(tm->tm_mday, st.mday, st.mday_interval, 1); + bool month_match = match(tm->tm_mon + 1, st.month, st.month_interval, 1); + bool wday_match = match(tm->tm_wday, st.wday, st.wday_interval, 0); + + if (minute_match && hour_match && mday_match && month_match && wday_match) { last_tm_ = *tm; return true; } @@ -84,20 +100,30 @@ 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) + if (util::HasPrefix(param, "*/")) { + auto s = ParseInt(param.substr(2), {lower_bound, upper_bound}, 10); + if (!s || *s == 0) { + return std::move(s).Prefixed(fmt::format("malformed cron token `{}`", param)); + } + is_interval = true; + return *s; + } + 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..9ce38a5a998 100644 --- a/tests/cppunit/cron_test.cc +++ b/tests/cppunit/cron_test.cc @@ -24,20 +24,51 @@ #include -class CronTest : public testing::Test { +// At minute 10 +class CronTestMin : public testing::Test { protected: - explicit CronTest() { + explicit CronTestMin() { + cron_ = std::make_unique(); + std::vector schedule{"10", "*", "*", "*", "*"}; + auto s = cron_->SetScheduleTime(schedule); + EXPECT_TRUE(s.IsOK()); + } + ~CronTestMin() override = default; + + std::unique_ptr cron_; +}; + +TEST_F(CronTestMin, IsTimeMatch) { + std::time_t t = std::time(nullptr); + std::tm *now = std::localtime(&t); + now->tm_min = 10; + now->tm_hour = 3; + ASSERT_TRUE(cron_->IsTimeMatch(now)); + now->tm_min = 15; + now->tm_hour = 4; + ASSERT_FALSE(cron_->IsTimeMatch(now)); +} + +TEST_F(CronTestMin, ToString) { + std::string got = cron_->ToString(); + ASSERT_EQ("10 * * * *", got); +} + +// At every minute past hour 3 +class CronTestHour : public testing::Test { + protected: + explicit CronTestHour() { cron_ = std::make_unique(); std::vector schedule{"*", "3", "*", "*", "*"}; auto s = cron_->SetScheduleTime(schedule); EXPECT_TRUE(s.IsOK()); } - ~CronTest() override = default; + ~CronTestHour() override = default; std::unique_ptr cron_; }; -TEST_F(CronTest, IsTimeMatch) { +TEST_F(CronTestHour, IsTimeMatch) { std::time_t t = std::time(nullptr); std::tm *now = std::localtime(&t); now->tm_hour = 3; @@ -46,7 +77,298 @@ TEST_F(CronTest, IsTimeMatch) { ASSERT_FALSE(cron_->IsTimeMatch(now)); } -TEST_F(CronTest, ToString) { +TEST_F(CronTestHour, ToString) { std::string got = cron_->ToString(); ASSERT_EQ("* 3 * * *", got); } + +// At 03:00 on day-of-month 5 +class CronTestMonthDay : public testing::Test { + protected: + explicit CronTestMonthDay() { + cron_ = std::make_unique(); + std::vector schedule{"0", "3", "5", "*", "*"}; + auto s = cron_->SetScheduleTime(schedule); + EXPECT_TRUE(s.IsOK()); + } + ~CronTestMonthDay() override = default; + + std::unique_ptr cron_; +}; + +TEST_F(CronTestMonthDay, IsTimeMatch) { + std::time_t t = std::time(nullptr); + std::tm *now = std::localtime(&t); + now->tm_min = 0; + now->tm_hour = 3; + now->tm_mday = 5; + ASSERT_TRUE(cron_->IsTimeMatch(now)); + now->tm_hour = 0; + now->tm_hour = 3; + now->tm_hour = 6; + ASSERT_FALSE(cron_->IsTimeMatch(now)); +} + +TEST_F(CronTestMonthDay, ToString) { + std::string got = cron_->ToString(); + ASSERT_EQ("0 3 5 * *", got); +} + +// At 03:00 on day-of-month 5 in September +class CronTestMonth : public testing::Test { + protected: + explicit CronTestMonth() { + cron_ = std::make_unique(); + std::vector schedule{"0", "3", "5", "9", "*"}; + auto s = cron_->SetScheduleTime(schedule); + EXPECT_TRUE(s.IsOK()); + } + ~CronTestMonth() override = default; + + std::unique_ptr cron_; +}; + +TEST_F(CronTestMonth, IsTimeMatch) { + std::time_t t = std::time(nullptr); + std::tm *now = std::localtime(&t); + now->tm_min = 0; + now->tm_hour = 3; + now->tm_mday = 5; + now->tm_mon = 8; + ASSERT_TRUE(cron_->IsTimeMatch(now)); + now->tm_min = 0; + now->tm_hour = 3; + now->tm_mday = 5; + now->tm_mon = 5; + ASSERT_FALSE(cron_->IsTimeMatch(now)); +} + +TEST_F(CronTestMonth, ToString) { + std::string got = cron_->ToString(); + ASSERT_EQ("0 3 5 9 *", got); +} + +// At 03:00 on Sunday in September +class CronTestWeekDay : public testing::Test { + protected: + explicit CronTestWeekDay() { + cron_ = std::make_unique(); + std::vector schedule{"0", "3", "*", "9", "0"}; + auto s = cron_->SetScheduleTime(schedule); + EXPECT_TRUE(s.IsOK()); + } + ~CronTestWeekDay() override = default; + + std::unique_ptr cron_; +}; + +TEST_F(CronTestWeekDay, IsTimeMatch) { + std::time_t t = std::time(nullptr); + std::tm *now = std::localtime(&t); + now->tm_min = 0; + now->tm_hour = 3; + now->tm_mon = 8; + now->tm_wday = 0; + ASSERT_TRUE(cron_->IsTimeMatch(now)); + now->tm_min = 0; + now->tm_hour = 3; + now->tm_mon = 8; + now->tm_wday = 0; + ASSERT_FALSE(cron_->IsTimeMatch(now)); +} + +TEST_F(CronTestWeekDay, ToString) { + std::string got = cron_->ToString(); + ASSERT_EQ("0 3 * 9 0", got); +} + +// At every 4th minute +class CronTestMinInterval : public testing::Test { + protected: + explicit CronTestMinInterval() { + cron_ = std::make_unique(); + std::vector schedule{"*/4", "*", "*", "*", "*"}; + auto s = cron_->SetScheduleTime(schedule); + EXPECT_TRUE(s.IsOK()); + } + ~CronTestMinInterval() override = default; + + std::unique_ptr cron_; +}; + +TEST_F(CronTestMinInterval, 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_min = 4; + ASSERT_TRUE(cron_->IsTimeMatch(now)); + now->tm_min = 8; + ASSERT_TRUE(cron_->IsTimeMatch(now)); + now->tm_min = 12; + ASSERT_TRUE(cron_->IsTimeMatch(now)); + now->tm_min = 3; + ASSERT_FALSE(cron_->IsTimeMatch(now)); + now->tm_min = 99; + ASSERT_FALSE(cron_->IsTimeMatch(now)); +} + +TEST_F(CronTestMinInterval, ToString) { + std::string got = cron_->ToString(); + ASSERT_EQ("*/4 * * * *", got); +} + +// At minute 0 past every 4th hour +class CronTestHourInterval : public testing::Test { + protected: + explicit CronTestHourInterval() { + cron_ = std::make_unique(); + std::vector schedule{"0", "*/4", "*", "*", "*"}; + auto s = cron_->SetScheduleTime(schedule); + EXPECT_TRUE(s.IsOK()); + } + ~CronTestHourInterval() override = default; + + std::unique_ptr cron_; +}; + +TEST_F(CronTestHourInterval, 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)); + now->tm_hour = 55; + ASSERT_FALSE(cron_->IsTimeMatch(now)); +} + +TEST_F(CronTestHourInterval, ToString) { + std::string got = cron_->ToString(); + ASSERT_EQ("0 */4 * * *", got); +} + +// At minute 0 on every 4th day-of-month +// https://crontab.guru/#0_0_*/4_*_* (click on next) +class CronTestMonthDayInterval : public testing::Test { + protected: + explicit CronTestMonthDayInterval() { + cron_ = std::make_unique(); + std::vector schedule{"0", "*", "*/4", "*", "*"}; + auto s = cron_->SetScheduleTime(schedule); + EXPECT_TRUE(s.IsOK()); + } + ~CronTestMonthDayInterval() override = default; + + std::unique_ptr cron_; +}; + +TEST_F(CronTestMonthDayInterval, IsTimeMatch) { + std::time_t t = std::time(nullptr); + std::tm *now = std::localtime(&t); + now->tm_min = 0; + now->tm_hour = 3; + now->tm_mday = 17; + now->tm_mon = 6; + ASSERT_TRUE(cron_->IsTimeMatch(now)); + now->tm_hour = 5; + now->tm_mday = 21; + now->tm_mon = 6; + ASSERT_TRUE(cron_->IsTimeMatch(now)); + now->tm_hour = 6; + now->tm_mday = 25; + now->tm_mon = 6; + ASSERT_TRUE(cron_->IsTimeMatch(now)); + now->tm_hour = 1; + now->tm_mday = 2; + now->tm_mon = 7; + ASSERT_FALSE(cron_->IsTimeMatch(now)); + now->tm_hour = 1; + now->tm_mday = 99; + ASSERT_FALSE(cron_->IsTimeMatch(now)); +} + +TEST_F(CronTestMonthDayInterval, ToString) { + std::string got = cron_->ToString(); + ASSERT_EQ("0 * */4 * *", got); +} + +// At minute 0 in every 4th month +class CronTestMonthInterval : public testing::Test { + protected: + explicit CronTestMonthInterval() { + cron_ = std::make_unique(); + std::vector schedule{"0", "*", "*", "*/4", "*"}; + auto s = cron_->SetScheduleTime(schedule); + EXPECT_TRUE(s.IsOK()); + } + ~CronTestMonthInterval() override = default; + + std::unique_ptr cron_; +}; + +TEST_F(CronTestMonthInterval, IsTimeMatch) { + std::time_t t = std::time(nullptr); + std::tm *now = std::localtime(&t); + now->tm_hour = 0; + now->tm_min = 0; + now->tm_mon = 4; + ASSERT_TRUE(cron_->IsTimeMatch(now)); + now->tm_hour = 5; + now->tm_mon = 8; + ASSERT_TRUE(cron_->IsTimeMatch(now)); + now->tm_hour = 1; + now->tm_mon = 3; + ASSERT_FALSE(cron_->IsTimeMatch(now)); + now->tm_hour = 1; + now->tm_mon = 99; + ASSERT_FALSE(cron_->IsTimeMatch(now)); +} + +TEST_F(CronTestMonthInterval, ToString) { + std::string got = cron_->ToString(); + ASSERT_EQ("0 * * */4 *", got); +} + +// At minute 0 on every 4th day-of-week +class CronTestWeekDayInterval : public testing::Test { + protected: + explicit CronTestWeekDayInterval() { + cron_ = std::make_unique(); + std::vector schedule{"0", "*", "*", "*", "*/4"}; + auto s = cron_->SetScheduleTime(schedule); + EXPECT_TRUE(s.IsOK()); + } + ~CronTestWeekDayInterval() override = default; + + std::unique_ptr cron_; +}; + +TEST_F(CronTestWeekDayInterval, IsTimeMatch) { + std::time_t t = std::time(nullptr); + std::tm *now = std::localtime(&t); + now->tm_hour = 0; + now->tm_min = 0; + now->tm_hour = 3; + now->tm_wday = 4; + ASSERT_TRUE(cron_->IsTimeMatch(now)); + now->tm_hour = 5; + now->tm_wday = 3; + ASSERT_FALSE(cron_->IsTimeMatch(now)); + now->tm_hour = 1; + now->tm_wday = 99; + ASSERT_FALSE(cron_->IsTimeMatch(now)); +} + +TEST_F(CronTestWeekDayInterval, ToString) { + std::string got = cron_->ToString(); + ASSERT_EQ("0 * * * */4", got); +}