Skip to content

Commit

Permalink
add redis backend and provide examples (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
sillygod authored May 25, 2020
1 parent 149af08 commit c2ae254
Show file tree
Hide file tree
Showing 10 changed files with 234 additions and 45 deletions.
16 changes: 16 additions & 0 deletions .github/workflows/remove-old-artifacts.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: Remove old artifacts

on:
schedule:
- cron: '0 1 * * *'

jobs:
remove-old-artifacts:
runs-on: ubuntu-latest
timeout-minutes: 10

steps:
- name: Remove old artifacts
uses: c-hive/gha-remove-artifacts@v1
with:
age: '1 month'
1 change: 1 addition & 0 deletions backends/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ func (f *FileBackend) Flush() error {
return f.file.Sync()
}

// Clean performs the purge storage
func (f *FileBackend) Clean() error {
f.subscription.WaitAll()
return os.Remove(f.file.Name())
Expand Down
4 changes: 4 additions & 0 deletions backends/memory.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,22 +87,26 @@ func (i *InMemoryBackend) Write(p []byte) (n int, err error) {
return i.content.Write(p)
}

// Flush do nothing here
func (i *InMemoryBackend) Flush() error {
return nil
}

// Clean performs the purge storage
func (i *InMemoryBackend) Clean() error {
// NOTE: there is no way to del or update the cache in groupcache
// Therefore, I use the cache invalidation instead.
return nil
}

// Close writeh the temp buffer's content to the groupcache
func (i *InMemoryBackend) Close() error {
i.Ctx = context.WithValue(i.Ctx, getterCtxKey, i.content.Bytes())
err := groupch.Get(i.Ctx, i.Key, groupcache.AllocatingByteSliceSink(&i.cachedBytes))
return err
}

// GetReader return a reader for the write public response
func (i *InMemoryBackend) GetReader() (io.ReadCloser, error) {
if len(i.cachedBytes) == 0 {
err := groupch.Get(i.Ctx, i.Key, groupcache.AllocatingByteSliceSink(&i.cachedBytes))
Expand Down
109 changes: 109 additions & 0 deletions backends/redis.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,113 @@
package backends

import (
"bytes"
"context"
"io"
"io/ioutil"
"strconv"
"strings"
"time"

"github.com/go-redis/redis"
)

var (
client *redis.Client
)

// RedisBackend saves the content into redis
type RedisBackend struct {
Ctx context.Context
Key string
content bytes.Buffer
expiration time.Time
}

func ParRedisConfig(connSetting string) (*redis.Options, error) {
var err error
addr, password, db := "localhost:6379", "", 0

args := strings.Split(connSetting, " ")
length := len(args)
// the format of args: addr db password

addr = args[0]

if length > 1 {
db, err = strconv.Atoi(args[1])
if err != nil {
return nil, err
}
}

if length > 2 {
password = args[2]
}

return &redis.Options{
Addr: addr,
DB: db,
Password: password,
}, nil
}

// InitRedisClient init the client for the redis
func InitRedisClient(addr, password string, db int) error {
l.Lock()
defer l.Unlock()

client = redis.NewClient(&redis.Options{
Addr: addr,
Password: password,
DB: db,
})

if _, err := client.Ping().Result(); err != nil {
return err
}

return nil
}

// NewRedisBackend new a redis backend for cache's storage
func NewRedisBackend(ctx context.Context, key string, expiration time.Time) (Backend, error) {
return &RedisBackend{
Ctx: ctx,
Key: key,
expiration: expiration,
}, nil
}

// Write writes the response content in a temp buffer
func (r *RedisBackend) Write(p []byte) (n int, err error) {
return r.content.Write(p)
}

// Flush do nothing here
func (r *RedisBackend) Flush() error {
return nil
}

// Close writeh the temp buffer's content to the groupcache
func (r *RedisBackend) Close() error {
_, err := client.Set(r.Key, r.content.Bytes(), r.expiration.Sub(time.Now())).Result()
return err
}

// Clean performs the purge storage
func (r *RedisBackend) Clean() error {
_, err := client.Del(r.Key).Result()
return err
}

// GetReader return a reader for the write public response
func (r *RedisBackend) GetReader() (io.ReadCloser, error) {
content, err := client.Get(r.Key).Result()
if err != nil {
return nil, err
}

rc := ioutil.NopCloser(strings.NewReader(content))
return rc, nil
}
3 changes: 3 additions & 0 deletions cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ func (e *Entry) Clean() error {
}

func (e *Entry) writePublicResponse(w http.ResponseWriter) error {
// TODO: Maybe we can redesign here to get a better performance
reader, err := e.Response.GetReader()

if err != nil {
Expand Down Expand Up @@ -335,6 +336,8 @@ func (e *Entry) setBackend(ctx context.Context, config *Config) error {
backend, err = backends.NewFileBackend(config.Path)
case inMemory:
backend, err = backends.NewInMemoryBackend(ctx, e.key, e.expiration)
case redis:
backend, err = backends.NewRedisBackend(ctx, e.key, e.expiration)
}

e.Response.SetBody(backend)
Expand Down
90 changes: 52 additions & 38 deletions caddyfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"strconv"
"strings"
"time"

"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
Expand Down Expand Up @@ -32,28 +33,33 @@ const (
)

var (
defaultStatusHeader = "X-Cache-Status"
defaultLockTimeout = time.Duration(5) * time.Minute
defaultMaxAge = time.Duration(5) * time.Minute
defaultPath = ""
defaultCacheType = file
defaultcacheBucketsNum = 256
defaultCacheMaxMemorySize = GB // default is 1 GB
defaultCacheKeyTemplate = "{http.request.method} {http.request.host}{http.request.uri.path}?{http.request.uri.query}"
defaultStatusHeader = "X-Cache-Status"
defaultLockTimeout = time.Duration(5) * time.Minute
defaultMaxAge = time.Duration(5) * time.Minute
defaultPath = ""
defaultCacheType = file
defaultcacheBucketsNum = 256
defaultCacheMaxMemorySize = GB // default is 1 GB
defaultRedisConnectionSetting = "localhost:6379 0"
defaultCacheKeyTemplate = "{http.request.method} {http.request.host}{http.request.uri.path}?{http.request.uri.query}"
// the key is refereced from github.com/caddyserver/caddy/v2/modules/caddyhttp.addHTTPVarsToReplacer
)

const (
keyStatusHeader = "status_header"
keyLockTimeout = "lock_timeout"
keyDefaultMaxAge = "default_max_age"
keyPath = "path"
keyMatchHeader = "match_header"
keyMatchPath = "match_path"
keyCacheKey = "cache_key"
keyCacheBucketsNum = "cache_bucket_num"
keyCacheMaxMemorySzie = "cache_max_memory_size"
keyCacheType = "cache_type"
keyStatusHeader = "status_header"
keyLockTimeout = "lock_timeout"
keyDefaultMaxAge = "default_max_age"
keyPath = "path"
keyMatchHeader = "match_header"
keyMatchPath = "match_path"
keyCacheKey = "cache_key"
keyCacheBucketsNum = "cache_bucket_num"
keyCacheMaxMemorySzie = "cache_max_memory_size"
keyCacheType = "cache_type"
keyRedisConnctionSetting = "redis_connection_setting"
// format: addr db password or addr db or addr
// ex.
// localhost:6789 0 => connect without password. only index and host:port provided
)

func init() {
Expand All @@ -62,30 +68,32 @@ func init() {

// Config is the configuration for cache process
type Config struct {
Type CacheType `json:"type,omitempty"`
StatusHeader string `json:"status_header,omitempty"`
DefaultMaxAge time.Duration `json:"default_max_age,omitempty"`
LockTimeout time.Duration `json:"lock_timeout,omitempty"`
RuleMatchersRaws []RuleMatcherRawWithType `json:"rule_matcher_raws,omitempty"`
RuleMatchers []RuleMatcher `json:"-"`
CacheBucketsNum int `json:"cache_buckets_num,omitempty"`
CacheMaxMemorySize int `json:"cache_max_memory_size,omitempty"`
Path string `json:"path,omitempty"`
CacheKeyTemplate string `json:"cache_key_template,omitempty"`
Type CacheType `json:"type,omitempty"`
StatusHeader string `json:"status_header,omitempty"`
DefaultMaxAge time.Duration `json:"default_max_age,omitempty"`
LockTimeout time.Duration `json:"lock_timeout,omitempty"`
RuleMatchersRaws []RuleMatcherRawWithType `json:"rule_matcher_raws,omitempty"`
RuleMatchers []RuleMatcher `json:"-"`
CacheBucketsNum int `json:"cache_buckets_num,omitempty"`
CacheMaxMemorySize int `json:"cache_max_memory_size,omitempty"`
Path string `json:"path,omitempty"`
CacheKeyTemplate string `json:"cache_key_template,omitempty"`
RedisConnectionSetting string `json:"redis_connection_setting,omitempty"`
}

func getDefaultConfig() *Config {
return &Config{
StatusHeader: defaultStatusHeader,
DefaultMaxAge: defaultMaxAge,
LockTimeout: defaultLockTimeout,
RuleMatchersRaws: []RuleMatcherRawWithType{},
RuleMatchers: []RuleMatcher{},
CacheBucketsNum: defaultcacheBucketsNum,
CacheMaxMemorySize: defaultCacheMaxMemorySize,
Path: defaultPath,
Type: defaultCacheType,
CacheKeyTemplate: defaultCacheKeyTemplate,
StatusHeader: defaultStatusHeader,
DefaultMaxAge: defaultMaxAge,
LockTimeout: defaultLockTimeout,
RuleMatchersRaws: []RuleMatcherRawWithType{},
RuleMatchers: []RuleMatcher{},
CacheBucketsNum: defaultcacheBucketsNum,
CacheMaxMemorySize: defaultCacheMaxMemorySize,
Path: defaultPath,
Type: defaultCacheType,
CacheKeyTemplate: defaultCacheKeyTemplate,
RedisConnectionSetting: defaultRedisConnectionSetting,
}
}

Expand Down Expand Up @@ -128,6 +136,12 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
}
config.StatusHeader = args[0]

case keyRedisConnctionSetting:
if len(args) > 3 {
return d.Err("Invalid usage of redis_connection_setting in cache config.")
}
config.RedisConnectionSetting = strings.Join(args, " ")

case keyCacheType:
if len(args) != 1 {
return d.Err("Invalid usage of cache_type in cache config.")
Expand Down
27 changes: 27 additions & 0 deletions example/redis/Caddyfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
order http_cache before reverse_proxy
}

:9991 {
reverse_proxy {
to localhost:9995
header_up Host {http.reverse_proxy.upstream.host}
}

http_cache {
cache_type redis
redis_connection_setting localhost:6379
match_path /
}
}


:9995 {
header Cache-control "public"
root * /tmp/caddy-benchmark
file_server

log {
level info
}
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.14

require (
github.com/caddyserver/caddy/v2 v2.0.0
github.com/go-redis/redis v6.15.8+incompatible
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e
github.com/pomerium/autocache v0.0.0-20200505053831-8c1cd659f055
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,8 @@ github.com/go-lintpack/lintpack v0.5.2/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTD
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
github.com/go-redis/redis v6.15.8+incompatible h1:BKZuG6mCnRj5AOaWJXoCgf6rqTYnYJLe4en2hxT7r9o=
github.com/go-redis/redis v6.15.8+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
Expand Down
26 changes: 19 additions & 7 deletions handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,18 +165,31 @@ func (h *Handler) Provision(ctx caddy.Context) error {

}

// NOTE: make cache global and implement a function to get it. Therefore, we can
// call its Del to purge the cache
// NOTE: A dirty work to assign the config and cache to global vars
// There will be the corresponding functions to get each of them.
// Therefore, we can call its Del to purge the cache via the admin interface
cache = NewHTTPCache(h.Config)
h.Cache = cache
h.URLLocks = NewURLLock(h.Config)

err := backends.InitGroupCacheRes(h.Config.CacheMaxMemorySize)
if err != nil {
return err
// Some type of the backends need extra initialization.
switch h.Config.Type {
case inMemory:
if err := backends.InitGroupCacheRes(h.Config.CacheMaxMemorySize); err != nil {
return err
}

case redis:
opts, err := backends.ParRedisConfig(h.Config.RedisConnectionSetting)
if err != nil {
return err
}

if err := backends.InitRedisClient(opts.Addr, opts.Password, opts.DB); err != nil {
return err
}
}

// NOTE: a dirty work to assign the config to global
config = h.Config

return nil
Expand All @@ -189,7 +202,6 @@ func (h *Handler) Validate() error {
}

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {

// add a log here to record the elapsed time (from receiving the request to send the response)
start := time.Now()
upstreamDuration := time.Duration(0)
Expand Down

0 comments on commit c2ae254

Please sign in to comment.