github.com/grafana/pyroscope@v1.18.0/pkg/ingester/retention_test.go (about)

     1  package ingester
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io/fs"
     8  	"math/rand"
     9  	"os"
    10  	"path/filepath"
    11  	"slices"
    12  	"sort"
    13  	"strings"
    14  	"testing"
    15  	"time"
    16  
    17  	"github.com/go-kit/log"
    18  	"github.com/oklog/ulid/v2"
    19  	"github.com/samber/lo"
    20  	"github.com/spf13/afero"
    21  	"github.com/stretchr/testify/mock"
    22  	"github.com/stretchr/testify/require"
    23  
    24  	"github.com/grafana/pyroscope/pkg/phlaredb"
    25  	"github.com/grafana/pyroscope/pkg/phlaredb/shipper"
    26  	diskutil "github.com/grafana/pyroscope/pkg/util/disk"
    27  )
    28  
    29  func TestDiskCleaner_DeleteUploadedBlocks(t *testing.T) {
    30  	t.Run("multi_tenant_blocks", func(t *testing.T) {
    31  		const anonTenantID = "anonymous"
    32  		const tenantID = "1234"
    33  
    34  		e := &mockBlockEvictor{}
    35  
    36  		bm := &mockBlockManager{}
    37  		bm.On("GetTenantIDs", mock.Anything).
    38  			Return([]string{anonTenantID, tenantID}, nil).
    39  			Once()
    40  		bm.On("GetBlocksForTenant", mock.Anything, anonTenantID).
    41  			Return([]*tenantBlock{{
    42  				ID:       ulid.MustParse(generateBlockID(t, "01AC")),
    43  				TenantID: anonTenantID,
    44  				Path:     fmt.Sprintf("./data/%s/%s", anonTenantID, generateBlockID(t, "01AC")),
    45  				Uploaded: true,
    46  			}}, nil).
    47  			Once()
    48  		bm.On("GetBlocksForTenant", mock.Anything, tenantID).
    49  			Return([]*tenantBlock{{
    50  				ID:       ulid.MustParse(generateBlockID(t, "01AB")),
    51  				TenantID: anonTenantID,
    52  				Path:     fmt.Sprintf("./data/%s/%s", anonTenantID, generateBlockID(t, "01AB")),
    53  				Uploaded: false,
    54  			}}, nil)
    55  		bm.On("DeleteBlock", mock.Anything, mock.Anything).
    56  			Return(nil).
    57  			Once()
    58  
    59  		dc := newDiskCleaner(log.NewNopLogger(), e, defaultRetentionPolicy(), phlaredb.Config{
    60  			DataPath: "./data",
    61  		})
    62  		dc.blockManager = bm
    63  
    64  		want := 1
    65  		got := dc.DeleteUploadedBlocks(context.Background())
    66  		require.Equal(t, want, got)
    67  	})
    68  
    69  	t.Run("delete_blocks_past_expiry", func(t *testing.T) {
    70  		// Two blocks are created and marked as "uploaded", but only one is past
    71  		// the expiry window. Only the expired one should be deleted.
    72  
    73  		const anonTenantID = "anonymous"
    74  		entropy := rand.New(rand.NewSource(0))
    75  		expiry := 10 * time.Minute
    76  
    77  		nowMS := ulid.Timestamp(time.Now())
    78  		nowID := ulid.MustNew(nowMS, entropy)
    79  
    80  		expiredMS := ulid.Timestamp(time.Now().Add(-(2 * expiry))) // Twice as long ago as the expiry.
    81  		expiredID := ulid.MustNew(expiredMS, entropy)
    82  
    83  		e := &mockBlockEvictor{}
    84  
    85  		bm := &mockBlockManager{}
    86  		bm.On("GetTenantIDs", mock.Anything).
    87  			Return([]string{anonTenantID}, nil).
    88  			Once()
    89  		bm.On("GetBlocksForTenant", mock.Anything, anonTenantID).
    90  			Return([]*tenantBlock{
    91  				{
    92  					ID:       nowID,
    93  					TenantID: anonTenantID,
    94  					Path:     fmt.Sprintf("./data/%s/%s", anonTenantID, nowID.String()),
    95  					Uploaded: true,
    96  				},
    97  				{
    98  					ID:       expiredID,
    99  					TenantID: anonTenantID,
   100  					Path:     fmt.Sprintf("./data/%s/%s", anonTenantID, expiredID.String()),
   101  					Uploaded: true,
   102  				},
   103  			}, nil).
   104  			Once()
   105  		bm.On("DeleteBlock", mock.Anything, mock.Anything).
   106  			Return(nil).
   107  			Once()
   108  
   109  		policy := defaultRetentionPolicy()
   110  		policy.Expiry = expiry
   111  
   112  		dc := newDiskCleaner(log.NewNopLogger(), e, policy, phlaredb.Config{
   113  			DataPath: "./data",
   114  		})
   115  		dc.blockManager = bm
   116  
   117  		want := 1
   118  		got := dc.DeleteUploadedBlocks(context.Background())
   119  		require.Equal(t, want, got)
   120  	})
   121  
   122  	t.Run("no_tenant_dirs", func(t *testing.T) {
   123  		e := &mockBlockEvictor{}
   124  
   125  		bm := &mockBlockManager{}
   126  		bm.On("GetTenantIDs", mock.Anything).
   127  			Return([]string{}, nil).
   128  			Once()
   129  
   130  		dc := newDiskCleaner(log.NewNopLogger(), e, defaultRetentionPolicy(), phlaredb.Config{
   131  			DataPath: "./data",
   132  		})
   133  		dc.blockManager = bm
   134  
   135  		want := 0
   136  		got := dc.DeleteUploadedBlocks(context.Background())
   137  		require.Equal(t, want, got)
   138  	})
   139  
   140  	t.Run("no_block_dirs", func(t *testing.T) {
   141  		const tenantID = "anonymous"
   142  
   143  		e := &mockBlockEvictor{}
   144  
   145  		bm := &mockBlockManager{}
   146  		bm.On("GetTenantIDs", mock.Anything).
   147  			Return([]string{tenantID}, nil).
   148  			Once()
   149  		bm.On("GetBlocksForTenant", mock.Anything, tenantID).
   150  			Return([]*tenantBlock{}, nil).
   151  			Once()
   152  
   153  		dc := newDiskCleaner(log.NewNopLogger(), e, defaultRetentionPolicy(), phlaredb.Config{
   154  			DataPath: "./data",
   155  		})
   156  		dc.blockManager = bm
   157  
   158  		want := 0
   159  		got := dc.DeleteUploadedBlocks(context.Background())
   160  		require.Equal(t, want, got)
   161  	})
   162  }
   163  
   164  func TestDiskCleaner_EnforceHighDiskUtilization(t *testing.T) {
   165  	t.Run("no_high_disk", func(t *testing.T) {
   166  		const anonTenantID = "anonymous"
   167  		e := &mockBlockEvictor{}
   168  
   169  		bm := &mockBlockManager{}
   170  		bm.On("GetTenantIDs", mock.Anything).
   171  			Return([]string{anonTenantID}, nil).
   172  			Once()
   173  		bm.On("GetBlocksForTenant", mock.Anything, anonTenantID).
   174  			Return([]*tenantBlock{
   175  				{
   176  					ID:       ulid.MustParse(generateBlockID(t, "01AC")),
   177  					TenantID: anonTenantID,
   178  					Path:     fmt.Sprintf("/data/%s/local/%s", anonTenantID, generateBlockID(t, "01AC")),
   179  					Uploaded: true,
   180  				},
   181  			}, nil).
   182  			Once()
   183  		bm.On("DeleteBlock", mock.Anything, mock.Anything).
   184  			Return(nil)
   185  
   186  		vc := &mockVolumeChecker{}
   187  		vc.On("HasHighDiskUtilization", mock.Anything).
   188  			Return(&diskutil.VolumeStats{
   189  				HighDiskUtilization: false,
   190  				BytesAvailable:      100,
   191  				BytesTotal:          200,
   192  			}, nil).
   193  			Once()
   194  
   195  		dc := newDiskCleaner(log.NewNopLogger(), e, defaultRetentionPolicy(), phlaredb.Config{
   196  			DataPath: "./data",
   197  		})
   198  		dc.blockManager = bm
   199  		dc.volumeChecker = vc
   200  
   201  		deleted, bytesFreed, hadHighDisk := dc.CleanupBlocksWhenHighDiskUtilization(context.Background())
   202  		require.Equal(t, 0, deleted)
   203  		require.Equal(t, 0, bytesFreed)
   204  		require.False(t, hadHighDisk)
   205  	})
   206  
   207  	t.Run("has_high_disk", func(t *testing.T) {
   208  		const anonTenantID = "anonymous"
   209  
   210  		e := &mockBlockEvictor{}
   211  
   212  		bm := &mockBlockManager{}
   213  		bm.On("GetTenantIDs", mock.Anything).
   214  			Return([]string{anonTenantID}, nil).
   215  			Once()
   216  		bm.On("GetBlocksForTenant", mock.Anything, anonTenantID).
   217  			Return([]*tenantBlock{
   218  				{
   219  					ID:       ulid.MustParse(generateBlockID(t, "01AC")),
   220  					TenantID: anonTenantID,
   221  					Path:     fmt.Sprintf("/data/%s/local/%s", anonTenantID, generateBlockID(t, "01AC")),
   222  					Uploaded: true,
   223  				},
   224  				{
   225  					ID:       ulid.MustParse(generateBlockID(t, "01AD")),
   226  					TenantID: anonTenantID,
   227  					Path:     fmt.Sprintf("/data/%s/local/%s", anonTenantID, generateBlockID(t, "01AD")),
   228  					Uploaded: false,
   229  				},
   230  				{
   231  					ID:       ulid.MustParse(generateBlockID(t, "01AE")),
   232  					TenantID: anonTenantID,
   233  					Path:     fmt.Sprintf("/data/%s/local/%s", anonTenantID, generateBlockID(t, "01AE")),
   234  					Uploaded: false,
   235  				},
   236  			}, nil).
   237  			Once()
   238  		bm.On("DeleteBlock", mock.Anything, mock.Anything).
   239  			Return(nil)
   240  
   241  		vc := &mockVolumeChecker{}
   242  		vc.On("HasHighDiskUtilization", mock.Anything).
   243  			Return(&diskutil.VolumeStats{
   244  				HighDiskUtilization: true,
   245  				BytesAvailable:      0,
   246  				BytesTotal:          200,
   247  			}, nil).
   248  			Once()
   249  		vc.On("HasHighDiskUtilization", mock.Anything).
   250  			Return(&diskutil.VolumeStats{
   251  				HighDiskUtilization: true,
   252  				BytesAvailable:      100,
   253  				BytesTotal:          200,
   254  			}, nil).
   255  			Once()
   256  		vc.On("HasHighDiskUtilization", mock.Anything).
   257  			Return(&diskutil.VolumeStats{
   258  				HighDiskUtilization: false,
   259  				BytesAvailable:      100,
   260  				BytesTotal:          200,
   261  			}, nil).
   262  			Once()
   263  
   264  		dc := newDiskCleaner(log.NewNopLogger(), e, defaultRetentionPolicy(), phlaredb.Config{
   265  			DataPath: "./data",
   266  		})
   267  		dc.blockManager = bm
   268  		dc.volumeChecker = vc
   269  
   270  		deleted, bytesFreed, hadHighDisk := dc.CleanupBlocksWhenHighDiskUtilization(context.Background())
   271  		require.Equal(t, 2, deleted)
   272  		require.Equal(t, 100, bytesFreed)
   273  		require.True(t, hadHighDisk)
   274  	})
   275  
   276  	t.Run("has_high_disk_with_delayed_volume_checker_stats", func(t *testing.T) {
   277  		const anonTenantID = "anonymous"
   278  
   279  		e := &mockBlockEvictor{}
   280  
   281  		bm := &mockBlockManager{}
   282  		bm.On("GetTenantIDs", mock.Anything).
   283  			Return([]string{anonTenantID}, nil).
   284  			Once()
   285  		bm.On("GetBlocksForTenant", mock.Anything, anonTenantID).
   286  			Return([]*tenantBlock{
   287  				{
   288  					ID:       ulid.MustParse(generateBlockID(t, "01AC")),
   289  					TenantID: anonTenantID,
   290  					Path:     fmt.Sprintf("/data/%s/local/%s", anonTenantID, generateBlockID(t, "01AC")),
   291  					Uploaded: true,
   292  				},
   293  				{
   294  					ID:       ulid.MustParse(generateBlockID(t, "01AD")),
   295  					TenantID: anonTenantID,
   296  					Path:     fmt.Sprintf("/data/%s/local/%s", anonTenantID, generateBlockID(t, "01AD")),
   297  					Uploaded: false,
   298  				},
   299  			}, nil).
   300  			Once()
   301  		bm.On("DeleteBlock", mock.Anything, mock.Anything).
   302  			Return(nil)
   303  
   304  		vc := &mockVolumeChecker{}
   305  		vc.On("HasHighDiskUtilization", mock.Anything).
   306  			Return(&diskutil.VolumeStats{
   307  				HighDiskUtilization: true,
   308  				BytesAvailable:      100,
   309  				BytesTotal:          200,
   310  			}, nil).
   311  			Twice() // Report the same result twice, causing the loop to break.
   312  
   313  		dc := newDiskCleaner(log.NewNopLogger(), e, defaultRetentionPolicy(), phlaredb.Config{
   314  			DataPath: "./data",
   315  		})
   316  		dc.blockManager = bm
   317  		dc.volumeChecker = vc
   318  
   319  		deleted, bytesFreed, hadHighDisk := dc.CleanupBlocksWhenHighDiskUtilization(context.Background())
   320  		require.Equal(t, 1, deleted)
   321  		require.Equal(t, 0, bytesFreed)
   322  		require.True(t, hadHighDisk)
   323  	})
   324  }
   325  
   326  func TestDiskCleaner_isBlockDeletableForUploadedBlocks(t *testing.T) {
   327  	tests := []struct {
   328  		Name   string
   329  		Expiry time.Duration
   330  		Block  *tenantBlock
   331  		Want   bool
   332  	}{
   333  		{
   334  			Name:   "uploaded_and_expired",
   335  			Expiry: 10 * time.Minute,
   336  			Block: &tenantBlock{
   337  				ID:       generateBlockIDFromTS(t, time.Now().Add(-(11 * time.Minute))),
   338  				Uploaded: true,
   339  			},
   340  			Want: true,
   341  		},
   342  		{
   343  			Name:   "not_uploaded",
   344  			Expiry: 10 * time.Minute,
   345  			Block: &tenantBlock{
   346  				ID:       generateBlockIDFromTS(t, time.Now().Add(-(11 * time.Minute))),
   347  				Uploaded: false,
   348  			},
   349  			Want: false,
   350  		},
   351  		{
   352  			Name:   "not_expired",
   353  			Expiry: 10 * time.Minute,
   354  			Block: &tenantBlock{
   355  				ID:       generateBlockIDFromTS(t, time.Now().Add(-(9 * time.Minute))),
   356  				Uploaded: true,
   357  			},
   358  			Want: false,
   359  		},
   360  		{
   361  			Name:   "not_uploaded_and_not_expired",
   362  			Expiry: 10 * time.Minute,
   363  			Block: &tenantBlock{
   364  				ID:       generateBlockIDFromTS(t, time.Now().Add(-(9 * time.Minute))),
   365  				Uploaded: false,
   366  			},
   367  			Want: false,
   368  		},
   369  	}
   370  
   371  	dc := &diskCleaner{
   372  		policy: defaultRetentionPolicy(),
   373  	}
   374  
   375  	for _, tt := range tests {
   376  		t.Run(tt.Name, func(t *testing.T) {
   377  			dc.policy.Expiry = tt.Expiry
   378  
   379  			got := tt.Block.Uploaded && dc.isExpired(tt.Block)
   380  			require.Equal(t, tt.Want, got)
   381  		})
   382  	}
   383  }
   384  
   385  func TestFSBlockManager(t *testing.T) {
   386  	const root = "/data"
   387  	blocksByTenant := map[string][]*tenantBlock{
   388  		"anonymous": {
   389  			{
   390  				ID:       ulid.MustParse(generateBlockID(t, "01AC")),
   391  				TenantID: "anonymous",
   392  				Path:     "/data/anonymous/local/" + generateBlockID(t, "01AC"),
   393  				Uploaded: false,
   394  			},
   395  			{
   396  				ID:       ulid.MustParse(generateBlockID(t, "01AD")),
   397  				TenantID: "anonymous",
   398  				Path:     "/data/anonymous/local/" + generateBlockID(t, "01AD"),
   399  				Uploaded: true,
   400  			},
   401  		},
   402  		"1218": {
   403  			{
   404  				ID:       ulid.MustParse(generateBlockID(t, "11AC")),
   405  				TenantID: "1218",
   406  				Path:     "/data/1218/local/" + generateBlockID(t, "11AC"),
   407  				Uploaded: false,
   408  			},
   409  			{
   410  				ID:       ulid.MustParse(generateBlockID(t, "11AD")),
   411  				TenantID: "1218",
   412  				Path:     "/data/1218/local/" + generateBlockID(t, "11AD"),
   413  				Uploaded: true,
   414  			},
   415  		},
   416  	}
   417  
   418  	e := &mockBlockEvictor{}
   419  
   420  	fs := &mockFS{
   421  		Fs:   afero.NewMemMapFs(),
   422  		Root: root,
   423  	}
   424  	for tenantID, blocks := range blocksByTenant {
   425  		blockIDs := lo.Map(blocks, func(block *tenantBlock, _ int) string {
   426  			return block.ID.String()
   427  		})
   428  		fs.createBlocksForTenant(t, tenantID, blockIDs...)
   429  
   430  		uploadedBlockIDs := lo.Map(lo.Filter(blocks, func(block *tenantBlock, _ int) bool {
   431  			return block.Uploaded
   432  		}), func(block *tenantBlock, _ int) string {
   433  			return block.ID.String()
   434  		})
   435  		fs.markBlocksShippedForTenant(t, tenantID, uploadedBlockIDs...)
   436  	}
   437  
   438  	// Create a lost+found directory.
   439  	fs.createDirectories(t, "lost+found")
   440  
   441  	t.Run("GetTenantIDs", func(t *testing.T) {
   442  		bm := newFSBlockManager(root, e, fs)
   443  		tenantIDs, err := bm.GetTenantIDs(context.Background())
   444  		require.NoError(t, err)
   445  		require.Equal(t, []string{"1218", "anonymous"}, tenantIDs)
   446  		// Explicitly check lost+found isn't in tenant id list.
   447  		require.NotContains(t, tenantIDs, "lost+found")
   448  	})
   449  
   450  	t.Run("GetBlocksForTenant", func(t *testing.T) {
   451  		bm := newFSBlockManager(root, e, fs)
   452  		blocks, err := bm.GetBlocksForTenant(context.Background(), "anonymous")
   453  		require.NoError(t, err)
   454  		require.Equal(t, blocksByTenant["anonymous"], blocks)
   455  
   456  		blocks, err = bm.GetBlocksForTenant(context.Background(), "1218")
   457  		require.NoError(t, err)
   458  		require.Equal(t, blocksByTenant["1218"], blocks)
   459  
   460  		_, err = bm.GetBlocksForTenant(context.Background(), "missing")
   461  		require.ErrorContains(t, err, "file does not exist")
   462  	})
   463  
   464  	t.Run("DeleteBlock", func(t *testing.T) {
   465  		e = &mockBlockEvictor{}
   466  		e.On("evictBlock", "anonymous", mock.Anything, mock.Anything).
   467  			Return(nil)
   468  
   469  		bm := newFSBlockManager(root, e, fs)
   470  		for _, block := range blocksByTenant["anonymous"] {
   471  			err := bm.DeleteBlock(context.Background(), block)
   472  			require.NoError(t, err)
   473  		}
   474  	})
   475  }
   476  
   477  func TestFSBlockManager_isTenantDir(t *testing.T) {
   478  	const root = "/data"
   479  	dirPaths := []string{
   480  		// Skip, not tenant ids
   481  		"lost+found",
   482  		".DS_Store",
   483  
   484  		// Skip, no local dir
   485  		"1234/head/01HKWWF79V1STKXBNYW7WCMDGM",
   486  		"1234/head/01HKWWF8939QM6E7BS69X0RASG",
   487  
   488  		// Tenant dirs
   489  		"anonymous/local/01HKWWF3CTFC5EJN6JJ96TY4W9",
   490  		"anonymous/local/01HKWWF4C298KVTEEQ3RW6TVHZ",
   491  		"1218/local/01HKWWF5BB2DJVDP0DTMT9MDMN",
   492  		"1218/local/01HKWWF6AKVZDCWQB12MHWG7FN",
   493  		"9876/local",
   494  	}
   495  	filePaths := []string{
   496  		// Skip all files
   497  		"somefile.txt",
   498  	}
   499  
   500  	fs := &mockFS{
   501  		Fs:   afero.NewMemMapFs(),
   502  		Root: root,
   503  	}
   504  	fs.createDirectories(t, dirPaths...)
   505  	fs.createFiles(t, filePaths...)
   506  
   507  	gotTenantIDs := []string{}
   508  	entries, err := fs.ReadDir(fs.Root)
   509  	require.NoError(t, err)
   510  
   511  	bm := &realFSBlockManager{
   512  		Root: fs.Root,
   513  		FS:   fs,
   514  	}
   515  	for _, entry := range entries {
   516  		if bm.isTenantDir(fs.Root, entry) {
   517  			gotTenantIDs = append(gotTenantIDs, entry.Name())
   518  		}
   519  	}
   520  	slices.Sort(gotTenantIDs)
   521  
   522  	wantTenantIDs := []string{"1218", "9876", "anonymous"}
   523  	require.Equal(t, wantTenantIDs, gotTenantIDs)
   524  }
   525  
   526  func TestSortBlocks(t *testing.T) {
   527  	createAnonymousBlock := func(t *testing.T, blockID string, uploaded bool) *tenantBlock {
   528  		t.Helper()
   529  
   530  		return &tenantBlock{
   531  			ID:       ulid.MustParse(blockID),
   532  			TenantID: "anonymous",
   533  			Path:     fmt.Sprintf("/data/anonymous/local/%s", blockID),
   534  			Uploaded: uploaded,
   535  		}
   536  	}
   537  
   538  	tests := []struct {
   539  		Name   string
   540  		Blocks []*tenantBlock
   541  		Want   []*tenantBlock
   542  	}{
   543  		{
   544  			Name: "uploaded_and_non_uploaded",
   545  			Blocks: []*tenantBlock{
   546  				createAnonymousBlock(t, "01HH5BVHA006AFVGQT5ZYC0GEK", true),  // unix ms: 1702061000000
   547  				createAnonymousBlock(t, "01HH5CT1W0ZW908PVKS1Q4ZYAZ", false), // unix ms: 1702062000000
   548  				createAnonymousBlock(t, "01HH5DRJE0YSHABVQ85AYZ8JHD", true),  // unix ms: 1702063000000
   549  				createAnonymousBlock(t, "01HH5EQ3001DTZP60DNX4AF7Q0", false), // unix ms: 1702064000000
   550  				createAnonymousBlock(t, "01HH5FNKJ0P46KJHJHGM7X98BR", true),  // unix ms: 1702065000000
   551  			},
   552  			Want: []*tenantBlock{
   553  				createAnonymousBlock(t, "01HH5BVHA006AFVGQT5ZYC0GEK", true),  // unix ms: 1702061000000
   554  				createAnonymousBlock(t, "01HH5DRJE0YSHABVQ85AYZ8JHD", true),  // unix ms: 1702063000000
   555  				createAnonymousBlock(t, "01HH5FNKJ0P46KJHJHGM7X98BR", true),  // unix ms: 1702065000000
   556  				createAnonymousBlock(t, "01HH5CT1W0ZW908PVKS1Q4ZYAZ", false), // unix ms: 1702062000000
   557  				createAnonymousBlock(t, "01HH5EQ3001DTZP60DNX4AF7Q0", false), // unix ms: 1702064000000
   558  			},
   559  		},
   560  		{
   561  			Name: "uploaded_and_non_uploaded_at_same_timestamp",
   562  			Blocks: []*tenantBlock{
   563  				createAnonymousBlock(t, "01HH5BVHA006AFVGQT5ZYC0GEK", false), // unix ms: 1702061000000
   564  				createAnonymousBlock(t, "01HH5BVHA0ZW908PVKS1Q4ZYAZ", true),  // unix ms: 1702061000000
   565  			},
   566  			Want: []*tenantBlock{
   567  				createAnonymousBlock(t, "01HH5BVHA0ZW908PVKS1Q4ZYAZ", true),  // unix ms: 1702061000000
   568  				createAnonymousBlock(t, "01HH5BVHA006AFVGQT5ZYC0GEK", false), // unix ms: 1702061000000
   569  			},
   570  		},
   571  	}
   572  
   573  	for _, tt := range tests {
   574  		t.Run(tt.Name, func(t *testing.T) {
   575  			sort.Sort(blocksByUploadAndAge(tt.Blocks))
   576  			require.Equal(t, tt.Want, tt.Blocks)
   577  		})
   578  	}
   579  }
   580  
   581  type mockFS struct {
   582  	afero.Fs
   583  
   584  	Root string
   585  }
   586  
   587  func (mfs *mockFS) Open(name string) (fs.File, error) {
   588  	return mfs.Fs.Open(name)
   589  }
   590  
   591  func (mfs *mockFS) ReadDir(name string) ([]fs.DirEntry, error) {
   592  	dirs, err := afero.ReadDir(mfs.Fs, name)
   593  	if err != nil {
   594  		return nil, err
   595  	}
   596  
   597  	entries := make([]fs.DirEntry, 0, len(dirs))
   598  	for _, dir := range dirs {
   599  		entries = append(entries, fs.FileInfoToDirEntry(dir))
   600  	}
   601  	return entries, nil
   602  }
   603  
   604  func (mfs *mockFS) createBlocksForTenant(t *testing.T, tenantID string, blockIDs ...string) {
   605  	t.Helper()
   606  	localDirPath := filepath.Join(mfs.Root, tenantID, phlareDBLocalPath)
   607  	for _, blockID := range blockIDs {
   608  		path := filepath.Join(localDirPath, blockID)
   609  		err := mfs.MkdirAll(path, 0755)
   610  		if err != nil {
   611  			t.Fatalf("failed to create block: %s: %v", localDirPath, err)
   612  			return
   613  		}
   614  	}
   615  }
   616  
   617  func (mfs *mockFS) markBlocksShippedForTenant(t *testing.T, tenantID string, blockIDs ...string) {
   618  	t.Helper()
   619  	localDirPath := filepath.Join(mfs.Root, tenantID, phlareDBLocalPath)
   620  	shipperPath := filepath.Join(localDirPath, shipper.MetaFilename)
   621  	bytes, err := fs.ReadFile(mfs, shipperPath)
   622  	if err != nil && !os.IsNotExist(err) {
   623  		t.Fatalf("failed to read shipper.json: %v", err)
   624  		return
   625  	}
   626  
   627  	meta := shipper.Meta{}
   628  	if len(bytes) != 0 {
   629  		err = json.Unmarshal(bytes, &meta)
   630  		if err != nil {
   631  			t.Fatalf("failed to unmarshal shipper.json: %v", err)
   632  			return
   633  		}
   634  	}
   635  
   636  	for _, blockID := range blockIDs {
   637  		id, err := ulid.Parse(blockID)
   638  		if err != nil {
   639  			t.Fatalf("failed to create ULID from %s: %v", blockID, err)
   640  			return
   641  		}
   642  		meta.Uploaded = append(meta.Uploaded, id)
   643  	}
   644  
   645  	bytes, err = json.Marshal(meta)
   646  	if err != nil {
   647  		t.Fatalf("failed to marshal shipper.json: %v", err)
   648  		return
   649  	}
   650  	err = afero.WriteFile(mfs.Fs, shipperPath, bytes, 0755)
   651  	if err != nil {
   652  		t.Fatalf("failed to update shipper.json: %v", err)
   653  	}
   654  }
   655  
   656  func (mfs *mockFS) createDirectories(t *testing.T, paths ...string) {
   657  	t.Helper()
   658  	for _, path := range paths {
   659  		path = filepath.Join(mfs.Root, path)
   660  		err := mfs.MkdirAll(path, 0755)
   661  		if err != nil {
   662  			t.Fatalf("failed to create directory: %s: %v", path, err)
   663  			return
   664  		}
   665  	}
   666  }
   667  
   668  func (mfs *mockFS) createFiles(t *testing.T, paths ...string) {
   669  	t.Helper()
   670  	for _, path := range paths {
   671  		path = filepath.Join(mfs.Root, path)
   672  		_, err := mfs.Create(path)
   673  		if err != nil {
   674  			t.Fatalf("failed to create file: %s: %v", path, err)
   675  			return
   676  		}
   677  	}
   678  }
   679  
   680  type mockBlockManager struct {
   681  	mock.Mock
   682  }
   683  
   684  func (bm *mockBlockManager) DeleteBlock(ctx context.Context, block *tenantBlock) error {
   685  	args := bm.Called(ctx, block)
   686  	return args.Error(0)
   687  }
   688  
   689  func (bm *mockBlockManager) GetBlocksForTenant(ctx context.Context, tenantID string) ([]*tenantBlock, error) {
   690  	args := bm.Called(ctx, tenantID)
   691  	return args[0].([]*tenantBlock), args.Error(1)
   692  }
   693  
   694  func (bm *mockBlockManager) GetTenantIDs(ctx context.Context) ([]string, error) {
   695  	args := bm.Called(ctx)
   696  	return args[0].([]string), args.Error(1)
   697  }
   698  
   699  type mockBlockEvictor struct {
   700  	mock.Mock
   701  }
   702  
   703  func (e *mockBlockEvictor) evictBlock(tenant string, b ulid.ULID, fn func() error) error {
   704  	args := e.Called(tenant, b, fn)
   705  
   706  	err := fn()
   707  	if err != nil {
   708  		return err
   709  	}
   710  
   711  	return args.Error(0)
   712  }
   713  
   714  type mockVolumeChecker struct {
   715  	mock.Mock
   716  }
   717  
   718  func (vc *mockVolumeChecker) HasHighDiskUtilization(path string) (*diskutil.VolumeStats, error) {
   719  	args := vc.Called(path)
   720  	return args[0].(*diskutil.VolumeStats), args.Error(1)
   721  }
   722  
   723  func generateBlockID(t *testing.T, prefix string) string {
   724  	t.Helper()
   725  
   726  	const maxLen = 26
   727  	const padding = "0"
   728  	return fmt.Sprintf("%s%s", prefix, strings.Repeat(padding, maxLen-len(prefix)))
   729  }
   730  
   731  func generateBlockIDFromTS(t *testing.T, ts time.Time) ulid.ULID {
   732  	t.Helper()
   733  
   734  	entropy := rand.New(rand.NewSource(time.Now().UnixNano()))
   735  	return ulid.MustNew(ulid.Timestamp(ts), entropy)
   736  }