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 }