github.com/grafana/pyroscope@v1.18.0/pkg/compactor/compactor_test.go (about)

     1  // SPDX-License-Identifier: AGPL-3.0-only
     2  // Provenance-includes-location: https://github.com/grafana/mimir/blob/main/pkg/compactor/compactor_test.go
     3  // Provenance-includes-license: Apache-2.0
     4  // Provenance-includes-copyright: The Cortex Authors.
     5  
     6  package compactor
     7  
     8  import (
     9  	"bytes"
    10  	"context"
    11  	"crypto/rand"
    12  	"encoding/json"
    13  	"flag"
    14  	"fmt"
    15  	"os"
    16  	"path"
    17  	"path/filepath"
    18  	"strconv"
    19  	"strings"
    20  	"testing"
    21  	"time"
    22  
    23  	"github.com/go-kit/log"
    24  	"github.com/grafana/dskit/concurrency"
    25  	"github.com/grafana/dskit/flagext"
    26  	"github.com/grafana/dskit/kv/consul"
    27  	"github.com/grafana/dskit/ring"
    28  	"github.com/grafana/dskit/services"
    29  	"github.com/grafana/dskit/test"
    30  	"github.com/oklog/ulid/v2"
    31  	"github.com/pkg/errors"
    32  	"github.com/prometheus/client_golang/prometheus"
    33  	prom_testutil "github.com/prometheus/client_golang/prometheus/testutil"
    34  	"github.com/prometheus/common/model"
    35  	"github.com/stretchr/testify/assert"
    36  	"github.com/stretchr/testify/mock"
    37  	"github.com/stretchr/testify/require"
    38  	"github.com/thanos-io/objstore"
    39  	"gopkg.in/yaml.v3"
    40  
    41  	pyroscope_objstore "github.com/grafana/pyroscope/pkg/objstore"
    42  	"github.com/grafana/pyroscope/pkg/objstore/providers/filesystem"
    43  	"github.com/grafana/pyroscope/pkg/phlaredb/block"
    44  	"github.com/grafana/pyroscope/pkg/phlaredb/block/testutil"
    45  	"github.com/grafana/pyroscope/pkg/phlaredb/bucket"
    46  	"github.com/grafana/pyroscope/pkg/pprof/testhelper"
    47  	"github.com/grafana/pyroscope/pkg/test/mocks/mockobjstore"
    48  	"github.com/grafana/pyroscope/pkg/validation"
    49  )
    50  
    51  const (
    52  	instanceID = "compactor-1"
    53  	addr       = "1.2.3.4"
    54  )
    55  
    56  func TestConfig_ShouldSupportYamlConfig(t *testing.T) {
    57  	yamlCfg := `
    58  block_ranges: [2h, 48h]
    59  block_sync_concurrency: 123
    60  data_dir: /tmp
    61  compaction_interval: 15m
    62  compaction_retries: 123
    63  `
    64  
    65  	cfg := Config{}
    66  	flagext.DefaultValues(&cfg)
    67  	assert.NoError(t, yaml.Unmarshal([]byte(yamlCfg), &cfg))
    68  	assert.Equal(t, DurationList{2 * time.Hour, 48 * time.Hour}, cfg.BlockRanges)
    69  	assert.Equal(t, 123, cfg.BlockSyncConcurrency)
    70  	assert.Equal(t, "/tmp", cfg.DataDir)
    71  	assert.Equal(t, 15*time.Minute, cfg.CompactionInterval)
    72  	assert.Equal(t, 123, cfg.CompactionRetries)
    73  }
    74  
    75  func TestConfig_ShouldSupportCliFlags(t *testing.T) {
    76  	fs := flag.NewFlagSet("", flag.PanicOnError)
    77  	cfg := Config{}
    78  	cfg.RegisterFlags(fs, log.NewNopLogger())
    79  	require.NoError(t, fs.Parse([]string{
    80  		"-compactor.block-ranges=2h,48h",
    81  		"-compactor.block-sync-concurrency=123",
    82  		"-compactor.data-dir=/tmp",
    83  		"-compactor.compaction-interval=15m",
    84  		"-compactor.compaction-retries=123",
    85  	}))
    86  
    87  	assert.Equal(t, DurationList{2 * time.Hour, 48 * time.Hour}, cfg.BlockRanges)
    88  	assert.Equal(t, 123, cfg.BlockSyncConcurrency)
    89  	assert.Equal(t, "/tmp", cfg.DataDir)
    90  	assert.Equal(t, 15*time.Minute, cfg.CompactionInterval)
    91  	assert.Equal(t, 123, cfg.CompactionRetries)
    92  }
    93  
    94  func TestConfig_Validate(t *testing.T) {
    95  	tests := map[string]struct {
    96  		setup    func(cfg *Config)
    97  		expected string
    98  		maxBlock time.Duration
    99  	}{
   100  		"should pass with the default config": {
   101  			setup:    func(cfg *Config) {},
   102  			expected: "",
   103  			maxBlock: 1 * time.Hour,
   104  		},
   105  		"should pass with only 1 block range period": {
   106  			setup: func(cfg *Config) {
   107  				cfg.BlockRanges = DurationList{time.Hour}
   108  			},
   109  			expected: "",
   110  			maxBlock: 1 * time.Hour,
   111  		},
   112  		"should fail with non divisible block range periods": {
   113  			setup: func(cfg *Config) {
   114  				cfg.BlockRanges = DurationList{2 * time.Hour, 12 * time.Hour, 24 * time.Hour, 30 * time.Hour}
   115  			},
   116  			expected: errors.Errorf(errInvalidBlockRanges, 30*time.Hour, 24*time.Hour).Error(),
   117  			maxBlock: 2 * time.Hour,
   118  		},
   119  		"should fail on unknown compaction jobs order": {
   120  			setup: func(cfg *Config) {
   121  				cfg.CompactionJobsOrder = "everything-is-important"
   122  			},
   123  			expected: errInvalidCompactionOrder.Error(),
   124  			maxBlock: 1 * time.Hour,
   125  		},
   126  		"should fail on invalid value of max-opening-blocks-concurrency": {
   127  			setup:    func(cfg *Config) { cfg.MaxOpeningBlocksConcurrency = 0 },
   128  			expected: errInvalidMaxOpeningBlocksConcurrency.Error(),
   129  			maxBlock: 1 * time.Hour,
   130  		},
   131  		"should fail on first range not divisible by max block duration": {
   132  			setup: func(cfg *Config) {
   133  				cfg.BlockRanges = DurationList{2 * time.Hour, 12 * time.Hour, 24 * time.Hour}
   134  			},
   135  			expected: errors.Errorf(errInvalidBlockDuration, (2 * time.Hour).String(), (15 * time.Hour).String()).Error(),
   136  			maxBlock: 15 * time.Hour,
   137  		},
   138  	}
   139  
   140  	for testName, testData := range tests {
   141  		t.Run(testName, func(t *testing.T) {
   142  			cfg := &Config{}
   143  			flagext.DefaultValues(cfg)
   144  			testData.setup(cfg)
   145  
   146  			if actualErr := cfg.Validate(testData.maxBlock); testData.expected != "" {
   147  				assert.EqualError(t, actualErr, testData.expected)
   148  			} else {
   149  				assert.NoError(t, actualErr)
   150  			}
   151  		})
   152  	}
   153  }
   154  
   155  func TestMultitenantCompactor_ShouldDoNothingOnNoUserBlocks(t *testing.T) {
   156  	t.Parallel()
   157  
   158  	// No user blocks stored in the bucket.
   159  	bucketClient := mockobjstore.NewMockBucketWithHelper(t)
   160  	bucketClient.MockIter("", []string{}, nil)
   161  	cfg := prepareConfig(t)
   162  	c, _, _, logs, registry := prepare(t, cfg, bucketClient)
   163  	require.NoError(t, services.StartAndAwaitRunning(context.Background(), c))
   164  
   165  	// Compactor doesn't wait for blocks cleaner to finish, but our test checks for cleaner metrics.
   166  	require.NoError(t, c.blocksCleaner.AwaitRunning(context.Background()))
   167  
   168  	// Wait until a run has completed.
   169  	test.Poll(t, time.Second, 1.0, func() interface{} {
   170  		return prom_testutil.ToFloat64(c.compactionRunsCompleted)
   171  	})
   172  
   173  	require.NoError(t, services.StopAndAwaitTerminated(context.Background(), c))
   174  
   175  	assert.Equal(t, prom_testutil.ToFloat64(c.compactionRunInterval), cfg.CompactionInterval.Seconds())
   176  
   177  	assert.Equal(t, []string{
   178  		`level=info component=compactor msg="waiting until compactor is ACTIVE in the ring"`,
   179  		`level=info component=compactor msg="compactor is ACTIVE in the ring"`,
   180  		`level=info component=compactor msg="discovering users from bucket"`,
   181  		`level=info component=compactor msg="discovered users from bucket" users=0`,
   182  	}, removeIgnoredLogs(strings.Split(strings.TrimSpace(logs.String()), "\n")))
   183  
   184  	assert.NoError(t, prom_testutil.GatherAndCompare(registry, strings.NewReader(`
   185  		# TYPE pyroscope_compactor_runs_started_total counter
   186  		# HELP pyroscope_compactor_runs_started_total Total number of compaction runs started.
   187  		pyroscope_compactor_runs_started_total 1
   188  
   189  		# TYPE pyroscope_compactor_runs_completed_total counter
   190  		# HELP pyroscope_compactor_runs_completed_total Total number of compaction runs successfully completed.
   191  		pyroscope_compactor_runs_completed_total 1
   192  
   193  		# TYPE pyroscope_compactor_runs_failed_total counter
   194  		# HELP pyroscope_compactor_runs_failed_total Total number of compaction runs failed.
   195  		pyroscope_compactor_runs_failed_total{reason="error"} 0
   196  		pyroscope_compactor_runs_failed_total{reason="shutdown"} 0
   197  
   198  		# HELP pyroscope_compactor_garbage_collection_duration_seconds Time it took to perform garbage collection iteration.
   199  		# TYPE pyroscope_compactor_garbage_collection_duration_seconds histogram
   200  		pyroscope_compactor_garbage_collection_duration_seconds_bucket{le="+Inf"} 0
   201  		pyroscope_compactor_garbage_collection_duration_seconds_sum 0
   202  		pyroscope_compactor_garbage_collection_duration_seconds_count 0
   203  
   204  		# HELP pyroscope_compactor_garbage_collection_failures_total Total number of failed garbage collection operations.
   205  		# TYPE pyroscope_compactor_garbage_collection_failures_total counter
   206  		pyroscope_compactor_garbage_collection_failures_total 0
   207  
   208  		# HELP pyroscope_compactor_garbage_collection_total Total number of garbage collection operations.
   209  		# TYPE pyroscope_compactor_garbage_collection_total counter
   210  		pyroscope_compactor_garbage_collection_total 0
   211  
   212  		# HELP pyroscope_compactor_meta_sync_duration_seconds Duration of the blocks metadata synchronization in seconds.
   213  		# TYPE pyroscope_compactor_meta_sync_duration_seconds histogram
   214  		pyroscope_compactor_meta_sync_duration_seconds_bucket{le="+Inf"} 0
   215  		pyroscope_compactor_meta_sync_duration_seconds_sum 0
   216  		pyroscope_compactor_meta_sync_duration_seconds_count 0
   217  
   218  		# HELP pyroscope_compactor_meta_sync_failures_total Total blocks metadata synchronization failures.
   219  		# TYPE pyroscope_compactor_meta_sync_failures_total counter
   220  		pyroscope_compactor_meta_sync_failures_total 0
   221  
   222  		# HELP pyroscope_compactor_meta_syncs_total Total blocks metadata synchronization attempts.
   223  		# TYPE pyroscope_compactor_meta_syncs_total counter
   224  		pyroscope_compactor_meta_syncs_total 0
   225  
   226  		# HELP pyroscope_compactor_group_compaction_runs_completed_total Total number of group completed compaction runs. This also includes compactor group runs that resulted with no compaction.
   227  		# TYPE pyroscope_compactor_group_compaction_runs_completed_total counter
   228  		pyroscope_compactor_group_compaction_runs_completed_total 0
   229  
   230  		# HELP pyroscope_compactor_group_compaction_runs_started_total Total number of group compaction attempts.
   231  		# TYPE pyroscope_compactor_group_compaction_runs_started_total counter
   232  		pyroscope_compactor_group_compaction_runs_started_total 0
   233  
   234  		# HELP pyroscope_compactor_group_compactions_failures_total Total number of failed group compactions.
   235  		# TYPE pyroscope_compactor_group_compactions_failures_total counter
   236  		pyroscope_compactor_group_compactions_failures_total 0
   237  
   238  		# HELP pyroscope_compactor_group_compactions_total Total number of group compaction attempts that resulted in new block(s).
   239  		# TYPE pyroscope_compactor_group_compactions_total counter
   240  		pyroscope_compactor_group_compactions_total 0
   241  
   242  		# TYPE pyroscope_compactor_block_cleanup_failures_total counter
   243  		# HELP pyroscope_compactor_block_cleanup_failures_total Total number of blocks failed to be deleted.
   244  		pyroscope_compactor_block_cleanup_failures_total 0
   245  
   246  		# HELP pyroscope_compactor_blocks_cleaned_total Total number of blocks deleted.
   247  		# TYPE pyroscope_compactor_blocks_cleaned_total counter
   248  		pyroscope_compactor_blocks_cleaned_total 0
   249  
   250  		# HELP pyroscope_compactor_blocks_marked_for_deletion_total Total number of blocks marked for deletion in compactor.
   251  		# TYPE pyroscope_compactor_blocks_marked_for_deletion_total counter
   252  		pyroscope_compactor_blocks_marked_for_deletion_total{reason="compaction"} 0
   253  		pyroscope_compactor_blocks_marked_for_deletion_total{reason="partial"} 0
   254  		pyroscope_compactor_blocks_marked_for_deletion_total{reason="retention"} 0
   255  
   256  		# TYPE pyroscope_compactor_block_cleanup_started_total counter
   257  		# HELP pyroscope_compactor_block_cleanup_started_total Total number of blocks cleanup runs started.
   258  		pyroscope_compactor_block_cleanup_started_total 1
   259  
   260  		# TYPE pyroscope_compactor_block_cleanup_completed_total counter
   261  		# HELP pyroscope_compactor_block_cleanup_completed_total Total number of blocks cleanup runs successfully completed.
   262  		pyroscope_compactor_block_cleanup_completed_total 1
   263  
   264  		# TYPE pyroscope_compactor_block_cleanup_failed_total counter
   265  		# HELP pyroscope_compactor_block_cleanup_failed_total Total number of blocks cleanup runs failed.
   266  		pyroscope_compactor_block_cleanup_failed_total 0
   267  	`),
   268  		"pyroscope_compactor_runs_started_total",
   269  		"pyroscope_compactor_runs_completed_total",
   270  		"pyroscope_compactor_runs_failed_total",
   271  		"pyroscope_compactor_garbage_collection_duration_seconds",
   272  		"pyroscope_compactor_garbage_collection_failures_total",
   273  		"pyroscope_compactor_garbage_collection_total",
   274  		"pyroscope_compactor_meta_sync_duration_seconds",
   275  		"pyroscope_compactor_meta_sync_failures_total",
   276  		"pyroscope_compactor_meta_syncs_total",
   277  		"pyroscope_compactor_group_compaction_runs_completed_total",
   278  		"pyroscope_compactor_group_compaction_runs_started_total",
   279  		"pyroscope_compactor_group_compactions_failures_total",
   280  		"pyroscope_compactor_group_compactions_total",
   281  		"pyroscope_compactor_block_cleanup_failures_total",
   282  		"pyroscope_compactor_blocks_cleaned_total",
   283  		"pyroscope_compactor_blocks_marked_for_deletion_total",
   284  		"pyroscope_compactor_block_cleanup_started_total",
   285  		"pyroscope_compactor_block_cleanup_completed_total",
   286  		"pyroscope_compactor_block_cleanup_failed_total",
   287  	))
   288  }
   289  
   290  func TestMultitenantCompactor_ShouldRetryCompactionOnFailureWhileDiscoveringUsersFromBucket(t *testing.T) {
   291  	t.Parallel()
   292  
   293  	// Fail to iterate over the bucket while discovering users.
   294  	bucketClient := mockobjstore.NewMockBucketWithHelper(t)
   295  	bucketClient.MockIter("", nil, errors.New("failed to iterate the bucket"))
   296  
   297  	c, _, _, logs, registry := prepare(t, prepareConfig(t), bucketClient)
   298  	require.NoError(t, services.StartAndAwaitRunning(context.Background(), c))
   299  
   300  	// Compactor doesn't wait for blocks cleaner to finish, but our test checks for cleaner metrics.
   301  	require.NoError(t, c.blocksCleaner.AwaitRunning(context.Background()))
   302  	t.Cleanup(func() {
   303  		t.Log(logs.String())
   304  	})
   305  
   306  	// Wait until all retry attempts have completed.
   307  	test.Poll(t, time.Second, 1.0, func() interface{} {
   308  		return prom_testutil.ToFloat64(c.compactionRunsErred)
   309  	})
   310  
   311  	require.NoError(t, services.StopAndAwaitTerminated(context.Background(), c))
   312  
   313  	// Ensure the bucket iteration has been retried the configured number of times.
   314  	bucketClient.AssertNumberOfCalls(t, "Iter", 1+3)
   315  
   316  	assert.Equal(t, []string{
   317  		`level=info component=compactor msg="waiting until compactor is ACTIVE in the ring"`,
   318  		`level=info component=compactor msg="compactor is ACTIVE in the ring"`,
   319  		`level=info component=compactor msg="discovering users from bucket"`,
   320  		`level=error component=compactor msg="failed to discover users from bucket" err="failed to iterate the bucket"`,
   321  	}, removeIgnoredLogs(strings.Split(strings.TrimSpace(logs.String()), "\n")))
   322  
   323  	assert.NoError(t, prom_testutil.GatherAndCompare(registry, strings.NewReader(`
   324  		# TYPE pyroscope_compactor_runs_started_total counter
   325  		# HELP pyroscope_compactor_runs_started_total Total number of compaction runs started.
   326  		pyroscope_compactor_runs_started_total 1
   327  
   328  		# TYPE pyroscope_compactor_runs_completed_total counter
   329  		# HELP pyroscope_compactor_runs_completed_total Total number of compaction runs successfully completed.
   330  		pyroscope_compactor_runs_completed_total 0
   331  
   332  		# TYPE pyroscope_compactor_runs_failed_total counter
   333  		# HELP pyroscope_compactor_runs_failed_total Total number of compaction runs failed.
   334  		pyroscope_compactor_runs_failed_total{reason="error"} 1
   335  		pyroscope_compactor_runs_failed_total{reason="shutdown"} 0
   336  
   337  		# HELP pyroscope_compactor_garbage_collection_duration_seconds Time it took to perform garbage collection iteration.
   338  		# TYPE pyroscope_compactor_garbage_collection_duration_seconds histogram
   339  		pyroscope_compactor_garbage_collection_duration_seconds_bucket{le="+Inf"} 0
   340  		pyroscope_compactor_garbage_collection_duration_seconds_sum 0
   341  		pyroscope_compactor_garbage_collection_duration_seconds_count 0
   342  
   343  		# HELP pyroscope_compactor_garbage_collection_failures_total Total number of failed garbage collection operations.
   344  		# TYPE pyroscope_compactor_garbage_collection_failures_total counter
   345  		pyroscope_compactor_garbage_collection_failures_total 0
   346  
   347  		# HELP pyroscope_compactor_garbage_collection_total Total number of garbage collection operations.
   348  		# TYPE pyroscope_compactor_garbage_collection_total counter
   349  		pyroscope_compactor_garbage_collection_total 0
   350  
   351  		# HELP pyroscope_compactor_meta_sync_duration_seconds Duration of the blocks metadata synchronization in seconds.
   352  		# TYPE pyroscope_compactor_meta_sync_duration_seconds histogram
   353  		pyroscope_compactor_meta_sync_duration_seconds_bucket{le="+Inf"} 0
   354  		pyroscope_compactor_meta_sync_duration_seconds_sum 0
   355  		pyroscope_compactor_meta_sync_duration_seconds_count 0
   356  
   357  		# HELP pyroscope_compactor_meta_sync_failures_total Total blocks metadata synchronization failures.
   358  		# TYPE pyroscope_compactor_meta_sync_failures_total counter
   359  		pyroscope_compactor_meta_sync_failures_total 0
   360  
   361  		# HELP pyroscope_compactor_meta_syncs_total Total blocks metadata synchronization attempts.
   362  		# TYPE pyroscope_compactor_meta_syncs_total counter
   363  		pyroscope_compactor_meta_syncs_total 0
   364  
   365  		# HELP pyroscope_compactor_group_compaction_runs_completed_total Total number of group completed compaction runs. This also includes compactor group runs that resulted with no compaction.
   366  		# TYPE pyroscope_compactor_group_compaction_runs_completed_total counter
   367  		pyroscope_compactor_group_compaction_runs_completed_total 0
   368  
   369  		# HELP pyroscope_compactor_group_compaction_runs_started_total Total number of group compaction attempts.
   370  		# TYPE pyroscope_compactor_group_compaction_runs_started_total counter
   371  		pyroscope_compactor_group_compaction_runs_started_total 0
   372  
   373  		# HELP pyroscope_compactor_group_compactions_failures_total Total number of failed group compactions.
   374  		# TYPE pyroscope_compactor_group_compactions_failures_total counter
   375  		pyroscope_compactor_group_compactions_failures_total 0
   376  
   377  		# HELP pyroscope_compactor_group_compactions_total Total number of group compaction attempts that resulted in new block(s).
   378  		# TYPE pyroscope_compactor_group_compactions_total counter
   379  		pyroscope_compactor_group_compactions_total 0
   380  
   381  		# TYPE pyroscope_compactor_block_cleanup_failures_total counter
   382  		# HELP pyroscope_compactor_block_cleanup_failures_total Total number of blocks failed to be deleted.
   383  		pyroscope_compactor_block_cleanup_failures_total 0
   384  
   385  		# HELP pyroscope_compactor_blocks_cleaned_total Total number of blocks deleted.
   386  		# TYPE pyroscope_compactor_blocks_cleaned_total counter
   387  		pyroscope_compactor_blocks_cleaned_total 0
   388  
   389  		# HELP pyroscope_compactor_blocks_marked_for_deletion_total Total number of blocks marked for deletion in compactor.
   390  		# TYPE pyroscope_compactor_blocks_marked_for_deletion_total counter
   391  		pyroscope_compactor_blocks_marked_for_deletion_total{reason="compaction"} 0
   392  		pyroscope_compactor_blocks_marked_for_deletion_total{reason="partial"} 0
   393  		pyroscope_compactor_blocks_marked_for_deletion_total{reason="retention"} 0
   394  
   395  		# TYPE pyroscope_compactor_block_cleanup_started_total counter
   396  		# HELP pyroscope_compactor_block_cleanup_started_total Total number of blocks cleanup runs started.
   397  		pyroscope_compactor_block_cleanup_started_total 1
   398  
   399  		# TYPE pyroscope_compactor_block_cleanup_completed_total counter
   400  		# HELP pyroscope_compactor_block_cleanup_completed_total Total number of blocks cleanup runs successfully completed.
   401  		pyroscope_compactor_block_cleanup_completed_total 0
   402  
   403  		# TYPE pyroscope_compactor_block_cleanup_failed_total counter
   404  		# HELP pyroscope_compactor_block_cleanup_failed_total Total number of blocks cleanup runs failed.
   405  		pyroscope_compactor_block_cleanup_failed_total 1
   406  	`),
   407  		"pyroscope_compactor_runs_started_total",
   408  		"pyroscope_compactor_runs_completed_total",
   409  		"pyroscope_compactor_runs_failed_total",
   410  		"pyroscope_compactor_garbage_collection_duration_seconds",
   411  		"pyroscope_compactor_garbage_collection_failures_total",
   412  		"pyroscope_compactor_garbage_collection_total",
   413  		"pyroscope_compactor_meta_sync_duration_seconds",
   414  		"pyroscope_compactor_meta_sync_failures_total",
   415  		"pyroscope_compactor_meta_syncs_total",
   416  		"pyroscope_compactor_group_compaction_runs_completed_total",
   417  		"pyroscope_compactor_group_compaction_runs_started_total",
   418  		"pyroscope_compactor_group_compactions_failures_total",
   419  		"pyroscope_compactor_group_compactions_total",
   420  		"pyroscope_compactor_block_cleanup_failures_total",
   421  		"pyroscope_compactor_blocks_cleaned_total",
   422  		"pyroscope_compactor_blocks_marked_for_deletion_total",
   423  		"pyroscope_compactor_block_cleanup_started_total",
   424  		"pyroscope_compactor_block_cleanup_completed_total",
   425  		"pyroscope_compactor_block_cleanup_failed_total",
   426  	))
   427  }
   428  
   429  func TestMultitenantCompactor_ShouldIncrementCompactionErrorIfFailedToCompactASingleTenant(t *testing.T) {
   430  	t.Parallel()
   431  
   432  	userID := "test-user"
   433  	bucketClient := mockobjstore.NewMockBucketWithHelper(t)
   434  	bucketClient.MockIter("", []string{userID}, nil)
   435  	bucketClient.MockIter(userID+"/phlaredb/", []string{userID + "/phlaredb/01DTVP434PA9VFXSW2JKB3392D", userID + "/phlaredb/01DTW0ZCPDDNV4BV83Q2SV4QAZ"}, nil)
   436  	bucketClient.MockIter(userID+"/phlaredb/markers/", nil, nil)
   437  	bucketClient.MockExists(path.Join(userID, "phlaredb", bucket.TenantDeletionMarkPath), false, nil)
   438  	bucketClient.MockGet(userID+"/phlaredb/01DTVP434PA9VFXSW2JKB3392D/meta.json", mockBlockMetaJSON("01DTVP434PA9VFXSW2JKB3392D"), nil)
   439  	bucketClient.MockGet(userID+"/phlaredb/01DTVP434PA9VFXSW2JKB3392D/deletion-mark.json", "", nil)
   440  	bucketClient.MockGet(userID+"/phlaredb/01DTVP434PA9VFXSW2JKB3392D/no-compact-mark.json", "", nil)
   441  	bucketClient.MockGet(userID+"/phlaredb/01DTW0ZCPDDNV4BV83Q2SV4QAZ/meta.json", mockBlockMetaJSON("01DTW0ZCPDDNV4BV83Q2SV4QAZ"), nil)
   442  	bucketClient.MockGet(userID+"/phlaredb/01DTW0ZCPDDNV4BV83Q2SV4QAZ/deletion-mark.json", "", nil)
   443  	bucketClient.MockGet(userID+"/phlaredb/01DTW0ZCPDDNV4BV83Q2SV4QAZ/no-compact-mark.json", "", nil)
   444  	bucketClient.MockGet(userID+"/phlaredb/bucket-index.json.gz", "", nil)
   445  	bucketClient.MockUpload(userID+"/phlaredb/bucket-index.json.gz", nil)
   446  
   447  	c, _, tsdbPlannerMock, _, registry := prepare(t, prepareConfig(t), bucketClient)
   448  	tsdbPlannerMock.On("Plan", mock.Anything, mock.Anything).Return([]*block.Meta{}, errors.New("Failed to plan"))
   449  	require.NoError(t, services.StartAndAwaitRunning(context.Background(), c))
   450  
   451  	// Wait until all retry attempts have completed.
   452  	test.Poll(t, time.Minute, 1.0, func() interface{} {
   453  		return prom_testutil.ToFloat64(c.compactionRunsErred)
   454  	})
   455  
   456  	require.NoError(t, services.StopAndAwaitTerminated(context.Background(), c))
   457  
   458  	assert.NoError(t, prom_testutil.GatherAndCompare(registry, strings.NewReader(`
   459  		# TYPE pyroscope_compactor_runs_started_total counter
   460  		# HELP pyroscope_compactor_runs_started_total Total number of compaction runs started.
   461  		pyroscope_compactor_runs_started_total 1
   462  
   463  		# TYPE pyroscope_compactor_runs_completed_total counter
   464  		# HELP pyroscope_compactor_runs_completed_total Total number of compaction runs successfully completed.
   465  		pyroscope_compactor_runs_completed_total 0
   466  
   467  		# TYPE pyroscope_compactor_runs_failed_total counter
   468  		# HELP pyroscope_compactor_runs_failed_total Total number of compaction runs failed.
   469  		pyroscope_compactor_runs_failed_total{reason="error"} 1
   470  		pyroscope_compactor_runs_failed_total{reason="shutdown"} 0
   471  	`),
   472  		"pyroscope_compactor_runs_started_total",
   473  		"pyroscope_compactor_runs_completed_total",
   474  		"pyroscope_compactor_runs_failed_total",
   475  	))
   476  }
   477  
   478  func TestMultitenantCompactor_ShouldIncrementCompactionShutdownIfTheContextIsCancelled(t *testing.T) {
   479  	t.Parallel()
   480  
   481  	userID := "test-user"
   482  	bucketClient := mockobjstore.NewMockBucketWithHelper(t)
   483  	bucketClient.MockIter("", []string{userID}, nil)
   484  	bucketClient.MockIter(userID+"/phlaredb/markers/", nil, nil)
   485  	bucketClient.MockIter(userID+"/phlaredb/", []string{userID + "/phlaredb/01DTVP434PA9VFXSW2JKB3392D", userID + "/phlaredb/01DTW0ZCPDDNV4BV83Q2SV4QAZ"}, nil)
   486  	bucketClient.MockExists(path.Join(userID, "phlaredb/", bucket.TenantDeletionMarkPath), false, nil)
   487  	bucketClient.MockGet(userID+"/phlaredb/01DTVP434PA9VFXSW2JKB3392D/meta.json", mockBlockMetaJSON("01DTVP434PA9VFXSW2JKB3392D"), nil)
   488  	bucketClient.MockGet(userID+"/phlaredb/01DTVP434PA9VFXSW2JKB3392D/deletion-mark.json", "", nil)
   489  	bucketClient.MockGet(userID+"/phlaredb/01DTVP434PA9VFXSW2JKB3392D/no-compact-mark.json", "", nil)
   490  	bucketClient.MockGet(userID+"/phlaredb/01DTW0ZCPDDNV4BV83Q2SV4QAZ/meta.json", mockBlockMetaJSON("01DTW0ZCPDDNV4BV83Q2SV4QAZ"), nil)
   491  	bucketClient.MockGet(userID+"/phlaredb/01DTW0ZCPDDNV4BV83Q2SV4QAZ/deletion-mark.json", "", nil)
   492  	bucketClient.MockGet(userID+"/phlaredb/01DTW0ZCPDDNV4BV83Q2SV4QAZ/no-compact-mark.json", "", nil)
   493  	bucketClient.MockGet(userID+"/phlaredb/bucket-index.json.gz", "", nil)
   494  	bucketClient.MockUpload(userID+"/phlaredb/bucket-index.json.gz", nil)
   495  
   496  	c, _, tsdbPlannerMock, logs, registry := prepare(t, prepareConfig(t), bucketClient)
   497  	t.Cleanup(func() {
   498  		t.Log(logs.String())
   499  	})
   500  	// Mock the planner as if a shutdown was triggered and the service was terminated.
   501  	tsdbPlannerMock.On("Plan", mock.Anything, mock.Anything).Return([]*block.Meta{}, context.Canceled)
   502  	require.NoError(t, services.StartAndAwaitRunning(context.Background(), c))
   503  
   504  	// Wait until the error is recorded.
   505  	test.Poll(t, time.Second, 1.0, func() interface{} {
   506  		return prom_testutil.ToFloat64(c.compactionRunsShutdown)
   507  	})
   508  
   509  	require.NoError(t, services.StopAndAwaitTerminated(context.Background(), c))
   510  
   511  	assert.NoError(t, prom_testutil.GatherAndCompare(registry, strings.NewReader(`
   512  		# TYPE pyroscope_compactor_runs_started_total counter
   513  		# HELP pyroscope_compactor_runs_started_total Total number of compaction runs started.
   514  		pyroscope_compactor_runs_started_total 1
   515  
   516  		# TYPE pyroscope_compactor_runs_completed_total counter
   517  		# HELP pyroscope_compactor_runs_completed_total Total number of compaction runs successfully completed.
   518  		pyroscope_compactor_runs_completed_total 0
   519  
   520  		# TYPE pyroscope_compactor_runs_failed_total counter
   521  		# HELP pyroscope_compactor_runs_failed_total Total number of compaction runs failed.
   522  		pyroscope_compactor_runs_failed_total{reason="error"} 0
   523  		pyroscope_compactor_runs_failed_total{reason="shutdown"} 1
   524  	`),
   525  		"pyroscope_compactor_runs_started_total",
   526  		"pyroscope_compactor_runs_completed_total",
   527  		"pyroscope_compactor_runs_failed_total",
   528  	))
   529  }
   530  
   531  func TestMultitenantCompactor_ShouldIterateOverUsersAndRunCompaction(t *testing.T) {
   532  	t.Parallel()
   533  
   534  	// Mock the bucket to contain two users, each one with two blocks (to make sure that grouper doesn't skip them).
   535  	bucketClient := mockobjstore.NewMockBucketWithHelper(t)
   536  	bucketClient.MockIter("", []string{"user-1", "user-2"}, nil)
   537  	bucketClient.MockExists(path.Join("user-1", "phlaredb/", bucket.TenantDeletionMarkPath), false, nil)
   538  	bucketClient.MockExists(path.Join("user-2", "phlaredb/", bucket.TenantDeletionMarkPath), false, nil)
   539  	bucketClient.MockIter("user-1/phlaredb/", []string{"user-1/phlaredb/01DTVP434PA9VFXSW2JKB3392D", "user-1/phlaredb/01FS51A7GQ1RQWV35DBVYQM4KF"}, nil)
   540  	bucketClient.MockIter("user-2/phlaredb/", []string{"user-2/phlaredb/01DTW0ZCPDDNV4BV83Q2SV4QAZ", "user-2/phlaredb/01FRSF035J26D6CGX7STCSD1KG"}, nil)
   541  	bucketClient.MockGet("user-1/phlaredb/01DTVP434PA9VFXSW2JKB3392D/meta.json", mockBlockMetaJSON("01DTVP434PA9VFXSW2JKB3392D"), nil)
   542  	bucketClient.MockGet("user-1/phlaredb/01DTVP434PA9VFXSW2JKB3392D/deletion-mark.json", "", nil)
   543  	bucketClient.MockGet("user-1/phlaredb/01DTVP434PA9VFXSW2JKB3392D/no-compact-mark.json", "", nil)
   544  	bucketClient.MockGet("user-1/phlaredb/01FS51A7GQ1RQWV35DBVYQM4KF/meta.json", mockBlockMetaJSON("01FS51A7GQ1RQWV35DBVYQM4KF"), nil)
   545  	bucketClient.MockGet("user-1/phlaredb/01FS51A7GQ1RQWV35DBVYQM4KF/deletion-mark.json", "", nil)
   546  	bucketClient.MockGet("user-1/phlaredb/01FS51A7GQ1RQWV35DBVYQM4KF/no-compact-mark.json", "", nil)
   547  
   548  	bucketClient.MockGet("user-2/phlaredb/01DTW0ZCPDDNV4BV83Q2SV4QAZ/meta.json", mockBlockMetaJSON("01DTW0ZCPDDNV4BV83Q2SV4QAZ"), nil)
   549  	bucketClient.MockGet("user-2/phlaredb/01DTW0ZCPDDNV4BV83Q2SV4QAZ/deletion-mark.json", "", nil)
   550  	bucketClient.MockGet("user-2/phlaredb/01DTW0ZCPDDNV4BV83Q2SV4QAZ/no-compact-mark.json", "", nil)
   551  	bucketClient.MockGet("user-2/phlaredb/01FRSF035J26D6CGX7STCSD1KG/meta.json", mockBlockMetaJSON("01FRSF035J26D6CGX7STCSD1KG"), nil)
   552  	bucketClient.MockGet("user-2/phlaredb/01FRSF035J26D6CGX7STCSD1KG/deletion-mark.json", "", nil)
   553  	bucketClient.MockGet("user-2/phlaredb/01FRSF035J26D6CGX7STCSD1KG/no-compact-mark.json", "", nil)
   554  	bucketClient.MockGet("user-1/phlaredb/bucket-index.json.gz", "", nil)
   555  	bucketClient.MockGet("user-2/phlaredb/bucket-index.json.gz", "", nil)
   556  	bucketClient.MockIter("user-1/phlaredb/markers/", nil, nil)
   557  	bucketClient.MockIter("user-2/phlaredb/markers/", nil, nil)
   558  	bucketClient.MockUpload("user-1/phlaredb/bucket-index.json.gz", nil)
   559  	bucketClient.MockUpload("user-2/phlaredb/bucket-index.json.gz", nil)
   560  
   561  	c, _, tsdbPlanner, logs, registry := prepare(t, prepareConfig(t), bucketClient)
   562  
   563  	// Mock the planner as if there's no compaction to do,
   564  	// in order to simplify tests (all in all, we just want to
   565  	// test our logic and not TSDB compactor which we expect to
   566  	// be already tested).
   567  	tsdbPlanner.On("Plan", mock.Anything, mock.Anything).Return([]*block.Meta{}, nil)
   568  
   569  	require.NoError(t, services.StartAndAwaitRunning(context.Background(), c))
   570  
   571  	// Compactor doesn't wait for blocks cleaner to finish, but our test checks for cleaner metrics.
   572  	require.NoError(t, c.blocksCleaner.AwaitRunning(context.Background()))
   573  
   574  	// Wait until a run has completed.
   575  	test.Poll(t, time.Second, 1.0, func() interface{} {
   576  		return prom_testutil.ToFloat64(c.compactionRunsCompleted)
   577  	})
   578  
   579  	require.NoError(t, services.StopAndAwaitTerminated(context.Background(), c))
   580  
   581  	// Ensure a plan has been executed for the blocks of each user.
   582  	tsdbPlanner.AssertNumberOfCalls(t, "Plan", 2)
   583  
   584  	assert.ElementsMatch(t, []string{
   585  		`level=info component=compactor msg="waiting until compactor is ACTIVE in the ring"`,
   586  		`level=info component=compactor msg="compactor is ACTIVE in the ring"`,
   587  		`level=info component=compactor msg="discovering users from bucket"`,
   588  		`level=info component=compactor msg="discovered users from bucket" users=2`,
   589  		`level=info component=compactor msg="starting compaction of user blocks" tenant=user-1`,
   590  		`level=info component=compactor tenant=user-1 msg="start sync of metas"`,
   591  		`level=info component=compactor tenant=user-1 msg="start of GC"`,
   592  		`level=debug component=compactor tenant=user-1 msg="grouper found a compactable blocks group" groupKey=0@17241709254077376921-merge--1574776800000-1574784000000 job="stage: merge, range start: 1574776800000, range end: 1574784000000, shard: , blocks: 01DTVP434PA9VFXSW2JKB3392D (min time: 2019-11-26T14:00:00Z, max time: 2019-11-26T16:00:00Z),01FS51A7GQ1RQWV35DBVYQM4KF (min time: 2019-11-26T14:00:00Z, max time: 2019-11-26T16:00:00Z)"`,
   593  		`level=info component=compactor tenant=user-1 msg="start of compactions"`,
   594  		`level=info component=compactor tenant=user-1 groupKey=0@17241709254077376921-merge--1574776800000-1574784000000 msg="compaction job succeeded"`,
   595  		`level=info component=compactor tenant=user-1 msg="compaction iterations done"`,
   596  		`level=info component=compactor msg="successfully compacted user blocks" tenant=user-1`,
   597  		`level=info component=compactor msg="starting compaction of user blocks" tenant=user-2`,
   598  		`level=info component=compactor tenant=user-2 msg="start sync of metas"`,
   599  		`level=info component=compactor tenant=user-2 msg="start of GC"`,
   600  		`level=debug component=compactor tenant=user-2 msg="grouper found a compactable blocks group" groupKey=0@17241709254077376921-merge--1574776800000-1574784000000 job="stage: merge, range start: 1574776800000, range end: 1574784000000, shard: , blocks: 01DTW0ZCPDDNV4BV83Q2SV4QAZ (min time: 2019-11-26T14:00:00Z, max time: 2019-11-26T16:00:00Z),01FRSF035J26D6CGX7STCSD1KG (min time: 2019-11-26T14:00:00Z, max time: 2019-11-26T16:00:00Z)"`,
   601  		`level=info component=compactor tenant=user-2 msg="start of compactions"`,
   602  		`level=info component=compactor tenant=user-2 groupKey=0@17241709254077376921-merge--1574776800000-1574784000000 msg="compaction job succeeded"`,
   603  		`level=info component=compactor tenant=user-2 msg="compaction iterations done"`,
   604  		`level=info component=compactor msg="successfully compacted user blocks" tenant=user-2`,
   605  	}, removeIgnoredLogs(strings.Split(strings.TrimSpace(logs.String()), "\n")))
   606  
   607  	// Instead of testing for shipper metrics, we only check our metrics here.
   608  	// Real shipper metrics are too variable to embed into a test.
   609  	testedMetrics := []string{
   610  		"pyroscope_compactor_runs_started_total", "pyroscope_compactor_runs_completed_total", "pyroscope_compactor_runs_failed_total",
   611  		"pyroscope_compactor_blocks_cleaned_total", "pyroscope_compactor_block_cleanup_failures_total", "pyroscope_compactor_blocks_marked_for_deletion_total",
   612  		"pyroscope_compactor_block_cleanup_started_total", "pyroscope_compactor_block_cleanup_completed_total", "pyroscope_compactor_block_cleanup_failed_total",
   613  		"pyroscope_compactor_group_compaction_runs_completed_total", "pyroscope_compactor_group_compaction_runs_started_total",
   614  		"pyroscope_compactor_group_compactions_failures_total", "pyroscope_compactor_group_compactions_total",
   615  	}
   616  
   617  	assert.NoError(t, prom_testutil.GatherAndCompare(registry, strings.NewReader(`
   618  		# TYPE pyroscope_compactor_runs_started_total counter
   619  		# HELP pyroscope_compactor_runs_started_total Total number of compaction runs started.
   620  		pyroscope_compactor_runs_started_total 1
   621  
   622  		# TYPE pyroscope_compactor_runs_completed_total counter
   623  		# HELP pyroscope_compactor_runs_completed_total Total number of compaction runs successfully completed.
   624  		pyroscope_compactor_runs_completed_total 1
   625  
   626  		# TYPE pyroscope_compactor_runs_failed_total counter
   627  		# HELP pyroscope_compactor_runs_failed_total Total number of compaction runs failed.
   628  		pyroscope_compactor_runs_failed_total{reason="error"} 0
   629  		pyroscope_compactor_runs_failed_total{reason="shutdown"} 0
   630  
   631  		# HELP pyroscope_compactor_group_compaction_runs_completed_total Total number of group completed compaction runs. This also includes compactor group runs that resulted with no compaction.
   632  		# TYPE pyroscope_compactor_group_compaction_runs_completed_total counter
   633  		pyroscope_compactor_group_compaction_runs_completed_total 2
   634  
   635  		# HELP pyroscope_compactor_group_compaction_runs_started_total Total number of group compaction attempts.
   636  		# TYPE pyroscope_compactor_group_compaction_runs_started_total counter
   637  		pyroscope_compactor_group_compaction_runs_started_total 2
   638  
   639  		# HELP pyroscope_compactor_group_compactions_failures_total Total number of failed group compactions.
   640  		# TYPE pyroscope_compactor_group_compactions_failures_total counter
   641  		pyroscope_compactor_group_compactions_failures_total 0
   642  
   643  		# HELP pyroscope_compactor_group_compactions_total Total number of group compaction attempts that resulted in new block(s).
   644  		# TYPE pyroscope_compactor_group_compactions_total counter
   645  		pyroscope_compactor_group_compactions_total 0
   646  
   647  		# TYPE pyroscope_compactor_block_cleanup_failures_total counter
   648  		# HELP pyroscope_compactor_block_cleanup_failures_total Total number of blocks failed to be deleted.
   649  		pyroscope_compactor_block_cleanup_failures_total 0
   650  
   651  		# HELP pyroscope_compactor_blocks_cleaned_total Total number of blocks deleted.
   652  		# TYPE pyroscope_compactor_blocks_cleaned_total counter
   653  		pyroscope_compactor_blocks_cleaned_total 0
   654  
   655  		# HELP pyroscope_compactor_blocks_marked_for_deletion_total Total number of blocks marked for deletion in compactor.
   656  		# TYPE pyroscope_compactor_blocks_marked_for_deletion_total counter
   657  		pyroscope_compactor_blocks_marked_for_deletion_total{reason="compaction"} 0
   658  		pyroscope_compactor_blocks_marked_for_deletion_total{reason="partial"} 0
   659  		pyroscope_compactor_blocks_marked_for_deletion_total{reason="retention"} 0
   660  
   661  		# TYPE pyroscope_compactor_block_cleanup_started_total counter
   662  		# HELP pyroscope_compactor_block_cleanup_started_total Total number of blocks cleanup runs started.
   663  		pyroscope_compactor_block_cleanup_started_total 1
   664  
   665  		# TYPE pyroscope_compactor_block_cleanup_completed_total counter
   666  		# HELP pyroscope_compactor_block_cleanup_completed_total Total number of blocks cleanup runs successfully completed.
   667  		pyroscope_compactor_block_cleanup_completed_total 1
   668  
   669  		# TYPE pyroscope_compactor_block_cleanup_failed_total counter
   670  		# HELP pyroscope_compactor_block_cleanup_failed_total Total number of blocks cleanup runs failed.
   671  		pyroscope_compactor_block_cleanup_failed_total 0
   672  	`), testedMetrics...))
   673  }
   674  
   675  func TestMultitenantCompactor_ShouldStopCompactingTenantOnReachingMaxCompactionTime(t *testing.T) {
   676  	t.Parallel()
   677  
   678  	// By using blocks with different labels, we get two compaction jobs. Only one of these jobs will be started,
   679  	// and since its planning will take longer than maxCompactionTime, we stop compactions early.
   680  	bucketClient := mockobjstore.NewMockBucketWithHelper(t)
   681  	bucketClient.MockIter("", []string{"user-1"}, nil)
   682  	bucketClient.MockExists(path.Join("user-1", "phlaredb/", bucket.TenantDeletionMarkPath), false, nil)
   683  	bucketClient.MockIter("user-1/phlaredb/", []string{"user-1/phlaredb/01DTVP434PA9VFXSW2JKB3392D", "user-1/phlaredb/01FN3VCQV5X342W2ZKMQQXAZRX", "user-1/phlaredb/01FS51A7GQ1RQWV35DBVYQM4KF", "user-1/phlaredb/01FRQGQB7RWQ2TS0VWA82QTPXE"}, nil)
   684  	bucketClient.MockGet("user-1/phlaredb/01DTVP434PA9VFXSW2JKB3392D/meta.json", mockBlockMetaJSONWithTimeRangeAndLabels("01DTVP434PA9VFXSW2JKB3392D", 1574776800000, 1574784000000, map[string]string{"A": "B"}), nil)
   685  	bucketClient.MockGet("user-1/phlaredb/01DTVP434PA9VFXSW2JKB3392D/deletion-mark.json", "", nil)
   686  	bucketClient.MockGet("user-1/phlaredb/01DTVP434PA9VFXSW2JKB3392D/no-compact-mark.json", "", nil)
   687  	bucketClient.MockGet("user-1/phlaredb/01FS51A7GQ1RQWV35DBVYQM4KF/meta.json", mockBlockMetaJSONWithTimeRangeAndLabels("01FS51A7GQ1RQWV35DBVYQM4KF", 1574776800000, 1574784000000, map[string]string{"A": "B"}), nil)
   688  	bucketClient.MockGet("user-1/phlaredb/01FS51A7GQ1RQWV35DBVYQM4KF/deletion-mark.json", "", nil)
   689  	bucketClient.MockGet("user-1/phlaredb/01FS51A7GQ1RQWV35DBVYQM4KF/no-compact-mark.json", "", nil)
   690  	bucketClient.MockGet("user-1/phlaredb/01FN3VCQV5X342W2ZKMQQXAZRX/meta.json", mockBlockMetaJSONWithTimeRangeAndLabels("01FN3VCQV5X342W2ZKMQQXAZRX", 1574776800000, 1574784000000, map[string]string{"C": "D"}), nil)
   691  	bucketClient.MockGet("user-1/phlaredb/01FN3VCQV5X342W2ZKMQQXAZRX/deletion-mark.json", "", nil)
   692  	bucketClient.MockGet("user-1/phlaredb/01FN3VCQV5X342W2ZKMQQXAZRX/no-compact-mark.json", "", nil)
   693  	bucketClient.MockGet("user-1/phlaredb/01FRQGQB7RWQ2TS0VWA82QTPXE/meta.json", mockBlockMetaJSONWithTimeRangeAndLabels("01FRQGQB7RWQ2TS0VWA82QTPXE", 1574776800000, 1574784000000, map[string]string{"C": "D"}), nil)
   694  	bucketClient.MockGet("user-1/phlaredb/01FRQGQB7RWQ2TS0VWA82QTPXE/deletion-mark.json", "", nil)
   695  	bucketClient.MockGet("user-1/phlaredb/01FRQGQB7RWQ2TS0VWA82QTPXE/no-compact-mark.json", "", nil)
   696  	bucketClient.MockGet("user-1/phlaredb/bucket-index.json.gz", "", nil)
   697  	bucketClient.MockIter("user-1/phlaredb/markers/", nil, nil)
   698  	bucketClient.MockUpload("user-1/phlaredb/bucket-index.json.gz", nil)
   699  
   700  	cfg := prepareConfig(t)
   701  	cfg.MaxCompactionTime = 500 * time.Millisecond // Enough time to start one compaction. We will make it last longer than this.
   702  	cfg.CompactionConcurrency = 1
   703  
   704  	c, _, tsdbPlanner, logs, _ := prepare(t, cfg, bucketClient)
   705  
   706  	// Planner is called at the beginning of each job. We make it return no work, but only after delay.
   707  	plannerDelay := 2 * cfg.MaxCompactionTime
   708  	tsdbPlanner.On("Plan", mock.Anything, mock.Anything).After(plannerDelay).Return([]*block.Meta{}, nil)
   709  
   710  	require.NoError(t, services.StartAndAwaitRunning(context.Background(), c))
   711  
   712  	// Compactor doesn't wait for blocks cleaner to finish, but our test checks for cleaner metrics.
   713  	require.NoError(t, c.blocksCleaner.AwaitRunning(context.Background()))
   714  
   715  	// Wait until a run has completed. Since planner takes "2*cfg.MaxCompactionTime", we wait for twice as long.
   716  	test.Poll(t, 2*plannerDelay, 1.0, func() interface{} {
   717  		return prom_testutil.ToFloat64(c.compactionRunsCompleted)
   718  	})
   719  
   720  	require.NoError(t, services.StopAndAwaitTerminated(context.Background(), c))
   721  
   722  	// Ensure a plan has been called only once.
   723  	tsdbPlanner.AssertNumberOfCalls(t, "Plan", 1)
   724  
   725  	assert.Equal(t, []string{
   726  		`level=info component=compactor msg="waiting until compactor is ACTIVE in the ring"`,
   727  		`level=info component=compactor msg="compactor is ACTIVE in the ring"`,
   728  		`level=info component=compactor msg="discovering users from bucket"`,
   729  		`level=info component=compactor msg="discovered users from bucket" users=1`,
   730  		`level=info component=compactor msg="starting compaction of user blocks" tenant=user-1`,
   731  		`level=info component=compactor tenant=user-1 msg="start sync of metas"`,
   732  		`level=info component=compactor tenant=user-1 msg="start of GC"`,
   733  		`level=debug component=compactor tenant=user-1 msg="grouper found a compactable blocks group" groupKey=0@12695595599644216241-merge--1574776800000-1574784000000 job="stage: merge, range start: 1574776800000, range end: 1574784000000, shard: , blocks: 01FN3VCQV5X342W2ZKMQQXAZRX (min time: 2019-11-26T14:00:00Z, max time: 2019-11-26T16:00:00Z),01FRQGQB7RWQ2TS0VWA82QTPXE (min time: 2019-11-26T14:00:00Z, max time: 2019-11-26T16:00:00Z)"`,
   734  		`level=debug component=compactor tenant=user-1 msg="grouper found a compactable blocks group" groupKey=0@414047632870839233-merge--1574776800000-1574784000000 job="stage: merge, range start: 1574776800000, range end: 1574784000000, shard: , blocks: 01DTVP434PA9VFXSW2JKB3392D (min time: 2019-11-26T14:00:00Z, max time: 2019-11-26T16:00:00Z),01FS51A7GQ1RQWV35DBVYQM4KF (min time: 2019-11-26T14:00:00Z, max time: 2019-11-26T16:00:00Z)"`,
   735  		`level=info component=compactor tenant=user-1 msg="start of compactions"`,
   736  		`level=info component=compactor tenant=user-1 msg="max compaction time reached, no more compactions will be started"`,
   737  		`level=info component=compactor tenant=user-1 groupKey=0@12695595599644216241-merge--1574776800000-1574784000000 msg="compaction job succeeded"`,
   738  		`level=info component=compactor tenant=user-1 msg="compaction iterations done"`,
   739  		`level=info component=compactor msg="successfully compacted user blocks" tenant=user-1`,
   740  	}, removeIgnoredLogs(strings.Split(strings.TrimSpace(logs.String()), "\n")))
   741  }
   742  
   743  func TestMultitenantCompactor_ShouldNotCompactBlocksMarkedForDeletion(t *testing.T) {
   744  	t.Parallel()
   745  
   746  	cfg := prepareConfig(t)
   747  	cfg.DeletionDelay = 10 * time.Minute // Delete block after 10 minutes
   748  
   749  	// Mock the bucket to contain two users, each one with one block.
   750  	bucketClient := mockobjstore.NewMockBucketWithHelper(t)
   751  	bucketClient.MockIter("", []string{"user-1"}, nil)
   752  	bucketClient.MockIter("user-1/phlaredb/", []string{"user-1/phlaredb/01DTVP434PA9VFXSW2JKB3392D", "user-1/phlaredb/01DTW0ZCPDDNV4BV83Q2SV4QAZ"}, nil)
   753  	bucketClient.MockExists(path.Join("user-1", "phlaredb/", bucket.TenantDeletionMarkPath), false, nil)
   754  
   755  	// Block that has just been marked for deletion. It will not be deleted just yet, and it also will not be compacted.
   756  	bucketClient.MockGet("user-1/phlaredb/01DTVP434PA9VFXSW2JKB3392D/meta.json", mockBlockMetaJSON("01DTVP434PA9VFXSW2JKB3392D"), nil)
   757  	bucketClient.MockGet("user-1/phlaredb/01DTVP434PA9VFXSW2JKB3392D/deletion-mark.json", mockDeletionMarkJSON("01DTVP434PA9VFXSW2JKB3392D", time.Now()), nil)
   758  	bucketClient.MockGet("user-1/phlaredb/markers/01DTVP434PA9VFXSW2JKB3392D-deletion-mark.json", mockDeletionMarkJSON("01DTVP434PA9VFXSW2JKB3392D", time.Now()), nil)
   759  
   760  	// This block will be deleted by cleaner.
   761  	bucketClient.MockGet("user-1/phlaredb/01DTW0ZCPDDNV4BV83Q2SV4QAZ/meta.json", mockBlockMetaJSON("01DTW0ZCPDDNV4BV83Q2SV4QAZ"), nil)
   762  	bucketClient.MockGet("user-1/phlaredb/01DTW0ZCPDDNV4BV83Q2SV4QAZ/deletion-mark.json", mockDeletionMarkJSON("01DTW0ZCPDDNV4BV83Q2SV4QAZ", time.Now().Add(-cfg.DeletionDelay)), nil)
   763  	bucketClient.MockGet("user-1/phlaredb/markers/01DTW0ZCPDDNV4BV83Q2SV4QAZ-deletion-mark.json", mockDeletionMarkJSON("01DTW0ZCPDDNV4BV83Q2SV4QAZ", time.Now().Add(-cfg.DeletionDelay)), nil)
   764  
   765  	bucketClient.MockIter("user-1/phlaredb/01DTW0ZCPDDNV4BV83Q2SV4QAZ", []string{
   766  		"user-1/phlaredb/01DTW0ZCPDDNV4BV83Q2SV4QAZ/meta.json",
   767  		"user-1/phlaredb/01DTW0ZCPDDNV4BV83Q2SV4QAZ/deletion-mark.json",
   768  	}, nil)
   769  
   770  	bucketClient.MockIter("user-1/phlaredb/markers/", []string{
   771  		"user-1/phlaredb/markers/01DTVP434PA9VFXSW2JKB3392D-deletion-mark.json",
   772  		"user-1/phlaredb/markers/01DTW0ZCPDDNV4BV83Q2SV4QAZ-deletion-mark.json",
   773  	}, nil)
   774  
   775  	bucketClient.MockDelete("user-1/phlaredb/01DTW0ZCPDDNV4BV83Q2SV4QAZ/meta.json", nil)
   776  	bucketClient.MockDelete("user-1/phlaredb/01DTW0ZCPDDNV4BV83Q2SV4QAZ/deletion-mark.json", nil)
   777  	bucketClient.MockDelete("user-1/phlaredb/markers/01DTW0ZCPDDNV4BV83Q2SV4QAZ-deletion-mark.json", nil)
   778  	bucketClient.MockDelete("user-1/phlaredb/01DTW0ZCPDDNV4BV83Q2SV4QAZ", nil)
   779  	bucketClient.MockGet("user-1/phlaredb/bucket-index.json.gz", "", nil)
   780  	bucketClient.MockUpload("user-1/phlaredb/bucket-index.json.gz", nil)
   781  
   782  	c, _, tsdbPlanner, logs, registry := prepare(t, cfg, bucketClient)
   783  
   784  	require.NoError(t, services.StartAndAwaitRunning(context.Background(), c))
   785  
   786  	// Compactor doesn't wait for blocks cleaner to finish, but our test checks for cleaner metrics.
   787  	require.NoError(t, c.blocksCleaner.AwaitRunning(context.Background()))
   788  
   789  	// Wait until a run has completed.
   790  	test.Poll(t, time.Second, 1.0, func() interface{} {
   791  		return prom_testutil.ToFloat64(c.compactionRunsCompleted)
   792  	})
   793  
   794  	require.NoError(t, services.StopAndAwaitTerminated(context.Background(), c))
   795  
   796  	// Since both blocks are marked for deletion, none of them are going to be compacted.
   797  	tsdbPlanner.AssertNumberOfCalls(t, "Plan", 0)
   798  
   799  	assert.ElementsMatch(t, []string{
   800  		`level=info component=compactor msg="waiting until compactor is ACTIVE in the ring"`,
   801  		`level=info component=compactor msg="compactor is ACTIVE in the ring"`,
   802  		`level=info component=compactor msg="discovering users from bucket"`,
   803  		`level=info component=compactor msg="discovered users from bucket" users=1`,
   804  		`level=info component=compactor msg="starting compaction of user blocks" tenant=user-1`,
   805  		`level=info component=compactor tenant=user-1 msg="start sync of metas"`,
   806  		`level=info component=compactor tenant=user-1 msg="start of GC"`,
   807  		`level=info component=compactor tenant=user-1 msg="start of compactions"`,
   808  		`level=info component=compactor tenant=user-1 msg="compaction iterations done"`,
   809  		`level=info component=compactor msg="successfully compacted user blocks" tenant=user-1`,
   810  	}, removeIgnoredLogs(strings.Split(strings.TrimSpace(logs.String()), "\n")))
   811  
   812  	// Instead of testing for shipper metrics, we only check our metrics here.
   813  	// Real shipper metrics are too variable to embed into a test.
   814  	testedMetrics := []string{
   815  		"pyroscope_compactor_runs_started_total", "pyroscope_compactor_runs_completed_total", "pyroscope_compactor_runs_failed_total",
   816  		"pyroscope_compactor_blocks_cleaned_total", "pyroscope_compactor_block_cleanup_failures_total", "pyroscope_compactor_blocks_marked_for_deletion_total",
   817  		"pyroscope_compactor_block_cleanup_started_total", "pyroscope_compactor_block_cleanup_completed_total", "pyroscope_compactor_block_cleanup_failed_total",
   818  	}
   819  	assert.NoError(t, prom_testutil.GatherAndCompare(registry, strings.NewReader(`
   820  		# TYPE pyroscope_compactor_runs_started_total counter
   821  		# HELP pyroscope_compactor_runs_started_total Total number of compaction runs started.
   822  		pyroscope_compactor_runs_started_total 1
   823  
   824  		# TYPE pyroscope_compactor_runs_completed_total counter
   825  		# HELP pyroscope_compactor_runs_completed_total Total number of compaction runs successfully completed.
   826  		pyroscope_compactor_runs_completed_total 1
   827  
   828  		# TYPE pyroscope_compactor_runs_failed_total counter
   829  		# HELP pyroscope_compactor_runs_failed_total Total number of compaction runs failed.
   830  		pyroscope_compactor_runs_failed_total{reason="error"} 0
   831  		pyroscope_compactor_runs_failed_total{reason="shutdown"} 0
   832  
   833  		# TYPE pyroscope_compactor_block_cleanup_failures_total counter
   834  		# HELP pyroscope_compactor_block_cleanup_failures_total Total number of blocks failed to be deleted.
   835  		pyroscope_compactor_block_cleanup_failures_total 0
   836  
   837  		# HELP pyroscope_compactor_blocks_cleaned_total Total number of blocks deleted.
   838  		# TYPE pyroscope_compactor_blocks_cleaned_total counter
   839  		pyroscope_compactor_blocks_cleaned_total 1
   840  
   841  		# HELP pyroscope_compactor_blocks_marked_for_deletion_total Total number of blocks marked for deletion in compactor.
   842  		# TYPE pyroscope_compactor_blocks_marked_for_deletion_total counter
   843  		pyroscope_compactor_blocks_marked_for_deletion_total{reason="compaction"} 0
   844  		pyroscope_compactor_blocks_marked_for_deletion_total{reason="partial"} 0
   845  		pyroscope_compactor_blocks_marked_for_deletion_total{reason="retention"} 0
   846  
   847  		# TYPE pyroscope_compactor_block_cleanup_started_total counter
   848  		# HELP pyroscope_compactor_block_cleanup_started_total Total number of blocks cleanup runs started.
   849  		pyroscope_compactor_block_cleanup_started_total 1
   850  
   851  		# TYPE pyroscope_compactor_block_cleanup_completed_total counter
   852  		# HELP pyroscope_compactor_block_cleanup_completed_total Total number of blocks cleanup runs successfully completed.
   853  		pyroscope_compactor_block_cleanup_completed_total 1
   854  
   855  		# TYPE pyroscope_compactor_block_cleanup_failed_total counter
   856  		# HELP pyroscope_compactor_block_cleanup_failed_total Total number of blocks cleanup runs failed.
   857  		pyroscope_compactor_block_cleanup_failed_total 0
   858  	`), testedMetrics...))
   859  }
   860  
   861  func TestMultitenantCompactor_ShouldNotCompactBlocksMarkedForNoCompaction(t *testing.T) {
   862  	t.Parallel()
   863  
   864  	cfg := prepareConfig(t)
   865  	cfg.DeletionDelay = 10 * time.Minute // Delete block after 10 minutes
   866  
   867  	// Mock the bucket to contain one user with a block marked for no-compaction.
   868  	bucketClient := mockobjstore.NewMockBucketWithHelper(t)
   869  	bucketClient.MockIter("", []string{"user-1"}, nil)
   870  	bucketClient.MockIter("user-1/phlaredb/", []string{"user-1/phlaredb/01DTVP434PA9VFXSW2JKB3392D"}, nil)
   871  	bucketClient.MockExists(path.Join("user-1", "phlaredb/", bucket.TenantDeletionMarkPath), false, nil)
   872  
   873  	// Block that is marked for no compaction. It will be ignored.
   874  	bucketClient.MockGet("user-1/phlaredb/01DTVP434PA9VFXSW2JKB3392D/meta.json", mockBlockMetaJSON("01DTVP434PA9VFXSW2JKB3392D"), nil)
   875  	bucketClient.MockGet("user-1/phlaredb/01DTVP434PA9VFXSW2JKB3392D/deletion-mark.json", "", nil)
   876  	bucketClient.MockGet("user-1/phlaredb/01DTVP434PA9VFXSW2JKB3392D/no-compact-mark.json", `{"id":"01DTVP434PA9VFXSW2JKB3392D","version":1,"details":"details","no_compact_time":1637757932,"reason":"reason"}`, nil)
   877  
   878  	bucketClient.MockIter("user-1/phlaredb/markers/", []string{"user-1/markers/01DTVP434PA9VFXSW2JKB3392D-no-compact-mark.json"}, nil)
   879  
   880  	bucketClient.MockGet("user-1/phlaredb/bucket-index.json.gz", "", nil)
   881  	bucketClient.MockUpload("user-1/phlaredb/bucket-index.json.gz", nil)
   882  
   883  	c, _, tsdbPlanner, logs, _ := prepare(t, cfg, bucketClient)
   884  
   885  	require.NoError(t, services.StartAndAwaitRunning(context.Background(), c))
   886  
   887  	// Compactor doesn't wait for blocks cleaner to finish, but our test checks for cleaner metrics.
   888  	require.NoError(t, c.blocksCleaner.AwaitRunning(context.Background()))
   889  
   890  	// Wait until a run has completed.
   891  	test.Poll(t, time.Second, 1.0, func() interface{} {
   892  		return prom_testutil.ToFloat64(c.compactionRunsCompleted)
   893  	})
   894  
   895  	require.NoError(t, services.StopAndAwaitTerminated(context.Background(), c))
   896  
   897  	// Since block is not compacted, there will be no planning done.
   898  	tsdbPlanner.AssertNumberOfCalls(t, "Plan", 0)
   899  
   900  	assert.ElementsMatch(t, []string{
   901  		`level=info component=compactor msg="waiting until compactor is ACTIVE in the ring"`,
   902  		`level=info component=compactor msg="compactor is ACTIVE in the ring"`,
   903  		`level=info component=compactor msg="discovering users from bucket"`,
   904  		`level=info component=compactor msg="discovered users from bucket" users=1`,
   905  		`level=info component=compactor msg="starting compaction of user blocks" tenant=user-1`,
   906  		`level=info component=compactor tenant=user-1 msg="start sync of metas"`,
   907  		`level=info component=compactor tenant=user-1 msg="start of GC"`,
   908  		`level=info component=compactor tenant=user-1 msg="start of compactions"`,
   909  		`level=info component=compactor tenant=user-1 msg="compaction iterations done"`,
   910  		`level=info component=compactor msg="successfully compacted user blocks" tenant=user-1`,
   911  	}, removeIgnoredLogs(strings.Split(strings.TrimSpace(logs.String()), "\n")))
   912  }
   913  
   914  func TestMultitenantCompactor_ShouldNotCompactBlocksForUsersMarkedForDeletion(t *testing.T) {
   915  	t.Parallel()
   916  
   917  	cfg := prepareConfig(t)
   918  	cfg.DeletionDelay = 10 * time.Minute      // Delete block after 10 minutes
   919  	cfg.TenantCleanupDelay = 10 * time.Minute // To make sure it's not 0.
   920  
   921  	// Mock the bucket to contain two users, each one with one block.
   922  	bucketClient := mockobjstore.NewMockBucketWithHelper(t)
   923  	bucketClient.MockIter("", []string{"user-1"}, nil)
   924  	bucketClient.MockIter("user-1/phlaredb/", []string{"user-1/phlaredb/01DTVP434PA9VFXSW2JKB3392D"}, nil)
   925  	bucketClient.MockGet(path.Join("user-1", "phlaredb/", bucket.TenantDeletionMarkPath), `{"deletion_time": 1}`, nil)
   926  	bucketClient.MockUpload(path.Join("user-1", "phlaredb/", bucket.TenantDeletionMarkPath), nil)
   927  
   928  	bucketClient.MockIter("user-1/phlaredb/01DTVP434PA9VFXSW2JKB3392D", []string{"user-1/phlaredb/01DTVP434PA9VFXSW2JKB3392D/meta.json", "user-1/phlaredb/01DTVP434PA9VFXSW2JKB3392D/index"}, nil)
   929  	bucketClient.MockGet("user-1/phlaredb/01DTVP434PA9VFXSW2JKB3392D/meta.json", mockBlockMetaJSON("01DTVP434PA9VFXSW2JKB3392D"), nil)
   930  	bucketClient.MockGet("user-1/phlaredb/01DTVP434PA9VFXSW2JKB3392D/index", "some index content", nil)
   931  	bucketClient.MockExists("user-1/phlaredb/01DTVP434PA9VFXSW2JKB3392D/deletion-mark.json", false, nil)
   932  	bucketClient.MockExists("user-1/phlaredb/markers/01DTVP434PA9VFXSW2JKB3392D-deletion-mark.json", false, nil)
   933  
   934  	bucketClient.MockDelete("user-1/phlaredb/01DTVP434PA9VFXSW2JKB3392D/meta.json", nil)
   935  	bucketClient.MockDelete("user-1/phlaredb/01DTVP434PA9VFXSW2JKB3392D/index", nil)
   936  	bucketClient.MockDelete("user-1/phlaredb/bucket-index.json.gz", nil)
   937  
   938  	c, _, tsdbPlanner, logs, registry := prepare(t, cfg, bucketClient)
   939  
   940  	// Mock the planner as if there's no compaction to do,
   941  	// in order to simplify tests (all in all, we just want to
   942  	// test our logic and not TSDB compactor which we expect to
   943  	// be already tested).
   944  	tsdbPlanner.On("Plan", mock.Anything, mock.Anything).Return([]*block.Meta{}, nil)
   945  
   946  	require.NoError(t, services.StartAndAwaitRunning(context.Background(), c))
   947  
   948  	// Compactor doesn't wait for blocks cleaner to finish, but our test checks for cleaner metrics.
   949  	require.NoError(t, c.blocksCleaner.AwaitRunning(context.Background()))
   950  
   951  	// Wait until a run has completed.
   952  	test.Poll(t, time.Second, 1.0, func() interface{} {
   953  		return prom_testutil.ToFloat64(c.compactionRunsCompleted)
   954  	})
   955  
   956  	require.NoError(t, services.StopAndAwaitTerminated(context.Background(), c))
   957  
   958  	// No user is compacted, single user we have is marked for deletion.
   959  	tsdbPlanner.AssertNumberOfCalls(t, "Plan", 0)
   960  
   961  	assert.ElementsMatch(t, []string{
   962  		`level=info component=compactor msg="waiting until compactor is ACTIVE in the ring"`,
   963  		`level=info component=compactor msg="compactor is ACTIVE in the ring"`,
   964  		`level=info component=compactor msg="discovering users from bucket"`,
   965  		`level=info component=compactor msg="discovered users from bucket" users=1`,
   966  		`level=debug component=compactor msg="skipping user because it is marked for deletion" tenant=user-1`,
   967  	}, removeIgnoredLogs(strings.Split(strings.TrimSpace(logs.String()), "\n")))
   968  
   969  	// Instead of testing for shipper metrics, we only check our metrics here.
   970  	// Real shipper metrics are too variable to embed into a test.
   971  	testedMetrics := []string{
   972  		"pyroscope_compactor_runs_started_total", "pyroscope_compactor_runs_completed_total", "pyroscope_compactor_runs_failed_total",
   973  		"pyroscope_compactor_blocks_cleaned_total", "pyroscope_compactor_block_cleanup_failures_total", "pyroscope_compactor_blocks_marked_for_deletion_total",
   974  		"pyroscope_compactor_block_cleanup_started_total", "pyroscope_compactor_block_cleanup_completed_total", "pyroscope_compactor_block_cleanup_failed_total",
   975  		"pyroscope_bucket_blocks_count", "pyroscope_bucket_blocks_marked_for_deletion_count", "pyroscope_bucket_index_last_successful_update_timestamp_seconds",
   976  	}
   977  	assert.NoError(t, prom_testutil.GatherAndCompare(registry, strings.NewReader(`
   978  		# TYPE pyroscope_compactor_runs_started_total counter
   979  		# HELP pyroscope_compactor_runs_started_total Total number of compaction runs started.
   980  		pyroscope_compactor_runs_started_total 1
   981  
   982  		# TYPE pyroscope_compactor_runs_completed_total counter
   983  		# HELP pyroscope_compactor_runs_completed_total Total number of compaction runs successfully completed.
   984  		pyroscope_compactor_runs_completed_total 1
   985  
   986  		# TYPE pyroscope_compactor_runs_failed_total counter
   987  		# HELP pyroscope_compactor_runs_failed_total Total number of compaction runs failed.
   988  		pyroscope_compactor_runs_failed_total{reason="error"} 0
   989  		pyroscope_compactor_runs_failed_total{reason="shutdown"} 0
   990  
   991  		# TYPE pyroscope_compactor_block_cleanup_failures_total counter
   992  		# HELP pyroscope_compactor_block_cleanup_failures_total Total number of blocks failed to be deleted.
   993  		pyroscope_compactor_block_cleanup_failures_total 0
   994  
   995  		# HELP pyroscope_compactor_blocks_cleaned_total Total number of blocks deleted.
   996  		# TYPE pyroscope_compactor_blocks_cleaned_total counter
   997  		pyroscope_compactor_blocks_cleaned_total 1
   998  
   999  		# HELP pyroscope_compactor_blocks_marked_for_deletion_total Total number of blocks marked for deletion in compactor.
  1000  		# TYPE pyroscope_compactor_blocks_marked_for_deletion_total counter
  1001  		pyroscope_compactor_blocks_marked_for_deletion_total{reason="compaction"} 0
  1002  		pyroscope_compactor_blocks_marked_for_deletion_total{reason="partial"} 0
  1003  		pyroscope_compactor_blocks_marked_for_deletion_total{reason="retention"} 0
  1004  
  1005  		# TYPE pyroscope_compactor_block_cleanup_started_total counter
  1006  		# HELP pyroscope_compactor_block_cleanup_started_total Total number of blocks cleanup runs started.
  1007  		pyroscope_compactor_block_cleanup_started_total 1
  1008  
  1009  		# TYPE pyroscope_compactor_block_cleanup_completed_total counter
  1010  		# HELP pyroscope_compactor_block_cleanup_completed_total Total number of blocks cleanup runs successfully completed.
  1011  		pyroscope_compactor_block_cleanup_completed_total 1
  1012  
  1013  		# TYPE pyroscope_compactor_block_cleanup_failed_total counter
  1014  		# HELP pyroscope_compactor_block_cleanup_failed_total Total number of blocks cleanup runs failed.
  1015  		pyroscope_compactor_block_cleanup_failed_total 0
  1016  	`), testedMetrics...))
  1017  }
  1018  
  1019  func TestMultitenantCompactor_ShouldCompactAllUsersOnShardingEnabledButOnlyOneInstanceRunning(t *testing.T) {
  1020  	t.Parallel()
  1021  
  1022  	// Mock the bucket to contain two users, each one with one block.
  1023  	bucketClient := mockobjstore.NewMockBucketWithHelper(t)
  1024  	bucketClient.MockIter("", []string{"user-1", "user-2"}, nil)
  1025  	bucketClient.MockExists(path.Join("user-1", "phlaredb", bucket.TenantDeletionMarkPath), false, nil)
  1026  	bucketClient.MockExists(path.Join("user-2", "phlaredb", bucket.TenantDeletionMarkPath), false, nil)
  1027  	bucketClient.MockIter("user-1/phlaredb/", []string{"user-1/phlaredb/01DTVP434PA9VFXSW2JKB3392D", "user-1/phlaredb/01FSTQ95C8FS0ZAGTQS2EF1NEG"}, nil)
  1028  	bucketClient.MockIter("user-2/phlaredb/", []string{"user-2/phlaredb/01DTW0ZCPDDNV4BV83Q2SV4QAZ", "user-2/phlaredb/01FSV54G6QFQH1G9QE93G3B9TB"}, nil)
  1029  	bucketClient.MockIter("user-1/phlaredb/markers/", nil, nil)
  1030  	bucketClient.MockIter("user-2/phlaredb/markers/", nil, nil)
  1031  	bucketClient.MockGet("user-1/phlaredb/01DTVP434PA9VFXSW2JKB3392D/meta.json", mockBlockMetaJSON("01DTVP434PA9VFXSW2JKB3392D"), nil)
  1032  	bucketClient.MockGet("user-1/phlaredb/01DTVP434PA9VFXSW2JKB3392D/deletion-mark.json", "", nil)
  1033  	bucketClient.MockGet("user-1/phlaredb/01DTVP434PA9VFXSW2JKB3392D/no-compact-mark.json", "", nil)
  1034  	bucketClient.MockGet("user-1/phlaredb/01FSTQ95C8FS0ZAGTQS2EF1NEG/meta.json", mockBlockMetaJSON("01FSTQ95C8FS0ZAGTQS2EF1NEG"), nil)
  1035  	bucketClient.MockGet("user-1/phlaredb/01FSTQ95C8FS0ZAGTQS2EF1NEG/deletion-mark.json", "", nil)
  1036  	bucketClient.MockGet("user-1/phlaredb/01FSTQ95C8FS0ZAGTQS2EF1NEG/no-compact-mark.json", "", nil)
  1037  	bucketClient.MockGet("user-2/phlaredb/01DTW0ZCPDDNV4BV83Q2SV4QAZ/meta.json", mockBlockMetaJSON("01DTW0ZCPDDNV4BV83Q2SV4QAZ"), nil)
  1038  	bucketClient.MockGet("user-2/phlaredb/01DTW0ZCPDDNV4BV83Q2SV4QAZ/deletion-mark.json", "", nil)
  1039  	bucketClient.MockGet("user-2/phlaredb/01DTW0ZCPDDNV4BV83Q2SV4QAZ/no-compact-mark.json", "", nil)
  1040  	bucketClient.MockGet("user-2/phlaredb/01FSV54G6QFQH1G9QE93G3B9TB/meta.json", mockBlockMetaJSON("01FSV54G6QFQH1G9QE93G3B9TB"), nil)
  1041  	bucketClient.MockGet("user-2/phlaredb/01FSV54G6QFQH1G9QE93G3B9TB/deletion-mark.json", "", nil)
  1042  	bucketClient.MockGet("user-2/phlaredb/01FSV54G6QFQH1G9QE93G3B9TB/no-compact-mark.json", "", nil)
  1043  	bucketClient.MockGet("user-1/phlaredb/bucket-index.json.gz", "", nil)
  1044  	bucketClient.MockGet("user-2/phlaredb/bucket-index.json.gz", "", nil)
  1045  	bucketClient.MockUpload("user-1/phlaredb/bucket-index.json.gz", nil)
  1046  	bucketClient.MockUpload("user-2/phlaredb/bucket-index.json.gz", nil)
  1047  
  1048  	ringStore, closer := consul.NewInMemoryClient(ring.GetCodec(), log.NewNopLogger(), nil)
  1049  	t.Cleanup(func() { assert.NoError(t, closer.Close()) })
  1050  
  1051  	cfg := prepareConfig(t)
  1052  	cfg.ShardingRing.Common.InstanceID = instanceID
  1053  	cfg.ShardingRing.Common.InstanceAddr = addr
  1054  	cfg.ShardingRing.Common.KVStore.Mock = ringStore
  1055  	c, _, tsdbPlanner, logs, registry := prepare(t, cfg, bucketClient)
  1056  
  1057  	// Mock the planner as if there's no compaction to do,
  1058  	// in order to simplify tests (all in all, we just want to
  1059  	// test our logic and not TSDB compactor which we expect to
  1060  	// be already tested).
  1061  	tsdbPlanner.On("Plan", mock.Anything, mock.Anything).Return([]*block.Meta{}, nil)
  1062  
  1063  	require.NoError(t, services.StartAndAwaitRunning(context.Background(), c))
  1064  
  1065  	// Compactor doesn't wait for blocks cleaner to finish, but our test checks for cleaner metrics.
  1066  	require.NoError(t, c.blocksCleaner.AwaitRunning(context.Background()))
  1067  
  1068  	// Wait until a run has completed.
  1069  	test.Poll(t, 5*time.Second, 1.0, func() interface{} {
  1070  		return prom_testutil.ToFloat64(c.compactionRunsCompleted)
  1071  	})
  1072  
  1073  	require.NoError(t, services.StopAndAwaitTerminated(context.Background(), c))
  1074  
  1075  	// Ensure a plan has been executed for the blocks of each user.
  1076  	tsdbPlanner.AssertNumberOfCalls(t, "Plan", 2)
  1077  
  1078  	assert.ElementsMatch(t, []string{
  1079  		`level=info component=compactor msg="waiting until compactor is ACTIVE in the ring"`,
  1080  		`level=info component=compactor msg="compactor is ACTIVE in the ring"`,
  1081  		`level=info component=compactor msg="discovering users from bucket"`,
  1082  		`level=info component=compactor msg="discovered users from bucket" users=2`,
  1083  		`level=info component=compactor msg="starting compaction of user blocks" tenant=user-1`,
  1084  		`level=info component=compactor tenant=user-1 msg="start sync of metas"`,
  1085  		`level=info component=compactor tenant=user-1 msg="start of GC"`,
  1086  		`level=debug component=compactor tenant=user-1 msg="grouper found a compactable blocks group" groupKey=0@17241709254077376921-merge--1574776800000-1574784000000 job="stage: merge, range start: 1574776800000, range end: 1574784000000, shard: , blocks: 01DTVP434PA9VFXSW2JKB3392D (min time: 2019-11-26T14:00:00Z, max time: 2019-11-26T16:00:00Z),01FSTQ95C8FS0ZAGTQS2EF1NEG (min time: 2019-11-26T14:00:00Z, max time: 2019-11-26T16:00:00Z)"`,
  1087  		`level=info component=compactor tenant=user-1 msg="start of compactions"`,
  1088  		`level=info component=compactor tenant=user-1 groupKey=0@17241709254077376921-merge--1574776800000-1574784000000 msg="compaction job succeeded"`,
  1089  		`level=info component=compactor tenant=user-1 msg="compaction iterations done"`,
  1090  		`level=info component=compactor msg="successfully compacted user blocks" tenant=user-1`,
  1091  		`level=info component=compactor msg="starting compaction of user blocks" tenant=user-2`,
  1092  		`level=info component=compactor tenant=user-2 msg="start sync of metas"`,
  1093  		`level=info component=compactor tenant=user-2 msg="start of GC"`,
  1094  		`level=debug component=compactor tenant=user-2 msg="grouper found a compactable blocks group" groupKey=0@17241709254077376921-merge--1574776800000-1574784000000 job="stage: merge, range start: 1574776800000, range end: 1574784000000, shard: , blocks: 01DTW0ZCPDDNV4BV83Q2SV4QAZ (min time: 2019-11-26T14:00:00Z, max time: 2019-11-26T16:00:00Z),01FSV54G6QFQH1G9QE93G3B9TB (min time: 2019-11-26T14:00:00Z, max time: 2019-11-26T16:00:00Z)"`,
  1095  		`level=info component=compactor tenant=user-2 msg="start of compactions"`,
  1096  		`level=info component=compactor tenant=user-2 groupKey=0@17241709254077376921-merge--1574776800000-1574784000000 msg="compaction job succeeded"`,
  1097  		`level=info component=compactor tenant=user-2 msg="compaction iterations done"`,
  1098  		`level=info component=compactor msg="successfully compacted user blocks" tenant=user-2`,
  1099  	}, removeIgnoredLogs(strings.Split(strings.TrimSpace(logs.String()), "\n")))
  1100  
  1101  	assert.NoError(t, prom_testutil.GatherAndCompare(registry, strings.NewReader(`
  1102  		# TYPE pyroscope_compactor_runs_started_total counter
  1103  		# HELP pyroscope_compactor_runs_started_total Total number of compaction runs started.
  1104  		pyroscope_compactor_runs_started_total 1
  1105  
  1106  		# TYPE pyroscope_compactor_runs_completed_total counter
  1107  		# HELP pyroscope_compactor_runs_completed_total Total number of compaction runs successfully completed.
  1108  		pyroscope_compactor_runs_completed_total 1
  1109  
  1110  		# TYPE pyroscope_compactor_runs_failed_total counter
  1111  		# HELP pyroscope_compactor_runs_failed_total Total number of compaction runs failed.
  1112  		pyroscope_compactor_runs_failed_total{reason="error"} 0
  1113  		pyroscope_compactor_runs_failed_total{reason="shutdown"} 0
  1114  
  1115  		# HELP pyroscope_compactor_group_compaction_runs_completed_total Total number of group completed compaction runs. This also includes compactor group runs that resulted with no compaction.
  1116  		# TYPE pyroscope_compactor_group_compaction_runs_completed_total counter
  1117  		pyroscope_compactor_group_compaction_runs_completed_total 2
  1118  
  1119  		# HELP pyroscope_compactor_group_compaction_runs_started_total Total number of group compaction attempts.
  1120  		# TYPE pyroscope_compactor_group_compaction_runs_started_total counter
  1121  		pyroscope_compactor_group_compaction_runs_started_total 2
  1122  
  1123  		# HELP pyroscope_compactor_group_compactions_failures_total Total number of failed group compactions.
  1124  		# TYPE pyroscope_compactor_group_compactions_failures_total counter
  1125  		pyroscope_compactor_group_compactions_failures_total 0
  1126  
  1127  		# HELP pyroscope_compactor_group_compactions_total Total number of group compaction attempts that resulted in new block(s).
  1128  		# TYPE pyroscope_compactor_group_compactions_total counter
  1129  		pyroscope_compactor_group_compactions_total 0
  1130  
  1131  		# HELP pyroscope_compactor_blocks_marked_for_deletion_total Total number of blocks marked for deletion in compactor.
  1132  		# TYPE pyroscope_compactor_blocks_marked_for_deletion_total counter
  1133  		pyroscope_compactor_blocks_marked_for_deletion_total{reason="compaction"} 0
  1134  		pyroscope_compactor_blocks_marked_for_deletion_total{reason="partial"} 0
  1135  		pyroscope_compactor_blocks_marked_for_deletion_total{reason="retention"} 0
  1136  	`),
  1137  		"pyroscope_compactor_runs_started_total",
  1138  		"pyroscope_compactor_runs_completed_total",
  1139  		"pyroscope_compactor_runs_failed_total",
  1140  		"pyroscope_compactor_group_compaction_runs_completed_total",
  1141  		"pyroscope_compactor_group_compaction_runs_started_total",
  1142  		"pyroscope_compactor_group_compactions_failures_total",
  1143  		"pyroscope_compactor_group_compactions_total",
  1144  		"pyroscope_compactor_blocks_marked_for_deletion_total",
  1145  	))
  1146  }
  1147  
  1148  func TestMultitenantCompactor_ShouldCompactOnlyUsersOwnedByTheInstanceOnShardingEnabledAndMultipleInstancesRunning(t *testing.T) {
  1149  	t.Parallel()
  1150  
  1151  	numUsers := 100
  1152  
  1153  	// Setup user IDs
  1154  	userIDs := make([]string, 0, numUsers)
  1155  	for i := 1; i <= numUsers; i++ {
  1156  		userIDs = append(userIDs, fmt.Sprintf("user-%d", i))
  1157  	}
  1158  
  1159  	// Mock the bucket to contain all users, each one with one block.
  1160  	bucketClient := mockobjstore.NewMockBucketWithHelper(t)
  1161  	bucketClient.MockIter("", userIDs, nil)
  1162  	for _, userID := range userIDs {
  1163  		bucketClient.MockIter(userID+"/phlaredb/", []string{userID + "/phlaredb/01DTVP434PA9VFXSW2JKB3392D"}, nil)
  1164  		bucketClient.MockIter(userID+"/phlaredb/markers/", nil, nil)
  1165  		bucketClient.MockExists(path.Join(userID, "phlaredb/", bucket.TenantDeletionMarkPath), false, nil)
  1166  		bucketClient.MockGet(userID+"/phlaredb/01DTVP434PA9VFXSW2JKB3392D/meta.json", mockBlockMetaJSON("01DTVP434PA9VFXSW2JKB3392D"), nil)
  1167  		bucketClient.MockGet(userID+"/phlaredb/01DTVP434PA9VFXSW2JKB3392D/deletion-mark.json", "", nil)
  1168  		bucketClient.MockGet(userID+"/phlaredb/01DTVP434PA9VFXSW2JKB3392D/no-compact-mark.json", "", nil)
  1169  		bucketClient.MockGet(userID+"/phlaredb/bucket-index.json.gz", "", nil)
  1170  		bucketClient.MockUpload(userID+"/phlaredb/bucket-index.json.gz", nil)
  1171  	}
  1172  
  1173  	// Create a shared KV Store
  1174  	kvstore, closer := consul.NewInMemoryClient(ring.GetCodec(), log.NewNopLogger(), nil)
  1175  	t.Cleanup(func() { assert.NoError(t, closer.Close()) })
  1176  
  1177  	// Create two compactors
  1178  	var compactors []*MultitenantCompactor
  1179  	var logs []*concurrency.SyncBuffer
  1180  
  1181  	for i := 1; i <= 2; i++ {
  1182  		cfg := prepareConfig(t)
  1183  		cfg.ShardingRing.Common.InstanceID = fmt.Sprintf("compactor-%d", i)
  1184  		cfg.ShardingRing.Common.InstanceAddr = fmt.Sprintf("127.0.0.%d", i)
  1185  		cfg.ShardingRing.WaitStabilityMinDuration = 3 * time.Second
  1186  		cfg.ShardingRing.WaitStabilityMaxDuration = 10 * time.Second
  1187  		cfg.ShardingRing.Common.KVStore.Mock = kvstore
  1188  
  1189  		var limits validation.Limits
  1190  		flagext.DefaultValues(&limits)
  1191  		limits.CompactorTenantShardSize = 1
  1192  		overrides, err := validation.NewOverrides(limits, nil)
  1193  		require.NoError(t, err)
  1194  
  1195  		c, _, tsdbPlanner, l, _ := prepareWithConfigProvider(t, cfg, bucketClient, overrides)
  1196  		defer services.StopAndAwaitTerminated(context.Background(), c) //nolint:errcheck
  1197  
  1198  		compactors = append(compactors, c)
  1199  		logs = append(logs, l)
  1200  
  1201  		// Mock the planner as if there's no compaction to do,
  1202  		// in order to simplify tests (all in all, we just want to
  1203  		// test our logic and not TSDB compactor which we expect to
  1204  		// be already tested).
  1205  		tsdbPlanner.On("Plan", mock.Anything, mock.Anything).Return([]*block.Meta{}, nil)
  1206  	}
  1207  
  1208  	// Start all compactors
  1209  	for _, c := range compactors {
  1210  		require.NoError(t, services.StartAndAwaitRunning(context.Background(), c))
  1211  	}
  1212  
  1213  	// Wait until a run has been completed on each compactor
  1214  	test.Poll(t, 30*time.Second, true, func() interface{} {
  1215  		for _, c := range compactors {
  1216  			if prom_testutil.ToFloat64(c.compactionRunsCompleted) < 1.0 {
  1217  				return false
  1218  			}
  1219  		}
  1220  		return true
  1221  	})
  1222  
  1223  	// Ensure that each user has been compacted by the correct instance
  1224  	for _, userID := range userIDs {
  1225  		_, l, err := findCompactorByUserID(compactors, logs, userID)
  1226  		require.NoError(t, err)
  1227  		assert.Contains(t, l.String(), fmt.Sprintf(`level=info component=compactor msg="successfully compacted user blocks" tenant=%s`, userID))
  1228  	}
  1229  }
  1230  
  1231  func TestMultitenantCompactor_ShouldSkipCompactionForJobsNoMoreOwnedAfterPlanning(t *testing.T) {
  1232  	t.Parallel()
  1233  
  1234  	// Mock the bucket to contain one user with two non-overlapping blocks (we expect two compaction jobs to be scheduled
  1235  	// for the splitting stage).
  1236  	bucketClient := mockobjstore.NewMockBucketWithHelper(t)
  1237  	bucketClient.MockIter("", []string{"user-1"}, nil)
  1238  	bucketClient.MockExists(path.Join("user-1", "phlaredb", bucket.TenantDeletionMarkPath), false, nil)
  1239  	bucketClient.MockIter("user-1/phlaredb/", []string{"user-1/phlaredb/01DTVP434PA9VFXSW2JK000001", "user-1/phlaredb/01DTVP434PA9VFXSW2JK000002"}, nil)
  1240  	bucketClient.MockIter("user-1/phlaredb/markers/", nil, nil)
  1241  	bucketClient.MockGet("user-1/phlaredb/01DTVP434PA9VFXSW2JK000001/meta.json", mockBlockMetaJSONWithTimeRange("01DTVP434PA9VFXSW2JK000001", 1574776800000, 1574784000000), nil)
  1242  	bucketClient.MockGet("user-1/phlaredb/01DTVP434PA9VFXSW2JK000001/deletion-mark.json", "", nil)
  1243  	bucketClient.MockGet("user-1/phlaredb/01DTVP434PA9VFXSW2JK000001/no-compact-mark.json", "", nil)
  1244  	bucketClient.MockGet("user-1/phlaredb/01DTVP434PA9VFXSW2JK000002/meta.json", mockBlockMetaJSONWithTimeRange("01DTVP434PA9VFXSW2JK000002", 1574863200000, 1574870400000), nil)
  1245  	bucketClient.MockGet("user-1/phlaredb/01DTVP434PA9VFXSW2JK000002/deletion-mark.json", "", nil)
  1246  	bucketClient.MockGet("user-1/phlaredb/01DTVP434PA9VFXSW2JK000002/no-compact-mark.json", "", nil)
  1247  	bucketClient.MockGet("user-1/phlaredb/bucket-index.json.gz", "", nil)
  1248  	bucketClient.MockUpload("user-1/phlaredb/bucket-index.json.gz", nil)
  1249  
  1250  	ringStore, closer := consul.NewInMemoryClient(ring.GetCodec(), log.NewNopLogger(), nil)
  1251  	t.Cleanup(func() { assert.NoError(t, closer.Close()) })
  1252  
  1253  	cfg := prepareConfig(t)
  1254  	cfg.CompactionConcurrency = 1
  1255  	cfg.ShardingRing.Common.InstanceID = instanceID
  1256  	cfg.ShardingRing.Common.InstanceAddr = addr
  1257  	cfg.ShardingRing.Common.KVStore.Mock = ringStore
  1258  
  1259  	limits := newMockConfigProvider()
  1260  	limits.splitAndMergeShards = map[string]int{"user-1": 4}
  1261  	limits.splitGroups = map[string]int{"user-1": 4}
  1262  
  1263  	c, _, tsdbPlanner, logs, registry := prepareWithConfigProvider(t, cfg, bucketClient, limits)
  1264  
  1265  	// Mock the planner as if there's no compaction to do, in order to simplify tests.
  1266  	tsdbPlanner.On("Plan", mock.Anything, mock.Anything).Return([]*block.Meta{}, nil).Run(func(args mock.Arguments) {
  1267  		// As soon as the first Plan() is called by the compactor, we do switch
  1268  		// the instance to LEAVING state. This way,  after this call, we expect the compactor
  1269  		// to skip next compaction job because not owned anymore by this instance.
  1270  		require.NoError(t, c.ringLifecycler.ChangeState(context.Background(), ring.LEAVING))
  1271  
  1272  		// Wait until the compactor ring client has updated.
  1273  		test.Poll(t, time.Second, 0, func() interface{} {
  1274  			set, _ := c.ring.GetAllHealthy(RingOp)
  1275  			return len(set.Instances)
  1276  		})
  1277  	})
  1278  
  1279  	require.NoError(t, services.StartAndAwaitRunning(context.Background(), c))
  1280  
  1281  	// Compactor doesn't wait for blocks cleaner to finish, but our test checks for cleaner metrics.
  1282  	require.NoError(t, c.blocksCleaner.AwaitRunning(context.Background()))
  1283  
  1284  	// Wait until a run has completed.
  1285  	test.Poll(t, 5*time.Second, 1.0, func() interface{} {
  1286  		return prom_testutil.ToFloat64(c.compactionRunsCompleted)
  1287  	})
  1288  
  1289  	require.NoError(t, services.StopAndAwaitTerminated(context.Background(), c))
  1290  
  1291  	// We expect only 1 compaction job has been expected, while the 2nd has been skipped.
  1292  	tsdbPlanner.AssertNumberOfCalls(t, "Plan", 1)
  1293  
  1294  	assert.ElementsMatch(t, []string{
  1295  		`level=info component=compactor msg="waiting until compactor is ACTIVE in the ring"`,
  1296  		`level=info component=compactor msg="compactor is ACTIVE in the ring"`,
  1297  		`level=info component=compactor msg="discovering users from bucket"`,
  1298  		`level=info component=compactor msg="discovered users from bucket" users=1`,
  1299  		`level=info component=compactor msg="starting compaction of user blocks" tenant=user-1`,
  1300  		`level=info component=compactor tenant=user-1 msg="start sync of metas"`,
  1301  		`level=info component=compactor tenant=user-1 msg="start of GC"`,
  1302  		`level=info component=compactor tenant=user-1 msg="start of compactions"`,
  1303  		`level=debug component=compactor tenant=user-1 msg="grouper found a compactable blocks group" groupKey=0@17241709254077376921-split-4_of_4-1574776800000-1574784000000 job="stage: split, range start: 1574776800000, range end: 1574784000000, shard: 4_of_4, blocks: 01DTVP434PA9VFXSW2JK000001 (min time: 2019-11-26T14:00:00Z, max time: 2019-11-26T16:00:00Z)"`,
  1304  		`level=debug component=compactor tenant=user-1 msg="grouper found a compactable blocks group" groupKey=0@17241709254077376921-split-1_of_4-1574863200000-1574870400000 job="stage: split, range start: 1574863200000, range end: 1574870400000, shard: 1_of_4, blocks: 01DTVP434PA9VFXSW2JK000002 (min time: 2019-11-27T14:00:00Z, max time: 2019-11-27T16:00:00Z)"`,
  1305  		// The ownership check is failing because, to keep this test simple, we've just switched
  1306  		// the instance state to LEAVING and there are no other instances in the ring.
  1307  		`level=info component=compactor tenant=user-1 groupKey=0@17241709254077376921-split-4_of_4-1574776800000-1574784000000 msg="compaction job succeeded"`,
  1308  		`level=info component=compactor tenant=user-1 msg="skipped compaction because unable to check whether the job is owned by the compactor instance" groupKey=0@17241709254077376921-split-1_of_4-1574863200000-1574870400000 err="at least 1 live replicas required, could only find 0 - unhealthy instances: 1.2.3.4:0"`,
  1309  		`level=info component=compactor tenant=user-1 msg="compaction iterations done"`,
  1310  		`level=info component=compactor msg="successfully compacted user blocks" tenant=user-1`,
  1311  	}, removeIgnoredLogs(strings.Split(strings.TrimSpace(logs.String()), "\n")))
  1312  
  1313  	assert.NoError(t, prom_testutil.GatherAndCompare(registry, strings.NewReader(`
  1314  		# TYPE pyroscope_compactor_runs_started_total counter
  1315  		# HELP pyroscope_compactor_runs_started_total Total number of compaction runs started.
  1316  		pyroscope_compactor_runs_started_total 1
  1317  
  1318  		# TYPE pyroscope_compactor_runs_completed_total counter
  1319  		# HELP pyroscope_compactor_runs_completed_total Total number of compaction runs successfully completed.
  1320  		pyroscope_compactor_runs_completed_total 1
  1321  
  1322  		# TYPE pyroscope_compactor_runs_failed_total counter
  1323  		# HELP pyroscope_compactor_runs_failed_total Total number of compaction runs failed.
  1324  		pyroscope_compactor_runs_failed_total{reason="error"} 0
  1325  		pyroscope_compactor_runs_failed_total{reason="shutdown"} 0
  1326  
  1327  		# HELP pyroscope_compactor_group_compaction_runs_completed_total Total number of group completed compaction runs. This also includes compactor group runs that resulted with no compaction.
  1328  		# TYPE pyroscope_compactor_group_compaction_runs_completed_total counter
  1329  		pyroscope_compactor_group_compaction_runs_completed_total 1
  1330  
  1331  		# HELP pyroscope_compactor_group_compaction_runs_started_total Total number of group compaction attempts.
  1332  		# TYPE pyroscope_compactor_group_compaction_runs_started_total counter
  1333  		pyroscope_compactor_group_compaction_runs_started_total 1
  1334  
  1335  		# HELP pyroscope_compactor_group_compactions_failures_total Total number of failed group compactions.
  1336  		# TYPE pyroscope_compactor_group_compactions_failures_total counter
  1337  		pyroscope_compactor_group_compactions_failures_total 0
  1338  
  1339  		# HELP pyroscope_compactor_group_compactions_total Total number of group compaction attempts that resulted in new block(s).
  1340  		# TYPE pyroscope_compactor_group_compactions_total counter
  1341  		pyroscope_compactor_group_compactions_total 0
  1342  
  1343  		# HELP pyroscope_compactor_blocks_marked_for_deletion_total Total number of blocks marked for deletion in compactor.
  1344  		# TYPE pyroscope_compactor_blocks_marked_for_deletion_total counter
  1345  		pyroscope_compactor_blocks_marked_for_deletion_total{reason="compaction"} 0
  1346  		pyroscope_compactor_blocks_marked_for_deletion_total{reason="partial"} 0
  1347  		pyroscope_compactor_blocks_marked_for_deletion_total{reason="retention"} 0
  1348  	`),
  1349  		"pyroscope_compactor_runs_started_total",
  1350  		"pyroscope_compactor_runs_completed_total",
  1351  		"pyroscope_compactor_runs_failed_total",
  1352  		"pyroscope_compactor_group_compaction_runs_completed_total",
  1353  		"pyroscope_compactor_group_compaction_runs_started_total",
  1354  		"pyroscope_compactor_group_compactions_failures_total",
  1355  		"pyroscope_compactor_group_compactions_total",
  1356  		"pyroscope_compactor_blocks_marked_for_deletion_total",
  1357  	))
  1358  }
  1359  
  1360  func TestMultitenantCompactor_ShouldSkipCompactionForJobsWithFirstLevelCompactionBlocksAndWaitPeriodNotElapsed(t *testing.T) {
  1361  	t.Parallel()
  1362  
  1363  	storageDir := t.TempDir()
  1364  	bucketClient, err := filesystem.NewBucket(storageDir)
  1365  	require.NoError(t, err)
  1366  	user1Meta1 := createDBBlock(t, bucketClient, "user-1", 0, (2 * time.Hour).Milliseconds(), 10, nil)
  1367  	user1Meta2 := createDBBlock(t, bucketClient, "user-1", 0, (2 * time.Hour).Milliseconds(), 10, nil)
  1368  	user2Meta1 := createDBBlock(t, bucketClient, "user-2", 0, (2 * time.Hour).Milliseconds(), 10, nil)
  1369  	user2Meta2 := createDBBlock(t, bucketClient, "user-2", 0, (2 * time.Hour).Milliseconds(), 10, nil)
  1370  
  1371  	// Mock the last modified timestamp returned for each of the block's meta.json.
  1372  	const waitPeriod = 10 * time.Minute
  1373  	mockClient := &bucketWithMockedAttributes{
  1374  		Bucket: bucketClient,
  1375  		customAttributes: map[string]objstore.ObjectAttributes{
  1376  			path.Join("user-1", "phlaredb", user1Meta1.String(), block.MetaFilename): {LastModified: time.Now().Add(-20 * time.Minute)},
  1377  			path.Join("user-1", "phlaredb", user1Meta2.String(), block.MetaFilename): {LastModified: time.Now().Add(-20 * time.Minute)},
  1378  			path.Join("user-2", "phlaredb", user2Meta1.String(), block.MetaFilename): {LastModified: time.Now().Add(-20 * time.Minute)},
  1379  			path.Join("user-2", "phlaredb", user2Meta2.String(), block.MetaFilename): {LastModified: time.Now().Add(-5 * time.Minute)},
  1380  		},
  1381  	}
  1382  
  1383  	cfg := prepareConfig(t)
  1384  	cfg.CompactionWaitPeriod = waitPeriod
  1385  	c, _, tsdbPlanner, logs, registry := prepare(t, cfg, mockClient)
  1386  
  1387  	// Mock the planner as if there's no compaction to do, in order to simplify tests.
  1388  	tsdbPlanner.On("Plan", mock.Anything, mock.Anything).Return([]*block.Meta{}, nil)
  1389  
  1390  	require.NoError(t, services.StartAndAwaitRunning(context.Background(), c))
  1391  
  1392  	// Compactor doesn't wait for blocks cleaner to finish, but our test checks for cleaner metrics.
  1393  	require.NoError(t, c.blocksCleaner.AwaitRunning(context.Background()))
  1394  
  1395  	// Wait until a run has completed.
  1396  	test.Poll(t, 5*time.Second, 1.0, func() interface{} {
  1397  		return prom_testutil.ToFloat64(c.compactionRunsCompleted)
  1398  	})
  1399  
  1400  	require.NoError(t, services.StopAndAwaitTerminated(context.Background(), c))
  1401  
  1402  	// We expect only 1 compaction job has been expected, while the 2nd has been skipped.
  1403  	tsdbPlanner.AssertNumberOfCalls(t, "Plan", 1)
  1404  
  1405  	// Ensure the skipped compaction job is the expected one.
  1406  	assert.Contains(t, strings.Split(strings.TrimSpace(logs.String()), "\n"),
  1407  		fmt.Sprintf(`level=info component=compactor tenant=user-2 msg="skipping compaction job because blocks in this job were uploaded too recently (within wait period)" groupKey=0@17241709254077376921-merge--0-7200000 waitPeriodNotElapsedFor="%s (min time: 1970-01-01T00:00:00Z, max time: 1970-01-01T02:00:00Z)"`, user2Meta2.String()))
  1408  
  1409  	assert.NoError(t, prom_testutil.GatherAndCompare(registry, strings.NewReader(`
  1410  		# TYPE pyroscope_compactor_runs_started_total counter
  1411  		# HELP pyroscope_compactor_runs_started_total Total number of compaction runs started.
  1412  		pyroscope_compactor_runs_started_total 1
  1413  
  1414  		# TYPE pyroscope_compactor_runs_completed_total counter
  1415  		# HELP pyroscope_compactor_runs_completed_total Total number of compaction runs successfully completed.
  1416  		pyroscope_compactor_runs_completed_total 1
  1417  
  1418  		# TYPE pyroscope_compactor_runs_failed_total counter
  1419  		# HELP pyroscope_compactor_runs_failed_total Total number of compaction runs failed.
  1420  		pyroscope_compactor_runs_failed_total{reason="error"} 0
  1421  		pyroscope_compactor_runs_failed_total{reason="shutdown"} 0
  1422  
  1423  		# HELP pyroscope_compactor_group_compaction_runs_completed_total Total number of group completed compaction runs. This also includes compactor group runs that resulted with no compaction.
  1424  		# TYPE pyroscope_compactor_group_compaction_runs_completed_total counter
  1425  		pyroscope_compactor_group_compaction_runs_completed_total 1
  1426  
  1427  		# HELP pyroscope_compactor_group_compaction_runs_started_total Total number of group compaction attempts.
  1428  		# TYPE pyroscope_compactor_group_compaction_runs_started_total counter
  1429  		pyroscope_compactor_group_compaction_runs_started_total 1
  1430  
  1431  		# HELP pyroscope_compactor_group_compactions_failures_total Total number of failed group compactions.
  1432  		# TYPE pyroscope_compactor_group_compactions_failures_total counter
  1433  		pyroscope_compactor_group_compactions_failures_total 0
  1434  
  1435  		# HELP pyroscope_compactor_group_compactions_total Total number of group compaction attempts that resulted in new block(s).
  1436  		# TYPE pyroscope_compactor_group_compactions_total counter
  1437  		pyroscope_compactor_group_compactions_total 0
  1438  
  1439  		# HELP pyroscope_compactor_blocks_marked_for_deletion_total Total number of blocks marked for deletion in compactor.
  1440  		# TYPE pyroscope_compactor_blocks_marked_for_deletion_total counter
  1441  		pyroscope_compactor_blocks_marked_for_deletion_total{reason="compaction"} 0
  1442  		pyroscope_compactor_blocks_marked_for_deletion_total{reason="partial"} 0
  1443  		pyroscope_compactor_blocks_marked_for_deletion_total{reason="retention"} 0
  1444  	`),
  1445  		"pyroscope_compactor_runs_started_total",
  1446  		"pyroscope_compactor_runs_completed_total",
  1447  		"pyroscope_compactor_runs_failed_total",
  1448  		"pyroscope_compactor_group_compaction_runs_completed_total",
  1449  		"pyroscope_compactor_group_compaction_runs_started_total",
  1450  		"pyroscope_compactor_group_compactions_failures_total",
  1451  		"pyroscope_compactor_group_compactions_total",
  1452  		"pyroscope_compactor_blocks_marked_for_deletion_total",
  1453  	))
  1454  }
  1455  
  1456  func createCustomBlock(t *testing.T, bkt objstore.Bucket, userID string, externalLabels map[string]string, generator func() []*testhelper.ProfileBuilder) ulid.ULID {
  1457  	meta, dir := testutil.CreateBlock(t, generator)
  1458  	blockLocalPath := filepath.Join(dir, meta.ULID.String())
  1459  
  1460  	meta.Source = "test"
  1461  	for k, v := range externalLabels {
  1462  		meta.Labels[k] = v
  1463  	}
  1464  	_, err := meta.WriteToFile(log.NewNopLogger(), blockLocalPath)
  1465  	require.NoError(t, err)
  1466  
  1467  	// Copy the block files to the bucket.
  1468  	require.NoError(t, filepath.Walk(blockLocalPath, func(file string, info os.FileInfo, err error) error {
  1469  		if err != nil {
  1470  			return err
  1471  		}
  1472  		if info.IsDir() {
  1473  			return nil
  1474  		}
  1475  
  1476  		// Read the file content in memory.
  1477  		content, err := os.ReadFile(file)
  1478  		if err != nil {
  1479  			return err
  1480  		}
  1481  
  1482  		// Upload it to the bucket.
  1483  		relPath, err := filepath.Rel(blockLocalPath, file)
  1484  		if err != nil {
  1485  			return err
  1486  		}
  1487  
  1488  		return bkt.Upload(context.Background(), path.Join(userID, "phlaredb/", meta.ULID.String(), relPath), bytes.NewReader(content))
  1489  	}))
  1490  
  1491  	return meta.ULID
  1492  }
  1493  
  1494  func createDBBlock(t *testing.T, bkt objstore.Bucket, userID string, minT, maxT int64, numSeries int, externalLabels map[string]string) ulid.ULID {
  1495  	return createCustomBlock(t, bkt, userID, externalLabels, func() []*testhelper.ProfileBuilder {
  1496  		result := []*testhelper.ProfileBuilder{}
  1497  		appendSample := func(seriesID int, ts int64) {
  1498  			profile := testhelper.NewProfileBuilder(ts*int64(time.Millisecond)).
  1499  				CPUProfile().
  1500  				WithLabels(
  1501  					"series_id", strconv.Itoa(seriesID),
  1502  				).ForStacktraceString("foo", "bar", "baz").AddSamples(1)
  1503  			result = append(result, profile)
  1504  		}
  1505  
  1506  		seriesID := 0
  1507  
  1508  		// Append a sample for each series, spreading it between minT and maxT-1 (both included).
  1509  		// Since we append one more series below, here we create N-1 series.
  1510  		if numSeries > 1 {
  1511  			for ts := minT; ts <= maxT; ts += (maxT - minT) / int64(numSeries-1) {
  1512  				appendSample(seriesID, ts)
  1513  				seriesID++
  1514  			}
  1515  		} else {
  1516  			appendSample(seriesID, minT)
  1517  		}
  1518  		// Guarantee a series with a sample at time maxT
  1519  		appendSample(seriesID, maxT)
  1520  		return result
  1521  	})
  1522  }
  1523  
  1524  func createDeletionMark(t *testing.T, bkt objstore.Bucket, userID string, blockID ulid.ULID, deletionTime time.Time) {
  1525  	content := mockDeletionMarkJSON(blockID.String(), deletionTime)
  1526  	blockPath := path.Join(userID, "phlaredb/", blockID.String())
  1527  	markPath := path.Join(blockPath, block.DeletionMarkFilename)
  1528  
  1529  	require.NoError(t, bkt.Upload(context.Background(), markPath, strings.NewReader(content)))
  1530  }
  1531  
  1532  func findCompactorByUserID(compactors []*MultitenantCompactor, logs []*concurrency.SyncBuffer, userID string) (*MultitenantCompactor, *concurrency.SyncBuffer, error) {
  1533  	var compactor *MultitenantCompactor
  1534  	var log *concurrency.SyncBuffer
  1535  
  1536  	for i, c := range compactors {
  1537  		owned, err := c.shardingStrategy.compactorOwnUser(userID)
  1538  		if err != nil {
  1539  			return nil, nil, err
  1540  		}
  1541  
  1542  		// Ensure the user is not owned by multiple compactors
  1543  		if owned && compactor != nil {
  1544  			return nil, nil, fmt.Errorf("user %s owned by multiple compactors", userID)
  1545  		}
  1546  		if owned {
  1547  			compactor = c
  1548  			log = logs[i]
  1549  		}
  1550  	}
  1551  
  1552  	// Return an error if we've not been able to find it
  1553  	if compactor == nil {
  1554  		return nil, nil, fmt.Errorf("user %s not owned by any compactor", userID)
  1555  	}
  1556  
  1557  	return compactor, log, nil
  1558  }
  1559  
  1560  func removeIgnoredLogs(input []string) []string {
  1561  	ignoredLogStringsMap := map[string]struct{}{
  1562  		// Since we moved to the component logger from the global logger for the ring in dskit these lines are now expected but are just ring setup information.
  1563  		`level=info component=compactor msg="ring doesn't exist in KV store yet"`:                                                                                 {},
  1564  		`level=info component=compactor msg="not loading tokens from file, tokens file path is empty"`:                                                            {},
  1565  		`level=info component=compactor msg="tokens verification succeeded" ring=compactor`:                                                                       {},
  1566  		`level=info component=compactor msg="waiting stable tokens" ring=compactor`:                                                                               {},
  1567  		`level=info component=compactor msg="instance not found in ring, adding with no tokens" ring=compactor`:                                                   {},
  1568  		`level=debug component=compactor msg="JoinAfter expired" ring=compactor`:                                                                                  {},
  1569  		`level=info component=compactor msg="auto-joining cluster after timeout" ring=compactor`:                                                                  {},
  1570  		`level=info component=compactor msg="lifecycler loop() exited gracefully" ring=compactor`:                                                                 {},
  1571  		`level=info component=compactor msg="changing instance state from" old_state=ACTIVE new_state=LEAVING ring=compactor`:                                     {},
  1572  		`level=error component=compactor msg="failed to set state to LEAVING" ring=compactor err="Changing instance state from LEAVING -> LEAVING is disallowed"`: {},
  1573  		`level=error component=compactor msg="failed to set state to LEAVING" ring=compactor err="Changing instance state from JOINING -> LEAVING is disallowed"`: {},
  1574  		`level=info component=compactor msg="unregistering instance from ring" ring=compactor`:                                                                    {},
  1575  		`level=info component=compactor msg="instance removed from the ring" ring=compactor`:                                                                      {},
  1576  		`level=info component=compactor msg="observing tokens before going ACTIVE" ring=compactor`:                                                                {},
  1577  		`level=info component=compactor msg="lifecycler entering final sleep before shutdown" final_sleep=0s`:                                                     {},
  1578  		`level=info component=compactor msg="ring lifecycler is shutting down" ring=compactor`:                                                                    {},
  1579  	}
  1580  
  1581  	out := make([]string, 0, len(input))
  1582  
  1583  	for i := 0; i < len(input); i++ {
  1584  		log := input[i]
  1585  		if strings.Contains(log, "block.MetaFetcher") || strings.Contains(log, "instance not found in the ring") {
  1586  			continue
  1587  		}
  1588  
  1589  		if _, exists := ignoredLogStringsMap[log]; exists {
  1590  			continue
  1591  		}
  1592  
  1593  		out = append(out, log)
  1594  	}
  1595  
  1596  	return out
  1597  }
  1598  
  1599  func prepareConfig(t *testing.T) Config {
  1600  	compactorCfg := Config{}
  1601  	flagext.DefaultValues(&compactorCfg)
  1602  	// Use multiple range for testing.
  1603  	compactorCfg.BlockRanges = DurationList{2 * time.Hour, 12 * time.Hour, 24 * time.Hour}
  1604  
  1605  	compactorCfg.retryMinBackoff = 0
  1606  	compactorCfg.retryMaxBackoff = 0
  1607  
  1608  	// Use settings that ensure things will be done concurrently, verifying ordering assumptions.
  1609  	compactorCfg.MaxOpeningBlocksConcurrency = 3
  1610  
  1611  	// Do not wait for ring stability by default, in order to speed up tests.
  1612  	compactorCfg.ShardingRing.WaitStabilityMinDuration = 0
  1613  	compactorCfg.ShardingRing.WaitStabilityMaxDuration = 0
  1614  
  1615  	// Set lower timeout for waiting on compactor to become ACTIVE in the ring for unit tests
  1616  	compactorCfg.ShardingRing.WaitActiveInstanceTimeout = 5 * time.Second
  1617  
  1618  	// Inject default KV store. Must be overridden if "real" sharding is required.
  1619  	inmem, closer := consul.NewInMemoryClient(ring.GetCodec(), log.NewNopLogger(), nil)
  1620  	t.Cleanup(func() { _ = closer.Close() })
  1621  	compactorCfg.ShardingRing.Common.KVStore.Mock = inmem
  1622  	compactorCfg.ShardingRing.Common.InstanceAddr = "localhost"
  1623  
  1624  	// The new default is 25m, but tests rely on the previous value of 0s
  1625  	compactorCfg.CompactionWaitPeriod = 0
  1626  
  1627  	return compactorCfg
  1628  }
  1629  
  1630  func prepare(t *testing.T, compactorCfg Config, bucketClient objstore.Bucket) (*MultitenantCompactor, *blockCompactorMock, *tsdbPlannerMock, *concurrency.SyncBuffer, prometheus.Gatherer) {
  1631  	var limits validation.Limits
  1632  	flagext.DefaultValues(&limits)
  1633  	overrides, err := validation.NewOverrides(limits, nil)
  1634  	require.NoError(t, err)
  1635  
  1636  	return prepareWithConfigProvider(t, compactorCfg, bucketClient, overrides)
  1637  }
  1638  
  1639  func prepareWithConfigProvider(t *testing.T, compactorCfg Config, bucketClient objstore.Bucket, limits ConfigProvider) (*MultitenantCompactor, *blockCompactorMock, *tsdbPlannerMock, *concurrency.SyncBuffer, prometheus.Gatherer) {
  1640  	// Create a temporary directory for compactor data.
  1641  	dataDir := t.TempDir()
  1642  
  1643  	compactorCfg.DataDir = dataDir
  1644  
  1645  	blockCompactor := &blockCompactorMock{}
  1646  	tsdbPlanner := &tsdbPlannerMock{}
  1647  	logs := &concurrency.SyncBuffer{}
  1648  	logger := &componentLogger{component: "compactor", log: log.NewLogfmtLogger(logs)}
  1649  	registry := prometheus.NewRegistry()
  1650  
  1651  	blocksCompactorFactory := func(ctx context.Context, cfg Config, cfgProvider ConfigProvider, userID string, logger log.Logger, metrics *CompactorMetrics) (Compactor, error) {
  1652  		return blockCompactor, nil
  1653  	}
  1654  
  1655  	blocksPlannerFactory := func(cfg Config) Planner {
  1656  		return tsdbPlanner
  1657  	}
  1658  
  1659  	c, err := newMultitenantCompactor(compactorCfg, pyroscope_objstore.NewBucket(bucketClient), limits, logger, registry, splitAndMergeGrouperFactory, blocksCompactorFactory, blocksPlannerFactory)
  1660  	require.NoError(t, err)
  1661  
  1662  	return c, blockCompactor, tsdbPlanner, logs, registry
  1663  }
  1664  
  1665  type componentLogger struct {
  1666  	component string
  1667  	log       log.Logger
  1668  }
  1669  
  1670  func (c *componentLogger) Log(keyvals ...interface{}) error {
  1671  	// Remove duration fields.
  1672  	for ix := 0; ix+1 < len(keyvals); {
  1673  		k := keyvals[ix]
  1674  
  1675  		ks, ok := k.(string)
  1676  		if !ok {
  1677  			ix += 2
  1678  			continue
  1679  		}
  1680  
  1681  		if ks == "duration" || ks == "duration_ms" {
  1682  			keyvals = append(keyvals[:ix], keyvals[ix+2:]...)
  1683  		} else {
  1684  			ix += 2
  1685  		}
  1686  	}
  1687  
  1688  	for ix := 0; ix+1 < len(keyvals); ix += 2 {
  1689  		k := keyvals[ix]
  1690  		v := keyvals[ix+1]
  1691  
  1692  		ks, ok := k.(string)
  1693  		if !ok {
  1694  			continue
  1695  		}
  1696  		vs, ok := v.(string)
  1697  		if !ok {
  1698  			continue
  1699  		}
  1700  		if ks == "component" && vs == c.component {
  1701  			return c.log.Log(keyvals...)
  1702  		}
  1703  	}
  1704  	return nil
  1705  }
  1706  
  1707  type blockCompactorMock struct {
  1708  	mock.Mock
  1709  }
  1710  
  1711  func (m *blockCompactorMock) CompactWithSplitting(ctx context.Context, dest string, dirs []string, shardCount, stageSize uint64) (result []ulid.ULID, _ error) {
  1712  	args := m.Called(ctx, dest, dirs, shardCount, stageSize)
  1713  	return args.Get(0).([]ulid.ULID), args.Error(1)
  1714  }
  1715  
  1716  type tsdbPlannerMock struct {
  1717  	mock.Mock
  1718  }
  1719  
  1720  func (m *tsdbPlannerMock) Plan(ctx context.Context, metasByMinTime []*block.Meta) ([]*block.Meta, error) {
  1721  	args := m.Called(ctx, metasByMinTime)
  1722  	return args.Get(0).([]*block.Meta), args.Error(1)
  1723  }
  1724  
  1725  func mockBlockMetaJSON(id string) string {
  1726  	return mockBlockMetaJSONWithTimeRange(id, 1574776800000, 1574784000000)
  1727  }
  1728  
  1729  func mockBlockMetaJSONWithTimeRange(id string, mint, maxt int64) string {
  1730  	return mockBlockMetaJSONWithTimeRangeAndLabels(id, mint, maxt, nil)
  1731  }
  1732  
  1733  func mockBlockMetaJSONWithTimeRangeAndLabels(id string, mint, maxt int64, lbls map[string]string) string {
  1734  	content, err := json.Marshal(blockMeta(id, mint, maxt, lbls))
  1735  	if err != nil {
  1736  		panic("failed to marshal mocked block meta")
  1737  	}
  1738  	return string(content)
  1739  }
  1740  
  1741  func blockMeta(id string, mint, maxt int64, lbls map[string]string) *block.Meta {
  1742  	return &block.Meta{
  1743  		Version: 1,
  1744  		ULID:    ulid.MustParse(id),
  1745  		MinTime: model.Time(mint),
  1746  		MaxTime: model.Time(maxt),
  1747  		Compaction: block.BlockMetaCompaction{
  1748  			Level:   1,
  1749  			Sources: []ulid.ULID{ulid.MustParse(id)},
  1750  		},
  1751  
  1752  		Labels: lbls,
  1753  	}
  1754  }
  1755  
  1756  func mockDeletionMarkJSON(id string, deletionTime time.Time) string {
  1757  	meta := block.DeletionMark{
  1758  		Version:      block.DeletionMarkVersion1,
  1759  		ID:           ulid.MustParse(id),
  1760  		DeletionTime: deletionTime.Unix(),
  1761  	}
  1762  
  1763  	content, err := json.Marshal(meta)
  1764  	if err != nil {
  1765  		panic("failed to marshal mocked block meta")
  1766  	}
  1767  
  1768  	return string(content)
  1769  }
  1770  
  1771  func TestMultitenantCompactor_DeleteLocalSyncFiles(t *testing.T) {
  1772  	numUsers := 10
  1773  
  1774  	// Setup user IDs
  1775  	userIDs := make([]string, 0, numUsers)
  1776  	for i := 1; i <= numUsers; i++ {
  1777  		userIDs = append(userIDs, fmt.Sprintf("user-%d", i))
  1778  	}
  1779  
  1780  	inmem := objstore.NewInMemBucket()
  1781  	for _, userID := range userIDs {
  1782  		id, err := ulid.New(ulid.Now(), rand.Reader)
  1783  		require.NoError(t, err)
  1784  		require.NoError(t, inmem.Upload(context.Background(), userID+"/"+id.String()+"/meta.json", strings.NewReader(mockBlockMetaJSON(id.String()))))
  1785  	}
  1786  
  1787  	// Create a shared KV Store
  1788  	kvstore, closer := consul.NewInMemoryClient(ring.GetCodec(), log.NewNopLogger(), nil)
  1789  	t.Cleanup(func() { assert.NoError(t, closer.Close()) })
  1790  
  1791  	// Create two compactors
  1792  	var compactors []*MultitenantCompactor
  1793  
  1794  	for i := 1; i <= 2; i++ {
  1795  		cfg := prepareConfig(t)
  1796  		cfg.CompactionInterval = 10 * time.Minute // We will only call compaction manually.
  1797  
  1798  		cfg.ShardingRing.Common.InstanceID = fmt.Sprintf("compactor-%d", i)
  1799  		cfg.ShardingRing.Common.InstanceAddr = fmt.Sprintf("127.0.0.%d", i)
  1800  		cfg.ShardingRing.WaitStabilityMinDuration = 3 * time.Second
  1801  		cfg.ShardingRing.WaitStabilityMaxDuration = 10 * time.Second
  1802  		cfg.ShardingRing.Common.KVStore.Mock = kvstore
  1803  
  1804  		// Each compactor will get its own temp dir for storing local files.
  1805  		var limits validation.Limits
  1806  		flagext.DefaultValues(&limits)
  1807  		limits.CompactorTenantShardSize = 1 // Each tenant will belong to single compactor only.
  1808  		overrides, err := validation.NewOverrides(limits, nil)
  1809  		require.NoError(t, err)
  1810  
  1811  		c, _, tsdbPlanner, _, _ := prepareWithConfigProvider(t, cfg, inmem, overrides)
  1812  		t.Cleanup(func() {
  1813  			require.NoError(t, services.StopAndAwaitTerminated(context.Background(), c))
  1814  		})
  1815  
  1816  		compactors = append(compactors, c)
  1817  
  1818  		// Mock the planner as if there's no compaction to do,
  1819  		// in order to simplify tests (all in all, we just want to
  1820  		// test our logic and not TSDB compactor which we expect to
  1821  		// be already tested).
  1822  		tsdbPlanner.On("Plan", mock.Anything, mock.Anything).Return([]*block.Meta{}, nil)
  1823  	}
  1824  
  1825  	require.Equal(t, 2, len(compactors))
  1826  	c1 := compactors[0]
  1827  	c2 := compactors[1]
  1828  
  1829  	// Start first compactor
  1830  	require.NoError(t, services.StartAndAwaitRunning(context.Background(), c1))
  1831  
  1832  	// Wait until a run has been completed on first compactor. This happens as soon as compactor starts.
  1833  	test.Poll(t, 10*time.Second, 1.0, func() interface{} {
  1834  		return prom_testutil.ToFloat64(c1.compactionRunsCompleted)
  1835  	})
  1836  
  1837  	require.NoError(t, os.Mkdir(c1.metaSyncDirForUser("new-user"), 0o600))
  1838  
  1839  	// Verify that first compactor has synced all the users, plus there is one extra we have just created.
  1840  	require.Equal(t, numUsers+1, len(c1.listTenantsWithMetaSyncDirectories()))
  1841  
  1842  	// Now start second compactor, and wait until it runs compaction.
  1843  	require.NoError(t, services.StartAndAwaitRunning(context.Background(), c2))
  1844  	test.Poll(t, 10*time.Second, 1.0, func() interface{} {
  1845  		return prom_testutil.ToFloat64(c2.compactionRunsCompleted)
  1846  	})
  1847  
  1848  	// Let's check how many users second compactor has.
  1849  	c2Users := len(c2.listTenantsWithMetaSyncDirectories())
  1850  	require.NotZero(t, c2Users)
  1851  
  1852  	// Force new compaction cycle on first compactor. It will run the cleanup of un-owned users at the end of compaction cycle.
  1853  	c1.compactUsers(context.Background())
  1854  	c1Users := len(c1.listTenantsWithMetaSyncDirectories())
  1855  
  1856  	// Now compactor 1 should have cleaned old sync files.
  1857  	require.NotEqual(t, numUsers, c1Users)
  1858  	require.Equal(t, numUsers, c1Users+c2Users)
  1859  }
  1860  
  1861  func TestMultitenantCompactor_ShouldFailCompactionOnTimeout(t *testing.T) {
  1862  	t.Parallel()
  1863  
  1864  	// Mock the bucket
  1865  	bucketClient := mockobjstore.NewMockBucketWithHelper(t)
  1866  	bucketClient.MockIter("", []string{}, nil)
  1867  
  1868  	ringStore, closer := consul.NewInMemoryClient(ring.GetCodec(), log.NewNopLogger(), nil)
  1869  	t.Cleanup(func() { assert.NoError(t, closer.Close()) })
  1870  
  1871  	cfg := prepareConfig(t)
  1872  	cfg.ShardingRing.Common.InstanceID = instanceID
  1873  	cfg.ShardingRing.Common.InstanceAddr = addr
  1874  	cfg.ShardingRing.Common.KVStore.Mock = ringStore
  1875  
  1876  	// Set ObservePeriod to longer than the timeout period to mock a timeout while waiting on ring to become ACTIVE
  1877  	cfg.ShardingRing.ObservePeriod = time.Second * 10
  1878  
  1879  	c, _, _, _, _ := prepare(t, cfg, bucketClient)
  1880  
  1881  	// Try to start the compactor with a bad consul kv-store. The
  1882  	err := services.StartAndAwaitRunning(context.Background(), c)
  1883  
  1884  	// Assert that the compactor timed out
  1885  	require.ErrorIs(t, err, context.DeadlineExceeded)
  1886  }
  1887  
  1888  type ownUserReason int
  1889  
  1890  const (
  1891  	ownUserReasonBlocksCleaner ownUserReason = iota
  1892  	ownUserReasonCompactor
  1893  )
  1894  
  1895  func TestOwnUser(t *testing.T) {
  1896  	type testCase struct {
  1897  		compactors      int
  1898  		enabledUsers    []string
  1899  		disabledUsers   []string
  1900  		compactorShards map[string]int
  1901  
  1902  		check func(t *testing.T, comps []*MultitenantCompactor)
  1903  	}
  1904  
  1905  	const user1 = "user1"
  1906  	const user2 = "another-user"
  1907  
  1908  	testCases := map[string]testCase{
  1909  		"5 compactors, sharding enabled, no compactor shard size": {
  1910  			compactors:      5,
  1911  			compactorShards: nil, // no limits
  1912  
  1913  			check: func(t *testing.T, comps []*MultitenantCompactor) {
  1914  				require.Len(t, owningCompactors(t, comps, user1, ownUserReasonCompactor), 5)
  1915  				require.Len(t, owningCompactors(t, comps, user1, ownUserReasonBlocksCleaner), 1)
  1916  
  1917  				require.Len(t, owningCompactors(t, comps, user2, ownUserReasonCompactor), 5)
  1918  				require.Len(t, owningCompactors(t, comps, user2, ownUserReasonBlocksCleaner), 1)
  1919  			},
  1920  		},
  1921  
  1922  		"10 compactors, sharding enabled, with non-zero shard sizes": {
  1923  			compactors:      10,
  1924  			compactorShards: map[string]int{user1: 2, user2: 3},
  1925  
  1926  			check: func(t *testing.T, comps []*MultitenantCompactor) {
  1927  				require.Len(t, owningCompactors(t, comps, user1, ownUserReasonCompactor), 2)
  1928  				require.Len(t, owningCompactors(t, comps, user1, ownUserReasonBlocksCleaner), 1)
  1929  				// Blocks cleanup is done by one of the compactors that "own" the user.
  1930  				require.Subset(t, owningCompactors(t, comps, user1, ownUserReasonCompactor), owningCompactors(t, comps, user1, ownUserReasonBlocksCleaner))
  1931  
  1932  				require.Len(t, owningCompactors(t, comps, user2, ownUserReasonCompactor), 3)
  1933  				require.Len(t, owningCompactors(t, comps, user2, ownUserReasonBlocksCleaner), 1)
  1934  				// Blocks cleanup is done by one of the compactors that "own" the user.
  1935  				require.Subset(t, owningCompactors(t, comps, user2, ownUserReasonCompactor), owningCompactors(t, comps, user2, ownUserReasonBlocksCleaner))
  1936  			},
  1937  		},
  1938  
  1939  		"10 compactors, sharding enabled, with zero shard size": {
  1940  			compactors:      10,
  1941  			compactorShards: map[string]int{user2: 0},
  1942  
  1943  			check: func(t *testing.T, comps []*MultitenantCompactor) {
  1944  				require.Len(t, owningCompactors(t, comps, user2, ownUserReasonCompactor), 10)
  1945  				require.Len(t, owningCompactors(t, comps, user2, ownUserReasonBlocksCleaner), 1)
  1946  			},
  1947  		},
  1948  	}
  1949  
  1950  	for name, tc := range testCases {
  1951  		tc := tc
  1952  		t.Run(name, func(t *testing.T) {
  1953  			t.Parallel()
  1954  
  1955  			kvStore, closer := consul.NewInMemoryClient(ring.GetCodec(), log.NewNopLogger(), nil)
  1956  			t.Cleanup(func() { assert.NoError(t, closer.Close()) })
  1957  
  1958  			inmem := objstore.NewInMemBucket()
  1959  
  1960  			compactors := []*MultitenantCompactor(nil)
  1961  
  1962  			for i := 0; i < tc.compactors; i++ {
  1963  				cfg := prepareConfig(t)
  1964  				cfg.CompactionInterval = 10 * time.Minute // We will only call compaction manually.
  1965  
  1966  				cfg.EnabledTenants = tc.enabledUsers
  1967  				cfg.DisabledTenants = tc.disabledUsers
  1968  
  1969  				cfg.ShardingRing.Common.InstanceID = fmt.Sprintf("compactor-%d", i)
  1970  				cfg.ShardingRing.Common.InstanceAddr = fmt.Sprintf("127.0.0.%d", i)
  1971  				// No need to wait. All compactors are started before we do any tests, and we wait for all of them
  1972  				// to appear in all rings.
  1973  				cfg.ShardingRing.WaitStabilityMinDuration = 0
  1974  				cfg.ShardingRing.WaitStabilityMaxDuration = 0
  1975  				cfg.ShardingRing.Common.KVStore.Mock = kvStore
  1976  
  1977  				limits := newMockConfigProvider()
  1978  				limits.instancesShardSize = tc.compactorShards
  1979  
  1980  				c, _, _, _, _ := prepareWithConfigProvider(t, cfg, inmem, limits)
  1981  				require.NoError(t, services.StartAndAwaitRunning(context.Background(), c))
  1982  				t.Cleanup(stopServiceFn(t, c))
  1983  
  1984  				compactors = append(compactors, c)
  1985  			}
  1986  
  1987  			// Make sure all compactors see all other compactors in the ring before running tests.
  1988  			test.Poll(t, 2*time.Second, true, func() interface{} {
  1989  				for _, c := range compactors {
  1990  					rs, err := c.ring.GetAllHealthy(RingOp)
  1991  					if err != nil {
  1992  						return false
  1993  					}
  1994  					if len(rs.Instances) != len(compactors) {
  1995  						return false
  1996  					}
  1997  				}
  1998  				return true
  1999  			})
  2000  
  2001  			tc.check(t, compactors)
  2002  		})
  2003  	}
  2004  }
  2005  
  2006  func owningCompactors(t *testing.T, comps []*MultitenantCompactor, user string, reason ownUserReason) []string {
  2007  	result := []string(nil)
  2008  	for _, c := range comps {
  2009  		var f func(string) (bool, error)
  2010  		if reason == ownUserReasonCompactor {
  2011  			f = c.shardingStrategy.compactorOwnUser
  2012  		} else {
  2013  			f = c.shardingStrategy.blocksCleanerOwnUser
  2014  		}
  2015  		ok, err := f(user)
  2016  		require.NoError(t, err)
  2017  		if ok {
  2018  			// We set instance ID even when not using sharding. It makes output nicer, since
  2019  			// calling method only wants to see some identifier.
  2020  			result = append(result, c.compactorCfg.ShardingRing.Common.InstanceID)
  2021  		}
  2022  	}
  2023  	return result
  2024  }
  2025  
  2026  func stopServiceFn(t *testing.T, serv services.Service) func() {
  2027  	return func() {
  2028  		require.NoError(t, services.StopAndAwaitTerminated(context.Background(), serv))
  2029  	}
  2030  }
  2031  
  2032  type bucketWithMockedAttributes struct {
  2033  	objstore.Bucket
  2034  
  2035  	customAttributes map[string]objstore.ObjectAttributes
  2036  }
  2037  
  2038  func (b *bucketWithMockedAttributes) Attributes(ctx context.Context, name string) (objstore.ObjectAttributes, error) {
  2039  	if attrs, ok := b.customAttributes[name]; ok {
  2040  		return attrs, nil
  2041  	}
  2042  
  2043  	return b.Bucket.Attributes(ctx, name)
  2044  }