Create & Init Project...

This commit is contained in:
2019-04-22 18:49:16 +08:00
commit fc4fa37393
25440 changed files with 4054998 additions and 0 deletions

View File

@@ -0,0 +1,101 @@
load(
"@io_bazel_rules_go//proto:def.bzl",
"go_proto_library",
)
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
proto_library(
name = "cache_proto",
srcs = ["page.proto"],
tags = ["automanaged"],
deps = ["@gogo_special_proto//github.com/gogo/protobuf/gogoproto"],
)
go_proto_library(
name = "cache_go_proto",
compilers = ["@io_bazel_rules_go//proto:gogofast_proto"],
importpath = "go-common/library/net/http/blademaster/middleware/cache",
proto = ":cache_proto",
tags = ["automanaged"],
deps = ["@com_github_gogo_protobuf//gogoproto:go_default_library"],
)
go_test(
name = "go_default_test",
srcs = ["cache_test.go"],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//library/cache/memcache:go_default_library",
"//library/container/pool:go_default_library",
"//library/ecode:go_default_library",
"//library/log:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/net/http/blademaster/middleware/cache/store:go_default_library",
"//library/time:go_default_library",
"//vendor/github.com/stretchr/testify/assert:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = [
"cache.go",
"control.go",
"degrade.go",
"page.go",
],
embed = [":cache_go_proto"],
importpath = "go-common/library/net/http/blademaster/middleware/cache",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//library/ecode:go_default_library",
"//library/log:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/net/http/blademaster/middleware/cache/store:go_default_library",
"@com_github_gogo_protobuf//gogoproto:go_default_library",
"@com_github_gogo_protobuf//proto:go_default_library",
],
)
go_test(
name = "go_default_xtest",
srcs = ["example_test.go"],
tags = ["automanaged"],
deps = [
"//library/cache/memcache:go_default_library",
"//library/container/pool:go_default_library",
"//library/ecode:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/net/http/blademaster/middleware/cache:go_default_library",
"//library/net/http/blademaster/middleware/cache/store:go_default_library",
"//library/time:go_default_library",
"//vendor/github.com/pkg/errors:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//library/net/http/blademaster/middleware/cache/store:all-srcs",
],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,10 @@
### business/blademaster/cache
##### Version 1.0.1
1. 添加 Control 策略(目前仅通过 Expires 和 Cache-Control 实现客户端缓存)
##### Version 1.0.0
1. 完成基本功能与测试
2. 完成 Degrade 与 PageCache 逻辑

View File

@@ -0,0 +1,5 @@
# Author
zhoujiahui
# Reviewer
maojian

View File

@@ -0,0 +1,7 @@
# See the OWNERS docs at https://go.k8s.io/owners
approvers:
- zhoujiahui
reviewers:
- maojian
- zhoujiahui

View File

@@ -0,0 +1,13 @@
#### business/blademaster/cache
##### 项目简介
blademaster 的通用 cache 模块,一般直接用于缓存返回的 response
##### 编译环境
- **请只用 Golang v1.8.x 以上版本编译执行**
##### 依赖包
- No other dependency

View File

@@ -0,0 +1,38 @@
package cache
import (
bm "go-common/library/net/http/blademaster"
"go-common/library/net/http/blademaster/middleware/cache/store"
)
// Cache is the abstract struct for any cache impl
type Cache struct {
store store.Store
}
// Filter is used to check is cache required for every request
type Filter func(*bm.Context) bool
// Policy is used to abstract different cache policy
type Policy interface {
Key(*bm.Context) string
Handler(store.Store) bm.HandlerFunc
}
// New will create a new Cache struct
func New(store store.Store) *Cache {
c := &Cache{
store: store,
}
return c
}
// Cache is used to mark path as customized cache policy
func (c *Cache) Cache(policy Policy, filter Filter) bm.HandlerFunc {
return func(ctx *bm.Context) {
if filter != nil && !filter(ctx) {
return
}
policy.Handler(c.store)(ctx)
}
}

View File

@@ -0,0 +1,353 @@
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
}

View File

@@ -0,0 +1,74 @@
package cache
import (
fmt "fmt"
"net/http"
"sync"
"time"
bm "go-common/library/net/http/blademaster"
"go-common/library/net/http/blademaster/middleware/cache/store"
)
const (
_maxMaxAge = 60 * 5 // 5 minutes
)
// Control is used to work as client side cache orchestrator
type Control struct {
MaxAge int32
pool sync.Pool
}
type controlWriter struct {
*Control
ctx *bm.Context
response http.ResponseWriter
}
var _ http.ResponseWriter = &controlWriter{}
// NewControl will create a new control cache struct
func NewControl(maxAge int32) *Control {
if maxAge > _maxMaxAge {
panic("MaxAge should be less than 300 seconds")
}
ctl := &Control{
MaxAge: maxAge,
}
ctl.pool.New = func() interface{} {
return &controlWriter{}
}
return ctl
}
// Key method is not needed in this situation
func (ctl *Control) Key(ctx *bm.Context) string { return "" }
// Handler is used to execute cache service
func (ctl *Control) Handler(_ store.Store) bm.HandlerFunc {
return func(ctx *bm.Context) {
writer := ctl.pool.Get().(*controlWriter)
writer.Control = ctl
writer.ctx = ctx
writer.response = ctx.Writer
ctx.Writer = writer
ctx.Next()
ctl.pool.Put(writer)
}
}
func (w *controlWriter) Header() http.Header { return w.response.Header() }
func (w *controlWriter) Write(data []byte) (size int, err error) { return w.response.Write(data) }
func (w *controlWriter) WriteHeader(code int) {
// do not inject header if this is an error response
if w.ctx.Error == nil {
headers := w.Header()
headers.Set("Expires", time.Now().UTC().Add(time.Duration(w.MaxAge)*time.Second).Format(http.TimeFormat))
headers.Set("Cache-Control", fmt.Sprintf("max-age=%d", w.MaxAge))
}
w.response.WriteHeader(code)
}

View File

@@ -0,0 +1,219 @@
package cache
import (
"context"
"crypto/md5"
"fmt"
"net/http"
"strings"
"sync"
"sync/atomic"
"time"
"go-common/library/ecode"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
"go-common/library/net/http/blademaster/middleware/cache/store"
)
const (
_degradeInterval = 60 * 10
_degradePrefix = "bm.degrade"
)
var (
_degradeBytes = []byte(fmt.Sprintf("{\"code\":%d, \"message\":\"\"}", ecode.Degrade))
)
// Degrader is the common degrader instance.
type Degrader struct {
lock sync.RWMutex
urls map[string]*state
expire int32
ch chan *result
pool sync.Pool // degradeWriter pool
}
// argsDegrader means the degrade will happened by args policy
type argsDegrader struct {
*Degrader
args []string
}
type degradeWriter struct {
*Degrader
ctx *bm.Context
response http.ResponseWriter
store store.Store
key string
state *state
}
type state struct {
// FIXME(zhoujiahui): using transient map to avoid potential memory leak?
// record last cached time
sync.RWMutex
gens map[string]*int64
}
type result struct {
key string
value []byte
store store.Store
}
var _ http.ResponseWriter = &degradeWriter{}
var _ Policy = &argsDegrader{}
// NewDegrader will create a new degrade struct
func NewDegrader(expire int32) (d *Degrader) {
d = &Degrader{
urls: make(map[string]*state),
ch: make(chan *result, 1024),
expire: expire,
}
d.pool.New = func() interface{} {
return &degradeWriter{
Degrader: d,
}
}
go d.degradeproc()
return
}
func (d *Degrader) degradeproc() {
for {
r := <-d.ch
if err := r.store.Set(context.Background(), r.key, r.value, d.expire); err != nil {
log.Error("store write key(%s) error(%v)", r.key, err)
}
}
}
// Args means this path will be degrade by specified args
func (d *Degrader) Args(args ...string) Policy {
return &argsDegrader{
Degrader: d,
args: args,
}
}
func (d *Degrader) state(path string) *state {
d.lock.RLock()
s, ok := d.urls[path]
d.lock.RUnlock()
if !ok {
s = &state{
gens: make(map[string]*int64),
}
d.lock.Lock()
d.urls[path] = s
d.lock.Unlock()
}
return s
}
// Key is used to identify response cache key in most key-value store
func (ad *argsDegrader) Key(ctx *bm.Context) string {
req := ctx.Request
path := req.URL.Path
params := req.Form
vs := make([]string, 0, len(ad.args))
for _, arg := range ad.args {
vs = append(vs, params.Get(arg))
}
return fmt.Sprintf("%s:%s_%x", _degradePrefix, strings.Replace(path, "/", "_", -1), md5.Sum([]byte(strings.Join(vs, "-"))))
}
// Handler is used to execute degrade service
func (ad *argsDegrader) Handler(store store.Store) bm.HandlerFunc {
return func(ctx *bm.Context) {
req := ctx.Request
path := req.URL.Path
writer := ad.pool.Get().(*degradeWriter)
writer.response = ctx.Writer
writer.ctx = ctx
writer.store = store
writer.state = ad.state(path)
writer.key = ad.Key(ctx)
ctx.Writer = writer // replace to degrade writer
ctx.Next()
ad.pool.Put(writer)
}
}
func (w *degradeWriter) Header() http.Header { return w.response.Header() }
func (w *degradeWriter) WriteHeader(code int) { w.response.WriteHeader(code) }
func (w *degradeWriter) Write(data []byte) (size int, err error) {
e := w.ctx.Error
// if an degrade error code is raised from upstream,
// degrade this request directly
if e != nil {
if ec := ecode.Cause(e); ec.Code() == ecode.Degrade.Code() {
return w.write()
}
}
// write origin response
if size, err = w.response.Write(data); err != nil {
return
}
// error raised, this is a unsuccessful response
if e != nil {
return
}
// is required to cache
if !w.state.required(w.key) {
return
}
// async cache succeeded response for further degradation
select {
case w.ch <- &result{key: w.key, value: data, store: w.store}:
default:
}
return
}
func (w *degradeWriter) write() (int, error) {
data, err := w.store.Get(w.ctx, w.key)
if err != nil || len(data) == 0 {
// FIXME(zhoujiahui): The default response data should be respect to render type or content-type header
data = _degradeBytes
}
return w.response.Write(data)
}
// check is required to cache response
// it depends on last cache time and _degradeInterval
func (st *state) required(key string) bool {
now := time.Now().Unix()
st.RLock()
pLast, ok := st.gens[key]
st.RUnlock()
if !ok {
st.Lock()
pLast = new(int64)
st.gens[key] = pLast
st.Unlock()
}
last := atomic.LoadInt64(pLast)
if now-last < _degradeInterval {
return false
}
return atomic.CompareAndSwapInt64(pLast, last, now)
}

View File

@@ -0,0 +1,77 @@
package cache_test
import (
"time"
"go-common/library/cache/memcache"
"go-common/library/container/pool"
"go-common/library/ecode"
"go-common/library/net/http/blademaster"
"go-common/library/net/http/blademaster/middleware/cache"
"go-common/library/net/http/blademaster/middleware/cache/store"
xtime "go-common/library/time"
"github.com/pkg/errors"
)
// This example create a cache middleware instance and two cache policy,
// then attach them to the specified path.
//
// The `PageCache` policy will attempt to cache the whole response by URI.
// It usually used to cache the common response.
//
// The `Degrader` policy usually used to prevent the API totaly unavailable if any disaster is happen.
// A succeeded response will be cached per 600s.
// The cache key is generated by specified args and its values.
// You can using file or memcache as cache backend for degradation currently.
//
// The `Cache` policy is used to work with multilevel HTTP caching architecture.
// This will cause client side response caching.
// We only support weak validator with `ETag` header currently.
func Example() {
mc := store.NewMemcache(&memcache.Config{
Config: &pool.Config{
Active: 10,
Idle: 2,
IdleTimeout: xtime.Duration(time.Second),
},
Name: "test",
Proto: "tcp",
Addr: "172.16.33.54:11211",
DialTimeout: xtime.Duration(time.Second),
ReadTimeout: xtime.Duration(time.Second),
WriteTimeout: xtime.Duration(time.Second),
})
ca := cache.New(mc)
deg := cache.NewDegrader(10)
pc := cache.NewPage(10)
ctl := cache.NewControl(10)
filter := func(ctx *blademaster.Context) bool {
if ctx.Request.Form.Get("cache") == "false" {
return false
}
return true
}
engine := blademaster.Default()
engine.GET("/users/profile", ca.Cache(deg.Args("name", "age"), nil), func(c *blademaster.Context) {
values := c.Request.URL.Query()
name := values.Get("name")
age := values.Get("age")
err := errors.New("error from others") // error from other call
if err != nil {
// mark this response should be degraded
c.JSON(nil, ecode.Degrade)
return
}
c.JSON(map[string]string{"name": name, "age": age}, nil)
})
engine.GET("/users/index", ca.Cache(pc, nil), func(c *blademaster.Context) {
c.String(200, "%s", "Title: User")
})
engine.GET("/users/list", ca.Cache(ctl, filter), func(c *blademaster.Context) {
c.JSON([]string{"user1", "user2", "user3"}, nil)
})
engine.Run(":18080")
}

View File

@@ -0,0 +1,171 @@
package cache
import (
"bytes"
"crypto/sha1"
"io"
"net/http"
"net/url"
"sync"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
"go-common/library/net/http/blademaster/middleware/cache/store"
proto "github.com/gogo/protobuf/proto"
)
// consts for blademaster cache
const (
_pagePrefix = "bm.page"
)
// Page is used to cache common response
type Page struct {
Expire int32
pool sync.Pool
}
type cachedWriter struct {
ctx *bm.Context
response http.ResponseWriter
store store.Store
status int
expire int32
key string
}
var _ http.ResponseWriter = &cachedWriter{}
// NewPage will create a new page cache struct
func NewPage(expire int32) *Page {
pc := &Page{
Expire: expire,
}
pc.pool.New = func() interface{} {
return &cachedWriter{}
}
return pc
}
// Key is used to identify response cache key in most key-value store
func (p *Page) Key(ctx *bm.Context) string {
url := ctx.Request.URL
key := urlEscape(_pagePrefix, url.RequestURI())
return key
}
// Handler is used to execute cache service
func (p *Page) Handler(store store.Store) bm.HandlerFunc {
return func(ctx *bm.Context) {
var (
resp *ResponseCache
cached []byte
err error
)
key := p.Key(ctx)
cached, err = store.Get(ctx, key)
// if we did got the previous cache,
// try to unmarshal it
if err == nil && len(cached) > 0 {
resp = new(ResponseCache)
err = proto.Unmarshal(cached, resp)
}
// if we failed to fetch the cache or failed to parse cached data,
// then consider try to cache this response
if err != nil || resp == nil {
writer := p.pool.Get().(*cachedWriter)
writer.ctx = ctx
writer.response = ctx.Writer
writer.key = key
writer.expire = p.Expire
writer.store = store
ctx.Writer = writer
ctx.Next()
p.pool.Put(writer)
return
}
// write cached response
headers := ctx.Writer.Header()
for key, value := range resp.Header {
headers[key] = value.Value
}
ctx.Writer.WriteHeader(int(resp.Status))
ctx.Writer.Write(resp.Data)
ctx.Abort()
}
}
func (w *cachedWriter) Header() http.Header {
return w.response.Header()
}
func (w *cachedWriter) WriteHeader(code int) {
w.status = int(code)
w.response.WriteHeader(code)
}
func (w *cachedWriter) Write(data []byte) (size int, err error) {
var (
origin []byte
pdata []byte
)
if size, err = w.response.Write(data); err != nil {
return
}
store := w.store
origin, err = store.Get(w.ctx, w.key)
resp := new(ResponseCache)
if err == nil || len(origin) > 0 {
err1 := proto.Unmarshal(origin, resp)
if err1 == nil {
data = append(resp.Data, data...)
}
}
resp.Status = int32(w.status)
resp.Header = headerValues(w.Header())
resp.Data = data
if pdata, err = proto.Marshal(resp); err != nil {
// cannot happen
log.Error("Failed to marshal response to protobuf: %v", err)
return
}
if err = store.Set(w.ctx, w.key, pdata, w.expire); err != nil {
log.Error("Failed to set response cache: %v", err)
return
}
return
}
func headerValues(headers http.Header) map[string]*HeaderValue {
result := make(map[string]*HeaderValue, len(headers))
for key, values := range headers {
result[key] = &HeaderValue{
Value: values,
}
}
return result
}
func urlEscape(prefix string, u string) string {
key := url.QueryEscape(u)
if len(key) > 200 {
h := sha1.New()
io.WriteString(h, u)
key = string(h.Sum(nil))
}
var buffer bytes.Buffer
buffer.WriteString(prefix)
buffer.WriteString(":")
buffer.WriteString(key)
return buffer.String()
}

View File

@@ -0,0 +1,104 @@
// Code generated by protoc-gen-gogo. DO NOT EDIT.
// source: page.proto
/*
Package cache is a generated protocol buffer package.
It is generated from these files:
page.proto
It has these top-level messages:
ResponseCache
HeaderValue
*/
package cache
import proto "github.com/gogo/protobuf/proto"
import fmt "fmt"
import math "math"
import _ "github.com/gogo/protobuf/gogoproto"
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.GoGoProtoPackageIsVersion2 // please upgrade the proto package
type ResponseCache struct {
Status int32 `protobuf:"varint,1,opt,name=Status,proto3" json:"Status,omitempty"`
Header map[string]*HeaderValue `protobuf:"bytes,2,rep,name=Header" json:"Header,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value"`
Data []byte `protobuf:"bytes,3,opt,name=Data,proto3" json:"Data,omitempty"`
}
func (m *ResponseCache) Reset() { *m = ResponseCache{} }
func (m *ResponseCache) String() string { return proto.CompactTextString(m) }
func (*ResponseCache) ProtoMessage() {}
func (*ResponseCache) Descriptor() ([]byte, []int) { return fileDescriptorPage, []int{0} }
func (m *ResponseCache) GetStatus() int32 {
if m != nil {
return m.Status
}
return 0
}
func (m *ResponseCache) GetHeader() map[string]*HeaderValue {
if m != nil {
return m.Header
}
return nil
}
func (m *ResponseCache) GetData() []byte {
if m != nil {
return m.Data
}
return nil
}
type HeaderValue struct {
Value []string `protobuf:"bytes,1,rep,name=Value" json:"Value,omitempty"`
}
func (m *HeaderValue) Reset() { *m = HeaderValue{} }
func (m *HeaderValue) String() string { return proto.CompactTextString(m) }
func (*HeaderValue) ProtoMessage() {}
func (*HeaderValue) Descriptor() ([]byte, []int) { return fileDescriptorPage, []int{1} }
func (m *HeaderValue) GetValue() []string {
if m != nil {
return m.Value
}
return nil
}
func init() {
proto.RegisterType((*ResponseCache)(nil), "cache.responseCache")
proto.RegisterType((*HeaderValue)(nil), "cache.headerValue")
}
func init() { proto.RegisterFile("page.proto", fileDescriptorPage) }
var fileDescriptorPage = []byte{
// 231 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x54, 0x8f, 0x41, 0x4b, 0xc4, 0x30,
0x10, 0x85, 0x49, 0x6b, 0x0b, 0x3b, 0x55, 0x90, 0x41, 0x24, 0xec, 0x29, 0xac, 0x97, 0x5c, 0xcc,
0xc2, 0x7a, 0x59, 0xbc, 0xaa, 0xe0, 0xc5, 0x4b, 0x04, 0xef, 0x69, 0x1d, 0x5b, 0x51, 0x37, 0xa5,
0x4d, 0x84, 0xfd, 0x7f, 0xfe, 0x30, 0xe9, 0xa4, 0x87, 0xee, 0xed, 0x3d, 0xde, 0xf7, 0xe6, 0x31,
0x00, 0xbd, 0x6b, 0xc9, 0xf4, 0x83, 0x0f, 0x1e, 0x8b, 0xc6, 0x35, 0x1d, 0xad, 0x6f, 0xdb, 0xcf,
0xd0, 0xc5, 0xda, 0x34, 0xfe, 0x67, 0xdb, 0xfa, 0xd6, 0x6f, 0x39, 0xad, 0xe3, 0x07, 0x3b, 0x36,
0xac, 0x52, 0x6b, 0xf3, 0x27, 0xe0, 0x62, 0xa0, 0xb1, 0xf7, 0x87, 0x91, 0x1e, 0xa6, 0x03, 0x78,
0x0d, 0xe5, 0x6b, 0x70, 0x21, 0x8e, 0x52, 0x28, 0xa1, 0x0b, 0x3b, 0x3b, 0xdc, 0x43, 0xf9, 0x4c,
0xee, 0x9d, 0x06, 0x99, 0xa9, 0x5c, 0x57, 0x3b, 0x65, 0x78, 0xd0, 0x9c, 0xb4, 0x4d, 0x42, 0x9e,
0x0e, 0x61, 0x38, 0xda, 0x99, 0x47, 0x84, 0xb3, 0x47, 0x17, 0x9c, 0xcc, 0x95, 0xd0, 0xe7, 0x96,
0xf5, 0xfa, 0x05, 0xaa, 0x05, 0x8a, 0x97, 0x90, 0x7f, 0xd1, 0x91, 0x17, 0x57, 0x76, 0x92, 0xa8,
0xa1, 0xf8, 0x75, 0xdf, 0x91, 0x64, 0xa6, 0x84, 0xae, 0x76, 0x38, 0xaf, 0x75, 0x5c, 0x7a, 0x9b,
0x12, 0x9b, 0x80, 0xfb, 0x6c, 0x2f, 0x36, 0x37, 0x50, 0x2d, 0x12, 0xbc, 0x82, 0x82, 0x85, 0x14,
0x2a, 0xd7, 0x2b, 0x9b, 0x4c, 0x5d, 0xf2, 0xcb, 0x77, 0xff, 0x01, 0x00, 0x00, 0xff, 0xff, 0x6f,
0x05, 0xcf, 0xb0, 0x36, 0x01, 0x00, 0x00,
}

View File

@@ -0,0 +1,13 @@
syntax = "proto3";
package cache;
import "github.com/gogo/protobuf/gogoproto/gogo.proto";
message responseCache {
int32 Status = 1;
map<string, headerValue> Header = 2;
bytes Data = 3;
}
message headerValue {
repeated string Value = 1;
}

View File

@@ -0,0 +1,37 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = [
"file.go",
"memcache.go",
"store.go",
],
importpath = "go-common/library/net/http/blademaster/middleware/cache/store",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//library/cache/memcache:go_default_library",
"//library/log:go_default_library",
"//vendor/github.com/pkg/errors:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,65 @@
package store
import (
"context"
"io/ioutil"
"os"
"path"
"go-common/library/log"
"github.com/pkg/errors"
)
// FileConfig config of File.
type FileConfig struct {
RootDir string
}
// File is a degrade file service.
type File struct {
c *FileConfig
}
var _ Store = &File{}
// NewFile new a file degrade service.
func NewFile(fc *FileConfig) *File {
if fc == nil {
panic(errors.New("file config is nil"))
}
fs := &File{c: fc}
if err := os.MkdirAll(fs.c.RootDir, 0755); err != nil {
panic(errors.Wrapf(err, "dir: %s", fs.c.RootDir))
}
return fs
}
// Set save the result of location to file.
// expire is not implemented in file storage.
func (fs *File) Set(ctx context.Context, key string, bs []byte, _ int32) (err error) {
file := path.Join(fs.c.RootDir, key)
tmp := file + ".tmp"
if err = ioutil.WriteFile(tmp, bs, 0644); err != nil {
log.Error("ioutil.WriteFile(%s, bs, 0644): error(%v)", tmp, err)
err = errors.Wrapf(err, "key: %s", key)
return
}
if err = os.Rename(tmp, file); err != nil {
log.Error("os.Rename(%s, %s): error(%v)", tmp, file, err)
err = errors.Wrapf(err, "key: %s", key)
return
}
return
}
// Get get result from file by locaiton+params.
func (fs *File) Get(ctx context.Context, key string) (bs []byte, err error) {
p := path.Join(fs.c.RootDir, key)
if bs, err = ioutil.ReadFile(p); err != nil {
log.Error("ioutil.ReadFile(%s): error(%v)", p, err)
err = errors.Wrapf(err, "key: %s", key)
return
}
return
}

View File

@@ -0,0 +1,54 @@
package store
import (
"context"
"go-common/library/cache/memcache"
"go-common/library/log"
)
// Memcache represents the cache with memcached persistence
type Memcache struct {
pool *memcache.Pool
}
// NewMemcache new a memcache store.
func NewMemcache(c *memcache.Config) *Memcache {
if c == nil {
panic("cache config is nil")
}
return &Memcache{
pool: memcache.NewPool(c),
}
}
// Set save the result to memcache store.
func (ms *Memcache) Set(ctx context.Context, key string, value []byte, expire int32) (err error) {
item := &memcache.Item{
Key: key,
Value: value,
Expiration: expire,
}
conn := ms.pool.Get(ctx)
defer conn.Close()
if err = conn.Set(item); err != nil {
log.Error("conn.Set(%s) error(%v)", key, err)
}
return
}
// Get get result from mc by locaiton+params.
func (ms *Memcache) Get(ctx context.Context, key string) ([]byte, error) {
conn := ms.pool.Get(ctx)
defer conn.Close()
r, err := conn.Get(key)
if err != nil {
if err == memcache.ErrNotFound {
//ignore not found error
return nil, nil
}
log.Error("conn.Get(%s) error(%v)", key, err)
return nil, err
}
return r.Value, nil
}

View File

@@ -0,0 +1,15 @@
package store
import (
"context"
)
// Store is the interface of a cache backend
type Store interface {
// Get retrieves an item from the cache. Returns the item or nil, and a bool indicating
// whether the key was found.
Get(ctx context.Context, key string) ([]byte, error)
// Set sets an item to the cache, replacing any existing item.
Set(ctx context.Context, key string, value []byte, expire int32) error
}