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 | | #pragma once |
19 | | |
20 | | #include <atomic> |
21 | | #include <chrono> |
22 | | #include <condition_variable> |
23 | | #include <cstdint> |
24 | | #include <functional> |
25 | | #include <iostream> |
26 | | #include <mutex> |
27 | | #include <shared_mutex> |
28 | | #include <string> |
29 | | #include <thread> |
30 | | #include <unordered_map> |
31 | | |
32 | | #include "common/status.h" |
33 | | |
34 | | namespace doris { |
35 | | |
36 | | // Same as |
37 | | // fe/fe-core/src/main/java/org/apache/doris/common/DNSCache.java |
38 | | class DNSCache { |
39 | | public: |
40 | | // (hostname, out_ip, is_ipv6, out_gai_err) -> Status. |
41 | | // out_gai_err receives the raw getaddrinfo() return code so the cache can tell an |
42 | | // authoritative "no such host" (EAI_NONAME) apart from a transient resolver failure. |
43 | | using Resolver = std::function<Status(const std::string&, std::string&, bool, int*)>; |
44 | | |
45 | | // Per-hostname failure bookkeeping. Only tracked for hostnames currently present in |
46 | | // `cache`, which keeps the map bounded (invariant: keys(failure_count) ⊆ keys(cache)). |
47 | | struct FailureState { |
48 | | // Consecutive resolution failures of any kind. Drives the log throttle. |
49 | | uint32_t count = 0; |
50 | | // Whether the most recent failure was an authoritative NXDOMAIN. Eviction requires |
51 | | // this to be true, so that a DNS-server outage (which yields EAI_AGAIN for every |
52 | | // hostname at once) degrades to the stale cached IP instead of wiping the cache. |
53 | | bool last_authoritative = false; |
54 | | }; |
55 | | |
56 | | DNSCache(); |
57 | | |
58 | | // Test-only constructor: uses a custom resolver and does NOT start the |
59 | | // background refresh thread. Call refresh_for_test() to drive one cycle. |
60 | | explicit DNSCache(Resolver resolver); |
61 | | |
62 | | ~DNSCache(); |
63 | | |
64 | | // get ip by hostname |
65 | | Status get(const std::string& hostname, std::string* ip); |
66 | | |
67 | | private: |
68 | | // Resolve hostname to IP address. |
69 | | // If resolution fails, falls back to cached IP if available. |
70 | | // Returns the resolved IP, or cached IP on failure, or empty string if no cache available. |
71 | | // *is_fresh is set to true when DNS returned a live result, false when the |
72 | | // returned IP is the stale cached fallback from a failed lookup. |
73 | | std::string _resolve_hostname(const std::string& hostname, bool* is_fresh = nullptr); |
74 | | |
75 | | // update the ip of hostname in cache; out_state (if non-null) is set to the |
76 | | // current failure bookkeeping read under the same lock; out_ip (if non-null) |
77 | | // receives the resolved IP so callers can use it without a second cache lookup |
78 | | // (avoids operator[] mutation under shared_lock). |
79 | | Status _update(const std::string& hostname, FailureState* out_state = nullptr, |
80 | | std::string* out_ip = nullptr); |
81 | | |
82 | | // erase a hostname from cache unconditionally (with unique_lock) |
83 | | void _erase(const std::string& hostname); |
84 | | |
85 | | // Erase a hostname from cache only if it still meets the eviction criteria: |
86 | | // failure_count >= threshold AND the most recent failure was authoritative. |
87 | | // Re-reads the live state under the same lock that performs the erase, so a |
88 | | // concurrent successful resolution that cleared it is not lost. |
89 | | // Returns true if the host was erased, false otherwise. |
90 | | bool _erase_if_still_failing(const std::string& hostname, uint32_t threshold); |
91 | | |
92 | | // Drop a hostname from the cache and write a negative-cache tombstone (subject to |
93 | | // dns_cache_negative_ttl_seconds). Caller must already hold a unique_lock on `mutex`. |
94 | | void _evict_locked(const std::string& hostname); |
95 | | |
96 | | // Record a tombstone for a hostname that could not be resolved and has no cached IP, |
97 | | // so repeated get() calls do not each pay a full blocking getaddrinfo. Uses |
98 | | // try_emplace so an entry just re-armed by get()'s single-flight path is preserved. |
99 | | void _remember_unresolvable(const std::string& hostname); |
100 | | |
101 | | // one refresh cycle: update every cached hostname and evict if needed |
102 | | void _refresh_once(); |
103 | | |
104 | | // a function for refresh daemon thread |
105 | | // update cache at fix internal |
106 | | void _refresh_cache(); |
107 | | |
108 | | // ── test helpers (accessible via friend class DNSCacheTest) ────────────── |
109 | 105 | size_t size_for_test() const { |
110 | 105 | std::shared_lock<std::shared_mutex> lock(mutex); |
111 | 105 | return cache.size(); |
112 | 105 | } |
113 | | |
114 | 22 | size_t negative_cache_size_for_test() const { |
115 | 22 | std::shared_lock<std::shared_mutex> lock(mutex); |
116 | 22 | return _negative_cache.size(); |
117 | 22 | } |
118 | | |
119 | 8 | uint32_t failure_count_for_test(const std::string& hostname) const { |
120 | 8 | std::shared_lock<std::shared_mutex> lock(mutex); |
121 | 8 | auto it = failure_count.find(hostname); |
122 | 8 | return it != failure_count.end() ? it->second.count : 0; |
123 | 8 | } |
124 | | |
125 | | // Run one refresh cycle synchronously (no sleep). Only meaningful when |
126 | | // the object was constructed with the test constructor (no background thread). |
127 | 98 | void refresh_for_test() { _refresh_once(); } |
128 | | |
129 | | // Backdate all negative-cache entries far into the past so they appear |
130 | | // expired without removing them. Use this to simulate TTL expiry in tests |
131 | | // that need the re-arm path in get() to trigger. |
132 | | // Backdating relative to now() (rather than to steady_clock's epoch, which is |
133 | | // typically boot time) keeps this correct on a freshly booted host. |
134 | 6 | void _expire_negative_cache_for_test() { |
135 | 6 | std::unique_lock<std::shared_mutex> lock(mutex); |
136 | 6 | auto backdated = std::chrono::steady_clock::now() - std::chrono::hours(24 * 365); |
137 | 6 | for (auto& [k, v] : _negative_cache) { |
138 | 6 | v = backdated; |
139 | 6 | } |
140 | 6 | } |
141 | | |
142 | | friend class DNSCacheTest; |
143 | | |
144 | | private: |
145 | | Resolver _resolver; // null → use global hostname_to_ip |
146 | | // hostname -> ip |
147 | | std::unordered_map<std::string, std::string> cache; |
148 | | // hostname -> consecutive resolution failure bookkeeping |
149 | | std::unordered_map<std::string, FailureState> failure_count; |
150 | | // hostname -> eviction timestamp; effective deadline is computed as |
151 | | // eviction_time + dns_cache_negative_ttl_seconds to honor live config changes. |
152 | | std::unordered_map<std::string, std::chrono::steady_clock::time_point> _negative_cache; |
153 | | mutable std::shared_mutex mutex; |
154 | | std::thread refresh_thread; |
155 | | // Protects stop_refresh and signals _refresh_cache to wake early on destroy. |
156 | | std::mutex _cv_mutex; |
157 | | std::condition_variable _cv; |
158 | | std::atomic<bool> stop_refresh {false}; |
159 | | }; |
160 | | |
161 | | } // end of namespace doris |