github.com/yankunsam/loki/v2@v2.6.3-0.20220817130409-389df5235c27/pkg/querier/queryrange/querysharding_test.go (about) 1 package queryrange 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "math" 8 "sort" 9 "sync" 10 "testing" 11 "time" 12 13 "github.com/go-kit/log" 14 "github.com/stretchr/testify/require" 15 "github.com/weaveworks/common/user" 16 17 "github.com/grafana/loki/pkg/loghttp" 18 "github.com/grafana/loki/pkg/logproto" 19 "github.com/grafana/loki/pkg/logql" 20 "github.com/grafana/loki/pkg/querier/queryrange/queryrangebase" 21 "github.com/grafana/loki/pkg/storage/config" 22 "github.com/grafana/loki/pkg/util" 23 ) 24 25 var ( 26 nilShardingMetrics = logql.NewShardMapperMetrics(nil) 27 defaultReq = func() *LokiRequest { 28 return &LokiRequest{ 29 Limit: 100, 30 StartTs: start, 31 EndTs: end, 32 Direction: logproto.BACKWARD, 33 Path: "/loki/api/v1/query_range", 34 } 35 } 36 lokiResps = []queryrangebase.Response{ 37 &LokiResponse{ 38 Status: loghttp.QueryStatusSuccess, 39 Direction: logproto.BACKWARD, 40 Limit: defaultReq().Limit, 41 Version: 1, 42 Data: LokiData{ 43 ResultType: loghttp.ResultTypeStream, 44 Result: []logproto.Stream{ 45 { 46 Labels: `{foo="bar", level="debug"}`, 47 Entries: []logproto.Entry{ 48 {Timestamp: time.Unix(0, 6), Line: "6"}, 49 {Timestamp: time.Unix(0, 5), Line: "5"}, 50 }, 51 }, 52 }, 53 }, 54 }, 55 &LokiResponse{ 56 Status: loghttp.QueryStatusSuccess, 57 Direction: logproto.BACKWARD, 58 Limit: 100, 59 Version: 1, 60 Data: LokiData{ 61 ResultType: loghttp.ResultTypeStream, 62 Result: []logproto.Stream{ 63 { 64 Labels: `{foo="bar", level="error"}`, 65 Entries: []logproto.Entry{ 66 {Timestamp: time.Unix(0, 2), Line: "2"}, 67 {Timestamp: time.Unix(0, 1), Line: "1"}, 68 }, 69 }, 70 }, 71 }, 72 }, 73 } 74 ) 75 76 func Test_shardSplitter(t *testing.T) { 77 req := defaultReq().WithStartEnd( 78 util.TimeToMillis(start), 79 util.TimeToMillis(end), 80 ) 81 82 for _, tc := range []struct { 83 desc string 84 lookback time.Duration 85 shouldShard bool 86 }{ 87 { 88 desc: "older than lookback", 89 lookback: -time.Minute, // a negative lookback will ensure the entire query doesn't cross the sharding boundary & can safely be sharded. 90 shouldShard: true, 91 }, 92 { 93 desc: "overlaps lookback", 94 lookback: end.Sub(start) / 2, // intersect the request causing it to avoid sharding 95 shouldShard: false, 96 }, 97 { 98 desc: "newer than lookback", 99 lookback: end.Sub(start) + 1, // the entire query is in the ingester range and should avoid sharding. 100 shouldShard: false, 101 }, 102 { 103 desc: "default", 104 lookback: 0, 105 shouldShard: true, 106 }, 107 } { 108 t.Run(tc.desc, func(t *testing.T) { 109 var didShard bool 110 splitter := &shardSplitter{ 111 shardingware: queryrangebase.HandlerFunc(func(ctx context.Context, req queryrangebase.Request) (queryrangebase.Response, error) { 112 didShard = true 113 return mockHandler(lokiResps[0], nil).Do(ctx, req) 114 }), 115 next: mockHandler(lokiResps[1], nil), 116 now: func() time.Time { return end }, 117 limits: fakeLimits{ 118 minShardingLookback: tc.lookback, 119 maxQueryParallelism: 1, 120 }, 121 } 122 123 resp, err := splitter.Do(user.InjectOrgID(context.Background(), "1"), req) 124 require.Nil(t, err) 125 126 require.Equal(t, tc.shouldShard, didShard) 127 require.Nil(t, err) 128 129 if tc.shouldShard { 130 require.Equal(t, lokiResps[0], resp) 131 } else { 132 require.Equal(t, lokiResps[1], resp) 133 } 134 }) 135 } 136 } 137 138 func Test_astMapper(t *testing.T) { 139 var lock sync.Mutex 140 called := 0 141 142 handler := queryrangebase.HandlerFunc(func(ctx context.Context, req queryrangebase.Request) (queryrangebase.Response, error) { 143 lock.Lock() 144 defer lock.Unlock() 145 resp := lokiResps[called] 146 called++ 147 return resp, nil 148 }) 149 150 mware := newASTMapperware( 151 ShardingConfigs{ 152 config.PeriodConfig{ 153 RowShards: 2, 154 }, 155 }, 156 handler, 157 log.NewNopLogger(), 158 nilShardingMetrics, 159 fakeLimits{maxSeries: math.MaxInt32, maxQueryParallelism: 1}, 160 ) 161 162 resp, err := mware.Do(user.InjectOrgID(context.Background(), "1"), defaultReq().WithQuery(`{food="bar"}`)) 163 require.Nil(t, err) 164 165 expected, err := LokiCodec.MergeResponse(lokiResps...) 166 sort.Sort(logproto.Streams(expected.(*LokiResponse).Data.Result)) 167 require.Nil(t, err) 168 require.Equal(t, called, 2) 169 require.Equal(t, expected.(*LokiResponse).Data, resp.(*LokiResponse).Data) 170 } 171 172 func Test_ShardingByPass(t *testing.T) { 173 called := 0 174 handler := queryrangebase.HandlerFunc(func(ctx context.Context, req queryrangebase.Request) (queryrangebase.Response, error) { 175 called++ 176 return nil, nil 177 }) 178 179 mware := newASTMapperware( 180 ShardingConfigs{ 181 config.PeriodConfig{ 182 RowShards: 2, 183 }, 184 }, 185 handler, 186 log.NewNopLogger(), 187 nilShardingMetrics, 188 fakeLimits{maxSeries: math.MaxInt32, maxQueryParallelism: 1}, 189 ) 190 191 _, err := mware.Do(user.InjectOrgID(context.Background(), "1"), defaultReq().WithQuery(`1+1`)) 192 require.Nil(t, err) 193 require.Equal(t, called, 1) 194 } 195 196 func Test_hasShards(t *testing.T) { 197 for i, tc := range []struct { 198 input ShardingConfigs 199 expected bool 200 }{ 201 { 202 input: ShardingConfigs{ 203 {}, 204 }, 205 expected: false, 206 }, 207 { 208 input: ShardingConfigs{ 209 {RowShards: 16}, 210 }, 211 expected: true, 212 }, 213 { 214 input: ShardingConfigs{ 215 {}, 216 {RowShards: 16}, 217 {}, 218 }, 219 expected: true, 220 }, 221 { 222 input: nil, 223 expected: false, 224 }, 225 } { 226 t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { 227 require.Equal(t, tc.expected, hasShards(tc.input)) 228 }) 229 } 230 } 231 232 // astmapper successful stream & prom conversion 233 234 func mockHandler(resp queryrangebase.Response, err error) queryrangebase.Handler { 235 return queryrangebase.HandlerFunc(func(ctx context.Context, req queryrangebase.Request) (queryrangebase.Response, error) { 236 if expired := ctx.Err(); expired != nil { 237 return nil, expired 238 } 239 240 return resp, err 241 }) 242 } 243 244 func Test_InstantSharding(t *testing.T) { 245 ctx := user.InjectOrgID(context.Background(), "1") 246 247 var lock sync.Mutex 248 called := 0 249 shards := []string{} 250 251 sharding := NewQueryShardMiddleware(log.NewNopLogger(), ShardingConfigs{ 252 config.PeriodConfig{ 253 RowShards: 3, 254 }, 255 }, queryrangebase.NewInstrumentMiddlewareMetrics(nil), 256 nilShardingMetrics, 257 fakeLimits{ 258 maxSeries: math.MaxInt32, 259 maxQueryParallelism: 10, 260 }) 261 response, err := sharding.Wrap(queryrangebase.HandlerFunc(func(c context.Context, r queryrangebase.Request) (queryrangebase.Response, error) { 262 lock.Lock() 263 defer lock.Unlock() 264 called++ 265 shards = append(shards, r.(*LokiInstantRequest).Shards...) 266 return &LokiPromResponse{Response: &queryrangebase.PrometheusResponse{ 267 Data: queryrangebase.PrometheusData{ 268 ResultType: loghttp.ResultTypeVector, 269 Result: []queryrangebase.SampleStream{ 270 { 271 Labels: []logproto.LabelAdapter{{Name: "foo", Value: "bar"}}, 272 Samples: []logproto.LegacySample{{Value: 10, TimestampMs: 10}}, 273 }, 274 }, 275 }, 276 }}, nil 277 })).Do(ctx, &LokiInstantRequest{ 278 Query: `rate({app="foo"}[1m])`, 279 TimeTs: util.TimeFromMillis(10), 280 Path: "/v1/query", 281 }) 282 require.NoError(t, err) 283 require.Equal(t, 3, called, "expected 3 calls but got {}", called) 284 require.Len(t, response.(*LokiPromResponse).Response.Data.Result, 3) 285 require.ElementsMatch(t, []string{"0_of_3", "1_of_3", "2_of_3"}, shards) 286 require.Equal(t, queryrangebase.PrometheusData{ 287 ResultType: loghttp.ResultTypeVector, 288 Result: []queryrangebase.SampleStream{ 289 { 290 Labels: []logproto.LabelAdapter{{Name: "foo", Value: "bar"}}, 291 Samples: []logproto.LegacySample{{Value: 10, TimestampMs: 10}}, 292 }, 293 { 294 Labels: []logproto.LabelAdapter{{Name: "foo", Value: "bar"}}, 295 Samples: []logproto.LegacySample{{Value: 10, TimestampMs: 10}}, 296 }, 297 { 298 Labels: []logproto.LabelAdapter{{Name: "foo", Value: "bar"}}, 299 Samples: []logproto.LegacySample{{Value: 10, TimestampMs: 10}}, 300 }, 301 }, 302 }, response.(*LokiPromResponse).Response.Data) 303 require.Equal(t, loghttp.QueryStatusSuccess, response.(*LokiPromResponse).Response.Status) 304 } 305 306 func Test_SeriesShardingHandler(t *testing.T) { 307 sharding := NewSeriesQueryShardMiddleware(log.NewNopLogger(), ShardingConfigs{ 308 config.PeriodConfig{ 309 RowShards: 3, 310 }, 311 }, 312 queryrangebase.NewInstrumentMiddlewareMetrics(nil), 313 nilShardingMetrics, 314 fakeLimits{ 315 maxQueryParallelism: 10, 316 }, 317 LokiCodec, 318 ) 319 ctx := user.InjectOrgID(context.Background(), "1") 320 321 response, err := sharding.Wrap(queryrangebase.HandlerFunc(func(c context.Context, r queryrangebase.Request) (queryrangebase.Response, error) { 322 req, ok := r.(*LokiSeriesRequest) 323 if !ok { 324 return nil, errors.New("not a series call") 325 } 326 return &LokiSeriesResponse{ 327 Status: "success", 328 Version: 1, 329 Data: []logproto.SeriesIdentifier{ 330 { 331 Labels: map[string]string{ 332 "foo": "bar", 333 }, 334 }, 335 { 336 Labels: map[string]string{ 337 "shard": req.Shards[0], 338 }, 339 }, 340 }, 341 }, nil 342 })).Do(ctx, &LokiSeriesRequest{ 343 Match: []string{"foo", "bar"}, 344 StartTs: time.Unix(0, 1), 345 EndTs: time.Unix(0, 10), 346 Path: "foo", 347 }) 348 349 expected := &LokiSeriesResponse{ 350 Status: "success", 351 Version: 1, 352 Data: []logproto.SeriesIdentifier{ 353 { 354 Labels: map[string]string{ 355 "foo": "bar", 356 }, 357 }, 358 { 359 Labels: map[string]string{ 360 "shard": "0_of_3", 361 }, 362 }, 363 { 364 Labels: map[string]string{ 365 "shard": "1_of_3", 366 }, 367 }, 368 { 369 Labels: map[string]string{ 370 "shard": "2_of_3", 371 }, 372 }, 373 }, 374 } 375 sort.Slice(expected.Data, func(i, j int) bool { 376 return expected.Data[i].Labels["shard"] > expected.Data[j].Labels["shard"] 377 }) 378 actual := response.(*LokiSeriesResponse) 379 sort.Slice(actual.Data, func(i, j int) bool { 380 return actual.Data[i].Labels["shard"] > actual.Data[j].Labels["shard"] 381 }) 382 require.NoError(t, err) 383 require.Equal(t, expected, actual) 384 }