Skip to content

Commit

Permalink
feat(proxy): Include a X-Cache Header that indicates whether chproxy …
Browse files Browse the repository at this point in the history
…had a cache miss or hit.

X-Cache will be set to HIT if a response came from the Cache, otherwise it will be set to MISS.

Fixes ContentSquare#288

Signed-off-by: Lennard Eijsackers <lennardeijsackers92@gmail.com>
  • Loading branch information
Blokje5 committed Mar 3, 2023
1 parent 5c1e8e7 commit a6f4f4a
Show file tree
Hide file tree
Showing 4 changed files with 34 additions and 9 deletions.
5 changes: 5 additions & 0 deletions docs/content/en/configuration/caching.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,8 @@ User Y will get the cached response from user X's query.

Since 1.20.0, the cache is specific for each user by default since it's better in terms of security.
It's possible to use the previous behavior by setting the following property of the cache in the config file `shared_with_all_users = true`

#### Detecting Cache Hits

`Chproxy` will respond with an `X-Cache` header with a value of `HIT` if it returned a response from either the local or the distributed cache. Otherwise `X-Cache` will be set to `MISS`. This can be used for example
to determine whether the ClickHouse query stats in the response can be trusted or are cached responses.
9 changes: 8 additions & 1 deletion io.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ type statResponseWriter struct {
bytesWritten prometheus.Counter
}

func RespondWithData(rw http.ResponseWriter, data io.Reader, metadata cache.ContentMetadata, ttl time.Duration, statusCode int, labels prometheus.Labels) error {
func RespondWithData(rw http.ResponseWriter, data io.Reader, metadata cache.ContentMetadata, ttl time.Duration, cacheHit bool, statusCode int, labels prometheus.Labels) error {
h := rw.Header()
if len(metadata.Type) > 0 {
h.Set("Content-Type", metadata.Type)
Expand All @@ -59,6 +59,13 @@ func RespondWithData(rw http.ResponseWriter, data io.Reader, metadata cache.Cont
expireSeconds := uint(ttl / time.Second)
h.Set("Cache-Control", fmt.Sprintf("max-age=%d", expireSeconds))
}

if cacheHit {
h.Set("X-Cache", "HIT")
} else {
h.Set("X-Cache", "MISS")
}

rw.WriteHeader(statusCode)

if _, err := io.Copy(rw, data); err != nil {
Expand Down
19 changes: 16 additions & 3 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ func TestServe(t *testing.T) {
t.Fatalf("unexpected status code: %d; expected: %d", resp.StatusCode, http.StatusOK)
}
checkResponse(t, resp.Body, expectedOkResp)
checkHeader(t, resp, "X-Cache", "MISS")

// check cached response
credHash, _ := calcCredentialHash("default", "qwerty")
Expand All @@ -120,11 +121,12 @@ func TestServe(t *testing.T) {
if err != nil {
t.Fatalf("unexpected error while getting response from cache: %s", err)
}
err = RespondWithData(rw, cachedData.Data, cachedData.ContentMetadata, cachedData.Ttl, 200, labels)
err = RespondWithData(rw, cachedData.Data, cachedData.ContentMetadata, cachedData.Ttl, true, 200, labels)
if err != nil {
t.Fatalf("unexpected error while getting response from cache: %s", err)
}
checkResponse(t, rw.Body, expectedOkResp)
checkHeader(t, rw.Result(), "X-Cache", "HIT")
},
startTLS,
},
Expand Down Expand Up @@ -198,7 +200,7 @@ func TestServe(t *testing.T) {
t.Fatalf("unexpected error while getting response from cache: %s", err)
}

err = RespondWithData(rw, cachedData.Data, cachedData.ContentMetadata, cachedData.Ttl, 200, labels)
err = RespondWithData(rw, cachedData.Data, cachedData.ContentMetadata, cachedData.Ttl, true, 200, labels)
if err != nil {
t.Fatalf("unexpected error while getting response from cache: %s", err)
}
Expand Down Expand Up @@ -250,7 +252,7 @@ func TestServe(t *testing.T) {
t.Fatalf("unexpected error while writing reposnse from cache: %s", err)
}

err = RespondWithData(rw, cachedData.Data, cachedData.ContentMetadata, cachedData.Ttl, 200, labels)
err = RespondWithData(rw, cachedData.Data, cachedData.ContentMetadata, cachedData.Ttl, true, 200, labels)
if err != nil {
t.Fatalf("unexpected error while getting response from cache: %s", err)
}
Expand Down Expand Up @@ -1183,6 +1185,17 @@ func httpGet(t *testing.T, url string, statusCode int) *http.Response {
return resp
}

func checkHeader(t *testing.T, resp *http.Response, header string, expected string) {
t.Helper()

h := resp.Header
v := h.Get(header)

if v != expected {
t.Fatalf("for header: %s got: %s, expected %s", header, v, expected)
}
}

func httpRequest(t *testing.T, request *http.Request, statusCode int) (*http.Response, error) {
t.Helper()
client := http.Client{}
Expand Down
10 changes: 5 additions & 5 deletions proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ func (rp *reverseProxy) serveFromCache(s *scope, srw *statResponseWriter, req *h
cacheHit.With(labels).Inc()
cachedResponseDuration.With(labels).Observe(time.Since(startTime).Seconds())
log.Debugf("%s: cache hit", s)
_ = RespondWithData(srw, cachedData.Data, cachedData.ContentMetadata, cachedData.Ttl, http.StatusOK, labels)
_ = RespondWithData(srw, cachedData.Data, cachedData.ContentMetadata, cachedData.Ttl, true, http.StatusOK, labels)
return
}
// Await for potential result from concurrent query
Expand All @@ -370,7 +370,7 @@ func (rp *reverseProxy) serveFromCache(s *scope, srw *statResponseWriter, req *h
cachedData, err := userCache.Get(key)
if err == nil {
defer cachedData.Data.Close()
_ = RespondWithData(srw, cachedData.Data, cachedData.ContentMetadata, cachedData.Ttl, http.StatusOK, labels)
_ = RespondWithData(srw, cachedData.Data, cachedData.ContentMetadata, cachedData.Ttl, true, http.StatusOK, labels)
cacheHitFromConcurrentQueries.With(labels).Inc()
log.Debugf("%s: cache hit after awaiting concurrent query", s)
return
Expand Down Expand Up @@ -444,7 +444,7 @@ func (rp *reverseProxy) serveFromCache(s *scope, srw *statResponseWriter, req *h
return
}

err = RespondWithData(srw, reader, contentMetadata, 0*time.Second, statusCode, labels)
err = RespondWithData(srw, reader, contentMetadata, 0*time.Second, false, statusCode, labels)
if err != nil {
err = fmt.Errorf("%s: %w; query: %q", s, err, q)
respondWith(srw, err, http.StatusInternalServerError)
Expand All @@ -457,7 +457,7 @@ func (rp *reverseProxy) serveFromCache(s *scope, srw *statResponseWriter, req *h

rp.completeTransaction(s, statusCode, userCache, key, q, "")

err = RespondWithData(srw, reader, contentMetadata, 0*time.Second, tmpFileRespWriter.StatusCode(), labels)
err = RespondWithData(srw, reader, contentMetadata, 0*time.Second, false, tmpFileRespWriter.StatusCode(), labels)
if err != nil {
err = fmt.Errorf("%s: %w; query: %q", s, err, q)
respondWith(srw, err, http.StatusInternalServerError)
Expand All @@ -481,7 +481,7 @@ func (rp *reverseProxy) serveFromCache(s *scope, srw *statResponseWriter, req *h
respondWith(srw, err, http.StatusInternalServerError)
return
}
err = RespondWithData(srw, reader, contentMetadata, expiration, statusCode, labels)
err = RespondWithData(srw, reader, contentMetadata, expiration, false, statusCode, labels)
if err != nil {
err = fmt.Errorf("%s: %w; query: %q", s, err, q)
respondWith(srw, err, http.StatusInternalServerError)
Expand Down

0 comments on commit a6f4f4a

Please sign in to comment.