Skip to content

Commit 8af6675

Browse files
GiedriusSpracucci
authored andcommitted
frontend: implement cache control (#1974)
* querier: do not cache results if requested Add an extra `Headers` field to the `PrometheusResponse` message which contains the headers and their values that came from Prometheus. Use them in other places to deduce if the response should be cached. If `Cache-Control` is equal to `no-store` then it is *not* cached. This will be used by the Thanos project to indicate when a partial response has been returned. In such cases the result should not be cached so that invalid data would not be stored there. Signed-off-by: Giedrius Statkevičius <[email protected]> * queryrange: factor out cache checking + add tests Signed-off-by: Giedrius Statkevičius <[email protected]> * querier: gofmt Signed-off-by: Giedrius Statkevičius <[email protected]> * querier: tests: add missing member Signed-off-by: Giedrius Statkevičius <[email protected]> * querier: fix logical mistake Signed-off-by: Giedrius Statkevičius <[email protected]> * queryrange: fix generated code Signed-off-by: Giedrius Statkevičius <[email protected]> * querier: test adjustments Signed-off-by: Giedrius Statkevičius <[email protected]> * querier: test adjustments Signed-off-by: Giedrius Statkevičius <[email protected]> * querier: results_cache: cache by default Signed-off-by: Giedrius Statkevičius <[email protected]> * queryrange: cache: check all header values `Cache-Control` might contain more than one value so check all of them. Signed-off-by: Giedrius Statkevičius <[email protected]> * Update according to Goutham's comments Signed-off-by: Giedrius Statkevičius <[email protected]> * CHANGELOG: add full stop, PR's number Signed-off-by: Giedrius Statkevičius <[email protected]>
1 parent 5365b84 commit 8af6675

File tree

7 files changed

+523
-59
lines changed

7 files changed

+523
-59
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
## master / unreleased
44

5+
* [CHANGE] The frontend component now does not cache results if it finds a `Cache-Control` header and if one of its values is `no-store`. #1974
56
* [ENHANCEMENT] metric `cortex_ingester_flush_reasons` gets a new `reason` value: `Spread`, when `-ingester.spread-flushes` option is enabled.
6-
77
* [CHANGE] Flags changed with transition to upstream Prometheus rules manager:
88
* `ruler.client-timeout` is now `ruler.configs.client-timeout` in order to match `ruler.configs.url`
99
* `ruler.group-timeout`has been removed

pkg/querier/queryrange/query_range.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ var (
3333

3434
// PrometheusCodec is a codec to encode and decode Prometheus query range requests and responses.
3535
PrometheusCodec Codec = &prometheusCodec{}
36+
37+
// Name of the cache control header.
38+
cachecontrolHeader = "Cache-Control"
3639
)
3740

3841
// Codec is used to encode/decode query range requests and responses so they can be passed down to middlewares.
@@ -221,6 +224,10 @@ func (prometheusCodec) DecodeResponse(ctx context.Context, r *http.Response, _ R
221224
if err := json.Unmarshal(buf, &resp); err != nil {
222225
return nil, httpgrpc.Errorf(http.StatusInternalServerError, "error decoding response: %v", err)
223226
}
227+
228+
for h, hv := range r.Header {
229+
resp.Headers = append(resp.Headers, &PrometheusResponseHeader{Name: h, Values: hv})
230+
}
224231
return &resp, nil
225232
}
226233

pkg/querier/queryrange/query_range_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,15 @@ func TestRequest(t *testing.T) {
7474
}
7575

7676
func TestResponse(t *testing.T) {
77+
r := *parsedResponse
78+
r.Headers = respHeaders
7779
for i, tc := range []struct {
7880
body string
7981
expected *PrometheusResponse
8082
}{
8183
{
8284
body: responseBody,
83-
expected: parsedResponse,
85+
expected: &r,
8486
},
8587
} {
8688
t.Run(strconv.Itoa(i), func(t *testing.T) {

pkg/querier/queryrange/queryrange.pb.go

Lines changed: 414 additions & 57 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/querier/queryrange/queryrange.proto

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,17 @@ message PrometheusRequest {
2121
string query = 6;
2222
}
2323

24+
message PrometheusResponseHeader {
25+
string Name = 1 [(gogoproto.jsontag) = "-"];
26+
repeated string Values = 2 [(gogoproto.jsontag) = "-"];
27+
}
28+
2429
message PrometheusResponse {
2530
string Status = 1 [(gogoproto.jsontag) = "status"];
2631
PrometheusData Data = 2 [(gogoproto.nullable) = false, (gogoproto.jsontag) = "data,omitempty"];
2732
string ErrorType = 3 [(gogoproto.jsontag) = "errorType,omitempty"];
2833
string Error = 4 [(gogoproto.jsontag) = "error,omitempty"];
34+
repeated PrometheusResponseHeader Headers = 5 [(gogoproto.jsontag) = "-"];
2935
}
3036

3137
message PrometheusData {

pkg/querier/queryrange/results_cache.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ import (
2222
"github.com/cortexproject/cortex/pkg/util/spanlogger"
2323
)
2424

25+
var (
26+
// Value that cachecontrolHeader has if the response indicates that the results should not be cached.
27+
noCacheValue = "no-store"
28+
)
29+
2530
// ResultsCacheConfig is the config for the results cache.
2631
type ResultsCacheConfig struct {
2732
CacheConfig cache.Config `yaml:"cache"`
@@ -59,6 +64,7 @@ var PrometheusResponseExtractor = ExtractorFunc(func(start, end int64, from Resp
5964
ResultType: promRes.Data.ResultType,
6065
Result: extractMatrix(start, end, promRes.Data.Result),
6166
},
67+
Headers: promRes.Headers,
6268
}
6369
})
6470

@@ -133,12 +139,41 @@ func (s resultsCache) Do(ctx context.Context, r Request) (Response, error) {
133139
return response, err
134140
}
135141

142+
// shouldCacheResponse says whether the response should be cached or not.
143+
func shouldCacheResponse(r Response) bool {
144+
if promResp, ok := r.(*PrometheusResponse); ok {
145+
shouldCache := true
146+
outer:
147+
for _, hv := range promResp.Headers {
148+
if hv == nil {
149+
continue
150+
}
151+
if hv.Name != cachecontrolHeader {
152+
continue
153+
}
154+
for _, v := range hv.Values {
155+
if v == noCacheValue {
156+
shouldCache = false
157+
break outer
158+
}
159+
}
160+
}
161+
return shouldCache
162+
}
163+
return true
164+
}
165+
136166
func (s resultsCache) handleMiss(ctx context.Context, r Request) (Response, []Extent, error) {
137167
response, err := s.next.Do(ctx, r)
138168
if err != nil {
139169
return nil, nil, err
140170
}
141171

172+
if !shouldCacheResponse(response) {
173+
level.Debug(s.logger).Log("msg", fmt.Sprintf("%s header in response is equal to %s, not caching the response", cachecontrolHeader, noCacheValue))
174+
return response, []Extent{}, nil
175+
}
176+
142177
extent, err := toExtent(ctx, r, response)
143178
if err != nil {
144179
return nil, nil, err

pkg/querier/queryrange/results_cache_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ var (
3131
Step: 120 * 1e3,
3232
Query: "sum(container_memory_rss) by (namespace)",
3333
}
34+
respHeaders = []*PrometheusResponseHeader{
35+
{
36+
Name: "Content-Type",
37+
Values: []string{"application/json"},
38+
},
39+
}
3440
parsedResponse = &PrometheusResponse{
3541
Status: "success",
3642
Data: PrometheusData{
@@ -108,6 +114,57 @@ func mkExtent(start, end int64) Extent {
108114
}
109115
}
110116

117+
func TestShouldCache(t *testing.T) {
118+
for i, tc := range []struct {
119+
input Response
120+
expected bool
121+
}{
122+
// Does not contain the needed header.
123+
{
124+
input: Response(&PrometheusResponse{
125+
Headers: []*PrometheusResponseHeader{
126+
{
127+
Name: "meaninglessheader",
128+
Values: []string{},
129+
},
130+
},
131+
}),
132+
expected: true,
133+
},
134+
// Does contain the header which has the value.
135+
{
136+
input: Response(&PrometheusResponse{
137+
Headers: []*PrometheusResponseHeader{
138+
{
139+
Name: cachecontrolHeader,
140+
Values: []string{noCacheValue},
141+
},
142+
},
143+
}),
144+
expected: false,
145+
},
146+
// Header contains extra values but still good.
147+
{
148+
input: Response(&PrometheusResponse{
149+
Headers: []*PrometheusResponseHeader{
150+
{
151+
Name: cachecontrolHeader,
152+
Values: []string{"foo", noCacheValue},
153+
},
154+
},
155+
}),
156+
expected: false,
157+
},
158+
} {
159+
{
160+
t.Run(strconv.Itoa(i), func(t *testing.T) {
161+
ret := shouldCacheResponse(tc.input)
162+
require.Equal(t, tc.expected, ret)
163+
})
164+
}
165+
}
166+
}
167+
111168
func TestPartiton(t *testing.T) {
112169
for i, tc := range []struct {
113170
input Request

0 commit comments

Comments
 (0)