Coverage Report

Created: 2026-07-27 11:22

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