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 }