github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/resource/opener_test.go (about)

     1  // Copyright 2020 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package resource_test
     5  
     6  import (
     7  	"bytes"
     8  	"io"
     9  	"sync"
    10  	"time"
    11  
    12  	"github.com/juju/charm/v12"
    13  	charmresource "github.com/juju/charm/v12/resource"
    14  	"github.com/juju/errors"
    15  	"github.com/juju/names/v5"
    16  	jc "github.com/juju/testing/checkers"
    17  	"go.uber.org/mock/gomock"
    18  	gc "gopkg.in/check.v1"
    19  
    20  	"github.com/juju/juju/core/resources"
    21  	"github.com/juju/juju/resource"
    22  	"github.com/juju/juju/resource/mocks"
    23  	"github.com/juju/juju/state"
    24  	coretesting "github.com/juju/juju/testing"
    25  )
    26  
    27  type OpenerSuite struct {
    28  	appName        string
    29  	unitName       string
    30  	charmURL       *charm.URL
    31  	charmOrigin    state.CharmOrigin
    32  	resources      *mocks.MockResources
    33  	resourceGetter *mocks.MockResourceGetter
    34  	limiter        *mocks.MockResourceDownloadLock
    35  
    36  	unleash sync.Mutex
    37  }
    38  
    39  var _ = gc.Suite(&OpenerSuite{})
    40  
    41  func (s *OpenerSuite) TestOpenResource(c *gc.C) {
    42  	defer s.setupMocks(c, true).Finish()
    43  	fp, _ := charmresource.ParseFingerprint("38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b")
    44  	res := resources.Resource{
    45  		Resource: charmresource.Resource{
    46  			Meta: charmresource.Meta{
    47  				Name: "wal-e",
    48  				Type: 1,
    49  			},
    50  			Origin:      2,
    51  			Revision:    0,
    52  			Fingerprint: fp,
    53  			Size:        0,
    54  		},
    55  		ApplicationID: "postgreql",
    56  	}
    57  	s.expectCacheMethods(res, 1)
    58  	s.resourceGetter.EXPECT().GetResource(gomock.Any()).Return(resource.ResourceData{
    59  		ReadCloser: nil,
    60  		Resource:   res.Resource,
    61  	}, nil)
    62  
    63  	opened, err := s.newOpener(0).OpenResource("wal-e")
    64  	c.Assert(err, jc.ErrorIsNil)
    65  	c.Check(opened.Resource, gc.DeepEquals, res)
    66  	c.Assert(opened.Close(), jc.ErrorIsNil)
    67  }
    68  
    69  func (s *OpenerSuite) TestOpenResourceThrottle(c *gc.C) {
    70  	defer s.setupMocks(c, true).Finish()
    71  	fp, _ := charmresource.ParseFingerprint("38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b")
    72  	res := resources.Resource{
    73  		Resource: charmresource.Resource{
    74  			Meta: charmresource.Meta{
    75  				Name: "wal-e",
    76  				Type: 1,
    77  			},
    78  			Origin:      2,
    79  			Revision:    0,
    80  			Fingerprint: fp,
    81  			Size:        0,
    82  		},
    83  		ApplicationID: "postgreql",
    84  	}
    85  	const (
    86  		numConcurrentRequests = 10
    87  		maxConcurrentRequests = 5
    88  	)
    89  	s.expectCacheMethods(res, numConcurrentRequests)
    90  	s.resourceGetter.EXPECT().GetResource(gomock.Any()).Return(resource.ResourceData{
    91  		ReadCloser: nil,
    92  		Resource:   res.Resource,
    93  	}, nil)
    94  
    95  	s.unleash.Lock()
    96  	start := sync.WaitGroup{}
    97  	finished := sync.WaitGroup{}
    98  	for i := 0; i < numConcurrentRequests; i++ {
    99  		start.Add(1)
   100  		finished.Add(1)
   101  		go func() {
   102  			defer finished.Done()
   103  			start.Done()
   104  			opened, err := s.newOpener(maxConcurrentRequests).OpenResource("wal-e")
   105  			c.Assert(err, jc.ErrorIsNil)
   106  			c.Check(opened.Resource, gc.DeepEquals, res)
   107  			c.Assert(opened.Close(), jc.ErrorIsNil)
   108  		}()
   109  	}
   110  	// Let all the test routines queue up then unleash.
   111  	start.Wait()
   112  	s.unleash.Unlock()
   113  
   114  	done := make(chan bool)
   115  	go func() {
   116  		finished.Wait()
   117  		close(done)
   118  	}()
   119  	select {
   120  	case <-done:
   121  	case <-time.After(coretesting.LongWait):
   122  		c.Fatal("timeout waiting for resources to be fetched")
   123  	}
   124  }
   125  
   126  func (s *OpenerSuite) TestOpenResourceApplication(c *gc.C) {
   127  	defer s.setupMocks(c, false).Finish()
   128  	fp, _ := charmresource.ParseFingerprint("38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b")
   129  	res := resources.Resource{
   130  		Resource: charmresource.Resource{
   131  			Meta: charmresource.Meta{
   132  				Name: "wal-e",
   133  				Type: 1,
   134  			},
   135  			Origin:      2,
   136  			Revision:    0,
   137  			Fingerprint: fp,
   138  			Size:        0,
   139  		},
   140  		ApplicationID: "postgreql",
   141  	}
   142  	s.expectCacheMethods(res, 1)
   143  	s.resourceGetter.EXPECT().GetResource(gomock.Any()).Return(resource.ResourceData{
   144  		ReadCloser: nil,
   145  		Resource:   res.Resource,
   146  	}, nil)
   147  
   148  	opened, err := s.newOpener(0).OpenResource("wal-e")
   149  	c.Assert(err, jc.ErrorIsNil)
   150  	c.Assert(opened.Resource, gc.DeepEquals, res)
   151  	err = opened.Close()
   152  	c.Assert(err, jc.ErrorIsNil)
   153  }
   154  
   155  func (s *OpenerSuite) setupMocks(c *gc.C, includeUnit bool) *gomock.Controller {
   156  	ctrl := gomock.NewController(c)
   157  	if includeUnit {
   158  		s.unitName = "postgresql/0"
   159  	}
   160  	s.appName = "postgresql"
   161  	s.resourceGetter = mocks.NewMockResourceGetter(ctrl)
   162  	s.resources = mocks.NewMockResources(ctrl)
   163  	s.limiter = mocks.NewMockResourceDownloadLock(ctrl)
   164  
   165  	s.charmURL, _ = charm.ParseURL("postgresql")
   166  	rev := 0
   167  	s.charmOrigin = state.CharmOrigin{
   168  		Source:   "charm-hub",
   169  		Type:     "charm",
   170  		Revision: &rev,
   171  		Channel:  &state.Channel{Risk: "stable"},
   172  		Platform: &state.Platform{
   173  			Architecture: "amd64",
   174  			OS:           "ubuntu",
   175  			Channel:      "20.04/stable",
   176  		},
   177  	}
   178  	return ctrl
   179  }
   180  
   181  func (s *OpenerSuite) expectCacheMethods(res resources.Resource, numConcurrentRequests int) {
   182  	if s.unitName != "" {
   183  		s.resources.EXPECT().OpenResourceForUniter("postgresql/0", "wal-e").DoAndReturn(func(unitName, resName string) (resources.Resource, io.ReadCloser, error) {
   184  			s.unleash.Lock()
   185  			defer s.unleash.Unlock()
   186  			return resources.Resource{}, io.NopCloser(bytes.NewBuffer([]byte{})), errors.NotFoundf("wal-e")
   187  		})
   188  	} else {
   189  		s.resources.EXPECT().OpenResource("postgresql", "wal-e").Return(resources.Resource{}, io.NopCloser(bytes.NewBuffer([]byte{})), errors.NotFoundf("wal-e"))
   190  	}
   191  	s.resources.EXPECT().GetResource("postgresql", "wal-e").Return(res, nil)
   192  	s.resources.EXPECT().SetResource("postgresql", "", res.Resource, gomock.Any(), state.DoNotIncrementCharmModifiedVersion).Return(res, nil)
   193  
   194  	other := res
   195  	other.ApplicationID = "postgreql"
   196  	if s.unitName != "" {
   197  		s.resources.EXPECT().OpenResourceForUniter("postgresql/0", "wal-e").Return(other, io.NopCloser(bytes.NewBuffer([]byte{})), nil).Times(numConcurrentRequests)
   198  	} else {
   199  		s.resources.EXPECT().OpenResource("postgresql", "wal-e").Return(other, io.NopCloser(bytes.NewBuffer([]byte{})), nil)
   200  	}
   201  }
   202  
   203  func (s *OpenerSuite) TestGetResourceErrorReleasesLock(c *gc.C) {
   204  	defer s.setupMocks(c, true).Finish()
   205  	fp, _ := charmresource.ParseFingerprint("38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b")
   206  	res := resources.Resource{
   207  		Resource: charmresource.Resource{
   208  			Meta: charmresource.Meta{
   209  				Name: "wal-e",
   210  				Type: 1,
   211  			},
   212  			Origin:      2,
   213  			Revision:    0,
   214  			Fingerprint: fp,
   215  			Size:        0,
   216  		},
   217  		ApplicationID: "postgreql",
   218  	}
   219  	s.resources.EXPECT().OpenResourceForUniter("postgresql/0", "wal-e").DoAndReturn(func(unitName, resName string) (resources.Resource, io.ReadCloser, error) {
   220  		s.unleash.Lock()
   221  		defer s.unleash.Unlock()
   222  		return resources.Resource{}, io.NopCloser(bytes.NewBuffer([]byte{})), errors.NotFoundf("wal-e")
   223  	})
   224  	s.resources.EXPECT().GetResource("postgresql", "wal-e").Return(res, nil)
   225  	const retryCount = 3
   226  	s.resourceGetter.EXPECT().GetResource(gomock.Any()).Return(resource.ResourceData{}, errors.New("boom")).Times(retryCount)
   227  	s.limiter.EXPECT().Acquire("uuid:postgresql")
   228  	s.limiter.EXPECT().Release("uuid:postgresql")
   229  
   230  	opened, err := s.newOpener(-1).OpenResource("wal-e")
   231  	c.Assert(err, gc.ErrorMatches, "failed after retrying: boom")
   232  	c.Check(opened, gc.NotNil)
   233  	c.Check(opened.Resource, gc.DeepEquals, resources.Resource{})
   234  	c.Check(opened.ReadCloser, gc.IsNil)
   235  }
   236  
   237  func (s *OpenerSuite) newOpener(maxRequests int) *resource.ResourceOpener {
   238  	tag, _ := names.ParseUnitTag("postgresql/0")
   239  	var limiter resource.ResourceDownloadLock = resource.NewResourceDownloadLimiter(maxRequests, 0)
   240  	if maxRequests < 0 {
   241  		limiter = s.limiter
   242  	}
   243  	return resource.NewResourceOpenerForTest(
   244  		s.resources,
   245  		tag,
   246  		s.unitName,
   247  		s.appName,
   248  		s.charmURL,
   249  		s.charmOrigin,
   250  		resource.NewResourceRetryClientForTest(s.resourceGetter),
   251  		limiter,
   252  	)
   253  }