@@ -30,6 +30,8 @@ import (
3030 "testing"
3131 "time"
3232
33+ "github.com/gin-gonic/gin"
34+ "github.com/google/uuid"
3335 "github.com/jellydator/ttlcache/v3"
3436 log "github.com/sirupsen/logrus"
3537 "github.com/stretchr/testify/assert"
@@ -1038,3 +1040,132 @@ func TestSendHeadReq(t *testing.T) {
10381040 assert .Nil (t , meta )
10391041 })
10401042}
1043+
1044+ // TestGenerateAvailabilityMaps verifies that the maps returned by generateAvailabilityMaps
1045+ // are keyed by ad.URL.String() rather than ad.Name. This is the key that availabilityWeightFn
1046+ // uses for lookup, so a mismatch would silently disable the availability axis of adaptive sort.
1047+ func TestGenerateAvailabilityMaps (t * testing.T ) {
1048+ setGinTestMode ()
1049+ t .Cleanup (test_utils .SetupTestLogging (t ))
1050+
1051+ // Save and restore global serverAds so we don't interfere with other tests.
1052+ oldAds := serverAds
1053+ t .Cleanup (func () {
1054+ cleanupMock ()
1055+ serverAds = oldAds
1056+ })
1057+ serverAds = ttlcache .New (ttlcache.WithTTL [string , * server_structs.Advertisement ](15 * time .Minute ))
1058+
1059+ makeCtx := func (method , reqPath string ) * gin.Context {
1060+ w := httptest .NewRecorder ()
1061+ c , _ := gin .CreateTestContext (w )
1062+ c .Request = httptest .NewRequest (method , reqPath , nil )
1063+ return c
1064+ }
1065+
1066+ originAd := server_structs.ServerAd {
1067+ URL : url.URL {Scheme : "https" , Host : "origin.example.com:8443" },
1068+ AuthURL : url.URL {Scheme : "https" , Host : "origin-auth.example.com:8444" },
1069+ Caps : server_structs.Capabilities {PublicReads : true },
1070+ Type : server_structs .OriginType .String (),
1071+ }
1072+ originAd .Initialize ("my-origin" )
1073+
1074+ cacheAd := server_structs.ServerAd {
1075+ URL : url.URL {Scheme : "https" , Host : "cache.example.com:8443" },
1076+ AuthURL : url.URL {Scheme : "https" , Host : "cache-auth.example.com:8444" },
1077+ Caps : server_structs.Capabilities {PublicReads : true },
1078+ Type : server_structs .CacheType .String (),
1079+ }
1080+ cacheAd .Initialize ("my-cache" )
1081+
1082+ bestNSAd := server_structs.NamespaceAdV2 {
1083+ Path : "/foo" ,
1084+ Caps : server_structs.Capabilities {PublicReads : true },
1085+ }
1086+ reqID := uuid .New ()
1087+
1088+ // When stat is skipped (neither CheckCachePresence nor CheckOriginPresence enabled),
1089+ // generateAvailabilityMaps assumes all servers are available and must key those maps
1090+ // by ad.URL.String() — not by ad.Name. Before the fix, the Name key meant
1091+ // availabilityWeightFn could never find a match, causing adaptive sort to treat all
1092+ // servers as equally available regardless of stat results.
1093+ t .Run ("skip-stat-path-maps-keyed-by-url" , func (t * testing.T ) {
1094+ server_utils .ResetTestState ()
1095+ require .NoError (t , param .Set (param .Director_CheckCachePresence .GetName (), false ))
1096+ require .NoError (t , param .Set (param .Director_CheckOriginPresence .GetName (), false ))
1097+
1098+ // Use an origin-redirect path so both skipped-stat branches execute.
1099+ ctx := makeCtx (http .MethodGet , "/api/v1.0/director/origin/foo/test.txt" )
1100+ oMap , cMap , err := generateAvailabilityMaps (
1101+ ctx ,
1102+ []server_structs.ServerAd {originAd },
1103+ []server_structs.ServerAd {cacheAd },
1104+ bestNSAd , reqID ,
1105+ )
1106+ require .NoError (t , err )
1107+
1108+ // Keys MUST be ad.URL.String() — availabilityWeightFn does availMap[ad.URL.String()].
1109+ assert .True (t , oMap [originAd .URL .String ()],
1110+ "origin availability map must be keyed by ad.URL.String() %q" , originAd .URL .String ())
1111+ assert .False (t , oMap [originAd .Name ],
1112+ "origin availability map must NOT be keyed by ad.Name %q" , originAd .Name )
1113+
1114+ assert .True (t , cMap [cacheAd .URL .String ()],
1115+ "cache availability map must be keyed by ad.URL.String() %q" , cacheAd .URL .String ())
1116+ assert .False (t , cMap [cacheAd .Name ],
1117+ "cache availability map must NOT be keyed by ad.Name %q" , cacheAd .Name )
1118+ })
1119+
1120+ // When stat is enabled and a server responds positively, the resulting map entry
1121+ // must still use ad.URL.String() as the key. Before the fix, a successful stat wrote
1122+ // ad.Name — again invisible to availabilityWeightFn.
1123+ t .Run ("stat-results-maps-keyed-by-url" , func (t * testing.T ) {
1124+ server_utils .ResetTestState ()
1125+ require .NoError (t , param .Set (param .Director_CheckCachePresence .GetName (), true ))
1126+ require .NoError (t , param .Set (param .Director_CheckOriginPresence .GetName (), false ))
1127+ require .NoError (t , param .Set (param .Director_StatTimeout .GetName (), 2 * time .Second ))
1128+ require .NoError (t , param .Set (param .Director_MinStatResponse .GetName (), 1 ))
1129+ require .NoError (t , param .Set (param .Director_MaxStatResponse .GetName (), 1 ))
1130+
1131+ // Mock HTTP server that returns 200 with Content-Length for every HEAD request.
1132+ mockSrv := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
1133+ w .Header ().Set ("Content-Length" , "10" )
1134+ w .WriteHeader (http .StatusOK )
1135+ }))
1136+ t .Cleanup (mockSrv .Close )
1137+
1138+ srvURL , err := url .Parse (mockSrv .URL )
1139+ require .NoError (t , err )
1140+
1141+ statCacheAd := server_structs.ServerAd {
1142+ URL : * srvURL ,
1143+ Caps : server_structs.Capabilities {PublicReads : true },
1144+ Type : server_structs .CacheType .String (),
1145+ }
1146+ statCacheAd .Initialize ("stat-cache" )
1147+
1148+ // Populate serverAds and statUtils so queryServersForObject can service the request.
1149+ serverAds .Set (statCacheAd .URL .String (),
1150+ & server_structs.Advertisement {ServerAd : statCacheAd },
1151+ ttlcache .DefaultTTL )
1152+ initMockStatUtils ()
1153+ t .Cleanup (cleanupMock )
1154+
1155+ // /api/v1.0/director/object paths are cache requests: shouldStatCaches → true,
1156+ // shouldStatOrigins → false (isCacheRequest && len(cAds) > 0).
1157+ ctx := makeCtx (http .MethodGet , "/api/v1.0/director/object/foo/test.txt" )
1158+ _ , cMap , err := generateAvailabilityMaps (
1159+ ctx ,
1160+ []server_structs.ServerAd {},
1161+ []server_structs.ServerAd {statCacheAd },
1162+ bestNSAd , reqID ,
1163+ )
1164+ require .NoError (t , err )
1165+
1166+ assert .True (t , cMap [statCacheAd .URL .String ()],
1167+ "cache map must be keyed by ad.URL.String() %q after a successful stat" , statCacheAd .URL .String ())
1168+ assert .False (t , cMap [statCacheAd .Name ],
1169+ "cache map must NOT be keyed by ad.Name %q after stat — this broke adaptive sort" , statCacheAd .Name )
1170+ })
1171+ }
0 commit comments