github.com/grafana/pyroscope@v1.18.0/pkg/metastore/index/cleaner/retention/retention_test.go (about)

     1  package retention
     2  
     3  import (
     4  	"iter"
     5  	"testing"
     6  	"time"
     7  
     8  	"github.com/go-kit/log"
     9  	"github.com/prometheus/common/model"
    10  	"github.com/stretchr/testify/assert"
    11  	"github.com/stretchr/testify/require"
    12  	"go.etcd.io/bbolt"
    13  
    14  	metastorev1 "github.com/grafana/pyroscope/api/gen/proto/go/metastore/v1"
    15  	indexstore "github.com/grafana/pyroscope/pkg/metastore/index/store"
    16  	"github.com/grafana/pyroscope/pkg/test"
    17  )
    18  
    19  type mockOverrides struct {
    20  	defaultConfig Config
    21  	overrides     map[string]Config
    22  }
    23  
    24  func (m *mockOverrides) Retention() (Config, iter.Seq2[string, Config]) {
    25  	return m.defaultConfig, func(yield func(string, Config) bool) {
    26  		for k, v := range m.overrides {
    27  			if !yield(k, v) {
    28  				return
    29  			}
    30  		}
    31  	}
    32  }
    33  
    34  type testBlock struct {
    35  	tenant    string
    36  	shard     uint32
    37  	createdAt time.Time
    38  	minTime   time.Time
    39  	maxTime   time.Time
    40  }
    41  
    42  func TestTimeBasedRetentionPolicy(t *testing.T) {
    43  	type testCase struct {
    44  		name               string
    45  		defaultConfig      Config
    46  		overrides          map[string]Config
    47  		gracePeriod        time.Duration
    48  		maxTombstones      int
    49  		now                time.Time
    50  		blocks             []testBlock
    51  		expectedTombstones int
    52  	}
    53  
    54  	now := test.Time("2024-01-01T00:00:00Z")
    55  	tests := []testCase{
    56  		{
    57  			name:          "no retention policies",
    58  			gracePeriod:   time.Hour,
    59  			maxTombstones: 10,
    60  			now:           now,
    61  			blocks: []testBlock{
    62  				{
    63  					tenant:    "tenant-1",
    64  					shard:     1,
    65  					createdAt: now.Add(-23 * time.Hour),
    66  					minTime:   now.Add(-25 * time.Hour),
    67  					maxTime:   now.Add(-20 * time.Hour),
    68  				},
    69  			},
    70  		},
    71  		{
    72  			name:          "no default retention policy but tenant override exists",
    73  			defaultConfig: Config{},
    74  			overrides: map[string]Config{
    75  				"tenant-1": {RetentionPeriod: model.Duration(12 * time.Hour)},
    76  			},
    77  			gracePeriod:   time.Hour,
    78  			maxTombstones: 10,
    79  			now:           now,
    80  			blocks: []testBlock{
    81  				{
    82  					tenant:    "tenant-1",
    83  					shard:     1,
    84  					createdAt: now.Add(-23 * time.Hour),
    85  					minTime:   now.Add(-25 * time.Hour),
    86  					maxTime:   now.Add(-20 * time.Hour),
    87  				},
    88  				{
    89  					tenant:    "tenant-1",
    90  					shard:     2,
    91  					createdAt: now.Add(-22 * time.Hour),
    92  					minTime:   now.Add(-24 * time.Hour),
    93  					maxTime:   now.Add(-18 * time.Hour),
    94  				},
    95  				{
    96  					tenant:    "tenant-2",
    97  					shard:     1,
    98  					createdAt: now.Add(-25 * time.Hour),
    99  					minTime:   now.Add(-26 * time.Hour),
   100  					maxTime:   now.Add(-22 * time.Hour),
   101  				},
   102  			},
   103  			expectedTombstones: 2,
   104  		},
   105  		{
   106  			name:          "retention policy override shorter than partition",
   107  			defaultConfig: Config{RetentionPeriod: model.Duration(24 * time.Hour)},
   108  			overrides: map[string]Config{
   109  				"tenant-2": {RetentionPeriod: model.Duration(6 * time.Hour)},
   110  			},
   111  			gracePeriod:   time.Hour,
   112  			maxTombstones: 10,
   113  			now:           now,
   114  			blocks: []testBlock{
   115  				{
   116  					tenant:    "tenant-2",
   117  					shard:     1,
   118  					createdAt: now.Add(-9 * time.Hour),
   119  					minTime:   now.Add(-9 * time.Hour),
   120  					maxTime:   now.Add(-8 * time.Hour),
   121  				},
   122  			},
   123  		},
   124  		{
   125  			name:          "retention policy override shorter than default",
   126  			defaultConfig: Config{RetentionPeriod: model.Duration(24 * time.Hour)},
   127  			overrides: map[string]Config{
   128  				"tenant-2": {RetentionPeriod: model.Duration(4 * time.Hour)},
   129  			},
   130  			gracePeriod:   time.Hour,
   131  			maxTombstones: 10,
   132  			now:           now,
   133  			blocks: []testBlock{
   134  				{
   135  					tenant:    "tenant-1",
   136  					shard:     2,
   137  					createdAt: now.Add(-18 * time.Hour),
   138  					minTime:   now.Add(-18 * time.Hour),
   139  					maxTime:   now.Add(-16 * time.Hour),
   140  				},
   141  				{
   142  					tenant:    "tenant-2",
   143  					shard:     1,
   144  					createdAt: now.Add(-12 * time.Hour),
   145  					minTime:   now.Add(-12 * time.Hour),
   146  					maxTime:   now.Add(-10 * time.Hour),
   147  				},
   148  			},
   149  			expectedTombstones: 1,
   150  		},
   151  		{
   152  			name:          "retention policy override longer than default",
   153  			defaultConfig: Config{RetentionPeriod: model.Duration(12 * time.Hour)},
   154  			overrides: map[string]Config{
   155  				"tenant-1": {RetentionPeriod: model.Duration(24 * time.Hour)},
   156  			},
   157  			gracePeriod:   time.Hour,
   158  			maxTombstones: 10,
   159  			now:           now,
   160  			blocks: []testBlock{
   161  				{
   162  					tenant:    "tenant-1", // Default.
   163  					shard:     1,
   164  					createdAt: now.Add(-16 * time.Hour),
   165  					minTime:   now.Add(-16 * time.Hour),
   166  					maxTime:   now.Add(-18 * time.Hour),
   167  				},
   168  			},
   169  		},
   170  		{
   171  			name:          "anonymous tenant retained due to other tenant shards",
   172  			defaultConfig: Config{RetentionPeriod: model.Duration(12 * time.Hour)},
   173  			overrides: map[string]Config{
   174  				"tenant-1": {RetentionPeriod: model.Duration(48 * time.Hour)},
   175  			},
   176  			gracePeriod:   time.Hour,
   177  			maxTombstones: 10,
   178  			now:           now,
   179  			blocks: []testBlock{
   180  				{
   181  					tenant:    "tenant-1",
   182  					shard:     1,
   183  					createdAt: now.Add(-30 * time.Hour),
   184  					minTime:   now.Add(-30 * time.Hour),
   185  					maxTime:   now.Add(-20 * time.Hour),
   186  				},
   187  				{
   188  					tenant:    "tenant-2", // Default.
   189  					shard:     1,
   190  					createdAt: now.Add(-30 * time.Hour),
   191  					minTime:   now.Add(-30 * time.Hour),
   192  					maxTime:   now.Add(-20 * time.Hour),
   193  				},
   194  				{
   195  					tenant:    "",
   196  					shard:     1,
   197  					createdAt: now.Add(-30 * time.Hour),
   198  					minTime:   now.Add(-30 * time.Hour),
   199  					maxTime:   now.Add(-20 * time.Hour),
   200  				},
   201  			},
   202  			expectedTombstones: 1,
   203  		},
   204  		{
   205  			name:          "anonymous tenant deleted when no other tenant shards",
   206  			defaultConfig: Config{RetentionPeriod: model.Duration(12 * time.Hour)},
   207  			overrides:     map[string]Config{},
   208  			gracePeriod:   time.Hour,
   209  			maxTombstones: 10,
   210  			now:           now,
   211  			blocks: []testBlock{
   212  				{
   213  					tenant:    "",
   214  					shard:     1,
   215  					createdAt: now.Add(-30 * time.Hour),
   216  					minTime:   now.Add(-30 * time.Hour),
   217  					maxTime:   now.Add(-20 * time.Hour),
   218  				},
   219  			},
   220  			expectedTombstones: 1,
   221  		},
   222  		{
   223  			name:          "max tombstones limit reached",
   224  			defaultConfig: Config{RetentionPeriod: model.Duration(12 * time.Hour)},
   225  			overrides:     map[string]Config{},
   226  			gracePeriod:   time.Hour,
   227  			maxTombstones: 2,
   228  			now:           now,
   229  			blocks: []testBlock{
   230  				{
   231  					tenant:    "tenant-1",
   232  					shard:     1,
   233  					createdAt: now.Add(-30 * time.Hour),
   234  					minTime:   now.Add(-30 * time.Hour),
   235  					maxTime:   now.Add(-20 * time.Hour),
   236  				},
   237  				{
   238  					tenant:    "tenant-1",
   239  					shard:     2,
   240  					createdAt: now.Add(-30 * time.Hour),
   241  					minTime:   now.Add(-30 * time.Hour),
   242  					maxTime:   now.Add(-20 * time.Hour),
   243  				},
   244  				{
   245  					tenant:    "tenant-1",
   246  					shard:     3,
   247  					createdAt: now.Add(-30 * time.Hour),
   248  					minTime:   now.Add(-30 * time.Hour),
   249  					maxTime:   now.Add(-20 * time.Hour),
   250  				},
   251  			},
   252  			expectedTombstones: 2,
   253  		},
   254  		{
   255  			name:          "multiple tenant overrides with different retention periods",
   256  			defaultConfig: Config{RetentionPeriod: model.Duration(24 * time.Hour)},
   257  			overrides: map[string]Config{
   258  				"tenant-short":    {RetentionPeriod: model.Duration(12 * time.Hour)},
   259  				"tenant-infinite": {RetentionPeriod: 0},
   260  			},
   261  			gracePeriod:   time.Hour,
   262  			maxTombstones: 10,
   263  			now:           now,
   264  			blocks: []testBlock{
   265  				{
   266  					tenant:    "tenant-infinite",
   267  					shard:     1,
   268  					createdAt: now.Add(-180 * 24 * time.Hour),
   269  					minTime:   now.Add(-180 * 24 * time.Hour),
   270  					maxTime:   now.Add(-180*24*time.Hour + time.Hour),
   271  				},
   272  				{
   273  					tenant:    "tenant-short",
   274  					shard:     2,
   275  					createdAt: now.Add(-30 * time.Hour),
   276  					minTime:   now.Add(-30 * time.Hour),
   277  					maxTime:   now.Add(-20 * time.Hour),
   278  				},
   279  				{
   280  					tenant:    "default-tenant",
   281  					shard:     3,
   282  					createdAt: now.Add(-20 * time.Hour),
   283  					minTime:   now.Add(-20 * time.Hour),
   284  					maxTime:   now.Add(-18 * time.Hour),
   285  				},
   286  			},
   287  			expectedTombstones: 1,
   288  		},
   289  		{
   290  			name: "zero retention period as override means infinite retention",
   291  			overrides: map[string]Config{
   292  				"tenant-1": {RetentionPeriod: model.Duration(0)},
   293  			},
   294  			gracePeriod:   time.Hour,
   295  			maxTombstones: 10,
   296  			now:           now,
   297  			blocks: []testBlock{
   298  				{
   299  					tenant:    "tenant-1",
   300  					shard:     1,
   301  					createdAt: now.Add(-23 * time.Hour),
   302  					minTime:   now.Add(-25 * time.Hour),
   303  					maxTime:   now.Add(-20 * time.Hour),
   304  				},
   305  			},
   306  		},
   307  		{
   308  			name:          "zero retention period as default means infinite retention",
   309  			defaultConfig: Config{RetentionPeriod: model.Duration(0)},
   310  			gracePeriod:   time.Hour,
   311  			maxTombstones: 10,
   312  			now:           now,
   313  			blocks: []testBlock{
   314  				{
   315  					tenant:    "tenant-1",
   316  					shard:     1,
   317  					createdAt: now.Add(-23 * time.Hour),
   318  					minTime:   now.Add(-25 * time.Hour),
   319  					maxTime:   now.Add(-20 * time.Hour),
   320  				},
   321  			},
   322  		},
   323  		{
   324  			name:          "partition exactly at retention boundary",
   325  			defaultConfig: Config{RetentionPeriod: model.Duration(24 * time.Hour)},
   326  			maxTombstones: 10,
   327  			now:           now,
   328  			blocks: []testBlock{
   329  				{
   330  					tenant:    "default-tenant",
   331  					shard:     3,
   332  					createdAt: now.Add(-26 * time.Hour),
   333  					minTime:   now.Add(-26 * time.Hour),
   334  					maxTime:   now.Add(-24 * time.Hour),
   335  				},
   336  			},
   337  		},
   338  	}
   339  
   340  	for _, tc := range tests {
   341  		t.Run(tc.name, func(t *testing.T) {
   342  			db := test.BoltDB(t)
   343  			store := indexstore.NewIndexStore()
   344  			require.NoError(t, db.Update(store.CreateBuckets))
   345  			defer db.Close()
   346  
   347  			policy := NewTimeBasedRetentionPolicy(
   348  				log.NewNopLogger(),
   349  				&mockOverrides{
   350  					defaultConfig: tc.defaultConfig,
   351  					overrides:     tc.overrides,
   352  				},
   353  				tc.maxTombstones,
   354  				tc.gracePeriod,
   355  				tc.now,
   356  			)
   357  
   358  			const partitionDuration = 6 * time.Hour
   359  			require.NoError(t, db.Update(func(tx *bbolt.Tx) error {
   360  				for _, block := range tc.blocks {
   361  					p := indexstore.NewPartition(block.createdAt.Truncate(partitionDuration), partitionDuration)
   362  					s := indexstore.NewShard(p, block.tenant, block.shard)
   363  					require.NoError(t, s.Store(tx, &metastorev1.BlockMeta{
   364  						Id:          test.ULID(block.createdAt.Format(time.RFC3339)),
   365  						Tenant:      1,
   366  						Shard:       block.shard,
   367  						MinTime:     block.minTime.UnixNano(),
   368  						MaxTime:     block.maxTime.UnixNano(),
   369  						StringTable: []string{"", block.tenant},
   370  					}))
   371  				}
   372  				return nil
   373  			}))
   374  
   375  			require.NoError(t, db.View(func(tx *bbolt.Tx) error {
   376  				// Multiple lines for better debugging.
   377  				partitions := store.Partitions(tx)
   378  				tombstones := policy.CreateTombstones(tx, partitions)
   379  				assert.Equal(t, tc.expectedTombstones, len(tombstones))
   380  				return nil
   381  			}))
   382  		})
   383  	}
   384  }