Skip to content

Commit

Permalink
Merge pull request #19 from kenshin579/feat/#18-caching
Browse files Browse the repository at this point in the history
[#18] caching with different expiration time
  • Loading branch information
kenshin579 committed Jul 15, 2023
2 parents 9e2b528 + c132c22 commit 4731aac
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 40 deletions.
42 changes: 31 additions & 11 deletions cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,19 +67,23 @@ type (

Store CacheStore

Expiration time.Duration
IncludePaths []string
ExcludePaths []string
Expiration time.Duration // default expiration
IncludePaths []string
IncludePathsWithExpiration map[string]time.Duration // key: path, value: expiration //IncludePathsWithExpiration has higher priority
ExcludePaths []string
}

// CacheResponse is the cached response data structure.
CacheResponse struct {
// Value is the cached response value.
Value []byte `json:"value"`
// URL is URL
URL string `json:"url"`

// Header is the cached response header.
Header http.Header `json:"header"`

// Body is the cached response value.
Body []byte `json:"body"`

// Expiration is the cached response Expiration date.
Expiration time.Time `json:"expiration"`

Expand Down Expand Up @@ -143,7 +147,6 @@ func CacheWithConfig(config CacheConfig) echo.MiddlewareFunc {
}

if c.Request().Method == http.MethodGet {
// isCached := false
sortURLParams(c.Request().URL)
key := generateKey(c.Request().Method, c.Request().URL.String())

Expand All @@ -162,7 +165,7 @@ func CacheWithConfig(config CacheConfig) echo.MiddlewareFunc {
c.Response().Header().Set(k, strings.Join(v, ","))
}
c.Response().WriteHeader(http.StatusOK)
c.Response().Write(response.Value)
c.Response().Write(response.Body)
return nil
}
}
Expand All @@ -178,18 +181,19 @@ func CacheWithConfig(config CacheConfig) echo.MiddlewareFunc {
}

if writer.statusCode < http.StatusBadRequest {
value := resBody.Bytes()
body := resBody.Bytes()
now := time.Now()

response := CacheResponse{
Value: value,
Body: body,
URL: c.Request().URL.String(),
Header: writer.Header(),
Expiration: now.Add(config.Expiration),
Expiration: config.getExpiration(now, c.Request().URL.String()),
LastAccess: now,
Frequency: 1,
}

if !isAllFieldsEmpty(value) {
if !isAllFieldsEmpty(body) {
config.Store.Set(key, response.bytes(), response.Expiration)
}
}
Expand All @@ -206,6 +210,12 @@ func (c *CacheConfig) isIncludePaths(URL string) bool {
return true
}
}

for k, _ := range c.IncludePathsWithExpiration {
if strings.Contains(URL, k) {
return true
}
}
return false
}

Expand All @@ -218,6 +228,16 @@ func (c *CacheConfig) isExcludePaths(URL string) bool {
return false
}

func (c *CacheConfig) getExpiration(now time.Time, URL string) time.Time {
for k, v := range c.IncludePathsWithExpiration {
if strings.Contains(URL, k) {
return now.Add(v)
}
}

return now.Add(c.Expiration)
}

type bodyDumpResponseWriter struct {
io.Writer
http.ResponseWriter
Expand Down
24 changes: 18 additions & 6 deletions cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func Test_CacheWithConfig(t *testing.T) {
wants wants
}{
{
name: "test IncludePaths",
name: "test IncludePathsWithExpiration",
args: args{
method: http.MethodGet,
url: "http://foo.bar/test-1",
Expand Down Expand Up @@ -138,7 +138,7 @@ func Test_CacheWithConfig(t *testing.T) {
var cacheResponse CacheResponse
err := json.Unmarshal(cacheResp, &cacheResponse)
assert.NoError(t, err)
assert.Equal(t, "test", string(cacheResponse.Value))
assert.Equal(t, "test", string(cacheResponse.Body))
}
})
}
Expand All @@ -161,7 +161,7 @@ func TestCache_panicBehavior(t *testing.T) {

func Test_toCacheResponse(t *testing.T) {
r := CacheResponse{
Value: []byte("value 1"),
Body: []byte("value 1"),
Expiration: time.Time{},
Frequency: 1,
LastAccess: time.Time{},
Expand All @@ -182,21 +182,21 @@ func Test_toCacheResponse(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := toCacheResponse(tt.b)
assert.Equal(t, tt.wantValue, string(got.Value))
assert.Equal(t, tt.wantValue, string(got.Body))
})
}
}

func Test_bytes(t *testing.T) {
r := CacheResponse{
Value: []byte("test"),
Body: []byte("test"),
Expiration: time.Time{},
Frequency: 1,
LastAccess: time.Time{},
}

bytes := r.bytes()
assert.Equal(t, `{"value":"dGVzdA==","header":null,"expiration":"0001-01-01T00:00:00Z","lastAccess":"0001-01-01T00:00:00Z","frequency":1}`, string(bytes))
assert.Equal(t, `{"url":"","header":null,"body":"dGVzdA==","expiration":"0001-01-01T00:00:00Z","lastAccess":"0001-01-01T00:00:00Z","frequency":1}`, string(bytes))
}

func Test_keyAsString(t *testing.T) {
Expand Down Expand Up @@ -324,3 +324,15 @@ func Test_isAllFieldsEmpty(t *testing.T) {
assert.True(t, isAllFieldsEmpty([]byte(`{"a":"","b":"","c":0}`)))
assert.True(t, isAllFieldsEmpty([]byte(`{"a":"","b":"","c":0.0}`)))
}

func Test_isIncludePaths(t *testing.T) {
config := CacheConfig{
IncludePathsWithExpiration: map[string]time.Duration{
"/test1": time.Duration(1) * time.Second,
"/test2": time.Duration(2) * time.Second,
},
}
assert.False(t, config.isIncludePaths("/test3"))
assert.True(t, config.isIncludePaths("/test1"))
assert.True(t, config.isIncludePaths("/test2"))
}
26 changes: 13 additions & 13 deletions memory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func TestGet(t *testing.T) {
LRU,
map[uint64][]byte{
14974843192121052621: CacheResponse{
Value: []byte("value 1"),
Body: []byte("value 1"),
Expiration: time.Now(),
LastAccess: time.Now(),
Frequency: 1,
Expand Down Expand Up @@ -47,7 +47,7 @@ func TestGet(t *testing.T) {
b, ok := store.Get(tt.key)
assert.Equal(t, tt.ok, ok)

got := toCacheResponse(b).Value
got := toCacheResponse(b).Body
assert.Equal(t, tt.want, got)
})
}
Expand All @@ -70,33 +70,33 @@ func TestSet(t *testing.T) {
"sets response cache",
1,
CacheResponse{
Value: []byte("value 1"),
Body: []byte("value 1"),
Expiration: time.Now().Add(1 * time.Minute),
},
},
{
"sets response cache",
2,
CacheResponse{
Value: []byte("value 2"),
Body: []byte("value 2"),
Expiration: time.Now().Add(1 * time.Minute),
},
},
{
"sets response cache",
3,
CacheResponse{
Value: []byte("value 3"),
Body: []byte("value 3"),
Expiration: time.Now().Add(1 * time.Minute),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
store.Set(tt.key, tt.response.bytes(), tt.response.Expiration)
if toCacheResponse(store.store[tt.key]).Value == nil {
if toCacheResponse(store.store[tt.key]).Body == nil {
t.Errorf(
"memory.Set() error = store[%v] response is not %s", tt.key, tt.response.Value,
"memory.Set() error = store[%v] response is not %s", tt.key, tt.response.Body,
)
}
})
Expand All @@ -111,15 +111,15 @@ func TestRelease(t *testing.T) {
map[uint64][]byte{
14974843192121052621: CacheResponse{
Expiration: time.Now().Add(1 * time.Minute),
Value: []byte("value 1"),
Body: []byte("value 1"),
}.bytes(),
14974839893586167988: CacheResponse{
Expiration: time.Now(),
Value: []byte("value 2"),
Body: []byte("value 2"),
}.bytes(),
14974840993097796199: CacheResponse{
Expiration: time.Now(),
Value: []byte("value 3"),
Body: []byte("value 3"),
}.bytes(),
},
}
Expand Down Expand Up @@ -185,19 +185,19 @@ func TestEvict(t *testing.T) {
tt.algorithm,
map[uint64][]byte{
14974843192121052621: CacheResponse{
Value: []byte("value 1"),
Body: []byte("value 1"),
Expiration: time.Now().Add(1 * time.Minute),
LastAccess: time.Now().Add(-1 * time.Minute),
Frequency: 2,
}.bytes(),
14974839893586167988: CacheResponse{
Value: []byte("value 2"),
Body: []byte("value 2"),
Expiration: time.Now().Add(1 * time.Minute),
LastAccess: time.Now().Add(-2 * time.Minute),
Frequency: 1,
}.bytes(),
14974840993097796199: CacheResponse{
Value: []byte("value 3"),
Body: []byte("value 3"),
Expiration: time.Now().Add(1 * time.Minute),
LastAccess: time.Now().Add(-3 * time.Minute),
Frequency: 3,
Expand Down
57 changes: 47 additions & 10 deletions redis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ func (suite *cacheRedisStoreTestSuite) SetupSuite() {
Store: store,
Expiration: 5 * time.Second,
IncludePaths: []string{"/test", "/empty"},
IncludePathsWithExpiration: map[string]time.Duration{
"/expired": 1 * time.Minute,
},
}))
}

Expand Down Expand Up @@ -70,10 +73,7 @@ func (suite *cacheRedisStoreTestSuite) Test_Redis_CacheStore() {
}

func (suite *cacheRedisStoreTestSuite) Test_Echo_CacheWithConfig() {
actualCalledCountForTestAPI := 0

suite.echo.GET("/test", func(c echo.Context) error {
actualCalledCountForTestAPI++
return c.String(http.StatusOK, "test")
})

Expand All @@ -85,6 +85,24 @@ func (suite *cacheRedisStoreTestSuite) Test_Echo_CacheWithConfig() {
return c.String(http.StatusOK, `{"symbolId":"","type":"","price":0.0}`)
})

suite.echo.GET("/expired", func(c echo.Context) error {
return c.String(http.StatusOK, "expired")
})

suite.Run("GET /expired - first call to store response in the cache", func() {
req := httptest.NewRequest(http.MethodGet, "/expired", nil)
rec := httptest.NewRecorder()

suite.echo.ServeHTTP(rec, req)

suite.Equal(http.StatusOK, rec.Code)
suite.Equal(`expired`, rec.Body.String())

key := generateKey(http.MethodGet, "/expired")
_, ok := suite.cacheStore.Get(key)
suite.True(ok)
})

suite.Run("GET /test - return actual response and store in the cache", func() {
req := httptest.NewRequest(http.MethodGet, "/test", nil)
rec := httptest.NewRecorder()
Expand All @@ -101,8 +119,8 @@ func (suite *cacheRedisStoreTestSuite) Test_Echo_CacheWithConfig() {
var cacheResponse CacheResponse
err := json.Unmarshal(data, &cacheResponse)
suite.NoError(err)
suite.Equal("test", string(cacheResponse.Value))
suite.Equal(1, actualCalledCountForTestAPI)
suite.Equal("test", string(cacheResponse.Body))
suite.Equal(1, cacheResponse.Frequency)
})

suite.Run("GET /test - not expired. return response from the cache", func() {
Expand All @@ -121,8 +139,8 @@ func (suite *cacheRedisStoreTestSuite) Test_Echo_CacheWithConfig() {
var cacheResponse CacheResponse
err := json.Unmarshal(data, &cacheResponse)
suite.NoError(err)
suite.Equal("test", string(cacheResponse.Value))
suite.Equal(1, actualCalledCountForTestAPI)
suite.Equal("test", string(cacheResponse.Body))
suite.Equal(2, cacheResponse.Frequency)
})

suite.Run("GET /test - expired. return actual response", func() {
Expand All @@ -143,8 +161,8 @@ func (suite *cacheRedisStoreTestSuite) Test_Echo_CacheWithConfig() {
var cacheResponse CacheResponse
err := json.Unmarshal(data, &cacheResponse)
suite.NoError(err)
suite.Equal("test", string(cacheResponse.Value))
suite.Equal(2, actualCalledCountForTestAPI)
suite.Equal("test", string(cacheResponse.Body))
suite.Equal(1, cacheResponse.Frequency)
})

suite.Run("GET /empty/string", func() {
Expand All @@ -170,8 +188,27 @@ func (suite *cacheRedisStoreTestSuite) Test_Echo_CacheWithConfig() {
suite.Equal(http.StatusOK, rec.Code)
suite.Equal(`{"symbolId":"","type":"","price":0.0}`, rec.Body.String())

key := generateKey(http.MethodGet, "/empty2")
key := generateKey(http.MethodGet, "/empty/json")
_, ok := suite.cacheStore.Get(key)
suite.False(ok)
})

suite.Run("GET /expired - second call, still get the response from the cache", func() {
req := httptest.NewRequest(http.MethodGet, "/expired", nil)
rec := httptest.NewRecorder()

suite.echo.ServeHTTP(rec, req)

suite.Equal(http.StatusOK, rec.Code)
suite.Equal(`expired`, rec.Body.String())

key := generateKey(http.MethodGet, "/expired")
data, ok := suite.cacheStore.Get(key)
suite.True(ok)

var cacheResponse CacheResponse
err := json.Unmarshal(data, &cacheResponse)
suite.NoError(err)
suite.Equal(2, cacheResponse.Frequency)
})
}

0 comments on commit 4731aac

Please sign in to comment.