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

asekretenko pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mesos.git

commit 470665e1575b9a91dffca974485940c7a0a1fe37
Author: Andrei Sekretenko <asekrete...@apache.org>
AuthorDate: Mon Aug 3 18:35:06 2020 +0200

    Implemented regex-based offer constraints.
    
    Review: https://reviews.apache.org/r/72786
---
 include/mesos/allocator/allocator.hpp              |  16 +++
 src/Makefile.am                                    |   1 +
 .../allocator/mesos/offer_constraints_filter.cpp   | 107 +++++++++++++++++++--
 src/master/constants.hpp                           |  10 ++
 src/master/flags.cpp                               |  18 ++++
 src/master/flags.hpp                               |   3 +
 src/master/master.cpp                              |   8 +-
 src/master/master.hpp                              |   3 +
 .../master/offer_constraints_filter_tests.cpp      |  33 ++++---
 9 files changed, 174 insertions(+), 25 deletions(-)

diff --git a/include/mesos/allocator/allocator.hpp 
b/include/mesos/allocator/allocator.hpp
index 05d0e9c..c6fca65 100644
--- a/include/mesos/allocator/allocator.hpp
+++ b/include/mesos/allocator/allocator.hpp
@@ -78,7 +78,23 @@ class OfferConstraintsFilterImpl;
 class OfferConstraintsFilter
 {
 public:
+  // TODO(asekretenko): Given that most dependants of the public allocator
+  // interface do not care about filter creation, we should consider decoupling
+  // the filter construction interface (which at this point consists of the
+  // `struct Options` and the `create()` method) from the allocator interface.
+  struct Options
+  {
+    struct RE2Limits
+    {
+      Bytes maxMem;
+      int maxProgramSize;
+    };
+
+    RE2Limits re2Limits;
+  };
+
   static Try<OfferConstraintsFilter> create(
+      const Options& options,
       scheduler::OfferConstraints&& constraints);
 
   OfferConstraintsFilter() = delete;
diff --git a/src/Makefile.am b/src/Makefile.am
index 1043c7b..943ed77 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -1133,6 +1133,7 @@ libmesos_no_3rdparty_la_SOURCES +=                        
                \
   master/allocator/mesos/hierarchical.hpp                              \
   master/allocator/mesos/metrics.cpp                                   \
   master/allocator/mesos/metrics.hpp                                   \
+  master/allocator/mesos/offer_constraints_filter.hpp                  \
   master/allocator/mesos/offer_constraints_filter.cpp                  \
   master/allocator/mesos/sorter/drf/metrics.cpp                                
\
   master/allocator/mesos/sorter/drf/metrics.hpp                                
\
diff --git a/src/master/allocator/mesos/offer_constraints_filter.cpp 
b/src/master/allocator/mesos/offer_constraints_filter.cpp
index d724fd0..30b671b 100644
--- a/src/master/allocator/mesos/offer_constraints_filter.cpp
+++ b/src/master/allocator/mesos/offer_constraints_filter.cpp
@@ -23,6 +23,8 @@
 #include <stout/try.hpp>
 #include <stout/variant.hpp>
 
+#include <re2/re2.h>
+
 #include <mesos/allocator/allocator.hpp>
 #include <mesos/attributes.hpp>
 
@@ -34,11 +36,42 @@ using std::unordered_map;
 using ::mesos::scheduler::AttributeConstraint;
 using ::mesos::scheduler::OfferConstraints;
 
+using RE2Limits =
+  ::mesos::allocator::OfferConstraintsFilter::Options::RE2Limits;
+
 namespace mesos {
 namespace allocator {
 
 namespace internal {
 
+// TODO(asekretenko): Cache returned RE2s to make sure that:
+//  - a RE2 corresponding to a pattern is not re-created needlessly
+//  - multiple RE2s corresponding to the same pattern don't coexist
+static Try<unique_ptr<const RE2>> createRE2(
+    const RE2Limits& limits,
+    const string& regex)
+{
+  RE2::Options options{RE2::CannedOptions::Quiet};
+  options.set_max_mem(limits.maxMem.bytes());
+  unique_ptr<const RE2> re2{new RE2(regex, options)};
+
+  if (!re2->ok()) {
+    return Error(
+        "Failed to construct regex from pattern"
+        " '" + regex + "': " + re2->error());
+  }
+
+  if (re2->ProgramSize() > limits.maxProgramSize) {
+    return Error(
+        "Regex '" + regex + "' is too complex: program size of " +
+        stringify(re2->ProgramSize()) + " is larger than maximum of " +
+        stringify(limits.maxProgramSize) + " allowed");
+  }
+
+  return re2;
+}
+
+
 using Selector = AttributeConstraint::Selector;
 
 
@@ -78,6 +111,7 @@ public:
   bool apply(const Attribute& attr) const { return apply_(attr); }
 
   static Try<AttributeConstraintPredicate> create(
+      const RE2Limits& re2Limits,
       AttributeConstraint::Predicate&& predicate)
   {
     using Self = AttributeConstraintPredicate;
@@ -96,6 +130,28 @@ public:
         return Self(TextNotEquals{
           std::move(*predicate.mutable_text_not_equals()->mutable_value())});
 
+      case AttributeConstraint::Predicate::kTextMatches: {
+        Try<unique_ptr<const RE2>> re2 =
+          createRE2(re2Limits, predicate.text_matches().regex());
+
+        if (re2.isError()) {
+          return Error(re2.error());
+        }
+
+        return Self(TextMatches{std::move(*re2)});
+      }
+
+      case AttributeConstraint::Predicate::kTextNotMatches: {
+        Try<unique_ptr<const RE2>> re2 =
+          createRE2(re2Limits, predicate.text_not_matches().regex());
+
+        if (re2.isError()) {
+          return Error(re2.error());
+        }
+
+        return Self(TextNotMatches{std::move(*re2)});
+      }
+
       case AttributeConstraint::Predicate::PREDICATE_NOT_SET:
         return Error("Unknown predicate type");
     }
@@ -146,15 +202,43 @@ private:
     }
   };
 
-  // TODO(asekretenko): Introduce offer constraints for regex match
-  // (MESOS-10173).
+  struct TextMatches
+  {
+    unique_ptr<const RE2> re2;
+
+    bool apply(const Nothing&) const { return false; }
+    bool apply(const string& str) const { return RE2::FullMatch(str, *re2); }
+
+    bool apply(const Attribute& attr) const
+    {
+      return attr.type() != Value::TEXT ||
+             RE2::FullMatch(attr.text().value(), *re2);
+    }
+  };
+
+  struct TextNotMatches
+  {
+    unique_ptr<const RE2> re2;
+
+    bool apply(const Nothing&) const { return true; }
+    bool apply(const string& str) const { return !RE2::FullMatch(str, *re2); }
+
+    bool apply(const Attribute& attr) const
+    {
+      return attr.type() != Value::TEXT ||
+             !RE2::FullMatch(attr.text().value(), *re2);
+    }
+  };
+
 
   using Predicate = Variant<
       Nothing,
       Exists,
       NotExists,
       TextEquals,
-      TextNotEquals>;
+      TextNotEquals,
+      TextMatches,
+      TextNotMatches>;
 
   Predicate predicate;
 
@@ -171,7 +255,9 @@ private:
         [&](const Exists& p) { return p.apply(attribute); },
         [&](const NotExists& p) { return p.apply(attribute); },
         [&](const TextEquals& p) { return p.apply(attribute); },
-        [&](const TextNotEquals& p) { return p.apply(attribute); });
+        [&](const TextNotEquals& p) { return p.apply(attribute); },
+        [&](const TextMatches& p) { return p.apply(attribute); },
+        [&](const TextNotMatches& p) { return p.apply(attribute); });
   }
 };
 
@@ -223,6 +309,7 @@ public:
   }
 
   static Try<AttributeConstraintEvaluator> create(
+      const RE2Limits& re2Limits,
       AttributeConstraint&& constraint)
   {
     Option<Error> error = validate(constraint.selector());
@@ -232,7 +319,7 @@ public:
 
     Try<AttributeConstraintPredicate> predicate =
       AttributeConstraintPredicate::create(
-          std::move(*constraint.mutable_predicate()));
+          re2Limits, std::move(*constraint.mutable_predicate()));
 
     if (predicate.isError()) {
       return Error(predicate.error());
@@ -293,7 +380,9 @@ public:
         });
   }
 
-  static Try<OfferConstraintsFilterImpl> create(OfferConstraints&& constraints)
+  static Try<OfferConstraintsFilterImpl> create(
+      const OfferConstraintsFilter::Options& options,
+      OfferConstraints&& constraints)
   {
     // TODO(asekretenko): This method performs a dumb 1:1 translation of
     // `AttributeConstraint`s without any reordering; this leaves room for
@@ -333,7 +422,8 @@ public:
         for (AttributeConstraint& constraint :
              *group_.mutable_attribute_constraints()) {
           Try<AttributeConstraintEvaluator> evaluator =
-            AttributeConstraintEvaluator::create(std::move(constraint));
+            AttributeConstraintEvaluator::create(
+                options.re2Limits, std::move(constraint));
 
           if (evaluator.isError()) {
             return Error(
@@ -360,10 +450,11 @@ using internal::OfferConstraintsFilterImpl;
 
 
 Try<OfferConstraintsFilter> OfferConstraintsFilter::create(
+    const Options& options,
     OfferConstraints&& constraints)
 {
   Try<OfferConstraintsFilterImpl> impl =
-    OfferConstraintsFilterImpl::create(std::move(constraints));
+    OfferConstraintsFilterImpl::create(options, std::move(constraints));
 
   if (impl.isError()) {
     return Error(impl.error());
diff --git a/src/master/constants.hpp b/src/master/constants.hpp
index c384b68..9aa075d 100644
--- a/src/master/constants.hpp
+++ b/src/master/constants.hpp
@@ -179,6 +179,16 @@ const Quota DEFAULT_QUOTA;
 // Default weight for a role.
 constexpr double DEFAULT_WEIGHT = 1.0;
 
+// Default values for the `max_mem` option and the limit on 
`RE2::ProgramSize()`
+// of RE2 regualr expressions used in offer constraints.
+//
+// As an example, for a regexp
+// "192.168.(1[0-9]|3[4-7]|[1-9]|4[2-9]|[1-4][0-9]|5[3-8]|20[4-7]|53[0-5]).1"
+// re2-2020-07-06 produces a RE2 object with a ProgramSize() of 54,
+// and that can be successfully constructed only with `max_mem` >= 1499.
+constexpr Bytes DEFAULT_OFFER_CONSTRAINTS_RE2_MAX_MEM = Bytes(4096);
+constexpr int DEFAULT_OFFER_CONSTRAINTS_RE2_MAX_PROGRAM_SIZE = 100;
+
 } // namespace master {
 } // namespace internal {
 } // namespace mesos {
diff --git a/src/master/flags.cpp b/src/master/flags.cpp
index 74f4daa..31a8da1 100644
--- a/src/master/flags.cpp
+++ b/src/master/flags.cpp
@@ -743,4 +743,22 @@ mesos::internal::master::Flags::Flags()
         }
         return None();
       });
+
+  add(&Flags::offer_constraints_re2_max_mem,
+      "offer_constraints_re2_max_mem",
+      "Limit on the memory usage of each RE2 regular expression in\n"
+      "framework's offer constraints. If `OfferConstraints` contain a regex\n"
+      "from which a RE2 object cannot be constructed without exceeding this\n"
+      "limit, then framework's attempt to subscribe or update subscription\n"
+      "with these `OfferConstraints` will fail.",
+      DEFAULT_OFFER_CONSTRAINTS_RE2_MAX_MEM);
+
+  add(&Flags::offer_constraints_re2_max_program_size,
+      "offer_constraints_re2_max_program_size",
+      "Limit on the RE2 program size of each regular expression in\n"
+      "framework's offer constraints. If `OfferConstraints` contain a regex\n"
+      "which results in a RE2 object exceeding this limit,\n"
+      "then framework's attempt to subscribe or update subscription\n"
+      "with these `OfferConstraints` will fail.",
+      DEFAULT_OFFER_CONSTRAINTS_RE2_MAX_PROGRAM_SIZE);
 }
diff --git a/src/master/flags.hpp b/src/master/flags.hpp
index 78623d6..9500a0a 100644
--- a/src/master/flags.hpp
+++ b/src/master/flags.hpp
@@ -121,6 +121,9 @@ public:
 #ifdef ENABLE_PORT_MAPPING_ISOLATOR
   Option<size_t> max_executors_per_agent;
 #endif  // ENABLE_PORT_MAPPING_ISOLATOR
+
+  Bytes offer_constraints_re2_max_mem;
+  int offer_constraints_re2_max_program_size;
 };
 
 } // namespace master {
diff --git a/src/master/master.cpp b/src/master/master.cpp
index 4428985..e3858b9 100644
--- a/src/master/master.cpp
+++ b/src/master/master.cpp
@@ -333,7 +333,10 @@ Master::Master(
     subscribers(this, flags.max_operator_event_stream_subscribers),
     authenticator(None()),
     metrics(new Metrics(*this)),
-    electedTime(None())
+    electedTime(None()),
+    offerConstraintsFilterOptions(
+        {flags.offer_constraints_re2_max_mem,
+         flags.offer_constraints_re2_max_program_size})
 {
   slaves.limiter = _slaveRemovalLimiter;
 
@@ -2671,6 +2674,7 @@ void Master::subscribe(
   // TODO(asekretenko): Validate roles in offer constraints (see MESOS-10176).
   if (validationError.isNone() && subscribe.has_offer_constraints()) {
     Try<OfferConstraintsFilter> filter = OfferConstraintsFilter::create(
+        offerConstraintsFilterOptions,
         std::move(*subscribe.mutable_offer_constraints()));
 
     if (filter.isError()) {
@@ -2913,6 +2917,7 @@ void Master::subscribe(
   // TODO(asekretenko): Validate roles in offer constraints (see MESOS-10176).
   if (validationError.isNone() && subscribe.has_offer_constraints()) {
     Try<OfferConstraintsFilter> filter = OfferConstraintsFilter::create(
+        offerConstraintsFilterOptions,
         std::move(*subscribe.mutable_offer_constraints()));
 
     if (filter.isError()) {
@@ -3253,6 +3258,7 @@ Future<process::http::Response> Master::updateFramework(
   if (call.has_offer_constraints()) {
     // TODO(asekretenko): Validate roles in offer constraints (see 
MESOS-10176).
     Try<OfferConstraintsFilter> filter = OfferConstraintsFilter::create(
+        offerConstraintsFilterOptions,
         std::move(*call.mutable_offer_constraints()));
 
     if (filter.isError()) {
diff --git a/src/master/master.hpp b/src/master/master.hpp
index e478f80..3d1d472 100644
--- a/src/master/master.hpp
+++ b/src/master/master.hpp
@@ -2379,6 +2379,9 @@ private:
   process::Time startTime; // Start time used to calculate uptime.
 
   Option<process::Time> electedTime; // Time when this master is elected.
+
+  ::mesos::allocator::OfferConstraintsFilter::Options
+    offerConstraintsFilterOptions;
 };
 
 
diff --git a/src/tests/master/offer_constraints_filter_tests.cpp 
b/src/tests/master/offer_constraints_filter_tests.cpp
index 84a1e91..db1976c 100644
--- a/src/tests/master/offer_constraints_filter_tests.cpp
+++ b/src/tests/master/offer_constraints_filter_tests.cpp
@@ -38,6 +38,15 @@ using ::mesos::allocator::OfferConstraintsFilter;
 using ::mesos::scheduler::OfferConstraints;
 
 
+static Try<OfferConstraintsFilter> createFilter(OfferConstraints constraints)
+{
+  OfferConstraintsFilter::Options options;
+  options.re2Limits.maxMem = Kilobytes(4);
+  options.re2Limits.maxProgramSize = 100;
+  return OfferConstraintsFilter::create(options, std::move(constraints));
+}
+
+
 static Try<OfferConstraints> OfferConstraintsFromJSON(const string& json)
 {
   Try<JSON::Object> jsonObject = JSON::parse<JSON::Object>(json);
@@ -77,8 +86,7 @@ TEST(OfferConstraintsFilter, NamedAttributeExists)
 
   ASSERT_SOME(constraints);
 
-  const Try<OfferConstraintsFilter> filter =
-    OfferConstraintsFilter::create(std::move(*constraints));
+  const Try<OfferConstraintsFilter> filter = createFilter(*constraints);
 
   ASSERT_SOME(filter);
 
@@ -112,8 +120,7 @@ TEST(OfferConstraintsFilter, NamedAttributeNotExists)
 
   ASSERT_SOME(constraints);
 
-  const Try<OfferConstraintsFilter> filter =
-    OfferConstraintsFilter::create(std::move(*constraints));
+  const Try<OfferConstraintsFilter> filter = createFilter(*constraints);
 
   ASSERT_SOME(filter);
 
@@ -147,8 +154,7 @@ TEST(OfferConstraintsFilter, NamedAttributeTextEquals)
 
   ASSERT_SOME(constraints);
 
-  const Try<OfferConstraintsFilter> filter =
-    OfferConstraintsFilter::create(std::move(*constraints));
+  const Try<OfferConstraintsFilter> filter = createFilter(*constraints);
 
   ASSERT_SOME(filter);
 
@@ -192,8 +198,7 @@ TEST(OfferConstraintsFilter, NamedAttributeTextNotEquals)
 
   ASSERT_SOME(constraints);
 
-  const Try<OfferConstraintsFilter> filter =
-    OfferConstraintsFilter::create(std::move(*constraints));
+  const Try<OfferConstraintsFilter> filter = createFilter(*constraints);
 
   ASSERT_SOME(filter);
 
@@ -238,8 +243,7 @@ TEST(OfferConstraintsFilter, TwoAttributesWithTheSameName)
 
   ASSERT_SOME(constraints);
 
-  const Try<OfferConstraintsFilter> filter =
-    OfferConstraintsFilter::create(std::move(*constraints));
+  const Try<OfferConstraintsFilter> filter = createFilter(*constraints);
 
   ASSERT_SOME(filter);
 
@@ -284,8 +288,7 @@ TEST(OfferConstraintsFilter, TwoConstraintsInGroup)
 
   ASSERT_SOME(constraints);
 
-  const Try<OfferConstraintsFilter> filter =
-    OfferConstraintsFilter::create(std::move(*constraints));
+  const Try<OfferConstraintsFilter> filter = createFilter(*constraints);
 
   ASSERT_SOME(filter);
 
@@ -332,8 +335,7 @@ TEST(OfferConstraintsFilter, TwoGroups)
 
   ASSERT_SOME(constraints);
 
-  const Try<OfferConstraintsFilter> filter =
-    OfferConstraintsFilter::create(std::move(*constraints));
+  const Try<OfferConstraintsFilter> filter = createFilter(*constraints);
 
   ASSERT_SOME(filter);
 
@@ -377,8 +379,7 @@ TEST(OfferConstraintsFilter, TwoRoles)
 
   ASSERT_SOME(constraints);
 
-  const Try<OfferConstraintsFilter> filter =
-    OfferConstraintsFilter::create(std::move(*constraints));
+  const Try<OfferConstraintsFilter> filter = createFilter(*constraints);
 
   ASSERT_SOME(filter);
 

Reply via email to