View
209
Download
5
Category
Tags:
Preview:
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
GAMERS DO RESTNOT REALLY
(these are real graphs, trust me)
(these are real graphs, trust me)
ABOUT DEMONWAREACTIVISION-BLIZZARD
Dublin
Vancouver
Shanghai
WHAT DO WE DO?We enable gamers to find one another
and then shoot each other in the face.
WHAT WE ACTUALLY DOleaderboardsmatchmakinganti-cheataccounts managementand more …
70+ SERVICES
TALK OVERVIEWAPI design
Process and tools
App configuration
Handling errors/logging/metrics
Authentication/authorization
WHY RESTinteroperability
scalability
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
API DESIGNGET /v1.0/users/1/ HTTP/1.1Accept: application/json
{ "userName": "cmac1", "email": "cmacleod@highlander.org", "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." ] } }}
PROCESS AND TOOLS
DEVELOPING WITH AGILITYScrum or Kanban
Continuous integration
TECH STACKPython 2.7
Django 1.6
MySQL 5.6
CentOS 6
Apache + mod_wsgi
CODE AND DEPLOYMENT
SCHEMA MIGRATIONS
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
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
APP CONFIGURATIONminimum_age = Option( type={ 'type': 'integer', 'valid': [['>=', 0]] }, default=13, description='The minimum age of a user.')
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
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
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
ERROR HANDLINGclass ErrorHandlingMiddleware(object): def process_exception(self, request, exception): return format_and_render_error( request, exception )
ERROR HANDLING{ "error": { "msg": "Request data validation failed, see context for more details.", "code": 227000, "name": "Error:ClientError:InvalidRequest:DataInvalid", "context": [ { "msg": "Email cmacleod@highlander.org already exists", "code": 288000, "name": "Error:ClientError:Conflict:EmailExists" }, { "msg": "Username cmac1 already exists", "code": 289000, "name": "Error:ClientError:Conflict:UsernameExists" } ] }}
LOGGING
LOGGING
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 }))
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!)
METRICS
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
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
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)
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)
SUMMARYRest is awesome
Be Pragmatic
Monitor Everything
We are hiring!!!
QUESTIONS?
aramboi@demonware.net@angelramboi
Recommended