github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/engine/pkg/externalresource/internal/local/file_manager_test.go (about)

     1  // Copyright 2022 PingCAP, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package local
    15  
    16  import (
    17  	"context"
    18  	"fmt"
    19  	"os"
    20  	"path/filepath"
    21  	"testing"
    22  
    23  	"github.com/pingcap/tiflow/engine/model"
    24  	"github.com/pingcap/tiflow/engine/pkg/externalresource/internal"
    25  	resModel "github.com/pingcap/tiflow/engine/pkg/externalresource/model"
    26  	"github.com/pingcap/tiflow/engine/pkg/tenant"
    27  	"github.com/stretchr/testify/require"
    28  )
    29  
    30  func newResourceIdentForTesting(executor, workerID, resourceName string) internal.ResourceIdent {
    31  	return internal.ResourceIdent{
    32  		Name: resourceName,
    33  		ResourceScope: internal.ResourceScope{
    34  			ProjectInfo: tenant.NewProjectInfo("fakeTenant", "fakeProject"),
    35  			Executor:    resModel.ExecutorID(executor),
    36  			WorkerID:    workerID,
    37  		},
    38  	}
    39  }
    40  
    41  func TestFileManagerBasics(t *testing.T) {
    42  	t.Parallel()
    43  
    44  	executorID := "executor-1"
    45  	dir := t.TempDir()
    46  	fm := NewLocalFileManager(model.ExecutorID(executorID), resModel.LocalFileConfig{BaseDir: dir})
    47  
    48  	// In this test, we create resource-1 and resource-2, and only
    49  	// resource-1 will be marked as persisted.
    50  	//
    51  	// Then we test that resource-2 can be correctly cleaned up as
    52  	// temporary files, while resource-1 can be cleaned up as a persisted
    53  	// resource.
    54  
    55  	ctx := context.Background()
    56  	// Creates resource-1
    57  	res, err := fm.CreateResource(ctx,
    58  		newResourceIdentForTesting(executorID, "worker-1", "resource-1"))
    59  	require.NoError(t, err)
    60  	res1, ok := res.(*resourceDescriptor)
    61  	require.True(t, ok)
    62  	require.Equal(t, &resourceDescriptor{
    63  		BasePath: dir,
    64  		Ident: internal.ResourceIdent{
    65  			Name: "resource-1",
    66  			ResourceScope: internal.ResourceScope{
    67  				ProjectInfo: tenant.NewProjectInfo("fakeTenant", "fakeProject"),
    68  				WorkerID:    "worker-1",
    69  				Executor:    model.ExecutorID(executorID),
    70  			},
    71  		},
    72  	}, res1)
    73  
    74  	storage, err := newBrStorageForLocalFile(res1.AbsolutePath())
    75  	require.NoError(t, err)
    76  	fwriter, err := storage.Create(context.Background(), "1.txt", nil)
    77  	require.NoError(t, err)
    78  	err = fwriter.Close(context.Background())
    79  	require.NoError(t, err)
    80  	require.FileExists(t, res1.AbsolutePath()+"/1.txt")
    81  
    82  	fm.SetPersisted(ctx, newResourceIdentForTesting(executorID, "worker-1", "resource-1"))
    83  
    84  	// Creates resource-2
    85  	res, err = fm.CreateResource(ctx, newResourceIdentForTesting(executorID, "worker-1", "resource-2"))
    86  	require.NoError(t, err)
    87  	res2, ok := res.(*resourceDescriptor)
    88  	require.True(t, ok)
    89  	require.Equal(t, &resourceDescriptor{
    90  		BasePath: dir,
    91  		Ident: internal.ResourceIdent{
    92  			Name: "resource-2",
    93  			ResourceScope: internal.ResourceScope{
    94  				ProjectInfo: tenant.NewProjectInfo("fakeTenant", "fakeProject"),
    95  				WorkerID:    "worker-1",
    96  				Executor:    model.ExecutorID(executorID),
    97  			},
    98  		},
    99  	}, res2)
   100  
   101  	storage, err = newBrStorageForLocalFile(res2.AbsolutePath())
   102  	require.NoError(t, err)
   103  	fwriter, err = storage.Create(context.Background(), "1.txt", nil)
   104  	require.NoError(t, err)
   105  	err = fwriter.Close(context.Background())
   106  	require.NoError(t, err)
   107  	require.FileExists(t, res2.AbsolutePath()+"/1.txt")
   108  
   109  	// Clean up temporary files
   110  	err = fm.RemoveTemporaryFiles(ctx, internal.ResourceScope{
   111  		Executor: model.ExecutorID(executorID), WorkerID: "worker-1",
   112  	})
   113  	require.NoError(t, err)
   114  
   115  	require.NoDirExists(t, res2.AbsolutePath())
   116  	require.DirExists(t, res1.AbsolutePath())
   117  
   118  	// Clean up persisted resource
   119  	err = fm.RemoveResource(ctx, newResourceIdentForTesting(executorID, "worker-1", "resource-1"))
   120  	require.NoError(t, err)
   121  	require.NoDirExists(t, res1.AbsolutePath())
   122  
   123  	// Test repeated removals
   124  	err = fm.RemoveResource(ctx, newResourceIdentForTesting(executorID, "worker-1", "resource-1"))
   125  	require.Error(t, err)
   126  	require.Regexp(t, ".*ErrResourceDoesNotExist.*", err)
   127  }
   128  
   129  func TestFileManagerManyWorkers(t *testing.T) {
   130  	t.Parallel()
   131  
   132  	const numWorkers = 10
   133  
   134  	dir := t.TempDir()
   135  	fm := NewLocalFileManager("", resModel.LocalFileConfig{BaseDir: dir})
   136  	ctx := context.Background()
   137  
   138  	for i := 0; i < numWorkers; i++ {
   139  		// For each worker, first create a persisted resource
   140  		res, err := fm.CreateResource(ctx, newResourceIdentForTesting("",
   141  			fmt.Sprintf("worker-%d", i),
   142  			fmt.Sprintf("resource-%d-1", i)))
   143  		require.NoError(t, err)
   144  		res1, ok := res.(*resourceDescriptor)
   145  		require.True(t, ok)
   146  
   147  		storage, err := newBrStorageForLocalFile(res1.AbsolutePath())
   148  		require.NoError(t, err)
   149  		fwriter, err := storage.Create(context.Background(), "1.txt", nil)
   150  		require.NoError(t, err)
   151  		err = fwriter.Close(context.Background())
   152  		require.NoError(t, err)
   153  		require.FileExists(t, res1.AbsolutePath()+"/1.txt")
   154  
   155  		fm.SetPersisted(ctx, newResourceIdentForTesting("",
   156  			fmt.Sprintf("worker-%d", i),
   157  			fmt.Sprintf("resource-%d-1", i)))
   158  
   159  		// Then create a temporary resource
   160  		res, err = fm.CreateResource(ctx, newResourceIdentForTesting("",
   161  			fmt.Sprintf("worker-%d", i),
   162  			fmt.Sprintf("resource-%d-2", i)))
   163  		require.NoError(t, err)
   164  		res2, ok := res.(*resourceDescriptor)
   165  		require.True(t, ok)
   166  
   167  		storage, err = newBrStorageForLocalFile(res2.AbsolutePath())
   168  		require.NoError(t, err)
   169  		fwriter, err = storage.Create(context.Background(), "1.txt", nil)
   170  		require.NoError(t, err)
   171  		err = fwriter.Close(context.Background())
   172  		require.NoError(t, err)
   173  		require.FileExists(t, res2.AbsolutePath()+"/1.txt")
   174  	}
   175  
   176  	// Garbage collects about half the workers' temporary files.
   177  	for i := 0; i < numWorkers/2; i++ {
   178  		workerID := fmt.Sprintf("worker-%d", i)
   179  		err := fm.RemoveTemporaryFiles(ctx, internal.ResourceScope{WorkerID: workerID})
   180  		require.NoError(t, err)
   181  	}
   182  
   183  	for i := 0; i < numWorkers; i++ {
   184  		workerID := fmt.Sprintf("worker-%d", i)
   185  		resourceID1 := fmt.Sprintf("resource-%d-1", i)
   186  		require.DirExists(t, filepath.Join(dir, workerID, ResourceNameToFilePathName(resourceID1)))
   187  
   188  		resourceID2 := fmt.Sprintf("resource-%d-2", i)
   189  		if i < numWorkers/2 {
   190  			require.NoDirExists(t, filepath.Join(dir, workerID, ResourceNameToFilePathName(resourceID2)))
   191  		} else {
   192  			require.DirExists(t, filepath.Join(dir, workerID, ResourceNameToFilePathName(resourceID2)))
   193  		}
   194  	}
   195  }
   196  
   197  func TestCleanUpTemporaryFilesNotFound(t *testing.T) {
   198  	t.Parallel()
   199  
   200  	dir := t.TempDir()
   201  	fm := NewLocalFileManager("", resModel.LocalFileConfig{BaseDir: dir})
   202  
   203  	// Note that worker-1 does not have any resource.
   204  	err := fm.RemoveTemporaryFiles(context.Background(),
   205  		internal.ResourceScope{WorkerID: "worker-1"})
   206  	// We expect NoError because it is normal for a worker
   207  	// to never create any resource.
   208  	require.NoError(t, err)
   209  }
   210  
   211  func TestCreateAndGetResource(t *testing.T) {
   212  	t.Parallel()
   213  
   214  	dir := t.TempDir()
   215  	fm := NewLocalFileManager("", resModel.LocalFileConfig{BaseDir: dir})
   216  	ctx := context.Background()
   217  	ident := newResourceIdentForTesting("", "worker-1", "resource-1")
   218  	_, err := fm.GetPersistedResource(ctx, ident)
   219  	require.Error(t, err)
   220  	require.Regexp(t, ".*ErrResourceDoesNotExist.*", err)
   221  
   222  	_, err = fm.CreateResource(ctx, ident)
   223  	require.NoError(t, err)
   224  
   225  	_, err = fm.GetPersistedResource(ctx, ident)
   226  	require.Error(t, err)
   227  	require.Regexp(t, ".*ErrResourceDoesNotExist.*", err)
   228  
   229  	fm.SetPersisted(ctx, ident)
   230  	_, err = fm.GetPersistedResource(ctx, ident)
   231  	require.NoError(t, err)
   232  
   233  	err = fm.RemoveResource(ctx, ident)
   234  	require.NoError(t, err)
   235  
   236  	_, err = fm.GetPersistedResource(ctx, ident)
   237  	require.Error(t, err)
   238  	require.Regexp(t, ".*ErrResourceDoesNotExist.*", err)
   239  }
   240  
   241  func TestResourceNamesWithSlash(t *testing.T) {
   242  	t.Parallel()
   243  
   244  	dir := t.TempDir()
   245  	fm := NewLocalFileManager("", resModel.LocalFileConfig{BaseDir: dir})
   246  
   247  	ctx := context.Background()
   248  	_, err := fm.CreateResource(ctx, newResourceIdentForTesting("", "worker-1", "a"))
   249  	require.NoError(t, err)
   250  
   251  	_, err = fm.CreateResource(ctx, newResourceIdentForTesting("", "worker-1", "a/b"))
   252  	require.NoError(t, err)
   253  
   254  	_, err = fm.CreateResource(ctx, newResourceIdentForTesting("", "worker-1", "a/b/c"))
   255  	require.NoError(t, err)
   256  
   257  	fm.SetPersisted(ctx, newResourceIdentForTesting("", "worker-1", "a/b/c"))
   258  	_, err = fm.GetPersistedResource(ctx, newResourceIdentForTesting("", "worker-1", "a/b/c"))
   259  	require.NoError(t, err)
   260  
   261  	err = fm.RemoveTemporaryFiles(ctx, internal.ResourceScope{WorkerID: "worker-1"})
   262  	require.NoError(t, err)
   263  
   264  	_, err = fm.GetPersistedResource(ctx, newResourceIdentForTesting("", "worker-1", "a/b/c"))
   265  	require.NoError(t, err)
   266  }
   267  
   268  func TestPreCheckConfig(t *testing.T) {
   269  	t.Parallel()
   270  
   271  	// Happy path
   272  	dir := t.TempDir()
   273  	err := PreCheckConfig(resModel.LocalFileConfig{BaseDir: dir})
   274  	require.NoError(t, err)
   275  
   276  	// Directory does not exist but can be created.
   277  	baseDir := filepath.Join(dir, "not-exist")
   278  	err = PreCheckConfig(resModel.LocalFileConfig{BaseDir: baseDir})
   279  	require.NoError(t, err)
   280  
   281  	// Directory exists but not writable
   282  	baseDir = filepath.Join(dir, "not-writable")
   283  	require.NoError(t, os.MkdirAll(baseDir, 0o400))
   284  	err = PreCheckConfig(resModel.LocalFileConfig{BaseDir: baseDir})
   285  	require.Error(t, err)
   286  	require.Regexp(t, ".*ErrLocalFileDirNotWritable.*", err)
   287  }