Browse Source
This migrates a feature that was previously reserved for enterprise users, according to https://github.com/caddyserver/caddy/issues/2786. The cache HTTP handler will be a high-performing, distributed cache layer for HTTP requests. Right now, the implementation is a very basic proof-of-concept, and further development is required.master
Matthew Holt
5 years ago
2 changed files with 206 additions and 0 deletions
@ -0,0 +1,205 @@ |
|||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
|||
//
|
|||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
// you may not use this file except in compliance with the License.
|
|||
// You may obtain a copy of the License at
|
|||
//
|
|||
// http://www.apache.org/licenses/LICENSE-2.0
|
|||
//
|
|||
// Unless required by applicable law or agreed to in writing, software
|
|||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
// See the License for the specific language governing permissions and
|
|||
// limitations under the License.
|
|||
|
|||
package httpcache |
|||
|
|||
import ( |
|||
"bytes" |
|||
"encoding/gob" |
|||
"fmt" |
|||
"io" |
|||
"log" |
|||
"net/http" |
|||
"sync" |
|||
|
|||
"github.com/caddyserver/caddy/v2" |
|||
"github.com/caddyserver/caddy/v2/modules/caddyhttp" |
|||
"github.com/golang/groupcache" |
|||
) |
|||
|
|||
func init() { |
|||
caddy.RegisterModule(Cache{}) |
|||
} |
|||
|
|||
// Cache implements a simple distributed cache.
|
|||
type Cache struct { |
|||
group *groupcache.Group |
|||
} |
|||
|
|||
// CaddyModule returns the Caddy module information.
|
|||
func (Cache) CaddyModule() caddy.ModuleInfo { |
|||
return caddy.ModuleInfo{ |
|||
Name: "http.handlers.cache", |
|||
New: func() caddy.Module { return new(Cache) }, |
|||
} |
|||
} |
|||
|
|||
// Provision provisions c.
|
|||
func (c *Cache) Provision(ctx caddy.Context) error { |
|||
// TODO: proper pool configuration
|
|||
me := "http://localhost:5555" |
|||
// TODO: Make max size configurable
|
|||
maxSize := int64(512 << 20) |
|||
poolMu.Lock() |
|||
if pool == nil { |
|||
pool = groupcache.NewHTTPPool(me) |
|||
c.group = groupcache.NewGroup(groupName, maxSize, groupcache.GetterFunc(c.getter)) |
|||
} else { |
|||
c.group = groupcache.GetGroup(groupName) |
|||
} |
|||
pool.Set(me) |
|||
poolMu.Unlock() |
|||
|
|||
return nil |
|||
} |
|||
|
|||
// Validate validates c.
|
|||
func (c *Cache) Validate() error { |
|||
// TODO: implement
|
|||
return nil |
|||
} |
|||
|
|||
func (c *Cache) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { |
|||
// TODO: proper RFC implementation of cache control headers...
|
|||
if r.Header.Get("Cache-Control") == "no-cache" || r.Method != "GET" { |
|||
return next.ServeHTTP(w, r) |
|||
} |
|||
|
|||
ctx := getterContext{w, r, next} |
|||
|
|||
// TODO: rigorous performance testing
|
|||
|
|||
// TODO: pretty much everything else to handle the nuances of HTTP caching...
|
|||
|
|||
// TODO: groupcache has no explicit cache eviction, so we need to embed
|
|||
// all information related to expiring cache entries into the key; right
|
|||
// now we just use the request URI as a proof-of-concept
|
|||
key := r.RequestURI |
|||
|
|||
var cachedBytes []byte |
|||
err := c.group.Get(ctx, key, groupcache.AllocatingByteSliceSink(&cachedBytes)) |
|||
if err == errUncacheable { |
|||
return nil |
|||
} |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
// the cached bytes consists of two parts: first a
|
|||
// gob encoding of the status and header, immediately
|
|||
// followed by the raw bytes of the response body
|
|||
rdr := bytes.NewReader(cachedBytes) |
|||
|
|||
// read the header and status first
|
|||
var hs headerAndStatus |
|||
err = gob.NewDecoder(rdr).Decode(&hs) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
// set and write the cached headers
|
|||
for k, v := range hs.Header { |
|||
w.Header()[k] = v |
|||
} |
|||
w.WriteHeader(hs.Status) |
|||
|
|||
// write the cached response body
|
|||
io.Copy(w, rdr) |
|||
|
|||
return nil |
|||
} |
|||
|
|||
func (c *Cache) getter(ctx groupcache.Context, key string, dest groupcache.Sink) error { |
|||
combo := ctx.(getterContext) |
|||
|
|||
// the buffer will store the gob-encoded header, then the body
|
|||
buf := bufPool.Get().(*bytes.Buffer) |
|||
buf.Reset() |
|||
defer bufPool.Put(buf) |
|||
|
|||
// we need to record the response if we are to cache it; only cache if
|
|||
// request is successful (TODO: there's probably much more nuance needed here)
|
|||
var rr caddyhttp.ResponseRecorder |
|||
rr = caddyhttp.NewResponseRecorder(combo.rw, buf, func(status int) bool { |
|||
shouldBuf := status < 300 |
|||
|
|||
if shouldBuf { |
|||
// store the header before the body, so we can efficiently
|
|||
// and conveniently use a single buffer for both; gob
|
|||
// decoder will only read up to end of gob message, and
|
|||
// the rest will be the body, which will be written
|
|||
// implicitly for us by the recorder
|
|||
err := gob.NewEncoder(buf).Encode(headerAndStatus{ |
|||
Header: rr.Header(), |
|||
Status: status, |
|||
}) |
|||
if err != nil { |
|||
log.Printf("[ERROR] Encoding headers for cache entry: %v; not caching this request", err) |
|||
return false |
|||
} |
|||
} |
|||
|
|||
return shouldBuf |
|||
}) |
|||
|
|||
// execute next handlers in chain
|
|||
err := combo.next.ServeHTTP(rr, combo.req) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
// if response body was not buffered, response was
|
|||
// already written and we are unable to cache
|
|||
if !rr.Buffered() { |
|||
return errUncacheable |
|||
} |
|||
|
|||
// add to cache
|
|||
dest.SetBytes(buf.Bytes()) |
|||
|
|||
return nil |
|||
} |
|||
|
|||
type headerAndStatus struct { |
|||
Header http.Header |
|||
Status int |
|||
} |
|||
|
|||
type getterContext struct { |
|||
rw http.ResponseWriter |
|||
req *http.Request |
|||
next caddyhttp.Handler |
|||
} |
|||
|
|||
var bufPool = sync.Pool{ |
|||
New: func() interface{} { |
|||
return new(bytes.Buffer) |
|||
}, |
|||
} |
|||
|
|||
var ( |
|||
pool *groupcache.HTTPPool |
|||
poolMu sync.Mutex |
|||
) |
|||
|
|||
var errUncacheable = fmt.Errorf("uncacheable") |
|||
|
|||
const groupName = "http_requests" |
|||
|
|||
// Interface guards
|
|||
var ( |
|||
_ caddy.Provisioner = (*Cache)(nil) |
|||
_ caddy.Validator = (*Cache)(nil) |
|||
_ caddyhttp.MiddlewareHandler = (*Cache)(nil) |
|||
) |
Loading…
Reference in new issue