github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/pkg/util/external_storage.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 util 15 16 import ( 17 "context" 18 "fmt" 19 "net/http" 20 "net/url" 21 "os" 22 "strings" 23 "time" 24 25 gcsStorage "cloud.google.com/go/storage" 26 "github.com/Azure/azure-sdk-for-go/sdk/azcore" 27 "github.com/aws/aws-sdk-go/aws/awserr" 28 "github.com/aws/aws-sdk-go/aws/client" 29 "github.com/aws/aws-sdk-go/aws/request" 30 "github.com/aws/aws-sdk-go/service/s3" 31 "github.com/pingcap/log" 32 "github.com/pingcap/tidb/br/pkg/storage" 33 "github.com/pingcap/tiflow/pkg/errors" 34 "go.uber.org/zap" 35 "golang.org/x/sync/errgroup" 36 ) 37 38 // GetExternalStorageFromURI creates a new storage.ExternalStorage from a uri. 39 func GetExternalStorageFromURI( 40 ctx context.Context, uri string, 41 ) (storage.ExternalStorage, error) { 42 return GetExternalStorage(ctx, uri, nil, DefaultS3Retryer()) 43 } 44 45 // GetExternalStorageWithTimeout creates a new storage.ExternalStorage from a uri 46 // without retry. It is the caller's responsibility to set timeout to the context. 47 func GetExternalStorageWithTimeout( 48 ctx context.Context, uri string, timeout time.Duration, 49 ) (storage.ExternalStorage, error) { 50 ctx, cancel := context.WithTimeout(ctx, timeout) 51 defer cancel() 52 s, err := GetExternalStorage(ctx, uri, nil, nil) 53 54 return &extStorageWithTimeout{ 55 ExternalStorage: s, 56 timeout: timeout, 57 }, err 58 } 59 60 // GetExternalStorage creates a new storage.ExternalStorage based on the uri and options. 61 func GetExternalStorage( 62 ctx context.Context, uri string, 63 opts *storage.BackendOptions, 64 retryer request.Retryer, 65 ) (storage.ExternalStorage, error) { 66 backEnd, err := storage.ParseBackend(uri, opts) 67 if err != nil { 68 return nil, errors.Trace(err) 69 } 70 71 ret, err := storage.New(ctx, backEnd, &storage.ExternalStorageOptions{ 72 SendCredentials: false, 73 S3Retryer: retryer, 74 }) 75 if err != nil { 76 retErr := errors.ErrFailToCreateExternalStorage.Wrap(errors.Trace(err)) 77 return nil, retErr.GenWithStackByArgs("creating ExternalStorage for s3") 78 } 79 80 // Check the connection and ignore the returned bool value, since we don't care if the file exists. 81 _, err = ret.FileExists(ctx, "test") 82 if err != nil { 83 retErr := errors.ErrFailToCreateExternalStorage.Wrap(errors.Trace(err)) 84 return nil, retErr.GenWithStackByArgs("creating ExternalStorage for s3") 85 } 86 return ret, nil 87 } 88 89 // GetTestExtStorage creates a test storage.ExternalStorage from a uri. 90 func GetTestExtStorage( 91 ctx context.Context, tmpDir string, 92 ) (storage.ExternalStorage, *url.URL, error) { 93 uriStr := fmt.Sprintf("file://%s", tmpDir) 94 ret, err := GetExternalStorageFromURI(ctx, uriStr) 95 if err != nil { 96 return nil, nil, err 97 } 98 uri, err := storage.ParseRawURL(uriStr) 99 if err != nil { 100 return nil, nil, err 101 } 102 return ret, uri, nil 103 } 104 105 // retryerWithLog wraps the client.DefaultRetryer, and logs when retrying. 106 type retryerWithLog struct { 107 client.DefaultRetryer 108 } 109 110 func isDeadlineExceedError(err error) bool { 111 return strings.Contains(err.Error(), "context deadline exceeded") 112 } 113 114 func (rl retryerWithLog) ShouldRetry(r *request.Request) bool { 115 if isDeadlineExceedError(r.Error) { 116 return false 117 } 118 return rl.DefaultRetryer.ShouldRetry(r) 119 } 120 121 func (rl retryerWithLog) RetryRules(r *request.Request) time.Duration { 122 backoffTime := rl.DefaultRetryer.RetryRules(r) 123 if backoffTime > 0 { 124 log.Warn("failed to request s3, retrying", 125 zap.Error(r.Error), 126 zap.Duration("backoff", backoffTime)) 127 } 128 return backoffTime 129 } 130 131 // DefaultS3Retryer is the default s3 retryer, maybe this function 132 // should be extracted to another place. 133 func DefaultS3Retryer() request.Retryer { 134 return retryerWithLog{ 135 DefaultRetryer: client.DefaultRetryer{ 136 NumMaxRetries: 3, 137 MinRetryDelay: 1 * time.Second, 138 MinThrottleDelay: 2 * time.Second, 139 }, 140 } 141 } 142 143 type extStorageWithTimeout struct { 144 storage.ExternalStorage 145 timeout time.Duration 146 } 147 148 // WriteFile writes a complete file to storage, similar to os.WriteFile, 149 // but WriteFile should be atomic 150 func (s *extStorageWithTimeout) WriteFile(ctx context.Context, name string, data []byte) error { 151 ctx, cancel := context.WithTimeout(ctx, s.timeout) 152 defer cancel() 153 return s.ExternalStorage.WriteFile(ctx, name, data) 154 } 155 156 // ReadFile reads a complete file from storage, similar to os.ReadFile 157 func (s *extStorageWithTimeout) ReadFile(ctx context.Context, name string) ([]byte, error) { 158 ctx, cancel := context.WithTimeout(ctx, s.timeout) 159 defer cancel() 160 return s.ExternalStorage.ReadFile(ctx, name) 161 } 162 163 // FileExists return true if file exists 164 func (s *extStorageWithTimeout) FileExists(ctx context.Context, name string) (bool, error) { 165 ctx, cancel := context.WithTimeout(ctx, s.timeout) 166 defer cancel() 167 return s.ExternalStorage.FileExists(ctx, name) 168 } 169 170 // DeleteFile delete the file in storage 171 func (s *extStorageWithTimeout) DeleteFile(ctx context.Context, name string) error { 172 ctx, cancel := context.WithTimeout(ctx, s.timeout) 173 defer cancel() 174 return s.ExternalStorage.DeleteFile(ctx, name) 175 } 176 177 // Open a Reader by file path. path is relative path to storage base path 178 func (s *extStorageWithTimeout) Open( 179 ctx context.Context, path string, _ *storage.ReaderOption, 180 ) (storage.ExternalFileReader, error) { 181 ctx, cancel := context.WithTimeout(ctx, s.timeout) 182 defer cancel() 183 return s.ExternalStorage.Open(ctx, path, nil) 184 } 185 186 // WalkDir traverse all the files in a dir. 187 func (s *extStorageWithTimeout) WalkDir( 188 ctx context.Context, opt *storage.WalkOption, fn func(path string, size int64) error, 189 ) error { 190 ctx, cancel := context.WithTimeout(ctx, s.timeout) 191 defer cancel() 192 return s.ExternalStorage.WalkDir(ctx, opt, fn) 193 } 194 195 // Create opens a file writer by path. path is relative path to storage base path 196 func (s *extStorageWithTimeout) Create( 197 ctx context.Context, path string, option *storage.WriterOption, 198 ) (storage.ExternalFileWriter, error) { 199 if option.Concurrency <= 1 { 200 var cancel context.CancelFunc 201 ctx, cancel = context.WithTimeout(ctx, s.timeout) 202 defer cancel() 203 } 204 // multipart uploading spawns a background goroutine, can't set timeout 205 return s.ExternalStorage.Create(ctx, path, option) 206 } 207 208 // Rename file name from oldFileName to newFileName 209 func (s *extStorageWithTimeout) Rename( 210 ctx context.Context, oldFileName, newFileName string, 211 ) error { 212 ctx, cancel := context.WithTimeout(ctx, s.timeout) 213 defer cancel() 214 return s.ExternalStorage.Rename(ctx, oldFileName, newFileName) 215 } 216 217 // IsNotExistInExtStorage checks if the error is caused by the file not exist in external storage. 218 func IsNotExistInExtStorage(err error) bool { 219 if err == nil { 220 return false 221 } 222 223 if os.IsNotExist(errors.Cause(err)) { 224 return true 225 } 226 227 if aerr, ok := errors.Cause(err).(awserr.Error); ok { // nolint:errorlint 228 switch aerr.Code() { 229 case s3.ErrCodeNoSuchBucket, s3.ErrCodeNoSuchKey, "NotFound": 230 return true 231 } 232 } 233 234 if errors.Cause(err) == gcsStorage.ErrObjectNotExist { // nolint:errorlint 235 return true 236 } 237 238 var respErr *azcore.ResponseError 239 if errors.As(err, &respErr) { 240 if respErr.StatusCode == http.StatusNotFound { 241 return true 242 } 243 } 244 return false 245 } 246 247 // RemoveFilesIf removes files from external storage if the path matches the predicate. 248 func RemoveFilesIf( 249 ctx context.Context, 250 extStorage storage.ExternalStorage, 251 pred func(path string) bool, 252 opt *storage.WalkOption, 253 ) error { 254 var toRemoveFiles []string 255 err := extStorage.WalkDir(ctx, opt, func(path string, _ int64) error { 256 path = strings.TrimPrefix(path, "/") 257 if pred(path) { 258 toRemoveFiles = append(toRemoveFiles, path) 259 } 260 return nil 261 }) 262 if err != nil { 263 return errors.ErrExternalStorageAPI.Wrap(err).GenWithStackByArgs("RemoveTemporaryFiles") 264 } 265 266 log.Debug("Removing files", zap.Any("toRemoveFiles", toRemoveFiles)) 267 return DeleteFilesInExtStorage(ctx, extStorage, toRemoveFiles) 268 } 269 270 // DeleteFilesInExtStorage deletes files in external storage concurrently. 271 // TODO: Add a test for this function to cover batch delete. 272 func DeleteFilesInExtStorage( 273 ctx context.Context, extStorage storage.ExternalStorage, toRemoveFiles []string, 274 ) error { 275 limit := make(chan struct{}, 32) 276 batch := 3000 277 eg, egCtx := errgroup.WithContext(ctx) 278 for len(toRemoveFiles) > 0 { 279 select { 280 case <-egCtx.Done(): 281 return egCtx.Err() 282 case limit <- struct{}{}: 283 } 284 285 if len(toRemoveFiles) < batch { 286 batch = len(toRemoveFiles) 287 } 288 files := toRemoveFiles[:batch] 289 eg.Go(func() error { 290 defer func() { <-limit }() 291 err := extStorage.DeleteFiles(egCtx, files) 292 if err != nil && !IsNotExistInExtStorage(err) { 293 // if fail then retry, may end up with notExit err, ignore the error 294 return errors.ErrExternalStorageAPI.Wrap(err) 295 } 296 return nil 297 }) 298 toRemoveFiles = toRemoveFiles[batch:] 299 } 300 return eg.Wait() 301 }