github.com/weaviate/weaviate@v1.24.6/usecases/backup/scheduler.go (about)

     1  //                           _       _
     2  // __      _____  __ ___   ___  __ _| |_ ___
     3  // \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \
     4  //  \ V  V /  __/ (_| |\ V /| | (_| | ||  __/
     5  //   \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___|
     6  //
     7  //  Copyright © 2016 - 2024 Weaviate B.V. All rights reserved.
     8  //
     9  //  CONTACT: hello@weaviate.io
    10  //
    11  
    12  package backup
    13  
    14  import (
    15  	"context"
    16  	"errors"
    17  	"fmt"
    18  	"time"
    19  
    20  	"github.com/sirupsen/logrus"
    21  	"github.com/weaviate/weaviate/entities/backup"
    22  	"github.com/weaviate/weaviate/entities/models"
    23  )
    24  
    25  var (
    26  	errLocalBackendDBRO = errors.New("local filesystem backend is not viable for backing up a node cluster, try s3 or gcs")
    27  	errIncludeExclude   = errors.New("malformed request: 'include' and 'exclude' cannot both contain values")
    28  )
    29  
    30  const (
    31  	errMsgHigherVersion = "unable to restore backup as it was produced by a higher version"
    32  )
    33  
    34  // Scheduler assigns backup operations to coordinators.
    35  type Scheduler struct {
    36  	// deps
    37  	logger     logrus.FieldLogger
    38  	authorizer authorizer
    39  	backupper  *coordinator
    40  	restorer   *coordinator
    41  	backends   BackupBackendProvider
    42  }
    43  
    44  // NewScheduler creates a new scheduler with two coordinators
    45  func NewScheduler(
    46  	authorizer authorizer,
    47  	client client,
    48  	sourcer selector,
    49  	backends BackupBackendProvider,
    50  	nodeResolver nodeResolver,
    51  	logger logrus.FieldLogger,
    52  ) *Scheduler {
    53  	m := &Scheduler{
    54  		logger:     logger,
    55  		authorizer: authorizer,
    56  		backends:   backends,
    57  		backupper: newCoordinator(
    58  			sourcer,
    59  			client,
    60  			logger, nodeResolver),
    61  		restorer: newCoordinator(
    62  			sourcer,
    63  			client,
    64  			logger, nodeResolver),
    65  	}
    66  	return m
    67  }
    68  
    69  func (s *Scheduler) Backup(ctx context.Context, pr *models.Principal, req *BackupRequest,
    70  ) (_ *models.BackupCreateResponse, err error) {
    71  	defer func(begin time.Time) {
    72  		logOperation(s.logger, "try_backup", req.ID, req.Backend, begin, err)
    73  	}(time.Now())
    74  
    75  	path := fmt.Sprintf("backups/%s/%s", req.Backend, req.ID)
    76  	if err := s.authorizer.Authorize(pr, "add", path); err != nil {
    77  		return nil, err
    78  	}
    79  	store, err := coordBackend(s.backends, req.Backend, req.ID)
    80  	if err != nil {
    81  		err = fmt.Errorf("no backup backend %q: %w, did you enable the right module?", req.Backend, err)
    82  		return nil, backup.NewErrUnprocessable(err)
    83  	}
    84  
    85  	classes, err := s.validateBackupRequest(ctx, store, req)
    86  	if err != nil {
    87  		return nil, backup.NewErrUnprocessable(err)
    88  	}
    89  
    90  	if err := store.Initialize(ctx); err != nil {
    91  		return nil, backup.NewErrUnprocessable(fmt.Errorf("init uploader: %w", err))
    92  	}
    93  	breq := Request{
    94  		Method:      OpCreate,
    95  		ID:          req.ID,
    96  		Backend:     req.Backend,
    97  		Classes:     classes,
    98  		Compression: req.Compression,
    99  	}
   100  	if err := s.backupper.Backup(ctx, store, &breq); err != nil {
   101  		return nil, backup.NewErrUnprocessable(err)
   102  	} else {
   103  		st := s.backupper.lastOp.get()
   104  		status := string(st.Status)
   105  		return &models.BackupCreateResponse{
   106  			Classes: classes,
   107  			ID:      req.ID,
   108  			Backend: req.Backend,
   109  			Status:  &status,
   110  			Path:    st.Path,
   111  		}, nil
   112  	}
   113  }
   114  
   115  func (s *Scheduler) Restore(ctx context.Context, pr *models.Principal,
   116  	req *BackupRequest,
   117  ) (_ *models.BackupRestoreResponse, err error) {
   118  	defer func(begin time.Time) {
   119  		logOperation(s.logger, "try_restore", req.ID, req.Backend, begin, err)
   120  	}(time.Now())
   121  	path := fmt.Sprintf("backups/%s/%s/restore", req.Backend, req.ID)
   122  	if err := s.authorizer.Authorize(pr, "restore", path); err != nil {
   123  		return nil, err
   124  	}
   125  	store, err := coordBackend(s.backends, req.Backend, req.ID)
   126  	if err != nil {
   127  		err = fmt.Errorf("no backup backend %q: %w, did you enable the right module?", req.Backend, err)
   128  		return nil, backup.NewErrUnprocessable(err)
   129  	}
   130  	meta, err := s.validateRestoreRequest(ctx, store, req)
   131  	if err != nil {
   132  		if errors.Is(err, errMetaNotFound) {
   133  			return nil, backup.NewErrNotFound(err)
   134  		}
   135  		return nil, backup.NewErrUnprocessable(err)
   136  	}
   137  	status := string(backup.Started)
   138  	data := &models.BackupRestoreResponse{
   139  		Backend: req.Backend,
   140  		ID:      req.ID,
   141  		Path:    store.HomeDir(),
   142  		Classes: meta.Classes(),
   143  	}
   144  
   145  	rReq := Request{
   146  		Method:      OpRestore,
   147  		ID:          req.ID,
   148  		Backend:     req.Backend,
   149  		Compression: req.Compression,
   150  	}
   151  	err = s.restorer.Restore(ctx, store, &rReq, meta)
   152  	if err != nil {
   153  		status = string(backup.Failed)
   154  		data.Error = err.Error()
   155  		return nil, backup.NewErrUnprocessable(err)
   156  	}
   157  
   158  	data.Status = &status
   159  	return data, nil
   160  }
   161  
   162  func (s *Scheduler) BackupStatus(ctx context.Context, principal *models.Principal,
   163  	backend, backupID string,
   164  ) (_ *Status, err error) {
   165  	defer func(begin time.Time) {
   166  		logOperation(s.logger, "backup_status", backupID, backend, begin, err)
   167  	}(time.Now())
   168  	path := fmt.Sprintf("backups/%s/%s", backend, backupID)
   169  	if err := s.authorizer.Authorize(principal, "get", path); err != nil {
   170  		return nil, err
   171  	}
   172  	store, err := coordBackend(s.backends, backend, backupID)
   173  	if err != nil {
   174  		err = fmt.Errorf("no backup provider %q: %w, did you enable the right module?", backend, err)
   175  		return nil, backup.NewErrUnprocessable(err)
   176  	}
   177  
   178  	req := &StatusRequest{OpCreate, backupID, backend}
   179  	st, err := s.backupper.OnStatus(ctx, store, req)
   180  	if err != nil {
   181  		return nil, backup.NewErrNotFound(err)
   182  	}
   183  	return st, nil
   184  }
   185  
   186  func (s *Scheduler) RestorationStatus(ctx context.Context, principal *models.Principal, backend, backupID string,
   187  ) (_ *Status, err error) {
   188  	defer func(begin time.Time) {
   189  		logOperation(s.logger, "restoration_status", backupID, backend, time.Now(), err)
   190  	}(time.Now())
   191  	path := fmt.Sprintf("backups/%s/%s/restore", backend, backupID)
   192  	if err := s.authorizer.Authorize(principal, "get", path); err != nil {
   193  		return nil, err
   194  	}
   195  	store, err := coordBackend(s.backends, backend, backupID)
   196  	if err != nil {
   197  		err = fmt.Errorf("no backup provider %q: %w, did you enable the right module?", backend, err)
   198  		return nil, backup.NewErrUnprocessable(err)
   199  	}
   200  	req := &StatusRequest{OpRestore, backupID, backend}
   201  	st, err := s.restorer.OnStatus(ctx, store, req)
   202  	if err != nil {
   203  		return nil, backup.NewErrNotFound(err)
   204  	}
   205  	return st, nil
   206  }
   207  
   208  func coordBackend(provider BackupBackendProvider, backend, id string) (coordStore, error) {
   209  	caps, err := provider.BackupBackend(backend)
   210  	if err != nil {
   211  		return coordStore{}, err
   212  	}
   213  	return coordStore{objStore{b: caps, BasePath: id}}, nil
   214  }
   215  
   216  func (s *Scheduler) validateBackupRequest(ctx context.Context, store coordStore, req *BackupRequest) ([]string, error) {
   217  	if !store.b.IsExternal() && s.backupper.nodeResolver.NodeCount() > 1 {
   218  		return nil, errLocalBackendDBRO
   219  	}
   220  
   221  	if err := validateID(req.ID); err != nil {
   222  		return nil, err
   223  	}
   224  	if len(req.Include) > 0 && len(req.Exclude) > 0 {
   225  		return nil, errIncludeExclude
   226  	}
   227  	if dup := findDuplicate(req.Include); dup != "" {
   228  		return nil, fmt.Errorf("class list 'include' contains duplicate: %s", dup)
   229  	}
   230  	classes := req.Include
   231  	if len(classes) == 0 {
   232  		classes = s.backupper.selector.ListClasses(ctx)
   233  		// no classes exist in the DB
   234  		if len(classes) == 0 {
   235  			return nil, fmt.Errorf("no available classes to backup, there's nothing to do here")
   236  		}
   237  	}
   238  	if classes = filterClasses(classes, req.Exclude); len(classes) == 0 {
   239  		return nil, fmt.Errorf("empty class list: please choose from : %v", classes)
   240  	}
   241  
   242  	if err := s.backupper.selector.Backupable(ctx, classes); err != nil {
   243  		return nil, err
   244  	}
   245  	destPath := store.HomeDir()
   246  	// there is no backup with given id on the backend, regardless of its state (valid or corrupted)
   247  	_, err := store.Meta(ctx, GlobalBackupFile)
   248  	if err == nil {
   249  		return nil, fmt.Errorf("backup %q already exists at %q", req.ID, destPath)
   250  	}
   251  	if _, ok := err.(backup.ErrNotFound); !ok {
   252  		return nil, fmt.Errorf("check if backup %q exists at %q: %w", req.ID, destPath, err)
   253  	}
   254  	return classes, nil
   255  }
   256  
   257  func (s *Scheduler) validateRestoreRequest(ctx context.Context, store coordStore, req *BackupRequest) (*backup.DistributedBackupDescriptor, error) {
   258  	if !store.b.IsExternal() && s.restorer.nodeResolver.NodeCount() > 1 {
   259  		return nil, errLocalBackendDBRO
   260  	}
   261  	if len(req.Include) > 0 && len(req.Exclude) > 0 {
   262  		return nil, errIncludeExclude
   263  	}
   264  	if dup := findDuplicate(req.Include); dup != "" {
   265  		return nil, fmt.Errorf("class list 'include' contains duplicate: %s", dup)
   266  	}
   267  	destPath := store.HomeDir()
   268  	meta, err := store.Meta(ctx, GlobalBackupFile)
   269  	if err != nil {
   270  		notFoundErr := backup.ErrNotFound{}
   271  		if errors.As(err, &notFoundErr) {
   272  			return nil, fmt.Errorf("backup id %q does not exist: %v: %w", req.ID, notFoundErr, errMetaNotFound)
   273  		}
   274  		return nil, fmt.Errorf("find backup %s: %w", destPath, err)
   275  	}
   276  	if meta.ID != req.ID {
   277  		return nil, fmt.Errorf("wrong backup file: expected %q got %q", req.ID, meta.ID)
   278  	}
   279  	if meta.Status != backup.Success {
   280  		return nil, fmt.Errorf("invalid backup %s status: %s", destPath, meta.Status)
   281  	}
   282  	if err := meta.Validate(); err != nil {
   283  		return nil, fmt.Errorf("corrupted backup file: %w", err)
   284  	}
   285  	if v := meta.Version; v > Version {
   286  		return nil, fmt.Errorf("%s: %s > %s", errMsgHigherVersion, v, Version)
   287  	}
   288  	cs := meta.Classes()
   289  	if len(req.Include) > 0 {
   290  		if first := meta.AllExist(req.Include); first != "" {
   291  			err = fmt.Errorf("class %s doesn't exist in the backup, but does have %v: ", first, cs)
   292  			return nil, err
   293  		}
   294  		meta.Include(req.Include)
   295  	} else {
   296  		meta.Exclude(req.Exclude)
   297  	}
   298  	if meta.RemoveEmpty().Count() == 0 {
   299  		return nil, fmt.Errorf("nothing left to restore: please choose from : %v", cs)
   300  	}
   301  	if len(req.NodeMapping) > 0 {
   302  		meta.NodeMapping = req.NodeMapping
   303  		meta.ApplyNodeMapping()
   304  	}
   305  	return meta, nil
   306  }
   307  
   308  func logOperation(logger logrus.FieldLogger, name, id, backend string, begin time.Time, err error) {
   309  	le := logger.WithField("action", name).
   310  		WithField("backup_id", id).WithField("backend", backend).
   311  		WithField("took", time.Since(begin))
   312  	if err != nil {
   313  		le.Error(err)
   314  	} else {
   315  		le.Info()
   316  	}
   317  }
   318  
   319  // findDuplicate returns first duplicate if it is found, and "" otherwise
   320  func findDuplicate(xs []string) string {
   321  	m := make(map[string]struct{}, len(xs))
   322  	for _, x := range xs {
   323  		if _, ok := m[x]; ok {
   324  			return x
   325  		}
   326  		m[x] = struct{}{}
   327  	}
   328  	return ""
   329  }