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      private final int minTtl;
47      private final int maxTtl;
48      private final int negativeTtl;
49  
50      /**
51       * Create a cache that respects the TTL returned by the DNS server
52       * and doesn't cache negative responses.
53       */
54      public DefaultDnsCache() {
55          this(0, Integer.MAX_VALUE, 0);
56      }
57  
58      /**
59       * Create a cache.
60       * @param minTtl the minimum TTL
61       * @param maxTtl the maximum TTL
62       * @param negativeTtl the TTL for failed queries
63       */
64      public DefaultDnsCache(int minTtl, int maxTtl, int negativeTtl) {
65          this.minTtl = checkPositiveOrZero(minTtl, "minTtl");
66          this.maxTtl = checkPositiveOrZero(maxTtl, "maxTtl");
67          if (minTtl > maxTtl) {
68              throw new IllegalArgumentException(
69                      "minTtl: " + minTtl + ", maxTtl: " + maxTtl + " (expected: 0 <= minTtl <= maxTtl)");
70          }
71          this.negativeTtl = checkPositiveOrZero(negativeTtl, "negativeTtl");
72      }
73  
74      /**
75       * Returns the minimum TTL of the cached DNS resource records (in seconds).
76       *
77       * @see #maxTtl()
78       */
79      public int minTtl() {
80          return minTtl;
81      }
82  
83      /**
84       * Returns the maximum TTL of the cached DNS resource records (in seconds).
85       *
86       * @see #minTtl()
87       */
88      public int maxTtl() {
89          return maxTtl;
90      }
91  
92      /**
93       * Returns the TTL of the cache for the failed DNS queries (in seconds). The default value is {@code 0}, which
94       * disables the cache for negative results.
95       */
96      public int negativeTtl() {
97          return negativeTtl;
98      }
99  
100     @Override
101     public void clear() {
102         while (!resolveCache.isEmpty()) {
103             for (Iterator<Map.Entry<String, Entries>> i = resolveCache.entrySet().iterator(); i.hasNext();) {
104                 Map.Entry<String, Entries> e = i.next();
105                 i.remove();
106 
107                 e.getValue().clearAndCancel();
108             }
109         }
110     }
111 
112     @Override
113     public boolean clear(String hostname) {
114         checkNotNull(hostname, "hostname");
115         Entries entries = resolveCache.remove(hostname);
116         return entries != null && entries.clearAndCancel();
117     }
118 
119     private static boolean emptyAdditionals(DnsRecord[] additionals) {
120         return additionals == null || additionals.length == 0;
121     }
122 
123     @Override
124     public List<? extends DnsCacheEntry> get(String hostname, DnsRecord[] additionals) {
125         checkNotNull(hostname, "hostname");
126         if (!emptyAdditionals(additionals)) {
127             return Collections.<DnsCacheEntry>emptyList();
128         }
129 
130         Entries entries = resolveCache.get(hostname);
131         return entries == null ? null : entries.get();
132     }
133 
134     @Override
135     public DnsCacheEntry cache(String hostname, DnsRecord[] additionals,
136                                InetAddress address, long originalTtl, EventLoop loop) {
137         checkNotNull(hostname, "hostname");
138         checkNotNull(address, "address");
139         checkNotNull(loop, "loop");
140         final DefaultDnsCacheEntry e = new DefaultDnsCacheEntry(hostname, address);
141         if (maxTtl == 0 || !emptyAdditionals(additionals)) {
142             return e;
143         }
144         cache0(e, Math.max(minTtl, (int) Math.min(maxTtl, originalTtl)), loop);
145         return e;
146     }
147 
148     @Override
149     public DnsCacheEntry cache(String hostname, DnsRecord[] additionals, Throwable cause, EventLoop loop) {
150         checkNotNull(hostname, "hostname");
151         checkNotNull(cause, "cause");
152         checkNotNull(loop, "loop");
153 
154         final DefaultDnsCacheEntry e = new DefaultDnsCacheEntry(hostname, cause);
155         if (negativeTtl == 0 || !emptyAdditionals(additionals)) {
156             return e;
157         }
158 
159         cache0(e, negativeTtl, loop);
160         return e;
161     }
162 
163     private void cache0(DefaultDnsCacheEntry e, int ttl, EventLoop loop) {
164         Entries entries = resolveCache.get(e.hostname());
165         if (entries == null) {
166             entries = new Entries(e);
167             Entries oldEntries = resolveCache.putIfAbsent(e.hostname(), entries);
168             if (oldEntries != null) {
169                 entries = oldEntries;
170                 entries.add(e);
171             }
172         }
173 
174         scheduleCacheExpiration(e, ttl, loop);
175     }
176 
177     private void scheduleCacheExpiration(final DefaultDnsCacheEntry e,
178                                          int ttl,
179                                          EventLoop loop) {
180         e.scheduleExpiration(loop, new Runnable() {
181                     @Override
182                     public void run() {
183                         // We always remove all entries for a hostname once one entry expire. This is not the
184                         // most efficient to do but this way we can guarantee that if a DnsResolver
185                         // be configured to prefer one ip family over the other we will not return unexpected
186                         // results to the enduser if one of the A or AAAA records has different TTL settings.
187                         //
188                         // As a TTL is just a hint of the maximum time a cache is allowed to cache stuff it's
189                         // completely fine to remove the entry even if the TTL is not reached yet.
190                         //
191                         // See https://github.com/netty/netty/issues/7329
192                         Entries entries = resolveCache.remove(e.hostname);
193                         if (entries != null) {
194                             entries.clearAndCancel();
195                         }
196                     }
197                 }, ttl, TimeUnit.SECONDS);
198     }
199 
200     @Override
201     public String toString() {
202         return new StringBuilder()
203                 .append("DefaultDnsCache(minTtl=")
204                 .append(minTtl).append(", maxTtl=")
205                 .append(maxTtl).append(", negativeTtl=")
206                 .append(negativeTtl).append(", cached resolved hostname=")
207                 .append(resolveCache.size()).append(")")
208                 .toString();
209     }
210 
211     private static final class DefaultDnsCacheEntry implements DnsCacheEntry {
212         private final String hostname;
213         private final InetAddress address;
214         private final Throwable cause;
215         private volatile ScheduledFuture<?> expirationFuture;
216 
217         DefaultDnsCacheEntry(String hostname, InetAddress address) {
218             this.hostname = checkNotNull(hostname, "hostname");
219             this.address = checkNotNull(address, "address");
220             cause = null;
221         }
222 
223         DefaultDnsCacheEntry(String hostname, Throwable cause) {
224             this.hostname = checkNotNull(hostname, "hostname");
225             this.cause = checkNotNull(cause, "cause");
226             address = null;
227         }
228 
229         @Override
230         public InetAddress address() {
231             return address;
232         }
233 
234         @Override
235         public Throwable cause() {
236             return cause;
237         }
238 
239         String hostname() {
240             return hostname;
241         }
242 
243         void scheduleExpiration(EventLoop loop, Runnable task, long delay, TimeUnit unit) {
244             assert expirationFuture == null : "expiration task scheduled already";
245             expirationFuture = loop.schedule(task, delay, unit);
246         }
247 
248         void cancelExpiration() {
249             ScheduledFuture<?> expirationFuture = this.expirationFuture;
250             if (expirationFuture != null) {
251                 expirationFuture.cancel(false);
252             }
253         }
254 
255         @Override
256         public String toString() {
257             if (cause != null) {
258                 return hostname + '/' + cause;
259             } else {
260                 return address.toString();
261             }
262         }
263     }
264 
265     // Directly extend AtomicReference for intrinsics and also to keep memory overhead low.
266     private static final class Entries extends AtomicReference<List<DefaultDnsCacheEntry>> {
267 
268         Entries(DefaultDnsCacheEntry entry) {
269             super(Collections.singletonList(entry));
270         }
271 
272         void add(DefaultDnsCacheEntry e) {
273             if (e.cause() == null) {
274                 for (;;) {
275                     List<DefaultDnsCacheEntry> entries = get();
276                     if (!entries.isEmpty()) {
277                         final DefaultDnsCacheEntry firstEntry = entries.get(0);
278                         if (firstEntry.cause() != null) {
279                             assert entries.size() == 1;
280                             if (compareAndSet(entries, Collections.singletonList(e))) {
281                                 firstEntry.cancelExpiration();
282                                 return;
283                             } else {
284                                 // Need to try again as CAS failed
285                                 continue;
286                             }
287                         }
288                         // Create a new List for COW semantics
289                         List<DefaultDnsCacheEntry> newEntries = new ArrayList<DefaultDnsCacheEntry>(entries.size() + 1);
290                         newEntries.addAll(entries);
291                         newEntries.add(e);
292                         if (compareAndSet(entries, newEntries)) {
293                             return;
294                         }
295                     } else if (compareAndSet(entries, Collections.singletonList(e))) {
296                         return;
297                     }
298                 }
299             } else {
300                 List<DefaultDnsCacheEntry> entries = getAndSet(Collections.singletonList(e));
301                 cancelExpiration(entries);
302             }
303         }
304 
305         boolean clearAndCancel() {
306             List<DefaultDnsCacheEntry> entries = getAndSet(Collections.<DefaultDnsCacheEntry>emptyList());
307             if (entries.isEmpty()) {
308                 return false;
309             }
310 
311             cancelExpiration(entries);
312             return true;
313         }
314 
315         private static void cancelExpiration(List<DefaultDnsCacheEntry> entryList) {
316             final int numEntries = entryList.size();
317             for (int i = 0; i < numEntries; i++) {
318                 entryList.get(i).cancelExpiration();
319             }
320         }
321     }
322 }