diff --git a/include/transactions.hpp b/include/transactions.hpp index ac79fbe2b..f745f97f1 100644 --- a/include/transactions.hpp +++ b/include/transactions.hpp @@ -24,11 +24,21 @@ namespace fair::graph { -static auto nullMatchPred = [](auto, auto) { return false; }; +static auto nullMatchPred = [](auto, auto, auto) { return std::nullopt; }; template class ctx_settings : public settings_base { - using MatchPredicate = std::function; + /** + * A predicate for matching two contexts + * The third "attempt" parameter indicates the current round of matching being done. + * This is useful for hierarchical matching schemes, + * e.g. in the first round the predicate could look for almost exact matches only, + * then in a a second round (attempt=1) it could be more forgiving, given that there are no exact matches available. + * + * The predicate will be called until it returns "true" (a match is found), or until it returns std::nullopt, + * which indicates that no matches were found and there is no chance of matching anything in a further round. + */ + using MatchPredicate = std::function(const property_map &, const property_map &, std::size_t)>; Node *_node = nullptr; mutable std::mutex _lock{}; @@ -249,10 +259,8 @@ class ctx_settings : public settings_base { ret = exact_match; } else { // try the match predicate instead - auto match = std::find_if(_settings.begin(), _settings.end(), [&](const auto &i) { return _match_pred(i.first.context, ctx.context); }); - if (match != _settings.end()) { - ret = match->second; - } + const auto &match = bestMatch(ctx.context); + ret = match.value_or(ret); } // return only the needed values @@ -402,6 +410,20 @@ class ctx_settings : public settings_base { } private: + std::optional + bestMatch(const property_map &context) const { + // retry until we either get a match or std::nullopt + for (std::size_t attempt = 0;; ++attempt) { + for (const auto &i : _settings) { + const auto matchres = _match_pred(i.first.context, context, attempt); + if (!matchres) { + return std::nullopt; + } else if (*matchres) { + return i.second; + } + } + } + } void store_default_settings(property_map &oldSettings) { // take a copy of the field -> map value of the old settings diff --git a/test/qa_settings.cpp b/test/qa_settings.cpp index 6dac229dd..9d9b8e2ce 100644 --- a/test/qa_settings.cpp +++ b/test/qa_settings.cpp @@ -565,29 +565,40 @@ const boost::ut::suite TransactionTests = [] { expect(eq(std::get(*s.get("scaling_factor", ctx0)), 42.f)); // get value with an older timestamp }; - auto matchPred = [](const auto &lhs, const auto &rhs) { - return !lhs.empty() && std::all_of(lhs.begin(), lhs.end(), [&](const auto &v) { return rhs.contains(v.first) && rhs.at(v.first) == v.second; }); + auto matchPred = [](const auto &lhs, const auto &rhs, const auto attempt) -> std::optional { + if (attempt >= 4) { + return std::nullopt; + } + + constexpr std::array fields = { "BPCID", "SID", "BPID", "GID" }; + // require increasingly less fields to match for each attempt + return std::ranges::all_of(fields | std::ranges::views::take(4 - attempt), [&](const auto &f) { return lhs.contains(f) && rhs.at(f) == lhs.at(f); }); }; "CtxSettings Parsing"_test = [&] { graph flow_graph; - auto &block = flow_graph.make_node>({ { "name", "TestName" }, { "scaling_factor", 2.f } }); + auto &block = flow_graph.make_node>({ { "scaling_factor", 42 } }); auto s = ctx_settings(block, matchPred); - const auto ctx0 = SettingsCtx(std::chrono::system_clock::now(), { { "BPCID", 1 } }); - std::ignore = s.set({ { "name", "TestNameAlt" }, { "scaling_factor", 42.f } }, ctx0); - const auto ctx1 = SettingsCtx(std::chrono::system_clock::now(), { { "BPCID", 1 }, { "SID", 2 } }); - std::ignore = s.set({ { "name", "TestNameNew" }, { "scaling_factor", 43.f } }, ctx1); + const auto ctx0 = SettingsCtx(std::chrono::system_clock::now(), { { "BPCID", 1 }, { "SID", 1 }, { "BPID", 1 }, { "GID", 1 } }); + std::ignore = s.set({ { "scaling_factor", 101 } }, ctx0); + const auto ctx1 = SettingsCtx(std::chrono::system_clock::now(), { { "BPCID", 1 }, { "SID", 1 }, { "BPID", 1 } }); + std::ignore = s.set({ { "scaling_factor", 102 } }, ctx1); + const auto ctx2 = SettingsCtx(std::chrono::system_clock::now(), { { "BPCID", 1 }, { "SID", 1 } }); + std::ignore = s.set({ { "scaling_factor", 103 } }, ctx2); + const auto ctx3 = SettingsCtx(std::chrono::system_clock::now(), { { "BPCID", 1 } }); + std::ignore = s.set({ { "scaling_factor", 104 } }, ctx3); // exact matches for contexts work - expect(eq(std::get(*s.get("scaling_factor", ctx0)), 42.f)); - expect(eq(std::get(*s.get("scaling_factor", ctx1)), 43.f)); - - // matching by using the custom predicate - auto ctx3 = SettingsCtx(std::chrono::system_clock::now(), { { "BPCID", 1 }, { "SID", 2 }, { "BPID", 1 } }); - expect(eq(std::get(*s.get("scaling_factor", ctx3)), 43.f)); - auto ctx4 = SettingsCtx(std::chrono::system_clock::now(), { { "BPCID", 1 }, { "SID", 2 }, { "BPID", 3 } }); - std::ignore = s.set({ { "scaling_factor", 44.f } }, ctx4); - expect(eq(std::get(*s.get("scaling_factor", ctx4)), 44.f)); + expect(eq(std::get(*s.get("scaling_factor", ctx0)), 101)); + expect(eq(std::get(*s.get("scaling_factor", ctx1)), 102)); + expect(eq(std::get(*s.get("scaling_factor", ctx2)), 103)); + expect(eq(std::get(*s.get("scaling_factor", ctx3)), 104)); + + // matching by using the custom predicate (no exact matching possible anymore) + const auto ctx4 = SettingsCtx(std::chrono::system_clock::now(), { { "BPCID", 1 }, { "SID", 1 }, { "BPID", 1 }, { "GID", 2 } }); + expect(eq(std::get(*s.get("scaling_factor", ctx4)), 102)); // no setting for 'gid=2' -> fall back to 'gid=-1' + const auto ctx5 = SettingsCtx(std::chrono::system_clock::now(), { { "BPCID", 1 }, { "SID", 1 }, { "BPID", 2 }, { "GID", 2 } }); + expect(eq(std::get(*s.get("scaling_factor", ctx5)), 103)); // no setting for 'pid=2' and 'gid=2' -> fall back to 'pid=gid=-1' // doesn't exist auto ctx6 = SettingsCtx(std::chrono::system_clock::now(), { { "BPCID", 9 }, { "SID", 9 }, { "BPID", 9 }, { "GID", 9 } });