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  }