Skip to content

Commit

Permalink
add admin endpoint to purge cache (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
sillygod authored May 20, 2020
1 parent 9e22d63 commit f4f2983
Show file tree
Hide file tree
Showing 4 changed files with 173 additions and 13 deletions.
26 changes: 15 additions & 11 deletions cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import (
var (
entries []map[string][]*Entry
entriesLock []*sync.RWMutex
l sync.Mutex
l sync.RWMutex
)

// RuleMatcherType specifies the type of matching rule to cache.
Expand Down Expand Up @@ -410,25 +410,28 @@ func (h *HTTPCache) Get(key string, request *http.Request) (*Entry, bool) {
}

// Del purge the key immediately
func (h *HTTPCache) Del(key string, request *http.Request) bool {
func (h *HTTPCache) Del(key string, request *http.Request) error {
b := h.getBucketIndexForKey(key)
h.entriesLock[b].Lock()
defer h.entriesLock[b].Unlock()

h.entriesLock[b].RLock()
previousEntries, exists := h.entries[b][key]
h.entriesLock[b].RUnlock()

if !exists {
return true
return nil
}

// the schedule will clean the entry automatically
for _, entry := range previousEntries {
if entry.IsFresh() {
h.cleanEntry(entry)
err := h.cleanEntry(entry)
if err != nil {
caddy.Log().Named("http.handlers.http_cache").Error(fmt.Sprintf("clean entry error: %s", err.Error()))
return err
}
}
}

return true
return nil
}

// Put adds the entry in the cache
Expand All @@ -452,7 +455,7 @@ func (h *HTTPCache) Put(request *http.Request, entry *Entry) {
h.entries[bucket][key] = append(h.entries[bucket][key], entry)
}

func (h *HTTPCache) cleanEntry(entry *Entry) {
func (h *HTTPCache) cleanEntry(entry *Entry) error {
key := entry.Key()
bucket := h.getBucketIndexForKey(key)

Expand All @@ -462,10 +465,11 @@ func (h *HTTPCache) cleanEntry(entry *Entry) {
for i, otherEntry := range h.entries[bucket][key] {
if entry == otherEntry {
h.entries[bucket][key] = append(h.entries[bucket][key][:i], h.entries[bucket][key][i+1:]...)
entry.Clean()
return
return entry.Clean()
}
}

return nil
}

func (h *HTTPCache) scheduleCleanEntry(entry *Entry) {
Expand Down
18 changes: 17 additions & 1 deletion handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,22 @@ var (
"path_prefix",
"mitm",
}

cache *HTTPCache
config *Config
)

func init() {
caddy.RegisterModule(Handler{})
}

// getHandlerCache is a singleton of HTTPCache
func getHandlerCache() *HTTPCache {
l.RLock()
defer l.RUnlock()
return cache
}

// Handler is a http handler as a middleware to cache the response
type Handler struct {
Config *Config `json:"config,omitempty"`
Expand Down Expand Up @@ -155,14 +165,20 @@ func (h *Handler) Provision(ctx caddy.Context) error {

}

h.Cache = NewHTTPCache(h.Config)
// NOTE: make cache global and implement a function to get it. Therefore, we can
// call its Del to purge the cache
cache = NewHTTPCache(h.Config)
h.Cache = cache
h.URLLocks = NewURLLock(h.Config)

err := backends.InitGroupCacheRes(h.Config.CacheMaxMemorySize)
if err != nil {
return err
}

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

return nil
}

Expand Down
140 changes: 140 additions & 0 deletions purge.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package httpcache

import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"sync"

"github.com/caddyserver/caddy/v2"
)

var (
bufPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}

purgeRepl *caddy.Replacer
)

func init() {

purgeRepl = caddy.NewReplacer()
caddy.RegisterModule(cachePurge{})
}

// cachePurge is a module that provides the /purge endpoint as the admin api.
type cachePurge struct{}

// PurgePayload holds the field which will be unmarshaled from the request's body
// NOTE: the format of URI can contains the query param.
// ex. when the client send a delete reqeust with the body
// {
// "method": "GET",
// "hots": "example.com",
// "uri": "/static?ext=txt",
// }
//
type PurgePayload struct {
Method string `json:"method"`
Host string `json:"host"`
URI string `json:"uri"`
path string
query string
}

func (p *PurgePayload) parseURI() {
tokens := strings.Split(p.URI, "?")
if len(tokens) > 1 {
p.query = tokens[1]
}
p.path = tokens[0]
}

func (p *PurgePayload) pruneHost() {

if strings.HasPrefix(p.Host, "http") {
p.Host = strings.Split(p.Host, ":")[1]
}

if !strings.HasSuffix(p.Host, "/") {
p.Host = p.Host + "/"
}
}

func (p *PurgePayload) transform() {
p.parseURI()
p.pruneHost()
}

// CaddyModule returns the Caddy module
func (cachePurge) CaddyModule() caddy.ModuleInfo {

return caddy.ModuleInfo{
ID: "admin.api.purge",
New: func() caddy.Module { return new(cachePurge) },
}
}

// Routes return a route for the /purge endpoint
func (c cachePurge) Routes() []caddy.AdminRoute {
return []caddy.AdminRoute{
{
Pattern: "/purge",
Handler: caddy.AdminHandlerFunc(c.handlePurge),
},
}
}

// handlePurge purges the cache matched the provided conditions
func (cachePurge) handlePurge(w http.ResponseWriter, r *http.Request) error {
if r.Method != http.MethodDelete {
return caddy.APIError{
Code: http.StatusMethodNotAllowed,
Err: fmt.Errorf("method not allowed"),
}
}

buf := bufPool.Get().(*bytes.Buffer)
buf.Reset()
defer bufPool.Put(buf)

_, err := io.Copy(buf, r.Body)
if err != nil {
return caddy.APIError{
Code: http.StatusBadRequest,
Err: fmt.Errorf("reading request body: %s", err.Error()),
}
}
// pass the body's content to the Del
body := buf.Bytes()
payload := &PurgePayload{}

err = json.Unmarshal(body, &payload)
if err != nil {
return err
}

payload.transform()

purgeRepl.Set("http.request.method", payload.Method)
purgeRepl.Set("http.request.host", payload.Host)
purgeRepl.Set("http.request.uri.query", payload.query)
purgeRepl.Set("http.request.uri.path", payload.path)

cache := getHandlerCache()
// find a way to produce the correspond key
// example key should be like "GET localhost/static/js/chunk-element.js?"
key := purgeRepl.ReplaceKnown(config.CacheKeyTemplate, "")
err = cache.Del(key, r)
if err != nil {
return err
}

return nil
}
2 changes: 1 addition & 1 deletion readme.org
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@
- [x] in memory cache (consider to use [[https://github.com/golang/groupcache][groupcache]])
- [X] chache control expiration issue default will add 24 hours if no rule applied (in the cacheobject library)
- [X] research about usagepool in the caddy2 (use global variable instead)
- [ ] purge cache entries
- [ ] custom log format (currently only add zap logger to print info)
Idealy, We can implement a custom log module.
- [ ] more optimization..
- [ ] purge cache entries

0 comments on commit f4f2983

Please sign in to comment.