├── .travis.yml ├── TODO.md ├── cache ├── README.md ├── inmemory_test.go ├── redis_test.go ├── memcached_test.go ├── serialization.go ├── inmemory.go ├── serialization_test.go ├── memcached.go ├── groupcache.go ├── cache.go ├── redis.go └── cache_test.go ├── mock_RiemannClient.go ├── mock_Client.go ├── mock_Responder.go ├── mock_Transport.go ├── mock_Stats.go ├── mock_CacheBackend.go ├── categorize.go ├── mock_StatsdClient.go ├── proxy.go ├── proxy_test.go ├── interfaces.go ├── categorize_test.go ├── LICENSE ├── http_test.go ├── http.go ├── testutils.go ├── upstream.go ├── cache_backend.go ├── cache_backend_test.go ├── collapse.go ├── cache.go ├── upstream_test.go ├── cmd └── templar │ └── templar.go ├── stats.go ├── stats_test.go ├── collapse_test.go ├── cache_test.go └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.3 4 | - 1.4 5 | - tip 6 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | * Use go-metrics to track stats internally with export 2 | * HTTP endpoint with json stream of requests as they happen 3 | * Endpoint throttling 4 | -------------------------------------------------------------------------------- /cache/README.md: -------------------------------------------------------------------------------- 1 | This code was lovely extracted from revel (github.com/revel/revel/cache) and 2 | the revel only bits (not much) were removed to make it standalone. 3 | -------------------------------------------------------------------------------- /mock_RiemannClient.go: -------------------------------------------------------------------------------- 1 | package templar 2 | 3 | import "github.com/stretchr/testify/mock" 4 | import "github.com/amir/raidman" 5 | 6 | type MockRiemannClient struct { 7 | mock.Mock 8 | } 9 | 10 | func (r *MockRiemannClient) Send(e *raidman.Event) error { 11 | r.Called(e) 12 | return nil 13 | } 14 | -------------------------------------------------------------------------------- /mock_Client.go: -------------------------------------------------------------------------------- 1 | package templar 2 | 3 | import "github.com/stretchr/testify/mock" 4 | 5 | import "net/http" 6 | 7 | type MockClient struct { 8 | mock.Mock 9 | } 10 | 11 | func (m *MockClient) Forward(res Responder, req *http.Request) error { 12 | ret := m.Called(res, req) 13 | 14 | r0 := ret.Error(0) 15 | 16 | return r0 17 | } 18 | -------------------------------------------------------------------------------- /mock_Responder.go: -------------------------------------------------------------------------------- 1 | package templar 2 | 3 | import "github.com/stretchr/testify/mock" 4 | 5 | import "io" 6 | import "net/http" 7 | 8 | type MockResponder struct { 9 | mock.Mock 10 | } 11 | 12 | func (m *MockResponder) Send(resp *http.Response) io.Writer { 13 | ret := m.Called(resp) 14 | 15 | r0 := ret.Get(0).(io.Writer) 16 | 17 | return r0 18 | } 19 | -------------------------------------------------------------------------------- /mock_Transport.go: -------------------------------------------------------------------------------- 1 | package templar 2 | 3 | import "github.com/stretchr/testify/mock" 4 | 5 | import "net/http" 6 | 7 | type MockTransport struct { 8 | mock.Mock 9 | } 10 | 11 | func (m *MockTransport) RoundTrip(_a0 *http.Request) (*http.Response, error) { 12 | ret := m.Called(_a0) 13 | 14 | r0 := ret.Get(0).(*http.Response) 15 | r1 := ret.Error(1) 16 | 17 | return r0, r1 18 | } 19 | func (m *MockTransport) CancelRequest(req *http.Request) { 20 | m.Called(req) 21 | } 22 | -------------------------------------------------------------------------------- /mock_Stats.go: -------------------------------------------------------------------------------- 1 | package templar 2 | 3 | import "github.com/stretchr/testify/mock" 4 | 5 | import "net/http" 6 | import "time" 7 | 8 | type MockStats struct { 9 | mock.Mock 10 | } 11 | 12 | func (m *MockStats) StartRequest(req *http.Request) { 13 | m.Called(req) 14 | } 15 | func (m *MockStats) Emit(req *http.Request, dur time.Duration) { 16 | m.Called(req, dur) 17 | } 18 | func (m *MockStats) RequestTimeout(req *http.Request, timeout time.Duration) { 19 | m.Called(req, timeout) 20 | } 21 | -------------------------------------------------------------------------------- /mock_CacheBackend.go: -------------------------------------------------------------------------------- 1 | package templar 2 | 3 | import "github.com/stretchr/testify/mock" 4 | 5 | import "net/http" 6 | 7 | type MockCacheBackend struct { 8 | mock.Mock 9 | } 10 | 11 | func (m *MockCacheBackend) Set(req *http.Request, resp *http.Response) { 12 | m.Called(req, resp) 13 | } 14 | func (m *MockCacheBackend) Get(req *http.Request) (*http.Response, bool) { 15 | ret := m.Called(req) 16 | 17 | r0 := ret.Get(0).(*http.Response) 18 | r1 := ret.Get(1).(bool) 19 | 20 | return r0, r1 21 | } 22 | -------------------------------------------------------------------------------- /categorize.go: -------------------------------------------------------------------------------- 1 | package templar 2 | 3 | import "net/http" 4 | 5 | type Categorizer struct{} 6 | 7 | const CategoryHeader = "X-Templar-Category" 8 | 9 | func NewCategorizer() *Categorizer { 10 | return &Categorizer{} 11 | } 12 | 13 | func (c *Categorizer) Stateless(req *http.Request) bool { 14 | explicit := req.Header.Get(CategoryHeader) 15 | 16 | switch explicit { 17 | case "stateful": 18 | return false 19 | case "stateless": 20 | return true 21 | default: 22 | if req.Method == "GET" { 23 | return true 24 | } 25 | } 26 | 27 | return false 28 | } 29 | -------------------------------------------------------------------------------- /mock_StatsdClient.go: -------------------------------------------------------------------------------- 1 | package templar 2 | 3 | import "github.com/stretchr/testify/mock" 4 | 5 | import "time" 6 | 7 | type MockStatsdClient struct { 8 | mock.Mock 9 | } 10 | 11 | func (m *MockStatsdClient) Incr(name string, count int64) error { 12 | ret := m.Called(name, count) 13 | 14 | r0 := ret.Error(0) 15 | 16 | return r0 17 | } 18 | func (m *MockStatsdClient) GaugeDelta(name string, delta int64) error { 19 | ret := m.Called(name, delta) 20 | 21 | r0 := ret.Error(0) 22 | 23 | return r0 24 | } 25 | func (m *MockStatsdClient) PrecisionTiming(name string, t time.Duration) error { 26 | ret := m.Called(name, t) 27 | 28 | r0 := ret.Error(0) 29 | 30 | return r0 31 | } 32 | -------------------------------------------------------------------------------- /proxy.go: -------------------------------------------------------------------------------- 1 | package templar 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "time" 7 | ) 8 | 9 | type Proxy struct { 10 | client Client 11 | stats Stats 12 | } 13 | 14 | func NewProxy(cl Client, stats Stats) *Proxy { 15 | return &Proxy{cl, stats} 16 | } 17 | 18 | type copyResonder struct { 19 | w http.ResponseWriter 20 | } 21 | 22 | func (c *copyResonder) Send(res *http.Response) io.Writer { 23 | for k, v := range res.Header { 24 | c.w.Header()[k] = v 25 | } 26 | 27 | c.w.WriteHeader(res.StatusCode) 28 | 29 | return c.w 30 | } 31 | 32 | func (p *Proxy) ServeHTTP(res http.ResponseWriter, req *http.Request) { 33 | start := time.Now() 34 | 35 | p.stats.StartRequest(req) 36 | 37 | p.client.Forward(©Resonder{res}, req) 38 | 39 | p.stats.Emit(req, time.Since(start)) 40 | } 41 | -------------------------------------------------------------------------------- /proxy_test.go: -------------------------------------------------------------------------------- 1 | package templar 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/mock" 9 | "github.com/stretchr/testify/require" 10 | "github.com/vektra/neko" 11 | ) 12 | 13 | func TestProxy(t *testing.T) { 14 | n := neko.Start(t) 15 | 16 | var ( 17 | client MockClient 18 | stats MockStats 19 | proxy *Proxy 20 | ) 21 | 22 | n.CheckMock(&client.Mock) 23 | n.CheckMock(&stats.Mock) 24 | 25 | n.Setup(func() { 26 | proxy = NewProxy(&client, &stats) 27 | }) 28 | 29 | n.It("sends the request on to the target", func() { 30 | req, err := http.NewRequest("GET", "http://google.com/foo/bar", nil) 31 | require.NoError(t, err) 32 | 33 | res := httptest.NewRecorder() 34 | 35 | stats.On("StartRequest", req).Return(nil) 36 | client.On("Forward", mock.Anything, req).Return(nil) 37 | stats.On("Emit", req, mock.Anything).Return(nil) 38 | 39 | proxy.ServeHTTP(res, req) 40 | }) 41 | 42 | n.Meow() 43 | } 44 | -------------------------------------------------------------------------------- /cache/inmemory_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | var newInMemoryCache = func(_ *testing.T, defaultExpiration time.Duration) Cache { 9 | return NewInMemoryCache(defaultExpiration) 10 | } 11 | 12 | // Test typical cache interactions 13 | func TestInMemoryCache_TypicalGetSet(t *testing.T) { 14 | typicalGetSet(t, newInMemoryCache) 15 | } 16 | 17 | // Test the increment-decrement cases 18 | func TestInMemoryCache_IncrDecr(t *testing.T) { 19 | incrDecr(t, newInMemoryCache) 20 | } 21 | 22 | func TestInMemoryCache_Expiration(t *testing.T) { 23 | expiration(t, newInMemoryCache) 24 | } 25 | 26 | func TestInMemoryCache_EmptyCache(t *testing.T) { 27 | emptyCache(t, newInMemoryCache) 28 | } 29 | 30 | func TestInMemoryCache_Replace(t *testing.T) { 31 | testReplace(t, newInMemoryCache) 32 | } 33 | 34 | func TestInMemoryCache_Add(t *testing.T) { 35 | testAdd(t, newInMemoryCache) 36 | } 37 | 38 | func TestInMemoryCache_GetMulti(t *testing.T) { 39 | testGetMulti(t, newInMemoryCache) 40 | } 41 | -------------------------------------------------------------------------------- /interfaces.go: -------------------------------------------------------------------------------- 1 | package templar 2 | 3 | import ( 4 | "github.com/amir/raidman" 5 | "io" 6 | "net/http" 7 | "time" 8 | ) 9 | 10 | type Responder interface { 11 | Send(resp *http.Response) io.Writer 12 | } 13 | 14 | type Client interface { 15 | Forward(res Responder, req *http.Request) error 16 | } 17 | 18 | type Stats interface { 19 | StartRequest(req *http.Request) 20 | Emit(req *http.Request, dur time.Duration) 21 | RequestTimeout(req *http.Request, timeout time.Duration) 22 | } 23 | 24 | type Transport interface { 25 | RoundTrip(*http.Request) (*http.Response, error) 26 | CancelRequest(req *http.Request) 27 | } 28 | 29 | type Fallback interface { 30 | Fallback(*http.Request) (*http.Response, error) 31 | } 32 | 33 | type CacheBackend interface { 34 | Set(req *http.Request, resp *http.Response) 35 | Get(req *http.Request) (*http.Response, bool) 36 | } 37 | 38 | type Finisher interface { 39 | Finish() 40 | } 41 | 42 | type StatsdClient interface { 43 | Incr(name string, count int64) error 44 | GaugeDelta(name string, delta int64) error 45 | PrecisionTiming(name string, t time.Duration) error 46 | } 47 | 48 | type RiemannClient interface { 49 | Send(*raidman.Event) error 50 | } 51 | -------------------------------------------------------------------------------- /cache/redis_test.go: -------------------------------------------------------------------------------- 1 | // +build redis 2 | 3 | package cache 4 | 5 | import ( 6 | "net" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | // These tests require redis server running on localhost:6379 (the default) 12 | const redisTestServer = "localhost:6379" 13 | 14 | var newRedisCache = func(t *testing.T, defaultExpiration time.Duration) Cache { 15 | c, err := net.Dial("tcp", redisTestServer) 16 | if err == nil { 17 | c.Write([]byte("flush_all\r\n")) 18 | c.Close() 19 | redisCache := NewRedisCache(redisTestServer, "", defaultExpiration) 20 | redisCache.Flush() 21 | return redisCache 22 | } 23 | t.Errorf("couldn't connect to redis on %s", redisTestServer) 24 | t.FailNow() 25 | panic("") 26 | } 27 | 28 | func TestRedisCache_TypicalGetSet(t *testing.T) { 29 | typicalGetSet(t, newRedisCache) 30 | } 31 | 32 | func TestRedisCache_IncrDecr(t *testing.T) { 33 | incrDecr(t, newRedisCache) 34 | } 35 | 36 | func TestRedisCache_Expiration(t *testing.T) { 37 | expiration(t, newRedisCache) 38 | } 39 | 40 | func TestRedisCache_EmptyCache(t *testing.T) { 41 | emptyCache(t, newRedisCache) 42 | } 43 | 44 | func TestRedisCache_Replace(t *testing.T) { 45 | testReplace(t, newRedisCache) 46 | } 47 | 48 | func TestRedisCache_Add(t *testing.T) { 49 | testAdd(t, newRedisCache) 50 | } 51 | 52 | func TestRedisCache_GetMulti(t *testing.T) { 53 | testGetMulti(t, newRedisCache) 54 | } 55 | -------------------------------------------------------------------------------- /cache/memcached_test.go: -------------------------------------------------------------------------------- 1 | // +build memcache 2 | 3 | package cache 4 | 5 | import ( 6 | "net" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | // These tests require memcached running on localhost:11211 (the default) 12 | const testServer = "localhost:11211" 13 | 14 | var newMemcachedCache = func(t *testing.T, defaultExpiration time.Duration) Cache { 15 | c, err := net.Dial("tcp", testServer) 16 | if err == nil { 17 | c.Write([]byte("flush_all\r\n")) 18 | c.Close() 19 | return NewMemcachedCache([]string{testServer}, defaultExpiration) 20 | } 21 | t.Errorf("couldn't connect to memcached on %s", testServer) 22 | t.FailNow() 23 | panic("") 24 | } 25 | 26 | func TestMemcachedCache_TypicalGetSet(t *testing.T) { 27 | typicalGetSet(t, newMemcachedCache) 28 | } 29 | 30 | func TestMemcachedCache_IncrDecr(t *testing.T) { 31 | incrDecr(t, newMemcachedCache) 32 | } 33 | 34 | func TestMemcachedCache_Expiration(t *testing.T) { 35 | expiration(t, newMemcachedCache) 36 | } 37 | 38 | func TestMemcachedCache_EmptyCache(t *testing.T) { 39 | emptyCache(t, newMemcachedCache) 40 | } 41 | 42 | func TestMemcachedCache_Replace(t *testing.T) { 43 | testReplace(t, newMemcachedCache) 44 | } 45 | 46 | func TestMemcachedCache_Add(t *testing.T) { 47 | testAdd(t, newMemcachedCache) 48 | } 49 | 50 | func TestMemcachedCache_GetMulti(t *testing.T) { 51 | testGetMulti(t, newMemcachedCache) 52 | } 53 | -------------------------------------------------------------------------------- /categorize_test.go: -------------------------------------------------------------------------------- 1 | package templar 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | "github.com/vektra/neko" 10 | ) 11 | 12 | func TestCategorize(t *testing.T) { 13 | n := neko.Start(t) 14 | 15 | n.It("indicates that a GET is stateless", func() { 16 | req, err := http.NewRequest("GET", "http://google.com/foo/bar", nil) 17 | require.NoError(t, err) 18 | 19 | cat := NewCategorizer() 20 | 21 | assert.True(t, cat.Stateless(req)) 22 | }) 23 | 24 | n.It("indicates that a POST is not stateless", func() { 25 | req, err := http.NewRequest("POST", "http://google.com/foo/bar", nil) 26 | require.NoError(t, err) 27 | 28 | cat := NewCategorizer() 29 | 30 | assert.False(t, cat.Stateless(req)) 31 | }) 32 | 33 | n.It("honors a header to override behavior on be stateful", func() { 34 | req, err := http.NewRequest("GET", "http://google.com/foo/bar", nil) 35 | require.NoError(t, err) 36 | 37 | req.Header.Add(CategoryHeader, "stateful") 38 | 39 | cat := NewCategorizer() 40 | 41 | assert.False(t, cat.Stateless(req)) 42 | }) 43 | 44 | n.It("honors a header to override behavior to be stateless", func() { 45 | req, err := http.NewRequest("POST", "http://google.com/foo/bar", nil) 46 | require.NoError(t, err) 47 | 48 | req.Header.Add(CategoryHeader, "stateless") 49 | 50 | cat := NewCategorizer() 51 | 52 | assert.True(t, cat.Stateless(req)) 53 | }) 54 | 55 | n.Meow() 56 | } 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Vektra 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | * Neither the name of Vektra nor the names of its contributors 13 | may be used to endorse or promote products derived from this software 14 | without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /http_test.go: -------------------------------------------------------------------------------- 1 | package templar 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | "github.com/vektra/neko" 9 | ) 10 | 11 | func TestHTTP(t *testing.T) { 12 | n := neko.Start(t) 13 | 14 | var ( 15 | mockTrans MockTransport 16 | ) 17 | 18 | n.CheckMock(&mockTrans.Mock) 19 | 20 | n.It("does not send templar headers", func() { 21 | req, err := http.NewRequest("GET", "http://google.com/foo/bar", nil) 22 | require.NoError(t, err) 23 | 24 | req.Header.Set(CategoryHeader, "funky") 25 | 26 | trans := &HTTPTransport{&mockTrans} 27 | 28 | resp := &http.Response{ 29 | Request: req, 30 | StatusCode: 304, 31 | Status: "304 Too Funky", 32 | } 33 | 34 | exp, err := http.NewRequest("GET", "http://google.com/foo/bar", nil) 35 | require.NoError(t, err) 36 | 37 | mockTrans.On("RoundTrip", exp).Return(resp, nil) 38 | 39 | _, err = trans.RoundTrip(req) 40 | require.NoError(t, err) 41 | }) 42 | 43 | n.It("upgrades to https on request", func() { 44 | req, err := http.NewRequest("GET", "http://google.com/foo/bar", nil) 45 | require.NoError(t, err) 46 | 47 | req.Header.Set(UpgradeHeader, "https") 48 | 49 | trans := &HTTPTransport{&mockTrans} 50 | 51 | resp := &http.Response{ 52 | Request: req, 53 | StatusCode: 304, 54 | Status: "304 Too Funky", 55 | } 56 | 57 | exp, err := http.NewRequest("GET", "https://google.com/foo/bar", nil) 58 | require.NoError(t, err) 59 | 60 | mockTrans.On("RoundTrip", exp).Return(resp, nil) 61 | 62 | _, err = trans.RoundTrip(req) 63 | require.NoError(t, err) 64 | }) 65 | 66 | n.Meow() 67 | } 68 | -------------------------------------------------------------------------------- /http.go: -------------------------------------------------------------------------------- 1 | package templar 2 | 3 | import ( 4 | "io" 5 | "net" 6 | "net/http" 7 | "net/url" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | const UpgradeHeader = "X-Templar-Upgrade" 13 | 14 | func NewHTTPTransport() Transport { 15 | return &HTTPTransport{ 16 | &http.Transport{ 17 | Dial: (&net.Dialer{ 18 | Timeout: 30 * time.Second, 19 | KeepAlive: 30 * time.Second, 20 | }).Dial, 21 | TLSHandshakeTimeout: 10 * time.Second, 22 | }, 23 | } 24 | } 25 | 26 | type HTTPTransport struct { 27 | h Transport 28 | } 29 | 30 | func (h *HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) { 31 | out := &http.Request{} 32 | *out = *req 33 | 34 | out.RequestURI = "" 35 | 36 | out.Header = make(http.Header) 37 | 38 | for k, v := range req.Header { 39 | if strings.HasPrefix(k, TemplarPrefix) { 40 | if k == UpgradeHeader && v[0] == "https" { 41 | u := &url.URL{} 42 | *u = *out.URL 43 | 44 | u.Scheme = "https" 45 | 46 | out.URL = u 47 | } 48 | 49 | continue 50 | } 51 | 52 | out.Header[k] = v 53 | } 54 | 55 | return h.h.RoundTrip(out) 56 | } 57 | 58 | func (h *HTTPTransport) CancelRequest(req *http.Request) { 59 | h.h.CancelRequest(req) 60 | } 61 | 62 | func CopyResponse(res http.ResponseWriter, upstream *http.Response) { 63 | for k, v := range upstream.Header { 64 | res.Header()[k] = v 65 | } 66 | 67 | res.WriteHeader(upstream.StatusCode) 68 | if upstream.Body != nil { 69 | io.Copy(res, upstream.Body) 70 | upstream.Body.Close() 71 | } 72 | } 73 | 74 | func CopyBody(dst io.Writer, src io.Reader) { 75 | if src != nil { 76 | io.Copy(dst, src) 77 | } 78 | 79 | if fin, ok := dst.(Finisher); ok { 80 | fin.Finish() 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /testutils.go: -------------------------------------------------------------------------------- 1 | package templar 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/ioutil" 7 | "net/http" 8 | "net/http/httptest" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | type recordingSender struct { 14 | w *httptest.ResponseRecorder 15 | } 16 | 17 | func newRecordingSender() *recordingSender { 18 | return &recordingSender{httptest.NewRecorder()} 19 | } 20 | 21 | func (r *recordingSender) Send(res *http.Response) io.Writer { 22 | for k, v := range res.Header { 23 | r.w.Header()[k] = v 24 | } 25 | 26 | r.w.WriteHeader(res.StatusCode) 27 | 28 | return r.w 29 | } 30 | 31 | type slowTransport struct { 32 | seconds time.Duration 33 | } 34 | 35 | func (st *slowTransport) RoundTrip(req *http.Request) (*http.Response, error) { 36 | response := &http.Response{ 37 | Request: req, 38 | StatusCode: 200, 39 | Body: ioutil.NopCloser(strings.NewReader(fmt.Sprintf("now: %s", time.Now()))), 40 | } 41 | 42 | time.Sleep(st.seconds * time.Second) 43 | 44 | return response, nil 45 | } 46 | 47 | func (st *slowTransport) CancelRequest(req *http.Request) {} 48 | 49 | type slowTransportFallback struct { 50 | seconds time.Duration 51 | fallback bool 52 | } 53 | 54 | func (st *slowTransportFallback) RoundTrip(req *http.Request) (*http.Response, error) { 55 | response := &http.Response{ 56 | Request: req, 57 | StatusCode: 200, 58 | Body: ioutil.NopCloser(strings.NewReader(fmt.Sprintf("now: %s", time.Now()))), 59 | } 60 | 61 | time.Sleep(st.seconds * time.Second) 62 | 63 | return response, nil 64 | } 65 | 66 | func (st *slowTransportFallback) CancelRequest(req *http.Request) {} 67 | 68 | func (st *slowTransportFallback) Fallback(req *http.Request) (*http.Response, error) { 69 | if !st.fallback { 70 | return nil, nil 71 | } 72 | 73 | response := &http.Response{ 74 | Request: req, 75 | StatusCode: 201, 76 | Body: ioutil.NopCloser(strings.NewReader(fmt.Sprintf("now: %s", time.Now()))), 77 | } 78 | 79 | return response, nil 80 | } 81 | -------------------------------------------------------------------------------- /upstream.go: -------------------------------------------------------------------------------- 1 | package templar 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "time" 7 | ) 8 | 9 | const TemplarPrefix = "X-Templar-" 10 | 11 | type Upstream struct { 12 | transport Transport 13 | stats Stats 14 | } 15 | 16 | func NewUpstream(client Transport, stats Stats) *Upstream { 17 | return &Upstream{client, stats} 18 | } 19 | 20 | const CTimeoutHeader = "X-Templar-Timeout" 21 | 22 | func (t *Upstream) extractTimeout(req *http.Request) (time.Duration, bool) { 23 | header := req.Header.Get(CTimeoutHeader) 24 | if header == "" { 25 | return 0, false 26 | } 27 | 28 | dur, err := time.ParseDuration(header) 29 | if err != nil { 30 | return 0, false 31 | } 32 | 33 | return dur, true 34 | } 35 | 36 | func (t *Upstream) forward(res Responder, req *http.Request) error { 37 | upstream, err := t.transport.RoundTrip(req) 38 | if err != nil { 39 | return err 40 | } 41 | 42 | w := res.Send(upstream) 43 | 44 | CopyBody(w, upstream.Body) 45 | 46 | return err 47 | } 48 | 49 | var ErrTimeout = errors.New("request timed out") 50 | 51 | func (t *Upstream) Forward(res Responder, req *http.Request) error { 52 | dur, ok := t.extractTimeout(req) 53 | 54 | if !ok { 55 | return t.forward(res, req) 56 | } 57 | 58 | fin := make(chan error) 59 | 60 | go func() { 61 | fin <- t.forward(res, req) 62 | }() 63 | 64 | time.AfterFunc(dur, func() { 65 | t.transport.CancelRequest(req) 66 | fin <- ErrTimeout 67 | }) 68 | 69 | err := <-fin 70 | 71 | if err == ErrTimeout { 72 | t.stats.RequestTimeout(req, dur) 73 | 74 | if fb, ok := t.transport.(Fallback); ok { 75 | upstream, err := fb.Fallback(req) 76 | if err != nil { 77 | return err 78 | } 79 | 80 | if upstream != nil { 81 | CopyBody(res.Send(upstream), upstream.Body) 82 | return nil 83 | } 84 | } 85 | 86 | uperr := &http.Response{ 87 | Request: req, 88 | StatusCode: 504, 89 | Header: make(http.Header), 90 | } 91 | 92 | uperr.Header.Set("X-Templar-TimedOut", "true") 93 | res.Send(uperr) 94 | } 95 | 96 | return nil 97 | } 98 | -------------------------------------------------------------------------------- /cache/serialization.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | "reflect" 7 | "strconv" 8 | ) 9 | 10 | // Serialize transforms the given value into bytes following these rules: 11 | // - If value is a byte array, it is returned as-is. 12 | // - If value is an int or uint type, it is returned as the ASCII representation 13 | // - Else, encoding/gob is used to serialize 14 | func Serialize(value interface{}) ([]byte, error) { 15 | if bytes, ok := value.([]byte); ok { 16 | return bytes, nil 17 | } 18 | 19 | switch v := reflect.ValueOf(value); v.Kind() { 20 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 21 | return []byte(strconv.FormatInt(v.Int(), 10)), nil 22 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 23 | return []byte(strconv.FormatUint(v.Uint(), 10)), nil 24 | } 25 | 26 | var b bytes.Buffer 27 | encoder := gob.NewEncoder(&b) 28 | if err := encoder.Encode(value); err != nil { 29 | return nil, err 30 | } 31 | return b.Bytes(), nil 32 | } 33 | 34 | // Deserialize transforms bytes produced by Serialize back into a Go object, 35 | // storing it into "ptr", which must be a pointer to the value type. 36 | func Deserialize(byt []byte, ptr interface{}) (err error) { 37 | if bytes, ok := ptr.(*[]byte); ok { 38 | *bytes = byt 39 | return 40 | } 41 | 42 | if v := reflect.ValueOf(ptr); v.Kind() == reflect.Ptr { 43 | switch p := v.Elem(); p.Kind() { 44 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 45 | var i int64 46 | i, err = strconv.ParseInt(string(byt), 10, 64) 47 | if err == nil { 48 | p.SetInt(i) 49 | } 50 | return 51 | 52 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 53 | var i uint64 54 | i, err = strconv.ParseUint(string(byt), 10, 64) 55 | if err == nil { 56 | p.SetUint(i) 57 | } 58 | return 59 | } 60 | } 61 | 62 | b := bytes.NewBuffer(byt) 63 | decoder := gob.NewDecoder(b) 64 | if err = decoder.Decode(ptr); err != nil { 65 | return 66 | } 67 | return 68 | } 69 | -------------------------------------------------------------------------------- /cache_backend.go: -------------------------------------------------------------------------------- 1 | package templar 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/vektra/templar/cache" 10 | ) 11 | 12 | type Cache struct { 13 | c cache.Cache 14 | } 15 | 16 | func NewMemoryCache(expire time.Duration) *Cache { 17 | return &Cache{ 18 | c: cache.NewInMemoryCache(expire), 19 | } 20 | } 21 | 22 | func NewMemcacheCache(hostlist []string, expire time.Duration) *Cache { 23 | return &Cache{ 24 | c: cache.NewMemcachedCache(hostlist, expire), 25 | } 26 | } 27 | 28 | func NewRedisCache(host string, password string, expire time.Duration) *Cache { 29 | return &Cache{ 30 | c: cache.NewRedisCache(host, password, expire), 31 | } 32 | } 33 | 34 | func NewGroupCacheCache(thisPeerURL string, otherPeersURLs string, defaultExpiration time.Duration, memoryLimit int64, transport Transport) *cache.GroupCacheCache { 35 | return cache.NewGroupCacheCache(thisPeerURL, otherPeersURLs, defaultExpiration, memoryLimit, transport) 36 | } 37 | 38 | type cachedRequest struct { 39 | body []byte 40 | status int 41 | headers http.Header 42 | } 43 | 44 | func (m *Cache) Set(req *http.Request, resp *http.Response) { 45 | cr := &cachedRequest{} 46 | 47 | if resp.Body != nil { 48 | body, err := ioutil.ReadAll(resp.Body) 49 | if err != nil { 50 | return 51 | } 52 | 53 | cr.body = body 54 | resp.Body = ioutil.NopCloser(bytes.NewReader(body)) 55 | } 56 | 57 | cr.status = resp.StatusCode 58 | cr.headers = resp.Header 59 | 60 | var expires time.Duration 61 | 62 | if reqExpire := req.Header.Get(CacheTimeHeader); reqExpire != "" { 63 | if dur, err := time.ParseDuration(reqExpire); err == nil { 64 | expires = dur 65 | } 66 | } 67 | 68 | m.c.Add(req.URL.String(), cr, expires) 69 | } 70 | 71 | func (m *Cache) Get(req *http.Request) (*http.Response, bool) { 72 | var cr *cachedRequest 73 | 74 | err := m.c.Get(req.URL.String(), &cr) 75 | 76 | if err != nil { 77 | return nil, false 78 | } 79 | 80 | resp := &http.Response{ 81 | StatusCode: cr.status, 82 | Header: make(http.Header), 83 | } 84 | 85 | for k, v := range cr.headers { 86 | resp.Header[k] = v 87 | } 88 | 89 | resp.Body = ioutil.NopCloser(bytes.NewReader(cr.body)) 90 | 91 | return resp, true 92 | } 93 | -------------------------------------------------------------------------------- /cache_backend_test.go: -------------------------------------------------------------------------------- 1 | package templar 2 | 3 | import ( 4 | "io/ioutil" 5 | "net/http" 6 | "strings" 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | "github.com/vektra/neko" 13 | ) 14 | 15 | func TestCache(t *testing.T) { 16 | n := neko.Start(t) 17 | 18 | var ( 19 | cache *Cache 20 | ) 21 | 22 | n.Setup(func() { 23 | cache = NewMemoryCache(30 * time.Second) 24 | }) 25 | 26 | n.It("can store and retrieve responses", func() { 27 | req, err := http.NewRequest("GET", "http://google.com/foo/bar", nil) 28 | require.NoError(t, err) 29 | 30 | upstream := &http.Response{ 31 | Request: req, 32 | StatusCode: 304, 33 | Status: "304 Too Funky", 34 | Header: make(http.Header), 35 | } 36 | 37 | cache.Set(req, upstream) 38 | 39 | out, ok := cache.Get(req) 40 | require.True(t, ok) 41 | 42 | assert.Equal(t, upstream.StatusCode, out.StatusCode) 43 | assert.Equal(t, upstream.Header, out.Header) 44 | }) 45 | 46 | n.It("makes the response body readable", func() { 47 | req, err := http.NewRequest("GET", "http://google.com/foo/bar", nil) 48 | require.NoError(t, err) 49 | 50 | funky := "waaay too funky" 51 | 52 | upstream := &http.Response{ 53 | Request: req, 54 | StatusCode: 304, 55 | Status: "304 Too Funky", 56 | Body: ioutil.NopCloser(strings.NewReader(funky)), 57 | } 58 | 59 | cache.Set(req, upstream) 60 | 61 | _, err = ioutil.ReadAll(upstream.Body) 62 | require.NoError(t, err) 63 | 64 | out, ok := cache.Get(req) 65 | require.True(t, ok) 66 | 67 | bytes, err := ioutil.ReadAll(out.Body) 68 | require.NoError(t, err) 69 | 70 | assert.Equal(t, funky, string(bytes)) 71 | }) 72 | 73 | n.It("honors cache time requested in header", func() { 74 | req, err := http.NewRequest("GET", "http://google.com/foo/bar", nil) 75 | require.NoError(t, err) 76 | 77 | upstream := &http.Response{ 78 | Request: req, 79 | StatusCode: 304, 80 | Status: "304 Too Funky", 81 | } 82 | 83 | req.Header.Set(CacheTimeHeader, "1s") 84 | 85 | cache.Set(req, upstream) 86 | 87 | time.Sleep(1 * time.Second) 88 | 89 | _, ok := cache.Get(req) 90 | require.False(t, ok) 91 | }) 92 | 93 | n.Meow() 94 | } 95 | -------------------------------------------------------------------------------- /cache/inmemory.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "time" 7 | 8 | "github.com/robfig/go-cache" 9 | ) 10 | 11 | type InMemoryCache struct { 12 | cache.Cache 13 | } 14 | 15 | func NewInMemoryCache(defaultExpiration time.Duration) InMemoryCache { 16 | return InMemoryCache{*cache.New(defaultExpiration, time.Minute)} 17 | } 18 | 19 | func (c InMemoryCache) Get(key string, ptrValue interface{}) error { 20 | value, found := c.Cache.Get(key) 21 | if !found { 22 | return ErrCacheMiss 23 | } 24 | 25 | v := reflect.ValueOf(ptrValue) 26 | if v.Type().Kind() == reflect.Ptr && v.Elem().CanSet() { 27 | v.Elem().Set(reflect.ValueOf(value)) 28 | return nil 29 | } 30 | 31 | err := fmt.Errorf("cache: attempt to get %s, but can not set value %v", key, v) 32 | return err 33 | } 34 | 35 | func (c InMemoryCache) GetMulti(keys ...string) (Getter, error) { 36 | return c, nil 37 | } 38 | 39 | func (c InMemoryCache) Set(key string, value interface{}, expires time.Duration) error { 40 | // NOTE: go-cache understands the values of DEFAULT and FOREVER 41 | c.Cache.Set(key, value, expires) 42 | return nil 43 | } 44 | 45 | func (c InMemoryCache) Add(key string, value interface{}, expires time.Duration) error { 46 | err := c.Cache.Add(key, value, expires) 47 | if err == cache.ErrKeyExists { 48 | return ErrNotStored 49 | } 50 | return err 51 | } 52 | 53 | func (c InMemoryCache) Replace(key string, value interface{}, expires time.Duration) error { 54 | if err := c.Cache.Replace(key, value, expires); err != nil { 55 | return ErrNotStored 56 | } 57 | return nil 58 | } 59 | 60 | func (c InMemoryCache) Delete(key string) error { 61 | if found := c.Cache.Delete(key); !found { 62 | return ErrCacheMiss 63 | } 64 | return nil 65 | } 66 | 67 | func (c InMemoryCache) Increment(key string, n uint64) (newValue uint64, err error) { 68 | newValue, err = c.Cache.Increment(key, n) 69 | if err == cache.ErrCacheMiss { 70 | return 0, ErrCacheMiss 71 | } 72 | return 73 | } 74 | 75 | func (c InMemoryCache) Decrement(key string, n uint64) (newValue uint64, err error) { 76 | newValue, err = c.Cache.Decrement(key, n) 77 | if err == cache.ErrCacheMiss { 78 | return 0, ErrCacheMiss 79 | } 80 | return 81 | } 82 | 83 | func (c InMemoryCache) Flush() error { 84 | c.Cache.Flush() 85 | return nil 86 | } 87 | -------------------------------------------------------------------------------- /cache/serialization_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | type Struct1 struct { 9 | X int 10 | } 11 | 12 | func (s Struct1) Method1() {} 13 | 14 | type Interface1 interface { 15 | Method1() 16 | } 17 | 18 | var ( 19 | struct1 Struct1 = Struct1{1} 20 | ptrStruct *Struct1 = &Struct1{2} 21 | emptyIface interface{} = Struct1{3} 22 | iface1 Interface1 = Struct1{4} 23 | sliceStruct []Struct1 = []Struct1{{5}, {6}, {7}} 24 | ptrSliceStruct []*Struct1 = []*Struct1{{8}, {9}, {10}} 25 | 26 | VALUE_MAP = map[string]interface{}{ 27 | "bytes": []byte{0x61, 0x62, 0x63, 0x64}, 28 | "string": "string", 29 | "bool": true, 30 | "int": 5, 31 | "int8": int8(5), 32 | "int16": int16(5), 33 | "int32": int32(5), 34 | "int64": int64(5), 35 | "uint": uint(5), 36 | "uint8": uint8(5), 37 | "uint16": uint16(5), 38 | "uint32": uint32(5), 39 | "uint64": uint64(5), 40 | "float32": float32(5), 41 | "float64": float64(5), 42 | "array": [5]int{1, 2, 3, 4, 5}, 43 | "slice": []int{1, 2, 3, 4, 5}, 44 | "emptyIf": emptyIface, 45 | "Iface1": iface1, 46 | "map": map[string]string{"foo": "bar"}, 47 | "ptrStruct": ptrStruct, 48 | "struct1": struct1, 49 | "sliceStruct": sliceStruct, 50 | "ptrSliceStruct": ptrSliceStruct, 51 | } 52 | ) 53 | 54 | // Test passing all kinds of data between serialize and deserialize. 55 | func TestRoundTrip(t *testing.T) { 56 | for _, expected := range VALUE_MAP { 57 | bytes, err := Serialize(expected) 58 | if err != nil { 59 | t.Error(err) 60 | continue 61 | } 62 | 63 | ptrActual := reflect.New(reflect.TypeOf(expected)).Interface() 64 | err = Deserialize(bytes, ptrActual) 65 | if err != nil { 66 | t.Error(err) 67 | continue 68 | } 69 | 70 | actual := reflect.ValueOf(ptrActual).Elem().Interface() 71 | if !reflect.DeepEqual(expected, actual) { 72 | t.Errorf("(expected) %T %v != %T %v (actual)", expected, expected, actual, actual) 73 | } 74 | } 75 | } 76 | 77 | func zeroMap(arg map[string]interface{}) map[string]interface{} { 78 | result := map[string]interface{}{} 79 | for key, value := range arg { 80 | result[key] = reflect.Zero(reflect.TypeOf(value)).Interface() 81 | } 82 | return result 83 | } 84 | -------------------------------------------------------------------------------- /collapse.go: -------------------------------------------------------------------------------- 1 | package templar 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "sync" 7 | ) 8 | 9 | type RunningRequest struct { 10 | Request *http.Request 11 | 12 | others []Responder 13 | done chan struct{} 14 | } 15 | 16 | type Collapser struct { 17 | client Client 18 | categorizer *Categorizer 19 | 20 | lock sync.Mutex 21 | running map[string]*RunningRequest 22 | } 23 | 24 | func NewCollapser(client Client, categorizer *Categorizer) *Collapser { 25 | return &Collapser{ 26 | client: client, 27 | categorizer: categorizer, 28 | running: make(map[string]*RunningRequest), 29 | } 30 | } 31 | 32 | type collapseResponder struct { 33 | collapser *Collapser 34 | request *http.Request 35 | running *RunningRequest 36 | } 37 | 38 | func (c *collapseResponder) Send(res *http.Response) io.Writer { 39 | return c.collapser.finish(c.request, res, c.running) 40 | } 41 | 42 | func (c *Collapser) finish(req *http.Request, res *http.Response, rr *RunningRequest) io.Writer { 43 | 44 | c.lock.Lock() 45 | 46 | key := req.URL.String() 47 | delete(c.running, key) 48 | 49 | c.lock.Unlock() 50 | 51 | cw := &collapsedWriter{running: rr} 52 | 53 | for _, c := range rr.others { 54 | cw.w = append(cw.w, c.Send(res)) 55 | } 56 | 57 | return cw 58 | } 59 | 60 | type collapsedWriter struct { 61 | w []io.Writer 62 | running *RunningRequest 63 | } 64 | 65 | func (cw *collapsedWriter) Write(p []byte) (n int, err error) { 66 | for _, w := range cw.w { 67 | n, err = w.Write(p) 68 | if err != nil { 69 | return 70 | } 71 | 72 | if n != len(p) { 73 | err = io.ErrShortWrite 74 | return 75 | } 76 | } 77 | 78 | return len(p), nil 79 | } 80 | 81 | func (cw *collapsedWriter) Finish() { 82 | close(cw.running.done) 83 | } 84 | 85 | func (c *Collapser) Forward(res Responder, req *http.Request) error { 86 | if !c.categorizer.Stateless(req) { 87 | return c.client.Forward(res, req) 88 | } 89 | 90 | c.lock.Lock() 91 | 92 | key := req.URL.String() 93 | 94 | if running, ok := c.running[key]; ok { 95 | running.others = append(running.others, res) 96 | 97 | c.lock.Unlock() 98 | 99 | <-running.done 100 | 101 | return nil 102 | } 103 | 104 | rr := &RunningRequest{ 105 | Request: req, 106 | others: []Responder{res}, 107 | done: make(chan struct{}), 108 | } 109 | 110 | c.running[key] = rr 111 | c.lock.Unlock() 112 | 113 | return c.client.Forward(&collapseResponder{c, req, rr}, req) 114 | } 115 | -------------------------------------------------------------------------------- /cache.go: -------------------------------------------------------------------------------- 1 | package templar 2 | 3 | import "net/http" 4 | 5 | type FallbackCacher struct { 6 | backend CacheBackend 7 | transport Transport 8 | 9 | categorizer *Categorizer 10 | } 11 | 12 | var _ = Transport(&FallbackCacher{}) 13 | 14 | func NewFallbackCacher(backend CacheBackend, transport Transport, categorizer *Categorizer) *FallbackCacher { 15 | return &FallbackCacher{backend, transport, categorizer} 16 | } 17 | 18 | const ( 19 | CacheHeader = "X-Templar-Cache" 20 | CacheTimeHeader = "X-Templar-CacheFor" 21 | CacheCachedHeader = "X-Templar-Cached" 22 | ) 23 | 24 | func (c *FallbackCacher) shouldCache(req *http.Request) bool { 25 | return c.categorizer.Stateless(req) && req.Header.Get(CacheHeader) == "fallback" 26 | } 27 | 28 | func (c *FallbackCacher) RoundTrip(req *http.Request) (*http.Response, error) { 29 | upstream, err := c.transport.RoundTrip(req) 30 | 31 | if !c.shouldCache(req) { 32 | return upstream, err 33 | } 34 | 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | c.backend.Set(req, upstream) 40 | 41 | return upstream, nil 42 | } 43 | 44 | func (c *FallbackCacher) Fallback(req *http.Request) (*http.Response, error) { 45 | if upstream, ok := c.backend.Get(req); ok { 46 | if upstream != nil { 47 | upstream.Header.Add(CacheCachedHeader, "yes") 48 | return upstream, nil 49 | } 50 | } 51 | 52 | return nil, nil 53 | } 54 | 55 | func (c *FallbackCacher) CancelRequest(req *http.Request) { 56 | c.transport.CancelRequest(req) 57 | } 58 | 59 | type EagerCacher struct { 60 | backend CacheBackend 61 | transport Transport 62 | 63 | categorizer *Categorizer 64 | } 65 | 66 | var _ = Transport(&EagerCacher{}) 67 | 68 | func NewEagerCacher(backend CacheBackend, transport Transport, categorizer *Categorizer) *EagerCacher { 69 | return &EagerCacher{backend, transport, categorizer} 70 | } 71 | 72 | func (c *EagerCacher) shouldCache(req *http.Request) bool { 73 | return c.categorizer.Stateless(req) && req.Header.Get(CacheHeader) == "eager" 74 | } 75 | 76 | func (c *EagerCacher) RoundTrip(req *http.Request) (*http.Response, error) { 77 | if !c.shouldCache(req) { 78 | upstream, err := c.transport.RoundTrip(req) 79 | return upstream, err 80 | } 81 | 82 | if upstream, ok := c.backend.Get(req); ok { 83 | upstream.Header.Add(CacheCachedHeader, "yes") 84 | 85 | return upstream, nil 86 | } 87 | 88 | upstream, err := c.transport.RoundTrip(req) 89 | if err != nil { 90 | return nil, err 91 | } 92 | 93 | c.backend.Set(req, upstream) 94 | 95 | return upstream, nil 96 | } 97 | 98 | func (c *EagerCacher) CancelRequest(req *http.Request) { 99 | c.transport.CancelRequest(req) 100 | } 101 | -------------------------------------------------------------------------------- /upstream_test.go: -------------------------------------------------------------------------------- 1 | package templar 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | "github.com/vektra/neko" 11 | ) 12 | 13 | func TestUpstream(t *testing.T) { 14 | n := neko.Start(t) 15 | 16 | var ( 17 | mockTrans MockTransport 18 | stats MockStats 19 | ) 20 | 21 | n.CheckMock(&mockTrans.Mock) 22 | n.CheckMock(&stats.Mock) 23 | 24 | n.It("sends a request to the transport", func() { 25 | req, err := http.NewRequest("GET", "http://google.com/foo/bar", nil) 26 | require.NoError(t, err) 27 | 28 | res := newRecordingSender() 29 | 30 | upstream := NewUpstream(&mockTrans, &stats) 31 | 32 | resp := &http.Response{ 33 | Request: req, 34 | StatusCode: 304, 35 | Status: "304 Too Funky", 36 | } 37 | 38 | mockTrans.On("RoundTrip", req).Return(resp, nil) 39 | 40 | err = upstream.Forward(res, req) 41 | require.NoError(t, err) 42 | 43 | assert.Equal(t, 304, res.w.Code) 44 | }) 45 | 46 | n.It("will timeout a request if requested", func() { 47 | req, err := http.NewRequest("GET", "http://google.com/foo/bar", nil) 48 | require.NoError(t, err) 49 | 50 | req.Header.Add("X-Templar-Timeout", "2s") 51 | 52 | res := newRecordingSender() 53 | 54 | upstream := NewUpstream(&slowTransport{10}, &stats) 55 | 56 | stats.On("RequestTimeout", req, 2*time.Second).Return(nil) 57 | 58 | err = upstream.Forward(res, req) 59 | require.NoError(t, err) 60 | 61 | assert.Equal(t, 504, res.w.Code) 62 | }) 63 | 64 | n.It("will invoke a transports fallback on timeout", func() { 65 | req, err := http.NewRequest("GET", "http://google.com/foo/bar", nil) 66 | require.NoError(t, err) 67 | 68 | req.Header.Add("X-Templar-Timeout", "2s") 69 | 70 | res := newRecordingSender() 71 | 72 | upstream := NewUpstream(&slowTransportFallback{seconds: 10, fallback: true}, &stats) 73 | 74 | stats.On("RequestTimeout", req, 2*time.Second).Return(nil) 75 | 76 | err = upstream.Forward(res, req) 77 | require.NoError(t, err) 78 | 79 | assert.Equal(t, 201, res.w.Code) 80 | }) 81 | 82 | n.It("handles the fallback indicating there is no fallback", func() { 83 | req, err := http.NewRequest("GET", "http://google.com/foo/bar", nil) 84 | require.NoError(t, err) 85 | 86 | req.Header.Add("X-Templar-Timeout", "2s") 87 | 88 | res := newRecordingSender() 89 | 90 | upstream := NewUpstream(&slowTransportFallback{seconds: 10, fallback: false}, &stats) 91 | 92 | stats.On("RequestTimeout", req, 2*time.Second).Return(nil) 93 | 94 | err = upstream.Forward(res, req) 95 | require.NoError(t, err) 96 | 97 | assert.Equal(t, 504, res.w.Code) 98 | }) 99 | 100 | n.Meow() 101 | } 102 | -------------------------------------------------------------------------------- /cache/memcached.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/bradfitz/gomemcache/memcache" 8 | ) 9 | 10 | // Wraps the Memcached client to meet the Cache interface. 11 | type MemcachedCache struct { 12 | *memcache.Client 13 | defaultExpiration time.Duration 14 | } 15 | 16 | func NewMemcachedCache(hostList []string, defaultExpiration time.Duration) MemcachedCache { 17 | return MemcachedCache{memcache.New(hostList...), defaultExpiration} 18 | } 19 | 20 | func (c MemcachedCache) Set(key string, value interface{}, expires time.Duration) error { 21 | return c.invoke((*memcache.Client).Set, key, value, expires) 22 | } 23 | 24 | func (c MemcachedCache) Add(key string, value interface{}, expires time.Duration) error { 25 | return c.invoke((*memcache.Client).Add, key, value, expires) 26 | } 27 | 28 | func (c MemcachedCache) Replace(key string, value interface{}, expires time.Duration) error { 29 | return c.invoke((*memcache.Client).Replace, key, value, expires) 30 | } 31 | 32 | func (c MemcachedCache) Get(key string, ptrValue interface{}) error { 33 | item, err := c.Client.Get(key) 34 | if err != nil { 35 | return convertMemcacheError(err) 36 | } 37 | return Deserialize(item.Value, ptrValue) 38 | } 39 | 40 | func (c MemcachedCache) GetMulti(keys ...string) (Getter, error) { 41 | items, err := c.Client.GetMulti(keys) 42 | if err != nil { 43 | return nil, convertMemcacheError(err) 44 | } 45 | return ItemMapGetter(items), nil 46 | } 47 | 48 | func (c MemcachedCache) Delete(key string) error { 49 | return convertMemcacheError(c.Client.Delete(key)) 50 | } 51 | 52 | func (c MemcachedCache) Increment(key string, delta uint64) (newValue uint64, err error) { 53 | newValue, err = c.Client.Increment(key, delta) 54 | return newValue, convertMemcacheError(err) 55 | } 56 | 57 | func (c MemcachedCache) Decrement(key string, delta uint64) (newValue uint64, err error) { 58 | newValue, err = c.Client.Decrement(key, delta) 59 | return newValue, convertMemcacheError(err) 60 | } 61 | 62 | func (c MemcachedCache) Flush() error { 63 | err := errors.New("cache: can not flush memcached.") 64 | return err 65 | } 66 | 67 | func (c MemcachedCache) invoke(f func(*memcache.Client, *memcache.Item) error, 68 | key string, value interface{}, expires time.Duration) error { 69 | 70 | switch expires { 71 | case DEFAULT: 72 | expires = c.defaultExpiration 73 | case FOREVER: 74 | expires = time.Duration(0) 75 | } 76 | 77 | b, err := Serialize(value) 78 | if err != nil { 79 | return err 80 | } 81 | return convertMemcacheError(f(c.Client, &memcache.Item{ 82 | Key: key, 83 | Value: b, 84 | Expiration: int32(expires / time.Second), 85 | })) 86 | } 87 | 88 | // Implement a Getter on top of the returned item map. 89 | type ItemMapGetter map[string]*memcache.Item 90 | 91 | func (g ItemMapGetter) Get(key string, ptrValue interface{}) error { 92 | item, ok := g[key] 93 | if !ok { 94 | return ErrCacheMiss 95 | } 96 | 97 | return Deserialize(item.Value, ptrValue) 98 | } 99 | 100 | func convertMemcacheError(err error) error { 101 | switch err { 102 | case nil: 103 | return nil 104 | case memcache.ErrCacheMiss: 105 | return ErrCacheMiss 106 | case memcache.ErrNotStored: 107 | return ErrNotStored 108 | } 109 | 110 | return err 111 | } 112 | -------------------------------------------------------------------------------- /cmd/templar/templar.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "log" 7 | "net/http" 8 | "strings" 9 | "time" 10 | 11 | "github.com/amir/raidman" 12 | "github.com/quipo/statsd" 13 | "github.com/vektra/templar" 14 | ) 15 | 16 | var fDebug = flag.Bool("debug", false, "show debugging info") 17 | var fStatsd = flag.String("statsd", "", "address to sends statsd stats") 18 | var fRiemann = flag.String("riemann", "", "address to sends riemann stats over tcp (e.g. localhost:5555)") 19 | var fExpire = flag.Duration("expire", 5*time.Minute, "how long to use cached values") 20 | 21 | var fMemcache = flag.String("memcache", "", "memcache servers to use for caching") 22 | var fRedis = flag.String("redis", "", "redis server to use for caching") 23 | var fRedisPassword = flag.String("redis-password", "", "password to redis server") 24 | var fGroupCacheThisPeer = flag.String("groupcache-this-peer", "", "groupcache peer url to use for this peer") 25 | var fGroupCacheOtherPeers = flag.String("groupcache-other-peers", "", "groupcache peer url set to use for caching (comma separated)") 26 | var fGroupCacheMemoryLimit = flag.Int("groupcache-memory-limit", 64<<20, "the memory limit to pass to groupcache. Defaults to 64mb") 27 | 28 | var fListen = flag.String("listen", "0.0.0.0:9224", "address to listen on") 29 | 30 | func main() { 31 | flag.Parse() 32 | 33 | var stats templar.MultiStats 34 | 35 | if *fDebug { 36 | stats = append(stats, &templar.DebugStats{}) 37 | } 38 | 39 | if *fStatsd != "" { 40 | client := statsd.NewStatsdClient(*fStatsd, "") 41 | err := client.CreateSocket() 42 | if err != nil { 43 | panic(err) 44 | } 45 | 46 | stats = append(stats, templar.NewStatsdOutput(client)) 47 | } 48 | 49 | if *fRiemann != "" { 50 | client, err := raidman.Dial("tcp", *fRiemann) 51 | if err != nil { 52 | panic(err) 53 | } 54 | 55 | stats = append(stats, templar.NewRiemannOutput(client)) 56 | } 57 | 58 | categorizer := templar.NewCategorizer() 59 | 60 | transport := templar.NewHTTPTransport() 61 | 62 | var cache templar.CacheBackend 63 | 64 | switch { 65 | case *fMemcache != "": 66 | cache = templar.NewMemcacheCache(strings.Split(*fMemcache, ":"), *fExpire) 67 | case *fRedis != "": 68 | cache = templar.NewRedisCache(*fRedis, *fRedisPassword, *fExpire) 69 | case *fGroupCacheThisPeer != "" && *fGroupCacheOtherPeers != "": 70 | cache = templar.NewGroupCacheCache(*fGroupCacheThisPeer, *fGroupCacheOtherPeers, *fExpire, int64(*fGroupCacheMemoryLimit), transport) 71 | case *fGroupCacheThisPeer != "" && *fGroupCacheOtherPeers == "": 72 | panic(errors.New("templar: passed --groupcache-this-peer without passing --groupcache-other-peers. You have to set both of them to use the group cache backend")) 73 | case *fGroupCacheThisPeer == "" && *fGroupCacheOtherPeers != "": 74 | panic(errors.New("templar: passed --groupcache-other-peers without passing --groupcache-this-peer. You have to set both of them to use the group cache backend")) 75 | default: 76 | cache = templar.NewMemoryCache(*fExpire) 77 | } 78 | 79 | fallback := templar.NewFallbackCacher(cache, transport, categorizer) 80 | eager := templar.NewEagerCacher(cache, fallback, categorizer) 81 | 82 | upstream := templar.NewUpstream(eager, stats) 83 | collapse := templar.NewCollapser(upstream, categorizer) 84 | 85 | proxy := templar.NewProxy(collapse, stats) 86 | 87 | log.Fatal(http.ListenAndServe(*fListen, proxy)) 88 | } 89 | -------------------------------------------------------------------------------- /cache/groupcache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "github.com/golang/groupcache" 7 | "io/ioutil" 8 | "net/http" 9 | "strconv" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | const ( 15 | CacheHeader = "X-Templar-Cache" 16 | CacheTimeHeader = "X-Templar-CacheFor" 17 | notRedoingRequestMessage = "not redoing a request on a fallback cache lookup" 18 | ) 19 | 20 | type Transport interface { 21 | RoundTrip(*http.Request) (*http.Response, error) 22 | } 23 | 24 | type GroupCacheCache struct { 25 | g groupcache.Group 26 | t Transport 27 | } 28 | 29 | type cachedResponse struct { 30 | Body []byte 31 | Status int 32 | Headers http.Header 33 | } 34 | 35 | func NewGroupCacheCache(thisPeerAddress string, otherPeersURLs string, defaultExpiration time.Duration, memoryLimit int64, transport Transport) *GroupCacheCache { 36 | data := []string{"http://" + thisPeerAddress} 37 | otherPeers := append(data, strings.Split(otherPeersURLs, ",")...) 38 | pool := groupcache.NewHTTPPool("http://" + thisPeerAddress) 39 | pool.Set(otherPeers...) 40 | getter := func(context groupcache.Context, k string, destination groupcache.Sink) error { 41 | req, ok := context.(*http.Request) 42 | if !ok { 43 | return errors.New("failed to cast groupcache context to an http request") 44 | } 45 | 46 | if !shouldCache(req) { 47 | return errors.New(notRedoingRequestMessage) 48 | } 49 | upstream, err := transport.RoundTrip(req) 50 | if err != nil { 51 | return err 52 | } 53 | body, err := ioutil.ReadAll(upstream.Body) 54 | if err != nil { 55 | return err 56 | } 57 | toCache := &cachedResponse{ 58 | Body: body, 59 | Status: upstream.StatusCode, 60 | Headers: upstream.Header, 61 | } 62 | 63 | b, err := Serialize(toCache) 64 | if err != nil { 65 | return err 66 | } 67 | destination.SetBytes(b) 68 | return nil 69 | } 70 | group := groupcache.NewGroup("templar", memoryLimit, groupcache.GetterFunc(getter)) 71 | go func() { 72 | http.ListenAndServe(thisPeerAddress, http.HandlerFunc(pool.ServeHTTP)) 73 | }() 74 | return &GroupCacheCache{*group, transport} 75 | } 76 | 77 | func (c *GroupCacheCache) Set(req *http.Request, resp *http.Response) { 78 | // intentionally does nothing: 79 | // groupcache doesn't support sets - just reads 80 | // as such, we don't support sets, and gets go through a fallback 81 | // to an underlying http transport 82 | } 83 | 84 | func calculateEpochedKey(req *http.Request, now time.Time) string { 85 | expires := FOREVER 86 | if reqExpire := req.Header.Get(CacheTimeHeader); reqExpire != "" { 87 | if dur, err := time.ParseDuration(reqExpire); err == nil { 88 | expires = dur 89 | } 90 | } 91 | if expires == FOREVER { 92 | return req.URL.String() 93 | } else { 94 | return strconv.Itoa(int(now.Truncate(expires).Unix())) + 95 | "-" + 96 | req.URL.String() 97 | } 98 | } 99 | 100 | func (c *GroupCacheCache) Get(req *http.Request) (*http.Response, bool) { 101 | var data []byte 102 | key := calculateEpochedKey(req, time.Now()) 103 | err := c.g.Get(req, key, groupcache.AllocatingByteSliceSink(&data)) 104 | if err != nil { 105 | return nil, false 106 | } else { 107 | cr := &cachedResponse{} 108 | Deserialize(data, cr) 109 | resp := &http.Response{ 110 | StatusCode: cr.Status, 111 | Header: make(http.Header), 112 | } 113 | for k, v := range cr.Headers { 114 | resp.Header[k] = v 115 | } 116 | 117 | resp.Body = ioutil.NopCloser(bytes.NewReader(cr.Body)) 118 | 119 | return resp, true 120 | } 121 | } 122 | 123 | func shouldCache(req *http.Request) bool { 124 | return req.Header.Get(CacheHeader) == "fallback" 125 | } 126 | -------------------------------------------------------------------------------- /stats.go: -------------------------------------------------------------------------------- 1 | package templar 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | "time" 8 | 9 | "github.com/amir/raidman" 10 | ) 11 | 12 | type DebugStats struct{} 13 | 14 | func (d *DebugStats) StartRequest(req *http.Request) { 15 | fmt.Printf("[%s] S %s %s\n", time.Now().Format(time.RFC3339Nano), req.Method, req.URL) 16 | } 17 | 18 | func (d *DebugStats) Emit(req *http.Request, dur time.Duration) { 19 | fmt.Printf("[%s] E %s %s (%s)\n", time.Now().Format(time.RFC3339Nano), req.Method, req.URL, dur) 20 | } 21 | 22 | func (d *DebugStats) RequestTimeout(req *http.Request, timeout time.Duration) { 23 | fmt.Printf("[%s] T %s %s (%s)\n", time.Now().Format(time.RFC3339Nano), req.Method, req.URL, timeout) 24 | } 25 | 26 | var _ = Stats(&DebugStats{}) 27 | 28 | type StatsdOutput struct { 29 | client StatsdClient 30 | } 31 | 32 | var _ = Stats(&StatsdOutput{}) 33 | 34 | func NewStatsdOutput(client StatsdClient) *StatsdOutput { 35 | return &StatsdOutput{client} 36 | } 37 | 38 | func (s *StatsdOutput) url(req *http.Request) string { 39 | return req.Host + strings.Replace(req.URL.Path, "/", "-", -1) 40 | } 41 | 42 | func (s *StatsdOutput) StartRequest(req *http.Request) { 43 | s.client.Incr("templar.request.method."+req.Method, 1) 44 | s.client.Incr("templar.request.host."+req.Host, 1) 45 | s.client.Incr("templar.request.url."+s.url(req), 1) 46 | s.client.GaugeDelta("templar.requests.active", 1) 47 | } 48 | 49 | func (s *StatsdOutput) Emit(req *http.Request, delta time.Duration) { 50 | s.client.GaugeDelta("templar.requests.active", -1) 51 | s.client.PrecisionTiming("templar.request.url."+s.url(req), delta) 52 | } 53 | 54 | func (s *StatsdOutput) RequestTimeout(req *http.Request, timeout time.Duration) { 55 | s.client.Incr("templar.timeout.host."+req.Host, 1) 56 | s.client.Incr("templar.timeout.url."+s.url(req), 1) 57 | } 58 | 59 | type RiemannOutput struct { 60 | client RiemannClient 61 | } 62 | 63 | func NewRiemannOutput(client RiemannClient) *RiemannOutput { 64 | return &RiemannOutput{client} 65 | } 66 | 67 | func (r *RiemannOutput) StartRequest(req *http.Request) { 68 | attributes := make(map[string]string) 69 | attributes["method"] = req.Method 70 | attributes["host"] = req.Host 71 | attributes["path"] = req.URL.Path 72 | var event = &raidman.Event{ 73 | State: "ok", 74 | Service: "templar request", 75 | Metric: 1, 76 | Attributes: attributes, 77 | } 78 | r.client.Send(event) 79 | } 80 | 81 | func (r *RiemannOutput) Emit(req *http.Request, delta time.Duration) { 82 | attributes := make(map[string]string) 83 | attributes["method"] = req.Method 84 | attributes["host"] = req.Host 85 | attributes["path"] = req.URL.Path 86 | var event = &raidman.Event{ 87 | State: "ok", 88 | Service: "templar response", 89 | Metric: 1000.0 * delta.Seconds(), 90 | Attributes: attributes, 91 | } 92 | r.client.Send(event) 93 | } 94 | 95 | func (r *RiemannOutput) RequestTimeout(req *http.Request, timeout time.Duration) { 96 | attributes := make(map[string]string) 97 | attributes["method"] = req.Method 98 | attributes["host"] = req.Host 99 | attributes["path"] = req.URL.Path 100 | var event = &raidman.Event{ 101 | State: "warning", 102 | Service: "templar timeout", 103 | Metric: timeout.Seconds() * 1000.0, 104 | Attributes: attributes, 105 | } 106 | r.client.Send(event) 107 | } 108 | 109 | type MultiStats []Stats 110 | 111 | var _ = Stats(MultiStats{}) 112 | 113 | func (m MultiStats) StartRequest(req *http.Request) { 114 | for _, s := range m { 115 | s.StartRequest(req) 116 | } 117 | } 118 | 119 | func (m MultiStats) Emit(req *http.Request, t time.Duration) { 120 | for _, s := range m { 121 | s.Emit(req, t) 122 | } 123 | } 124 | 125 | func (m MultiStats) RequestTimeout(req *http.Request, timeout time.Duration) { 126 | for _, s := range m { 127 | s.RequestTimeout(req, timeout) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /stats_test.go: -------------------------------------------------------------------------------- 1 | package templar 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | "time" 7 | 8 | "github.com/amir/raidman" 9 | "github.com/stretchr/testify/require" 10 | "github.com/vektra/neko" 11 | ) 12 | 13 | func TestStatsdOutput(t *testing.T) { 14 | n := neko.Start(t) 15 | 16 | var ( 17 | output *StatsdOutput 18 | client MockStatsdClient 19 | ) 20 | 21 | n.CheckMock(&client.Mock) 22 | 23 | n.Setup(func() { 24 | output = NewStatsdOutput(&client) 25 | }) 26 | 27 | n.It("emits stats about a request on start", func() { 28 | req, err := http.NewRequest("GET", "http://google.com/foo/bar", nil) 29 | require.NoError(t, err) 30 | 31 | client.On("Incr", "templar.request.method.GET", int64(1)).Return(nil) 32 | client.On("Incr", "templar.request.host.google.com", int64(1)).Return(nil) 33 | client.On("Incr", "templar.request.url.google.com-foo-bar", int64(1)).Return(nil) 34 | client.On("GaugeDelta", "templar.requests.active", int64(1)).Return(nil) 35 | 36 | output.StartRequest(req) 37 | }) 38 | 39 | n.It("emits stats about a request on end", func() { 40 | req, err := http.NewRequest("GET", "http://google.com/foo/bar", nil) 41 | require.NoError(t, err) 42 | 43 | t := 1 * time.Second 44 | 45 | client.On("GaugeDelta", "templar.requests.active", int64(-1)).Return(nil) 46 | client.On("PrecisionTiming", 47 | "templar.request.url.google.com-foo-bar", t).Return(nil) 48 | 49 | output.Emit(req, t) 50 | }) 51 | 52 | n.It("emits stats when a request times out", func() { 53 | req, err := http.NewRequest("GET", "http://google.com/foo/bar", nil) 54 | require.NoError(t, err) 55 | 56 | t := 5 * time.Second 57 | 58 | client.On("Incr", "templar.timeout.host.google.com", int64(1)).Return(nil) 59 | client.On("Incr", "templar.timeout.url.google.com-foo-bar", int64(1)).Return(nil) 60 | 61 | output.RequestTimeout(req, t) 62 | 63 | }) 64 | 65 | n.Meow() 66 | } 67 | 68 | func TestRiemannOutput(t *testing.T) { 69 | n := neko.Start(t) 70 | 71 | var ( 72 | output *RiemannOutput 73 | client MockRiemannClient 74 | ) 75 | 76 | n.CheckMock(&client.Mock) 77 | 78 | n.Setup(func() { 79 | output = NewRiemannOutput(&client) 80 | }) 81 | 82 | n.It("emits stats about a request on start", func() { 83 | req, err := http.NewRequest("GET", "http://google.com/foo/bar", nil) 84 | require.NoError(t, err) 85 | 86 | attributes := make(map[string]string) 87 | attributes["method"] = "GET" 88 | attributes["host"] = "google.com" 89 | attributes["path"] = "/foo/bar" 90 | var event = &raidman.Event{ 91 | State: "ok", 92 | Service: "templar request", 93 | Metric: 1, 94 | Attributes: attributes, 95 | } 96 | client.On("Send", event).Return(nil) 97 | 98 | output.StartRequest(req) 99 | }) 100 | 101 | n.It("emits stats about a request on end", func() { 102 | req, err := http.NewRequest("GET", "http://google.com/foo/bar", nil) 103 | require.NoError(t, err) 104 | 105 | t := 1 * time.Second 106 | 107 | attributes := make(map[string]string) 108 | attributes["method"] = "GET" 109 | attributes["host"] = "google.com" 110 | attributes["path"] = "/foo/bar" 111 | var event = &raidman.Event{ 112 | State: "ok", 113 | Service: "templar response", 114 | Metric: 1000.0, 115 | Attributes: attributes, 116 | } 117 | client.On("Send", event).Return(nil) 118 | 119 | output.Emit(req, t) 120 | }) 121 | 122 | n.It("emits stats when a request times out", func() { 123 | req, err := http.NewRequest("GET", "http://google.com/foo/bar", nil) 124 | require.NoError(t, err) 125 | 126 | t := 5 * time.Second 127 | 128 | attributes := make(map[string]string) 129 | attributes["method"] = "GET" 130 | attributes["host"] = "google.com" 131 | attributes["path"] = "/foo/bar" 132 | var event = &raidman.Event{ 133 | State: "warning", 134 | Service: "templar timeout", 135 | Metric: 5000.0, 136 | Attributes: attributes, 137 | } 138 | client.On("Send", event).Return(nil) 139 | 140 | output.RequestTimeout(req, t) 141 | }) 142 | 143 | n.Meow() 144 | } 145 | -------------------------------------------------------------------------------- /collapse_test.go: -------------------------------------------------------------------------------- 1 | package templar 2 | 3 | import ( 4 | "net/http" 5 | "sync" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/mock" 11 | "github.com/stretchr/testify/require" 12 | "github.com/vektra/neko" 13 | ) 14 | 15 | type heldClient struct { 16 | begin chan struct{} 17 | wait chan struct{} 18 | } 19 | 20 | func (h *heldClient) Finish() { 21 | h.wait <- struct{}{} 22 | } 23 | 24 | func (h *heldClient) Forward(res Responder, req *http.Request) error { 25 | h.begin <- struct{}{} 26 | <-h.wait 27 | 28 | return nil 29 | } 30 | 31 | func TestCollapse(t *testing.T) { 32 | n := neko.Start(t) 33 | 34 | var ( 35 | collapse *Collapser 36 | client MockClient 37 | ) 38 | 39 | n.CheckMock(&client.Mock) 40 | 41 | n.Setup(func() { 42 | collapse = NewCollapser(&client, NewCategorizer()) 43 | }) 44 | 45 | n.It("sends a request on to the downstream client", func() { 46 | req, err := http.NewRequest("GET", "http://google.com/foo/bar", nil) 47 | require.NoError(t, err) 48 | 49 | res := newRecordingSender() 50 | 51 | client.On("Forward", mock.Anything, req).Return(nil) 52 | 53 | collapse.Forward(res, req) 54 | }) 55 | 56 | n.It("registers itself as an ongoing request", func() { 57 | req, err := http.NewRequest("GET", "http://google.com/foo/bar", nil) 58 | require.NoError(t, err) 59 | 60 | res := newRecordingSender() 61 | 62 | held := &heldClient{ 63 | make(chan struct{}), 64 | make(chan struct{}), 65 | } 66 | 67 | defer held.Finish() 68 | 69 | collapse := NewCollapser(held, NewCategorizer()) 70 | 71 | go func() { 72 | collapse.Forward(res, req) 73 | }() 74 | 75 | <-held.begin 76 | 77 | _, ok := collapse.running[req.URL.String()] 78 | assert.True(t, ok) 79 | }) 80 | 81 | n.It("reuses a ongoing request if possible", func() { 82 | req, err := http.NewRequest("GET", "http://google.com/foo/bar", nil) 83 | require.NoError(t, err) 84 | 85 | parentRes := newRecordingSender() 86 | childRes := newRecordingSender() 87 | 88 | rr := &RunningRequest{ 89 | Request: req, 90 | others: []Responder{parentRes}, 91 | done: make(chan struct{}), 92 | } 93 | 94 | collapse.running[req.URL.String()] = rr 95 | 96 | c := make(chan struct{}) 97 | 98 | go func() { 99 | collapse.Forward(childRes, req) 100 | c <- struct{}{} 101 | }() 102 | 103 | // simulate waiting on the upstream 104 | time.Sleep(10 * time.Millisecond) 105 | 106 | upres := &http.Response{ 107 | Status: "200 OK", 108 | StatusCode: 200, 109 | Proto: "HTTP/1.1", 110 | ProtoMajor: 1, 111 | ProtoMinor: 1, 112 | Header: http.Header{"X-Templar-Check": []string{"ok"}}, 113 | Body: nil, 114 | Request: req, 115 | } 116 | 117 | io := collapse.finish(req, upres, rr) 118 | CopyBody(io, upres.Body) 119 | 120 | select { 121 | case <-time.NewTimer(1 * time.Second).C: 122 | t.Fatal() 123 | case <-c: 124 | // all good 125 | } 126 | 127 | assert.Equal(t, childRes.w.Code, 200) 128 | assert.Equal(t, "ok", childRes.w.Header().Get("X-Templar-Check")) 129 | }) 130 | 131 | n.It("does not collapse stateful requests", func() { 132 | req, err := http.NewRequest("POST", "http://google.com/foo/bar", nil) 133 | require.NoError(t, err) 134 | 135 | res := newRecordingSender() 136 | 137 | client.On("Forward", mock.Anything, req).Return(nil) 138 | 139 | collapse.Forward(res, req) 140 | 141 | assert.Equal(t, len(collapse.running), 0) 142 | }) 143 | 144 | n.It("can collapse multiple requests into one", func() { 145 | upstream := NewUpstream(&slowTransport{1}, &MockStats{}) 146 | 147 | collapse := NewCollapser(upstream, NewCategorizer()) 148 | 149 | var wg sync.WaitGroup 150 | 151 | var responses []*recordingSender 152 | 153 | for i := 0; i < 10; i++ { 154 | req, err := http.NewRequest("GET", "http://google.com/foo/bar", nil) 155 | require.NoError(t, err) 156 | 157 | res := newRecordingSender() 158 | 159 | responses = append(responses, res) 160 | 161 | wg.Add(1) 162 | go func(res Responder, req *http.Request) { 163 | defer wg.Done() 164 | 165 | collapse.Forward(res, req) 166 | }(res, req) 167 | } 168 | 169 | wg.Wait() 170 | 171 | first := responses[0].w.Body.String() 172 | 173 | assert.True(t, len(first) != 0) 174 | 175 | for _, resp := range responses { 176 | assert.Equal(t, first, resp.w.Body.String()) 177 | } 178 | }) 179 | 180 | n.Meow() 181 | } 182 | -------------------------------------------------------------------------------- /cache_test.go: -------------------------------------------------------------------------------- 1 | package templar 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | "github.com/vektra/neko" 10 | ) 11 | 12 | func TestFallbackCache(t *testing.T) { 13 | n := neko.Start(t) 14 | 15 | var ( 16 | backend MockCacheBackend 17 | transport MockTransport 18 | cache *FallbackCacher 19 | ) 20 | 21 | n.CheckMock(&backend.Mock) 22 | n.CheckMock(&transport.Mock) 23 | 24 | n.Setup(func() { 25 | cache = NewFallbackCacher(&backend, &transport, NewCategorizer()) 26 | }) 27 | 28 | n.It("does not cache anything unless the cache header indicates so", func() { 29 | req, err := http.NewRequest("GET", "http://google.com/foo/bar", nil) 30 | require.NoError(t, err) 31 | 32 | upstream := &http.Response{ 33 | Request: req, 34 | Header: make(http.Header), 35 | } 36 | 37 | transport.On("RoundTrip", req).Return(upstream, nil) 38 | 39 | _, err = cache.RoundTrip(req) 40 | require.NoError(t, err) 41 | }) 42 | 43 | n.It("caches a response that flows though it", func() { 44 | req, err := http.NewRequest("GET", "http://google.com/foo/bar", nil) 45 | require.NoError(t, err) 46 | 47 | req.Header.Add(CacheHeader, "fallback") 48 | 49 | upstream := &http.Response{ 50 | Request: req, 51 | Header: make(http.Header), 52 | } 53 | 54 | transport.On("RoundTrip", req).Return(upstream, nil) 55 | backend.On("Set", req, upstream).Return(nil) 56 | 57 | _, err = cache.RoundTrip(req) 58 | require.NoError(t, err) 59 | }) 60 | 61 | n.It("retrieves the value from the cache via fallback", func() { 62 | req, err := http.NewRequest("GET", "http://google.com/foo/bar", nil) 63 | require.NoError(t, err) 64 | 65 | req.Header.Add(CacheHeader, "fallback") 66 | 67 | upstream := &http.Response{ 68 | Request: req, 69 | Header: make(http.Header), 70 | } 71 | 72 | backend.On("Get", req).Return(upstream, true) 73 | 74 | out, err := cache.Fallback(req) 75 | require.NoError(t, err) 76 | 77 | assert.Equal(t, upstream, out) 78 | 79 | assert.Equal(t, "yes", out.Header.Get("X-Templar-Cached")) 80 | }) 81 | 82 | n.It("does not cache stateful requests", func() { 83 | req, err := http.NewRequest("POST", "http://google.com/foo/bar", nil) 84 | require.NoError(t, err) 85 | 86 | req.Header.Add(CacheHeader, "fallback") 87 | 88 | upstream := &http.Response{ 89 | Request: req, 90 | Header: make(http.Header), 91 | } 92 | 93 | transport.On("RoundTrip", req).Return(upstream, nil) 94 | 95 | _, err = cache.RoundTrip(req) 96 | require.NoError(t, err) 97 | }) 98 | 99 | n.Meow() 100 | } 101 | 102 | func TestEagerCache(t *testing.T) { 103 | n := neko.Start(t) 104 | 105 | var ( 106 | backend MockCacheBackend 107 | transport MockTransport 108 | cache *EagerCacher 109 | ) 110 | 111 | n.CheckMock(&backend.Mock) 112 | n.CheckMock(&transport.Mock) 113 | 114 | n.Setup(func() { 115 | cache = NewEagerCacher(&backend, &transport, NewCategorizer()) 116 | }) 117 | 118 | n.It("normally does not cache anything", func() { 119 | req, err := http.NewRequest("GET", "http://google.com/foo/bar", nil) 120 | require.NoError(t, err) 121 | 122 | upstream := &http.Response{ 123 | Request: req, 124 | Header: make(http.Header), 125 | } 126 | 127 | transport.On("RoundTrip", req).Return(upstream, nil) 128 | 129 | _, err = cache.RoundTrip(req) 130 | require.NoError(t, err) 131 | }) 132 | 133 | n.It("caches a response that flows though it", func() { 134 | req, err := http.NewRequest("GET", "http://google.com/foo/bar", nil) 135 | require.NoError(t, err) 136 | 137 | req.Header.Add(CacheHeader, "eager") 138 | 139 | upstream := &http.Response{ 140 | Request: req, 141 | Header: make(http.Header), 142 | } 143 | 144 | backend.On("Get", req).Return((*http.Response)(nil), false) 145 | transport.On("RoundTrip", req).Return(upstream, nil) 146 | backend.On("Set", req, upstream).Return(nil) 147 | 148 | _, err = cache.RoundTrip(req) 149 | require.NoError(t, err) 150 | }) 151 | 152 | n.It("returns a cached value if available", func() { 153 | req, err := http.NewRequest("GET", "http://google.com/foo/bar", nil) 154 | require.NoError(t, err) 155 | 156 | req.Header.Add(CacheHeader, "eager") 157 | 158 | upstream := &http.Response{ 159 | Request: req, 160 | Header: make(http.Header), 161 | } 162 | 163 | backend.On("Get", req).Return(upstream, true) 164 | 165 | out, err := cache.RoundTrip(req) 166 | require.NoError(t, err) 167 | 168 | assert.Equal(t, "yes", out.Header.Get("X-Templar-Cached")) 169 | }) 170 | 171 | n.It("does not cache stateful requests", func() { 172 | req, err := http.NewRequest("POST", "http://google.com/foo/bar", nil) 173 | require.NoError(t, err) 174 | 175 | req.Header.Add(CacheHeader, "eager") 176 | 177 | upstream := &http.Response{ 178 | Request: req, 179 | Header: make(http.Header), 180 | } 181 | 182 | transport.On("RoundTrip", req).Return(upstream, nil) 183 | 184 | _, err = cache.RoundTrip(req) 185 | require.NoError(t, err) 186 | }) 187 | 188 | n.Meow() 189 | } 190 | -------------------------------------------------------------------------------- /cache/cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | ) 7 | 8 | // Length of time to cache an item. 9 | const ( 10 | DEFAULT = time.Duration(0) 11 | FOREVER = time.Duration(-1) 12 | ) 13 | 14 | // Getter is an interface for getting / decoding an element from a cache. 15 | type Getter interface { 16 | // Get the content associated with the given key. decoding it into the given 17 | // pointer. 18 | // 19 | // Returns: 20 | // - nil if the value was successfully retrieved and ptrValue set 21 | // - ErrCacheMiss if the value was not in the cache 22 | // - an implementation specific error otherwise 23 | Get(key string, ptrValue interface{}) error 24 | } 25 | 26 | // Cache is an interface to an expiring cache. It behaves (and is modeled) like 27 | // the Memcached interface. It is keyed by strings (250 bytes at most). 28 | // 29 | // Many callers will make exclusive use of Set and Get, but more exotic 30 | // functions are also available. 31 | // 32 | // Example 33 | // 34 | // Here is a typical Get/Set interaction: 35 | // 36 | // var items []*Item 37 | // if err := cache.Get("items", &items); err != nil { 38 | // items = loadItems() 39 | // go cache.Set("items", items, cache.DEFAULT) 40 | // } 41 | // 42 | // Note that the caller will frequently not wait for Set() to complete. 43 | // 44 | 45 | type Cache interface { 46 | // The Cache implements a Getter. 47 | Getter 48 | 49 | // Set the given key/value in the cache, overwriting any existing value 50 | // associated with that key. Keys may be at most 250 bytes in length. 51 | // 52 | // Returns: 53 | // - nil on success 54 | // - an implementation specific error otherwise 55 | Set(key string, value interface{}, expires time.Duration) error 56 | 57 | // Get the content associated multiple keys at once. On success, the caller 58 | // may decode the values one at a time from the returned Getter. 59 | // 60 | // Returns: 61 | // - the value getter, and a nil error if the operation completed. 62 | // - an implementation specific error otherwise 63 | GetMulti(keys ...string) (Getter, error) 64 | 65 | // Delete the given key from the cache. 66 | // 67 | // Returns: 68 | // - nil on a successful delete 69 | // - ErrCacheMiss if the value was not in the cache 70 | // - an implementation specific error otherwise 71 | Delete(key string) error 72 | 73 | // Add the given key/value to the cache ONLY IF the key does not already exist. 74 | // 75 | // Returns: 76 | // - nil if the value was added to the cache 77 | // - ErrNotStored if the key was already present in the cache 78 | // - an implementation-specific error otherwise 79 | Add(key string, value interface{}, expires time.Duration) error 80 | 81 | // Set the given key/value in the cache ONLY IF the key already exists. 82 | // 83 | // Returns: 84 | // - nil if the value was replaced 85 | // - ErrNotStored if the key does not exist in the cache 86 | // - an implementation specific error otherwise 87 | Replace(key string, value interface{}, expires time.Duration) error 88 | 89 | // Increment the value stored at the given key by the given amount. 90 | // The value silently wraps around upon exceeding the uint64 range. 91 | // 92 | // Returns the new counter value if the operation was successful, or: 93 | // - ErrCacheMiss if the key was not found in the cache 94 | // - an implementation specific error otherwise 95 | Increment(key string, n uint64) (newValue uint64, err error) 96 | 97 | // Decrement the value stored at the given key by the given amount. 98 | // The value is capped at 0 on underflow, with no error returned. 99 | // 100 | // Returns the new counter value if the operation was successful, or: 101 | // - ErrCacheMiss if the key was not found in the cache 102 | // - an implementation specific error otherwise 103 | Decrement(key string, n uint64) (newValue uint64, err error) 104 | 105 | // Expire all cache entries immediately. 106 | // This is not implemented for the memcached cache (intentionally). 107 | // Returns an implementation specific error if the operation failed. 108 | Flush() error 109 | } 110 | 111 | var ( 112 | Instance Cache 113 | 114 | ErrCacheMiss = errors.New("cache: key not found.") 115 | ErrNotStored = errors.New("cache: not stored.") 116 | ) 117 | 118 | // The package implements the Cache interface (as sugar). 119 | 120 | func Get(key string, ptrValue interface{}) error { return Instance.Get(key, ptrValue) } 121 | func GetMulti(keys ...string) (Getter, error) { return Instance.GetMulti(keys...) } 122 | func Delete(key string) error { return Instance.Delete(key) } 123 | func Increment(key string, n uint64) (newValue uint64, err error) { return Instance.Increment(key, n) } 124 | func Decrement(key string, n uint64) (newValue uint64, err error) { return Instance.Decrement(key, n) } 125 | func Flush() error { return Instance.Flush() } 126 | func Set(key string, value interface{}, expires time.Duration) error { 127 | return Instance.Set(key, value, expires) 128 | } 129 | func Add(key string, value interface{}, expires time.Duration) error { 130 | return Instance.Add(key, value, expires) 131 | } 132 | func Replace(key string, value interface{}, expires time.Duration) error { 133 | return Instance.Replace(key, value, expires) 134 | } 135 | -------------------------------------------------------------------------------- /cache/redis.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/garyburd/redigo/redis" 7 | ) 8 | 9 | // Wraps the Redis client to meet the Cache interface. 10 | type RedisCache struct { 11 | pool *redis.Pool 12 | defaultExpiration time.Duration 13 | } 14 | 15 | // until redigo supports sharding/clustering, only one host will be in hostList 16 | func NewRedisCache(host string, password string, defaultExpiration time.Duration) RedisCache { 17 | var pool = &redis.Pool{ 18 | MaxIdle: 5, 19 | MaxActive: 0, 20 | IdleTimeout: 240 * time.Second, 21 | Dial: func() (redis.Conn, error) { 22 | protocol := "tcp" 23 | toc := time.Millisecond * 10000 24 | tor := time.Millisecond * 5000 25 | tow := time.Millisecond * 5000 26 | 27 | c, err := redis.DialTimeout(protocol, host, toc, tor, tow) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | if len(password) > 0 { 33 | if _, err := c.Do("AUTH", password); err != nil { 34 | c.Close() 35 | return nil, err 36 | } 37 | } else { 38 | // check with PING 39 | if _, err := c.Do("PING"); err != nil { 40 | c.Close() 41 | return nil, err 42 | } 43 | } 44 | return c, err 45 | }, 46 | // custom connection test method 47 | TestOnBorrow: func(c redis.Conn, t time.Time) error { 48 | if _, err := c.Do("PING"); err != nil { 49 | return err 50 | } 51 | return nil 52 | }, 53 | } 54 | return RedisCache{pool, defaultExpiration} 55 | } 56 | 57 | func (c RedisCache) Set(key string, value interface{}, expires time.Duration) error { 58 | conn := c.pool.Get() 59 | defer conn.Close() 60 | return c.invoke(conn.Do, key, value, expires) 61 | } 62 | 63 | func (c RedisCache) Add(key string, value interface{}, expires time.Duration) error { 64 | conn := c.pool.Get() 65 | defer conn.Close() 66 | existed, err := exists(conn, key) 67 | if err != nil { 68 | return err 69 | } else if existed { 70 | return ErrNotStored 71 | } 72 | return c.invoke(conn.Do, key, value, expires) 73 | } 74 | 75 | func (c RedisCache) Replace(key string, value interface{}, expires time.Duration) error { 76 | conn := c.pool.Get() 77 | defer conn.Close() 78 | existed, err := exists(conn, key) 79 | if err != nil { 80 | return err 81 | } else if !existed { 82 | return ErrNotStored 83 | } 84 | err = c.invoke(conn.Do, key, value, expires) 85 | if value == nil { 86 | return ErrNotStored 87 | } else { 88 | return err 89 | } 90 | } 91 | 92 | func (c RedisCache) Get(key string, ptrValue interface{}) error { 93 | conn := c.pool.Get() 94 | defer conn.Close() 95 | raw, err := conn.Do("GET", key) 96 | if err != nil { 97 | return err 98 | } else if raw == nil { 99 | return ErrCacheMiss 100 | } 101 | item, err := redis.Bytes(raw, err) 102 | if err != nil { 103 | return err 104 | } 105 | return Deserialize(item, ptrValue) 106 | } 107 | 108 | func generalizeStringSlice(strs []string) []interface{} { 109 | ret := make([]interface{}, len(strs)) 110 | for i, str := range strs { 111 | ret[i] = str 112 | } 113 | return ret 114 | } 115 | 116 | func (c RedisCache) GetMulti(keys ...string) (Getter, error) { 117 | conn := c.pool.Get() 118 | defer conn.Close() 119 | 120 | items, err := redis.Values(conn.Do("MGET", generalizeStringSlice(keys)...)) 121 | if err != nil { 122 | return nil, err 123 | } else if items == nil { 124 | return nil, ErrCacheMiss 125 | } 126 | 127 | m := make(map[string][]byte) 128 | for i, key := range keys { 129 | m[key] = nil 130 | if i < len(items) && items[i] != nil { 131 | s, ok := items[i].([]byte) 132 | if ok { 133 | m[key] = s 134 | } 135 | } 136 | } 137 | return RedisItemMapGetter(m), nil 138 | } 139 | 140 | func exists(conn redis.Conn, key string) (bool, error) { 141 | return redis.Bool(conn.Do("EXISTS", key)) 142 | } 143 | 144 | func (c RedisCache) Delete(key string) error { 145 | conn := c.pool.Get() 146 | defer conn.Close() 147 | existed, err := redis.Bool(conn.Do("DEL", key)) 148 | if err == nil && !existed { 149 | err = ErrCacheMiss 150 | } 151 | return err 152 | } 153 | 154 | func (c RedisCache) Increment(key string, delta uint64) (uint64, error) { 155 | conn := c.pool.Get() 156 | defer conn.Close() 157 | // Check for existance *before* increment as per the cache contract. 158 | // redis will auto create the key, and we don't want that. Since we need to do increment 159 | // ourselves instead of natively via INCRBY (redis doesn't support wrapping), we get the value 160 | // and do the exists check this way to minimize calls to Redis 161 | val, err := conn.Do("GET", key) 162 | if err != nil { 163 | return 0, err 164 | } else if val == nil { 165 | return 0, ErrCacheMiss 166 | } 167 | currentVal, err := redis.Int64(val, nil) 168 | if err != nil { 169 | return 0, err 170 | } 171 | var sum int64 = currentVal + int64(delta) 172 | _, err = conn.Do("SET", key, sum) 173 | if err != nil { 174 | return 0, err 175 | } 176 | return uint64(sum), nil 177 | } 178 | 179 | func (c RedisCache) Decrement(key string, delta uint64) (newValue uint64, err error) { 180 | conn := c.pool.Get() 181 | defer conn.Close() 182 | // Check for existance *before* increment as per the cache contract. 183 | // redis will auto create the key, and we don't want that, hence the exists call 184 | existed, err := exists(conn, key) 185 | if err != nil { 186 | return 0, err 187 | } else if !existed { 188 | return 0, ErrCacheMiss 189 | } 190 | // Decrement contract says you can only go to 0 191 | // so we go fetch the value and if the delta is greater than the amount, 192 | // 0 out the value 193 | currentVal, err := redis.Int64(conn.Do("GET", key)) 194 | if err != nil { 195 | return 0, err 196 | } 197 | if delta > uint64(currentVal) { 198 | tempint, err := redis.Int64(conn.Do("DECRBY", key, currentVal)) 199 | return uint64(tempint), err 200 | } 201 | tempint, err := redis.Int64(conn.Do("DECRBY", key, delta)) 202 | return uint64(tempint), err 203 | } 204 | 205 | func (c RedisCache) Flush() error { 206 | conn := c.pool.Get() 207 | defer conn.Close() 208 | _, err := conn.Do("FLUSHALL") 209 | return err 210 | } 211 | 212 | func (c RedisCache) invoke(f func(string, ...interface{}) (interface{}, error), 213 | key string, value interface{}, expires time.Duration) error { 214 | 215 | switch expires { 216 | case DEFAULT: 217 | expires = c.defaultExpiration 218 | case FOREVER: 219 | expires = time.Duration(0) 220 | } 221 | 222 | b, err := Serialize(value) 223 | if err != nil { 224 | return err 225 | } 226 | conn := c.pool.Get() 227 | defer conn.Close() 228 | if expires > 0 { 229 | _, err := f("SETEX", key, int32(expires/time.Second), b) 230 | return err 231 | } else { 232 | _, err := f("SET", key, b) 233 | return err 234 | } 235 | } 236 | 237 | // Implement a Getter on top of the returned item map. 238 | type RedisItemMapGetter map[string][]byte 239 | 240 | func (g RedisItemMapGetter) Get(key string, ptrValue interface{}) error { 241 | item, ok := g[key] 242 | if !ok { 243 | return ErrCacheMiss 244 | } 245 | return Deserialize(item, ptrValue) 246 | } 247 | -------------------------------------------------------------------------------- /cache/cache_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | // Tests against a generic Cache interface. 10 | // They should pass for all implementations. 11 | type cacheFactory func(*testing.T, time.Duration) Cache 12 | 13 | // Test typical cache interactions 14 | func typicalGetSet(t *testing.T, newCache cacheFactory) { 15 | var err error 16 | cache := newCache(t, time.Hour) 17 | 18 | value := "foo" 19 | if err = cache.Set("value", value, DEFAULT); err != nil { 20 | t.Errorf("Error setting a value: %s", err) 21 | } 22 | 23 | value = "" 24 | err = cache.Get("value", &value) 25 | if err != nil { 26 | t.Errorf("Error getting a value: %s", err) 27 | } 28 | if value != "foo" { 29 | t.Errorf("Expected to get foo back, got %s", value) 30 | } 31 | } 32 | 33 | // Test the increment-decrement cases 34 | func incrDecr(t *testing.T, newCache cacheFactory) { 35 | var err error 36 | cache := newCache(t, time.Hour) 37 | 38 | // Normal increment / decrement operation. 39 | if err = cache.Set("int", 10, DEFAULT); err != nil { 40 | t.Errorf("Error setting int: %s", err) 41 | } 42 | newValue, err := cache.Increment("int", 50) 43 | if err != nil { 44 | t.Errorf("Error incrementing int: %s", err) 45 | } 46 | if newValue != 60 { 47 | t.Errorf("Expected 60, was %d", newValue) 48 | } 49 | 50 | if newValue, err = cache.Decrement("int", 50); err != nil { 51 | t.Errorf("Error decrementing: %s", err) 52 | } 53 | if newValue != 10 { 54 | t.Errorf("Expected 10, was %d", newValue) 55 | } 56 | 57 | // Increment wraparound 58 | newValue, err = cache.Increment("int", math.MaxUint64-5) 59 | if err != nil { 60 | t.Errorf("Error wrapping around: %s", err) 61 | } 62 | if newValue != 4 { 63 | t.Errorf("Expected wraparound 4, got %d", newValue) 64 | } 65 | 66 | // Decrement capped at 0 67 | newValue, err = cache.Decrement("int", 25) 68 | if err != nil { 69 | t.Errorf("Error decrementing below 0: %s", err) 70 | } 71 | if newValue != 0 { 72 | t.Errorf("Expected capped at 0, got %d", newValue) 73 | } 74 | } 75 | 76 | func expiration(t *testing.T, newCache cacheFactory) { 77 | // memcached does not support expiration times less than 1 second. 78 | var err error 79 | cache := newCache(t, time.Second) 80 | // Test Set w/ DEFAULT 81 | value := 10 82 | cache.Set("int", value, DEFAULT) 83 | time.Sleep(2 * time.Second) 84 | err = cache.Get("int", &value) 85 | if err != ErrCacheMiss { 86 | t.Errorf("Expected CacheMiss, but got: %s", err) 87 | } 88 | 89 | // Test Set w/ short time 90 | cache.Set("int", value, time.Second) 91 | time.Sleep(2 * time.Second) 92 | err = cache.Get("int", &value) 93 | if err != ErrCacheMiss { 94 | t.Errorf("Expected CacheMiss, but got: %s", err) 95 | } 96 | 97 | // Test Set w/ longer time. 98 | cache.Set("int", value, time.Hour) 99 | time.Sleep(2 * time.Second) 100 | err = cache.Get("int", &value) 101 | if err != nil { 102 | t.Errorf("Expected to get the value, but got: %s", err) 103 | } 104 | 105 | // Test Set w/ forever. 106 | cache.Set("int", value, FOREVER) 107 | time.Sleep(2 * time.Second) 108 | err = cache.Get("int", &value) 109 | if err != nil { 110 | t.Errorf("Expected to get the value, but got: %s", err) 111 | } 112 | } 113 | 114 | func emptyCache(t *testing.T, newCache cacheFactory) { 115 | var err error 116 | cache := newCache(t, time.Hour) 117 | 118 | err = cache.Get("notexist", 0) 119 | if err == nil { 120 | t.Errorf("Error expected for non-existent key") 121 | } 122 | if err != ErrCacheMiss { 123 | t.Errorf("Expected ErrCacheMiss for non-existent key: %s", err) 124 | } 125 | 126 | err = cache.Delete("notexist") 127 | if err != ErrCacheMiss { 128 | t.Errorf("Expected ErrCacheMiss for non-existent key: %s", err) 129 | } 130 | 131 | _, err = cache.Increment("notexist", 1) 132 | if err != ErrCacheMiss { 133 | t.Errorf("Expected cache miss incrementing non-existent key: %s", err) 134 | } 135 | 136 | _, err = cache.Decrement("notexist", 1) 137 | if err != ErrCacheMiss { 138 | t.Errorf("Expected cache miss decrementing non-existent key: %s", err) 139 | } 140 | } 141 | 142 | func testReplace(t *testing.T, newCache cacheFactory) { 143 | var err error 144 | cache := newCache(t, time.Hour) 145 | 146 | // Replace in an empty cache. 147 | if err = cache.Replace("notexist", 1, FOREVER); err != ErrNotStored { 148 | t.Errorf("Replace in empty cache: expected ErrNotStored, got: %s", err) 149 | } 150 | 151 | // Set a value of 1, and replace it with 2 152 | if err = cache.Set("int", 1, time.Second); err != nil { 153 | t.Errorf("Unexpected error: %s", err) 154 | } 155 | 156 | if err = cache.Replace("int", 2, time.Second); err != nil { 157 | t.Errorf("Unexpected error: %s", err) 158 | } 159 | var i int 160 | if err = cache.Get("int", &i); err != nil { 161 | t.Errorf("Unexpected error getting a replaced item: %s", err) 162 | } 163 | if i != 2 { 164 | t.Errorf("Expected 2, got %d", i) 165 | } 166 | 167 | // Wait for it to expire and replace with 3 (unsuccessfully). 168 | time.Sleep(2 * time.Second) 169 | if err = cache.Replace("int", 3, time.Second); err != ErrNotStored { 170 | t.Errorf("Expected ErrNotStored, got: %s", err) 171 | } 172 | if err = cache.Get("int", &i); err != ErrCacheMiss { 173 | t.Errorf("Expected cache miss, got: %s", err) 174 | } 175 | } 176 | 177 | func testAdd(t *testing.T, newCache cacheFactory) { 178 | var err error 179 | cache := newCache(t, time.Hour) 180 | // Add to an empty cache. 181 | if err = cache.Add("int", 1, time.Second); err != nil { 182 | t.Errorf("Unexpected error adding to empty cache: %s", err) 183 | } 184 | 185 | // Try to add again. (fail) 186 | if err = cache.Add("int", 2, time.Second); err != ErrNotStored { 187 | t.Errorf("Expected ErrNotStored adding dupe to cache: %s", err) 188 | } 189 | 190 | // Wait for it to expire, and add again. 191 | time.Sleep(2 * time.Second) 192 | if err = cache.Add("int", 3, time.Second); err != nil { 193 | t.Errorf("Unexpected error adding to cache: %s", err) 194 | } 195 | 196 | // Get and verify the value. 197 | var i int 198 | if err = cache.Get("int", &i); err != nil { 199 | t.Errorf("Unexpected error: %s", err) 200 | } 201 | if i != 3 { 202 | t.Errorf("Expected 3, got: %d", i) 203 | } 204 | } 205 | 206 | func testGetMulti(t *testing.T, newCache cacheFactory) { 207 | cache := newCache(t, time.Hour) 208 | 209 | m := map[string]interface{}{ 210 | "str": "foo", 211 | "num": 42, 212 | "foo": struct{ Bar string }{"baz"}, 213 | } 214 | 215 | var keys []string 216 | for key, value := range m { 217 | keys = append(keys, key) 218 | if err := cache.Set(key, value, DEFAULT); err != nil { 219 | t.Errorf("Error setting a value: %s", err) 220 | } 221 | } 222 | 223 | g, err := cache.GetMulti(keys...) 224 | if err != nil { 225 | t.Errorf("Error in get-multi: %s", err) 226 | } 227 | 228 | var str string 229 | if err = g.Get("str", &str); err != nil || str != "foo" { 230 | t.Errorf("Error getting str: %s / %s", err, str) 231 | } 232 | 233 | var num int 234 | if err = g.Get("num", &num); err != nil || num != 42 { 235 | t.Errorf("Error getting num: %s / %v", err, num) 236 | } 237 | 238 | var foo struct{ Bar string } 239 | if err = g.Get("foo", &foo); err != nil || foo.Bar != "baz" { 240 | t.Errorf("Error getting foo: %s / %v", err, foo) 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | templar 2 | ======= 3 | 4 | [![Build Status](https://travis-ci.org/vektra/templar.svg?branch=master)](https://travis-ci.org/vektra/templar) 5 | 6 | HTTP APIs, they're everywhere. But they have a serious problem: their 7 | sychronous nature means that code using them stalls while waiting 8 | for a reply. 9 | 10 | This means that your apps uptime and reliability are intertwined with 11 | whatever HTTP APIs, especially SaaS ones, you use. 12 | 13 | templar helps you control the problem. 14 | 15 | It is a an HTTP proxy that provides advanced features to help you make 16 | better use of and tame HTTP APIs. 17 | 18 | ## Installation 19 | 20 | Directly via go: `go get github.com/vektra/templar/cmd/templar` 21 | 22 | ### Linux 23 | 24 | * [i386](https://bintray.com/artifact/download/evanphx/templar/templar-linux-386.tar.gz) 25 | * [amd64](https://bintray.com/artifact/download/evanphx/templar/templar-linux-amd64.tar.gz) 26 | 27 | ### Darwin 28 | 29 | * [amd64](https://bintray.com/artifact/download/evanphx/templar/templar-darwin-amd64.tar.gz) 30 | 31 | ### Windows 32 | 33 | * [i386](https://bintray.com/artifact/download/evanphx/templar/templar-windows-386.zip) 34 | * [amd64](https://bintray.com/artifact/download/evanphx/templar/templar-windows-amd64.zip) 35 | 36 | ## Usage 37 | 38 | templar functions like an HTTP proxy, allowing you use your favorite HTTP client 39 | to easily send requests through it. Various languages have different HTTP clients 40 | but many respect the `http_proxy` environment variable that you can set to the 41 | address templar is running on. 42 | 43 | Most HTTP clients in various programming languages have some configuration 44 | to configure the proxy directly as well. Nearly all of them do, just check 45 | the docs. 46 | 47 | ### HTTPS 48 | 49 | Many HTTP APIs located in SaaS products are available only via HTTPS. This is a 50 | good thing though it makes templar's job a little harder. We don't want to a client 51 | to use CONNECT because then we can't provide any value. So to interact with these APIs, 52 | use the `X-Templar-Upgrade` header. Configure your client to talk to the API 53 | as normal http but include `X-Templar-Upgrade: https` and templar will be able 54 | manage your requests and still talk to the https service! 55 | 56 | ### Examples 57 | 58 | Do a request through templar, no timeout, no caching: 59 | 60 | `curl -x http://localhost:9224 http://api.openweathermap.org/data/2.5/weather?q=Los+Angeles,CA` 61 | 62 | 63 | Now add some caching in, caching the value for a minute at a time: 64 | 65 | `curl -x http://localhost:9224 -H "X-Templar-Cache: eager" -H "X-Templar-CacheFor: 1m" 'http://api.openweathermap.org/data/2.5/weather?q=Los+Angeles,CA'` 66 | 67 | 68 | ## Features 69 | 70 | ### Timeouts 71 | 72 | It's important that timeouts are used when accessing a synchronous API like an 73 | HTTP endpoint. It's not uncommon for upstream APIs to have no 74 | timeouts to fulfill a request so that typically needs to be done on the client 75 | side. Effect use of timeouts on these APIs will improve the robustness 76 | of your own system. 77 | 78 | For great discussion on this, check out Jeff Hodges thoughts on the topic: 79 | * https://www.youtube.com/watch?v=BKqgGpAOv1w 80 | * http://www.somethingsimilar.com/2013/01/14/notes-on-distributed-systems-for-young-bloods/ 81 | 82 | At present, templar does not enforce a default timeout, it needs to be set 83 | per request via the `X-Templar-Timeout` header. The format is documented 84 | below under Duration format. 85 | 86 | ### Request collapsing 87 | 88 | Say that your app hits an HTTP endpoint at http://isitawesomeyet.com. 89 | When you send those HTTP requests through templar, it will reduce the 90 | number of requests to the external service to the bare minimum by combining 91 | requests together. So if a request comes in while we're waiting on another 92 | request to the same endpoint, we combine those requests together and 93 | serve the same data to both. This improves response times and reduces 94 | load on upstream servers. 95 | 96 | ### Caching 97 | 98 | Templar can, if requested, cache upstream requests. By setting the 99 | `X-Templar-Cache` header to either `fallback` or `eager`, templar 100 | will cache responses to the endpoint and serve them back. 101 | 102 | `fallback` will only use the cache if accessing the endpoint times out. 103 | `eager` will use the cache if it exists first and always repopulate 104 | from the endpoint when needed. 105 | 106 | The `X-Templar-CacheFor` header time is used to control how long a cached 107 | value will be used for. See Duration format below for how to specify the time. 108 | 109 | There are 4 caches available presently: 110 | 111 | * Memory (the default) 112 | * Memcache 113 | * Redis 114 | * Groupcache 115 | 116 | The later 3 are used only if configure on the command line. 117 | 118 | In the future, the plan is to name the caches and allow requests to say which 119 | caching backend they'd like to use. Currently they all use the same one. 120 | 121 | ### Stats generation 122 | 123 | Tracking what APIs are used and how well they're performing is critical to 124 | understanding. When requests flow through templar, it can generate metrics 125 | about those requests and send them to statsd. 126 | 127 | Just specify a statsd host via `-statsd` and templar will start sending them. 128 | 129 | We'll support more metrics backends in the future. 130 | 131 | ## Request categorization 132 | 133 | Not all requests should use some of these features, for instance, request collapsing. 134 | So templar includes a categorizer to identify requests that it should apply 135 | additional handling to. It identifies a request as `stateless` or not. If 136 | it is stateless, then things like request collapsing and caching can be used. 137 | 138 | By default, only GET requests are treated as `stateless`. The `X-Templar-Category` 139 | header allows the user to explicitly specify the category. The 2 valid values are 140 | `stateful` and `stateless`. 141 | 142 | Again, a stateless request is subject to the following additional handling: 143 | 144 | * Request collapsing 145 | * Caching 146 | 147 | ## Duration format 148 | 149 | A number of headers take time durations, for instances, 30 seconds. These use the simple "(number)(unit)" parser, so for 1 second, use `1s` and 5 minutes use `5m`. Units supported are: `ns`, `us`, `ms`, `s`, `m`, and `h`. 150 | 151 | ## Control Headers 152 | 153 | Templar uses a number of headers to control how the requests are processed. 154 | 155 | ### X-Templar-Cache 156 | 157 | Possible values: 158 | 159 | * **eager**: Return a value from the cache before checking upstream 160 | * **fallback**: Return a value from the cache only if the upstream has issues 161 | 162 | ### X-Templar-CacheFor 163 | 164 | When caching, how long to cache the value for. If caching and this isn't set, 165 | the default is used. 166 | 167 | ### X-Templar-Cached 168 | 169 | Set on responses that are served from the cache. 170 | 171 | ### X-Templar-Category 172 | 173 | Possible values: 174 | 175 | * **stateless**: Process the request as stateless 176 | * **stateful**: Process the request as stateful 177 | 178 | ### X-Templar-Timeout 179 | 180 | Specifies how long to wait for the response before giving up. 181 | 182 | ### X-Templar-TimedOut 183 | 184 | Set to `true` on a response when the request timed out. 185 | 186 | ### X-Templar-Upgrade 187 | 188 | Possible values: 189 | 190 | * **https**: When connecting to the upstream, switch to https 191 | 192 | 193 | # Future features 194 | 195 | * Automatic caching based on HTTP Expire headers 196 | * Request throttling 197 | * Multiple active caching backends 198 | * Request stream inspection 199 | * Fire-and-forget requests 200 | * Return response via AMQP 201 | --------------------------------------------------------------------------------