Samuel Williams Saturday, 20 October 2012

DNS resolution in a typical application is a blocking operation. This is especially problematic if your program is event driven as you can cause the run loop to stall while waiting for a response. RubyDNS provides a fully featured asynchronous DNS resolver built on top of EventMachine, and can be used to minimise the latency of name resolution in your program.

require 'rubydns'

resolver =[[:udp, "", 53], [:tcp, "", 53]])

EventMachine::run do
	resolver.query('') do |response|
		case response
		when RubyDNS::Message
			response.answer.each do |answer|
				host = answer[0].to_s
				ttl = answer[1]
				resource_class = answer[2]'::', 4)[-1]
				address = answer[2].address.to_s
				puts host.ljust(20) + ttl.to_s.ljust(5) + resource_class.ljust(15) + address.to_s
		when RubyDNS::ResolutionFailure
			puts "Error"

# Gives the output
#   287  IN::A
#   287  IN::A

In your own code you'd probably want to use the new resolver.addresses_for method which helpfully returns a list of addresses.


Event driving programming is pretty straight forward. You essentially have a loop that reads events and responds to them. Processing is kept to a minimum per cycle, otherwise the loop stalls and becomes unresponsive.

Software that uses a network for communication typically relies on name resolution. In particular, RubyDNS::Server is primarily interested in two things - receiving incoming requests and sending out a response - and - sending out requests and waiting for a response.

DNS resolution is one task that is typically done using operating system functions such as gethostbyname or getaddrinfo. The main problem is that these functions cause your process to sleep until a result is available.

(Have you ever noticed how sometimes in games, the entire game stalls when logging in or listing servers? This is very often due to name resolution latency affecting the main event loop)

If RubyDNS uses these functions, no other events can be processed while we are waiting for the operating system to respond. In practice, this means that RubyDNS::Server may perform poorly if many people are using it simultaneously.

To avoid these problems, RubyDNS recently introduced its own RubyDNS::Resolver which provides robust asynchronous DNS resolution built on top of EventMachine. This resolver isn't just for RubyDNS::Server though, it can be used in any EventMachine event driven code that wants high performance name resolution.


Given a request, which consists of one or more DNS questions, our resolver firstly checks whether UDP is a suitable transport. DNS packets are typically routed over UDP but if the packet is too big it should use TCP:

# `message` is the outgoing DNS request. `servers` is an array of potential
# upstream servers who might be able to provide an answer.
def initialize(message, servers, options = {}, &block)
	@message = message
	@packet = message.encode
	@servers = servers.dup
	# We select the protocol based on the size of the data:
	if @packet.bytesize > UDP_TRUNCATION_SIZE
		@servers.delete_if{|server| server[0] == :udp}
	# Measured in seconds:
	@timeout = options[:timeout] || 5
	@logger = options[:logger]

With this list of candidates, we connect to each one and send the request. In all failure cases, we try the next server if one is available, otherwise if no servers have been successful we signal a resolution failure:

def try_next_server!
	if @request
		@request = nil
	if @servers.size > 0
		@server = @servers.shift

		# We make requests one at a time to the given server, naturally the servers
		# are ordered in terms of priority.
		case @server[0]
		when :udp
			@request =[1], @server[2], self)
		when :tcp
			@request =[1], @server[2], self)
		# Setting up the timeout... do
		# Signal that the deferrable has failed and resolution was not possible:
		fail"No available servers responded to the request.")

We then wait until EventMachine tells us one of two things: some response was received or there was a timeout. If we receive a response, as long as it wasn't truncated, we are successful:

def process_response!(response)
	if != 0
		# We hardcode this behaviour for now.
		succeed response

In practice, we use EventMachine::Deferrable to handle this signalling. Using deferrables ultiamtely led to concise and reliable code and I was very happy with the results. I'd recommend taking a look at the full source code.


Leave a comment

Please note, comments must be formatted using Markdown. Links can be enclosed in angle brackets, e.g. <>.