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 }