github.com/weaviate/weaviate@v1.24.6/usecases/backup/handler.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  	"fmt"
    17  	"regexp"
    18  	"time"
    19  
    20  	"github.com/sirupsen/logrus"
    21  	"github.com/weaviate/weaviate/entities/backup"
    22  	"github.com/weaviate/weaviate/entities/models"
    23  	"github.com/weaviate/weaviate/entities/modulecapabilities"
    24  )
    25  
    26  // Version of backup structure
    27  const (
    28  	// Version > version1 support compression
    29  	Version = "2.0"
    30  	// version1 store plain files without compression
    31  	version1 = "1.0"
    32  )
    33  
    34  // TODO error handling need to be implemented properly.
    35  // Current error handling is not idiomatic and relays on string comparisons which makes testing very brittle.
    36  
    37  var regExpID = regexp.MustCompile("^[a-z0-9_-]+$")
    38  
    39  type BackupBackendProvider interface {
    40  	BackupBackend(backend string) (modulecapabilities.BackupBackend, error)
    41  }
    42  
    43  type authorizer interface {
    44  	Authorize(principal *models.Principal, verb, resource string) error
    45  }
    46  
    47  type schemaManger interface {
    48  	RestoreClass(ctx context.Context, d *backup.ClassDescriptor, nodeMapping map[string]string) error
    49  	NodeName() string
    50  }
    51  
    52  type nodeResolver interface {
    53  	NodeHostname(nodeName string) (string, bool)
    54  	AllNames() []string
    55  	NodeCount() int
    56  }
    57  
    58  type Status struct {
    59  	Path        string
    60  	StartedAt   time.Time
    61  	CompletedAt time.Time
    62  	Status      backup.Status
    63  	Err         string
    64  }
    65  
    66  type Handler struct {
    67  	node string
    68  	// deps
    69  	logger     logrus.FieldLogger
    70  	authorizer authorizer
    71  	backupper  *backupper
    72  	restorer   *restorer
    73  	backends   BackupBackendProvider
    74  }
    75  
    76  func NewHandler(
    77  	logger logrus.FieldLogger,
    78  	authorizer authorizer,
    79  	schema schemaManger,
    80  	sourcer Sourcer,
    81  	backends BackupBackendProvider,
    82  ) *Handler {
    83  	node := schema.NodeName()
    84  	m := &Handler{
    85  		node:       node,
    86  		logger:     logger,
    87  		authorizer: authorizer,
    88  		backends:   backends,
    89  		backupper: newBackupper(node, logger,
    90  			sourcer,
    91  			backends),
    92  		restorer: newRestorer(node, logger,
    93  			sourcer,
    94  			backends,
    95  			schema,
    96  		),
    97  	}
    98  	return m
    99  }
   100  
   101  // Compression is the compression configuration.
   102  type Compression struct {
   103  	// Level is one of DefaultCompression, BestSpeed, BestCompression
   104  	Level CompressionLevel
   105  
   106  	// ChunkSize represents the desired size for chunks between 1 - 512  MB
   107  	// However, during compression, the chunk size might
   108  	// slightly deviate from this value, being either slightly
   109  	// below or above the specified size
   110  	ChunkSize int
   111  
   112  	// CPUPercentage desired CPU core utilization (1%-80%), default: 50%
   113  	CPUPercentage int
   114  }
   115  
   116  // BackupRequest a transition request from API to Backend.
   117  type BackupRequest struct {
   118  	// Compression is the compression configuration.
   119  	Compression
   120  
   121  	// ID is the backup ID
   122  	ID string
   123  	// Backend specify on which backend to store backups (gcs, s3, ..)
   124  	Backend string
   125  
   126  	// Include is list of class which need to be backed up
   127  	// The same class cannot appear in both Include and Exclude in the same request
   128  	Include []string
   129  	// Exclude means include all classes but those specified in Exclude
   130  	// The same class cannot appear in both Include and Exclude in the same request
   131  	Exclude []string
   132  
   133  	// NodeMapping is a map of node name replacement where key is the old name and value is the new name
   134  	// No effect if the map is empty
   135  	NodeMapping map[string]string
   136  }
   137  
   138  // OnCanCommit will be triggered when coordinator asks the node to participate
   139  // in a distributed backup operation
   140  func (m *Handler) OnCanCommit(ctx context.Context, req *Request) *CanCommitResponse {
   141  	ret := &CanCommitResponse{Method: req.Method, ID: req.ID}
   142  
   143  	nodeName := m.node
   144  	// If we are doing a restore and have a nodeMapping specified, ensure we use the "old" node name from the backup to retrieve/store the
   145  	// backup information.
   146  	if req.Method == OpRestore {
   147  		for oldNodeName, newNodeName := range req.NodeMapping {
   148  			if nodeName == newNodeName {
   149  				nodeName = oldNodeName
   150  				break
   151  			}
   152  		}
   153  	}
   154  	store, err := nodeBackend(nodeName, m.backends, req.Backend, req.ID)
   155  	if err != nil {
   156  		ret.Err = fmt.Sprintf("no backup backend %q, did you enable the right module?", req.Backend)
   157  		return ret
   158  	}
   159  
   160  	switch req.Method {
   161  	case OpCreate:
   162  		if err := m.backupper.sourcer.Backupable(ctx, req.Classes); err != nil {
   163  			ret.Err = err.Error()
   164  			return ret
   165  		}
   166  		if err = store.Initialize(ctx); err != nil {
   167  			ret.Err = fmt.Sprintf("init uploader: %v", err)
   168  			return ret
   169  		}
   170  		res, err := m.backupper.backup(ctx, store, req)
   171  		if err != nil {
   172  			ret.Err = err.Error()
   173  			return ret
   174  		}
   175  		ret.Timeout = res.Timeout
   176  	case OpRestore:
   177  		meta, _, err := m.restorer.validate(ctx, &store, req)
   178  		if err != nil {
   179  			ret.Err = err.Error()
   180  			return ret
   181  		}
   182  		res, err := m.restorer.restore(ctx, req, meta, store)
   183  		if err != nil {
   184  			ret.Err = err.Error()
   185  			return ret
   186  		}
   187  		ret.Timeout = res.Timeout
   188  	default:
   189  		ret.Err = fmt.Sprintf("unknown backup operation: %s", req.Method)
   190  		return ret
   191  	}
   192  
   193  	return ret
   194  }
   195  
   196  // OnCommit will be triggered when the coordinator confirms the execution of a previous operation
   197  func (m *Handler) OnCommit(ctx context.Context, req *StatusRequest) (err error) {
   198  	switch req.Method {
   199  	case OpCreate:
   200  		return m.backupper.OnCommit(ctx, req)
   201  	case OpRestore:
   202  		return m.restorer.OnCommit(ctx, req)
   203  	default:
   204  		return fmt.Errorf("%w: %s", errUnknownOp, req.Method)
   205  	}
   206  }
   207  
   208  // OnAbort will be triggered when the coordinator abort the execution of a previous operation
   209  func (m *Handler) OnAbort(ctx context.Context, req *AbortRequest) error {
   210  	switch req.Method {
   211  	case OpCreate:
   212  		return m.backupper.OnAbort(ctx, req)
   213  	case OpRestore:
   214  		return m.restorer.OnAbort(ctx, req)
   215  	default:
   216  		return fmt.Errorf("%w: %s", errUnknownOp, req.Method)
   217  
   218  	}
   219  }
   220  
   221  func (m *Handler) OnStatus(ctx context.Context, req *StatusRequest) *StatusResponse {
   222  	ret := StatusResponse{
   223  		Method: req.Method,
   224  		ID:     req.ID,
   225  	}
   226  	switch req.Method {
   227  	case OpCreate:
   228  		st, err := m.backupper.OnStatus(ctx, req)
   229  		ret.Status = st.Status
   230  		if err != nil {
   231  			ret.Status = backup.Failed
   232  			ret.Err = err.Error()
   233  		}
   234  	case OpRestore:
   235  		st, err := m.restorer.status(req.Backend, req.ID)
   236  		ret.Status = st.Status
   237  		ret.Err = st.Err
   238  		if err != nil {
   239  			ret.Status = backup.Failed
   240  			ret.Err = err.Error()
   241  		} else if st.Err != "" {
   242  			ret.Err = st.Err
   243  		}
   244  	default:
   245  		ret.Status = backup.Failed
   246  		ret.Err = fmt.Sprintf("%v: %s", errUnknownOp, req.Method)
   247  	}
   248  
   249  	return &ret
   250  }
   251  
   252  func validateID(backupID string) error {
   253  	if !regExpID.MatchString(backupID) {
   254  		return fmt.Errorf("invalid backup id: allowed characters are lowercase, 0-9, _, -")
   255  	}
   256  	return nil
   257  }
   258  
   259  func nodeBackend(node string, provider BackupBackendProvider, backend, id string) (nodeStore, error) {
   260  	caps, err := provider.BackupBackend(backend)
   261  	if err != nil {
   262  		return nodeStore{}, err
   263  	}
   264  	return nodeStore{objStore{b: caps, BasePath: fmt.Sprintf("%s/%s", id, node)}}, nil
   265  }
   266  
   267  // basePath of the backup
   268  func basePath(backendType, backupID string) string {
   269  	return fmt.Sprintf("%s/%s", backendType, backupID)
   270  }
   271  
   272  func filterClasses(classes, excludes []string) []string {
   273  	if len(excludes) == 0 {
   274  		return classes
   275  	}
   276  	m := make(map[string]struct{}, len(classes))
   277  	for _, c := range classes {
   278  		m[c] = struct{}{}
   279  	}
   280  	for _, x := range excludes {
   281  		delete(m, x)
   282  	}
   283  	if len(classes) != len(m) {
   284  		classes = classes[:len(m)]
   285  		i := 0
   286  		for k := range m {
   287  			classes[i] = k
   288  			i++
   289  		}
   290  	}
   291  
   292  	return classes
   293  }