~jameinel/juju-core/scale-testing

« back to all changes in this revision

Viewing changes to state/state.go

[r=dimitern],[bug=1067979] state;apiserver: Fix a race - lp bug #1067979

This introduces some changes to how charm store
charms are added through the API (in state and
to provider storage). Now PrepareStoreCharmUpload
is called before trying to download the charm,
repackage it and upload it to storage, in order
to reserve a charm URL in state with pending
state. Added a test that demonstrates multiple
concurrent deployments of the same charm does
not cause the race issues, like mentioned in
the bug.

A few drive-by fixes brought up during review:
* Added ReadSHA256 and ReadFileSHA256 helpers in
  utils, and changed most places where hashes
  are calculated to use them.
* Charms are now uploaded to storage with a
  randomly generated archive names with the
  format "<charm-name>-<revision>-<uuid>".
  This allows multiple concurrent uploads
  to happen safely, and at the end AddCharm
  in the API checks to see if the charm info
  is already updated in state and if so, deletes
  the duplicated upload.
* Added GetEnvironStorage helper to environs/testing.
* Fixed potential compatibility issues with older
  versions and the recently added PendingUpload
  and Placeholder fields of the charm document.

Also tested multiple concurrent deployments with
the local provider manually and updated the bug
accordingly.

https://codereview.appspot.com/53210044/

R=fwereade

Show diffs side-by-side

added added

removed removed

Lines of Context:
445
445
        return coll, id, nil
446
446
}
447
447
 
448
 
// AddCharm adds the ch charm with curl to the state.  bundleUrl must be
449
 
// set to a URL where the bundle for ch may be downloaded from.
450
 
// On success the newly added charm state is returned.
 
448
// AddCharm adds the ch charm with curl to the state. bundleURL must
 
449
// be set to a URL where the bundle for ch may be downloaded from. On
 
450
// success the newly added charm state is returned.
451
451
func (st *State) AddCharm(ch charm.Charm, curl *charm.URL, bundleURL *url.URL, bundleSha256 string) (stch *Charm, err error) {
452
 
        // The charm may already exist in state as a placeholder, so we check for that situation and update the
453
 
        // existing charm record if necessary, otherwise add a new record.
 
452
        // The charm may already exist in state as a placeholder, so we
 
453
        // check for that situation and update the existing charm record
 
454
        // if necessary, otherwise add a new record.
454
455
        var existing charmDoc
455
456
        err = st.charms.Find(D{{"_id", curl.String()}, {"placeholder", true}}).One(&existing)
456
457
        if err == mgo.ErrNotFound {
469
470
        } else if err != nil {
470
471
                return nil, err
471
472
        }
472
 
        return st.updateCharmDoc(ch, curl, bundleURL, bundleSha256, StillPlaceholder)
 
473
        return st.updateCharmDoc(ch, curl, bundleURL, bundleSha256, stillPlaceholder)
473
474
}
474
475
 
475
 
// Charm returns the charm with the given URL.
 
476
// Charm returns the charm with the given URL. Charms pending upload
 
477
// to storage and placeholders are never returned.
476
478
func (st *State) Charm(curl *charm.URL) (*Charm, error) {
477
479
        cdoc := &charmDoc{}
478
 
        err := st.charms.Find(D{{"_id", curl}, {"pendingupload", false}, {"placeholder", false}}).One(cdoc)
 
480
        what := D{
 
481
                {"_id", curl},
 
482
                {"placeholder", D{{"$ne", true}}},
 
483
                {"pendingupload", D{{"$ne", true}}},
 
484
        }
 
485
        err := st.charms.Find(what).One(&cdoc)
479
486
        if err == mgo.ErrNotFound {
480
487
                return nil, errors.NotFoundf("charm %q", curl)
481
488
        }
488
495
        return newCharm(st, cdoc)
489
496
}
490
497
 
491
 
// LatestPlaceholderCharm returns the latest charm described by the given URL but which is not yet deployed.
 
498
// LatestPlaceholderCharm returns the latest charm described by the
 
499
// given URL but which is not yet deployed.
492
500
func (st *State) LatestPlaceholderCharm(curl *charm.URL) (*Charm, error) {
493
501
        noRevURL := curl.WithRevision(-1)
494
502
        curlRegex := "^" + regexp.QuoteMeta(noRevURL.String())
510
518
        return newCharm(st, &latest)
511
519
}
512
520
 
513
 
// PrepareLocalCharmUpload must be called before a charm is uploaded
514
 
// to the provider storage in order to create a charm document in
515
 
// state. It returns a new Charm with no metadata and a unique
516
 
// revision number.
 
521
// PrepareLocalCharmUpload must be called before a local charm is
 
522
// uploaded to the provider storage in order to create a charm
 
523
// document in state. It returns the chosen unique charm URL reserved
 
524
// in state for the charm.
517
525
//
518
526
// The url's schema must be "local" and it must include a revision.
519
527
func (st *State) PrepareLocalCharmUpload(curl *charm.URL) (chosenUrl *charm.URL, err error) {
575
583
        return chosenUrl, nil
576
584
}
577
585
 
578
 
var StillPending = D{{"pendingupload", true}}
579
 
var StillPlaceholder = D{{"placeholder", true}}
 
586
// PrepareStoreCharmUpload must be called before a charm store charm
 
587
// is uploaded to the provider storage in order to create a charm
 
588
// document in state. If a charm with the same URL is already in
 
589
// state, it will be returned as a *state.Charm (is can be still
 
590
// pending or already uploaded). Otherwise, a new charm document is
 
591
// added in state with just the given charm URL and
 
592
// PendingUpload=true, which is then returned as a *state.Charm.
 
593
//
 
594
// The url's schema must be "cs" and it must include a revision.
 
595
func (st *State) PrepareStoreCharmUpload(curl *charm.URL) (*Charm, error) {
 
596
        // Perform a few sanity checks first.
 
597
        if curl.Schema != "cs" {
 
598
                return nil, fmt.Errorf("expected charm URL with cs schema, got %q", curl)
 
599
        }
 
600
        if curl.Revision < 0 {
 
601
                return nil, fmt.Errorf("expected charm URL with revision, got %q", curl)
 
602
        }
 
603
 
 
604
        var (
 
605
                uploadedCharm charmDoc
 
606
                err           error
 
607
        )
 
608
        for attempt := 0; attempt < 3; attempt++ {
 
609
                // Find an uploaded or pending charm with the given exact curl.
 
610
                err = st.charms.FindId(curl).One(&uploadedCharm)
 
611
                if err != nil && err != mgo.ErrNotFound {
 
612
                        return nil, err
 
613
                } else if err == nil && !uploadedCharm.Placeholder {
 
614
                        // The charm exists and it's either uploaded or still
 
615
                        // pending, but it's not a placeholder. In any case, we
 
616
                        // just return what we got.
 
617
                        return newCharm(st, &uploadedCharm)
 
618
                } else if err == mgo.ErrNotFound {
 
619
                        // Prepare the pending charm document for insertion.
 
620
                        uploadedCharm = charmDoc{
 
621
                                URL:           curl,
 
622
                                PendingUpload: true,
 
623
                                Placeholder:   false,
 
624
                        }
 
625
                }
 
626
 
 
627
                var ops []txn.Op
 
628
                if uploadedCharm.Placeholder {
 
629
                        // Convert the placeholder to a pending charm, while
 
630
                        // asserting the fields updated after an upload have not
 
631
                        // changed yet.
 
632
                        ops = []txn.Op{{
 
633
                                C:  st.charms.Name,
 
634
                                Id: curl,
 
635
                                Assert: D{
 
636
                                        {"bundlesha256", ""},
 
637
                                        {"pendingupload", false},
 
638
                                        {"placeholder", true},
 
639
                                },
 
640
                                Update: D{{"$set", D{
 
641
                                        {"pendingupload", true},
 
642
                                        {"placeholder", false},
 
643
                                }}},
 
644
                        }}
 
645
                        // Update the fields of the document we're returning.
 
646
                        uploadedCharm.PendingUpload = true
 
647
                        uploadedCharm.Placeholder = false
 
648
                } else {
 
649
                        // No charm document with this curl yet, insert it.
 
650
                        ops = []txn.Op{{
 
651
                                C:      st.charms.Name,
 
652
                                Id:     curl,
 
653
                                Assert: txn.DocMissing,
 
654
                                Insert: uploadedCharm,
 
655
                        }}
 
656
                }
 
657
 
 
658
                // Run the transaction, and retry on abort.
 
659
                err = st.runTransaction(ops)
 
660
                if err == txn.ErrAborted {
 
661
                        continue
 
662
                } else if err != nil {
 
663
                        return nil, err
 
664
                } else if err == nil {
 
665
                        return newCharm(st, &uploadedCharm)
 
666
                }
 
667
        }
 
668
        return nil, ErrExcessiveContention
 
669
}
 
670
 
 
671
var (
 
672
        stillPending     = D{{"pendingupload", true}}
 
673
        stillPlaceholder = D{{"placeholder", true}}
 
674
)
580
675
 
581
676
// AddStoreCharmPlaceholder creates a charm document in state for the given charm URL which
582
677
// must reference a charm from the store. The charm document is marked as a placeholder which
653
748
                ops = append(ops, txn.Op{
654
749
                        C:      st.charms.Name,
655
750
                        Id:     doc.URL.String(),
656
 
                        Assert: StillPlaceholder,
 
751
                        Assert: stillPlaceholder,
657
752
                        Remove: true,
658
753
                })
659
754
        }
660
755
        return ops, nil
661
756
}
662
757
 
 
758
// ErrCharmAlreadyUploaded is returned by UpdateUploadedCharm() when
 
759
// the given charm is already uploaded and marked as not pending in
 
760
// state.
 
761
type ErrCharmAlreadyUploaded struct {
 
762
        curl *charm.URL
 
763
}
 
764
 
 
765
func (e *ErrCharmAlreadyUploaded) Error() string {
 
766
        return fmt.Sprintf("charm %q already uploaded", e.curl)
 
767
}
 
768
 
 
769
// IsCharmAlreadyUploadedError returns if the given error is
 
770
// ErrCharmAlreadyUploaded.
 
771
func IsCharmAlreadyUploadedError(err interface{}) bool {
 
772
        if err == nil {
 
773
                return false
 
774
        }
 
775
        _, ok := err.(*ErrCharmAlreadyUploaded)
 
776
        return ok
 
777
}
 
778
 
 
779
// ErrCharmRevisionAlreadyModified is returned when a pending or
 
780
// placeholder charm is no longer pending or a placeholder, signaling
 
781
// the charm is available in state with its full information.
 
782
var ErrCharmRevisionAlreadyModified = fmt.Errorf("charm revision already modified")
 
783
 
663
784
// UpdateUploadedCharm marks the given charm URL as uploaded and
664
785
// updates the rest of its data, returning it as *state.Charm.
665
786
func (st *State) UpdateUploadedCharm(ch charm.Charm, curl *charm.URL, bundleURL *url.URL, bundleSha256 string) (*Charm, error) {
672
793
                return nil, err
673
794
        }
674
795
        if !doc.PendingUpload {
675
 
                return nil, fmt.Errorf("charm %q already uploaded", curl)
 
796
                return nil, &ErrCharmAlreadyUploaded{curl}
676
797
        }
677
798
 
678
 
        return st.updateCharmDoc(ch, curl, bundleURL, bundleSha256, StillPending)
 
799
        return st.updateCharmDoc(ch, curl, bundleURL, bundleSha256, stillPending)
679
800
}
680
801
 
681
 
// updateCharmDoc updates the charm with specified URL with the given data, and resets the placeholder
682
 
// and pendingupdate flags.
 
802
// updateCharmDoc updates the charm with specified URL with the given
 
803
// data, and resets the placeholder and pendingupdate flags.  If the
 
804
// charm is no longer a placeholder or pending (depending on preReq),
 
805
// it returns ErrCharmRevisionAlreadyModified.
683
806
func (st *State) updateCharmDoc(
684
807
        ch charm.Charm, curl *charm.URL, bundleURL *url.URL, bundleSha256 string, preReq interface{}) (*Charm, error) {
685
808
 
698
821
                Update: updateFields,
699
822
        }}
700
823
        if err := st.runTransaction(ops); err != nil {
701
 
                return nil, onAbort(err, fmt.Errorf("charm revision already modified"))
 
824
                return nil, onAbort(err, ErrCharmRevisionAlreadyModified)
702
825
        }
703
826
        return st.Charm(curl)
704
827
}