github.com/grafana/pyroscope@v1.18.0/pkg/phlaredb/phlaredb_test.go (about)

     1  package phlaredb
     2  
     3  import (
     4  	"context"
     5  	"io"
     6  	"math"
     7  	"net/http"
     8  	"os"
     9  	"testing"
    10  	"time"
    11  
    12  	"connectrpc.com/connect"
    13  	"github.com/google/pprof/profile"
    14  	"github.com/google/uuid"
    15  	"github.com/pkg/errors"
    16  	"github.com/prometheus/common/model"
    17  	"github.com/stretchr/testify/assert"
    18  	"github.com/stretchr/testify/require"
    19  	"go.uber.org/goleak"
    20  
    21  	googlev1 "github.com/grafana/pyroscope/api/gen/proto/go/google/v1"
    22  	ingestv1 "github.com/grafana/pyroscope/api/gen/proto/go/ingester/v1"
    23  	"github.com/grafana/pyroscope/api/gen/proto/go/ingester/v1/ingesterv1connect"
    24  	pushv1 "github.com/grafana/pyroscope/api/gen/proto/go/push/v1"
    25  	typesv1 "github.com/grafana/pyroscope/api/gen/proto/go/types/v1"
    26  	connectapi "github.com/grafana/pyroscope/pkg/api/connect"
    27  	phlaremodel "github.com/grafana/pyroscope/pkg/model"
    28  	"github.com/grafana/pyroscope/pkg/testhelper"
    29  )
    30  
    31  func TestCreateLocalDir(t *testing.T) {
    32  	ctx := testContext(t)
    33  	dataPath := contextDataDir(ctx)
    34  	localFile := dataPath + "/local"
    35  	require.NoError(t, os.WriteFile(localFile, []byte("d"), 0o644))
    36  	_, err := New(testContext(t), Config{
    37  		DataPath:         dataPath,
    38  		MaxBlockDuration: 30 * time.Minute,
    39  	}, NoLimit, ctx.localBucketClient)
    40  	require.Error(t, err)
    41  	require.NoError(t, os.Remove(localFile))
    42  	_, err = New(ctx, Config{
    43  		DataPath:         dataPath,
    44  		MaxBlockDuration: 30 * time.Minute,
    45  	}, NoLimit, ctx.localBucketClient)
    46  	require.NoError(t, err)
    47  }
    48  
    49  var cpuProfileGenerator = func(tsNano int64, t testing.TB) (*googlev1.Profile, string) {
    50  	p := parseProfile(t, "testdata/profile")
    51  	p.TimeNanos = tsNano
    52  	return p, "process_cpu"
    53  }
    54  
    55  func ingestProfiles(b testing.TB, db *PhlareDB, generator func(tsNano int64, t testing.TB) (*googlev1.Profile, string), from, to int64, step time.Duration, externalLabels ...*typesv1.LabelPair) {
    56  	b.Helper()
    57  	for i := from; i <= to; i += int64(step) {
    58  		p, name := generator(i, b)
    59  		require.NoError(b, db.Ingest(
    60  			context.Background(), p, uuid.New(), nil, append(externalLabels, &typesv1.LabelPair{Name: model.MetricNameLabel, Value: name})...))
    61  	}
    62  }
    63  
    64  type fakeBidiServerMergeProfilesStacktraces struct {
    65  	profilesSent []*ingestv1.ProfileSets
    66  	keep         [][]bool
    67  	t            *testing.T
    68  }
    69  
    70  func (f *fakeBidiServerMergeProfilesStacktraces) Send(resp *ingestv1.MergeProfilesStacktracesResponse) error {
    71  	f.profilesSent = append(f.profilesSent, testhelper.CloneProto(f.t, resp.SelectedProfiles))
    72  	return nil
    73  }
    74  
    75  func (f *fakeBidiServerMergeProfilesStacktraces) Receive() (*ingestv1.MergeProfilesStacktracesRequest, error) {
    76  	res := &ingestv1.MergeProfilesStacktracesRequest{
    77  		Profiles: f.keep[0],
    78  	}
    79  	f.keep = f.keep[1:]
    80  	return res, nil
    81  }
    82  
    83  func (q Queriers) ingesterClient() (ingesterv1connect.IngesterServiceClient, func()) {
    84  	mux := http.NewServeMux()
    85  	mux.Handle(ingesterv1connect.NewIngesterServiceHandler(&ingesterHandlerPhlareDB{q}, connectapi.DefaultHandlerOptions()...))
    86  	serv := testhelper.NewInMemoryServer(mux)
    87  
    88  	var httpClient = serv.Client()
    89  
    90  	client := ingesterv1connect.NewIngesterServiceClient(
    91  		httpClient, serv.URL(), connectapi.DefaultClientOptions()...,
    92  	)
    93  
    94  	return client, serv.Close
    95  }
    96  
    97  type ingesterHandlerPhlareDB struct {
    98  	Queriers
    99  	// *PhlareDB
   100  }
   101  
   102  func (i *ingesterHandlerPhlareDB) MergeProfilesStacktraces(ctx context.Context, stream *connect.BidiStream[ingestv1.MergeProfilesStacktracesRequest, ingestv1.MergeProfilesStacktracesResponse]) error {
   103  	return MergeProfilesStacktraces(ctx, stream, i.ForTimeRange)
   104  }
   105  
   106  func (i *ingesterHandlerPhlareDB) MergeProfilesLabels(ctx context.Context, stream *connect.BidiStream[ingestv1.MergeProfilesLabelsRequest, ingestv1.MergeProfilesLabelsResponse]) error {
   107  	return MergeProfilesLabels(ctx, stream, i.ForTimeRange)
   108  }
   109  
   110  func (i *ingesterHandlerPhlareDB) MergeProfilesPprof(ctx context.Context, stream *connect.BidiStream[ingestv1.MergeProfilesPprofRequest, ingestv1.MergeProfilesPprofResponse]) error {
   111  	return MergeProfilesPprof(ctx, stream, i.ForTimeRange)
   112  }
   113  
   114  func (i *ingesterHandlerPhlareDB) MergeSpanProfile(ctx context.Context, stream *connect.BidiStream[ingestv1.MergeSpanProfileRequest, ingestv1.MergeSpanProfileResponse]) error {
   115  	return MergeSpanProfile(ctx, stream, i.ForTimeRange)
   116  }
   117  
   118  func (i *ingesterHandlerPhlareDB) Push(context.Context, *connect.Request[pushv1.PushRequest]) (*connect.Response[pushv1.PushResponse], error) {
   119  	return nil, errors.New("not implemented")
   120  }
   121  
   122  func (i *ingesterHandlerPhlareDB) LabelValues(context.Context, *connect.Request[typesv1.LabelValuesRequest]) (*connect.Response[typesv1.LabelValuesResponse], error) {
   123  	return nil, errors.New("not implemented")
   124  }
   125  
   126  func (i *ingesterHandlerPhlareDB) LabelNames(context.Context, *connect.Request[typesv1.LabelNamesRequest]) (*connect.Response[typesv1.LabelNamesResponse], error) {
   127  	return nil, errors.New("not implemented")
   128  }
   129  
   130  func (i *ingesterHandlerPhlareDB) ProfileTypes(context.Context, *connect.Request[ingestv1.ProfileTypesRequest]) (*connect.Response[ingestv1.ProfileTypesResponse], error) {
   131  	return nil, errors.New("not implemented")
   132  }
   133  
   134  func (i *ingesterHandlerPhlareDB) Series(context.Context, *connect.Request[ingestv1.SeriesRequest]) (*connect.Response[ingestv1.SeriesResponse], error) {
   135  	return nil, errors.New("not implemented")
   136  }
   137  
   138  func (i *ingesterHandlerPhlareDB) Flush(context.Context, *connect.Request[ingestv1.FlushRequest]) (*connect.Response[ingestv1.FlushResponse], error) {
   139  	return nil, errors.New("not implemented")
   140  }
   141  
   142  func (i *ingesterHandlerPhlareDB) BlockMetadata(context.Context, *connect.Request[ingestv1.BlockMetadataRequest]) (*connect.Response[ingestv1.BlockMetadataResponse], error) {
   143  	return nil, errors.New("not implemented")
   144  }
   145  
   146  func (i *ingesterHandlerPhlareDB) GetProfileStats(context.Context, *connect.Request[typesv1.GetProfileStatsRequest]) (*connect.Response[typesv1.GetProfileStatsResponse], error) {
   147  	return nil, errors.New("not implemented")
   148  }
   149  
   150  func (i *ingesterHandlerPhlareDB) GetBlockStats(context.Context, *connect.Request[ingestv1.GetBlockStatsRequest]) (*connect.Response[ingestv1.GetBlockStatsResponse], error) {
   151  	return nil, errors.New("not implemented")
   152  }
   153  
   154  func TestMergeProfilesStacktraces(t *testing.T) {
   155  	defer goleak.VerifyNone(t, goleak.IgnoreCurrent())
   156  
   157  	// ingest some sample data
   158  	var (
   159  		ctx     = testContext(t)
   160  		testDir = contextDataDir(ctx)
   161  		end     = time.Unix(0, int64(time.Hour))
   162  		start   = end.Add(-time.Minute)
   163  		step    = 15 * time.Second
   164  	)
   165  
   166  	db, err := New(ctx, Config{
   167  		DataPath:         testDir,
   168  		MaxBlockDuration: time.Duration(100000) * time.Minute, // we will manually flush
   169  	}, NoLimit, ctx.localBucketClient)
   170  	require.NoError(t, err)
   171  	defer func() {
   172  		require.NoError(t, db.Close())
   173  	}()
   174  
   175  	ingestProfiles(t, db, cpuProfileGenerator, start.UnixNano(), end.UnixNano(), step,
   176  		&typesv1.LabelPair{Name: "namespace", Value: "my-namespace"},
   177  		&typesv1.LabelPair{Name: "pod", Value: "my-pod"},
   178  	)
   179  
   180  	// create client
   181  	client, cleanup := db.queriers().ingesterClient()
   182  	defer cleanup()
   183  
   184  	t.Run("request the one existing series", func(t *testing.T) {
   185  		bidi := client.MergeProfilesStacktraces(ctx)
   186  
   187  		require.NoError(t, bidi.Send(&ingestv1.MergeProfilesStacktracesRequest{
   188  			Request: &ingestv1.SelectProfilesRequest{
   189  				LabelSelector: `{pod="my-pod"}`,
   190  				Type:          mustParseProfileSelector(t, "process_cpu:cpu:nanoseconds:cpu:nanoseconds"),
   191  				Start:         start.UnixMilli(),
   192  				End:           end.UnixMilli(),
   193  			},
   194  		}))
   195  
   196  		resp, err := bidi.Receive()
   197  		require.NoError(t, err)
   198  		require.Nil(t, resp.Result)
   199  		require.Len(t, resp.SelectedProfiles.Fingerprints, 1)
   200  		require.Len(t, resp.SelectedProfiles.Profiles, 5)
   201  
   202  		require.NoError(t, bidi.Send(&ingestv1.MergeProfilesStacktracesRequest{
   203  			Profiles: []bool{true},
   204  		}))
   205  
   206  		// expect empty response
   207  		resp, err = bidi.Receive()
   208  		require.NoError(t, err)
   209  		require.Nil(t, resp.Result)
   210  
   211  		// received result
   212  		resp, err = bidi.Receive()
   213  		require.NoError(t, err)
   214  		require.NotNil(t, resp.Result)
   215  
   216  		at, err := phlaremodel.UnmarshalTree(resp.Result.TreeBytes)
   217  		require.NoError(t, err)
   218  		require.Equal(t, int64(500000000), at.Total())
   219  	})
   220  
   221  	t.Run("request non existing series", func(t *testing.T) {
   222  		bidi := client.MergeProfilesStacktraces(ctx)
   223  
   224  		require.NoError(t, bidi.Send(&ingestv1.MergeProfilesStacktracesRequest{
   225  			Request: &ingestv1.SelectProfilesRequest{
   226  				LabelSelector: `{pod="not-my-pod"}`,
   227  				Type:          mustParseProfileSelector(t, "process_cpu:cpu:nanoseconds:cpu:nanoseconds"),
   228  				Start:         start.UnixMilli(),
   229  				End:           end.UnixMilli(),
   230  			},
   231  		}))
   232  
   233  		// expect empty resp to signal it is finished
   234  		resp, err := bidi.Receive()
   235  		require.NoError(t, err)
   236  		require.Nil(t, resp.Result)
   237  		require.Nil(t, resp.SelectedProfiles)
   238  
   239  		// still receiving a result
   240  		resp, err = bidi.Receive()
   241  		require.NoError(t, err)
   242  		require.NotNil(t, resp.Result)
   243  		require.Len(t, resp.Result.Stacktraces, 0)
   244  		require.Len(t, resp.Result.FunctionNames, 0)
   245  		require.Nil(t, resp.SelectedProfiles)
   246  	})
   247  
   248  	t.Run("empty request fails", func(t *testing.T) {
   249  		bidi := client.MergeProfilesStacktraces(ctx)
   250  
   251  		// It is possible that the error returned by server side of the
   252  		// stream closes the net.Conn before bidi.Send has finished. The
   253  		// short timing for that to happen with real HTTP servers makes this
   254  		// unlikely, but it does happen with the synchronous in memory
   255  		// net.Pipe() that is used here.
   256  		// See https://github.com/grafana/pyroscope/issues/3549 for more details.
   257  		if err := bidi.Send(&ingestv1.MergeProfilesStacktracesRequest{}); !errors.Is(err, io.EOF) {
   258  			require.NoError(t, err)
   259  		}
   260  
   261  		_, err := bidi.Receive()
   262  		require.EqualError(t, err, "invalid_argument: missing initial select request")
   263  	})
   264  
   265  	t.Run("test cancellation", func(t *testing.T) {
   266  		ctx, cancel := context.WithCancel(ctx)
   267  		bidi := client.MergeProfilesStacktraces(ctx)
   268  		require.NoError(t, bidi.Send(&ingestv1.MergeProfilesStacktracesRequest{
   269  			Request: &ingestv1.SelectProfilesRequest{
   270  				LabelSelector: `{pod="my-pod"}`,
   271  				Type:          mustParseProfileSelector(t, "process_cpu:cpu:nanoseconds:cpu:nanoseconds"),
   272  				Start:         start.UnixMilli(),
   273  				End:           end.UnixMilli(),
   274  			},
   275  		}))
   276  		cancel()
   277  	})
   278  
   279  	t.Run("test close request", func(t *testing.T) {
   280  		bidi := client.MergeProfilesStacktraces(ctx)
   281  		require.NoError(t, bidi.Send(&ingestv1.MergeProfilesStacktracesRequest{
   282  			Request: &ingestv1.SelectProfilesRequest{
   283  				LabelSelector: `{pod="my-pod"}`,
   284  				Type:          mustParseProfileSelector(t, "process_cpu:cpu:nanoseconds:cpu:nanoseconds"),
   285  				Start:         start.UnixMilli(),
   286  				End:           end.UnixMilli(),
   287  			},
   288  		}))
   289  		require.NoError(t, bidi.CloseRequest())
   290  	})
   291  }
   292  
   293  // See https://github.com/grafana/pyroscope/pull/3356
   294  func Test_HeadFlush_DuplicateLabels(t *testing.T) {
   295  	defer goleak.VerifyNone(t, goleak.IgnoreCurrent())
   296  
   297  	// ingest some sample data
   298  	var (
   299  		ctx     = testContext(t)
   300  		testDir = contextDataDir(ctx)
   301  		end     = time.Unix(0, int64(time.Hour))
   302  		start   = end.Add(-time.Minute)
   303  		step    = 15 * time.Second
   304  	)
   305  
   306  	db, err := New(ctx, Config{
   307  		DataPath:         testDir,
   308  		MaxBlockDuration: time.Duration(100000) * time.Minute,
   309  	}, NoLimit, ctx.localBucketClient)
   310  	require.NoError(t, err)
   311  	defer func() {
   312  		require.NoError(t, db.Close())
   313  	}()
   314  
   315  	ingestProfiles(t, db, cpuProfileGenerator, start.UnixNano(), end.UnixNano(), step,
   316  		&typesv1.LabelPair{Name: "namespace", Value: "my-namespace"},
   317  		&typesv1.LabelPair{Name: "pod", Value: "my-pod"},
   318  		&typesv1.LabelPair{Name: "pod", Value: "not-my-pod"},
   319  	)
   320  }
   321  
   322  func TestMergeProfilesPprof(t *testing.T) {
   323  	defer goleak.VerifyNone(t, goleak.IgnoreCurrent())
   324  
   325  	// ingest some sample data
   326  	var (
   327  		ctx     = testContext(t)
   328  		testDir = contextDataDir(ctx)
   329  		end     = time.Unix(0, int64(time.Hour))
   330  		start   = end.Add(-time.Minute)
   331  		step    = 15 * time.Second
   332  	)
   333  
   334  	db, err := New(ctx, Config{
   335  		DataPath:         testDir,
   336  		MaxBlockDuration: time.Duration(100000) * time.Minute, // we will manually flush
   337  	}, NoLimit, ctx.localBucketClient)
   338  	require.NoError(t, err)
   339  	defer func() {
   340  		require.NoError(t, db.Close())
   341  	}()
   342  
   343  	ingestProfiles(t, db, cpuProfileGenerator, start.UnixNano(), end.UnixNano(), step,
   344  		&typesv1.LabelPair{Name: "namespace", Value: "my-namespace"},
   345  		&typesv1.LabelPair{Name: "pod", Value: "my-pod"},
   346  	)
   347  
   348  	client, cleanup := db.queriers().ingesterClient()
   349  	defer cleanup()
   350  
   351  	t.Run("request the one existing series", func(t *testing.T) {
   352  		bidi := client.MergeProfilesPprof(ctx)
   353  
   354  		require.NoError(t, bidi.Send(&ingestv1.MergeProfilesPprofRequest{
   355  			Request: &ingestv1.SelectProfilesRequest{
   356  				LabelSelector: `{pod="my-pod"}`,
   357  				Type:          mustParseProfileSelector(t, "process_cpu:cpu:nanoseconds:cpu:nanoseconds"),
   358  				Start:         start.UnixMilli(),
   359  				End:           end.UnixMilli(),
   360  			},
   361  		}))
   362  
   363  		resp, err := bidi.Receive()
   364  		require.NoError(t, err)
   365  		require.Nil(t, resp.Result)
   366  		require.Len(t, resp.SelectedProfiles.Fingerprints, 1)
   367  		require.Len(t, resp.SelectedProfiles.Profiles, 5)
   368  
   369  		require.NoError(t, bidi.Send(&ingestv1.MergeProfilesPprofRequest{
   370  			Profiles: []bool{true},
   371  		}))
   372  
   373  		// expect empty resp to signal it is finished
   374  		resp, err = bidi.Receive()
   375  		require.NoError(t, err)
   376  		require.Nil(t, resp.Result)
   377  
   378  		// received result
   379  		resp, err = bidi.Receive()
   380  		require.NoError(t, err)
   381  		require.NotNil(t, resp.Result)
   382  		p, err := profile.ParseUncompressed(resp.Result)
   383  		require.NoError(t, err)
   384  		require.Len(t, p.Sample, 48)
   385  		require.Len(t, p.Location, 287)
   386  	})
   387  
   388  	t.Run("request non existing series", func(t *testing.T) {
   389  		bidi := client.MergeProfilesPprof(ctx)
   390  
   391  		require.NoError(t, bidi.Send(&ingestv1.MergeProfilesPprofRequest{
   392  			Request: &ingestv1.SelectProfilesRequest{
   393  				LabelSelector: `{pod="not-my-pod"}`,
   394  				Type:          mustParseProfileSelector(t, "process_cpu:cpu:nanoseconds:cpu:nanoseconds"),
   395  				Start:         start.UnixMilli(),
   396  				End:           end.UnixMilli(),
   397  			},
   398  		}))
   399  
   400  		// expect empty resp to signal it is finished
   401  		resp, err := bidi.Receive()
   402  		require.NoError(t, err)
   403  		require.Nil(t, resp.Result)
   404  		require.Nil(t, resp.SelectedProfiles)
   405  
   406  		// still receiving a result
   407  		resp, err = bidi.Receive()
   408  		require.NoError(t, err)
   409  		require.NotNil(t, resp.Result)
   410  		p, err := profile.ParseUncompressed(resp.Result)
   411  		require.NoError(t, err)
   412  		require.Len(t, p.Sample, 0)
   413  		require.Len(t, p.Location, 0)
   414  		require.Nil(t, resp.SelectedProfiles)
   415  	})
   416  
   417  	t.Run("empty request fails", func(t *testing.T) {
   418  		bidi := client.MergeProfilesPprof(ctx)
   419  
   420  		// It is possible that the error returned by server side of the
   421  		// stream closes the net.Conn before bidi.Send has finished. The
   422  		// short timing for that to happen with real HTTP servers makes this
   423  		// unlikely, but it does happen with the synchronous in memory
   424  		// net.Pipe() that is used here.
   425  		// See https://github.com/grafana/pyroscope/issues/3549 for more details.
   426  		if err := bidi.Send(&ingestv1.MergeProfilesPprofRequest{}); !errors.Is(err, io.EOF) {
   427  			require.NoError(t, err)
   428  		}
   429  
   430  		_, err := bidi.Receive()
   431  		require.EqualError(t, err, "invalid_argument: missing initial select request")
   432  	})
   433  
   434  	t.Run("test cancellation", func(t *testing.T) {
   435  		ctx, cancel := context.WithCancel(ctx)
   436  		bidi := client.MergeProfilesPprof(ctx)
   437  		require.NoError(t, bidi.Send(&ingestv1.MergeProfilesPprofRequest{
   438  			Request: &ingestv1.SelectProfilesRequest{
   439  				LabelSelector: `{pod="my-pod"}`,
   440  				Type:          mustParseProfileSelector(t, "process_cpu:cpu:nanoseconds:cpu:nanoseconds"),
   441  				Start:         start.UnixMilli(),
   442  				End:           end.UnixMilli(),
   443  			},
   444  		}))
   445  		cancel()
   446  	})
   447  
   448  	t.Run("test close request", func(t *testing.T) {
   449  		bidi := client.MergeProfilesPprof(ctx)
   450  		require.NoError(t, bidi.Send(&ingestv1.MergeProfilesPprofRequest{
   451  			Request: &ingestv1.SelectProfilesRequest{
   452  				LabelSelector: `{pod="my-pod"}`,
   453  				Type:          mustParseProfileSelector(t, "process_cpu:cpu:nanoseconds:cpu:nanoseconds"),
   454  				Start:         start.UnixMilli(),
   455  				End:           end.UnixMilli(),
   456  			},
   457  		}))
   458  		require.NoError(t, bidi.CloseRequest())
   459  	})
   460  
   461  	t.Run("timerange with no Profiles", func(t *testing.T) {
   462  		bidi := client.MergeProfilesPprof(ctx)
   463  		require.NoError(t, bidi.Send(&ingestv1.MergeProfilesPprofRequest{
   464  			Request: &ingestv1.SelectProfilesRequest{
   465  				LabelSelector: `{pod="my-pod"}`,
   466  				Type:          mustParseProfileSelector(t, "process_cpu:cpu:nanoseconds:cpu:nanoseconds"),
   467  				Start:         0,
   468  				End:           1,
   469  			},
   470  		}))
   471  		_, err := bidi.Receive()
   472  		require.NoError(t, err)
   473  		_, err = bidi.Receive()
   474  		require.NoError(t, err)
   475  	})
   476  }
   477  
   478  func Test_QueryNotInitializedHead(t *testing.T) {
   479  	defer goleak.VerifyNone(t, goleak.IgnoreCurrent())
   480  
   481  	ctx := testContext(t)
   482  
   483  	db, err := New(ctx, Config{
   484  		DataPath:         contextDataDir(ctx),
   485  		MaxBlockDuration: time.Duration(100000) * time.Minute, // we will manually flush
   486  	}, NoLimit, ctx.localBucketClient)
   487  	require.NoError(t, err)
   488  	defer func() {
   489  		require.NoError(t, db.Close())
   490  	}()
   491  
   492  	client, cleanup := db.queriers().ingesterClient()
   493  	defer cleanup()
   494  
   495  	t.Run("ProfileTypes", func(t *testing.T) {
   496  		resp, err := db.ProfileTypes(ctx, connect.NewRequest(new(ingestv1.ProfileTypesRequest)))
   497  		assert.NoError(t, err)
   498  		assert.NotNil(t, resp)
   499  		assert.NotNil(t, resp.Msg)
   500  	})
   501  
   502  	t.Run("LabelNames", func(t *testing.T) {
   503  		resp, err := db.LabelNames(ctx, connect.NewRequest(new(typesv1.LabelNamesRequest)))
   504  		assert.NoError(t, err)
   505  		assert.NotNil(t, resp)
   506  		assert.NotNil(t, resp.Msg)
   507  	})
   508  
   509  	t.Run("LabelValues", func(t *testing.T) {
   510  		resp, err := db.LabelValues(ctx, connect.NewRequest(new(typesv1.LabelValuesRequest)))
   511  		assert.NoError(t, err)
   512  		assert.NotNil(t, resp)
   513  		assert.NotNil(t, resp.Msg)
   514  	})
   515  
   516  	t.Run("Series", func(t *testing.T) {
   517  		resp, err := db.Series(ctx, connect.NewRequest(&ingestv1.SeriesRequest{}))
   518  		assert.NoError(t, err)
   519  		assert.NotNil(t, resp)
   520  		assert.NotNil(t, resp.Msg)
   521  	})
   522  
   523  	t.Run("MergeProfilesLabels", func(t *testing.T) {
   524  		ctx, cancel := context.WithCancel(ctx)
   525  		bidi := client.MergeProfilesLabels(ctx)
   526  		require.NoError(t, bidi.Send(&ingestv1.MergeProfilesLabelsRequest{
   527  			Request: &ingestv1.SelectProfilesRequest{},
   528  		}))
   529  		cancel()
   530  	})
   531  
   532  	t.Run("MergeProfilesStacktraces", func(t *testing.T) {
   533  		ctx, cancel := context.WithCancel(ctx)
   534  		bidi := client.MergeProfilesStacktraces(ctx)
   535  		require.NoError(t, bidi.Send(&ingestv1.MergeProfilesStacktracesRequest{
   536  			Request: &ingestv1.SelectProfilesRequest{},
   537  		}))
   538  		cancel()
   539  	})
   540  
   541  	t.Run("MergeProfilesPprof", func(t *testing.T) {
   542  		ctx, cancel := context.WithCancel(ctx)
   543  		bidi := client.MergeProfilesPprof(ctx)
   544  		require.NoError(t, bidi.Send(&ingestv1.MergeProfilesPprofRequest{
   545  			Request: &ingestv1.SelectProfilesRequest{},
   546  		}))
   547  		cancel()
   548  	})
   549  }
   550  
   551  func Test_FlushNotInitializedHead(t *testing.T) {
   552  	defer goleak.VerifyNone(t, goleak.IgnoreCurrent())
   553  
   554  	ctx := testContext(t)
   555  
   556  	db, err := New(ctx, Config{
   557  		DataPath:         contextDataDir(ctx),
   558  		MaxBlockDuration: 1 * time.Hour,
   559  	}, NoLimit, ctx.localBucketClient)
   560  
   561  	var (
   562  		end   = time.Unix(0, int64(time.Hour))
   563  		start = end.Add(-time.Minute)
   564  		step  = 5 * time.Second
   565  	)
   566  
   567  	require.NoError(t, err)
   568  	defer func() {
   569  		require.NoError(t, db.Close())
   570  	}()
   571  
   572  	ingestProfiles(t, db, cpuProfileGenerator, start.UnixNano(), end.UnixNano(), step,
   573  		&typesv1.LabelPair{Name: "namespace", Value: "my-namespace"},
   574  		&typesv1.LabelPair{Name: "pod", Value: "my-pod"},
   575  	)
   576  	require.NoError(t, db.Flush(ctx, true, ""))
   577  	require.Zero(t, db.headSize())
   578  
   579  	require.NoError(t, db.Flush(ctx, true, ""))
   580  	require.Zero(t, db.headSize())
   581  
   582  	ingestProfiles(t, db, cpuProfileGenerator, start.UnixNano(), end.UnixNano(), step,
   583  		&typesv1.LabelPair{Name: "namespace", Value: "my-namespace"},
   584  		&typesv1.LabelPair{Name: "pod", Value: "my-pod"},
   585  	)
   586  
   587  	require.NotZero(t, db.headSize())
   588  	require.NoError(t, db.Flush(ctx, true, ""))
   589  }
   590  
   591  func Test_endRangeForTimestamp(t *testing.T) {
   592  	for _, tt := range []struct {
   593  		name     string
   594  		ts       int64
   595  		expected int64
   596  	}{
   597  		{
   598  			name:     "start of first range",
   599  			ts:       0,
   600  			expected: 1 * time.Hour.Nanoseconds(),
   601  		},
   602  		{
   603  			name:     "end of first range",
   604  			ts:       1*time.Hour.Nanoseconds() - 1,
   605  			expected: 1 * time.Hour.Nanoseconds(),
   606  		},
   607  		{
   608  			name:     "start of second range",
   609  			ts:       1 * time.Hour.Nanoseconds(),
   610  			expected: 2 * time.Hour.Nanoseconds(),
   611  		},
   612  		{
   613  			name:     "end of second range",
   614  			ts:       2*time.Hour.Nanoseconds() - 1,
   615  			expected: 2 * time.Hour.Nanoseconds(),
   616  		},
   617  	} {
   618  		tt := tt
   619  		t.Run(tt.name, func(t *testing.T) {
   620  			require.Equal(t, tt.expected, endRangeForTimestamp(tt.ts, 1*time.Hour.Nanoseconds()))
   621  		})
   622  	}
   623  }
   624  
   625  func Test_getProfileStatsFromMetas(t *testing.T) {
   626  	tests := []struct {
   627  		name     string
   628  		minTimes []model.Time
   629  		maxTimes []model.Time
   630  		want     *typesv1.GetProfileStatsResponse
   631  	}{
   632  		{
   633  			name:     "no metas should result in no data ingested",
   634  			minTimes: []model.Time{},
   635  			maxTimes: []model.Time{},
   636  			want: &typesv1.GetProfileStatsResponse{
   637  				DataIngested:      false,
   638  				OldestProfileTime: math.MaxInt64,
   639  				NewestProfileTime: math.MinInt64,
   640  			},
   641  		},
   642  		{
   643  			name: "valid metas should result in data ingested",
   644  			minTimes: []model.Time{
   645  				model.TimeFromUnix(1710161819),
   646  				model.TimeFromUnix(1710171819),
   647  			},
   648  			maxTimes: []model.Time{
   649  				model.TimeFromUnix(1710172239),
   650  				model.TimeFromUnix(1710174239),
   651  			},
   652  			want: &typesv1.GetProfileStatsResponse{
   653  				DataIngested:      true,
   654  				OldestProfileTime: 1710161819000,
   655  				NewestProfileTime: 1710174239000,
   656  			},
   657  		},
   658  	}
   659  	for _, tt := range tests {
   660  		t.Run(tt.name, func(t *testing.T) {
   661  			response, err := getProfileStatsFromBounds(tt.minTimes, tt.maxTimes)
   662  			require.NoError(t, err)
   663  			assert.Equalf(t, tt.want, response, "getProfileStatsFromBounds(%v, %v)", tt.minTimes, tt.maxTimes)
   664  		})
   665  	}
   666  }