View Javadoc
1   /*
2    * Copyright 2016 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package io.netty.resolver.dns;
17  
18  import io.netty.channel.EventLoop;
19  import io.netty.handler.codec.dns.DnsRecord;
20  import io.netty.util.concurrent.ScheduledFuture;
21  import io.netty.util.internal.PlatformDependent;
22  import io.netty.util.internal.UnstableApi;
23  
24  import java.net.InetAddress;
25  import java.util.ArrayList;
26  import java.util.Collections;
27  import java.util.Iterator;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.concurrent.ConcurrentMap;
31  import java.util.concurrent.TimeUnit;
32  import java.util.concurrent.atomic.AtomicReference;
33  
34  import static io.netty.util.internal.ObjectUtil.checkNotNull;
35  import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
36  
37  /**
38   * Default implementation of {@link DnsCache}, backed by a {@link ConcurrentMap}.
39   * If any additional {@link DnsRecord} is used, no caching takes place.
40   */
41  @UnstableApi
42  public class DefaultDnsCache implements DnsCache {
43  
44      private final ConcurrentMap<String, Entries> resolveCache = PlatformDependent.newConcurrentHashMap();
45  
46      // Two years are supported by all our EventLoop implementations and so safe to use as maximum.
47      // See also: https://github.com/netty/netty/commit/b47fb817991b42ec8808c7d26538f3f2464e1fa6
48      private static final int MAX_SUPPORTED_TTL_SECS = (int) TimeUnit.DAYS.toSeconds(365 * 2);
49      private final int minTtl;
50      private final int maxTtl;
51      private final int negativeTtl;
52  
53      /**
54       * Create a cache that respects the TTL returned by the DNS server
55       * and doesn't cache negative responses.
56       */
57      public DefaultDnsCache() {
58          this(0, MAX_SUPPORTED_TTL_SECS, 0);
59      }
60  
61      /**
62       * Create a cache.
63       * @param minTtl the minimum TTL
64       * @param maxTtl the maximum TTL
65       * @param negativeTtl the TTL for failed queries
66       */
67      public DefaultDnsCache(int minTtl, int maxTtl, int negativeTtl) {
68          this.minTtl = Math.min(MAX_SUPPORTED_TTL_SECS, checkPositiveOrZero(minTtl, "minTtl"));
69          this.maxTtl = Math.min(MAX_SUPPORTED_TTL_SECS, checkPositiveOrZero(maxTtl, "maxTtl"));
70          if (minTtl > maxTtl) {
71              throw new IllegalArgumentException(
72                      "minTtl: " + minTtl + ", maxTtl: " + maxTtl + " (expected: 0 <= minTtl <= maxTtl)");
73          }
74          this.negativeTtl = checkPositiveOrZero(negativeTtl, "negativeTtl");
75      }
76  
77      /**
78       * Returns the minimum TTL of the cached DNS resource records (in seconds).
79       *
80       * @see #maxTtl()
81       */
82      public int minTtl() {
83          return minTtl;
84      }
85  
86      /**
87       * Returns the maximum TTL of the cached DNS resource records (in seconds).
88       *
89       * @see #minTtl()
90       */
91      public int maxTtl() {
92          return maxTtl;
93      }
94  
95      /**
96       * Returns the TTL of the cache for the failed DNS queries (in seconds). The default value is {@code 0}, which
97       * disables the cache for negative results.
98       */
99      public int negativeTtl() {
100         return negativeTtl;
101     }
102 
103     @Override
104     public void clear() {
105         while (!resolveCache.isEmpty()) {
106             for (Iterator<Map.Entry<String, Entries>> i = resolveCache.entrySet().iterator(); i.hasNext();) {
107                 Map.Entry<String, Entries> e = i.next();
108                 i.remove();
109 
110                 e.getValue().clearAndCancel();
111             }
112         }
113     }
114 
115     @Override
116     public boolean clear(String hostname) {
117         checkNotNull(hostname, "hostname");
118         Entries entries = resolveCache.remove(hostname);
119         return entries != null && entries.clearAndCancel();
120     }
121 
122     private static boolean emptyAdditionals(DnsRecord[] additionals) {
123         return additionals == null || additionals.length == 0;
124     }
125 
126     @Override
127     public List<? extends DnsCacheEntry> get(String hostname, DnsRecord[] additionals) {
128         checkNotNull(hostname, "hostname");
129         if (!emptyAdditionals(additionals)) {
130             return Collections.<DnsCacheEntry>emptyList();
131         }
132 
133         Entries entries = resolveCache.get(hostname);
134         return entries == null ? null : entries.get();
135     }
136 
137     @Override
138     public DnsCacheEntry cache(String hostname, DnsRecord[] additionals,
139                                InetAddress address, long originalTtl, EventLoop loop) {
140         checkNotNull(hostname, "hostname");
141         checkNotNull(address, "address");
142         checkNotNull(loop, "loop");
143         final DefaultDnsCacheEntry e = new DefaultDnsCacheEntry(hostname, address);
144         if (maxTtl == 0 || !emptyAdditionals(additionals)) {
145             return e;
146         }
147         cache0(e, Math.max(minTtl, Math.min(MAX_SUPPORTED_TTL_SECS, (int) Math.min(maxTtl, originalTtl))), loop);
148         return e;
149     }
150 
151     @Override
152     public DnsCacheEntry cache(String hostname, DnsRecord[] additionals, Throwable cause, EventLoop loop) {
153         checkNotNull(hostname, "hostname");
154         checkNotNull(cause, "cause");
155         checkNotNull(loop, "loop");
156 
157         final DefaultDnsCacheEntry e = new DefaultDnsCacheEntry(hostname, cause);
158         if (negativeTtl == 0 || !emptyAdditionals(additionals)) {
159             return e;
160         }
161 
162         cache0(e, Math.min(MAX_SUPPORTED_TTL_SECS, negativeTtl), loop);
163         return e;
164     }
165 
166     private void cache0(DefaultDnsCacheEntry e, int ttl, EventLoop loop) {
167         Entries entries = resolveCache.get(e.hostname());
168         if (entries == null) {
169             entries = new Entries(e);
170             Entries oldEntries = resolveCache.putIfAbsent(e.hostname(), entries);
171             if (oldEntries != null) {
172                 entries = oldEntries;
173             }
174         }
175         entries.add(e);
176 
177         scheduleCacheExpiration(e, ttl, loop);
178     }
179 
180     private void scheduleCacheExpiration(final DefaultDnsCacheEntry e,
181                                          int ttl,
182                                          EventLoop loop) {
183         e.scheduleExpiration(loop, new Runnable() {
184                     @Override
185                     public void run() {
186                         // We always remove all entries for a hostname once one entry expire. This is not the
187                         // most efficient to do but this way we can guarantee that if a DnsResolver
188                         // be configured to prefer one ip family over the other we will not return unexpected
189                         // results to the enduser if one of the A or AAAA records has different TTL settings.
190                         //
191                         // As a TTL is just a hint of the maximum time a cache is allowed to cache stuff it's
192                         // completely fine to remove the entry even if the TTL is not reached yet.
193                         //
194                         // See https://github.com/netty/netty/issues/7329
195                         Entries entries = resolveCache.remove(e.hostname);
196                         if (entries != null) {
197                             entries.clearAndCancel();
198                         }
199                     }
200                 }, ttl, TimeUnit.SECONDS);
201     }
202 
203     @Override
204     public String toString() {
205         return new StringBuilder()
206                 .append("DefaultDnsCache(minTtl=")
207                 .append(minTtl).append(", maxTtl=")
208                 .append(maxTtl).append(", negativeTtl=")
209                 .append(negativeTtl).append(", cached resolved hostname=")
210                 .append(resolveCache.size()).append(")")
211                 .toString();
212     }
213 
214     private static final class DefaultDnsCacheEntry implements DnsCacheEntry {
215         private final String hostname;
216         private final InetAddress address;
217         private final Throwable cause;
218         private volatile ScheduledFuture<?> expirationFuture;
219 
220         DefaultDnsCacheEntry(String hostname, InetAddress address) {
221             this.hostname = checkNotNull(hostname, "hostname");
222             this.address = checkNotNull(address, "address");
223             cause = null;
224         }
225 
226         DefaultDnsCacheEntry(String hostname, Throwable cause) {
227             this.hostname = checkNotNull(hostname, "hostname");
228             this.cause = checkNotNull(cause, "cause");
229             address = null;
230         }
231 
232         @Override
233         public InetAddress address() {
234             return address;
235         }
236 
237         @Override
238         public Throwable cause() {
239             return cause;
240         }
241 
242         String hostname() {
243             return hostname;
244         }
245 
246         void scheduleExpiration(EventLoop loop, Runnable task, long delay, TimeUnit unit) {
247             assert expirationFuture == null : "expiration task scheduled already";
248             expirationFuture = loop.schedule(task, delay, unit);
249         }
250 
251         void cancelExpiration() {
252             ScheduledFuture<?> expirationFuture = this.expirationFuture;
253             if (expirationFuture != null) {
254                 expirationFuture.cancel(false);
255             }
256         }
257 
258         @Override
259         public String toString() {
260             if (cause != null) {
261                 return hostname + '/' + cause;
262             } else {
263                 return address.toString();
264             }
265         }
266     }
267 
268     // Directly extend AtomicReference for intrinsics and also to keep memory overhead low.
269     private static final class Entries extends AtomicReference<List<DefaultDnsCacheEntry>> {
270 
271         Entries(DefaultDnsCacheEntry entry) {
272             super(Collections.singletonList(entry));
273         }
274 
275         void add(DefaultDnsCacheEntry e) {
276             if (e.cause() == null) {
277                 for (;;) {
278                     List<DefaultDnsCacheEntry> entries = get();
279                     if (!entries.isEmpty()) {
280                         final DefaultDnsCacheEntry firstEntry = entries.get(0);
281                         if (firstEntry.cause() != null) {
282                             assert entries.size() == 1;
283                             if (compareAndSet(entries, Collections.singletonList(e))) {
284                                 firstEntry.cancelExpiration();
285                                 return;
286                             } else {
287                                 // Need to try again as CAS failed
288                                 continue;
289                             }
290                         }
291 
292                         // Create a new List for COW semantics
293                         List<DefaultDnsCacheEntry> newEntries = new ArrayList<DefaultDnsCacheEntry>(entries.size() + 1);
294                         DefaultDnsCacheEntry replacedEntry = null;
295                         for (int i = 0; i < entries.size(); i++) {
296                             DefaultDnsCacheEntry entry = entries.get(i);
297                             // Only add old entry if the address is not the same as the one we try to add as well.
298                             // In this case we will skip it and just add the new entry as this may have
299                             // more up-to-date data and cancel the old after we were able to update the cache.
300                             if (!e.address().equals(entry.address())) {
301                                 newEntries.add(entry);
302                             } else {
303                                 assert replacedEntry == null;
304                                 replacedEntry = entry;
305                             }
306                         }
307                         newEntries.add(e);
308                         if (compareAndSet(entries, newEntries)) {
309                             if (replacedEntry != null) {
310                                 replacedEntry.cancelExpiration();
311                             }
312                             return;
313                         }
314                     } else if (compareAndSet(entries, Collections.singletonList(e))) {
315                         return;
316                     }
317                 }
318             } else {
319                 List<DefaultDnsCacheEntry> entries = getAndSet(Collections.singletonList(e));
320                 cancelExpiration(entries);
321             }
322         }
323 
324         boolean clearAndCancel() {
325             List<DefaultDnsCacheEntry> entries = getAndSet(Collections.<DefaultDnsCacheEntry>emptyList());
326             if (entries.isEmpty()) {
327                 return false;
328             }
329 
330             cancelExpiration(entries);
331             return true;
332         }
333 
334         private static void cancelExpiration(List<DefaultDnsCacheEntry> entryList) {
335             final int numEntries = entryList.size();
336             for (int i = 0; i < numEntries; i++) {
337                 entryList.get(i).cancelExpiration();
338             }
339         }
340     }
341 }