Skip to content

Commit 65324e8

Browse files
authored
feat(proxy): Include a X-Cache Header that indicates whether chproxy had a cache miss or hit. (#313)
X-Cache will be set to HIT if a response came from the Cache, otherwise it will be set to MISS. Fixes #288 Signed-off-by: Lennard Eijsackers <[email protected]>
1 parent 6833740 commit 65324e8

File tree

4 files changed

+38
-9
lines changed

4 files changed

+38
-9
lines changed

docs/content/en/configuration/caching.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,9 @@ User Y will get the cached response from user X's query.
6262

6363
Since 1.20.0, the cache is specific for each user by default since it's better in terms of security.
6464
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`
65+
66+
#### Detecting Cache Hits
67+
68+
`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`.
69+
If the response couldn't be cached due to the configuration (e.g. a payload that is too large), `N/A` will be returned. This can be used for example to determine
70+
whether the ClickHouse query stats in the response can be trusted or are cached responses.

io.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,13 @@ type statResponseWriter struct {
4444
bytesWritten prometheus.Counter
4545
}
4646

47-
func RespondWithData(rw http.ResponseWriter, data io.Reader, metadata cache.ContentMetadata, ttl time.Duration, statusCode int, labels prometheus.Labels) error {
47+
const (
48+
XCacheHit = "HIT"
49+
XCacheMiss = "MISS"
50+
XCacheNA = "N/A"
51+
)
52+
53+
func RespondWithData(rw http.ResponseWriter, data io.Reader, metadata cache.ContentMetadata, ttl time.Duration, cacheHit string, statusCode int, labels prometheus.Labels) error {
4854
h := rw.Header()
4955
if len(metadata.Type) > 0 {
5056
h.Set("Content-Type", metadata.Type)
@@ -59,6 +65,9 @@ func RespondWithData(rw http.ResponseWriter, data io.Reader, metadata cache.Cont
5965
expireSeconds := uint(ttl / time.Second)
6066
h.Set("Cache-Control", fmt.Sprintf("max-age=%d", expireSeconds))
6167
}
68+
69+
h.Set("X-Cache", cacheHit)
70+
6271
rw.WriteHeader(statusCode)
6372

6473
if _, err := io.Copy(rw, data); err != nil {

main_test.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ func TestServe(t *testing.T) {
9999
t.Fatalf("unexpected status code: %d; expected: %d", resp.StatusCode, http.StatusOK)
100100
}
101101
checkResponse(t, resp.Body, expectedOkResp)
102+
checkHeader(t, resp, "X-Cache", "MISS")
102103

103104
// check cached response
104105
credHash, _ := calcCredentialHash("default", "qwerty")
@@ -120,11 +121,12 @@ func TestServe(t *testing.T) {
120121
if err != nil {
121122
t.Fatalf("unexpected error while getting response from cache: %s", err)
122123
}
123-
err = RespondWithData(rw, cachedData.Data, cachedData.ContentMetadata, cachedData.Ttl, 200, labels)
124+
err = RespondWithData(rw, cachedData.Data, cachedData.ContentMetadata, cachedData.Ttl, XCacheHit, 200, labels)
124125
if err != nil {
125126
t.Fatalf("unexpected error while getting response from cache: %s", err)
126127
}
127128
checkResponse(t, rw.Body, expectedOkResp)
129+
checkHeader(t, rw.Result(), "X-Cache", XCacheHit)
128130
},
129131
startTLS,
130132
},
@@ -145,6 +147,7 @@ func TestServe(t *testing.T) {
145147
}
146148

147149
checkResponse(t, resp.Body, expectedOkResp)
150+
checkHeader(t, resp, "X-Cache", XCacheNA)
148151

149152
key := &cache.Key{
150153
Query: []byte(q),
@@ -198,7 +201,7 @@ func TestServe(t *testing.T) {
198201
t.Fatalf("unexpected error while getting response from cache: %s", err)
199202
}
200203

201-
err = RespondWithData(rw, cachedData.Data, cachedData.ContentMetadata, cachedData.Ttl, 200, labels)
204+
err = RespondWithData(rw, cachedData.Data, cachedData.ContentMetadata, cachedData.Ttl, XCacheHit, 200, labels)
202205
if err != nil {
203206
t.Fatalf("unexpected error while getting response from cache: %s", err)
204207
}
@@ -250,7 +253,7 @@ func TestServe(t *testing.T) {
250253
t.Fatalf("unexpected error while writing reposnse from cache: %s", err)
251254
}
252255

253-
err = RespondWithData(rw, cachedData.Data, cachedData.ContentMetadata, cachedData.Ttl, 200, labels)
256+
err = RespondWithData(rw, cachedData.Data, cachedData.ContentMetadata, cachedData.Ttl, XCacheMiss, 200, labels)
254257
if err != nil {
255258
t.Fatalf("unexpected error while getting response from cache: %s", err)
256259
}
@@ -1183,6 +1186,17 @@ func httpGet(t *testing.T, url string, statusCode int) *http.Response {
11831186
return resp
11841187
}
11851188

1189+
func checkHeader(t *testing.T, resp *http.Response, header string, expected string) {
1190+
t.Helper()
1191+
1192+
h := resp.Header
1193+
v := h.Get(header)
1194+
1195+
if v != expected {
1196+
t.Fatalf("for header: %s got: %s, expected %s", header, v, expected)
1197+
}
1198+
}
1199+
11861200
func httpRequest(t *testing.T, request *http.Request, statusCode int) (*http.Response, error) {
11871201
t.Helper()
11881202
client := http.Client{}

proxy.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,7 @@ func (rp *reverseProxy) serveFromCache(s *scope, srw *statResponseWriter, req *h
357357
cacheHit.With(labels).Inc()
358358
cachedResponseDuration.With(labels).Observe(time.Since(startTime).Seconds())
359359
log.Debugf("%s: cache hit", s)
360-
_ = RespondWithData(srw, cachedData.Data, cachedData.ContentMetadata, cachedData.Ttl, http.StatusOK, labels)
360+
_ = RespondWithData(srw, cachedData.Data, cachedData.ContentMetadata, cachedData.Ttl, XCacheHit, http.StatusOK, labels)
361361
return
362362
}
363363
// Await for potential result from concurrent query
@@ -370,7 +370,7 @@ func (rp *reverseProxy) serveFromCache(s *scope, srw *statResponseWriter, req *h
370370
cachedData, err := userCache.Get(key)
371371
if err == nil {
372372
defer cachedData.Data.Close()
373-
_ = RespondWithData(srw, cachedData.Data, cachedData.ContentMetadata, cachedData.Ttl, http.StatusOK, labels)
373+
_ = RespondWithData(srw, cachedData.Data, cachedData.ContentMetadata, cachedData.Ttl, XCacheHit, http.StatusOK, labels)
374374
cacheHitFromConcurrentQueries.With(labels).Inc()
375375
log.Debugf("%s: cache hit after awaiting concurrent query", s)
376376
return
@@ -444,7 +444,7 @@ func (rp *reverseProxy) serveFromCache(s *scope, srw *statResponseWriter, req *h
444444
return
445445
}
446446

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

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

460-
err = RespondWithData(srw, reader, contentMetadata, 0*time.Second, tmpFileRespWriter.StatusCode(), labels)
460+
err = RespondWithData(srw, reader, contentMetadata, 0*time.Second, XCacheNA, tmpFileRespWriter.StatusCode(), labels)
461461
if err != nil {
462462
err = fmt.Errorf("%s: %w; query: %q", s, err, q)
463463
respondWith(srw, err, http.StatusInternalServerError)
@@ -481,7 +481,7 @@ func (rp *reverseProxy) serveFromCache(s *scope, srw *statResponseWriter, req *h
481481
respondWith(srw, err, http.StatusInternalServerError)
482482
return
483483
}
484-
err = RespondWithData(srw, reader, contentMetadata, expiration, statusCode, labels)
484+
err = RespondWithData(srw, reader, contentMetadata, expiration, XCacheMiss, statusCode, labels)
485485
if err != nil {
486486
err = fmt.Errorf("%s: %w; query: %q", s, err, q)
487487
respondWith(srw, err, http.StatusInternalServerError)

0 commit comments

Comments
 (0)