This is an automated email from the ASF dual-hosted git repository.
hulk pushed a commit to branch unstable
in repository https://gitbox.apache.org/repos/asf/kvrocks.git
The following commit(s) were added to refs/heads/unstable by this push:
new 93c8dc23 feat(cron): add support for "*/n" interval cronjob syntax
(#2360)
93c8dc23 is described below
commit 93c8dc23c81c985be16307a5e14b450dc49e8c8d
Author: Yann Defretin <[email protected]>
AuthorDate: Fri Jun 14 07:05:16 2024 +0200
feat(cron): add support for "*/n" interval cronjob syntax (#2360)
Co-authored-by: Twice <[email protected]>
---
kvrocks.conf | 6 +-
src/common/cron.cc | 50 +++++--
src/common/cron.h | 9 +-
tests/cppunit/cron_test.cc | 332 ++++++++++++++++++++++++++++++++++++++++++++-
4 files changed, 376 insertions(+), 21 deletions(-)
diff --git a/kvrocks.conf b/kvrocks.conf
index 113344de..a5f20f09 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 2c4a03ba..9bac0ca5 100644
--- a/src/common/cron.cc
+++ b/src/common/cron.cc
@@ -24,11 +24,16 @@
#include <utility>
#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<std::string> &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<Scheduler> 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<int> Cron::convertParam(const std::string ¶m, int lower_bound,
int upper_bound) {
+StatusOr<int> 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<int>(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<int>(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 5385a0ef..325745e9 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<Scheduler> convertToScheduleTime(const std::string &minute,
const std::string &hour,
const std::string &mday,
const std::string &month,
const std::string &wday);
- static StatusOr<int> convertParam(const std::string ¶m, int lower_bound,
int upper_bound);
+ static StatusOr<int> 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 9322050c..9ce38a5a 100644
--- a/tests/cppunit/cron_test.cc
+++ b/tests/cppunit/cron_test.cc
@@ -24,20 +24,51 @@
#include <memory>
-class CronTest : public testing::Test {
+// At minute 10
+class CronTestMin : public testing::Test {
protected:
- explicit CronTest() {
+ explicit CronTestMin() {
+ cron_ = std::make_unique<Cron>();
+ std::vector<std::string> schedule{"10", "*", "*", "*", "*"};
+ auto s = cron_->SetScheduleTime(schedule);
+ EXPECT_TRUE(s.IsOK());
+ }
+ ~CronTestMin() override = default;
+
+ std::unique_ptr<Cron> 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<Cron>();
std::vector<std::string> schedule{"*", "3", "*", "*", "*"};
auto s = cron_->SetScheduleTime(schedule);
EXPECT_TRUE(s.IsOK());
}
- ~CronTest() override = default;
+ ~CronTestHour() override = default;
std::unique_ptr<Cron> 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<Cron>();
+ std::vector<std::string> schedule{"0", "3", "5", "*", "*"};
+ auto s = cron_->SetScheduleTime(schedule);
+ EXPECT_TRUE(s.IsOK());
+ }
+ ~CronTestMonthDay() override = default;
+
+ std::unique_ptr<Cron> 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<Cron>();
+ std::vector<std::string> schedule{"0", "3", "5", "9", "*"};
+ auto s = cron_->SetScheduleTime(schedule);
+ EXPECT_TRUE(s.IsOK());
+ }
+ ~CronTestMonth() override = default;
+
+ std::unique_ptr<Cron> 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<Cron>();
+ std::vector<std::string> schedule{"0", "3", "*", "9", "0"};
+ auto s = cron_->SetScheduleTime(schedule);
+ EXPECT_TRUE(s.IsOK());
+ }
+ ~CronTestWeekDay() override = default;
+
+ std::unique_ptr<Cron> 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<Cron>();
+ std::vector<std::string> schedule{"*/4", "*", "*", "*", "*"};
+ auto s = cron_->SetScheduleTime(schedule);
+ EXPECT_TRUE(s.IsOK());
+ }
+ ~CronTestMinInterval() override = default;
+
+ std::unique_ptr<Cron> 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<Cron>();
+ std::vector<std::string> schedule{"0", "*/4", "*", "*", "*"};
+ auto s = cron_->SetScheduleTime(schedule);
+ EXPECT_TRUE(s.IsOK());
+ }
+ ~CronTestHourInterval() override = default;
+
+ std::unique_ptr<Cron> 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<Cron>();
+ std::vector<std::string> schedule{"0", "*", "*/4", "*", "*"};
+ auto s = cron_->SetScheduleTime(schedule);
+ EXPECT_TRUE(s.IsOK());
+ }
+ ~CronTestMonthDayInterval() override = default;
+
+ std::unique_ptr<Cron> 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<Cron>();
+ std::vector<std::string> schedule{"0", "*", "*", "*/4", "*"};
+ auto s = cron_->SetScheduleTime(schedule);
+ EXPECT_TRUE(s.IsOK());
+ }
+ ~CronTestMonthInterval() override = default;
+
+ std::unique_ptr<Cron> 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<Cron>();
+ std::vector<std::string> schedule{"0", "*", "*", "*", "*/4"};
+ auto s = cron_->SetScheduleTime(schedule);
+ EXPECT_TRUE(s.IsOK());
+ }
+ ~CronTestWeekDayInterval() override = default;
+
+ std::unique_ptr<Cron> 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);
+}