Tuesday, February 07, 2006

Making Progress

I'm happy to report that I'm making progress. It seems my breakthrough epiphany is bearing fruit. I still don't have the big picture breakdown like I want but thanx to my incessant note taking I have a bunch of little pictures that I can work on until I discover a way to tie it all together.

In the mean time I am happy to report the DNS querying code that I posted in A Breakthrough! has graduated from the Whiteboard and found it's own home. It's been fully tricked out with hostname resolution caching and all. There are a few concurrency related goodies in the code that I want to talk about to those readers still trying to figure out what the java.util.concurrency packages are good for. Since the code is 183 lines long (with comments) I'm splitting its discussion into two entries. The first entry is the one you are reading now and it contains the source code. The second entry will discuss the concurrency goodies and I'll post that sometime later today or maybe tomorrow.

Here is the code, enjoy:

(Updated 13 Feb 06, 10:14)


6    import javax.naming.CommunicationException;
7    import javax.naming.NameNotFoundException;
8    import javax.naming.NamingException;
9    import javax.naming.ServiceUnavailableException;
10   
11   import javax.naming.directory.InitialDirContext;
12   
13   import java.net.InetAddress;
14   import java.net.PortUnreachableException;
15   import java.net.UnknownHostException;
16   
17   import java.util.Enumeration;
18   import java.util.Queue;
19   import java.util.TreeSet;
20   
21   import java.util.concurrent.ConcurrentHashMap;
22   import java.util.concurrent.ConcurrentLinkedQueue;
23   import java.util.concurrent.ConcurrentMap;
24   import java.util.concurrent.CountDownLatch;
25   
26   /**
27    * Lookup service for DNS MX records.
28    * <p>
29    * <b>Thread safe:</b> yes
30    * </p>
31    *
32    * @author HashiDiKo</a>
33    */
34   public final class MXRecordQueryService
35   {
36       private static final String URL = "dns:/";
37   
38       private static final String MXATT = "MX";
39   
40       private static final String[] MXFILTER = {MXATT};
41   
42       private static final MXRecordQueryService THIS = new MXRecordQueryService();
43   
44       /**
45        * Cache queries.
46        * @see MXRecordQueryService.CacheEntry
47        */
48       private final ConcurrentMap<String, CacheEntry> cache = new ConcurrentHashMap<String, CacheEntry>();
49   
50       /**
51        * Provides a mechanism for ensuring that no 2 (or more) threads will race to query the same domain but at the same
52        * time allow concurrent queries of different domains.
53        */
54       private final ConcurrentMap<String, CountDownLatch> domainLocks = new ConcurrentHashMap<String, CountDownLatch>();
55   
56       private MXRecordQueryService()
57       {
58       }
59   
60       public static MXRecordQueryService getInstance()
61       {
62           return THIS;
63       }
64   
65       /**
66        * Returns the IP addresses of the MX records for <tt>domain</tt> sorted by preference or <tt>null</tt> if no valid
67        * hostnames exits for the MX records.
68        *
69        * @param domain The domain name.
70        *
71        * @return The  IP addresses of the MX records for <tt>domain</tt> sorted by preference or <tt>null</tt> if no valid
72        *         hostnames exits for the MX records.
73        *
74        * @throws NameNotFoundException       Indicates that the <tt>domain</tt> is invalid.
75        * @throws ServiceUnavailableException Indicates that <tt>domain</tt> is valid but no MX records exists for it.
76        * @throws CommunicationException      Indicates a problem contacting the configured DNS server(s).
77        * @throws RuntimeException            If some other <tt>javax.naming.NamingException</tt> occurs.
78        */
79       public Queue<String> get( String domain )
80               throws ServiceUnavailableException, NameNotFoundException, CommunicationException, InterruptedException
81       {
82           CacheEntry cached = cache.get( domain );
83           if( null != cached )
84           {
85               if( System.currentTimeMillis() <= cached.expiration )
86                   return cached.object;
87               cache.remove( domain, cached );
88           }
89   
90           CountDownLatch lock = domainLocks.get( domain );
91           if( null == lock )
92           {
93               lock = new CountDownLatch( 1 );
94               CountDownLatch prempted = domainLocks.putIfAbsent( domain, lock );
95               if( null == prempted )
96               {   /* Take another look because we may have preempted and it's cheaper to look than to lookup. */
97                   cached = cache.get( domain );
98                   if( null != cached && System.currentTimeMillis() <= cached.expiration )
99                       return cached.object;
100  
101                  try
102                  {
103                      TreeSet<MXRecord> sorter = new TreeSet<MXRecord>();
104                      String url = URL.concat( domain );
105                      for( Enumeration en = new InitialDirContext().getAttributes( url, MXFILTER ).get( MXATT ).getAll();
106                           en.hasMoreElements(); )
107                          sorter.add( new MXRecord( (String)en.nextElement() ) );
108  
109                      Queue<String> queue = new ConcurrentLinkedQueue<String>();
110                      for( MXRecord mx : sorter )
111                      {
112                          try
113                          {
114                              for( InetAddress addr : InetAddress.getAllByName( mx.hostname ) )
115                                  queue.offer( addr.getHostAddress() );
116                          }
117                          catch( UnknownHostException ignore )
118                          {
119                          }
120                      }
121                      cache.put( domain, new CacheEntry( queue ) );
122                      return queue;
123                  }
124                  catch( NameNotFoundException nex )
125                  {
126                      throw nex;
127                  }
128                  catch( ServiceUnavailableException sux )
129                  {
130                      throw sux;
131                  }
132                  catch( CommunicationException comx )
133                  {
134                      if( comx.getRootCause() instanceof PortUnreachableException )
135                          throw comx;
136                      throw new RuntimeException( comx );
137                  }
138                  catch( NamingException nex )
139                  {
140                      throw new RuntimeException( nex );
141                  }
142                  finally
143                  {
144                      lock.countDown();
145                      boolean invariant = domainLocks.remove( domain, lock );
146                      assert invariant : "Impossible!";
147                  }
148              }
149              else
150                  lock = prempted;
151          }
152          lock.await();
153          return get( domain );
154      }
155  
156      private static final class CacheEntry
157      {
158          final Queue<String> object;
159  
160          final long expiration;
161  
162          CacheEntry( Queue<String> object )
163          {
164              this.object = object;
165              expiration = System.currentTimeMillis() + 60000;
166          }
167      }
168  
169      /**
170       * Utility class for extracting the different parts of a MX record string.
171       */
172      private static final class MXRecord implements Comparable<MXRecord>
173      {
174          final String hostname;
175  
176          private final int pref;
177  
178          MXRecord( String mx )
179          {
180              assert mx.matches( "\\d++\\s++.+\\." ) : "Invalid MX record format: ".concat( mx );
181  
182              int offset = 0;
183              int len = mx.length() - 1;// Strip trailing dot.
184  
185              /* Extract preference integer. */
186              while( offset < len && Character.isDigit( mx.charAt( offset ) ) )
187                  offset++;
188              pref = Integer.parseInt( mx.substring( 0, offset ) );
189  
190              /* Extract domain name. */
191              while( offset < len && Character.isWhitespace( mx.charAt( offset ) ) )
192                  offset++;
193  
194              hostname = mx.substring( offset, len );
195          }
196  
197          public int compareTo( MXRecord o )
198          {
199              int diff = pref - o.pref;
200              return 0 == diff ? hostname.compareTo( o.hostname ) : diff;
201          }
202      }
203  }
204  

No comments:

Post a Comment