Skip to content

Commit

Permalink
feat: 增加中间件缓存
Browse files Browse the repository at this point in the history
  • Loading branch information
thinkgos committed Aug 29, 2024
1 parent 0b4c417 commit a433b27
Show file tree
Hide file tree
Showing 19 changed files with 1,099 additions and 8 deletions.
8 changes: 5 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
go-version: ["1.21.x"]
go-version: ["1.21.x", "1.22.x"]
os: [ubuntu-latest, windows-latest, macos-latest]

steps:
Expand Down Expand Up @@ -48,19 +48,21 @@ jobs:
uses: actions/cache@v4
with:
path: |
${{ steps.vars.outputs.go_cache }}
${{ steps.vars.outputs.GO_CACHE }}
~/go/pkg/mod
key: ${{ runner.os }}-${{ matrix.go-version }}-go-ci-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-${{ matrix.go-version }}-go-ci
${{ runner.os }}-${{ matrix.go-version }}-go-ci-
- name: Unit test
run: |
go test -v -race -coverprofile=coverage -covermode=atomic ./...
- name: Upload coverage to Codecov
if: matrix.os == 'ubuntu-latest'
uses: codecov/codecov-action@v4
with:
files: ./coverage
flags: unittests
token: ${{ secrets.CODECOV_TOKEN }}
verbose: true
8 changes: 4 additions & 4 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ name: CodeQL
on:
push:
paths-ignore:
- '**.md'
- "**.md"
pull_request:
paths-ignore:
- '**.md'
- "**.md"
schedule:
- cron: '0 5 * * 6'
- cron: "0 5 * * 6"

jobs:
analyze:
Expand Down Expand Up @@ -48,4 +48,4 @@ jobs:
# make release

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@v3
2 changes: 1 addition & 1 deletion .github/workflows/pr_review_dog.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.21.x"
go-version: "1.22.x"

- name: Check out code into the Go module directory
uses: actions/checkout@v4
Expand Down
209 changes: 209 additions & 0 deletions cache/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
package cache

import (
"bytes"
"crypto/sha1"
"net/url"
"time"

"github.com/gin-gonic/gin"
"golang.org/x/sync/singleflight"

"github.com/things-go/gin-contrib/cache/persist"
)

// PageCachePrefix default page cache key prefix
var PageCachePrefix = "gincache.page.cache:"

// Logger logger interface
type Logger interface {
Errorf(format string, args ...any)
}

// Encoding interface
type Encoding interface {
Marshal(v any) ([]byte, error)
Unmarshal(data []byte, v any) error
}

// Config for cache
type Config struct {
// store the cache backend to store response
store persist.Store
// expire the cache expiration time
expire time.Duration
// rand duration for expire
rand func() time.Duration
// generate key for store, bool means need cache or not
generateKey func(c *gin.Context) (string, bool)
// group single flight group
group *singleflight.Group
// logger debug
logger Logger
// encoding default: JSONEncoding
encode Encoding
}

// Option custom option
type Option func(c *Config)

// WithGenerateKey custom generate key ,default is GenerateRequestURIKey.
func WithGenerateKey(f func(c *gin.Context) (string, bool)) Option {
return func(c *Config) {
if f != nil {
c.generateKey = f
}
}
}

// WithSingleflight custom single flight group, default is private single flight group.
func WithSingleflight(group *singleflight.Group) Option {
return func(c *Config) {
if group != nil {
c.group = group
}
}
}

// WithRandDuration custom rand duration for expire, default return zero
// expiration time always expire + rand()
func WithRandDuration(rand func() time.Duration) Option {
return func(c *Config) {
if rand != nil {
c.rand = rand
}
}
}

// WithLogger custom logger, default is Discard.
func WithLogger(l Logger) Option {
return func(c *Config) {
if l != nil {
c.logger = l
}
}
}

// WithEncoding custom Encoding, default is JSONEncoding.
func WithEncoding(encode Encoding) Option {
return func(c *Config) {
if encode != nil {
c.encode = encode
}
}
}

// Cache user must pass store and store expiration time to cache and with custom option.
// default caching response with uri, which use PageCachePrefix
func Cache(store persist.Store, expire time.Duration, opts ...Option) gin.HandlerFunc {
cfg := Config{
store: store,
expire: expire,
rand: func() time.Duration { return 0 },
generateKey: GenerateRequestUri,
group: new(singleflight.Group),
logger: NewDiscard(),
encode: JSONEncoding{},
}
for _, opt := range opts {
opt(&cfg)
}

return func(c *gin.Context) {
key, needCache := cfg.generateKey(c)
if !needCache {
c.Next()
return
}

// read cache first
bodyCache := poolGet()
defer poolPut(bodyCache)
bodyCache.encoding = cfg.encode

if err := cfg.store.Get(key, bodyCache); err != nil {
// BodyWriter in order to dup the response
bodyWriter := &BodyWriter{ResponseWriter: c.Writer}
c.Writer = bodyWriter

inFlight := false
// use single flight to avoid Hotspot Invalid
bc, _, shared := cfg.group.Do(key, func() (any, error) {
c.Next()
inFlight = true
bc := getBodyCacheFromBodyWriter(bodyWriter, cfg.encode)
if !c.IsAborted() && bodyWriter.Status() < 300 && bodyWriter.Status() >= 200 {
if err = cfg.store.Set(key, bc, cfg.expire+cfg.rand()); err != nil {
cfg.logger.Errorf("set cache key error: %s, cache key: %s", err, key)

Check warning on line 137 in cache/cache.go

View check run for this annotation

Codecov / codecov/patch

cache/cache.go#L137

Added line #L137 was not covered by tests
}
}
return bc, nil
})
if !inFlight && shared {
c.Abort()
responseWithBodyCache(c, bc.(*BodyCache))
}
} else {
c.Abort()
responseWithBodyCache(c, bodyCache)
}
}
}

// GenerateKeyWithPrefix generate key with GenerateKeyWithPrefix and u,
// if key is larger than 200,it will use sha1.Sum
// key like: prefix+u or prefix+sha1(u)
func GenerateKeyWithPrefix(prefix, key string) string {
if len(key) > 200 {
d := sha1.Sum([]byte(key))
return prefix + string(d[:])
}
return prefix + key
}

// GenerateRequestUri generate key with PageCachePrefix and request uri
func GenerateRequestUri(c *gin.Context) (string, bool) {
return GenerateKeyWithPrefix(PageCachePrefix, url.QueryEscape(c.Request.RequestURI)), true
}

// GenerateRequestPath generate key with PageCachePrefix and request Path
func GenerateRequestPath(c *gin.Context) (string, bool) {
return GenerateKeyWithPrefix(PageCachePrefix, url.QueryEscape(c.Request.URL.Path)), true
}

// BodyWriter dup response writer body
type BodyWriter struct {
gin.ResponseWriter
dupBody bytes.Buffer
}

// Write writes the data to the connection as part of an HTTP reply.
func (w *BodyWriter) Write(b []byte) (int, error) {
w.dupBody.Write(b)
return w.ResponseWriter.Write(b)
}

// WriteString the string into the response body.
func (w *BodyWriter) WriteString(s string) (int, error) {
w.dupBody.WriteString(s)
return w.ResponseWriter.WriteString(s)
}

func getBodyCacheFromBodyWriter(writer *BodyWriter, encode Encoding) *BodyCache {
return &BodyCache{
writer.Status(),
writer.Header().Clone(),
writer.dupBody.Bytes(),
encode,
}
}

func responseWithBodyCache(c *gin.Context, bodyCache *BodyCache) {
c.Writer.WriteHeader(bodyCache.Status)
for k, v := range bodyCache.Header {
for _, vv := range v {
c.Writer.Header().Add(k, vv)
}
}
c.Writer.Write(bodyCache.Data) // nolint: errcheck
}
Loading

0 comments on commit a433b27

Please sign in to comment.