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