Coverage Report

Created: 2026-08-27 15:13

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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