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, ¬FoundErr) { 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 }