github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/engine/pkg/externalresource/internal/bucket/file_manager.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 bucket
    15  
    16  import (
    17  	"context"
    18  	"fmt"
    19  	"strings"
    20  	"sync"
    21  
    22  	"github.com/pingcap/log"
    23  	brStorage "github.com/pingcap/tidb/br/pkg/storage"
    24  	"github.com/pingcap/tiflow/engine/model"
    25  	"github.com/pingcap/tiflow/engine/pkg/externalresource/internal"
    26  	resModel "github.com/pingcap/tiflow/engine/pkg/externalresource/model"
    27  	"github.com/pingcap/tiflow/pkg/errors"
    28  	"go.uber.org/zap"
    29  )
    30  
    31  const (
    32  	// TODO: use the contents of placeholder to indicate persistent status
    33  	placeholderFileName = ".keep"
    34  )
    35  
    36  type persistedResources map[resModel.ResourceName]struct{}
    37  
    38  func newPersistedResources() persistedResources {
    39  	return make(map[resModel.ResourceName]struct{})
    40  }
    41  
    42  func (f persistedResources) SetPersisted(ident internal.ResourceIdent) bool {
    43  	if _, ok := f[ident.Name]; ok {
    44  		return false
    45  	}
    46  	f[ident.Name] = struct{}{}
    47  	return true
    48  }
    49  
    50  func (f persistedResources) UnsetPersisted(ident internal.ResourceIdent) bool {
    51  	if _, ok := f[ident.Name]; !ok {
    52  		return false
    53  	}
    54  	delete(f, ident.Name)
    55  	return true
    56  }
    57  
    58  // FileManager manages resource files stored on s3.
    59  type FileManager struct {
    60  	executorID     model.ExecutorID
    61  	storageCreator Creator
    62  
    63  	mu              sync.RWMutex
    64  	persistedResMap map[resModel.WorkerID]persistedResources
    65  }
    66  
    67  // NewFileManagerWithConfig returns a new bucket FileManager.
    68  // Note that the lifetime of the returned object should span the whole
    69  // lifetime of the executor.
    70  func NewFileManagerWithConfig(
    71  	executorID resModel.ExecutorID, config *resModel.Config,
    72  ) *FileManager {
    73  	creator := NewCreator(config)
    74  	return NewFileManager(executorID, creator)
    75  }
    76  
    77  // NewFileManager creates a new bucket FileManager.
    78  func NewFileManager(
    79  	executorID resModel.ExecutorID,
    80  	creator Creator,
    81  ) *FileManager {
    82  	return &FileManager{
    83  		executorID:      executorID,
    84  		storageCreator:  creator,
    85  		persistedResMap: make(map[string]persistedResources),
    86  	}
    87  }
    88  
    89  // CreateResource creates a new resource on s3.
    90  // It can only be used to create resources that belong to the current executor.
    91  func (m *FileManager) CreateResource(
    92  	ctx context.Context, ident internal.ResourceIdent,
    93  ) (internal.ResourceDescriptor, error) {
    94  	m.validateExecutor(ident.Executor, ident)
    95  	desc := newResourceDescriptor(ident, m.storageCreator)
    96  	storage, err := desc.ExternalStorage(ctx)
    97  	if err != nil {
    98  		return nil, err
    99  	}
   100  
   101  	if err := createPlaceholderFile(ctx, storage); err != nil {
   102  		return nil, err
   103  	}
   104  	return desc, nil
   105  }
   106  
   107  // GetPersistedResource returns the descriptor of a resource that has already
   108  // been marked as persisted.
   109  // Note that GetPersistedResource will work on any executor for any persisted resource.
   110  func (m *FileManager) GetPersistedResource(
   111  	ctx context.Context, ident internal.ResourceIdent,
   112  ) (internal.ResourceDescriptor, error) {
   113  	desc := newResourceDescriptor(ident, m.storageCreator)
   114  	storage, err := desc.ExternalStorage(ctx)
   115  	if err != nil {
   116  		return nil, err
   117  	}
   118  
   119  	ok, err := storage.FileExists(ctx, placeholderFileName)
   120  	if err != nil {
   121  		return nil, errors.ErrExternalStorageAPI.Wrap(err).GenWithStackByArgs("check placeholder file")
   122  	}
   123  	if !ok {
   124  		return nil, errors.ErrResourceFilesNotFound.GenWithStackByArgs()
   125  	}
   126  
   127  	return desc, nil
   128  }
   129  
   130  // CleanOrRecreatePersistedResource cleans the bucket directory or recreates placeholder
   131  // file of the given resource.
   132  // Note that CleanOrRecreatePersistedResource will work on any executor for any persisted resource.
   133  func (m *FileManager) CleanOrRecreatePersistedResource(
   134  	ctx context.Context, ident internal.ResourceIdent,
   135  ) (internal.ResourceDescriptor, error) {
   136  	desc, err := m.GetPersistedResource(ctx, ident)
   137  	if errors.Is(err, errors.ErrResourceFilesNotFound) {
   138  		desc := newResourceDescriptor(ident, m.storageCreator)
   139  		storage, err := desc.ExternalStorage(ctx)
   140  		if err != nil {
   141  			return nil, err
   142  		}
   143  
   144  		if err := createPlaceholderFile(ctx, storage); err != nil {
   145  			return nil, err
   146  		}
   147  		return desc, nil
   148  	}
   149  	if err != nil {
   150  		return nil, err
   151  	}
   152  
   153  	err = m.removeFilesIf(ctx, ident.Scope(), getPathPredByName(ident.Name, true))
   154  	if err != nil {
   155  		return nil, err
   156  	}
   157  
   158  	return desc, nil
   159  }
   160  
   161  // RemoveTemporaryFiles removes all temporary resources (those that are not persisted).
   162  // It can only be used to clean up resources created by the local executor.
   163  func (m *FileManager) RemoveTemporaryFiles(
   164  	ctx context.Context, scope internal.ResourceScope,
   165  ) error {
   166  	m.validateExecutor(scope.Executor, scope)
   167  	if scope.WorkerID == "" {
   168  		return m.removeTemporaryFilesForExecutor(ctx, scope)
   169  	}
   170  	return m.removeTemporaryFilesForWorker(ctx, scope)
   171  }
   172  
   173  func (m *FileManager) removeTemporaryFilesForWorker(
   174  	ctx context.Context, scope internal.ResourceScope,
   175  ) error {
   176  	m.mu.RLock()
   177  	resources, ok := m.persistedResMap[scope.WorkerID]
   178  	// unlock here is safe because `resources` will not be changed after worker exits.
   179  	m.mu.RUnlock()
   180  
   181  	log.Info("Removing temporary resources for single worker", zap.Any("scope", scope))
   182  	if !ok {
   183  		return m.removeFilesIf(ctx, scope, getPathPredAlwaysTrue())
   184  	}
   185  
   186  	return m.removeFilesIf(ctx, scope, getPathPredByPersistedResources(resources, 1))
   187  }
   188  
   189  func (m *FileManager) removeTemporaryFilesForExecutor(
   190  	ctx context.Context, scope internal.ResourceScope,
   191  ) error {
   192  	// Get all persisted files which is created by current executor.
   193  	persistedResSet := make(map[string]struct{})
   194  
   195  	m.mu.RLock()
   196  	for workerID, resources := range m.persistedResMap {
   197  		for resName := range resources {
   198  			resPath := fmt.Sprintf("%s/%s", workerID, resName)
   199  			persistedResSet[resPath] = struct{}{}
   200  		}
   201  	}
   202  	m.mu.RUnlock()
   203  
   204  	return m.removeAllTemporaryFilesByMeta(ctx, scope, persistedResSet)
   205  }
   206  
   207  // removeAllTemporaryFilesByMeta removes all temporary resources located in the given scope.
   208  // Note that this function could be called from executor and master.
   209  func (m *FileManager) removeAllTemporaryFilesByMeta(
   210  	ctx context.Context,
   211  	scope internal.ResourceScope,
   212  	persistedResSet map[string]struct{},
   213  ) error {
   214  	log.Info("Removing temporary resources for executor", zap.Any("scope", scope))
   215  
   216  	return m.removeFilesIf(ctx, scope, getPathPredByPersistedResources(persistedResSet, 2))
   217  }
   218  
   219  // RemoveResource removes a resource from s3.
   220  // It can be called on any executor node.
   221  func (m *FileManager) RemoveResource(
   222  	ctx context.Context, ident internal.ResourceIdent,
   223  ) error {
   224  	log.Info("Removing resource",
   225  		zap.Any("ident", ident))
   226  
   227  	err := m.removeFilesIf(ctx, ident.Scope(), getPathPredByName(ident.Name, false))
   228  	if err != nil {
   229  		return err
   230  	}
   231  
   232  	if m.executorID == ident.Executor {
   233  		// Remove from persistedResMap
   234  		m.mu.Lock()
   235  		defer m.mu.Unlock()
   236  		if resources, ok := m.persistedResMap[ident.WorkerID]; ok {
   237  			resources.UnsetPersisted(ident)
   238  		}
   239  	}
   240  	return nil
   241  }
   242  
   243  // SetPersisted marks a resource as persisted. It can only be called
   244  // on the creator of the resource.
   245  func (m *FileManager) SetPersisted(
   246  	ctx context.Context, ident internal.ResourceIdent,
   247  ) error {
   248  	m.validateExecutor(ident.Executor, ident)
   249  
   250  	m.mu.Lock()
   251  	defer m.mu.Unlock()
   252  	resources, ok := m.persistedResMap[ident.WorkerID]
   253  	if !ok {
   254  		resources = newPersistedResources()
   255  		m.persistedResMap[ident.WorkerID] = resources
   256  	}
   257  
   258  	ok = resources.SetPersisted(ident)
   259  	if !ok {
   260  		log.Warn("resource is already persisted",
   261  			zap.Any("ident", ident))
   262  	}
   263  	return nil
   264  }
   265  
   266  func (m *FileManager) validateExecutor(creator model.ExecutorID, res interface{}) {
   267  	if creator != m.executorID {
   268  		log.Panic("inconsistent executor ID of bucket file",
   269  			zap.Any("resource", res),
   270  			zap.Any("creator", creator),
   271  			zap.String("currentExecutor", string(m.executorID)))
   272  	}
   273  }
   274  
   275  func (m *FileManager) removeFilesIf(
   276  	ctx context.Context,
   277  	scope internal.ResourceScope,
   278  	pred func(path string) bool,
   279  ) error {
   280  	// TODO: add a cache here to reuse storage.
   281  	storage, err := m.storageCreator.newBucketForScope(ctx, scope)
   282  	if err != nil {
   283  		return err
   284  	}
   285  
   286  	var toRemoveFiles []string
   287  	err = storage.WalkDir(ctx, &brStorage.WalkOption{}, func(path string, _ int64) error {
   288  		path = strings.TrimPrefix(path, "/")
   289  		if pred(path) {
   290  			toRemoveFiles = append(toRemoveFiles, path)
   291  		}
   292  		return nil
   293  	})
   294  	if err != nil {
   295  		return errors.ErrExternalStorageAPI.Wrap(err).GenWithStackByArgs("RemoveTemporaryFiles")
   296  	}
   297  
   298  	log.Info("Removing resources",
   299  		zap.Any("scope", scope),
   300  		zap.Any("numOfToRemoveFiles", len(toRemoveFiles)))
   301  	log.Debug("Removing files", zap.Any("toRemoveFiles", toRemoveFiles))
   302  
   303  	for _, path := range toRemoveFiles {
   304  		if err := storage.DeleteFile(ctx, path); err != nil {
   305  			return errors.ErrExternalStorageAPI.Wrap(err)
   306  		}
   307  	}
   308  	return nil
   309  }
   310  
   311  func createPlaceholderFile(ctx context.Context, storage brStorage.ExternalStorage) error {
   312  	exists, err := storage.FileExists(ctx, placeholderFileName)
   313  	if err != nil {
   314  		return errors.ErrExternalStorageAPI.Wrap(err).GenWithStackByArgs("checking placeholder file")
   315  	}
   316  	if exists {
   317  		// This should not happen in production. Unless the caller of the FileManager has a bug.
   318  		return errors.ErrExternalStorageAPI.GenWithStackByArgs("resource already exists")
   319  	}
   320  
   321  	writer, err := storage.Create(ctx, placeholderFileName, nil)
   322  	if err != nil {
   323  		return errors.ErrExternalStorageAPI.Wrap(err).GenWithStackByArgs("creating placeholder file")
   324  	}
   325  
   326  	_, err = writer.Write(ctx, []byte("placeholder"))
   327  	if err != nil {
   328  		return errors.ErrExternalStorageAPI.Wrap(err).GenWithStackByArgs("writing placeholder file")
   329  	}
   330  
   331  	if err := writer.Close(ctx); err != nil {
   332  		return errors.ErrExternalStorageAPI.Wrap(err).GenWithStackByArgs("closing placeholder file")
   333  	}
   334  	return nil
   335  }
   336  
   337  // PreCheckConfig does a preflight check on the executor's storage configurations.
   338  func PreCheckConfig(config *resModel.Config) error {
   339  	// TODO: use customized retry policy.
   340  	log.Debug("pre-checking s3Storage config", zap.Any("config", config))
   341  	creator := NewCreator(config)
   342  	_, err := creator.newBucketForScope(context.Background(), internal.ResourceScope{})
   343  	if err != nil {
   344  		return err
   345  	}
   346  	return nil
   347  }