github.com/weaviate/weaviate@v1.24.6/usecases/backup/restorer_test.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  	"encoding/json"
    17  	"errors"
    18  	"strings"
    19  	"testing"
    20  	"time"
    21  
    22  	"github.com/stretchr/testify/assert"
    23  	"github.com/stretchr/testify/mock"
    24  	"github.com/weaviate/weaviate/entities/backup"
    25  	"github.com/weaviate/weaviate/entities/models"
    26  	"github.com/weaviate/weaviate/usecases/sharding"
    27  )
    28  
    29  // ErrAny represent a random error
    30  var (
    31  	ErrAny = errors.New("any error")
    32  	any    = mock.Anything
    33  )
    34  
    35  func (r *restorer) Restore(ctx context.Context,
    36  	req *Request,
    37  	desc *backup.BackupDescriptor,
    38  	store nodeStore,
    39  ) (*models.BackupRestoreResponse, error) {
    40  	status := string(backup.Started)
    41  	returnData := &models.BackupRestoreResponse{
    42  		Classes: req.Classes,
    43  		ID:      req.ID,
    44  		Backend: req.Backend,
    45  		Status:  &status,
    46  		Path:    store.HomeDir(),
    47  	}
    48  	if _, err := r.restore(ctx, req, desc, store); err != nil {
    49  		return nil, err
    50  	}
    51  	return returnData, nil
    52  }
    53  
    54  func (r *restorer) waitForCompletion(backend, id string, n, ms int) Status {
    55  	for i := 0; i < n; i++ {
    56  		delay := time.Millisecond * time.Duration(ms)
    57  		time.Sleep(delay)
    58  		status, err := r.status(backend, id)
    59  		if err != nil {
    60  			continue
    61  		}
    62  		if status.Status == backup.Success || status.Status == backup.Failed {
    63  			return status
    64  		}
    65  	}
    66  	return Status{}
    67  }
    68  
    69  func TestRestoreStatus(t *testing.T) {
    70  	t.Parallel()
    71  	var (
    72  		backendType = "s3"
    73  		id          = "1234"
    74  		m           = createManager(nil, nil, nil, nil)
    75  		starTime    = time.Now().UTC()
    76  		nodeHome    = id + "/" + nodeName
    77  		path        = "bucket/backups/" + nodeHome
    78  	)
    79  	// initial state
    80  	_, err := m.restorer.status(backendType, id)
    81  	if err == nil || !strings.Contains(err.Error(), "not found") {
    82  		t.Errorf("must return an error if backup doesn't exist")
    83  	}
    84  	// active state
    85  	m.restorer.lastOp.reqStat = reqStat{
    86  		Starttime: starTime,
    87  		ID:        id,
    88  		Status:    backup.Transferring,
    89  		Path:      path,
    90  	}
    91  	st, err := m.restorer.status(backendType, id)
    92  	if err != nil {
    93  		t.Errorf("get active status: %v", err)
    94  	}
    95  	expected := Status{Path: path, StartedAt: starTime, Status: backup.Transferring}
    96  	if expected != st {
    97  		t.Errorf("get active status: got=%v want=%v", st, expected)
    98  	}
    99  	// cached status
   100  	m.restorer.lastOp.reset()
   101  	st.CompletedAt = starTime
   102  	m.restorer.restoreStatusMap.Store("s3/"+id, st)
   103  	st, err = m.restorer.status(backendType, id)
   104  	if err != nil {
   105  		t.Errorf("fetch status from map: %v", err)
   106  	}
   107  	expected.CompletedAt = starTime
   108  	if expected != st {
   109  		t.Errorf("fetch status from map got=%v want=%v", st, expected)
   110  	}
   111  }
   112  
   113  func TestRestoreRequestValidation(t *testing.T) {
   114  	var (
   115  		cls         = "MyClass"
   116  		backendName = "s3"
   117  		rawbytes    = []byte("hello")
   118  		id          = "1234"
   119  		timept      = time.Now().UTC()
   120  		ctx         = context.Background()
   121  		nodeHome    = id + "/" + nodeName
   122  		path        = "bucket/backups/" + nodeHome
   123  		req         = &BackupRequest{
   124  			Backend: backendName,
   125  			ID:      id,
   126  			Include: []string{cls},
   127  			Exclude: []string{},
   128  		}
   129  	)
   130  	meta := backup.BackupDescriptor{
   131  		ID:            id,
   132  		StartedAt:     timept,
   133  		Version:       "1",
   134  		ServerVersion: "1",
   135  		Status:        string(backup.Success),
   136  		Classes: []backup.ClassDescriptor{{
   137  			Name: cls, Schema: rawbytes, ShardingState: rawbytes,
   138  		}},
   139  	}
   140  
   141  	t.Run("BackendFailure", func(t *testing.T) { //  backend provider fails
   142  		backend := newFakeBackend()
   143  		m2 := createManager(nil, nil, backend, ErrAny)
   144  		_, err := m2.Restore(ctx, nil, &BackupRequest{
   145  			Backend: backendName,
   146  			ID:      id,
   147  			Include: []string{cls},
   148  			Exclude: []string{},
   149  		})
   150  		assert.NotNil(t, err)
   151  		assert.Contains(t, err.Error(), backendName)
   152  	})
   153  
   154  	t.Run("GetMetadataFile", func(t *testing.T) {
   155  		backend := newFakeBackend()
   156  		backend.On("GetObject", ctx, nodeHome, BackupFile).Return(nil, ErrAny)
   157  		backend.On("GetObject", ctx, req.ID, BackupFile).Return(nil, ErrAny)
   158  
   159  		backend.On("HomeDir", mock.Anything).Return(path)
   160  		m2 := createManager(nil, nil, backend, nil)
   161  		_, err := m2.Restore(ctx, nil, req)
   162  		if err == nil || !strings.Contains(err.Error(), "find") {
   163  			t.Errorf("must return an error if it fails to get meta data: %v", err)
   164  		}
   165  		// meta data not found
   166  		backend = newFakeBackend()
   167  		backend.On("HomeDir", mock.Anything).Return(path)
   168  		backend.On("GetObject", ctx, nodeHome, BackupFile).Return(nil, backup.ErrNotFound{})
   169  		backend.On("GetObject", ctx, req.ID, BackupFile).Return(nil, backup.ErrNotFound{})
   170  
   171  		m3 := createManager(nil, nil, backend, nil)
   172  
   173  		_, err = m3.Restore(ctx, nil, req)
   174  		if _, ok := err.(backup.ErrNotFound); !ok {
   175  			t.Errorf("must return an error if meta data doesn't exist: %v", err)
   176  		}
   177  	})
   178  
   179  	t.Run("FailedBackup", func(t *testing.T) {
   180  		backend := newFakeBackend()
   181  		bytes := marshalMeta(backup.BackupDescriptor{ID: id, Status: string(backup.Failed)})
   182  		backend.On("GetObject", ctx, nodeHome, BackupFile).Return(bytes, nil)
   183  		backend.On("HomeDir", mock.Anything).Return(path)
   184  		m2 := createManager(nil, nil, backend, nil)
   185  		_, err := m2.Restore(ctx, nil, req)
   186  		assert.NotNil(t, err)
   187  		assert.Contains(t, err.Error(), backup.Failed)
   188  		assert.IsType(t, backup.ErrUnprocessable{}, err)
   189  	})
   190  
   191  	t.Run("BackupWithHigherVersion", func(t *testing.T) {
   192  		var (
   193  			backend = newFakeBackend()
   194  			meta    = backup.BackupDescriptor{
   195  				ID:            id,
   196  				StartedAt:     timept,
   197  				Version:       "3.0",
   198  				ServerVersion: "1",
   199  				Status:        string(backup.Success),
   200  				Classes: []backup.ClassDescriptor{{
   201  					Name: cls, Schema: rawbytes, ShardingState: rawbytes,
   202  				}},
   203  			}
   204  			bytes = marshalMeta(meta)
   205  		)
   206  		backend.On("GetObject", ctx, nodeHome, BackupFile).Return(bytes, nil)
   207  		backend.On("HomeDir", mock.Anything).Return(path)
   208  		m2 := createManager(nil, nil, backend, nil)
   209  		_, err := m2.Restore(ctx, nil, req)
   210  		assert.NotNil(t, err)
   211  		assert.Contains(t, err.Error(), errMsgHigherVersion)
   212  		assert.IsType(t, backup.ErrUnprocessable{}, err)
   213  	})
   214  
   215  	t.Run("FailedOldBackup", func(t *testing.T) {
   216  		backend := newFakeBackend()
   217  		bytes := marshalMeta(backup.BackupDescriptor{ID: id, Status: string(backup.Failed)})
   218  		backend.On("GetObject", ctx, nodeHome, BackupFile).Return(bytes, ErrAny)
   219  		backend.On("GetObject", ctx, id, BackupFile).Return(bytes, nil)
   220  
   221  		backend.On("HomeDir", mock.Anything).Return(path)
   222  		m2 := createManager(nil, nil, backend, nil)
   223  		_, err := m2.Restore(ctx, nil, req)
   224  		assert.NotNil(t, err)
   225  		assert.Contains(t, err.Error(), backup.Failed)
   226  		assert.IsType(t, backup.ErrUnprocessable{}, err)
   227  	})
   228  
   229  	t.Run("CorruptedBackupFile", func(t *testing.T) {
   230  		backend := newFakeBackend()
   231  		bytes := marshalMeta(backup.BackupDescriptor{ID: id, Status: string(backup.Success)})
   232  		backend.On("GetObject", ctx, nodeHome, BackupFile).Return(bytes, nil)
   233  		backend.On("HomeDir", mock.Anything).Return(path)
   234  		m2 := createManager(nil, nil, backend, nil)
   235  		_, err := m2.Restore(ctx, nil, req)
   236  		assert.NotNil(t, err)
   237  		assert.IsType(t, backup.ErrUnprocessable{}, err)
   238  		assert.Contains(t, err.Error(), "corrupted")
   239  	})
   240  
   241  	t.Run("WrongBackupFile", func(t *testing.T) {
   242  		backend := newFakeBackend()
   243  		bytes := marshalMeta(backup.BackupDescriptor{ID: "123", Status: string(backup.Success)})
   244  		backend.On("GetObject", ctx, nodeHome, BackupFile).Return(bytes, nil)
   245  		backend.On("HomeDir", mock.Anything).Return(path)
   246  		m2 := createManager(nil, nil, backend, nil)
   247  		_, err := m2.Restore(ctx, nil, req)
   248  		assert.NotNil(t, err)
   249  		assert.IsType(t, backup.ErrUnprocessable{}, err)
   250  		assert.Contains(t, err.Error(), "wrong backup file")
   251  	})
   252  
   253  	t.Run("UnknownClass", func(t *testing.T) {
   254  		backend := newFakeBackend()
   255  		bytes := marshalMeta(meta)
   256  		backend.On("GetObject", ctx, nodeHome, BackupFile).Return(bytes, nil)
   257  		backend.On("HomeDir", mock.Anything).Return(path)
   258  		m2 := createManager(nil, nil, backend, nil)
   259  		_, err := m2.Restore(ctx, nil, &BackupRequest{ID: id, Include: []string{"unknown"}})
   260  		assert.NotNil(t, err)
   261  		assert.Contains(t, err.Error(), "unknown")
   262  	})
   263  
   264  	t.Run("EmptyResultClassList", func(t *testing.T) { //  backup was successful but class list is empty
   265  		backend := newFakeBackend()
   266  		bytes := marshalMeta(meta)
   267  		backend.On("GetObject", ctx, nodeHome, BackupFile).Return(bytes, nil)
   268  		backend.On("HomeDir", mock.Anything).Return(path)
   269  		m2 := createManager(nil, nil, backend, nil)
   270  		_, err := m2.Restore(ctx, nil, &BackupRequest{ID: id, Exclude: []string{cls}})
   271  		assert.NotNil(t, err)
   272  		assert.Contains(t, err.Error(), "empty")
   273  	})
   274  }
   275  
   276  func TestManagerRestoreBackup(t *testing.T) {
   277  	var (
   278  		cls         = "Article"
   279  		rawbytes    = []byte("hello")
   280  		backendName = "gcs"
   281  		backupID    = "1"
   282  		timept      = time.Now().UTC()
   283  		ctx         = context.Background()
   284  		nodeHome    = backupID + "/" + nodeName
   285  		path        = "bucket/backups/" + nodeHome
   286  	)
   287  
   288  	rawShardingStateBytes, _ := json.Marshal(&sharding.State{
   289  		IndexID: cls,
   290  		Physical: map[string]sharding.Physical{"cT9eTErXgmTX": {
   291  			Name:           "cT9eTErXgmTX",
   292  			BelongsToNodes: []string{nodeName},
   293  		}},
   294  	})
   295  	rawClassBytes, _ := json.Marshal(&models.Class{
   296  		Class: cls,
   297  	})
   298  	meta2 := backup.BackupDescriptor{
   299  		ID:            backupID,
   300  		StartedAt:     timept,
   301  		Version:       Version,
   302  		ServerVersion: "1",
   303  		Status:        string(backup.Success),
   304  
   305  		Classes: []backup.ClassDescriptor{{
   306  			Name:          cls,
   307  			Schema:        rawClassBytes,
   308  			ShardingState: rawShardingStateBytes,
   309  			Chunks:        map[int32][]string{1: {"Shard1"}},
   310  			Shards: []*backup.ShardDescriptor{
   311  				{
   312  					Name: "Shard1", Node: "Node-1",
   313  					Chunk: 1,
   314  				},
   315  			},
   316  		}},
   317  	}
   318  	meta1 := backup.BackupDescriptor{
   319  		ID:            backupID,
   320  		StartedAt:     timept,
   321  		Version:       version1,
   322  		ServerVersion: "1",
   323  		Status:        string(backup.Success),
   324  
   325  		Classes: []backup.ClassDescriptor{{
   326  			Name:          cls,
   327  			Schema:        rawClassBytes,
   328  			ShardingState: rawShardingStateBytes,
   329  			Chunks:        map[int32][]string{1: {"Shard1"}},
   330  			Shards: []*backup.ShardDescriptor{
   331  				{
   332  					Name: "Shard1", Node: "Node-1",
   333  					Files:                 []string{"dir1/file1", "dir2/file2"},
   334  					DocIDCounterPath:      "counter.txt",
   335  					ShardVersionPath:      "version.txt",
   336  					PropLengthTrackerPath: "prop.txt",
   337  					DocIDCounter:          rawbytes,
   338  					Version:               rawbytes,
   339  					PropLengthTracker:     rawbytes,
   340  					Chunk:                 1,
   341  				},
   342  			},
   343  		}},
   344  	}
   345  
   346  	t.Run("ClassAlreadyExists", func(t *testing.T) {
   347  		var (
   348  			req1 = BackupRequest{
   349  				ID:      backupID,
   350  				Include: []string{cls},
   351  				Backend: backendName,
   352  			}
   353  			backend  = newFakeBackend()
   354  			sourcer  = &fakeSourcer{}
   355  			dataPath = t.TempDir()
   356  		)
   357  		sourcer.On("ClassExists", cls).Return(true)
   358  		bytes := marshalMeta(meta2)
   359  		backend.On("GetObject", ctx, nodeHome, BackupFile).Return(bytes, nil)
   360  		backend.On("HomeDir", mock.Anything).Return(path)
   361  		backend.On("SourceDataPath").Return(dataPath)
   362  		backend.On("Read", any, nodeHome, mock.Anything, mock.Anything).Return(any, nil)
   363  		m := createManager(sourcer, nil, backend, nil)
   364  		resp1, err := m.Restore(ctx, nil, &req1)
   365  		assert.Nil(t, err)
   366  		status1 := string(backup.Started)
   367  		want1 := &models.BackupRestoreResponse{
   368  			Backend: backendName,
   369  			Classes: req1.Include,
   370  			ID:      backupID,
   371  			Status:  &status1,
   372  			Path:    path,
   373  		}
   374  		assert.Equal(t, resp1, want1)
   375  		lastStatus := m.restorer.waitForCompletion(req1.Backend, req1.ID, 10, 50)
   376  		assert.Nil(t, err)
   377  		assert.Equal(t, backup.Failed, lastStatus.Status)
   378  	})
   379  
   380  	t.Run("AnotherBackupIsInProgress", func(t *testing.T) {
   381  		req1 := BackupRequest{
   382  			ID:      backupID,
   383  			Include: []string{cls},
   384  			Backend: backendName,
   385  		}
   386  		backend := newFakeBackend()
   387  		sourcer := &fakeSourcer{}
   388  		sourcer.On("ClassExists", cls).Return(false)
   389  		bytes := marshalMeta(meta2)
   390  		backend.On("GetObject", ctx, nodeHome, BackupFile).Return(bytes, nil)
   391  		backend.On("HomeDir", mock.Anything).Return(path)
   392  		// simulate work by delaying return of SourceDataPath()
   393  		backend.On("SourceDataPath").Return(t.TempDir()).After(time.Hour)
   394  		m2 := createManager(sourcer, nil, backend, nil)
   395  		_, err := m2.Restore(ctx, nil, &BackupRequest{ID: backupID})
   396  		assert.Nil(t, err)
   397  		m := createManager(sourcer, nil, backend, nil)
   398  		resp1, err := m.Restore(ctx, nil, &req1)
   399  		assert.Nil(t, err)
   400  		status1 := string(backup.Started)
   401  		want1 := &models.BackupRestoreResponse{
   402  			Backend: backendName,
   403  			Classes: req1.Include,
   404  			ID:      backupID,
   405  			Status:  &status1,
   406  			Path:    path,
   407  		}
   408  		assert.Equal(t, resp1, want1)
   409  		// another caller
   410  		resp2, err := m.Restore(ctx, nil, &req1)
   411  		assert.NotNil(t, err)
   412  		assert.Contains(t, err.Error(), "already in progress")
   413  		assert.IsType(t, backup.ErrUnprocessable{}, err)
   414  		assert.Nil(t, resp2)
   415  	})
   416  
   417  	t.Run("Success", func(t *testing.T) {
   418  		var (
   419  			req1 = BackupRequest{
   420  				ID:      backupID,
   421  				Include: []string{cls},
   422  				Backend: backendName,
   423  			}
   424  			backend  = newFakeBackend()
   425  			sourcer  = &fakeSourcer{}
   426  			dataPath = t.TempDir()
   427  		)
   428  		sourcer.On("ClassExists", cls).Return(false)
   429  		bytes := marshalMeta(meta2)
   430  		backend.On("GetObject", ctx, nodeHome, BackupFile).Return(bytes, nil)
   431  		backend.On("HomeDir", mock.Anything).Return(path)
   432  		backend.On("SourceDataPath").Return(dataPath)
   433  		backend.On("Read", any, nodeHome, mock.Anything, mock.Anything).Return(any, nil)
   434  		m := createManager(sourcer, nil, backend, nil)
   435  		resp1, err := m.Restore(ctx, nil, &req1)
   436  		assert.Nil(t, err)
   437  		status1 := string(backup.Started)
   438  		want1 := &models.BackupRestoreResponse{
   439  			Backend: backendName,
   440  			Classes: req1.Include,
   441  			ID:      backupID,
   442  			Status:  &status1,
   443  			Path:    path,
   444  		}
   445  		assert.Equal(t, resp1, want1)
   446  		assert.Nil(t, err)
   447  		lastStatus := m.restorer.waitForCompletion(req1.Backend, req1.ID, 12, 50)
   448  		assert.Equal(t, backup.Success, lastStatus.Status)
   449  	})
   450  
   451  	readSourceFile := func(t *testing.T, newVerion bool) {
   452  		req1 := BackupRequest{
   453  			ID:      backupID,
   454  			Include: []string{cls},
   455  			Backend: backendName,
   456  		}
   457  		backend := newFakeBackend()
   458  		sourcer := &fakeSourcer{}
   459  		sourcer.On("ClassExists", cls).Return(false)
   460  		var bytes []byte
   461  		if newVerion {
   462  			bytes = marshalMeta(meta2)
   463  			backend.On("Read", any, nodeHome, mock.Anything, mock.Anything).Return(any, ErrAny)
   464  		} else {
   465  			bytes = marshalMeta(meta1)
   466  			backend.On("WriteToFile", any, nodeHome, mock.Anything, mock.Anything).Return(ErrAny)
   467  		}
   468  
   469  		backend.On("GetObject", ctx, nodeHome, BackupFile).Return(bytes, nil)
   470  		backend.On("HomeDir", mock.Anything).Return(path)
   471  		backend.On("SourceDataPath").Return(t.TempDir())
   472  		m := createManager(sourcer, nil, backend, nil)
   473  		resp1, err := m.Restore(ctx, nil, &req1)
   474  		assert.Nil(t, err)
   475  		status1 := string(backup.Started)
   476  		want1 := &models.BackupRestoreResponse{
   477  			Backend: backendName,
   478  			Classes: req1.Include,
   479  			ID:      backupID,
   480  			Status:  &status1,
   481  			Path:    path,
   482  		}
   483  		assert.Equal(t, resp1, want1)
   484  		lastStatus := m.restorer.waitForCompletion(req1.Backend, req1.ID, 10, 50)
   485  
   486  		assert.Nil(t, err)
   487  		assert.Equal(t, backup.Failed, lastStatus.Status)
   488  		//
   489  	}
   490  	t.Run("ReadSourceFile", func(t *testing.T) {
   491  		readSourceFile(t, true)
   492  	})
   493  	t.Run("ReadSourceFileV1", func(t *testing.T) {
   494  		readSourceFile(t, false)
   495  	})
   496  
   497  	t.Run("RestoreClassFails", func(t *testing.T) {
   498  		req1 := BackupRequest{
   499  			ID:      backupID,
   500  			Include: []string{cls},
   501  			Backend: backendName,
   502  		}
   503  		backend := newFakeBackend()
   504  		sourcer := &fakeSourcer{}
   505  		schema := fakeSchemaManger{errRestoreClass: ErrAny, nodeName: nodeName}
   506  		sourcer.On("ClassExists", cls).Return(false)
   507  		bytes := marshalMeta(meta2)
   508  		backend.On("GetObject", ctx, nodeHome, BackupFile).Return(bytes, nil)
   509  		backend.On("HomeDir", mock.Anything).Return(path)
   510  		backend.On("SourceDataPath").Return(t.TempDir())
   511  		backend.On("Read", any, nodeHome, mock.Anything, mock.Anything).Return(any, nil)
   512  		m := createManager(sourcer, &schema, backend, nil)
   513  		resp1, err := m.Restore(ctx, nil, &req1)
   514  		assert.Nil(t, err)
   515  		status1 := string(backup.Started)
   516  		want1 := &models.BackupRestoreResponse{
   517  			Backend: backendName,
   518  			Classes: req1.Include,
   519  			ID:      backupID,
   520  			Status:  &status1,
   521  			Path:    path,
   522  		}
   523  		assert.Equal(t, resp1, want1)
   524  		lastStatus := m.restorer.waitForCompletion(req1.Backend, req1.ID, 10, 50)
   525  
   526  		assert.Nil(t, err)
   527  		assert.Equal(t, lastStatus.Status, backup.Failed)
   528  	})
   529  }
   530  
   531  func TestManagerCoordinatedRestore(t *testing.T) {
   532  	var (
   533  		backendName = "gcs"
   534  		rawbytes    = []byte("hello")
   535  		timept      = time.Now().UTC()
   536  		cls         = "Class-A"
   537  		backupID    = "2"
   538  		ctx         = context.Background()
   539  		nodeHome    = backupID + "/" + nodeName
   540  		path        = "bucket/backups/" + nodeHome
   541  		req         = Request{
   542  			Method:   OpRestore,
   543  			ID:       backupID,
   544  			Classes:  []string{cls},
   545  			Backend:  backendName,
   546  			Duration: time.Millisecond * 20,
   547  		}
   548  	)
   549  	rawShardingStateBytes, _ := json.Marshal(&sharding.State{
   550  		IndexID: cls,
   551  		Physical: map[string]sharding.Physical{"cT9eTErXgmTX": {
   552  			Name:           "cT9eTErXgmTX",
   553  			BelongsToNodes: []string{nodeName},
   554  		}},
   555  	})
   556  	rawClassBytes, _ := json.Marshal(&models.Class{
   557  		Class: cls,
   558  	})
   559  
   560  	metadata := backup.BackupDescriptor{
   561  		ID:            backupID,
   562  		StartedAt:     timept,
   563  		Version:       "1",
   564  		ServerVersion: "1",
   565  		Status:        string(backup.Success),
   566  		Classes: []backup.ClassDescriptor{{
   567  			Name:          cls,
   568  			Schema:        rawClassBytes,
   569  			ShardingState: rawShardingStateBytes,
   570  			Shards: []*backup.ShardDescriptor{
   571  				{
   572  					Name: "Shard1", Node: "Node-1",
   573  					Files:                 []string{"dir1/file1", "dir2/file2"},
   574  					DocIDCounterPath:      "counter.txt",
   575  					ShardVersionPath:      "version.txt",
   576  					PropLengthTrackerPath: "prop.txt",
   577  					DocIDCounter:          rawbytes,
   578  					Version:               rawbytes,
   579  					PropLengthTracker:     rawbytes,
   580  				},
   581  			},
   582  		}},
   583  	}
   584  
   585  	t.Run("GetMetadataFile", func(t *testing.T) {
   586  		backend := newFakeBackend()
   587  		backend.On("GetObject", ctx, nodeHome, BackupFile).Return(nil, backup.ErrNotFound{})
   588  		backend.On("GetObject", ctx, backupID, BackupFile).Return(nil, backup.ErrNotFound{})
   589  		backend.On("HomeDir", mock.Anything).Return(path)
   590  		bm := createManager(nil, nil, backend, nil)
   591  		resp := bm.OnCanCommit(ctx, &req)
   592  		assert.Contains(t, resp.Err, errMetaNotFound.Error())
   593  		assert.Equal(t, resp.Timeout, time.Duration(0))
   594  	})
   595  
   596  	t.Run("AnotherBackupIsInProgress", func(t *testing.T) {
   597  		backend := newFakeBackend()
   598  		sourcer := &fakeSourcer{}
   599  		sourcer.On("ClassExists", cls).Return(false)
   600  		bytes := marshalMeta(metadata)
   601  		backend.On("GetObject", ctx, nodeHome, BackupFile).Return(bytes, nil)
   602  		backend.On("HomeDir", mock.Anything).Return(path)
   603  		// simulate work by delaying return of SourceDataPath()
   604  		backend.On("SourceDataPath").Return(t.TempDir()).After(time.Minute * 2)
   605  		m := createManager(sourcer, nil, backend, nil)
   606  		resp := m.OnCanCommit(ctx, &req)
   607  		assert.Equal(t, resp.Err, "")
   608  		resp = m.OnCanCommit(ctx, &req)
   609  		assert.Contains(t, resp.Err, "already in progress")
   610  		assert.Equal(t, time.Duration(0), resp.Timeout)
   611  	})
   612  
   613  	t.Run("Success", func(t *testing.T) {
   614  		req := req
   615  		req.Duration = time.Hour
   616  		backend := newFakeBackend()
   617  		sourcer := &fakeSourcer{}
   618  		sourcer.On("ClassExists", cls).Return(false)
   619  		bytes := marshalMeta(metadata)
   620  		backend.On("GetObject", ctx, nodeHome, BackupFile).Return(bytes, nil)
   621  		backend.On("HomeDir", mock.Anything).Return(path)
   622  		backend.On("SourceDataPath").Return(t.TempDir())
   623  		backend.On("WriteToFile", any, nodeHome, mock.Anything, mock.Anything).Return(nil)
   624  		m := createManager(sourcer, nil, backend, nil)
   625  		resp1 := m.OnCanCommit(ctx, &req)
   626  		want1 := &CanCommitResponse{
   627  			Method:  OpRestore,
   628  			ID:      req.ID,
   629  			Timeout: _TimeoutShardCommit,
   630  		}
   631  		assert.Equal(t, want1, resp1)
   632  		err := m.OnCommit(ctx, &StatusRequest{Method: OpRestore, ID: req.ID, Backend: req.Backend})
   633  		assert.Nil(t, err)
   634  		lastStatus := m.restorer.waitForCompletion(req.Backend, req.ID, 12, 50)
   635  		assert.Nil(t, err)
   636  		assert.Equal(t, lastStatus.Status, backup.Success)
   637  	})
   638  
   639  	t.Run("Abort", func(t *testing.T) {
   640  		req := req
   641  		req.Duration = time.Hour
   642  		backend := newFakeBackend()
   643  		sourcer := &fakeSourcer{}
   644  		sourcer.On("ClassExists", cls).Return(false)
   645  		bytes := marshalMeta(metadata)
   646  		backend.On("GetObject", ctx, nodeHome, BackupFile).Return(bytes, nil)
   647  		backend.On("HomeDir", mock.Anything).Return(path)
   648  		backend.On("SourceDataPath").Return(t.TempDir())
   649  		backend.On("WriteToFile", any, nodeHome, mock.Anything, mock.Anything).Return(nil)
   650  		m := createManager(sourcer, nil, backend, nil)
   651  		resp1 := m.OnCanCommit(ctx, &req)
   652  		want1 := &CanCommitResponse{
   653  			Method:  OpRestore,
   654  			ID:      req.ID,
   655  			Timeout: _TimeoutShardCommit,
   656  		}
   657  		assert.Equal(t, want1, resp1)
   658  		err := m.OnAbort(ctx, &AbortRequest{Method: OpRestore, ID: req.ID})
   659  		assert.Nil(t, err)
   660  		lastStatus := m.restorer.waitForCompletion(req.Backend, req.ID, 10, 50)
   661  
   662  		assert.Nil(t, err)
   663  		assert.Equal(t, lastStatus.Status, backup.Failed)
   664  	})
   665  }
   666  
   667  func TestRestoreOnStatus(t *testing.T) {
   668  	t.Parallel()
   669  	var (
   670  		backendType = "s3"
   671  		id          = "1234"
   672  		m           = createManager(nil, nil, nil, nil)
   673  		ctx         = context.Background()
   674  		starTime    = time.Now().UTC()
   675  		nodeHome    = id + "/" + nodeName
   676  		path        = "bucket/backups/" + nodeHome
   677  		req         = StatusRequest{
   678  			Method:  OpRestore,
   679  			ID:      id,
   680  			Backend: backendType,
   681  		}
   682  	)
   683  	// initial state
   684  	got := m.OnStatus(ctx, &req)
   685  	if !strings.Contains(got.Err, "not found") {
   686  		t.Errorf("must return an error if backup doesn't exist")
   687  	}
   688  	// active state
   689  	m.restorer.lastOp.reqStat = reqStat{
   690  		Starttime: starTime,
   691  		ID:        id,
   692  		Status:    backup.Transferring,
   693  		Path:      path,
   694  	}
   695  	got = m.OnStatus(ctx, &req)
   696  	expected := StatusResponse{Method: OpRestore, ID: req.ID, Status: backup.Transferring}
   697  	if expected != *got {
   698  		t.Errorf("get active status: got=%v want=%v", got, expected)
   699  	}
   700  	// cached status
   701  	m.restorer.lastOp.reset()
   702  	st := Status{Path: path, StartedAt: starTime, Status: backup.Transferring, CompletedAt: starTime}
   703  	m.restorer.restoreStatusMap.Store("s3/"+id, st)
   704  	got = m.OnStatus(ctx, &req)
   705  	if expected != *got {
   706  		t.Errorf("fetch status from map got=%v want=%v", st, expected)
   707  	}
   708  }
   709  
   710  func marshalMeta(m backup.BackupDescriptor) []byte {
   711  	bytes, _ := json.MarshalIndent(m, "", "")
   712  	return bytes
   713  }