Ruby Web-Services with Facebook's Thrift
Facebook's Thrift framework made a nice splash in the news on Wednesday with an official announcement of the move to the Apache Incubator. This is certainly exciting news for many developers as Thrift is arguably one of the best (recent) lightweight systems for cross-language programming using code-generation, RPC, and object serialization. Designed from the ground up by the Facebook development team it enables seamless integration of the most commonly used languages (Ruby, Perl, C/C++, Haskell, Python, Java, and more) via a set of common RPC libraries and interfaces. Not to mention, the guys at Facebook know a thing or two about high-performance websites, and Thrift does not disappoint!
Defining a service in Thrift
The core goal of Thrift is to enable efficient and reliable communications across multiple languages. All datatypes and services are defined in a single language-neutral file and the necessary code is auto-generated for the developer by the 'thrift compiler'. The beauty of this approach is, of course, the ability to mix and match implementations of services: your server may be written in C++, but you can access it seamlessly via a Python, Java, or a Ruby client. Thrift will take care of the communications links, object serialization, and socket management! Having said that, nothing stops you from using the same language on both the client and the web-server. Let's take a look at a Ruby-specific example.
Assuming you've downloaded, compiled, and installed the thrift libraries (./configure && make && make install), we can jump right into the definition of our new service:
# Define a struct, an exception and a 'quick service - QService'
# - To learn more about Thrift's datatypes, head to:
# - http://developers.facebook.com/thrift/
struct Lookup {
1:string bucket,
2:string key
}
exception InvalidKey {
1: string error
}
service QService {
/**
* A method definition looks like C code. It has a return type, arguments,
* and optionally a list of exceptions that it may throw. Note that argument
* lists and exception lists are specified using the exact same syntax as
* field lists in struct or exception definitions.
*/
i32 exponent(1:i32 base, 2:i32 exp),
string get_key(1:Lookup l) throws (1:InvalidKey e),
async void run_task()
}
We've defined three different functions: remote exponentiation, a key lookup, and an asynchronous method call. To generate the required code, we simply call the thrift generator:
# Generate C++, Ruby, and Python implementations # - generated code will be in 'gen-cpp', 'gen-rb', 'gen-py' $ thrift -cpp -rb -py qservice.thrift
Implementing a Thrift powered server in Ruby
Thrift does most of the work, and we just need to provide the actual implementation of our functions in either Python, Ruby or C++. A quick and dirty Ruby implementation might look something like this:
# include thrift-generated code
$:.push('../gen-rb')
require 'thrift/transport/tsocket'
require 'thrift/protocol/tbinaryprotocol'
require 'thrift/server/tserver'
require 'QService'
# provide an implementation of QService
class QServiceHandler
def initialize()
@log = {}
@hash = {'bucket1' => {'key1' => 'value1'}}
end
def exponent(base, exp)
print "#{base}**#{exp}\n"
return base**exp
end
def get_key(lookup)
if @hash[lookup.bucket][lookup.key]
return @hash[lookup.bucket][lookup.key]
else
e = InvalidKey.new
e.error = 'Cache miss'
raise e
end
end
def run_task()
print "kick off long running task...\n"
end
end
# Thrift provides mutiple communication endpoints
# - Here we will expose our service via a TCP socket
# - Web-service will run as a single thread, on port 9090
handler = QServiceHandler.new()
processor = QService::Processor.new(handler)
transport = TServerSocket.new(9090)
transportFactory = TBufferedTransportFactory.new()
server = TSimpleServer.new(processor, transport, transportFactory)
puts "Starting the QService server..."
server.serve()
puts "Done"
To expose our QService to the outer world, we wrap it into a TCP socket listening on port 9090. From this point on, Thrift takes over all communications, serialization and handling of the incoming requests. Because the protocol is identical in every language, the client may be written in any language of choice.
Building a Ruby client
In similar fashion, we can build a Ruby client for any Thrift service with just a few lines of code. To interface with our server implementation above, we can do the following:
$:.push('../gen-rb')
require 'thrift/transport/tsocket.rb'
require 'thrift/protocol/tbinaryprotocol.rb'
require 'QService'
begin
transport = TBufferedTransport.new(TSocket.new('localhost', 9090))
protocol = TBinaryProtocol.new(transport)
client = QService::Client.new(protocol)
transport.open()
# Run a remote calculation
answer = client.exponent(50,2)
print "50**2=", answer, "\n"
# Run a 'cache' lookup
lookup = Lookup.new()
lookup.bucket = 'bucket1'
lookup.key = 'key1'
print "Lookup: ", client.get_key(lookup), "\n"
# Force a cache miss
begin
lookup.bucket = 'bucket2'
print "Lookup: ", client.get_key(lookup), "\n"
rescue InvalidKey => e
print "InvalidKey: ", e.error, "\n"
end
print client.run_task()
transport.close()
rescue TException => tx
print 'TException: ', tx.message, "\n"
end
Every language has its strong points, and the ability to mix and match server/client implementations on the fly becomes a nice bonus - kudos to the Facebook developers for open-sourcing this gem. Thrift is certainly a project to keep a close eye on, as it may well become your swiss-army knife when it comes to distributed web-service development.
Additional resources and reading: