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

     1  // SPDX-License-Identifier: AGPL-3.0-only
     2  // Provenance-includes-location: https://github.com/cortexproject/cortex/blob/master/pkg/storage/tsdb/bucketindex/loader_test.go
     3  // Provenance-includes-license: Apache-2.0
     4  // Provenance-includes-copyright: The Cortex Authors.
     5  
     6  package bucketindex
     7  
     8  import (
     9  	"bytes"
    10  	"context"
    11  	"path"
    12  	"strings"
    13  	"testing"
    14  	"time"
    15  
    16  	"github.com/go-kit/log"
    17  	"github.com/grafana/dskit/services"
    18  	"github.com/grafana/dskit/test"
    19  	"github.com/oklog/ulid/v2"
    20  	"github.com/prometheus/client_golang/prometheus"
    21  	"github.com/prometheus/client_golang/prometheus/testutil"
    22  	"github.com/stretchr/testify/assert"
    23  	"github.com/stretchr/testify/require"
    24  
    25  	objstore_testutil "github.com/grafana/pyroscope/pkg/objstore/testutil"
    26  )
    27  
    28  func TestLoader_GetIndex_ShouldLazyLoadBucketIndex(t *testing.T) {
    29  	ctx := context.Background()
    30  	reg := prometheus.NewPedanticRegistry()
    31  	bkt, _ := objstore_testutil.NewFilesystemBucket(t, ctx, t.TempDir())
    32  
    33  	// Create a bucket index.
    34  	idx := &Index{
    35  		Version: IndexVersion1,
    36  		Blocks: Blocks{
    37  			{ID: ulid.MustNew(1, nil), MinTime: 10, MaxTime: 20},
    38  		},
    39  		BlockDeletionMarks: nil,
    40  		UpdatedAt:          time.Now().Unix(),
    41  	}
    42  	require.NoError(t, WriteIndex(ctx, bkt, "user-1", nil, idx))
    43  
    44  	// Create the loader.
    45  	loader := NewLoader(prepareLoaderConfig(), bkt, nil, log.NewNopLogger(), reg)
    46  	require.NoError(t, services.StartAndAwaitRunning(ctx, loader))
    47  	t.Cleanup(func() {
    48  		require.NoError(t, services.StopAndAwaitTerminated(ctx, loader))
    49  	})
    50  
    51  	// Ensure no index has been loaded yet.
    52  	assert.NoError(t, testutil.GatherAndCompare(reg, bytes.NewBufferString(`
    53  		# HELP pyroscope_bucket_index_load_failures_total Total number of bucket index loading failures.
    54  		# TYPE pyroscope_bucket_index_load_failures_total counter
    55  		pyroscope_bucket_index_load_failures_total 0
    56  		# HELP pyroscope_bucket_index_loaded Number of bucket indexes currently loaded in-memory.
    57  		# TYPE pyroscope_bucket_index_loaded gauge
    58  		pyroscope_bucket_index_loaded 0
    59  		# HELP pyroscope_bucket_index_loads_total Total number of bucket index loading attempts.
    60  		# TYPE pyroscope_bucket_index_loads_total counter
    61  		pyroscope_bucket_index_loads_total 0
    62  	`),
    63  		"pyroscope_bucket_index_loads_total",
    64  		"pyroscope_bucket_index_load_failures_total",
    65  		"pyroscope_bucket_index_loaded",
    66  	))
    67  
    68  	// Request the index multiple times.
    69  	for i := 0; i < 10; i++ {
    70  		actualIdx, err := loader.GetIndex(ctx, "user-1")
    71  		require.NoError(t, err)
    72  		assert.Equal(t, idx, actualIdx)
    73  	}
    74  
    75  	// Ensure metrics have been updated accordingly.
    76  	assert.NoError(t, testutil.GatherAndCompare(reg, bytes.NewBufferString(`
    77  		# HELP pyroscope_bucket_index_load_failures_total Total number of bucket index loading failures.
    78  		# TYPE pyroscope_bucket_index_load_failures_total counter
    79  		pyroscope_bucket_index_load_failures_total 0
    80  		# HELP pyroscope_bucket_index_loaded Number of bucket indexes currently loaded in-memory.
    81  		# TYPE pyroscope_bucket_index_loaded gauge
    82  		pyroscope_bucket_index_loaded 1
    83  		# HELP pyroscope_bucket_index_loads_total Total number of bucket index loading attempts.
    84  		# TYPE pyroscope_bucket_index_loads_total counter
    85  		pyroscope_bucket_index_loads_total 1
    86  	`),
    87  		"pyroscope_bucket_index_loads_total",
    88  		"pyroscope_bucket_index_load_failures_total",
    89  		"pyroscope_bucket_index_loaded",
    90  	))
    91  }
    92  
    93  func TestLoader_GetIndex_ShouldCacheError(t *testing.T) {
    94  	ctx := context.Background()
    95  	reg := prometheus.NewPedanticRegistry()
    96  	bkt, _ := objstore_testutil.NewFilesystemBucket(t, ctx, t.TempDir())
    97  
    98  	// Create the loader.
    99  	loader := NewLoader(prepareLoaderConfig(), bkt, nil, log.NewNopLogger(), reg)
   100  	require.NoError(t, services.StartAndAwaitRunning(ctx, loader))
   101  	t.Cleanup(func() {
   102  		require.NoError(t, services.StopAndAwaitTerminated(ctx, loader))
   103  	})
   104  
   105  	// Write a corrupted index.
   106  	require.NoError(t, bkt.Upload(ctx, path.Join("user-1", "phlaredb/", IndexCompressedFilename), strings.NewReader("invalid!}")))
   107  
   108  	// Request the index multiple times.
   109  	for i := 0; i < 10; i++ {
   110  		_, err := loader.GetIndex(ctx, "user-1")
   111  		require.Equal(t, ErrIndexCorrupted, err)
   112  	}
   113  
   114  	// Ensure metrics have been updated accordingly.
   115  	assert.NoError(t, testutil.GatherAndCompare(reg, bytes.NewBufferString(`
   116  		# HELP pyroscope_bucket_index_load_failures_total Total number of bucket index loading failures.
   117  		# TYPE pyroscope_bucket_index_load_failures_total counter
   118  		pyroscope_bucket_index_load_failures_total 1
   119  		# HELP pyroscope_bucket_index_loaded Number of bucket indexes currently loaded in-memory.
   120  		# TYPE pyroscope_bucket_index_loaded gauge
   121  		pyroscope_bucket_index_loaded 0
   122  		# HELP pyroscope_bucket_index_loads_total Total number of bucket index loading attempts.
   123  		# TYPE pyroscope_bucket_index_loads_total counter
   124  		pyroscope_bucket_index_loads_total 1
   125  	`),
   126  		"pyroscope_bucket_index_loads_total",
   127  		"pyroscope_bucket_index_load_failures_total",
   128  		"pyroscope_bucket_index_loaded",
   129  	))
   130  }
   131  
   132  func TestLoader_GetIndex_ShouldCacheIndexNotFoundError(t *testing.T) {
   133  	ctx := context.Background()
   134  	reg := prometheus.NewPedanticRegistry()
   135  	bkt, _ := objstore_testutil.NewFilesystemBucket(t, ctx, t.TempDir())
   136  
   137  	// Create the loader.
   138  	loader := NewLoader(prepareLoaderConfig(), bkt, nil, log.NewNopLogger(), reg)
   139  	require.NoError(t, services.StartAndAwaitRunning(ctx, loader))
   140  	t.Cleanup(func() {
   141  		require.NoError(t, services.StopAndAwaitTerminated(ctx, loader))
   142  	})
   143  
   144  	// Request the index multiple times.
   145  	for i := 0; i < 10; i++ {
   146  		_, err := loader.GetIndex(ctx, "user-1")
   147  		require.Equal(t, ErrIndexNotFound, err)
   148  	}
   149  
   150  	// Ensure metrics have been updated accordingly.
   151  	assert.NoError(t, testutil.GatherAndCompare(reg, bytes.NewBufferString(`
   152  		# HELP pyroscope_bucket_index_load_failures_total Total number of bucket index loading failures.
   153  		# TYPE pyroscope_bucket_index_load_failures_total counter
   154  		pyroscope_bucket_index_load_failures_total 0
   155  		# HELP pyroscope_bucket_index_loaded Number of bucket indexes currently loaded in-memory.
   156  		# TYPE pyroscope_bucket_index_loaded gauge
   157  		pyroscope_bucket_index_loaded 0
   158  		# HELP pyroscope_bucket_index_loads_total Total number of bucket index loading attempts.
   159  		# TYPE pyroscope_bucket_index_loads_total counter
   160  		pyroscope_bucket_index_loads_total 1
   161  	`),
   162  		"pyroscope_bucket_index_loads_total",
   163  		"pyroscope_bucket_index_load_failures_total",
   164  		"pyroscope_bucket_index_loaded",
   165  	))
   166  }
   167  
   168  func TestLoader_ShouldUpdateIndexInBackgroundOnPreviousLoadSuccess(t *testing.T) {
   169  	ctx := context.Background()
   170  	reg := prometheus.NewPedanticRegistry()
   171  	bkt, _ := objstore_testutil.NewFilesystemBucket(t, ctx, t.TempDir())
   172  
   173  	// Create a bucket index.
   174  	idx := &Index{
   175  		Version: IndexVersion1,
   176  		Blocks: Blocks{
   177  			{ID: ulid.MustNew(1, nil), MinTime: 10, MaxTime: 20},
   178  		},
   179  		BlockDeletionMarks: nil,
   180  		UpdatedAt:          time.Now().Unix(),
   181  	}
   182  	require.NoError(t, WriteIndex(ctx, bkt, "user-1", nil, idx))
   183  
   184  	// Create the loader.
   185  	cfg := LoaderConfig{
   186  		CheckInterval:         time.Second,
   187  		UpdateOnStaleInterval: time.Second,
   188  		UpdateOnErrorInterval: time.Hour, // Intentionally high to not hit it.
   189  		IdleTimeout:           time.Hour, // Intentionally high to not hit it.
   190  	}
   191  
   192  	loader := NewLoader(cfg, bkt, nil, log.NewNopLogger(), reg)
   193  	require.NoError(t, services.StartAndAwaitRunning(ctx, loader))
   194  	t.Cleanup(func() {
   195  		require.NoError(t, services.StopAndAwaitTerminated(ctx, loader))
   196  	})
   197  
   198  	actualIdx, err := loader.GetIndex(ctx, "user-1")
   199  	require.NoError(t, err)
   200  	assert.Equal(t, idx, actualIdx)
   201  
   202  	// Update the bucket index.
   203  	idx.Blocks = append(idx.Blocks, &Block{ID: ulid.MustNew(2, nil), MinTime: 20, MaxTime: 30})
   204  	require.NoError(t, WriteIndex(ctx, bkt, "user-1", nil, idx))
   205  
   206  	// Wait until the index has been updated in background.
   207  	test.Poll(t, 3*time.Second, 2, func() interface{} {
   208  		actualIdx, err := loader.GetIndex(ctx, "user-1")
   209  		if err != nil {
   210  			return 0
   211  		}
   212  		return len(actualIdx.Blocks)
   213  	})
   214  
   215  	actualIdx, err = loader.GetIndex(ctx, "user-1")
   216  	require.NoError(t, err)
   217  	assert.Equal(t, idx, actualIdx)
   218  
   219  	// Ensure metrics have been updated accordingly.
   220  	assert.NoError(t, testutil.GatherAndCompare(reg, bytes.NewBufferString(`
   221  		# HELP pyroscope_bucket_index_load_failures_total Total number of bucket index loading failures.
   222  		# TYPE pyroscope_bucket_index_load_failures_total counter
   223  		pyroscope_bucket_index_load_failures_total 0
   224  		# HELP pyroscope_bucket_index_loaded Number of bucket indexes currently loaded in-memory.
   225  		# TYPE pyroscope_bucket_index_loaded gauge
   226  		pyroscope_bucket_index_loaded 1
   227  	`),
   228  		"pyroscope_bucket_index_load_failures_total",
   229  		"pyroscope_bucket_index_loaded",
   230  	))
   231  }
   232  
   233  func TestLoader_ShouldUpdateIndexInBackgroundOnPreviousLoadFailure(t *testing.T) {
   234  	ctx := context.Background()
   235  	reg := prometheus.NewPedanticRegistry()
   236  	bkt, _ := objstore_testutil.NewFilesystemBucket(t, ctx, t.TempDir())
   237  
   238  	// Write a corrupted index.
   239  	require.NoError(t, bkt.Upload(ctx, path.Join("user-1", "phlaredb/", IndexCompressedFilename), strings.NewReader("invalid!}")))
   240  
   241  	// Create the loader.
   242  	cfg := LoaderConfig{
   243  		CheckInterval:         time.Second,
   244  		UpdateOnStaleInterval: time.Hour, // Intentionally high to not hit it.
   245  		UpdateOnErrorInterval: time.Second,
   246  		IdleTimeout:           time.Hour, // Intentionally high to not hit it.
   247  	}
   248  
   249  	loader := NewLoader(cfg, bkt, nil, log.NewNopLogger(), reg)
   250  	require.NoError(t, services.StartAndAwaitRunning(ctx, loader))
   251  	t.Cleanup(func() {
   252  		require.NoError(t, services.StopAndAwaitTerminated(ctx, loader))
   253  	})
   254  
   255  	_, err := loader.GetIndex(ctx, "user-1")
   256  	assert.Equal(t, ErrIndexCorrupted, err)
   257  
   258  	// Upload the bucket index.
   259  	idx := &Index{
   260  		Version: IndexVersion1,
   261  		Blocks: Blocks{
   262  			{ID: ulid.MustNew(1, nil), MinTime: 10, MaxTime: 20},
   263  		},
   264  		BlockDeletionMarks: nil,
   265  		UpdatedAt:          time.Now().Unix(),
   266  	}
   267  	require.NoError(t, WriteIndex(ctx, bkt, "user-1", nil, idx))
   268  
   269  	// Wait until the index has been updated in background.
   270  	test.Poll(t, 3*time.Second, nil, func() interface{} {
   271  		_, err := loader.GetIndex(ctx, "user-1")
   272  		return err
   273  	})
   274  
   275  	actualIdx, err := loader.GetIndex(ctx, "user-1")
   276  	require.NoError(t, err)
   277  	assert.Equal(t, idx, actualIdx)
   278  
   279  	// Ensure metrics have been updated accordingly.
   280  	assert.NoError(t, testutil.GatherAndCompare(reg, bytes.NewBufferString(`
   281  		# HELP pyroscope_bucket_index_loaded Number of bucket indexes currently loaded in-memory.
   282  		# TYPE pyroscope_bucket_index_loaded gauge
   283  		pyroscope_bucket_index_loaded 1
   284  	`),
   285  		"pyroscope_bucket_index_loaded",
   286  	))
   287  }
   288  
   289  func TestLoader_ShouldUpdateIndexInBackgroundOnPreviousIndexNotFound(t *testing.T) {
   290  	ctx := context.Background()
   291  	reg := prometheus.NewPedanticRegistry()
   292  	bkt, _ := objstore_testutil.NewFilesystemBucket(t, ctx, t.TempDir())
   293  
   294  	// Create the loader.
   295  	cfg := LoaderConfig{
   296  		CheckInterval:         time.Second,
   297  		UpdateOnStaleInterval: time.Second,
   298  		UpdateOnErrorInterval: time.Hour, // Intentionally high to not hit it.
   299  		IdleTimeout:           time.Hour, // Intentionally high to not hit it.
   300  	}
   301  
   302  	loader := NewLoader(cfg, bkt, nil, log.NewNopLogger(), reg)
   303  	require.NoError(t, services.StartAndAwaitRunning(ctx, loader))
   304  	t.Cleanup(func() {
   305  		require.NoError(t, services.StopAndAwaitTerminated(ctx, loader))
   306  	})
   307  
   308  	_, err := loader.GetIndex(ctx, "user-1")
   309  	assert.Equal(t, ErrIndexNotFound, err)
   310  
   311  	// Upload the bucket index.
   312  	idx := &Index{
   313  		Version: IndexVersion1,
   314  		Blocks: Blocks{
   315  			{ID: ulid.MustNew(1, nil), MinTime: 10, MaxTime: 20},
   316  		},
   317  		BlockDeletionMarks: nil,
   318  		UpdatedAt:          time.Now().Unix(),
   319  	}
   320  	require.NoError(t, WriteIndex(ctx, bkt, "user-1", nil, idx))
   321  
   322  	// Wait until the index has been updated in background.
   323  	test.Poll(t, 3*time.Second, nil, func() interface{} {
   324  		_, err := loader.GetIndex(ctx, "user-1")
   325  		return err
   326  	})
   327  
   328  	actualIdx, err := loader.GetIndex(ctx, "user-1")
   329  	require.NoError(t, err)
   330  	assert.Equal(t, idx, actualIdx)
   331  
   332  	// Ensure metrics have been updated accordingly.
   333  	assert.NoError(t, testutil.GatherAndCompare(reg, bytes.NewBufferString(`
   334  		# HELP pyroscope_bucket_index_loaded Number of bucket indexes currently loaded in-memory.
   335  		# TYPE pyroscope_bucket_index_loaded gauge
   336  		pyroscope_bucket_index_loaded 1
   337  	`),
   338  		"pyroscope_bucket_index_loaded",
   339  	))
   340  }
   341  
   342  func TestLoader_ShouldNotCacheCriticalErrorOnBackgroundUpdates(t *testing.T) {
   343  	ctx := context.Background()
   344  	reg := prometheus.NewPedanticRegistry()
   345  	bkt, _ := objstore_testutil.NewFilesystemBucket(t, ctx, t.TempDir())
   346  
   347  	// Create a bucket index.
   348  	idx := &Index{
   349  		Version: IndexVersion1,
   350  		Blocks: Blocks{
   351  			{ID: ulid.MustNew(1, nil), MinTime: 10, MaxTime: 20},
   352  		},
   353  		BlockDeletionMarks: nil,
   354  		UpdatedAt:          time.Now().Unix(),
   355  	}
   356  	require.NoError(t, WriteIndex(ctx, bkt, "user-1", nil, idx))
   357  
   358  	// Create the loader.
   359  	cfg := LoaderConfig{
   360  		CheckInterval:         time.Second,
   361  		UpdateOnStaleInterval: time.Second,
   362  		UpdateOnErrorInterval: time.Second,
   363  		IdleTimeout:           time.Hour, // Intentionally high to not hit it.
   364  	}
   365  
   366  	loader := NewLoader(cfg, bkt, nil, log.NewNopLogger(), reg)
   367  	require.NoError(t, services.StartAndAwaitRunning(ctx, loader))
   368  	t.Cleanup(func() {
   369  		require.NoError(t, services.StopAndAwaitTerminated(ctx, loader))
   370  	})
   371  
   372  	actualIdx, err := loader.GetIndex(ctx, "user-1")
   373  	require.NoError(t, err)
   374  	assert.Equal(t, idx, actualIdx)
   375  
   376  	// Write a corrupted index.
   377  	require.NoError(t, bkt.Upload(ctx, path.Join("user-1", "phlaredb/", IndexCompressedFilename), strings.NewReader("invalid!}")))
   378  
   379  	// Wait until the first failure has been tracked.
   380  	test.Poll(t, 3*time.Second, true, func() interface{} {
   381  		return testutil.ToFloat64(loader.loadFailures) > 0
   382  	})
   383  
   384  	actualIdx, err = loader.GetIndex(ctx, "user-1")
   385  	require.NoError(t, err)
   386  	assert.Equal(t, idx, actualIdx)
   387  
   388  	// Ensure metrics have been updated accordingly.
   389  	assert.NoError(t, testutil.GatherAndCompare(reg, bytes.NewBufferString(`
   390  		# HELP pyroscope_bucket_index_loaded Number of bucket indexes currently loaded in-memory.
   391  		# TYPE pyroscope_bucket_index_loaded gauge
   392  		pyroscope_bucket_index_loaded 1
   393  	`),
   394  		"pyroscope_bucket_index_loaded",
   395  	))
   396  }
   397  
   398  func TestLoader_ShouldCacheIndexNotFoundOnBackgroundUpdates(t *testing.T) {
   399  	ctx := context.Background()
   400  	reg := prometheus.NewPedanticRegistry()
   401  	bkt, _ := objstore_testutil.NewFilesystemBucket(t, ctx, t.TempDir())
   402  
   403  	// Create a bucket index.
   404  	idx := &Index{
   405  		Version: IndexVersion1,
   406  		Blocks: Blocks{
   407  			{ID: ulid.MustNew(1, nil), MinTime: 10, MaxTime: 20},
   408  		},
   409  		BlockDeletionMarks: nil,
   410  		UpdatedAt:          time.Now().Unix(),
   411  	}
   412  	require.NoError(t, WriteIndex(ctx, bkt, "user-1", nil, idx))
   413  
   414  	// Create the loader.
   415  	cfg := LoaderConfig{
   416  		CheckInterval:         time.Second,
   417  		UpdateOnStaleInterval: time.Second,
   418  		UpdateOnErrorInterval: time.Second,
   419  		IdleTimeout:           time.Hour, // Intentionally high to not hit it.
   420  	}
   421  
   422  	loader := NewLoader(cfg, bkt, nil, log.NewNopLogger(), reg)
   423  	require.NoError(t, services.StartAndAwaitRunning(ctx, loader))
   424  	t.Cleanup(func() {
   425  		require.NoError(t, services.StopAndAwaitTerminated(ctx, loader))
   426  	})
   427  
   428  	actualIdx, err := loader.GetIndex(ctx, "user-1")
   429  	require.NoError(t, err)
   430  	assert.Equal(t, idx, actualIdx)
   431  
   432  	// Delete the bucket index.
   433  	require.NoError(t, DeleteIndex(ctx, bkt, "user-1", nil))
   434  
   435  	// We expect the bucket index is not considered loaded because of the error.
   436  	test.Poll(t, 3*time.Second, nil, func() any {
   437  		return testutil.GatherAndCompare(reg, bytes.NewBufferString(`
   438  			# HELP pyroscope_bucket_index_loaded Number of bucket indexes currently loaded in-memory.
   439  			# TYPE pyroscope_bucket_index_loaded gauge
   440  			pyroscope_bucket_index_loaded 0
   441  		`),
   442  			"pyroscope_bucket_index_loaded",
   443  		)
   444  	})
   445  
   446  	// Try to get the index again. We expect no load attempt because the error has been cached.
   447  	test.Poll(t, 3*time.Second, true, func() any {
   448  		prevLoads := testutil.ToFloat64(loader.loadAttempts)
   449  		actualIdx, err = loader.GetIndex(ctx, "user-1")
   450  		loadAttemps := testutil.ToFloat64(loader.loadAttempts)
   451  		assert.Equal(t, ErrIndexNotFound, err)
   452  		assert.Nil(t, actualIdx)
   453  		return prevLoads == loadAttemps
   454  	})
   455  }
   456  
   457  func TestLoader_ShouldOffloadIndexIfNotFoundDuringBackgroundUpdates(t *testing.T) {
   458  	ctx := context.Background()
   459  	reg := prometheus.NewPedanticRegistry()
   460  	bkt, _ := objstore_testutil.NewFilesystemBucket(t, ctx, t.TempDir())
   461  
   462  	// Create a bucket index.
   463  	idx := &Index{
   464  		Version: IndexVersion1,
   465  		Blocks: Blocks{
   466  			{ID: ulid.MustNew(1, nil), MinTime: 10, MaxTime: 20},
   467  		},
   468  		BlockDeletionMarks: nil,
   469  		UpdatedAt:          time.Now().Unix(),
   470  	}
   471  	require.NoError(t, WriteIndex(ctx, bkt, "user-1", nil, idx))
   472  
   473  	// Create the loader.
   474  	cfg := LoaderConfig{
   475  		CheckInterval:         time.Second,
   476  		UpdateOnStaleInterval: time.Second,
   477  		UpdateOnErrorInterval: time.Second,
   478  		IdleTimeout:           time.Hour, // Intentionally high to not hit it.
   479  	}
   480  
   481  	loader := NewLoader(cfg, bkt, nil, log.NewNopLogger(), reg)
   482  	require.NoError(t, services.StartAndAwaitRunning(ctx, loader))
   483  	t.Cleanup(func() {
   484  		require.NoError(t, services.StopAndAwaitTerminated(ctx, loader))
   485  	})
   486  
   487  	actualIdx, err := loader.GetIndex(ctx, "user-1")
   488  	require.NoError(t, err)
   489  	assert.Equal(t, idx, actualIdx)
   490  
   491  	// Delete the index
   492  	require.NoError(t, DeleteIndex(ctx, bkt, "user-1", nil))
   493  
   494  	// Wait until the index is offloaded.
   495  	test.Poll(t, 3*time.Second, float64(0), func() interface{} {
   496  		return testutil.ToFloat64(loader.loaded)
   497  	})
   498  
   499  	_, err = loader.GetIndex(ctx, "user-1")
   500  	require.Equal(t, ErrIndexNotFound, err)
   501  
   502  	// Ensure metrics have been updated accordingly.
   503  	assert.NoError(t, testutil.GatherAndCompare(reg, bytes.NewBufferString(`
   504  		# HELP pyroscope_bucket_index_loaded Number of bucket indexes currently loaded in-memory.
   505  		# TYPE pyroscope_bucket_index_loaded gauge
   506  		pyroscope_bucket_index_loaded 0
   507  	`),
   508  		"pyroscope_bucket_index_loaded",
   509  	))
   510  }
   511  
   512  func TestLoader_ShouldOffloadIndexIfIdleTimeoutIsReachedDuringBackgroundUpdates(t *testing.T) {
   513  	ctx := context.Background()
   514  	reg := prometheus.NewPedanticRegistry()
   515  	bkt, _ := objstore_testutil.NewFilesystemBucket(t, ctx, t.TempDir())
   516  
   517  	// Create a bucket index.
   518  	idx := &Index{
   519  		Version: IndexVersion1,
   520  		Blocks: Blocks{
   521  			{ID: ulid.MustNew(1, nil), MinTime: 10, MaxTime: 20},
   522  		},
   523  		BlockDeletionMarks: nil,
   524  		UpdatedAt:          time.Now().Unix(),
   525  	}
   526  	require.NoError(t, WriteIndex(ctx, bkt, "user-1", nil, idx))
   527  
   528  	// Create the loader.
   529  	cfg := LoaderConfig{
   530  		CheckInterval:         time.Second,
   531  		UpdateOnStaleInterval: time.Second,
   532  		UpdateOnErrorInterval: time.Second,
   533  		IdleTimeout:           0, // Offload at first check.
   534  	}
   535  
   536  	loader := NewLoader(cfg, bkt, nil, log.NewNopLogger(), reg)
   537  	require.NoError(t, services.StartAndAwaitRunning(ctx, loader))
   538  	t.Cleanup(func() {
   539  		require.NoError(t, services.StopAndAwaitTerminated(ctx, loader))
   540  	})
   541  
   542  	actualIdx, err := loader.GetIndex(ctx, "user-1")
   543  	require.NoError(t, err)
   544  	assert.Equal(t, idx, actualIdx)
   545  
   546  	// Wait until the index is offloaded.
   547  	test.Poll(t, 3*time.Second, float64(0), func() interface{} {
   548  		return testutil.ToFloat64(loader.loaded)
   549  	})
   550  
   551  	// Ensure metrics have been updated accordingly.
   552  	assert.NoError(t, testutil.GatherAndCompare(reg, bytes.NewBufferString(`
   553  		# HELP pyroscope_bucket_index_loaded Number of bucket indexes currently loaded in-memory.
   554  		# TYPE pyroscope_bucket_index_loaded gauge
   555  		pyroscope_bucket_index_loaded 0
   556  		# HELP pyroscope_bucket_index_loads_total Total number of bucket index loading attempts.
   557  		# TYPE pyroscope_bucket_index_loads_total counter
   558  		pyroscope_bucket_index_loads_total 1
   559  	`),
   560  		"pyroscope_bucket_index_loaded",
   561  		"pyroscope_bucket_index_loads_total",
   562  	))
   563  
   564  	// Load it again.
   565  	actualIdx, err = loader.GetIndex(ctx, "user-1")
   566  	require.NoError(t, err)
   567  	assert.Equal(t, idx, actualIdx)
   568  
   569  	// Ensure metrics have been updated accordingly.
   570  	assert.NoError(t, testutil.GatherAndCompare(reg, bytes.NewBufferString(`
   571  		# HELP pyroscope_bucket_index_loads_total Total number of bucket index loading attempts.
   572  		# TYPE pyroscope_bucket_index_loads_total counter
   573  		pyroscope_bucket_index_loads_total 2
   574  	`),
   575  		"pyroscope_bucket_index_loads_total",
   576  	))
   577  }
   578  
   579  func prepareLoaderConfig() LoaderConfig {
   580  	return LoaderConfig{
   581  		CheckInterval:         time.Minute,
   582  		UpdateOnStaleInterval: 15 * time.Minute,
   583  		UpdateOnErrorInterval: time.Minute,
   584  		IdleTimeout:           time.Hour,
   585  	}
   586  }