Slide 1

Slide 1 text

Interfaces Will Save the Future Case Study: Sphinx Rate Limiter 21 Jan 2015 Mohit Gupta & Alex Zylman Clever

Slide 2

Slide 2 text

What is Sphinx? HTTP rate limiter Based on leakybucket algorithm (supporting both Redis and in-memory stores) Supports matching on request headers and IP address Transparent limiting through X-Rate-Limit headers Low request latencies (~15 ms)

Slide 3

Slide 3 text

Limiting requests Sphinx allows you to set any number of limits, where a limit defines: How many requests to allow in an interval Which requests this limit applies to (e.g. only a specific path, IP, or header) What part of the request you're ratelimiting by (e.g. IP, user)

Slide 4

Slide 4 text

♥ interfaces ♥

Slide 5

Slide 5 text

♥ interfaces in Go ♥ Interfaces are sets of methods Any struct with the set of methods implements the interface type LimitKey interface { Key(common.Request) (string, error) Type() string } type ipLimitKey struct{} func (ilk ipLimitKey) Type() string { return "ip" } func (ilk ipLimitKey) Key(request common.Request) (string, error) { if _, ok := request["remoteaddr"]; !ok { return "", EmptyKeyError{ilk, "No remoteaddr key in request"} } return "ip:" + request["remoteaddr"].(string), nil }

Slide 6

Slide 6 text

♥ interfaces in Go♥ Improved collaboration Easier testing Swappable behaviors

Slide 7

Slide 7 text

Collaboration through interfaces

Slide 8

Slide 8 text

Collaboration through interfaces "Who works on what" and "where do you start" is a hard problem Desire: Working on different parts simultaneously Flexibility in deciding what to work on Not be blocked by your teammates

Slide 9

Slide 9 text

Collaboration through interfaces: Example With defined interfaces, you can use mock implementations // A RateLimiter adds a request to a rate limiting bucket and returns the result. type RateLimiter interface { Add(request common.Request) ([]Status, error) } // A Status contains the result of adding a request to our limiting buckets. type Status struct { Capacity uint Reset time.Time Remaining uint Name string }

Slide 10

Slide 10 text

Collaboration through interfaces: Example import ( "github.com/stretchr/testify/mock" ) type MockRateLimiter struct { *mock.Mock } func (r *MockRateLimiter) Add(request common.Request) ([]Status, error) { args := r.Mock.Called(request) return args.Get(0).([]Status), args.Error(1) } // Verify at compile time that MockRateLimiter implements the RateLimiter interface var _ RateLimiter = &MockRateLimiter{}

Slide 11

Slide 11 text

Collaboration through interfaces: Example func DoSomethingWithRateLimiter(limiter RateLimiter) error { _, err := limiter.Add(common.Request{"path": "/debug"}) return err } var tests = []struct{ error }{ {error: nil}, {error: errors.New("garbage")}, } func TestDoingSomethingWithRateLimiter(t *testing.T) { for _, test := range tests { limiter := &MockRateLimiter{Mock: new(mock.Mock)} limiter.On("Add", common.Request{"path": "/debug"}).Return([]Status{}, test.error) assert.Equal(t, DoSomethingWithRateLimiter(limiter), test.error) limiter.AssertExpectations(t) } }

Slide 12

Slide 12 text

Easier testing

Slide 13

Slide 13 text

Easier testing Have different implementations of the same interface Desire: - Different implementations stay in sync - Bugs fixed in one implementation are fixed in all

Slide 14

Slide 14 text

Easier testing: Example Write tests against the interface so each implementation can reuse them A test case for a bug will ensure all implementations are bug-free type Storage interface { Create(name string, capacity uint, rate time.Duration) (Bucket, error) }

Slide 15

Slide 15 text

Easier testing: Example func CreateTest(s Storage) func(*testing.T) { return func(t *testing.T) { now := time.Now() bucket, err := s.Create("testbucket", 100, time.Minute) if err != nil { t.Fatal(err) } if capacity := bucket.Capacity(); capacity != 100 { t.Fatalf("expected capacity of %d, got %d", 100, capacity) } e := float64(1 * time.Second) // margin of error if error := float64(bucket.Reset().Sub(now.Add(time.Minute))); math.Abs(error) > e { t.Fatalf("expected reset time close to %s, got %s", now.Add(time.Minute), bucket.Reset()) } } }

Slide 16

Slide 16 text

Easier testing: Example Tests for the memory implementation: func TestCreate(t *testing.T) { CreateTest(New())(t) } Tests for the redis implementation: func TestCreate(t *testing.T) { s, err := New("tcp", os.Getenv("REDIS_URL")) assert.Nil(t, err) CreateTest(s)(t) }

Slide 17

Slide 17 text

Swappable behaviors

Slide 18

Slide 18 text

Swappable behaviors Desire: Change behavior with just config type Handler interface { ServeHTTP(ResponseWriter, *Request) } func NewHTTPLimiter(rateLimiter ratelimiter.RateLimiter, proxy http.Handler) http.Handler { return &httpRateLimiter{rateLimiter: rateLimiter, proxy: proxy} } func NewHTTPLogger(rateLimiter ratelimiter.RateLimiter, proxy http.Handler) http.Handler { return &httpRateLogger{rateLimiter: rateLimiter, proxy: proxy} }

Slide 19

Slide 19 text

Plugins / components Support matching and limiting on different parts of the request Desire: Easily add Matchers and Keys without diving into the whole system Each component responsible for it's own configuration

Slide 20

Slide 20 text

Plugins / components: Example // A Matcher determines if a common.Request matches its requirements. type Matcher interface { Match(common.Request) bool } // A MatcherFactory creates a Matcher based on a config. type MatcherFactory interface { Type() string Create(config interface{}) (Matcher, error) } type pathMatcherConfig struct { MatchAny []string `yaml:"match_any"` } type pathMatcher struct { Paths []*regexp.Regexp } type pathMatcherFactory struct{} func (pmf pathMatcherFactory) Type() string { return "paths" } func (pmf pathMatcherFactory) Create(config interface{}) (Matcher, error) { ...

Slide 21

Slide 21 text

Bonus

Slide 22

Slide 22 text

Bonus: Adding dynamic configuration reload type daemon struct { handler http.Handler proxy config.Proxy } +func (d *daemon) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + d.handler.ServeHTTP(rw, req) +} + func (d *daemon) Start() { log.Printf("Listening on %s", d.proxy.Listen) - log.Fatal(http.ListenAndServe(d.proxy.Listen, d.handler)) + log.Fatal(http.ListenAndServe(d.proxy.Listen, d)) return } func (d *daemon) LoadConfig(newConfig config.Config) error { d.proxy = newConfig.Proxy target, _ := url.Parse(d.proxy.Host) // already tested for invalid Host proxy := httputil.NewSingleHostReverseProxy(target) ...

Slide 23

Slide 23 text

Thank you 21 Jan 2015 Mohit Gupta & Alex Zylman @mohitgupta (http://twitter.com/mohitgupta) @amzylman (http://twitter.com/amzylman) Clever https://clever.com (https://clever.com) https://github.com/Clever (https://github.com/Clever)