// Package rediscache is a piece of middleware you can use to drastically // improve the performance of any HTTP handler that returns mostly static // content. // // Usage is pretty straightforward: // // redisAddr = "localhost:6379" // rp := &redis.Pool{ // MaxIdle: 80, // MaxActive: 12000, // Dial: func() (redis.Conn, error) { // c, err := redis.Dial("tcp", redisAddr) // // if strings.HasPrefix(redisAddr, "unix:") { // c, err = redis.Dial("unix", strings.TrimPrefix(redisAddr, "unix:")) // } // // if err != nil { // log.Println("Redis dial error: ", err) // } // // return c, err // }, // } // // handler := rediscache.HandleWIthParams(rp, "some-key", someHandler) // // This handler will continue to serve up the cached output until then // // key is cleared in redis package rediscache import ( "bytes" "log" "net/http" "os" "github.com/gomodule/redigo/redis" "github.com/vmihailenco/msgpack" ) // Logger is the default logger used for this package, feel free to // override. var Logger = log.New(os.Stderr, "REDIS: ", log.LstdFlags) // redisHTTPResponseWriter is essentially a fake http.ResponseWriter that // is going to let us suck out information and chuck it into redis // implements the interface as defined in net/http. type redisHTTPResponseWriter struct { Headers http.Header StatusCode int Data []byte buf *bytes.Buffer } // Simply for satisfying the http.ResponseWriter interface. func (rw *redisHTTPResponseWriter) Header() http.Header { if rw.Headers == nil { rw.Headers = http.Header{} } return rw.Headers } // Writes to the internal buffer. func (rw *redisHTTPResponseWriter) Write(msg []byte) (int, error) { if rw.buf == nil { rw.buf = &bytes.Buffer{} } return rw.buf.Write(msg) } // Simply for satisfying the http.ResponseWriter interface. func (rw *redisHTTPResponseWriter) WriteHeader(code int) { rw.StatusCode = code } // WriteData takes the internal buffer and writes out the entire // contents to the 'Data' field for storage in Redis. func (rw *redisHTTPResponseWriter) WriteData() { rw.Data = rw.buf.Bytes() } // HandleWithParams is the same as Handle but caches for the GET params // rather than discarding them. func HandleWithParams(pool *redis.Pool, key string, next http.Handler) http.Handler { return handle(pool, key, true, next) } // Handle is a Simple function that will cache the response for given handler in // redis and instead of responding with the result from the handler it will // simply dump the contents of the redis key if it exists. func Handle(pool *redis.Pool, key string, next http.Handler) http.Handler { return handle(pool, key, false, next) } func handle(pool *redis.Pool, key string, params bool, next http.Handler) http.Handler { //nolint:funlen return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { client := pool.Get() defer client.Close() subkey := r.URL.Path if params { subkey = r.URL.Path + "?" + r.URL.RawQuery } content: data, err := client.Do("HGET", key, subkey) if err != nil { // Assume something bad has happened with redis, we're // just going to log this and then pass through the // request as normal. Logger.Println("ERROR: ", err) next.ServeHTTP(w, r) return } else if data == nil { rw := &redisHTTPResponseWriter{} next.ServeHTTP(rw, r) rw.WriteData() b, err := msgpack.Marshal(rw) if err != nil { Logger.Println("ERROR: marshaling: ", err) return } _, err = client.Do("HSET", key, subkey, b) if err != nil { Logger.Println("ERROR: during set: ", err) return } // We got the content, let's go back around again and dump // it out from redis goto content } rw := &redisHTTPResponseWriter{} err = msgpack.Unmarshal(data.([]byte), rw) if err != nil { Logger.Println("ERROR: unmarshaling: ", err) return } if rw.Headers != nil { for k, v := range rw.Headers { w.Header()[k] = v } } if rw.StatusCode != 0 { w.WriteHeader(rw.StatusCode) } _, _ = w.Write(rw.Data) }) }