This is an automated email from the ASF dual-hosted git repository.

twice 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 8547cfb4 refactor(cron): add CronPattern and implement more cron 
syntax (#2377)
8547cfb4 is described below

commit 8547cfb48cb30dc3ad26214980e2c6a61c0e609a
Author: Twice <[email protected]>
AuthorDate: Sat Jun 29 21:00:07 2024 +0900

    refactor(cron): add CronPattern and implement more cron syntax (#2377)
    
    Co-authored-by: hulk <[email protected]>
---
 .github/workflows/kvrocks.yaml |  31 ++++-----
 src/common/cron.cc             |  94 ++++++++++-----------------
 src/common/cron.h              | 141 +++++++++++++++++++++++++++++++++++------
 3 files changed, 171 insertions(+), 95 deletions(-)

diff --git a/.github/workflows/kvrocks.yaml b/.github/workflows/kvrocks.yaml
index 270d4e70..1d8c24ea 100644
--- a/.github/workflows/kvrocks.yaml
+++ b/.github/workflows/kvrocks.yaml
@@ -110,24 +110,25 @@ jobs:
       fail-fast: false
       matrix:
         include:
-          - name: Darwin Clang
-            os: macos-11
-            compiler: auto
+          # FIXME: update macos-11 to macos-12/13
+          # - name: Darwin Clang
+          #   os: macos-11
+          #   compiler: auto
           - name: Darwin Clang arm64
             os: macos-14
             compiler: auto
-          - name: Darwin Clang without Jemalloc
-            os: macos-11
-            compiler: auto
-            without_jemalloc: -DDISABLE_JEMALLOC=ON
-          - name: Darwin Clang with OpenSSL
-            os: macos-11
-            compiler: auto
-            with_openssl: -DENABLE_OPENSSL=ON
-          - name: Darwin Clang without luaJIT
-            os: macos-11
-            compiler: auto
-            without_luajit: -DENABLE_LUAJIT=OFF
+          # - name: Darwin Clang without Jemalloc
+          #   os: macos-11
+          #   compiler: auto
+          #   without_jemalloc: -DDISABLE_JEMALLOC=ON
+          # - name: Darwin Clang with OpenSSL
+          #   os: macos-11
+          #   compiler: auto
+          #   with_openssl: -DENABLE_OPENSSL=ON
+          # - name: Darwin Clang without luaJIT
+          #   os: macos-11
+          #   compiler: auto
+          #   without_luajit: -DENABLE_LUAJIT=OFF
           - name: Ubuntu GCC
             os: ubuntu-20.04
             compiler: gcc
diff --git a/src/common/cron.cc b/src/common/cron.cc
index 9bac0ca5..26db65f3 100644
--- a/src/common/cron.cc
+++ b/src/common/cron.cc
@@ -23,17 +23,36 @@
 #include <stdexcept>
 #include <utility>
 
+#include "fmt/core.h"
 #include "parse_util.h"
 #include "string_util.h"
 
-std::string Scheduler::ToString() const {
-  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);
+std::string CronScheduler::ToString() const {
+  return fmt::format("{} {} {} {} {}", minute.ToString(), hour.ToString(), 
mday.ToString(), month.ToString(),
+                     wday.ToString());
+}
+
+bool CronScheduler::IsMatch(const tm *tm) const {
+  bool minute_match = minute.IsMatch(tm->tm_min);
+  bool hour_match = hour.IsMatch(tm->tm_hour);
+  bool mday_match = mday.IsMatch(tm->tm_mday, 1);
+  bool month_match = month.IsMatch(tm->tm_mon + 1, 1);
+  bool wday_match = wday.IsMatch(tm->tm_wday);
+
+  return minute_match && hour_match && mday_match && month_match && wday_match;
+}
+
+StatusOr<CronScheduler> CronScheduler::Parse(std::string_view minute, 
std::string_view hour, std::string_view mday,
+                                             std::string_view month, 
std::string_view wday) {
+  CronScheduler st;
+
+  st.minute = GET_OR_RET(CronPattern::Parse(minute, {0, 59}));
+  st.hour = GET_OR_RET(CronPattern::Parse(hour, {0, 23}));
+  st.mday = GET_OR_RET(CronPattern::Parse(mday, {1, 31}));
+  st.month = GET_OR_RET(CronPattern::Parse(month, {1, 12}));
+  st.wday = GET_OR_RET(CronPattern::Parse(wday, {0, 6}));
+
+  return st;
 }
 
 Status Cron::SetScheduleTime(const std::vector<std::string> &args) {
@@ -42,14 +61,14 @@ Status Cron::SetScheduleTime(const std::vector<std::string> 
&args) {
     return Status::OK();
   }
   if (args.size() % 5 != 0) {
-    return {Status::NotOK, "time expression format error,should only contain 
5x fields"};
+    return {Status::NotOK, "cron expression format error, should only contain 
5x fields"};
   }
 
-  std::vector<Scheduler> new_schedulers;
+  std::vector<CronScheduler> new_schedulers;
   for (size_t i = 0; i < args.size(); i += 5) {
-    auto s = convertToScheduleTime(args[i], args[i + 1], args[i + 2], args[i + 
3], args[i + 4]);
+    auto s = CronScheduler::Parse(args[i], args[i + 1], args[i + 2], args[i + 
3], args[i + 4]);
     if (!s.IsOK()) {
-      return std::move(s).Prefixed("time expression format error");
+      return std::move(s).Prefixed("cron expression format error");
     }
     new_schedulers.push_back(*s);
   }
@@ -63,20 +82,8 @@ bool Cron::IsTimeMatch(const tm *tm) {
     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_) {
-    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) 
{
+    if (st.IsMatch(tm)) {
       last_tm_ = *tm;
       return true;
     }
@@ -94,40 +101,3 @@ std::string Cron::ToString() const {
   }
   return ret;
 }
-
-StatusOr<Scheduler> Cron::convertToScheduleTime(const std::string &minute, 
const std::string &hour,
-                                                const std::string &mday, const 
std::string &month,
-                                                const std::string &wday) {
-  Scheduler st;
-
-  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, 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));
-  }
-
-  return *s;
-}
diff --git a/src/common/cron.h b/src/common/cron.h
index 325745e9..b228a69e 100644
--- a/src/common/cron.h
+++ b/src/common/cron.h
@@ -23,25 +23,135 @@
 #include <ctime>
 #include <iostream>
 #include <string>
+#include <variant>
 #include <vector>
 
+#include "parse_util.h"
 #include "status.h"
+#include "string_util.h"
 
-struct Scheduler {
-  int minute;
-  int hour;
-  int mday;
-  int month;
-  int wday;
+struct CronPattern {
+  using Number = int;
+  using Range = std::pair<int, int>;
 
-  // 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;
+  struct Interval {
+    int interval;
+  };                                                         // */n
+  struct Any {};                                             // *
+  using Numbers = std::vector<std::variant<Number, Range>>;  // 1,2,3-6,7
+
+  std::variant<Numbers, Interval, Any> val;
+
+  static StatusOr<CronPattern> Parse(std::string_view str, std::tuple<int, 
int> minmax) {
+    if (str == "*") {
+      return CronPattern{Any{}};
+    } else if (str.rfind("*/", 0) == 0) {
+      auto num_str = str.substr(2);
+      auto interval = GET_OR_RET(ParseInt<int>(std::string(num_str.begin(), 
num_str.end()), minmax)
+                                     .Prefixed("an integer is expected after 
`*/` in a cron expression"));
+
+      if (interval == 0) {
+        return {Status::NotOK, "interval value after `*/` cannot be zero"};
+      }
+
+      return CronPattern{Interval{interval}};
+    } else {
+      auto num_strs = util::Split(str, ",");
+
+      Numbers results;
+      for (const auto &num_str : num_strs) {
+        if (auto pos = num_str.find('-'); pos != num_str.npos) {
+          auto l_str = num_str.substr(0, pos);
+          auto r_str = num_str.substr(pos + 1);
+          auto l = GET_OR_RET(ParseInt<int>(std::string(num_str.begin(), 
num_str.end()), minmax)
+                                  .Prefixed("an integer is expected before `-` 
in a cron expression"));
+          auto r = GET_OR_RET(ParseInt<int>(std::string(num_str.begin(), 
num_str.end()), minmax)
+                                  .Prefixed("an integer is expected after `-` 
in a cron expression"));
+
+          if (l >= r) {
+            return {Status::NotOK, "for pattern `l-r` in cron expression, r 
should be larger than l"};
+          }
+          results.push_back(Range(l, r));
+        } else {
+          auto n = GET_OR_RET(ParseInt<int>(std::string(num_str.begin(), 
num_str.end()), minmax)
+                                  .Prefixed("an integer is expected in a cron 
expression"));
+          results.push_back(n);
+        }
+      }
+
+      if (results.empty()) {
+        return {Status::NotOK, "invalid cron expression"};
+      }
+
+      return CronPattern{results};
+    }
+  }
+
+  std::string ToString() const {
+    if (std::holds_alternative<Numbers>(val)) {
+      std::string result;
+      bool first = true;
+
+      for (const auto &v : std::get<Numbers>(val)) {
+        if (first)
+          first = false;
+        else
+          result += ",";
+
+        if (std::holds_alternative<Number>(v)) {
+          result += std::to_string(std::get<Number>(v));
+        } else {
+          auto range = std::get<Range>(v);
+          result += std::to_string(range.first) + "-" + 
std::to_string(range.second);
+        }
+      }
+
+      return result;
+    } else if (std::holds_alternative<Interval>(val)) {
+      return "*/" + std::to_string(std::get<Interval>(val).interval);
+    } else if (std::holds_alternative<Any>(val)) {
+      return "*";
+    }
+
+    __builtin_unreachable();
+  }
+
+  bool IsMatch(int input, int interval_offset = 0) const {
+    if (std::holds_alternative<Numbers>(val)) {
+      bool result = false;
+      for (const auto &v : std::get<Numbers>(val)) {
+        if (std::holds_alternative<Number>(v)) {
+          result = result || input == std::get<Number>(v);
+        } else {
+          auto range = std::get<Range>(v);
+          result = result || (range.first <= input && input <= range.second);
+        }
+      }
+
+      return result;
+    } else if (std::holds_alternative<Interval>(val)) {
+      return (input - interval_offset) % std::get<Interval>(val).interval == 0;
+    } else if (std::holds_alternative<Any>(val)) {
+      return true;
+    }
+
+    __builtin_unreachable();
+  }
+};
+
+struct CronScheduler {
+  CronPattern minute;
+  CronPattern hour;
+  CronPattern mday;
+  CronPattern month;
+  CronPattern wday;
 
   std::string ToString() const;
+
+  static StatusOr<CronScheduler> Parse(std::string_view minute, 
std::string_view hour, std::string_view mday,
+                                       std::string_view month, 
std::string_view wday);
+
+  bool IsMatch(const tm *tm) const;
 };
 
 class Cron {
@@ -55,11 +165,6 @@ class Cron {
   bool IsEnabled() const;
 
  private:
-  std::vector<Scheduler> schedulers_;
+  std::vector<CronScheduler> schedulers_;
   tm last_tm_ = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, nullptr};
-
-  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, bool &is_interval);
 };

Reply via email to