bilibili-backup/library/net/http/blademaster/middleware/cache/cache_test.go
2019-04-22 02:59:20 +00:00

354 lines
8.5 KiB
Go

package cache
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
"go-common/library/cache/memcache"
"go-common/library/container/pool"
"go-common/library/ecode"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
"go-common/library/net/http/blademaster/middleware/cache/store"
xtime "go-common/library/time"
"github.com/stretchr/testify/assert"
)
const (
SockAddr = "127.0.0.1:18080"
McSockAddr = "172.16.33.54:11211"
)
func uri(base, path string) string {
return fmt.Sprintf("%s://%s%s", "http", base, path)
}
func init() {
log.Init(nil)
}
func newMemcache() (*Cache, func()) {
s := store.NewMemcache(&memcache.Config{
Config: &pool.Config{
Active: 10,
Idle: 2,
IdleTimeout: xtime.Duration(time.Second),
},
Name: "test",
Proto: "tcp",
Addr: McSockAddr,
DialTimeout: xtime.Duration(time.Second),
ReadTimeout: xtime.Duration(time.Second),
WriteTimeout: xtime.Duration(time.Second),
})
return New(s), func() {}
}
func newFile() (*Cache, func()) {
path, err := ioutil.TempDir("", "cache-test")
if err != nil {
panic("Failed to create cache directory")
}
s := store.NewFile(&store.FileConfig{
RootDir: path,
})
remove := func() {
os.RemoveAll(path)
}
return New(s), remove
}
func TestPage(t *testing.T) {
memcache, remove1 := newMemcache()
filestore, remove2 := newFile()
defer func() {
remove1()
remove2()
}()
t.Run("Memcache Store", pageCase(memcache, true))
t.Run("File Store", pageCase(filestore, false))
}
func TestControl(t *testing.T) {
memcache, remove1 := newMemcache()
filestore, remove2 := newFile()
defer func() {
remove1()
remove2()
}()
t.Run("Memcache Store", controlCase(memcache, true))
t.Run("File Store", controlCase(filestore, false))
}
func TestPageCacheMultiWrite(t *testing.T) {
memcache, remove1 := newMemcache()
filestore, remove2 := newFile()
defer func() {
remove1()
remove2()
}()
t.Run("Memcache Store", pageMultiWriteCase(memcache))
t.Run("File Store", pageMultiWriteCase(filestore))
}
func TestDegrade(t *testing.T) {
memcache, remove1 := newMemcache()
filestore, remove2 := newFile()
defer func() {
remove1()
remove2()
}()
t.Run("Memcache Store", degradeCase(memcache))
t.Run("File Store", degradeCase(filestore))
}
func pageCase(cache *Cache, testExpire bool) func(t *testing.T) {
return func(t *testing.T) {
expire := int32(3)
pc := NewPage(expire)
engine := bm.Default()
engine.GET("/page-cache", cache.Cache(pc, nil), func(ctx *bm.Context) {
ctx.Writer.Header().Set("X-Hello", "World")
ctx.String(203, "%s\n", time.Now().String())
})
go engine.Run(SockAddr)
defer func() {
engine.Server().Shutdown(context.Background())
}()
time.Sleep(time.Second)
code1, content1, headers1, err1 := httpGet(uri(SockAddr, "/page-cache"))
code2, content2, headers2, err2 := httpGet(uri(SockAddr, "/page-cache"))
assert.Nil(t, err1)
assert.Nil(t, err2)
assert.Equal(t, code1, 203)
assert.Equal(t, code2, 203)
assert.NotNil(t, content1)
assert.NotNil(t, content2)
assert.Equal(t, headers1["X-Hello"], []string{"World"})
assert.Equal(t, headers2["X-Hello"], []string{"World"})
assert.Equal(t, string(content1), string(content2))
if !testExpire {
return
}
// test if the last caching is expired
t.Logf("Waiting %d seconds for caching expire test", expire+1)
time.Sleep(time.Second * time.Duration(expire+1))
_, content3, _, err3 := httpGet(uri(SockAddr, "/page-cache"))
_, content4, _, err4 := httpGet(uri(SockAddr, "/page-cache"))
assert.Nil(t, err3)
assert.Nil(t, err4)
assert.NotNil(t, content1)
assert.NotNil(t, content2)
assert.NotEqual(t, string(content1), string(content3))
assert.Equal(t, string(content3), string(content4))
}
}
func pageMultiWriteCase(cache *Cache) func(t *testing.T) {
return func(t *testing.T) {
chunks := []string{
"Hello",
"World",
"Hello",
"World",
"Hello",
"World",
"Hello",
"World",
}
pc := NewPage(3)
engine := bm.Default()
engine.GET("/page-cache-write", cache.Cache(pc, nil), func(ctx *bm.Context) {
ctx.Writer.Header().Set("X-Hello", "World")
ctx.Writer.WriteHeader(203)
for _, chunk := range chunks {
ctx.Writer.Write([]byte(chunk))
}
})
go engine.Run(SockAddr)
defer func() {
engine.Server().Shutdown(context.Background())
}()
time.Sleep(time.Second)
code1, content1, headers1, err1 := httpGet(uri(SockAddr, "/page-cache-write"))
code2, content2, headers2, err2 := httpGet(uri(SockAddr, "/page-cache-write"))
assert.Nil(t, err1)
assert.Nil(t, err2)
assert.Equal(t, code1, 203)
assert.Equal(t, code2, 203)
assert.NotNil(t, content1)
assert.NotNil(t, content2)
assert.Equal(t, headers1["X-Hello"], []string{"World"})
assert.Equal(t, headers2["X-Hello"], []string{"World"})
assert.Equal(t, strings.Join(chunks, ""), string(content1))
assert.Equal(t, strings.Join(chunks, ""), string(content2))
assert.Equal(t, string(content1), string(content2))
}
}
func degradeCase(cache *Cache) func(t *testing.T) {
return func(t *testing.T) {
wg := sync.WaitGroup{}
i := int32(0)
degrade := NewDegrader(10)
engine := bm.Default()
engine.GET("/scheduled/error", cache.Cache(degrade.Args("name", "age"), nil), func(c *bm.Context) {
code := atomic.AddInt32(&i, 1)
if code == 5 {
c.JSON("succeed", nil)
return
}
if code%2 == 0 {
c.JSON("", ecode.Degrade)
return
}
c.JSON(fmt.Sprintf("Code: %d", code), ecode.Int(int(code)))
})
wg.Add(1)
go func() {
engine.Run(":18080")
wg.Done()
}()
defer func() {
engine.Server().Shutdown(context.TODO())
wg.Wait()
}()
time.Sleep(time.Second)
for index := 1; index < 10; index++ {
_, content, _, _ := httpGet(uri(SockAddr, "/scheduled/error?name=degrader&age=26"))
t.Log(index, string(content))
var res struct {
Data string `json:"data"`
}
err := json.Unmarshal(content, &res)
assert.Nil(t, err)
if index == 5 {
// ensure response is write to cache
time.Sleep(time.Second)
}
if index > 5 && index%2 == 0 {
if res.Data != "succeed" {
t.Fatalf("Failed to degrade at index: %d", index)
} else {
t.Logf("This request is degraded at index: %d", index)
}
}
}
}
}
func controlCase(cache *Cache, testExpire bool) func(t *testing.T) {
return func(t *testing.T) {
wg := sync.WaitGroup{}
i := int32(0)
expire := int32(30)
control := NewControl(expire)
filter := func(ctx *bm.Context) bool {
if ctx.Request.Form.Get("cache") == "false" {
return false
}
return true
}
engine := bm.Default()
engine.GET("/large/response", cache.Cache(control, filter), func(c *bm.Context) {
c.JSON(map[string]interface{}{
"index": atomic.AddInt32(&i, 1),
"Hello0": "World",
"Hello1": "World",
"Hello2": "World",
"Hello3": "World",
"Hello4": "World",
"Hello5": "World",
"Hello6": "World",
"Hello7": "World",
"Hello8": "World",
}, nil)
})
engine.GET("/large/response/error", cache.Cache(control, filter), func(c *bm.Context) {
c.JSON(nil, ecode.RequestErr)
})
wg.Add(1)
go func() {
engine.Run(":18080")
wg.Done()
}()
defer func() {
engine.Server().Shutdown(context.TODO())
wg.Wait()
}()
time.Sleep(time.Second)
code, content, headers, err := httpGet(uri(SockAddr, "/large/response?name=hello&age=1"))
assert.NoError(t, err)
assert.Equal(t, 200, code)
assert.NotEmpty(t, content)
assert.Equal(t, "max-age=30", headers.Get("Cache-Control"))
exp, err := http.ParseTime(headers.Get("Expires"))
assert.NoError(t, err)
assert.InDelta(t, 30, exp.Unix()-time.Now().Unix(), 5)
code, content, headers, err = httpGet(uri(SockAddr, "/large/response/error?name=hello&age=1&cache=false"))
assert.NoError(t, err)
assert.Equal(t, 200, code)
assert.NotEmpty(t, content)
assert.Empty(t, headers.Get("Expires"))
assert.Empty(t, headers.Get("Cache-Control"))
code, content, headers, err = httpGet(uri(SockAddr, "/large/response/error?name=hello&age=1"))
assert.NoError(t, err)
assert.Equal(t, 200, code)
assert.NotEmpty(t, content)
assert.Empty(t, headers.Get("Expires"))
assert.Empty(t, headers.Get("Cache-Control"))
}
}
func httpGet(url string) (code int, content []byte, headers http.Header, err error) {
resp, err := http.Get(url)
if err != nil {
return
}
defer resp.Body.Close()
if content, err = ioutil.ReadAll(resp.Body); err != nil {
return
}
code = resp.StatusCode
headers = resp.Header
return
}