folly-clib-20260203.1245: folly/folly/container/RegexMatchCache.h
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <array>
#include <cassert>
#include <chrono>
#include <iosfwd>
#include <string>
#include <string_view>
#include <tuple>
#include <type_traits>
#include <unordered_map>
#include <vector>
#include <folly/Chrono.h>
#include <folly/Function.h>
#include <folly/container/F14Map.h>
#include <folly/container/F14Set.h>
#include <folly/container/Reserve.h>
#include <folly/hash/UniqueHashKey.h>
#include <folly/lang/Bits.h>
namespace folly {
/// RegexMatchCacheDynamicBitset
///
/// A dynamic bitset for use within, and optimized for, RegexMatchCache.
/// * Small, having the same size and alignment as a pointer.
/// * Optimistically non-allocating, using in-situ storage for small bitsets.
///
/// Intended for use only within RegexMatchCache.
///
/// Incomplete as a generic container.
class RegexMatchCacheDynamicBitset {
private:
template <typename Word>
struct bit_span {
Word* data;
size_t size;
bit_span(Word* const data_, size_t const size_) noexcept
: data{data_}, size{size_} {}
bit_span(bit_span const&) = default;
bit_span& operator=(bit_span const&) = default;
auto as_tuple() const noexcept { return std::tuple{data, size}; }
friend bool operator==(bit_span const& a, bit_span const& b) noexcept {
return a.as_tuple() == b.as_tuple();
}
friend bool operator!=(bit_span const& a, bit_span const& b) noexcept {
return a.as_tuple() != b.as_tuple();
}
};
public:
RegexMatchCacheDynamicBitset() = default;
RegexMatchCacheDynamicBitset(RegexMatchCacheDynamicBitset const&) = delete;
RegexMatchCacheDynamicBitset(RegexMatchCacheDynamicBitset&& that) noexcept
: data_{std::exchange(that.data_, {})} {}
~RegexMatchCacheDynamicBitset() { reset_(); }
void operator=(RegexMatchCacheDynamicBitset const&) = delete;
RegexMatchCacheDynamicBitset& operator=(
RegexMatchCacheDynamicBitset&& that) noexcept {
reset_();
data_ = std::exchange(that.data_, {});
return *this;
}
bool get_value(size_t const index) const noexcept {
auto data = get_bit_span_();
if (!(index < data.size)) {
return false;
}
return get_value_(data, index);
}
void set_value(size_t const index, bool const value) {
constexpr auto wordbits = sizeof(uintptr_t) * 8;
auto data = get_bit_span_();
if (!(index < data.size) ||
(data.size == wordbits && index == wordbits - 1)) {
if (!value) {
return;
}
data = reserve_(index);
}
assert(index < data.size);
set_value_(data, index, value);
}
void reset() noexcept { reset_(); }
class index_set_view {
private:
friend RegexMatchCacheDynamicBitset;
bit_span<uintptr_t const> bitset_;
explicit index_set_view(RegexMatchCacheDynamicBitset const& bitset) noexcept
: bitset_{bitset.get_bit_span_()} {}
public:
using value_type = size_t;
class const_iterator {
public:
using value_type = size_t;
using difference_type = ptrdiff_t;
using pointer = void;
using iterator_category = std::forward_iterator_tag;
struct reference {
private:
friend class const_iterator;
size_t const index_;
explicit reference(size_t const index) noexcept : index_{index} {}
public:
operator size_t() const noexcept { return index_; }
};
private:
using self = const_iterator;
bit_span<uintptr_t const> const data_;
size_t index_;
size_t ceil_valid_index(size_t index) const noexcept {
constexpr auto wordbits = sizeof(uintptr_t) * 8;
while (index < data_.size) {
auto const wordidx = index / wordbits;
auto const wordoff = index % wordbits;
if (auto const word = data_.data[wordidx] >> wordoff) {
return index + findFirstSet(word) - 1;
}
index = (wordidx + 1) * wordbits;
}
return index;
}
public:
const_iterator(
bit_span<uintptr_t const> const data, size_t const index) noexcept
: data_{data}, index_{ceil_valid_index(index)} {}
reference operator*() const noexcept { return reference{index_}; }
const_iterator& operator++() noexcept {
index_ = ceil_valid_index(index_ + 1);
return *this;
}
friend bool operator==(self const& a, self const& b) noexcept {
return a.index_ == b.index_;
}
friend bool operator!=(self const& a, self const& b) noexcept {
return a.index_ != b.index_;
}
};
const_iterator begin() const noexcept { return const_iterator{bitset_, 0}; }
const_iterator end() const noexcept {
return const_iterator{bitset_, bitset_.size};
}
bool empty() const noexcept { return begin() == end(); }
};
index_set_view as_index_set_view() const noexcept {
return index_set_view{*this};
}
private:
bool has_capacity_(size_t const index) const noexcept {
constexpr auto wordbits = sizeof(uintptr_t) * 8;
auto const buf = get_bit_span_();
return index < buf.size && !(buf.size == wordbits && index == wordbits - 1);
}
bit_span<uintptr_t> reserve_(size_t const index) {
assert(!has_capacity_(index));
constexpr auto wordbits = sizeof(uintptr_t) * 8;
constexpr auto minsize = wordbits * 2; // min growth from in-situ to on-heap
auto const newsize = std::max(strictNextPowTwo(index), minsize);
assert(newsize >= minsize);
assert(newsize % wordbits == 0);
auto const newdata = new uintptr_t[newsize / 8];
auto const buf = get_bit_span_();
auto const buf2size = nextPowTwo(buf.size);
std::memcpy(newdata, buf.data, buf2size / 8);
std::memset(newdata + buf2size / wordbits, 0, (newsize - buf2size) / 8);
if (!(to_signed(data_) < 0)) {
auto const data = new bit_span<uintptr_t>{newdata, newsize};
assert(!(reinterpret_cast<uintptr_t>(data) & 1));
data_ = (reinterpret_cast<uintptr_t>(data) >> 1) | ~(~uintptr_t(0) >> 1);
return *data;
} else {
auto const data = reinterpret_cast<bit_span<uintptr_t>*>(data_ << 1);
delete[] data->data;
*data = {newdata, newsize};
return *data;
}
}
void reset_() {
if (!(to_signed(data_) < 0)) {
data_ = 0;
} else {
auto const data = reinterpret_cast<bit_span<uintptr_t>*>(data_ << 1);
delete[] data->data;
delete data;
data_ = 0;
}
}
template <typename Word>
static bool get_value_(
bit_span<Word> const buf, size_t const index) noexcept {
assert(index < buf.size);
constexpr auto wordbits = sizeof(Word) * 8;
auto const wordidx = index / wordbits;
auto const wordoff = index % wordbits;
auto const mask = Word(1) << wordoff;
auto& word = buf.data[wordidx];
return word & mask;
}
template <typename Word>
static void set_value_(
bit_span<Word> const buf, size_t const index, bool const value) noexcept {
assert(index < buf.size);
constexpr auto wordbits = sizeof(Word) * 8;
assert(buf.size != wordbits || index != wordbits - 1);
auto const wordidx = index / wordbits;
auto const wordoff = index % wordbits;
auto const mask = Word(1) << wordoff;
auto& word = buf.data[wordidx];
word = value ? word | mask : word & ~mask;
}
bit_span<uintptr_t const> get_bit_span_() const noexcept {
if (!(to_signed(data_) < 0)) {
return {&data_, sizeof(data_) * 8};
} else {
return *reinterpret_cast<bit_span<uintptr_t const> const*>(data_ << 1);
}
}
bit_span<uintptr_t> get_bit_span_() noexcept {
if (!(to_signed(data_) < 0)) {
return {&data_, sizeof(data_) * 8};
} else {
return *reinterpret_cast<bit_span<uintptr_t> const*>(data_ << 1);
}
}
uintptr_t data_{};
};
/// RegexMatchCacheIndexedVector
///
/// An indexed vector, which is a vector for which the index of any element can
/// be found efficiently.
///
/// Intended for use only within RegexMatchCache.
///
/// Incomplete as a generic container.
template <typename Value>
class RegexMatchCacheIndexedVector {
public:
size_t size() const noexcept { return forward_.size(); }
bool contains_index(size_t index) const noexcept {
return reverse_.contains(index);
}
bool contains_value(Value const& value) const noexcept {
return forward_.contains(value);
}
std::pair<size_t, bool> insert_value(Value const& value) {
auto [iter, inserted] = forward_.try_emplace(value);
if (inserted) {
auto rollback_forward = makeGuard([&, iter_ = iter] {
forward_.erase(iter_);
});
if (free_.capacity() < forward_.size()) {
grow_capacity_by(free_, forward_.size() - free_.size());
}
assert(!(free_.capacity() < forward_.size()));
auto const from_free = !free_.empty();
auto const index = from_free ? free_.back() : forward_.size() - 1;
from_free ? free_.pop_back() : void();
iter->second = index;
auto rollback_free = makeGuard([&] {
from_free ? free_.push_back(index) : void();
});
assert(!reverse_.contains(index));
reverse_[index] = value;
rollback_free.dismiss();
rollback_forward.dismiss();
}
return {iter->second, inserted};
}
bool erase_value(Value const& value) noexcept {
auto iter = forward_.find(value);
if (iter == forward_.end()) {
return false;
}
assert(free_.size() < free_.capacity());
auto index = iter->second;
free_.push_back(index);
forward_.erase(iter);
reverse_.erase(index);
return true;
}
void clear() noexcept {
reverse_.clear();
forward_.clear();
free_.clear();
}
Value const& value_at_index(size_t index) const { return reverse_.at(index); }
size_t index_of_value(Value const& value) const { return forward_.at(value); }
class forward_view {
private:
friend RegexMatchCacheIndexedVector;
using map_t = folly::F14FastMap<Value, size_t>;
map_t const& map;
explicit forward_view(map_t const& map_) noexcept : map{map_} {}
public:
using value_type = typename map_t::value_type;
using size_type = typename map_t::size_type;
using iterator = typename map_t::const_iterator;
size_t size() const noexcept { return map.size(); }
iterator begin() const noexcept { return map.begin(); }
iterator end() const noexcept { return map.end(); }
};
forward_view as_forward_view() const noexcept {
return forward_view{forward_};
}
private:
std::vector<size_t> free_;
folly::F14FastMap<Value, size_t> forward_;
folly::F14FastMap<size_t, Value> reverse_;
};
using RegexMatchCacheKeyBase = unique_hash_key_strong_sha256<32>;
/// RegexMatchCacheKey
///
/// A key derived from a string. Used with RegexMatchCache.
///
/// Intended for use only with RegexMatchCache.
///
/// Incomplete as a generic facility.
class RegexMatchCacheKey : public RegexMatchCacheKeyBase {
public:
explicit RegexMatchCacheKey(std::string_view regex) noexcept
: RegexMatchCacheKeyBase{std::tuple(regex)} {}
};
} // namespace folly
namespace std {
template <>
struct hash<::folly::RegexMatchCacheKey>
: hash<::folly::RegexMatchCacheKeyBase> {};
} // namespace std
namespace folly {
/// RegexMatchCacheKeyAndView
///
/// A composite key and view derived from a string. Used with RegexMatchCache.
///
/// Intended for use only with RegexMatchCache.
///
/// Incomplete as a generic facility.
class RegexMatchCacheKeyAndView {
public:
using regex_key = RegexMatchCacheKey;
regex_key const key;
std::string_view const view;
explicit RegexMatchCacheKeyAndView(std::string_view regex) noexcept
: key{regex}, view{regex} {}
/* implicit */ operator RegexMatchCacheKey const&() const noexcept {
return key;
}
/* implicit */ operator std::string_view const&() const noexcept {
return view;
}
private:
RegexMatchCacheKeyAndView(
regex_key const& k, std::string_view const v) noexcept
: key{k}, view{v} {}
};
/// RegexMatchCache
///
/// A cache around boost::regex_match(string, regex).
///
/// For efficiency, assumes several constraints and makes several guarantees.
///
/// The data structure owns regexes but does not own strings. The lifetimes of
/// all strings in the cache must surround their additions to the cache and
/// their subsequent removals from the cache or destruction of the cache.
///
/// The data structure is in two parts:
/// * A bidirectional match-cache contains all known matches.
/// * a bidirectional string-queue contains unknown, hypothetical matches.
///
/// Cached lookup operates only over the match-cache. When the string-queue for
/// a given regex is not empty, that regex is said to be uncoalesced. Cached
/// lookups are not permitted for an uncoalesced regex; that regex must first be
/// coalesced.
///
/// Addition of a string adds the string to the string-queue corresponding to
/// all known regexes. It does not perform any regex-match operations.
///
/// Addition and coalesce of a regex performs regex-matches for that regex only.
/// The string-queue for the given regex is removed and all elements matched
/// against the regex, and matching strings are added to the match-cache.
///
/// Lookup must follow a pattern like this:
///
/// if (!cache.isReadyToFindMatches(regex)) { // const
/// cache.prepareToFindMatches(regex); // non-const
/// }
/// auto matches = cache.findMatches(regex); // const
///
/// This is to support concurrent lookups, where the cache is protected by a
/// shared mutex.
///
/// The data structure is exception-safe in a sense. If an exception is thrown
/// within any non-const member function and escapes, the data structure may
/// purge all cached regexes while leaving all strings. In most such member
/// functions, only a memory-allocation failure would cause an exception to be
/// thrown. But in prepareToFindMatches, the provided regex may be syntactically
/// invalid and parsing it may throw, or it may be pathological and evaluating
/// it over a string may throw. In any event, the resolution is to clear out all
/// added regexes and to leave only the added strings. The reason is that this
/// resolution is simple and likely to be correct, while any other mechanism
/// would be complex and would be likely to have bugs.
class RegexMatchCache {
public:
using clock = folly::chrono::coarse_steady_clock;
using time_point = clock::time_point;
using regex_key = RegexMatchCacheKey;
using regex_key_and_view = RegexMatchCacheKeyAndView;
private:
using regex_pointer = regex_key const*;
using string_pointer = std::string const*;
class RegexObject;
struct RegexToMatchEntry : MoveOnly {
mutable std::atomic<time_point> accessed_at{};
folly::F14VectorSet<string_pointer> matches;
};
struct MatchToRegexEntry : MoveOnly {
RegexMatchCacheDynamicBitset regexes;
};
struct StringQueueForwardEntry : MoveOnly {
RegexMatchCacheDynamicBitset regexes;
};
struct StringQueueReverseEntry : MoveOnly {
folly::F14VectorSet<string_pointer> strings;
};
RegexMatchCacheIndexedVector<regex_pointer> regexVector_;
/// cacheRegexToMatch_
///
/// A match-cache map from regexes to the sets of matching strings.
///
/// The set of matching strings for a given regex may be incomplete. This
/// happens when strings are added to the universe but have not yet been
/// coalesced for the given regex. The set of uncoalesced strings for a
/// given regex is in stringQueueReverse_.
///
/// For each regex, includes a last-accessed-at timestamp. This timestamp
/// is used when purging old regexes from the cache, for the caller's own
/// definition of old.
folly::F14NodeMap<regex_key, RegexToMatchEntry> cacheRegexToMatch_;
/// cacheMatchToRegex_
///
/// A match-cache map from strings to the sets of matching regexes.
///
/// The set of matching regexes for a given string may be incomplete. This
/// happens when strings are added to the universe but have not yet been
/// coalesced for all regexes in the universe. The set of regexes for which
/// a given string has not yet been coalesced is in stringQueueForward_.
folly::F14FastMap<string_pointer, MatchToRegexEntry> cacheMatchToRegex_;
/// stringQueueForward_
///
/// A pending-coalesce map from strings to regexes for which the strings have
/// not yet been coalesced, that is, for which it is not yet known that the
/// strings do or do not match the given regexes.
///
/// In a steady-state when all strings have been coalesced for all regexes,
/// this map would be empty.
folly::F14FastMap<string_pointer, StringQueueForwardEntry>
stringQueueForward_;
/// stringQueueReverse_
///
/// A pending-coalesce map from regexes to strings which have not yet been
/// coalesced for the given regex, that is, for which it is not yet known that
/// the strings do or do not match the given regexes.
///
/// In a steady-state when all strings have been coalesced for all regexes,
/// this map would be empty.
folly::F14FastMap<regex_pointer, StringQueueReverseEntry> stringQueueReverse_;
void repair() noexcept;
public:
class KeyMap {
public:
using regex_key = RegexMatchCacheKey;
using regex_key_and_view = RegexMatchCacheKeyAndView;
virtual ~KeyMap() = 0;
virtual std::string_view lookup(regex_key const& regex) const = 0;
};
class InspectView {
friend RegexMatchCache;
private:
RegexMatchCache const& ref_;
KeyMap const& keys_;
explicit InspectView(
RegexMatchCache const& ref, KeyMap const& keys) noexcept
: ref_{ref}, keys_{keys} {}
void print(std::ostream& o) const;
public:
friend std::ostream& operator<<(std::ostream& o, InspectView const view) {
return (view.print(o), o);
}
};
class ConsistencyReportMatcher {
private:
struct state;
std::unique_ptr<state> state_;
public:
using regex_key = RegexMatchCache::regex_key;
using regex_key_and_view = RegexMatchCache::regex_key_and_view;
using string_pointer = RegexMatchCache::string_pointer;
ConsistencyReportMatcher();
virtual ~ConsistencyReportMatcher();
virtual bool match(
KeyMap const& keys, regex_key regex, string_pointer string);
};
class FindMatchesUnsafeResult {
private:
friend class RegexMatchCache;
using map_t = folly::F14VectorSet<string_pointer>;
map_t const& matches_;
/* implicit */ FindMatchesUnsafeResult(map_t const& matches) noexcept
: matches_{matches} {}
public:
using value_type = map_t::value_type;
auto size() const noexcept { return matches_.size(); }
auto begin() const noexcept { return matches_.begin(); }
auto end() const noexcept { return matches_.end(); }
};
RegexMatchCache() noexcept;
~RegexMatchCache();
std::vector<std::string_view> getRegexList(KeyMap const& keys) const;
std::vector<string_pointer> getStringList() const;
InspectView inspect(KeyMap const& keys) const noexcept {
return InspectView{*this, keys};
}
void consistency(
ConsistencyReportMatcher& crcache,
KeyMap const& keys,
FunctionRef<void(std::string)> report) const;
bool hasRegex(regex_key const& regex) const noexcept;
void addRegex(regex_key const& regex);
void eraseRegex(regex_key const& regex);
bool hasString(string_pointer string) const noexcept;
void addString(string_pointer string);
void eraseString(string_pointer string);
std::vector<string_pointer> findMatchesUncached(std::string_view regex) const;
bool isReadyToFindMatches(regex_key const& regex) const noexcept;
void prepareToFindMatches(regex_key_and_view const& regex);
FindMatchesUnsafeResult findMatchesUnsafe(
regex_key const& regex, time_point now) const;
std::vector<string_pointer> findMatches(
regex_key const& regex, time_point now) const;
bool hasItemsToPurge(time_point expiry) const noexcept;
void clear();
void purge(time_point expiry);
};
} // namespace folly