View Javadoc
1   /*
2    * Copyright 2014 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  
17  package io.netty.resolver.dns;
18  
19  import io.netty.buffer.Unpooled;
20  import io.netty.channel.ChannelFuture;
21  import io.netty.channel.ChannelFutureListener;
22  import io.netty.handler.codec.dns.DnsQuery;
23  import io.netty.handler.codec.dns.DnsQuestion;
24  import io.netty.handler.codec.dns.DnsResource;
25  import io.netty.handler.codec.dns.DnsResponse;
26  import io.netty.handler.codec.dns.DnsType;
27  import io.netty.resolver.dns.DnsNameResolver.DnsCacheEntry;
28  import io.netty.util.concurrent.Promise;
29  import io.netty.util.concurrent.ScheduledFuture;
30  import io.netty.util.internal.OneTimeTask;
31  import io.netty.util.internal.StringUtil;
32  import io.netty.util.internal.ThreadLocalRandom;
33  import io.netty.util.internal.logging.InternalLogger;
34  import io.netty.util.internal.logging.InternalLoggerFactory;
35  
36  import java.net.InetSocketAddress;
37  import java.net.UnknownHostException;
38  import java.util.Iterator;
39  import java.util.concurrent.TimeUnit;
40  
41  final class DnsQueryContext {
42  
43      private static final InternalLogger logger = InternalLoggerFactory.getInstance(DnsQueryContext.class);
44  
45      private final DnsNameResolver parent;
46      private final Promise<DnsResponse> promise;
47      private final int id;
48      private final DnsQuestion question;
49      private final DnsResource optResource;
50      private final Iterator<InetSocketAddress> nameServerAddresses;
51  
52      private final boolean recursionDesired;
53      private final int maxTries;
54      private int remainingTries;
55      private volatile ScheduledFuture<?> timeoutFuture;
56      private StringBuilder trace;
57  
58      DnsQueryContext(DnsNameResolver parent,
59                      Iterable<InetSocketAddress> nameServerAddresses,
60                      DnsQuestion question, Promise<DnsResponse> promise) {
61  
62          this.parent = parent;
63          this.promise = promise;
64          this.question = question;
65  
66          id = allocateId();
67          recursionDesired = parent.isRecursionDesired();
68          maxTries = parent.maxTriesPerQuery();
69          remainingTries = maxTries;
70          optResource = new DnsResource("", DnsType.OPT, parent.maxPayloadSizeClass(), 0, Unpooled.EMPTY_BUFFER);
71  
72          this.nameServerAddresses = nameServerAddresses.iterator();
73      }
74  
75      private int allocateId() {
76          int id = ThreadLocalRandom.current().nextInt(parent.promises.length());
77          final int maxTries = parent.promises.length() << 1;
78          int tries = 0;
79          for (;;) {
80              if (parent.promises.compareAndSet(id, null, this)) {
81                  return id;
82              }
83  
84              id = id + 1 & 0xFFFF;
85  
86              if (++ tries >= maxTries) {
87                  throw new IllegalStateException("query ID space exhausted: " + question);
88              }
89          }
90      }
91  
92      Promise<DnsResponse> promise() {
93          return promise;
94      }
95  
96      DnsQuestion question() {
97          return question;
98      }
99  
100     ScheduledFuture<?> timeoutFuture() {
101         return timeoutFuture;
102     }
103 
104     void query() {
105         final DnsQuestion question = this.question;
106 
107         if (remainingTries <= 0 || !nameServerAddresses.hasNext()) {
108             parent.promises.set(id, null);
109 
110             int tries = maxTries - remainingTries;
111             UnknownHostException cause;
112             if (tries > 1) {
113                 cause = new UnknownHostException(
114                         "failed to resolve " + question + " after " + tries + " attempts:" +
115                         trace);
116             } else {
117                 cause = new UnknownHostException("failed to resolve " + question + ':' + trace);
118             }
119 
120             cache(question, cause);
121             promise.tryFailure(cause);
122             return;
123         }
124 
125         remainingTries --;
126 
127         final InetSocketAddress nameServerAddr = nameServerAddresses.next();
128         final DnsQuery query = new DnsQuery(id, nameServerAddr);
129         query.addQuestion(question);
130         query.header().setRecursionDesired(recursionDesired);
131         query.addAdditionalResource(optResource);
132 
133         if (logger.isDebugEnabled()) {
134             logger.debug("{} WRITE: [{}: {}], {}", parent.ch, id, nameServerAddr, question);
135         }
136 
137         sendQuery(query, nameServerAddr);
138     }
139 
140     private void sendQuery(final DnsQuery query, final InetSocketAddress nameServerAddr) {
141         if (parent.bindFuture.isDone()) {
142             writeQuery(query, nameServerAddr);
143         } else {
144             parent.bindFuture.addListener(new ChannelFutureListener() {
145                 @Override
146                 public void operationComplete(ChannelFuture future) throws Exception {
147                     if (future.isSuccess()) {
148                         writeQuery(query, nameServerAddr);
149                     } else {
150                         promise.tryFailure(future.cause());
151                     }
152                  }
153             });
154         }
155     }
156 
157     private void writeQuery(final DnsQuery query, final InetSocketAddress nameServerAddr) {
158         final ChannelFuture writeFuture = parent.ch.writeAndFlush(query);
159         if (writeFuture.isDone()) {
160             onQueryWriteCompletion(writeFuture, nameServerAddr);
161         } else {
162             writeFuture.addListener(new ChannelFutureListener() {
163                 @Override
164                 public void operationComplete(ChannelFuture future) throws Exception {
165                     onQueryWriteCompletion(writeFuture, nameServerAddr);
166                 }
167             });
168         }
169     }
170 
171     private void onQueryWriteCompletion(ChannelFuture writeFuture, final InetSocketAddress nameServerAddr) {
172         if (!writeFuture.isSuccess()) {
173             retry(nameServerAddr, "failed to send a query: " + writeFuture.cause());
174             return;
175         }
176 
177         // Schedule a query timeout task if necessary.
178         final long queryTimeoutMillis = parent.queryTimeoutMillis();
179         if (queryTimeoutMillis > 0) {
180             timeoutFuture = parent.ch.eventLoop().schedule(new OneTimeTask() {
181                 @Override
182                 public void run() {
183                     if (promise.isDone()) {
184                         // Received a response before the query times out.
185                         return;
186                     }
187 
188                     retry(nameServerAddr, "query timed out after " + queryTimeoutMillis + " milliseconds");
189                 }
190             }, queryTimeoutMillis, TimeUnit.MILLISECONDS);
191         }
192     }
193 
194     void retry(InetSocketAddress nameServerAddr, String message) {
195         if (promise.isCancelled()) {
196             return;
197         }
198 
199         if (trace == null) {
200             trace = new StringBuilder(128);
201         }
202 
203         trace.append(StringUtil.NEWLINE);
204         trace.append("\tfrom ");
205         trace.append(nameServerAddr);
206         trace.append(": ");
207         trace.append(message);
208         query();
209     }
210 
211     private void cache(final DnsQuestion question, Throwable cause) {
212         final int negativeTtl = parent.negativeTtl();
213         if (negativeTtl == 0) {
214             return;
215         }
216 
217         parent.cache(question, new DnsCacheEntry(cause), negativeTtl);
218     }
219 }