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 }