github.com/yankunsam/loki/v2@v2.6.3-0.20220817130409-389df5235c27/pkg/ingester/flush_test.go (about) 1 package ingester 2 3 import ( 4 "fmt" 5 "os" 6 "sort" 7 "sync" 8 "syscall" 9 "testing" 10 "time" 11 12 gokitlog "github.com/go-kit/log" 13 "github.com/grafana/dskit/flagext" 14 "github.com/grafana/dskit/kv" 15 "github.com/grafana/dskit/ring" 16 "github.com/grafana/dskit/services" 17 "github.com/prometheus/common/model" 18 "github.com/prometheus/prometheus/model/labels" 19 "github.com/stretchr/testify/require" 20 "github.com/weaveworks/common/user" 21 "golang.org/x/net/context" 22 23 "github.com/grafana/dskit/tenant" 24 25 "github.com/grafana/loki/pkg/chunkenc" 26 "github.com/grafana/loki/pkg/ingester/client" 27 "github.com/grafana/loki/pkg/iter" 28 "github.com/grafana/loki/pkg/logproto" 29 "github.com/grafana/loki/pkg/logql" 30 "github.com/grafana/loki/pkg/logql/log" 31 "github.com/grafana/loki/pkg/runtime" 32 "github.com/grafana/loki/pkg/storage/chunk" 33 "github.com/grafana/loki/pkg/storage/chunk/fetcher" 34 "github.com/grafana/loki/pkg/storage/config" 35 "github.com/grafana/loki/pkg/storage/stores/index/stats" 36 "github.com/grafana/loki/pkg/validation" 37 ) 38 39 const ( 40 numSeries = 10 41 samplesPerSeries = 100 42 ) 43 44 func TestChunkFlushingIdle(t *testing.T) { 45 cfg := defaultIngesterTestConfig(t) 46 cfg.FlushCheckPeriod = 20 * time.Millisecond 47 cfg.MaxChunkIdle = 100 * time.Millisecond 48 cfg.RetainPeriod = 500 * time.Millisecond 49 50 store, ing := newTestStore(t, cfg, nil) 51 defer services.StopAndAwaitTerminated(context.Background(), ing) //nolint:errcheck 52 testData := pushTestSamples(t, ing) 53 54 // wait beyond idle time so samples flush 55 time.Sleep(cfg.MaxChunkIdle * 2) 56 store.checkData(t, testData) 57 } 58 59 func TestChunkFlushingShutdown(t *testing.T) { 60 store, ing := newTestStore(t, defaultIngesterTestConfig(t), nil) 61 testData := pushTestSamples(t, ing) 62 require.NoError(t, services.StopAndAwaitTerminated(context.Background(), ing)) 63 store.checkData(t, testData) 64 } 65 66 type fullWAL struct{} 67 68 func (fullWAL) Log(_ *WALRecord) error { return &os.PathError{Err: syscall.ENOSPC} } 69 func (fullWAL) Start() {} 70 func (fullWAL) Stop() error { return nil } 71 72 func Benchmark_FlushLoop(b *testing.B) { 73 var ( 74 size = 5 75 descs [][]*chunkDesc 76 lbs = makeRandomLabels() 77 ctx = user.InjectOrgID(context.Background(), "foo") 78 _, ing = newTestStore(b, defaultIngesterTestConfig(b), nil) 79 ) 80 81 for i := 0; i < size; i++ { 82 descs = append(descs, buildChunkDecs(b)) 83 } 84 85 b.ResetTimer() 86 b.ReportAllocs() 87 88 for n := 0; n < b.N; n++ { 89 var wg sync.WaitGroup 90 for i := 0; i < size; i++ { 91 wg.Add(1) 92 go func(loop int) { 93 defer wg.Done() 94 require.NoError(b, ing.flushChunks(ctx, 0, lbs, descs[loop], &sync.RWMutex{})) 95 }(i) 96 } 97 wg.Wait() 98 } 99 } 100 101 func Test_Flush(t *testing.T) { 102 var ( 103 store, ing = newTestStore(t, defaultIngesterTestConfig(t), nil) 104 lbs = makeRandomLabels() 105 ctx = user.InjectOrgID(context.Background(), "foo") 106 ) 107 store.onPut = func(ctx context.Context, chunks []chunk.Chunk) error { 108 for _, c := range chunks { 109 buf, err := c.Encoded() 110 require.Nil(t, err) 111 if err := c.Decode(chunk.NewDecodeContext(), buf); err != nil { 112 return err 113 } 114 } 115 return nil 116 } 117 require.NoError(t, ing.flushChunks(ctx, 0, lbs, buildChunkDecs(t), &sync.RWMutex{})) 118 } 119 120 func buildChunkDecs(t testing.TB) []*chunkDesc { 121 res := make([]*chunkDesc, 10) 122 for i := range res { 123 res[i] = &chunkDesc{ 124 closed: true, 125 chunk: chunkenc.NewMemChunk(chunkenc.EncSnappy, chunkenc.UnorderedHeadBlockFmt, dummyConf().BlockSize, dummyConf().TargetChunkSize), 126 } 127 fillChunk(t, res[i].chunk) 128 require.NoError(t, res[i].chunk.Close()) 129 } 130 return res 131 } 132 133 func TestWALFullFlush(t *testing.T) { 134 // technically replaced with a fake wal, but the ingester New() function creates a regular wal first, 135 // so we enable creation/cleanup even though it remains unused. 136 walDir := t.TempDir() 137 138 store, ing := newTestStore(t, defaultIngesterTestConfigWithWAL(t, walDir), fullWAL{}) 139 testData := pushTestSamples(t, ing) 140 require.NoError(t, services.StopAndAwaitTerminated(context.Background(), ing)) 141 store.checkData(t, testData) 142 } 143 144 func TestFlushingCollidingLabels(t *testing.T) { 145 cfg := defaultIngesterTestConfig(t) 146 cfg.FlushCheckPeriod = 20 * time.Millisecond 147 cfg.MaxChunkIdle = 100 * time.Millisecond 148 cfg.RetainPeriod = 500 * time.Millisecond 149 150 store, ing := newTestStore(t, cfg, nil) 151 defer store.Stop() 152 153 const userID = "testUser" 154 ctx := user.InjectOrgID(context.Background(), userID) 155 156 // checkData only iterates between unix seconds 0 and 1000 157 now := time.Unix(0, 0) 158 159 req := &logproto.PushRequest{Streams: []logproto.Stream{ 160 // some colliding label sets 161 {Labels: model.LabelSet{"app": "l", "uniq0": "0", "uniq1": "1"}.String(), Entries: entries(5, now.Add(time.Minute))}, 162 {Labels: model.LabelSet{"app": "m", "uniq0": "1", "uniq1": "1"}.String(), Entries: entries(5, now)}, 163 {Labels: model.LabelSet{"app": "l", "uniq0": "1", "uniq1": "0"}.String(), Entries: entries(5, now.Add(time.Minute))}, 164 {Labels: model.LabelSet{"app": "m", "uniq0": "0", "uniq1": "0"}.String(), Entries: entries(5, now)}, 165 {Labels: model.LabelSet{"app": "l", "uniq0": "0", "uniq1": "0"}.String(), Entries: entries(5, now.Add(time.Minute))}, 166 {Labels: model.LabelSet{"app": "m", "uniq0": "1", "uniq1": "0"}.String(), Entries: entries(5, now)}, 167 }} 168 169 sort.Slice(req.Streams, func(i, j int) bool { 170 return req.Streams[i].Labels < req.Streams[j].Labels 171 }) 172 173 _, err := ing.Push(ctx, req) 174 require.NoError(t, err) 175 176 // force flush 177 require.NoError(t, services.StopAndAwaitTerminated(context.Background(), ing)) 178 179 // verify that we get all the data back 180 store.checkData(t, map[string][]logproto.Stream{userID: req.Streams}) 181 182 // make sure all chunks have different fingerprint, even colliding ones. 183 chunkFingerprints := map[model.Fingerprint]bool{} 184 for _, c := range store.getChunksForUser(userID) { 185 require.False(t, chunkFingerprints[c.FingerprintModel()]) 186 chunkFingerprints[c.FingerprintModel()] = true 187 } 188 } 189 190 func TestFlushMaxAge(t *testing.T) { 191 cfg := defaultIngesterTestConfig(t) 192 cfg.FlushCheckPeriod = time.Millisecond * 100 193 cfg.MaxChunkAge = time.Minute 194 cfg.MaxChunkIdle = time.Hour 195 196 store, ing := newTestStore(t, cfg, nil) 197 defer store.Stop() 198 199 now := time.Unix(0, 0) 200 201 firstEntries := []logproto.Entry{ 202 {Timestamp: now.Add(time.Nanosecond), Line: "1"}, 203 {Timestamp: now.Add(time.Minute), Line: "2"}, 204 } 205 206 secondEntries := []logproto.Entry{ 207 {Timestamp: now.Add(time.Second * 61), Line: "3"}, 208 } 209 210 req := &logproto.PushRequest{Streams: []logproto.Stream{ 211 {Labels: model.LabelSet{"app": "l"}.String(), Entries: firstEntries}, 212 }} 213 214 const userID = "testUser" 215 ctx := user.InjectOrgID(context.Background(), userID) 216 217 _, err := ing.Push(ctx, req) 218 require.NoError(t, err) 219 220 time.Sleep(2 * cfg.FlushCheckPeriod) 221 222 // ensure chunk is not flushed after flush period elapses 223 store.checkData(t, map[string][]logproto.Stream{}) 224 225 req2 := &logproto.PushRequest{Streams: []logproto.Stream{ 226 {Labels: model.LabelSet{"app": "l"}.String(), Entries: secondEntries}, 227 }} 228 229 _, err = ing.Push(ctx, req2) 230 require.NoError(t, err) 231 232 time.Sleep(2 * cfg.FlushCheckPeriod) 233 234 // assert stream is now both batches 235 store.checkData(t, map[string][]logproto.Stream{ 236 userID: { 237 {Labels: model.LabelSet{"app": "l"}.String(), Entries: append(firstEntries, secondEntries...)}, 238 }, 239 }) 240 241 require.NoError(t, services.StopAndAwaitTerminated(context.Background(), ing)) 242 } 243 244 type testStore struct { 245 mtx sync.Mutex 246 // Chunks keyed by userID. 247 chunks map[string][]chunk.Chunk 248 onPut func(ctx context.Context, chunks []chunk.Chunk) error 249 } 250 251 // Note: the ingester New() function creates it's own WAL first which we then override if specified. 252 // Because of this, ensure any WAL directories exist/are cleaned up even when overriding the wal. 253 // This is an ugly hook for testing :( 254 func newTestStore(t require.TestingT, cfg Config, walOverride WAL) (*testStore, *Ingester) { 255 store := &testStore{ 256 chunks: map[string][]chunk.Chunk{}, 257 } 258 259 limits, err := validation.NewOverrides(defaultLimitsTestConfig(), nil) 260 require.NoError(t, err) 261 262 ing, err := New(cfg, client.Config{}, store, limits, runtime.DefaultTenantConfigs(), nil) 263 require.NoError(t, err) 264 require.NoError(t, services.StartAndAwaitRunning(context.Background(), ing)) 265 266 if walOverride != nil { 267 _ = ing.wal.Stop() 268 ing.wal = walOverride 269 } 270 271 return store, ing 272 } 273 274 // nolint 275 func defaultIngesterTestConfig(t testing.TB) Config { 276 kvClient, err := kv.NewClient(kv.Config{Store: "inmemory"}, ring.GetCodec(), nil, gokitlog.NewNopLogger()) 277 require.NoError(t, err) 278 279 cfg := Config{} 280 flagext.DefaultValues(&cfg) 281 cfg.FlushCheckPeriod = 99999 * time.Hour 282 cfg.MaxChunkIdle = 99999 * time.Hour 283 cfg.ConcurrentFlushes = 1 284 cfg.LifecyclerConfig.RingConfig.KVStore.Mock = kvClient 285 cfg.LifecyclerConfig.NumTokens = 1 286 cfg.LifecyclerConfig.ListenPort = 0 287 cfg.LifecyclerConfig.Addr = "localhost" 288 cfg.LifecyclerConfig.ID = "localhost" 289 cfg.LifecyclerConfig.FinalSleep = 0 290 cfg.LifecyclerConfig.MinReadyDuration = 0 291 cfg.BlockSize = 256 * 1024 292 cfg.TargetChunkSize = 1500 * 1024 293 cfg.WAL.Enabled = false 294 return cfg 295 } 296 297 func (s *testStore) Put(ctx context.Context, chunks []chunk.Chunk) error { 298 s.mtx.Lock() 299 defer s.mtx.Unlock() 300 if s.onPut != nil { 301 return s.onPut(ctx, chunks) 302 } 303 userID, err := tenant.TenantID(ctx) 304 if err != nil { 305 return err 306 } 307 for ix, chunk := range chunks { 308 for _, label := range chunk.Metric { 309 if label.Value == "" { 310 return fmt.Errorf("Chunk has blank label %q", label.Name) 311 } 312 } 313 314 // remove __name__ label 315 if chunk.Metric.Has("__name__") { 316 labelsBuilder := labels.NewBuilder(chunk.Metric) 317 labelsBuilder.Del("__name__") 318 chunks[ix].Metric = labelsBuilder.Labels() 319 } 320 } 321 s.chunks[userID] = append(s.chunks[userID], chunks...) 322 return nil 323 } 324 325 func (s *testStore) IsLocal() bool { 326 return false 327 } 328 329 func (s *testStore) SelectLogs(ctx context.Context, req logql.SelectLogParams) (iter.EntryIterator, error) { 330 return nil, nil 331 } 332 333 func (s *testStore) SelectSamples(ctx context.Context, req logql.SelectSampleParams) (iter.SampleIterator, error) { 334 return nil, nil 335 } 336 337 func (s *testStore) GetChunkRefs(ctx context.Context, userID string, from, through model.Time, matchers ...*labels.Matcher) ([][]chunk.Chunk, []*fetcher.Fetcher, error) { 338 return nil, nil, nil 339 } 340 341 func (s *testStore) GetSchemaConfigs() []config.PeriodConfig { 342 return defaultPeriodConfigs 343 } 344 345 func (s *testStore) Stop() {} 346 347 func (s *testStore) SetChunkFilterer(_ chunk.RequestChunkFilterer) {} 348 349 func (s *testStore) Stats(ctx context.Context, userID string, from, through model.Time, matchers ...*labels.Matcher) (*stats.Stats, error) { 350 return &stats.Stats{}, nil 351 } 352 353 func pushTestSamples(t *testing.T, ing logproto.PusherServer) map[string][]logproto.Stream { 354 userIDs := []string{"1", "2", "3"} 355 356 // Create test samples. 357 testData := map[string][]logproto.Stream{} 358 for i, userID := range userIDs { 359 testData[userID] = buildTestStreams(i) 360 } 361 362 // Append samples. 363 for _, userID := range userIDs { 364 ctx := user.InjectOrgID(context.Background(), userID) 365 _, err := ing.Push(ctx, &logproto.PushRequest{ 366 Streams: testData[userID], 367 }) 368 require.NoError(t, err) 369 } 370 return testData 371 } 372 373 func buildTestStreams(offset int) []logproto.Stream { 374 var m []logproto.Stream 375 for i := 0; i < numSeries; i++ { 376 ss := logproto.Stream{ 377 Labels: model.Metric{ 378 "name": model.LabelValue(fmt.Sprintf("testmetric_%d", i)), 379 model.JobLabel: "testjob", 380 }.String(), 381 } 382 for j := 0; j < samplesPerSeries; j++ { 383 ss.Entries = append(ss.Entries, logproto.Entry{ 384 Timestamp: time.Unix(int64(i+j+offset), 0), 385 Line: "line", 386 }) 387 } 388 m = append(m, ss) 389 } 390 391 sort.Slice(m, func(i, j int) bool { 392 return m[i].Labels < m[j].Labels 393 }) 394 395 return m 396 } 397 398 // check that the store is holding data equivalent to what we expect 399 func (s *testStore) checkData(t *testing.T, testData map[string][]logproto.Stream) { 400 for userID, expected := range testData { 401 streams := s.getStreamsForUser(t, userID) 402 require.Equal(t, expected, streams) 403 } 404 } 405 406 func (s *testStore) getStreamsForUser(t *testing.T, userID string) []logproto.Stream { 407 var streams []logproto.Stream 408 for _, c := range s.getChunksForUser(userID) { 409 lokiChunk := c.Data.(*chunkenc.Facade).LokiChunk() 410 streams = append(streams, buildStreamsFromChunk(t, c.Metric.String(), lokiChunk)) 411 } 412 sort.Slice(streams, func(i, j int) bool { 413 return streams[i].Labels < streams[j].Labels 414 }) 415 return streams 416 } 417 418 func (s *testStore) getChunksForUser(userID string) []chunk.Chunk { 419 s.mtx.Lock() 420 defer s.mtx.Unlock() 421 422 return s.chunks[userID] 423 } 424 425 func buildStreamsFromChunk(t *testing.T, lbs string, chk chunkenc.Chunk) logproto.Stream { 426 it, err := chk.Iterator(context.TODO(), time.Unix(0, 0), time.Unix(1000, 0), logproto.FORWARD, log.NewNoopPipeline().ForStream(labels.Labels{})) 427 require.NoError(t, err) 428 429 stream := logproto.Stream{ 430 Labels: lbs, 431 } 432 for it.Next() { 433 stream.Entries = append(stream.Entries, it.Entry()) 434 } 435 require.NoError(t, it.Error()) 436 return stream 437 }