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  }