39
GAMERS DO REST NOT REALLY

Gamers do REST - EuroPython 2014

Embed Size (px)

DESCRIPTION

An overview (sprinkled with implementation details and solutions to issues we encountered) of how Demonware uses Python and Django to build RESTful APIs and how we manage to reliably serve millions of gamers all over the world that play Activision-Blizzard’s successful franchises Call of Duty and Skylanders. Topics the presentation will touch: tech stack overview; API design; configuration handling; middleware usage for logging, metrics and error handling; authentication/authorization.

Citation preview

Page 1: Gamers do REST - EuroPython 2014

GAMERS DO RESTNOT REALLY

Page 2: Gamers do REST - EuroPython 2014

(these are real graphs, trust me)

Page 3: Gamers do REST - EuroPython 2014

(these are real graphs, trust me)

Page 4: Gamers do REST - EuroPython 2014

ABOUT MEAngel Ramboi

programmer, gamer, geek @demonware

Page 5: Gamers do REST - EuroPython 2014
Page 6: Gamers do REST - EuroPython 2014

ABOUT DEMONWAREACTIVISION-BLIZZARD

Dublin

Vancouver

Shanghai

Page 7: Gamers do REST - EuroPython 2014

WHAT DO WE DO?We enable gamers to find one another

and then shoot each other in the face.

Page 8: Gamers do REST - EuroPython 2014

WHAT WE ACTUALLY DOleaderboardsmatchmakinganti-cheataccounts managementand more …

70+ SERVICES

Page 9: Gamers do REST - EuroPython 2014
Page 10: Gamers do REST - EuroPython 2014
Page 11: Gamers do REST - EuroPython 2014

WE HIRE SUPERHEROES

demonware.net/jobs

Page 12: Gamers do REST - EuroPython 2014

TALK OVERVIEWAPI design

Process and tools

App configuration

Handling errors/logging/metrics

Authentication/authorization

Page 13: Gamers do REST - EuroPython 2014

WHY RESTinteroperability

scalability

Page 14: Gamers do REST - EuroPython 2014

API DESIGNfollowing the REST principles outlined in Roy Fielding's thesis

GET, POST, PUT, DELETE verbs for API CRUD

HTTP as the communication protocol

JSON representation

pragmatic approach: “good enough” > perfect

Page 15: Gamers do REST - EuroPython 2014

API DESIGNGET /v1.0/users/1/ HTTP/1.1Accept: application/json

{ "userName": "cmac1", "email": "[email protected]", "firstName" : "Connor", "lastName" : "MacLeod", "dateOfBirth": "1518-03-18", "country": "GB", "gender": "male", "created": "1986-03-07T13:37:00Z", "quotes": { "href": "/v1.0/users/1/quotes/", "summary": { "quotes": [ "There can be only one.", "I am Connor MacLeod of the Clan MacLeod." ] } }}

Page 16: Gamers do REST - EuroPython 2014

PROCESS AND TOOLS

Page 17: Gamers do REST - EuroPython 2014

DEVELOPING WITH AGILITYScrum or Kanban

Continuous integration

Page 18: Gamers do REST - EuroPython 2014

TECH STACKPython 2.7

Django 1.6

MySQL 5.6

CentOS 6

Apache + mod_wsgi

Page 19: Gamers do REST - EuroPython 2014

CODE AND DEPLOYMENT

Page 20: Gamers do REST - EuroPython 2014

SCHEMA MIGRATIONS

Page 21: Gamers do REST - EuroPython 2014

SCHEMA MIGRATIONSPercona Toolkit to the rescue with “online-shema-change”

www.percona.com/software/percona-toolkit

creates an already altered tablesetup triggers for insert/update/deletecopies the datarenames the tables

Downside: uses a lot of space as it duplicates all the data

Page 22: Gamers do REST - EuroPython 2014

APP CONFIGURATIONYAML 1.2---django: DEBUG: False ALLOWED_HOSTS: ["*"] TIME_ZONE: UTC LANGUAGE_CODE: en-us USE_I18N: True SECRET_KEY: "secret key :)" TEMPLATE_LOADERS: - django.template.loaders.filesystem.Loader INSTALLED_APPS: - django.contrib.contenttypes ...

Cross project & Validation

Page 23: Gamers do REST - EuroPython 2014

APP CONFIGURATIONminimum_age = Option( type={ 'type': 'integer', 'valid': [['>=', 0]] }, default=13, description='The minimum age of a user.')

Page 24: Gamers do REST - EuroPython 2014

JSON VALIDATION{ "title": "Example Schema", "type": "object", "properties": { "username": { "type": "string", "pattern": "̂[a-z0-9_-]{3,15}$" }, "age": { "description": "Age in years", "type": "integer", "minimum": 0 } }}

json-schema.orgpypi.python.org/pypi/jsonschema

Page 25: Gamers do REST - EuroPython 2014

JSON VALIDATION{ "title": "Example Schema", "type": "object", "properties": { "firstName": { "type": "string", "minLength": 2, "maxLength": 100 }, "lastName": { "type": "string", "minLength": 2, "maxLength": 100 } }, "required": ["firstName", "lastName"]}

json-schema.orgpypi.python.org/pypi/jsonschema

Page 26: Gamers do REST - EuroPython 2014

JSON VALIDATION{ "title": "Example Schema", "type": "object", "properties": { "gender": { "type": "string", "enum": ["male", "female", "other"], "exceptions": { "required": errors.GenderMissingError, "type": errors.InvalidGenderError, "enum": errors.InvalidGenderError } } }, "required": ["gender"]}

json-schema.orgpypi.python.org/pypi/jsonschema

Page 27: Gamers do REST - EuroPython 2014

ERROR HANDLINGclass ErrorHandlingMiddleware(object): def process_exception(self, request, exception): return format_and_render_error( request, exception )

Page 28: Gamers do REST - EuroPython 2014

ERROR HANDLING{ "error": { "msg": "Request data validation failed, see context for more details.", "code": 227000, "name": "Error:ClientError:InvalidRequest:DataInvalid", "context": [ { "msg": "Email [email protected] already exists", "code": 288000, "name": "Error:ClientError:Conflict:EmailExists" }, { "msg": "Username cmac1 already exists", "code": 289000, "name": "Error:ClientError:Conflict:UsernameExists" } ] }}

Page 29: Gamers do REST - EuroPython 2014

LOGGING

Page 30: Gamers do REST - EuroPython 2014

LOGGING

Page 31: Gamers do REST - EuroPython 2014

LOGGING// Bad message - not suitable/useful for production.logger.debug("Variable x={}".format(var))

// Good message - suitable for production.logger.error( "Request {request} failed unexpectedly for reason {reason} " "resulting in client error {error}" .format({ "request": req, "reason": expl, "error": client_error_code }))

Page 32: Gamers do REST - EuroPython 2014

LOGGING2014-05-10T22:58:56.394565+00:00 level=error project=highlander app=usersview=get_UsersView client=127.0.0.1 method=GET path=/v1.0/users/2msg=Error:NotFound(No user with user_id 2 could be found.There can be only one!)

Page 33: Gamers do REST - EuroPython 2014

METRICS

Page 34: Gamers do REST - EuroPython 2014

METRICSclass MetricsMiddleware(object):

def process_request(self, request): request.metrics_start_time = time.time()

def process_response(self, request, response): if hasattr(request, 'metrics_start_time'): request_time = (time.time() - request.metrics_start_time) * 1000 metrics.write( name='request_time', value=request_time ) return response

Page 35: Gamers do REST - EuroPython 2014

AUTHWe use JSON Web Tokens:

JSON Web Signature (JWS) objectsJSON Web Encryption (JWE) objects

JOSE is a framework intended to provide a method to securelytransfer claims:

github.com/Demonware/josepypi.python.org/pypi/jose

% pip install jose

Page 36: Gamers do REST - EuroPython 2014

JSON WEB SIGNATURE (JWS) OBJECTSimport josefrom time import time

claims = { 'iss': 'http://www.example.com', 'exp': int(time()) + 3600, 'sub': 42}jwk = {'k': 'password'}

jws = jose.sign(claims, jwk, alg='HS256')

JWS(header='eyJhbGciOiAiSFMyNTYifQ', payload='eyJpc3MiOiAiaHR0cDovL3d3dy5leGFtcGxlLmNvbSIsICJzdWIiOiA0MiwgImV4cCI6IDEzOTU2NzQ0Mjd9', signature='WYApAiwiKd-eDClA1fg7XFrnfHzUTgrmdRQY4M19Vr8')

jwt = jose.serialize_compact(jws)

# on the client sidejose.verify(jose.deserialize_compact(jwt), jwk)

Page 37: Gamers do REST - EuroPython 2014

JSON WEB ENCRYPTION (JWE) OBJECTSimport josefrom time import timefrom Crypto.PublicKey import RSA

claims = { 'iss': 'http://www.example.com', 'exp': int(time()) + 3600, 'sub': 42}key = RSA.generate(2048)pub_jwk = {'k': key.publickey().exportKey('PEM')}

jwe = jose.encrypt(claims, pub_jwk)

JWE(header='eyJhbGciOiAiUlNBLU9BRVAiLCAiZW5jIjogIkExMjhDQkMtSFMyNTYifQ', cek='SsLgP2bNKYDYGzHvLYY7rsVEBHSms6_jW-WfglHqD9giJhWwrOwqLZOaoOycsf_EBJCkHq9-vbxRb7WiNdy_C9J0_RnRRBGII6z_G4bVb18bkbJMeZMV6vpUut_iuRWoct_weg_VZ3iR2xMbl-yE8Hnc63pAGJcIwngfZ3sMX8rBeni_koxCc88LhioP8zRQxNkoNpvw-kTCz0xv6SU_zL8p79_-_2zilVyMt76Pc7WV46iI3EWIvP6SG04sguaTzrDXCLp6ykLGaXB7NRFJ5PJ9Lmh5yinAJzCdWQ-4XKKkNPorSiVmRiRSQ4z0S2eo2LtvqJhXCrghKpBNgbtnJQ', iv='Awelp3ryBVpdFhRckQ-KKw', ciphertext='1MyZ-3nky1EFO4UgTB-9C2EHpYh1Z-ij0RbiuuMez70nIH7uqL9hlhskutO0oPjqdpmNc9glSmO9pheMH2DVag', tag='Xccck85XZMvG-fAJ6oDnAw')

jwt = jose.serialize_compact(jwe)

# on the client sidepriv_jwk = {'k': key.exportKey('PEM')}jwt = jose.decrypt(jose.deserialize_compact(jwt), priv_jwk)

Page 38: Gamers do REST - EuroPython 2014

SUMMARYRest is awesome

Be Pragmatic

Monitor Everything

We are hiring!!!