github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/state/binarystorage/binarystorage_test.go (about)

     1  // Copyright 2014 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package binarystorage_test
     5  
     6  import (
     7  	"bytes"
     8  	"fmt"
     9  	"io"
    10  	"strings"
    11  	stdtesting "testing"
    12  
    13  	"github.com/juju/blobstore/v3"
    14  	"github.com/juju/errors"
    15  	mgotesting "github.com/juju/mgo/v3/testing"
    16  	jc "github.com/juju/testing/checkers"
    17  	jujutxn "github.com/juju/txn/v3"
    18  	txntesting "github.com/juju/txn/v3/testing"
    19  	"github.com/juju/version/v2"
    20  	gc "gopkg.in/check.v1"
    21  
    22  	"github.com/juju/juju/mongo"
    23  	"github.com/juju/juju/state/binarystorage"
    24  	"github.com/juju/juju/testing"
    25  )
    26  
    27  const current = "2.0.42-ubuntu-amd64"
    28  
    29  func TestPackage(t *stdtesting.T) {
    30  	testing.MgoTestPackage(t)
    31  }
    32  
    33  type binaryStorageSuite struct {
    34  	mgotesting.IsolatedMgoSuite
    35  
    36  	storage            binarystorage.Storage
    37  	managedStorage     blobstore.ManagedStorage
    38  	metadataCollection mongo.Collection
    39  	txnRunner          jujutxn.Runner
    40  
    41  	cleanUps []func(*gc.C)
    42  }
    43  
    44  var _ = gc.Suite(&binaryStorageSuite{})
    45  
    46  func (s *binaryStorageSuite) SetUpTest(c *gc.C) {
    47  	s.IsolatedMgoSuite.SetUpTest(c)
    48  
    49  	catalogue := s.Session.DB("catalogue")
    50  	rs := blobstore.NewGridFS("blobstore", "blobstore", catalogue.Session)
    51  	var closer func()
    52  	s.metadataCollection, closer = mongo.CollectionFromName(catalogue, "binarymetadata")
    53  	s.addCleanup(func(*gc.C) { closer() })
    54  	s.managedStorage = blobstore.NewManagedStorage(s.metadataCollection.Writeable().Underlying().Database, rs)
    55  	s.txnRunner = jujutxn.NewRunner(jujutxn.RunnerParams{
    56  		Database:                  catalogue,
    57  		TransactionCollectionName: "txns",
    58  		ChangeLogName:             "-",
    59  		ServerSideTransactions:    true,
    60  		MaxRetryAttempts:          3,
    61  	})
    62  	s.storage = binarystorage.New("my-uuid", s.managedStorage, s.metadataCollection, s.txnRunner)
    63  }
    64  
    65  func (s *binaryStorageSuite) addCleanup(f func(*gc.C)) {
    66  	s.cleanUps = append(s.cleanUps, f)
    67  }
    68  
    69  func (s *binaryStorageSuite) TearDownTest(c *gc.C) {
    70  	for _, f := range s.cleanUps {
    71  		// Ensure to close sessions before IsolatedMgoSuite.TearDownTest here.
    72  		f(c)
    73  	}
    74  
    75  	s.storage = nil
    76  	s.managedStorage = nil
    77  	s.metadataCollection = nil
    78  	s.txnRunner = nil
    79  	s.IsolatedMgoSuite.TearDownTest(c)
    80  }
    81  
    82  func (s *binaryStorageSuite) TestAdd(c *gc.C) {
    83  	s.testAdd(c, "some-binary")
    84  }
    85  
    86  func (s *binaryStorageSuite) TestAddReplaces(c *gc.C) {
    87  	s.testAdd(c, "abc")
    88  	s.testAdd(c, "def")
    89  }
    90  
    91  func (s *binaryStorageSuite) testAdd(c *gc.C, content string) {
    92  	r := bytes.NewReader([]byte(content))
    93  	addedMetadata := binarystorage.Metadata{
    94  		Version: current,
    95  		Size:    int64(len(content)),
    96  		SHA256:  "hash(" + content + ")",
    97  	}
    98  	err := s.storage.Add(r, addedMetadata)
    99  	c.Assert(err, jc.ErrorIsNil)
   100  
   101  	metadata, rc, err := s.storage.Open(current)
   102  	c.Assert(err, jc.ErrorIsNil)
   103  	c.Assert(r, gc.NotNil)
   104  	defer rc.Close()
   105  	c.Assert(metadata, gc.Equals, addedMetadata)
   106  
   107  	data, err := io.ReadAll(rc)
   108  	c.Assert(err, jc.ErrorIsNil)
   109  	c.Assert(string(data), gc.Equals, content)
   110  }
   111  
   112  func bumpVersion(v string) string {
   113  	vers := version.MustParseBinary(v)
   114  	vers.Build++
   115  	return vers.String()
   116  }
   117  
   118  func (s *binaryStorageSuite) TestAllMetadata(c *gc.C) {
   119  	metadata, err := s.storage.AllMetadata()
   120  	c.Assert(err, jc.ErrorIsNil)
   121  	c.Assert(metadata, gc.HasLen, 0)
   122  
   123  	s.addMetadataDoc(c, current, 3, "hash(abc)", "path")
   124  	metadata, err = s.storage.AllMetadata()
   125  	c.Assert(err, jc.ErrorIsNil)
   126  	c.Assert(metadata, gc.HasLen, 1)
   127  	expected := []binarystorage.Metadata{{
   128  		Version: current,
   129  		Size:    3,
   130  		SHA256:  "hash(abc)",
   131  	}}
   132  	c.Assert(metadata, jc.SameContents, expected)
   133  
   134  	alias := bumpVersion(current)
   135  	s.addMetadataDoc(c, alias, 3, "hash(abc)", "path")
   136  
   137  	metadata, err = s.storage.AllMetadata()
   138  	c.Assert(err, jc.ErrorIsNil)
   139  	c.Assert(metadata, gc.HasLen, 2)
   140  	expected = append(expected, binarystorage.Metadata{
   141  		Version: alias,
   142  		Size:    3,
   143  		SHA256:  "hash(abc)",
   144  	})
   145  	c.Assert(metadata, jc.SameContents, expected)
   146  }
   147  
   148  func (s *binaryStorageSuite) TestMetadata(c *gc.C) {
   149  	_, err := s.storage.Metadata(current)
   150  	c.Assert(err, jc.Satisfies, errors.IsNotFound)
   151  
   152  	s.addMetadataDoc(c, current, 3, "hash(abc)", "path")
   153  	metadata, err := s.storage.Metadata(current)
   154  	c.Assert(err, jc.ErrorIsNil)
   155  	c.Assert(metadata, gc.Equals, binarystorage.Metadata{
   156  		Version: current,
   157  		Size:    3,
   158  		SHA256:  "hash(abc)",
   159  	})
   160  }
   161  
   162  func (s *binaryStorageSuite) TestOpen(c *gc.C) {
   163  	_, _, err := s.storage.Open(current)
   164  	c.Assert(err, jc.Satisfies, errors.IsNotFound)
   165  	c.Assert(err, gc.ErrorMatches, `.* binary metadata not found`)
   166  
   167  	s.addMetadataDoc(c, current, 3, "hash(abc)", "path")
   168  	_, _, err = s.storage.Open(current)
   169  	c.Assert(err, jc.Satisfies, errors.IsNotFound)
   170  	c.Assert(err, gc.ErrorMatches, `resource at path "buckets/my-uuid/path" not found`)
   171  
   172  	err = s.managedStorage.PutForBucket("my-uuid", "path", strings.NewReader("blah"), 4)
   173  	c.Assert(err, jc.ErrorIsNil)
   174  
   175  	metadata, r, err := s.storage.Open(current)
   176  	c.Assert(err, jc.ErrorIsNil)
   177  	defer r.Close()
   178  	c.Assert(metadata, gc.Equals, binarystorage.Metadata{
   179  		Version: current,
   180  		Size:    3,
   181  		SHA256:  "hash(abc)",
   182  	})
   183  
   184  	data, err := io.ReadAll(r)
   185  	c.Assert(err, jc.ErrorIsNil)
   186  	c.Assert(string(data), gc.Equals, "blah")
   187  }
   188  
   189  func (s *binaryStorageSuite) TestAddRemovesExisting(c *gc.C) {
   190  	// Add a metadata doc and a blob at a known path, then
   191  	// call Add and ensure the original blob is removed.
   192  	s.addMetadataDoc(c, current, 3, "hash(abc)", "path")
   193  	err := s.managedStorage.PutForBucket("my-uuid", "path", strings.NewReader("blah"), 4)
   194  	c.Assert(err, jc.ErrorIsNil)
   195  
   196  	addedMetadata := binarystorage.Metadata{
   197  		Version: current,
   198  		Size:    6,
   199  		SHA256:  "hash(xyzzzz)",
   200  	}
   201  	err = s.storage.Add(strings.NewReader("xyzzzz"), addedMetadata)
   202  	c.Assert(err, jc.ErrorIsNil)
   203  
   204  	// old blob should be gone
   205  	_, _, err = s.managedStorage.GetForBucket("my-uuid", "path")
   206  	c.Assert(err, jc.Satisfies, errors.IsNotFound)
   207  
   208  	s.assertMetadataAndContent(c, addedMetadata, "xyzzzz")
   209  }
   210  
   211  func (s *binaryStorageSuite) TestAddRemovesExistingRemoveFails(c *gc.C) {
   212  	// Add a metadata doc and a blob at a known path, then
   213  	// call Add and ensure that Add attempts to remove
   214  	// the original blob, but does not return an error if it
   215  	// fails.
   216  	s.addMetadataDoc(c, current, 3, "hash(abc)", "path")
   217  	err := s.managedStorage.PutForBucket("my-uuid", "path", strings.NewReader("blah"), 4)
   218  	c.Assert(err, jc.ErrorIsNil)
   219  
   220  	storage := binarystorage.New(
   221  		"my-uuid",
   222  		removeFailsManagedStorage{s.managedStorage},
   223  		s.metadataCollection,
   224  		s.txnRunner,
   225  	)
   226  	addedMetadata := binarystorage.Metadata{
   227  		Version: current,
   228  		Size:    6,
   229  		SHA256:  "hash(xyzzzz)",
   230  	}
   231  	err = storage.Add(strings.NewReader("xyzzzz"), addedMetadata)
   232  	c.Assert(err, jc.ErrorIsNil)
   233  
   234  	// old blob should still be there
   235  	r, _, err := s.managedStorage.GetForBucket("my-uuid", "path")
   236  	c.Assert(err, jc.ErrorIsNil)
   237  	r.Close()
   238  
   239  	s.assertMetadataAndContent(c, addedMetadata, "xyzzzz")
   240  }
   241  
   242  func (s *binaryStorageSuite) TestAddRemovesBlobOnFailure(c *gc.C) {
   243  	storage := binarystorage.New(
   244  		"my-uuid",
   245  		s.managedStorage,
   246  		s.metadataCollection,
   247  		errorTransactionRunner{s.txnRunner},
   248  	)
   249  	addedMetadata := binarystorage.Metadata{
   250  		Version: current,
   251  		Size:    6,
   252  		SHA256:  "hash",
   253  	}
   254  	err := storage.Add(strings.NewReader("xyzzzz"), addedMetadata)
   255  	c.Assert(err, gc.ErrorMatches, "cannot store binary metadata: Run fails")
   256  
   257  	path := fmt.Sprintf("tools/%s-%s", addedMetadata.Version, addedMetadata.SHA256)
   258  	_, _, err = s.managedStorage.GetForBucket("my-uuid", path)
   259  	c.Assert(err, jc.Satisfies, errors.IsNotFound)
   260  }
   261  
   262  func (s *binaryStorageSuite) TestAddRemovesBlobOnFailureRemoveFails(c *gc.C) {
   263  	storage := binarystorage.New(
   264  		"my-uuid",
   265  		removeFailsManagedStorage{s.managedStorage},
   266  		s.metadataCollection,
   267  		errorTransactionRunner{s.txnRunner},
   268  	)
   269  	addedMetadata := binarystorage.Metadata{
   270  		Version: current,
   271  		Size:    6,
   272  		SHA256:  "hash",
   273  	}
   274  	err := storage.Add(strings.NewReader("xyzzzz"), addedMetadata)
   275  	c.Assert(err, gc.ErrorMatches, "cannot store binary metadata: Run fails")
   276  
   277  	// blob should still be there, because the removal failed.
   278  	path := fmt.Sprintf("tools/%s-%s", addedMetadata.Version, addedMetadata.SHA256)
   279  	r, _, err := s.managedStorage.GetForBucket("my-uuid", path)
   280  	c.Assert(err, jc.ErrorIsNil)
   281  	r.Close()
   282  }
   283  
   284  func (s *binaryStorageSuite) TestAddSame(c *gc.C) {
   285  	metadata := binarystorage.Metadata{Version: current, Size: 1, SHA256: "0"}
   286  	for i := 0; i < 2; i++ {
   287  		err := s.storage.Add(strings.NewReader("0"), metadata)
   288  		c.Assert(err, jc.ErrorIsNil)
   289  		s.assertMetadataAndContent(c, metadata, "0")
   290  	}
   291  }
   292  
   293  func (s *binaryStorageSuite) TestAddConcurrent(c *gc.C) {
   294  	metadata0 := binarystorage.Metadata{Version: current, Size: 1, SHA256: "0"}
   295  	metadata1 := binarystorage.Metadata{Version: current, Size: 1, SHA256: "1"}
   296  
   297  	addMetadata := func() {
   298  		err := s.storage.Add(strings.NewReader("0"), metadata0)
   299  		c.Assert(err, jc.ErrorIsNil)
   300  		r, _, err := s.managedStorage.GetForBucket("my-uuid", fmt.Sprintf("tools/%s-0", current))
   301  		c.Assert(err, jc.ErrorIsNil)
   302  		r.Close()
   303  	}
   304  	defer txntesting.SetBeforeHooks(c, s.txnRunner, addMetadata).Check()
   305  
   306  	err := s.storage.Add(strings.NewReader("1"), metadata1)
   307  	c.Assert(err, jc.ErrorIsNil)
   308  
   309  	// Blob added in before-hook should be removed.
   310  	_, _, err = s.managedStorage.GetForBucket("my-uuid", fmt.Sprintf("tools/%s-0", current))
   311  	c.Assert(err, jc.Satisfies, errors.IsNotFound)
   312  
   313  	s.assertMetadataAndContent(c, metadata1, "1")
   314  }
   315  
   316  func (s *binaryStorageSuite) TestAddExcessiveContention(c *gc.C) {
   317  	metadata := []binarystorage.Metadata{
   318  		{Version: current, Size: 1, SHA256: "0"},
   319  		{Version: current, Size: 1, SHA256: "1"},
   320  		{Version: current, Size: 1, SHA256: "2"},
   321  		{Version: current, Size: 1, SHA256: "3"},
   322  	}
   323  
   324  	i := 1
   325  	addMetadata := func() {
   326  		err := s.storage.Add(strings.NewReader(metadata[i].SHA256), metadata[i])
   327  		c.Assert(err, jc.ErrorIsNil)
   328  		i++
   329  	}
   330  	defer txntesting.SetBeforeHooks(c, s.txnRunner, addMetadata, addMetadata, addMetadata).Check()
   331  
   332  	err := s.storage.Add(strings.NewReader(metadata[0].SHA256), metadata[0])
   333  	c.Assert(err, gc.ErrorMatches, "cannot store binary metadata: state changing too quickly; try again soon")
   334  
   335  	// There should be no blobs apart from the last one added by the before-hook.
   336  	for _, metadata := range metadata[:3] {
   337  		path := fmt.Sprintf("tools/%s-%s", metadata.Version, metadata.SHA256)
   338  		_, _, err = s.managedStorage.GetForBucket("my-uuid", path)
   339  		c.Assert(err, jc.Satisfies, errors.IsNotFound)
   340  	}
   341  
   342  	s.assertMetadataAndContent(c, metadata[3], "3")
   343  }
   344  
   345  func (s *binaryStorageSuite) addMetadataDoc(c *gc.C, v string, size int64, hash, path string) {
   346  	doc := struct {
   347  		Id      string `bson:"_id"`
   348  		Version string `bson:"version"`
   349  		Size    int64  `bson:"size"`
   350  		SHA256  string `bson:"sha256,omitempty"`
   351  		Path    string `bson:"path"`
   352  	}{
   353  		Id:      v,
   354  		Version: v,
   355  		Size:    size,
   356  		SHA256:  hash,
   357  		Path:    path,
   358  	}
   359  	err := s.metadataCollection.Writeable().Insert(&doc)
   360  	c.Assert(err, jc.ErrorIsNil)
   361  }
   362  
   363  func (s *binaryStorageSuite) assertMetadataAndContent(c *gc.C, expected binarystorage.Metadata, content string) {
   364  	metadata, r, err := s.storage.Open(expected.Version)
   365  	c.Assert(err, jc.ErrorIsNil)
   366  	defer r.Close()
   367  	c.Assert(metadata, gc.Equals, expected)
   368  
   369  	data, err := io.ReadAll(r)
   370  	c.Assert(err, jc.ErrorIsNil)
   371  	c.Assert(string(data), gc.Equals, content)
   372  }
   373  
   374  type removeFailsManagedStorage struct {
   375  	blobstore.ManagedStorage
   376  }
   377  
   378  func (removeFailsManagedStorage) RemoveForBucket(uuid, path string) error {
   379  	return errors.Errorf("cannot remove %s:%s", uuid, path)
   380  }
   381  
   382  type errorTransactionRunner struct {
   383  	jujutxn.Runner
   384  }
   385  
   386  func (errorTransactionRunner) Run(transactions jujutxn.TransactionSource) error {
   387  	return errors.New("Run fails")
   388  }