Slide 1

Slide 1 text

No content

Slide 2

Slide 2 text

About me Andriy Savchenko /ptico CTO Aejis [email protected]

Slide 3

Slide 3 text

RubyMeditation

Slide 4

Slide 4 text

W A R N I N G THIS TALK CONTAINS LOTS
 OF CODE THIS IS YOUR LAST CHANCE TO LEAVE AUDITORY

Slide 5

Slide 5 text

Problems with rails

Slide 6

Slide 6 text

• Low latency • Dependency hell • MVC is only suitable for simple CRUD • ActiveSupport

Slide 7

Slide 7 text

Other frameworks

Slide 8

Slide 8 text

• grape • sinatra • rum • nyny

Slide 9

Slide 9 text

And…

Slide 10

Slide 10 text

Rack

Slide 11

Slide 11 text

run ->(env) { [ 200, # <= Response code {'Content-Type' => 'application/json'}, # <= Headers [ '{"a": 1}' ] # <= Body ] }

Slide 12

Slide 12 text

require 'json' run ->(env) { [ 200, {'Content-Type' => 'application/json'}, [ JSON.dump({ a: 1 }) ] # <= Almost API ;) ] }

Slide 13

Slide 13 text

Add some OOP

Slide 14

Slide 14 text

run ->(env) { [ 200, # <= Response code {'Content-Type' => 'application/json'}, # <= Headers [ '{"a": 1}' ] # <= Body ] }

Slide 15

Slide 15 text

class Responder def response_code 200 end def headers {'Content-Type' => 'application/json'} end def body [ JSON.dump({ a: 1 }) ] end end

Slide 16

Slide 16 text

class Responder def response_code @code end def headers @headers end def body [ JSON.dump(@body) ] end end

Slide 17

Slide 17 text

class Example < Responder def initialize(env) @code = 200 @headers = {'Content-Type' => 'application/json'} @body = { a: 1 } end end

Slide 18

Slide 18 text

class ReadUsers < Responder def initialize(env) @code = 200 @headers = {'Content-Type' => 'application/json'} @body = DB[:users].all end end

Slide 19

Slide 19 text

run ->(env) { result = ReadUsers.new(env) [result.response_code, result.headers, result.body] }

Slide 20

Slide 20 text

result = ReadUsers.new(env) r = Nginx::Request.new result.headers.each_pair { |k, v| r.headers_out[k] = v } Nginx.rputs result.body[0] Nginx.return result.response_code

Slide 21

Slide 21 text

Less generic example

Slide 22

Slide 22 text

Good API • Proper status codes • Compatibility (?suppress_response_code=true) • Metadata

Slide 23

Slide 23 text

class Responder class << self def call(env) req = ::Rack::Request.new(env) instance = new(req) instance.call instance.to_rack_array end end attr_reader :request, :params, :headers def initialize(req) @request = req @params = req.params @headers = default_response_headers end def call; end def to_rack_array [http_response_code, http_response_headers, http_response_body] end end

Slide 24

Slide 24 text

class Responder def response_code @response_code || default_response_code end private def default_response_code 200 end def http_response_code params['suppress_response_codes'] ? 200 : response_code end end

Slide 25

Slide 25 text

class Responder def default_response_headers { 'Content-Type' => 'application/json' }.dup end def http_response_headers @headers end end

Slide 26

Slide 26 text

class Responder def body @body end private def http_response_body [ JSON.dump(body) ] end end

Slide 27

Slide 27 text

class ReadUsers < Responder def call @body = DB[:users].all end end

Slide 28

Slide 28 text

class Read < Responder def call @body = fetch end end

Slide 29

Slide 29 text

class ReadUsers < Read def fetch DB[:users].all end end

Slide 30

Slide 30 text

class Write < Responder def call @body = valid_params? ? success : failure end private def success; end def failure; end def valid_params? true end end

Slide 31

Slide 31 text

class CreateUser < Write def default_response_code 201 end def valid_params? params['login'] && params['email'] end def success DB[:users].insert(params) end def failure @response_code = 400 { error: 'Invalid params' } end end

Slide 32

Slide 32 text

class Responder def body { code: http_response_code, result: @body, meta: meta } end def meta { server_time: Time.now.to_i } end end

Slide 33

Slide 33 text

{ "code": 200, "result": [ { "id": 1, "name": "Andriy Savchenko", "email": "[email protected]", "company": "Aejis", "hiring": true } ], "meta": { "server_time": 1447939835 } }

Slide 34

Slide 34 text

Awesome!

Slide 35

Slide 35 text

Routers • Rack::Builder • http_router (gh:joshbuddy/http_router) • lotus-router (gh:lotus/router) • signpost (gh:Ptico/signpost) • journey (dead)

Slide 36

Slide 36 text

Advantages

Slide 37

Slide 37 text

Faster $ ruby -v ruby 2.2.3p173 (2015-08-18 revision 51636) [x86_64-darwin15] $ puma -e production $ ab -n 10000 -c 100 http://0.0.0.0:9292/users/ |======================|====Rails-API====|=====Sinatra=====|=====Rack API====| |Time taken for tests: | 13.262 seconds | 6.858 seconds | 3.665 seconds | |Complete requests: | 10000 | 10000 | 10000 | |Failed requests: | 0 | 0 | 0 | |Requests per second: | 754.03 [#/sec] | 1458.20 [#/sec] | 2728.28 [#/sec] | |Time per request: | 132.620 [ms] | 68.578 [ms] | 36.653 [ms] | |Time per request (c): | 1.326 [ms] | 0.686 [ms] | 0.367 [ms] | |Transfer rate: | 301.91 [KB/sec] | 262.02 [KB/sec] | 402.31 [KB/sec] | |============================================================================|

Slide 38

Slide 38 text

Faster • 4x faster then rails-api & 2x then sinatra • Ready for further improvements

Slide 39

Slide 39 text

Magic-less • Base responder takes ≈ 65LOC • The only dependency is Rack (optional)

Slide 40

Slide 40 text

Maintainable • Stable object interface • Each responder can have its own file structure • SOLID • Test-friendly

Slide 41

Slide 41 text

Questions? Credits and attributions: • Title illustration by Max Bohdanowski • Lobster Two font by Pablo Impallari & Igino Marini (OFL) • Font Awesome by Dave Gandy - http://fontawesome.io (OFL) • https://www.flickr.com/photos/mattsh/14194586111/ (CC BY-NC-SA 2.0) Andriy Savchenko /ptico [email protected]