The video game industry frequently tests the limits of web- and service-oriented architectures. Poor-performing launches have made it clear that gamers expect fast, responsive services if we're going to make games that are in constant communication with centralized data networks. Having infrastructure out-of-the-gate that's quick, scalable, and tunable is a must. This talk is a brief delve into the non-blocking Ruby web framework, Goliath, and how we're using it to power numerous APIs in a service-oriented infrastructure that ties together our game titles, players, social outreach, and internal stakeholders.
Good Stuff Happens in 1:1 Meetings: Why you need them and how to do them well
Building high-performance APIs for the video game industry with Goliath, Grape and EventMachine
1. Building high-performance APIs for
the video game industry with Goliath,
Grape, and EventMachine
Matt E. Patterson
Digimonkey Studios
Monday, June 10, 13
3. Game Web Services
Must be fast.
Must be reliable.
Must be scalable...
...and able to handle sudden bursts
(launches, new DLC, etc.)
Monday, June 10, 13
4. Goliath
open-source, non-blocking, asychronous Ruby web server
framework from postrank-labs
EventMachine reactor.
HTTP parser, Rack, Ruby 1.9+, Ruby Fibers
each request executes in its own Fiber
Monday, June 10, 13
5. Ubiquitous Goliath
“Hello, world” example
require 'goliath'
class Hello < Goliath::API
def response(env)
[ 200, {}, "Hello, world!" ]
end
end
Monday, June 10, 13
6. Ubiquitous Goliath
“Hello, world” example
require 'goliath'
class Hello < Goliath::API
def response(env)
[ 200, {}, "Hello, world!" ]
end
end
$ ruby hello.rb -sv -p 8000
Monday, June 10, 13
7. Ubiquitous Goliath
“Hello, world” example
require 'goliath'
class Hello < Goliath::API
def response(env)
[ 200, {}, "Hello, world!" ]
end
end
$ ruby hello.rb -sv -p 8000
[45584:INFO] 2013-05-28 11:52:52 :: Starting server on
0.0.0.0:8000 in development mode. Watch out for stones.
Monday, June 10, 13
8. Ubiquitous Goliath
“Hello, world” example
require 'goliath'
class Hello < Goliath::API
def response(env)
[ 200, {}, "Hello, world!" ]
end
end
$ ruby hello.rb -sv -p 8000
[45584:INFO] 2013-05-28 11:52:52 :: Starting server on
0.0.0.0:8000 in development mode. Watch out for stones.
$ curl http://lvh.me:8000/
Monday, June 10, 13
9. Ubiquitous Goliath
“Hello, world” example
require 'goliath'
class Hello < Goliath::API
def response(env)
[ 200, {}, "Hello, world!" ]
end
end
$ ruby hello.rb -sv -p 8000
[45584:INFO] 2013-05-28 11:52:52 :: Starting server on
0.0.0.0:8000 in development mode. Watch out for stones.
$ curl http://lvh.me:8000/
Hello, world!
Monday, June 10, 13
10. What I needed...
a full-featured API
versioned endpoints
multiple resources
standard RESTful CRUD
JSON requests / responses
secured requests
logging
localization / translation
file attachments with S3
storage
asynchronous MySQL
queries
tests!
Monday, June 10, 13
11. What I used...
Grape
REST-like API micro-
framework for Ruby
Carrierwave, Fog, MiniMagick
file uploads and S3 support
Rspec + FactoryGirl
testing
Rabl + MultiJson
JSON requests / responses
em-synchrony + mysql2
async MySQL
globalize3
localization / translation
standalone_migrations
Rails-style migrations
capistrano
deployment stuff
Monday, June 10, 13
12. Simple Goliath + Grape
./app.rb
require 'rubygems'
require 'bundler/setup'
require 'goliath'
require 'em-synchrony/activerecord'
require 'grape'
Dir["./app/models/*.rb"].each { |f| require f }
require './app/api'
class Application < Goliath::API
def response(env)
::API.call(env)
end
end
Monday, June 10, 13
13. Simple Goliath + Grape
./app/api.rb
Dir["./app/apis/v1/*.rb"].each { |f| require f }
class API < Grape::API
mount APIv1::Unlocks
resource 'servicehealth' do
desc "Returns a basic status report."
get "/" do
MultiJson.dump({
status: 'OK',
environment: Goliath::env })
end
end
end
Monday, June 10, 13
14. Simple Goliath + Grape
./app/api.rb
Dir["./app/apis/v1/*.rb"].each { |f| require f }
class API < Grape::API
mount APIv1::Unlocks
resource 'servicehealth' do
desc "Returns a basic status report."
get "/" do
MultiJson.dump({
status: 'OK',
environment: Goliath::env })
end
end
end
Monday, June 10, 13
15. Simple Goliath + Grape
./app/apis/v1/unlocks.rb
class APIv1
class Unlocks < Grape::API
version 'v1', using: :path, format: :json
resource :unlocks do
# GET /unlocks/1.json
desc "Returns a single Unlock record by ID"
get "/:id" do
unlock = Unlock.find(params[:id])
custom_render "api_v1/unlocks/show", unlock, 200
end
end
end
end
Monday, June 10, 13
16. Simple Goliath + Grape
./app/apis/v1/unlocks.rb
class APIv1
class Unlocks < Grape::API
version 'v1', using: :path, format: :json
resource :unlocks do
# GET /unlocks/1.json
desc "Returns a single Unlock record by ID"
get "/:id" do
unlock = Unlock.find(params[:id])
custom_render "api_v1/unlocks/show", unlock, 200
end
end
end
end
Wait, what?
Monday, June 10, 13
17. Simple Goliath + Grape
back to our ./app/api.rb
Dir["./app/apis/v1/*.rb"].each { |f| require f }
class API < Grape::API
helpers do
def custom_render(rabl_template, object, status, args={})
args[:format] ||= 'json'
args[:success] ||= true
render_options = { format: args[:format] }
render_options[:locals] = args[:locals] if args[:locals]
data = Rabl::Renderer.new(rabl_template, object, render_options).render
%({ "success": #{args[:success]}, "data": #{data} })
end
end
mount APIv1::Unlocks
...
Monday, June 10, 13
18. Simple Goliath + Grape
back to our ./app/api.rb
Dir["./app/apis/v1/*.rb"].each { |f| require f }
class API < Grape::API
helpers do
def custom_render(rabl_template, object, status, args={})
args[:format] ||= 'json'
args[:success] ||= true
render_options = { format: args[:format] }
render_options[:locals] = args[:locals] if args[:locals]
data = Rabl::Renderer.new(rabl_template, object, render_options).render
%({ "success": #{args[:success]}, "data": #{data} })
end
end
mount APIv1::Unlocks
...
Monday, June 10, 13
19. Simple Goliath + Grape
back to our ./app/api.rb
Dir["./app/apis/v1/*.rb"].each { |f| require f }
class API < Grape::API
helpers do
def custom_render(rabl_template, object, status, args={})
args[:format] ||= 'json'
args[:success] ||= true
render_options = { format: args[:format] }
render_options[:locals] = args[:locals] if args[:locals]
data = Rabl::Renderer.new(rabl_template, object, render_options).render
%({ "success": #{args[:success]}, "data": #{data} })
end
end
mount APIv1::Unlocks
...
Monday, June 10, 13
20. Simple Goliath + Grape
back to our ./app/api.rb
Dir["./app/apis/v1/*.rb"].each { |f| require f }
class API < Grape::API
helpers do
def custom_render(rabl_template, object, status, args={})
args[:format] ||= 'json'
args[:success] ||= true
render_options = { format: args[:format] }
render_options[:locals] = args[:locals] if args[:locals]
data = Rabl::Renderer.new(rabl_template, object, render_options).render
%({ "success": #{args[:success]}, "data": #{data} })
end
end
mount APIv1::Unlocks
...
Monday, June 10, 13
21. Simple Goliath + Grape
back to our ./app/api.rb
Dir["./app/apis/v1/*.rb"].each { |f| require f }
class API < Grape::API
helpers do
def custom_render(rabl_template, object, status, args={})
args[:format] ||= 'json'
args[:success] ||= true
render_options = { format: args[:format] }
render_options[:locals] = args[:locals] if args[:locals]
data = Rabl::Renderer.new(rabl_template, object, render_options).render
%({ "success": #{args[:success]}, "data": #{data} })
end
end
mount APIv1::Unlocks
...
Monday, June 10, 13
22. Simple Goliath + Grape
back to our ./app/api.rb
Dir["./app/apis/v1/*.rb"].each { |f| require f }
class API < Grape::API
helpers do
def custom_render(rabl_template, object, status, args={})
args[:format] ||= 'json'
args[:success] ||= true
render_options = { format: args[:format] }
render_options[:locals] = args[:locals] if args[:locals]
data = Rabl::Renderer.new(rabl_template, object, render_options).render
%({ "success": #{args[:success]}, "data": #{data} })
end
end
mount APIv1::Unlocks
...
Monday, June 10, 13
26. Simple Goliath + Grape
./app/views/api_v1/unlocks/show.json.rabl
attributes :id, :name, :code, :description, :created_at
node(:unique_tags) { |unlock| unlock.tags.uniq }
child :images => :images do
attributes :id, :caption, :mime_type, :url
end
renders =>
{ "id":1,"name":"Fancy
Thing","code":"001FT","description":"Enim doloribus id
minima.","unique_tags":["foo","bar","woot"],"images":
[{"image":{"id":"1","caption":"dolor sit
amet","mime_type":"image/jpg","url":"http://s3.amazon.com/
whatever.jpg"}}],"created_at":1364916167.000000000 }
Monday, June 10, 13
27. Simple Goliath + Grape
./app/views/api_v1/unlocks/show.json.rabl
attributes :id, :name, :code, :description, :created_at
node(:unique_tags) { |unlock| unlock.tags.uniq }
child :images => :images do
attributes :id, :caption, :mime_type, :url
end
renders =>
{ "success": true, "data": {"id":1,"name":"Fancy
Thing","code":"001FT","description":"Enim doloribus id
minima.","unique_tags":["foo","bar","woot"],"images":
[{"image":{"id":"1","caption":"dolor sit
amet","mime_type":"image/jpg","url":"http://s3.amazon.com/
whatever.jpg"}}],"created_at":1364916167.000000000} }
Monday, June 10, 13
28. Simple Goliath + Grape
./app/models/unlock.rb
Well, that would have worked, if we had an Unlock model...
Monday, June 10, 13
29. Simple Goliath + Grape
./app/models/unlock.rb
Well, that would have worked, if we had an Unlock model...
require 'rocket_tag'
class Unlock < ActiveRecord::Base
has_many :images, as: :image_attachable, dependent: :destroy
attr_taggable :tags
validates :name, presence: true
validates :code, presence: true
end
Monday, June 10, 13
30. Simple Goliath + Grape
./app/models/unlock.rb
Well, that would have worked, if we had an Unlock model...
require 'rocket_tag'
class Unlock < ActiveRecord::Base
has_many :images, as: :image_attachable, dependent: :destroy
attr_taggable :tags
validates :name, presence: true
validates :code, presence: true
end
Ordinary ActiveRecord like you’re accustomed to...
Monday, June 10, 13
32. Simple Goliath + Grape
Try it out!
$ ruby app.rb -sv -p 8000
Monday, June 10, 13
33. Simple Goliath + Grape
Try it out!
$ ruby app.rb -sv -p 8000
[45584:INFO] 2013-05-28 11:52:52 :: Starting server on
0.0.0.0:8000 in development mode. Watch out for stones.
Monday, June 10, 13
34. Simple Goliath + Grape
Try it out!
$ ruby app.rb -sv -p 8000
[45584:INFO] 2013-05-28 11:52:52 :: Starting server on
0.0.0.0:8000 in development mode. Watch out for stones.
$ curl http://lvh.me:8000/v1/unlocks/1
Monday, June 10, 13
35. Simple Goliath + Grape
Try it out!
$ ruby app.rb -sv -p 8000
[45584:INFO] 2013-05-28 11:52:52 :: Starting server on
0.0.0.0:8000 in development mode. Watch out for stones.
$ curl http://lvh.me:8000/v1/unlocks/1
{ "success": true, "data": {"id":1,"name":"Fancy
Thing","code":"001FT","description":"Enim doloribus id
minima.","unique_tags":["foo","bar","woot"],"images":
[{"image":{"id":"1","caption":"dolor sit
amet","mime_type":"image/jpg","url":"http://s3.amazon.com/
whatever.jpg"}}],"created_at":1364916167.000000000}}} }
Monday, June 10, 13
36. Simple Goliath + Grape
“Could the API
give us users
too? That’d be
great...”
Monday, June 10, 13
37. Simple Goliath + Grape
./app/api.rb
Dir["./app/apis/v1/*.rb"].each { |f| require f }
class API < Grape::API
mount APIv1::Unlocks
resource 'servicehealth' do
desc "Returns a basic status report."
get "/" do
MultiJson.dump({
status: 'OK',
environment: Goliath::env })
end
end
end
Monday, June 10, 13
38. Simple Goliath + Grape
./app/api.rb
Dir["./app/apis/v1/*.rb"].each { |f| require f }
class API < Grape::API
mount APIv1::Unlocks
resource 'servicehealth' do
desc "Returns a basic status report."
get "/" do
MultiJson.dump({
status: 'OK',
environment: Goliath::env })
end
end
end
Remember me?
Monday, June 10, 13
39. Simple Goliath + Grape
./app/api.rb
Remember me?
Dir["./app/apis/v1/*.rb"].each { |f| require f }
class API < Grape::API
mount APIv1::Unlocks
mount APIv1::Users
resource 'servicehealth' do
desc "Returns a basic status report."
get "/" do
MultiJson.dump({
status: 'OK',
environment: Goliath::env })
end
end
end
Monday, June 10, 13
41. Need communication?
Build a Client Gem!
require 'virtus'
require 'rest_client'
require 'multi_json'
module UnlocksClient
class Unlock
include Virtus
attribute :id
attribute :name
attribute :code
attribute :description
attribute :tags
attr_accessor :media_rewards
def self.find(id, params={})
client = RestClient::Resource.new("#{BASE_URL)}/unlocks/#{id}")
response = client.get({params: params})
data = MultiJson.load(response)["data"]
return nil if !response || data.empty?
new(params)
end
end
end
Monday, June 10, 13
42. Asynch MySQL Gotcha
Used to Rails?
If you’re not paying attention, you might do this...
Monday, June 10, 13
43. Asynch MySQL Gotcha
Used to Rails?
If you’re not paying attention, you might do this...
development:
host: localhost
adapter: mysql2
database: unlocks_dev
pool: 20
timeout: 5000
reconnect: true
username: dbuser
password: 123whatever
Monday, June 10, 13
44. Asynch MySQL Gotcha
Used to Rails?
If you’re not paying attention, you might do this...
development:
host: localhost
adapter: em_mysql2
database: unlocks_dev
pool: 20
timeout: 5000
reconnect: true
username: dbuser
password: 123whatever
Monday, June 10, 13
45. Asynch MySQL Gotcha
Used to Rails?
If you’re not paying attention, you might do this...
development:
host: localhost
adapter: em_mysql2
database: unlocks_dev
pool: 20
timeout: 5000
reconnect: true
username: dbuser
password: 123whatever
YMMV
Monday, June 10, 13
46. Goliath Tips
curl is your friend on the command line.
Console mode!
ruby app.rb -svC
using Pry to debug stuff: Just add binding.pry
Monday, June 10, 13
47. Links
Goliath / Grape / EM Stuff
https://github.com/
postrank-labs/goliath
https://github.com/
igrigorik/em-synchrony
https://github.com/
intridea/grape
Matt Stuff
code.digimonkey.com
mepatterson.net
github.com/mepatterson
twitter.com/mepatterson
Monday, June 10, 13