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 &param, int lower_bound, 
int upper_bound) {
+StatusOr<int> Cron::convertParam(const std::string &param, 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 &param, int lower_bound, 
int upper_bound);
+  static StatusOr<int> convertParam(const std::string &param, 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);
+}

Reply via email to