be/src/exprs/function/regexps.h
Line | Count | Source |
1 | | // Licensed to the Apache Software Foundation (ASF) under one |
2 | | // or more contributor license agreements. See the NOTICE file |
3 | | // distributed with this work for additional information |
4 | | // regarding copyright ownership. The ASF licenses this file |
5 | | // to you under the Apache License, Version 2.0 (the |
6 | | // "License"); you may not use this file except in compliance |
7 | | // with the License. You may obtain a copy of the License at |
8 | | // |
9 | | // http://www.apache.org/licenses/LICENSE-2.0 |
10 | | // |
11 | | // Unless required by applicable law or agreed to in writing, |
12 | | // software distributed under the License is distributed on an |
13 | | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
14 | | // KIND, either express or implied. See the License for the |
15 | | // specific language governing permissions and limitations |
16 | | // under the License. |
17 | | // This file is copied from |
18 | | // https://github.com/ClickHouse/ClickHouse/blob/master/src/Functions/Regexps.h |
19 | | // and modified by Doris |
20 | | |
21 | | #pragma once |
22 | | |
23 | | #include <hs/hs.h> |
24 | | #include <hs/hs_common.h> |
25 | | |
26 | | #include <boost/container_hash/hash.hpp> |
27 | | #include <memory> |
28 | | #include <mutex> |
29 | | #include <optional> |
30 | | #include <string> |
31 | | #include <utility> |
32 | | #include <vector> |
33 | | |
34 | | #include "common/exception.h" |
35 | | #include "core/string_ref.h" |
36 | | #include "util/hyperscan_util.h" |
37 | | |
38 | | namespace doris::multiregexps { |
39 | | |
40 | | template <typename Deleter, Deleter deleter> |
41 | | struct HyperscanDeleter { |
42 | | template <typename T> |
43 | 34 | void operator()(T* ptr) const { |
44 | 34 | deleter(ptr); |
45 | 34 | } _ZNK5doris12multiregexps16HyperscanDeleterIPFiP16hs_compile_errorEXadL_Z21hs_free_compile_errorEEEclIS2_EEvPT_ Line | Count | Source | 43 | 1 | void operator()(T* ptr) const { | 44 | 1 | deleter(ptr); | 45 | 1 | } |
_ZNK5doris12multiregexps16HyperscanDeleterIPFiP11hs_databaseEXadL_Z16hs_free_databaseEEEclIS2_EEvPT_ Line | Count | Source | 43 | 1 | void operator()(T* ptr) const { | 44 | 1 | deleter(ptr); | 45 | 1 | } |
_ZNK5doris12multiregexps16HyperscanDeleterIPFiP10hs_scratchEXadL_Z15hs_free_scratchEEEclIS2_EEvPT_ Line | Count | Source | 43 | 32 | void operator()(T* ptr) const { | 44 | 32 | deleter(ptr); | 45 | 32 | } |
|
46 | | }; |
47 | | |
48 | | /// Helper unique pointers to correctly delete the allocated space when hyperscan cannot compile something and we throw an exception. |
49 | | using CompilerError = |
50 | | std::unique_ptr<hs_compile_error_t, |
51 | | HyperscanDeleter<decltype(&hs_free_compile_error), &hs_free_compile_error>>; |
52 | | using ScratchPtr = std::unique_ptr<hs_scratch_t, |
53 | | HyperscanDeleter<decltype(&hs_free_scratch), &hs_free_scratch>>; |
54 | | using DataBasePtr = |
55 | | std::unique_ptr<hs_database_t, |
56 | | HyperscanDeleter<decltype(&hs_free_database), &hs_free_database>>; |
57 | | |
58 | | /// Database is thread safe across multiple threads and Scratch is not but we can copy it whenever we use it in the searcher. |
59 | | class Regexps { |
60 | | public: |
61 | 28 | Regexps(hs_database_t* db_, hs_scratch_t* scratch_) : db {db_}, scratch {scratch_} {} |
62 | | |
63 | 36 | hs_database_t* getDB() const { return db.get(); } |
64 | 31 | hs_scratch_t* getScratch() const { return scratch.get(); } |
65 | | |
66 | | private: |
67 | | DataBasePtr db; |
68 | | ScratchPtr scratch; |
69 | | }; |
70 | | |
71 | | class DeferredConstructedRegexps { |
72 | | public: |
73 | | explicit DeferredConstructedRegexps(std::function<Regexps()> constructor_) |
74 | 29 | : constructor(std::move(constructor_)) {} |
75 | | |
76 | 32 | Regexps* get() { |
77 | 32 | std::lock_guard lock(mutex); |
78 | 32 | if (regexps) { |
79 | 3 | return &*regexps; |
80 | 3 | } |
81 | 29 | regexps = constructor(); |
82 | 29 | return &*regexps; |
83 | 32 | } |
84 | | |
85 | | private: |
86 | | std::mutex mutex; |
87 | | std::function<Regexps()> constructor; |
88 | | std::optional<Regexps> regexps; |
89 | | }; |
90 | | |
91 | | using DeferredConstructedRegexpsPtr = std::shared_ptr<DeferredConstructedRegexps>; |
92 | | |
93 | | template <bool save_indices, bool WithEditDistance> |
94 | | Regexps constructRegexps(const std::vector<String>& str_patterns, |
95 | 30 | [[maybe_unused]] std::optional<UInt32> edit_distance) { |
96 | | /// Common pointers |
97 | 30 | std::vector<const char*> patterns; |
98 | 30 | std::vector<unsigned int> flags; |
99 | | |
100 | | /// Pointer for external edit distance compilation |
101 | 30 | std::vector<hs_expr_ext> ext_exprs; |
102 | 30 | std::vector<const hs_expr_ext*> ext_exprs_ptrs; |
103 | | |
104 | 30 | patterns.reserve(str_patterns.size()); |
105 | 30 | flags.reserve(str_patterns.size()); |
106 | | |
107 | | if constexpr (WithEditDistance) { |
108 | | ext_exprs.reserve(str_patterns.size()); |
109 | | ext_exprs_ptrs.reserve(str_patterns.size()); |
110 | | } |
111 | | |
112 | 335 | for (const auto& ref : str_patterns) { |
113 | 335 | patterns.push_back(ref.data()); |
114 | | /* Flags below are the pattern matching flags. |
115 | | * HS_FLAG_DOTALL is a compile flag where matching a . will not exclude newlines. This is a good |
116 | | * performance practice according to Hyperscan API. https://intel.github.io/hyperscan/dev-reference/performance.html#dot-all-mode |
117 | | * HS_FLAG_ALLOWEMPTY is a compile flag where empty strings are allowed to match. |
118 | | * HS_FLAG_UTF8 is a flag where UTF8 literals are matched. |
119 | | * HS_FLAG_SINGLEMATCH is a compile flag where each pattern match will be returned only once. it is a good performance practice |
120 | | * as it is said in the Hyperscan documentation. https://intel.github.io/hyperscan/dev-reference/performance.html#single-match-flag |
121 | | */ |
122 | 335 | flags.push_back(HS_FLAG_DOTALL | HS_FLAG_SINGLEMATCH | HS_FLAG_ALLOWEMPTY | HS_FLAG_UTF8); |
123 | | if constexpr (WithEditDistance) { |
124 | | /// Hyperscan currently does not support UTF8 matching with edit distance. |
125 | | flags.back() &= ~HS_FLAG_UTF8; |
126 | | ext_exprs.emplace_back(); |
127 | | /// HS_EXT_FLAG_EDIT_DISTANCE is a compile flag responsible for Levenstein distance. |
128 | | ext_exprs.back().flags = HS_EXT_FLAG_EDIT_DISTANCE; |
129 | | ext_exprs.back().edit_distance = edit_distance.value(); |
130 | | ext_exprs_ptrs.push_back(&ext_exprs.back()); |
131 | | } |
132 | 335 | } |
133 | 30 | hs_database_t* db = nullptr; |
134 | 30 | hs_compile_error_t* compile_error = nullptr; |
135 | | |
136 | 30 | std::unique_ptr<unsigned int[]> ids; |
137 | | |
138 | | /// We mark the patterns to provide the callback results. |
139 | | if constexpr (save_indices) { |
140 | | ids.reset(new unsigned int[patterns.size()]); |
141 | | for (size_t i = 0; i < patterns.size(); ++i) { |
142 | | ids[i] = static_cast<unsigned>(i + 1); |
143 | | } |
144 | | } |
145 | | |
146 | 335 | for (auto& pattern : patterns) { |
147 | 335 | LOG(INFO) << "pattern: " << pattern << "\n"; |
148 | 335 | if (is_hyperscan_regexp_expensive(pattern)) { |
149 | 1 | throw doris::Exception(Status::InvalidArgument(HYPERSCAN_BOUNDED_REPEAT_ERROR)); |
150 | 1 | } |
151 | 335 | } |
152 | | |
153 | 29 | hs_error_t err; |
154 | 29 | if constexpr (!WithEditDistance) { |
155 | 29 | err = hs_compile_multi(patterns.data(), flags.data(), ids.get(), |
156 | 29 | static_cast<unsigned>(patterns.size()), HS_MODE_BLOCK, nullptr, &db, |
157 | 29 | &compile_error); |
158 | | } else { |
159 | | err = hs_compile_ext_multi(patterns.data(), flags.data(), ids.get(), ext_exprs_ptrs.data(), |
160 | | static_cast<unsigned>(patterns.size()), HS_MODE_BLOCK, nullptr, |
161 | | &db, &compile_error); |
162 | | } |
163 | | |
164 | 29 | if (err != HS_SUCCESS) [[unlikely]] { |
165 | | /// CompilerError is a unique_ptr, so correct memory free after the exception is thrown. |
166 | 1 | CompilerError error(compile_error); |
167 | | |
168 | 1 | if (error->expression < 0) { // error has nothing to do with the patterns themselves |
169 | 0 | throw doris::Exception(Status::InternalError("Compile regexp expression failed. got {}", |
170 | 0 | error->message)); |
171 | 1 | } else { |
172 | 1 | throw doris::Exception(Status::InvalidArgument( |
173 | 1 | "Compile regexp expression failed. got {}. some expressions may be illegal", |
174 | 1 | error->message)); |
175 | 1 | } |
176 | 1 | } |
177 | | |
178 | | /// We allocate the scratch space only once, then copy it across multiple threads with hs_clone_scratch |
179 | | /// function which is faster than allocating scratch space each time in each thread. |
180 | 28 | hs_scratch_t* scratch = nullptr; |
181 | 28 | err = hs_alloc_scratch(db, &scratch); |
182 | | |
183 | 28 | if (err != HS_SUCCESS) [[unlikely]] { |
184 | 0 | if (err == HS_NOMEM) [[unlikely]] { |
185 | 0 | throw doris::Exception(Status::MemoryAllocFailed( |
186 | 0 | "Allocating memory failed on compiling regexp expressions.")); |
187 | 0 | } else { |
188 | 0 | throw doris::Exception(Status::InvalidArgument( |
189 | 0 | "Compile regexp expression failed with unexpected arguments perhaps")); |
190 | 0 | } |
191 | 0 | } |
192 | | |
193 | 28 | return {db, scratch}; |
194 | 28 | } |
195 | | |
196 | | /// Maps string pattern vectors + edit distance to compiled vectorscan regexps. Uses the same eviction mechanism as the LocalCacheTable for |
197 | | /// re2 patterns. Because vectorscan regexes are overall more heavy-weight (more expensive compilation, regexes can grow up to multiple |
198 | | /// MBs, usage of scratch space), 1. GlobalCacheTable is a global singleton and, as a result, needs locking 2. the pattern compilation is |
199 | | /// done outside GlobalCacheTable's lock, at the cost of another level of locking. |
200 | | struct GlobalCacheTable { |
201 | | constexpr static size_t CACHE_SIZE = 500; /// collision probability |
202 | | |
203 | | struct Bucket { |
204 | | std::vector<String> patterns; /// key |
205 | | std::optional<UInt32> edit_distance; /// key |
206 | | /// The compiled patterns and their state (vectorscan 'database' + scratch space) are wrapped in a shared_ptr. Refcounting guarantees |
207 | | /// that eviction of a pattern does not affect parallel threads still using the pattern. |
208 | | DeferredConstructedRegexpsPtr regexps; /// value |
209 | | }; |
210 | | |
211 | | std::mutex mutex; |
212 | | std::array<Bucket, CACHE_SIZE> known_regexps; |
213 | | |
214 | | static size_t getBucketIndexFor(const std::vector<String> patterns, |
215 | 32 | std::optional<UInt32> edit_distance) { |
216 | 32 | size_t hash = 0; |
217 | 348 | for (const auto& pattern : patterns) { |
218 | 348 | boost::hash_combine(hash, pattern); |
219 | 348 | } |
220 | 32 | boost::hash_combine(hash, edit_distance); |
221 | 32 | return hash % CACHE_SIZE; |
222 | 32 | } |
223 | | }; |
224 | | |
225 | | /// If WithEditDistance is False, edit_distance must be nullopt. Also, we use templates here because each instantiation of function template |
226 | | /// has its own copy of local static variables which must not be the same for different hyperscan compilations. |
227 | | template <bool save_indices, bool WithEditDistance> |
228 | | DeferredConstructedRegexpsPtr getOrSet(const std::vector<StringRef>& patterns, |
229 | 32 | std::optional<UInt32> edit_distance) { |
230 | 32 | static GlobalCacheTable |
231 | 32 | pool; /// Different variables for different pattern parameters, thread-safe in C++11 |
232 | | |
233 | 32 | std::vector<String> str_patterns; |
234 | 32 | str_patterns.reserve(patterns.size()); |
235 | 348 | for (const auto& pattern : patterns) { |
236 | 348 | str_patterns.emplace_back(pattern.to_string()); |
237 | 348 | } |
238 | | |
239 | 32 | size_t bucket_idx = GlobalCacheTable::getBucketIndexFor(str_patterns, edit_distance); |
240 | | |
241 | | /// Lock cache to find compiled regexp for given pattern vector + edit distance. |
242 | 32 | std::lock_guard lock(pool.mutex); |
243 | | |
244 | 32 | GlobalCacheTable::Bucket& bucket = pool.known_regexps[bucket_idx]; |
245 | | |
246 | | /// Pattern compilation is expensive and we don't want to block other threads reading from / inserting into the cache while we hold the |
247 | | /// cache lock during pattern compilation. Therefore, when a cache entry is created or replaced, only set the regexp constructor method |
248 | | /// and compile outside the cache lock. |
249 | | /// Note that the string patterns and the edit distance is passed into the constructor lambda by value, i.e. copied - it is not an |
250 | | /// option to reference the corresponding string patterns / edit distance key in the cache table bucket because the cache entry may |
251 | | /// already be evicted at the time the compilation starts. |
252 | | |
253 | 32 | if (bucket.regexps == nullptr) { |
254 | | /// insert new entry |
255 | 28 | auto deferred_constructed_regexps = |
256 | 28 | std::make_shared<DeferredConstructedRegexps>([str_patterns, edit_distance]() { |
257 | 28 | return constructRegexps<save_indices, WithEditDistance>(str_patterns, |
258 | 28 | edit_distance); |
259 | 28 | }); |
260 | 28 | bucket = {std::move(str_patterns), edit_distance, deferred_constructed_regexps}; |
261 | 28 | } else if (bucket.patterns != str_patterns || bucket.edit_distance != edit_distance) { |
262 | | /// replace existing entry |
263 | 1 | auto deferred_constructed_regexps = |
264 | 1 | std::make_shared<DeferredConstructedRegexps>([str_patterns, edit_distance]() { |
265 | 1 | return constructRegexps<save_indices, WithEditDistance>(str_patterns, |
266 | 1 | edit_distance); |
267 | 1 | }); |
268 | 1 | bucket = {std::move(str_patterns), edit_distance, deferred_constructed_regexps}; |
269 | 1 | } |
270 | | |
271 | 32 | return bucket.regexps; |
272 | 32 | } |
273 | | |
274 | | } // namespace doris::multiregexps |