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  }