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 }