Coverage Report

Created: 2026-08-03 07:41

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 <netdb.h>
21
22
#include <algorithm>
23
#include <atomic>
24
#include <unordered_set>
25
26
#include "common/config.h"
27
#include "service/backend_options.h"
28
#include "util/network_util.h"
29
30
namespace doris {
31
32
10
DNSCache::DNSCache() {
33
10
    refresh_thread = std::thread(&DNSCache::_refresh_cache, this);
34
10
}
35
36
21
DNSCache::DNSCache(Resolver resolver) : _resolver(std::move(resolver)) {}
37
38
27
DNSCache::~DNSCache() {
39
27
    {
40
27
        std::lock_guard<std::mutex> lk(_cv_mutex);
41
27
        stop_refresh = true;
42
27
    }
43
27
    _cv.notify_all();
44
27
    if (refresh_thread.joinable()) {
45
6
        refresh_thread.join();
46
6
    }
47
27
}
48
49
148k
Status DNSCache::get(const std::string& hostname, std::string* ip) {
50
148k
    bool has_negative_entry = false;
51
148k
    {
52
148k
        std::shared_lock<std::shared_mutex> lock(mutex);
53
148k
        auto it = cache.find(hostname);
54
148k
        if (it != cache.end()) {
55
148k
            *ip = it->second;
56
148k
            return Status::OK();
57
148k
        }
58
24
        auto neg_it = _negative_cache.find(hostname);
59
27
        if (neg_it != _negative_cache.end()) {
60
27
            int32_t ttl = config::dns_cache_negative_ttl_seconds;
61
27
            if (ttl > 0) {
62
26
                auto deadline = neg_it->second + std::chrono::seconds(ttl);
63
26
                if (std::chrono::steady_clock::now() < deadline) {
64
                    // No stack trace: this is an expected steady state, not an anomaly, and
65
                    // it is returned once per caller request for as long as the host stays
66
                    // unresolvable. Capturing a stack here would make Status::Error() log a
67
                    // WARNING per call (and every caller logs status.to_string() again),
68
                    // recreating exactly the be.WARNING flood this cache exists to stop.
69
20
                    return Status::InternalError<false>(
70
20
                            "Hostname {} is in negative DNS cache (recently evicted or "
71
20
                            "unresolvable), skipping resolve",
72
20
                            hostname);
73
20
                }
74
26
            }
75
7
            has_negative_entry = true;
76
7
        }
77
24
    }
78
79
    // If the host was in the negative cache with an expired (or disabled) TTL,
80
    // claim the single-flight retry under unique_lock before the blocking DNS
81
    // call.  Re-arming the eviction_time to now() makes concurrent callers see
82
    // an unexpired entry, bounding retries to one per host per TTL period.
83
7
    if (has_negative_entry) {
84
7
        std::unique_lock<std::shared_mutex> lock(mutex);
85
7
        auto neg_it = _negative_cache.find(hostname);
86
7
        if (neg_it != _negative_cache.end()) {
87
7
            int32_t ttl = config::dns_cache_negative_ttl_seconds;
88
7
            if (ttl <= 0) {
89
1
                _negative_cache.erase(neg_it);
90
6
            } else {
91
6
                auto deadline = neg_it->second + std::chrono::seconds(ttl);
92
6
                if (std::chrono::steady_clock::now() >= deadline) {
93
6
                    neg_it->second = std::chrono::steady_clock::now();
94
6
                } else {
95
                    // Lost the single-flight race; see above for why this carries no stack.
96
0
                    return Status::InternalError<false>(
97
0
                            "Hostname {} is in negative DNS cache (recently evicted or "
98
0
                            "unresolvable), skipping resolve",
99
0
                            hostname);
100
0
                }
101
6
            }
102
7
        }
103
7
    }
104
105
    // First access (or negative TTL expired): resolve and populate the cache.
106
    // Consume the IP returned by _update() directly to avoid a second cache
107
    // lookup — operator[] under a shared_lock would mutate the map and could
108
    // reinsert an empty entry if a concurrent refresh cycle evicted the hostname
109
    // between _update() and here.
110
4
    return _update(hostname, nullptr, ip);
111
4
}
112
113
// Resolve hostname to IP address, similar to Java's DNSCache.resolveHostname.
114
// If resolution fails, falls back to cached IP if available.
115
// Returns the resolved IP, or cached IP on failure, or empty string if no cache available.
116
// *is_fresh (if non-null) is set to true when DNS returned a live result, false
117
// when the IP comes from the stale cached fallback path.
118
390
std::string DNSCache::_resolve_hostname(const std::string& hostname, bool* is_fresh) {
119
    // Get cached IP first (if any)
120
390
    std::string cached_ip;
121
390
    {
122
390
        std::shared_lock<std::shared_mutex> lock(mutex);
123
390
        auto it = cache.find(hostname);
124
390
        if (it != cache.end()) {
125
348
            cached_ip = it->second;
126
348
        }
127
390
    }
128
129
    // Try to resolve hostname
130
390
    std::string resolved_ip;
131
390
    int gai_err = 0;
132
390
    Status status =
133
390
            _resolver ? _resolver(hostname, resolved_ip, BackendOptions::is_bind_ipv6(), &gai_err)
134
390
                      : hostname_to_ip(hostname, resolved_ip, BackendOptions::is_bind_ipv6(),
135
261
                                       &gai_err);
136
137
390
    if (!status.ok() || resolved_ip.empty()) {
138
109
        if (is_fresh) {
139
108
            *is_fresh = false;
140
108
        }
141
        // EAI_NONAME is the resolver authoritatively answering "this name does not exist",
142
        // which is the only evidence that a backend is really gone. Everything else
143
        // (EAI_AGAIN = resolver unreachable or timed out, EAI_SYSTEM, EAI_FAIL, ...) means
144
        // DNS itself is unhealthy while the host is most likely still up at its last known
145
        // address, so those failures must never lead to eviction — otherwise a resolver
146
        // outage would wipe every hostname at once and turn a DNS incident into a
147
        // cluster-wide RPC outage.
148
109
        const bool authoritative = (gai_err == EAI_NONAME);
149
109
        if (!cached_ip.empty()) {
150
            // Only track failure counts for hosts that are currently in the cache.
151
            // Hosts that were never cached or have already been evicted are not
152
            // tracked, which prevents unbounded growth of failure_count.
153
96
            uint32_t failures = 0;
154
96
            {
155
96
                std::unique_lock<std::shared_mutex> lock(mutex);
156
                // Re-check that the host is still cached under the unique_lock:
157
                // it may have been evicted by the refresh thread between our
158
                // earlier shared_lock read of cached_ip and now (hostname_to_ip
159
                // can block for seconds on DNS timeout, widening the window).
160
                // Skipping the bump here preserves keys(failure_count) ⊆ keys(cache).
161
96
                if (cache.find(hostname) != cache.end()) {
162
94
                    FailureState& state = failure_count[hostname];
163
                    // The counter tracks failures of any kind so the throttled log below
164
                    // stays informative during a resolver outage; only `last_authoritative`
165
                    // gates eviction.
166
94
                    failures = ++state.count;
167
94
                    state.last_authoritative = authoritative;
168
94
                }
169
96
            }
170
            // Throttle the log: only every N failures or the first failure.
171
96
            if (failures > 0) {
172
94
                int32_t every_n = std::max(1, config::dns_cache_log_every_n_failures);
173
94
                if (failures == 1 || failures % static_cast<uint32_t>(every_n) == 0) {
174
21
                    LOG(WARNING) << "Failed to resolve hostname " << hostname
175
21
                                 << " (consecutive failures: " << failures << ", error: "
176
21
                                 << (authoritative ? "NXDOMAIN, host is gone"
177
21
                                                   : "transient, DNS unhealthy")
178
21
                                 << "), use cached ip: " << cached_ip;
179
21
                }
180
94
            }
181
96
            return cached_ip;
182
96
        } else {
183
            // Throttle to avoid flooding be.WARNING when callers repeatedly
184
            // query an evicted or never-resolvable hostname.  This branch
185
            // deliberately does not maintain a per-hostname counter (that
186
            // would break the keys(failure_count) ⊆ keys(cache) invariant),
187
            // so the throttle is a coarse global rate limit shared across
188
            // all hostnames hitting this code path.
189
13
            static std::atomic<uint64_t> no_cache_warn_counter {0};
190
13
            uint64_t n = no_cache_warn_counter.fetch_add(1, std::memory_order_relaxed) + 1;
191
13
            int32_t every_n = std::max(1, config::dns_cache_log_every_n_failures);
192
13
            if (n == 1 || n % static_cast<uint64_t>(every_n) == 0) {
193
2
                LOG(WARNING) << "Failed to resolve hostname " << hostname
194
2
                             << ", no cached ip available";
195
2
            }
196
13
            return "";
197
13
        }
198
109
    }
199
200
    // Resolution succeeded - clear failure counter for this hostname.
201
281
    if (is_fresh) {
202
281
        *is_fresh = true;
203
281
    }
204
281
    {
205
281
        std::unique_lock<std::shared_mutex> lock(mutex);
206
281
        failure_count.erase(hostname);
207
281
    }
208
281
    return resolved_ip;
209
390
}
210
211
13
void DNSCache::_evict_locked(const std::string& hostname) {
212
13
    cache.erase(hostname);
213
13
    failure_count.erase(hostname);
214
13
    int32_t ttl = config::dns_cache_negative_ttl_seconds;
215
13
    if (ttl > 0) {
216
13
        _negative_cache[hostname] = std::chrono::steady_clock::now();
217
13
    }
218
13
}
219
220
13
void DNSCache::_remember_unresolvable(const std::string& hostname) {
221
13
    int32_t ttl = config::dns_cache_negative_ttl_seconds;
222
13
    if (ttl <= 0) {
223
8
        return;
224
8
    }
225
5
    std::unique_lock<std::shared_mutex> lock(mutex);
226
    // try_emplace, not operator[]: get()'s single-flight path may have just re-armed this
227
    // tombstone to now(); overwriting it would be harmless there but would also let two
228
    // callers racing on the same host each reset the deadline, loosening the rate limit.
229
5
    _negative_cache.try_emplace(hostname, std::chrono::steady_clock::now());
230
5
}
231
232
2
void DNSCache::_erase(const std::string& hostname) {
233
2
    std::unique_lock<std::shared_mutex> lock(mutex);
234
2
    _evict_locked(hostname);
235
2
}
236
237
13
bool DNSCache::_erase_if_still_failing(const std::string& hostname, uint32_t threshold) {
238
13
    std::unique_lock<std::shared_mutex> lock(mutex);
239
13
    auto fc_it = failure_count.find(hostname);
240
13
    if (fc_it == failure_count.end() || fc_it->second.count < threshold ||
241
13
        !fc_it->second.last_authoritative) {
242
        // Either a concurrent successful resolution cleared or reset the counter between
243
        // _update() returning and this call — do not erase a now-healthy entry — or the
244
        // most recent failure was transient (resolver unreachable) rather than an
245
        // authoritative NXDOMAIN, in which case the host is probably still alive.
246
2
        return false;
247
2
    }
248
11
    _evict_locked(hostname);
249
11
    return true;
250
13
}
251
252
Status DNSCache::_update(const std::string& hostname, FailureState* out_state,
253
389
                         std::string* out_ip) {
254
389
    bool is_fresh = false;
255
389
    std::string real_ip = _resolve_hostname(hostname, &is_fresh);
256
389
    if (real_ip.empty()) {
257
13
        if (out_state) {
258
0
            *out_state = FailureState {};
259
0
        }
260
13
        if (out_ip) {
261
13
            out_ip->clear();
262
13
        }
263
        // The host could not be resolved and has no cached IP to fall back on, so it never
264
        // entered `cache` and will therefore never reach the eviction path that writes a
265
        // tombstone. Record one here as well: otherwise every single get() on a hostname
266
        // that has never resolved (a typo in the FE, a backend registered before its DNS
267
        // record propagated) pays a full blocking getaddrinfo, and many of those calls run
268
        // on bthreads where long blocking is especially costly.
269
13
        _remember_unresolvable(hostname);
270
13
        return Status::InternalError<false>(
271
13
                "Failed to resolve hostname {} and no cached ip available", hostname);
272
13
    }
273
274
376
    std::unique_lock<std::shared_mutex> lock(mutex);
275
    // _resolve_hostname may have captured a stale cached_ip before a concurrent
276
    // eviction completed.  If the host is now in the negative cache we must not
277
    // reinsert the stale IP: that would silently undo the eviction and clear the
278
    // tombstone, defeating the whole purpose of eviction.  Only a fresh DNS
279
    // result (is_fresh == true, meaning DNS actually resolved) may override an
280
    // eviction — which indicates the backend is genuinely back.
281
376
    if (!is_fresh && _negative_cache.count(hostname)) {
282
1
        if (out_state) {
283
1
            *out_state = FailureState {};
284
1
        }
285
1
        if (out_ip) {
286
0
            out_ip->clear();
287
0
        }
288
        // No stack trace: like the negative-cache hits in get(), this is an expected
289
        // outcome that can repeat on every request while the host stays evicted.
290
1
        return Status::InternalError<false>(
291
1
                "Hostname {} was concurrently evicted; stale-fallback not reinserted", hostname);
292
1
    }
293
375
    auto it = cache.find(hostname);
294
375
    if (it == cache.end() || it->second != real_ip) {
295
29
        cache[hostname] = real_ip;
296
29
        LOG(INFO) << "update hostname " << hostname << "'s ip to " << real_ip;
297
29
    }
298
    // DNS resolved successfully — remove any negative cache tombstone so
299
    // subsequent get() calls go straight to the main cache.
300
375
    _negative_cache.erase(hostname);
301
375
    if (out_ip) {
302
29
        *out_ip = real_ip;
303
29
    }
304
    // Read failure_count under the same lock we already hold, so _refresh_once
305
    // does not need a second lock acquisition to decide on eviction.
306
375
    if (out_state) {
307
346
        auto fc_it = failure_count.find(hostname);
308
346
        *out_state = fc_it != failure_count.end() ? fc_it->second : FailureState {};
309
346
    }
310
375
    return Status::OK();
311
376
}
312
313
353
void DNSCache::_refresh_once() {
314
353
    std::unordered_set<std::string> keys;
315
353
    {
316
353
        std::shared_lock<std::shared_mutex> lock(mutex);
317
353
        std::transform(cache.begin(), cache.end(), std::inserter(keys, keys.end()),
318
353
                       [](const auto& pair) { return pair.first; });
319
353
    }
320
353
    for (auto& key : keys) {
321
        // Each _update() below performs a blocking getaddrinfo, so one cycle over a
322
        // cluster whose DNS is timing out can take minutes. Without this check the
323
        // destructor's join() would be held up for exactly that long, which would
324
        // undo the point of making the wait itself interruptible.
325
348
        if (stop_refresh.load(std::memory_order_acquire)) {
326
1
            break;
327
1
        }
328
347
        FailureState state;
329
347
        Status st = _update(key, &state);
330
347
        if (!st.ok()) {
331
            // _update returns an error either when _resolve_hostname returns ""
332
            // (no fallback IP) or when a stale fallback was suppressed because
333
            // the host was concurrently evicted.  Either way, log and move on;
334
            // the threshold check below handles the normal eviction path.
335
1
            LOG(WARNING) << "Failed to update DNS cache for hostname " << key << ": "
336
1
                         << st.to_string();
337
1
        }
338
        // Evict hostnames that have failed to resolve for too long.
339
        // This avoids two pathological symptoms after a backend is dropped
340
        // from the cluster and its DNS record is removed:
341
        //   1) be.WARNING gets flooded with `failed to get ip from host`.
342
        //   2) brpc keeps re-using the stale IP from cache, producing
343
        //      `Fail to wait EPOLLOUT ... Connection timed out`.
344
        // `last_authoritative` keeps this restricted to hosts the resolver has positively
345
        // reported as non-existent; a DNS outage yields transient errors for every host at
346
        // once and must leave the cache (and the stale-IP fallback) intact.
347
347
        int32_t threshold = config::dns_cache_max_consecutive_failures;
348
347
        if (threshold > 0 && state.last_authoritative &&
349
347
            state.count >= static_cast<uint32_t>(threshold)) {
350
            // Re-read failure_count under the mutex that also performs the erase
351
            // to fence any concurrent success that cleared the counter between
352
            // _update() returning and this point.
353
11
            if (_erase_if_still_failing(key, static_cast<uint32_t>(threshold))) {
354
11
                LOG(WARNING) << "Evicting hostname " << key << " from DNS cache after "
355
11
                             << state.count << " consecutive resolution failures";
356
11
            }
357
11
        }
358
347
    }
359
353
}
360
361
10
void DNSCache::_refresh_cache() {
362
275
    while (!stop_refresh) {
363
265
        {
364
265
            std::unique_lock<std::mutex> lk(_cv_mutex);
365
            // Wake up either after 1 minute or when the destructor signals stop.
366
526
            _cv.wait_for(lk, std::chrono::minutes(1), [this] { return stop_refresh.load(); });
367
265
        }
368
265
        if (!stop_refresh) {
369
255
            _refresh_once();
370
255
        }
371
265
    }
372
10
}
373
374
} // end of namespace doris