github.com/pelicanplatform/pelican@v1.0.5/director/cache_ads_test.go (about) 1 /*************************************************************** 2 * 3 * Copyright (C) 2023, Pelican Project, Morgridge Institute for Research 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); you 6 * may not use this file except in compliance with the License. You may 7 * obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 * 17 ***************************************************************/ 18 19 package director 20 21 import ( 22 "context" 23 "net/url" 24 "sync" 25 "testing" 26 "time" 27 28 "github.com/jellydator/ttlcache/v3" 29 "github.com/stretchr/testify/assert" 30 "github.com/stretchr/testify/require" 31 ) 32 33 func hasServerAdWithName(serverAds []ServerAd, name string) bool { 34 for _, serverAd := range serverAds { 35 if serverAd.Name == name { 36 return true 37 } 38 } 39 return false 40 } 41 42 // Test getAdsForPath to make sure various nuanced cases work. Under the hood 43 // this really tests matchesPrefix, but we test this higher level function to 44 // avoid having to mess with the cache. 45 func TestGetAdsForPath(t *testing.T) { 46 /* 47 FLOW: 48 - Set up a few dummy namespaces, origin, and cache ads 49 - Record the ads 50 - Query for a few paths and make sure the correct ads are returned 51 */ 52 nsAd1 := NamespaceAd{ 53 RequireToken: true, 54 Path: "/chtc", 55 Issuer: url.URL{ 56 Scheme: "https", 57 Host: "wisc.edu", 58 }, 59 } 60 61 nsAd2 := NamespaceAd{ 62 RequireToken: false, 63 Path: "/chtc/PUBLIC", 64 Issuer: url.URL{ 65 Scheme: "https", 66 Host: "wisc.edu", 67 }, 68 } 69 70 nsAd3 := NamespaceAd{ 71 RequireToken: false, 72 Path: "/chtc/PUBLIC2/", 73 Issuer: url.URL{ 74 Scheme: "https", 75 Host: "wisc.edu", 76 }, 77 } 78 79 cacheAd1 := ServerAd{ 80 Name: "cache1", 81 AuthURL: url.URL{ 82 Scheme: "https", 83 Host: "wisc.edu", 84 }, 85 URL: url.URL{ 86 Scheme: "https", 87 Host: "wisc.edu", 88 }, 89 Type: CacheType, 90 } 91 92 cacheAd2 := ServerAd{ 93 Name: "cache2", 94 AuthURL: url.URL{ 95 Scheme: "https", 96 Host: "wisc.edu", 97 }, 98 URL: url.URL{ 99 Scheme: "https", 100 Host: "wisc.edu", 101 }, 102 Type: CacheType, 103 } 104 105 originAd1 := ServerAd{ 106 Name: "origin1", 107 AuthURL: url.URL{ 108 Scheme: "https", 109 Host: "wisc.edu", 110 }, 111 URL: url.URL{ 112 Scheme: "https", 113 Host: "wisc.edu", 114 }, 115 Type: OriginType, 116 } 117 118 originAd2 := ServerAd{ 119 Name: "origin2", 120 AuthURL: url.URL{ 121 Scheme: "https", 122 Host: "wisc.edu", 123 }, 124 URL: url.URL{ 125 Scheme: "https", 126 Host: "wisc.edu", 127 }, 128 Type: OriginType, 129 } 130 131 o1Slice := []NamespaceAd{nsAd1} 132 o2Slice := []NamespaceAd{nsAd2, nsAd3} 133 c1Slice := []NamespaceAd{nsAd1, nsAd2} 134 RecordAd(originAd2, &o2Slice) 135 RecordAd(originAd1, &o1Slice) 136 RecordAd(cacheAd1, &c1Slice) 137 RecordAd(cacheAd2, &o1Slice) 138 139 nsAd, oAds, cAds := GetAdsForPath("/chtc") 140 assert.Equal(t, nsAd.Path, "/chtc") 141 assert.Equal(t, len(oAds), 1) 142 assert.Equal(t, len(cAds), 2) 143 assert.True(t, hasServerAdWithName(oAds, "origin1")) 144 assert.True(t, hasServerAdWithName(cAds, "cache1")) 145 assert.True(t, hasServerAdWithName(cAds, "cache2")) 146 147 nsAd, oAds, cAds = GetAdsForPath("/chtc/") 148 assert.Equal(t, nsAd.Path, "/chtc") 149 assert.Equal(t, len(oAds), 1) 150 assert.Equal(t, len(cAds), 2) 151 assert.True(t, hasServerAdWithName(oAds, "origin1")) 152 assert.True(t, hasServerAdWithName(cAds, "cache1")) 153 assert.True(t, hasServerAdWithName(cAds, "cache2")) 154 155 nsAd, oAds, cAds = GetAdsForPath("/chtc/PUBLI") 156 assert.Equal(t, nsAd.Path, "/chtc") 157 assert.Equal(t, len(oAds), 1) 158 assert.Equal(t, len(cAds), 2) 159 assert.True(t, hasServerAdWithName(oAds, "origin1")) 160 assert.True(t, hasServerAdWithName(cAds, "cache1")) 161 assert.True(t, hasServerAdWithName(cAds, "cache2")) 162 163 nsAd, oAds, cAds = GetAdsForPath("/chtc/PUBLIC") 164 assert.Equal(t, nsAd.Path, "/chtc/PUBLIC") 165 assert.Equal(t, len(oAds), 1) 166 assert.Equal(t, len(cAds), 1) 167 assert.True(t, hasServerAdWithName(oAds, "origin2")) 168 assert.True(t, hasServerAdWithName(cAds, "cache1")) 169 170 nsAd, oAds, cAds = GetAdsForPath("/chtc/PUBLIC2") 171 // since the stored path is actually /chtc/PUBLIC2/, the extra / is returned 172 assert.Equal(t, nsAd.Path, "/chtc/PUBLIC2/") 173 assert.Equal(t, len(oAds), 1) 174 assert.Equal(t, len(cAds), 0) 175 assert.True(t, hasServerAdWithName(oAds, "origin2")) 176 177 // Finally, let's throw in a test for a path we know shouldn't exist 178 // in the ttlcache 179 nsAd, oAds, cAds = GetAdsForPath("/does/not/exist") 180 assert.Equal(t, nsAd.Path, "") 181 assert.Equal(t, len(oAds), 0) 182 assert.Equal(t, len(cAds), 0) 183 } 184 185 func TestConfigCacheEviction(t *testing.T) { 186 mockPelicanOriginServerAd := ServerAd{ 187 Name: "test-origin-server", 188 AuthURL: url.URL{}, 189 URL: url.URL{ 190 Scheme: "https", 191 Host: "fake-origin.org:8443", 192 }, 193 WebURL: url.URL{ 194 Scheme: "https", 195 Host: "fake-origin.org:8444", 196 }, 197 Type: OriginType, 198 Latitude: 123.05, 199 Longitude: 456.78, 200 } 201 mockNamespaceAd := NamespaceAd{ 202 RequireToken: true, 203 Path: "/foo/bar/", 204 Issuer: url.URL{}, 205 MaxScopeDepth: 1, 206 Strategy: "", 207 BasePath: "", 208 VaultServer: "", 209 } 210 211 t.Run("evicted-origin-can-cancel-health-test", func(t *testing.T) { 212 // Start cache eviction 213 shutdownCtx, shutdownCancel := context.WithCancel(context.Background()) 214 var wg sync.WaitGroup 215 ConfigTTLCache(shutdownCtx, &wg) 216 wg.Add(1) 217 defer func() { 218 shutdownCancel() 219 wg.Wait() 220 }() 221 222 ctx, cancelFunc := context.WithDeadline(context.Background(), time.Now().Add(time.Second*5)) 223 224 func() { 225 serverAdMutex.Lock() 226 defer serverAdMutex.Unlock() 227 serverAds.DeleteAll() 228 serverAds.Set(mockPelicanOriginServerAd, []NamespaceAd{mockNamespaceAd}, ttlcache.DefaultTTL) 229 healthTestCancelFuncsMutex.Lock() 230 defer healthTestCancelFuncsMutex.Unlock() 231 // Clear the map for the new test 232 healthTestCancelFuncs = make(map[ServerAd]context.CancelFunc) 233 healthTestCancelFuncs[mockPelicanOriginServerAd] = cancelFunc 234 235 require.True(t, serverAds.Has(mockPelicanOriginServerAd), "serverAds failed to register the originAd") 236 }() 237 238 cancelChan := make(chan int) 239 go func() { 240 <-ctx.Done() 241 if ctx.Err() == context.Canceled { 242 cancelChan <- 1 243 } 244 }() 245 246 func() { 247 serverAdMutex.Lock() 248 defer serverAdMutex.Unlock() 249 serverAds.Delete(mockPelicanOriginServerAd) // This should call onEviction handler and close the context 250 251 require.False(t, serverAds.Has(mockPelicanOriginServerAd), "serverAds didn't delete originAd") 252 }() 253 254 // OnEviction is handled on a different goroutine than the cache management 255 // So we want to wait for a bit so that OnEviction can have time to be 256 // executed 257 select { 258 case <-cancelChan: 259 require.True(t, true) 260 case <-time.After(3 * time.Second): 261 require.False(t, true) 262 } 263 func() { 264 healthTestCancelFuncsMutex.RLock() 265 defer healthTestCancelFuncsMutex.RUnlock() 266 assert.True(t, healthTestCancelFuncs[mockPelicanOriginServerAd] == nil, "Evicted origin didn't clear cancelFunc in the map") 267 }() 268 }) 269 } 270 271 func TestServerAdsCacheEviction(t *testing.T) { 272 mockServerAd := ServerAd{Name: "foo", Type: OriginType, URL: url.URL{}} 273 274 t.Run("evict-after-expire-time", func(t *testing.T) { 275 // Start cache eviction 276 shutdownCtx, shutdownCancel := context.WithCancel(context.Background()) 277 var wg sync.WaitGroup 278 ConfigTTLCache(shutdownCtx, &wg) 279 wg.Add(1) 280 defer func() { 281 shutdownCancel() 282 wg.Wait() 283 }() 284 285 deletedChan := make(chan int) 286 cancelChan := make(chan int) 287 288 func() { 289 serverAdMutex.Lock() 290 defer serverAdMutex.Unlock() 291 serverAds.DeleteAll() 292 293 serverAds.Set(mockServerAd, []NamespaceAd{}, time.Second*2) 294 require.True(t, serverAds.Has(mockServerAd), "Failed to register server Ad") 295 }() 296 297 // Keep checking if the cache item is present until absent or cancelled 298 go func() { 299 for { 300 select { 301 case <-cancelChan: 302 return 303 default: 304 if !serverAds.Has(mockServerAd) { 305 deletedChan <- 1 306 return 307 } 308 } 309 } 310 }() 311 312 // Wait for 3s to check if the expired cache item is evicted 313 select { 314 case <-deletedChan: 315 require.True(t, true) 316 case <-time.After(3 * time.Second): 317 cancelChan <- 1 318 require.False(t, true, "Cache didn't evict expired item") 319 } 320 }) 321 }