Link to wealthfront.com

Fork me on GitHub

Monday, March 21, 2011

Dealing with Missing Data on the Frontend

What if you got an unexpected result from an ActiveRecord query? If it's important for the current page, rendering a 500 is a good idea. If the data is being used for a non-vital section, it's best to render the page without the section:
@manager = Manager.find(params[:id]) rescue nil

<div id="sidebar">
<% if @manager.nil? %>
Data currently unavailable.
<% else %>
...
<% end %>
<div>

At Wealthfront, we don't use ActiveRecord. Our frontends talk to backend services via RPCs (Remote Procedure Calls).

When an RPC fails unexpectedly, it raises a QueryException. This bubbles up to a 500 error page. If we're grabbing data for non-vital fragments on a page, we mark the RPCs as failsafe:
class ManagersController
def index
@manager = service.get_manager(...) # raises QueryException if it fails
failsafe do
# RPCs in a failsafe block will return nil if it fails
@sidebar = service.get_sidebar_data(...)
@footer = service.get_footer_data(...)
end
end
end

In our views, we make sure the data is available before using it:
<% if @sidebar.nil? %>
Data currently unavailable.
<% else %>
...
<% end %>

Implementation

Clients need to know whether to raise an exception or return nil on errors. They're initialized with the boolean failsafe.
class ServiceClient
def initialize(failsafe)
@failsafe = failsafe
end

def get_sidebar_data
# make http request, raise a QueryException for status codes >= 400
rescue Timeout::Error, QueryException => e
if @failsafe
Rails.logger.error("GetSidebarData failed")
nil
else
raise
end
end
end

Our controller will keep track of the failsafe state and pass it to ServiceClient:
class ApplicationController
private
def failsafe
@failsafe += 1
yield
ensure
@failsafe -= 1
end

def service
ServiceClient.new(@failsafe >= 1)
end
end

Calls within failsafe can be inlined or batched together:
@result1 = failsafe { service1.call }

failsafe do
@result1 = service1.call
@result2 = service2.call
end

Testing

It's important for us to verify that pages render correctly with missing data. We've gone through all the trouble of not raising an exception when an RPC fails; we don't want to raise any whiny nils in our views:
describe ManagersController do
it "should render index" do
service.stubs(:get_sidebar_data => ...)
get :index
response.status.should == 200
end

it "should still render index if failed RPCs occur" do
stub_failsafe!
get :index
response.status.should == 200
end
end

Controller specs are configured to render views. We use Mocha to stub out RPCs and return expected data. The method stub_failsafe! will stub all of our services to return nil when called within a failsafe block.

This will catch errors in templates like:
<%= @sidebar.something %>

That causes our example to fail with:
=> NoMethodError: undefined method `something' for nil:NilClass

since we should be checking that @sidebar is not nil before using it.