Coverage Report

Created: 2026-07-30 13:04

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/util/dns_cache.cpp
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
18
#include "util/dns_cache.h"
19
20
#include <algorithm>
21
#include <atomic>
22
#include <unordered_set>
23
24
#include "common/config.h"
25
#include "service/backend_options.h"
26
#include "util/network_util.h"
27
28
namespace doris {
29
30
4
DNSCache::DNSCache() {
31
4
    refresh_thread = std::thread(&DNSCache::_refresh_cache, this);
32
4
}
33
34
14
DNSCache::DNSCache(Resolver resolver) : _resolver(std::move(resolver)) {}
35
36
18
DNSCache::~DNSCache() {
37
18
    {
38
18
        std::lock_guard<std::mutex> lk(_cv_mutex);
39
18
        stop_refresh = true;
40
18
    }
41
18
    _cv.notify_all();
42
18
    if (refresh_thread.joinable()) {
43
4
        refresh_thread.join();
44
4
    }
45
18
}
46
47
49
Status DNSCache::get(const std::string& hostname, std::string* ip) {
48
49
    bool has_negative_entry = false;
49
49
    {
50
49
        std::shared_lock<std::shared_mutex> lock(mutex);
51
49
        auto it = cache.find(hostname);
52
49
        if (it != cache.end()) {
53
8
            *ip = it->second;
54
8
            return Status::OK();
55
8
        }
56
41
        auto neg_it = _negative_cache.find(hostname);
57
41
        if (neg_it != _negative_cache.end()) {
58
21
            int32_t ttl = config::dns_cache_negative_ttl_seconds;
59
21
            if (ttl > 0) {
60
20
                auto deadline = neg_it->second + std::chrono::seconds(ttl);
61
20
                if (std::chrono::steady_clock::now() < deadline) {
62
15
                    return Status::InternalError(
63
15
                            "Hostname {} is in negative DNS cache (recently evicted), "
64
15
                            "skipping resolve",
65
15
                            hostname);
66
15
                }
67
20
            }
68
6
            has_negative_entry = true;
69
6
        }
70
41
    }
71
72
    // If the host was in the negative cache with an expired (or disabled) TTL,
73
    // claim the single-flight retry under unique_lock before the blocking DNS
74
    // call.  Re-arming the eviction_time to now() makes concurrent callers see
75
    // an unexpired entry, bounding retries to one per host per TTL period.
76
26
    if (has_negative_entry) {
77
6
        std::unique_lock<std::shared_mutex> lock(mutex);
78
6
        auto neg_it = _negative_cache.find(hostname);
79
6
        if (neg_it != _negative_cache.end()) {
80
6
            int32_t ttl = config::dns_cache_negative_ttl_seconds;
81
6
            if (ttl <= 0) {
82
1
                _negative_cache.erase(neg_it);
83
5
            } else {
84
5
                auto deadline = neg_it->second + std::chrono::seconds(ttl);
85
5
                if (std::chrono::steady_clock::now() >= deadline) {
86
5
                    neg_it->second = std::chrono::steady_clock::now();
87
5
                } else {
88
0
                    return Status::InternalError(
89
0
                            "Hostname {} is in negative DNS cache (recently evicted), "
90
0
                            "skipping resolve",
91
0
                            hostname);
92
0
                }
93
5
            }
94
6
        }
95
6
    }
96
97
    // First access (or negative TTL expired): resolve and populate the cache.
98
    // Consume the IP returned by _update() directly to avoid a second cache
99
    // lookup — operator[] under a shared_lock would mutate the map and could
100
    // reinsert an empty entry if a concurrent refresh cycle evicted the hostname
101
    // between _update() and here.
102
26
    return _update(hostname, nullptr, ip);
103
26
}
104
105
// Resolve hostname to IP address, similar to Java's DNSCache.resolveHostname.
106
// If resolution fails, falls back to cached IP if available.
107
// Returns the resolved IP, or cached IP on failure, or empty string if no cache available.
108
// *is_fresh (if non-null) is set to true when DNS returned a live result, false
109
// when the IP comes from the stale cached fallback path.
110
47
std::string DNSCache::_resolve_hostname(const std::string& hostname, bool* is_fresh) {
111
    // Get cached IP first (if any)
112
47
    std::string cached_ip;
113
47
    {
114
47
        std::shared_lock<std::shared_mutex> lock(mutex);
115
47
        auto it = cache.find(hostname);
116
47
        if (it != cache.end()) {
117
21
            cached_ip = it->second;
118
21
        }
119
47
    }
120
121
    // Try to resolve hostname
122
47
    std::string resolved_ip;
123
47
    Status status = _resolver
124
47
                            ? _resolver(hostname, resolved_ip, BackendOptions::is_bind_ipv6())
125
47
                            : hostname_to_ip(hostname, resolved_ip, BackendOptions::is_bind_ipv6());
126
127
47
    if (!status.ok() || resolved_ip.empty()) {
128
30
        if (is_fresh) *is_fresh = false;
129
30
        if (!cached_ip.empty()) {
130
            // Only track failure counts for hosts that are currently in the cache.
131
            // Hosts that were never cached or have already been evicted are not
132
            // tracked, which prevents unbounded growth of failure_count.
133
20
            uint32_t failures = 0;
134
20
            {
135
20
                std::unique_lock<std::shared_mutex> lock(mutex);
136
                // Re-check that the host is still cached under the unique_lock:
137
                // it may have been evicted by the refresh thread between our
138
                // earlier shared_lock read of cached_ip and now (hostname_to_ip
139
                // can block for seconds on DNS timeout, widening the window).
140
                // Skipping the bump here preserves keys(failure_count) ⊆ keys(cache).
141
20
                if (cache.find(hostname) != cache.end()) {
142
18
                    failures = ++failure_count[hostname];
143
18
                }
144
20
            }
145
            // Throttle the log: only every N failures or the first failure.
146
20
            if (failures > 0) {
147
18
                int32_t every_n = std::max(1, config::dns_cache_log_every_n_failures);
148
18
                if (failures == 1 || failures % static_cast<uint32_t>(every_n) == 0) {
149
11
                    LOG(WARNING) << "Failed to resolve hostname " << hostname
150
11
                                 << " (consecutive failures: " << failures
151
11
                                 << "), use cached ip: " << cached_ip;
152
11
                }
153
18
            }
154
20
            return cached_ip;
155
20
        } else {
156
            // Throttle to avoid flooding be.WARNING when callers repeatedly
157
            // query an evicted or never-resolvable hostname.  This branch
158
            // deliberately does not maintain a per-hostname counter (that
159
            // would break the keys(failure_count) ⊆ keys(cache) invariant),
160
            // so the throttle is a coarse global rate limit shared across
161
            // all hostnames hitting this code path.
162
10
            static std::atomic<uint64_t> no_cache_warn_counter {0};
163
10
            uint64_t n = no_cache_warn_counter.fetch_add(1, std::memory_order_relaxed) + 1;
164
10
            int32_t every_n = std::max(1, config::dns_cache_log_every_n_failures);
165
10
            if (n == 1 || n % static_cast<uint64_t>(every_n) == 0) {
166
2
                LOG(WARNING) << "Failed to resolve hostname " << hostname
167
2
                             << ", no cached ip available";
168
2
            }
169
10
            return "";
170
10
        }
171
30
    }
172
173
    // Resolution succeeded - clear failure counter for this hostname.
174
17
    if (is_fresh) *is_fresh = true;
175
17
    {
176
17
        std::unique_lock<std::shared_mutex> lock(mutex);
177
17
        failure_count.erase(hostname);
178
17
    }
179
17
    return resolved_ip;
180
47
}
181
182
2
void DNSCache::_erase(const std::string& hostname) {
183
2
    std::unique_lock<std::shared_mutex> lock(mutex);
184
2
    cache.erase(hostname);
185
2
    failure_count.erase(hostname);
186
2
    int32_t ttl = config::dns_cache_negative_ttl_seconds;
187
2
    if (ttl > 0) {
188
2
        _negative_cache[hostname] = std::chrono::steady_clock::now();
189
2
    }
190
2
}
191
192
11
bool DNSCache::_erase_if_still_failing(const std::string& hostname, uint32_t threshold) {
193
11
    std::unique_lock<std::shared_mutex> lock(mutex);
194
11
    auto fc_it = failure_count.find(hostname);
195
11
    if (fc_it == failure_count.end() || fc_it->second < threshold) {
196
        // A concurrent successful resolution cleared or reset the counter between
197
        // _update() returning and this call — do not erase a now-healthy entry.
198
1
        return false;
199
1
    }
200
10
    cache.erase(hostname);
201
10
    failure_count.erase(hostname);
202
10
    int32_t ttl = config::dns_cache_negative_ttl_seconds;
203
10
    if (ttl > 0) {
204
10
        _negative_cache[hostname] = std::chrono::steady_clock::now();
205
10
    }
206
10
    return true;
207
11
}
208
209
46
Status DNSCache::_update(const std::string& hostname, uint32_t* out_failures, std::string* out_ip) {
210
46
    bool is_fresh = false;
211
46
    std::string real_ip = _resolve_hostname(hostname, &is_fresh);
212
46
    if (real_ip.empty()) {
213
10
        if (out_failures) *out_failures = 0;
214
10
        if (out_ip) out_ip->clear();
215
10
        return Status::InternalError("Failed to resolve hostname {} and no cached ip available",
216
10
                                     hostname);
217
10
    }
218
219
36
    std::unique_lock<std::shared_mutex> lock(mutex);
220
    // _resolve_hostname may have captured a stale cached_ip before a concurrent
221
    // eviction completed.  If the host is now in the negative cache we must not
222
    // reinsert the stale IP: that would silently undo the eviction and clear the
223
    // tombstone, defeating the whole purpose of eviction.  Only a fresh DNS
224
    // result (is_fresh == true, meaning DNS actually resolved) may override an
225
    // eviction — which indicates the backend is genuinely back.
226
36
    if (!is_fresh && _negative_cache.count(hostname)) {
227
1
        if (out_failures) *out_failures = 0;
228
1
        if (out_ip) out_ip->clear();
229
1
        return Status::InternalError(
230
1
                "Hostname {} was concurrently evicted; stale-fallback not reinserted", hostname);
231
1
    }
232
35
    auto it = cache.find(hostname);
233
35
    if (it == cache.end() || it->second != real_ip) {
234
16
        cache[hostname] = real_ip;
235
16
        LOG(INFO) << "update hostname " << hostname << "'s ip to " << real_ip;
236
16
    }
237
    // DNS resolved successfully — remove any negative cache tombstone so
238
    // subsequent get() calls go straight to the main cache.
239
35
    _negative_cache.erase(hostname);
240
35
    if (out_ip) *out_ip = real_ip;
241
    // Read failure_count under the same lock we already hold, so _refresh_once
242
    // does not need a second lock acquisition to decide on eviction.
243
35
    if (out_failures) {
244
19
        auto fc_it = failure_count.find(hostname);
245
19
        *out_failures = fc_it != failure_count.end() ? fc_it->second : 0;
246
19
    }
247
35
    return Status::OK();
248
36
}
249
250
21
void DNSCache::_refresh_once() {
251
21
    std::unordered_set<std::string> keys;
252
21
    {
253
21
        std::shared_lock<std::shared_mutex> lock(mutex);
254
21
        std::transform(cache.begin(), cache.end(), std::inserter(keys, keys.end()),
255
21
                       [](const auto& pair) { return pair.first; });
256
21
    }
257
21
    for (auto& key : keys) {
258
20
        uint32_t failures = 0;
259
20
        Status st = _update(key, &failures);
260
20
        if (!st.ok()) {
261
            // _update returns an error either when _resolve_hostname returns ""
262
            // (no fallback IP) or when a stale fallback was suppressed because
263
            // the host was concurrently evicted.  Either way, log and move on;
264
            // the threshold check below handles the normal eviction path.
265
1
            LOG(WARNING) << "Failed to update DNS cache for hostname " << key << ": "
266
1
                         << st.to_string();
267
1
        }
268
        // Evict hostnames that have failed to resolve for too long.
269
        // This avoids two pathological symptoms after a backend is dropped
270
        // from the cluster and its DNS record is removed:
271
        //   1) be.WARNING gets flooded with `failed to get ip from host`.
272
        //   2) brpc keeps re-using the stale IP from cache, producing
273
        //      `Fail to wait EPOLLOUT ... Connection timed out`.
274
20
        int32_t threshold = config::dns_cache_max_consecutive_failures;
275
20
        if (threshold > 0 && failures >= static_cast<uint32_t>(threshold)) {
276
            // Re-read failure_count under the mutex that also performs the erase
277
            // to fence any concurrent success that cleared the counter between
278
            // _update() returning and this point.
279
10
            if (_erase_if_still_failing(key, static_cast<uint32_t>(threshold))) {
280
10
                LOG(WARNING) << "Evicting hostname " << key << " from DNS cache after " << failures
281
10
                             << " consecutive resolution failures";
282
10
            }
283
10
        }
284
20
    }
285
21
}
286
287
4
void DNSCache::_refresh_cache() {
288
8
    while (!stop_refresh) {
289
4
        {
290
4
            std::unique_lock<std::mutex> lk(_cv_mutex);
291
            // Wake up either after 1 minute or when the destructor signals stop.
292
8
            _cv.wait_for(lk, std::chrono::minutes(1), [this] { return stop_refresh.load(); });
293
4
        }
294
4
        if (!stop_refresh) {
295
0
            _refresh_once();
296
0
        }
297
4
    }
298
4
}
299
300
} // end of namespace doris