|
9 | 9 | package rest |
10 | 10 |
|
11 | 11 | import ( |
| 12 | + "context" |
| 13 | + "errors" |
12 | 14 | "fmt" |
13 | 15 | "maps" |
14 | 16 | "net/http" |
@@ -1935,3 +1937,90 @@ func TestClusterCompatRefreshAndStopUntrackVanishedBucket(t *testing.T) { |
1935 | 1937 | require.NotContains(t, clusterCompat.trackedBucketList(), fakeBucket, "vanished bucket should be untracked") |
1936 | 1938 | } |
1937 | 1939 | } |
| 1940 | + |
| 1941 | +// fakeVanishingBootstrapConnection simulates a bucket that no longer exists in the cluster: |
| 1942 | +// GetMetadataDocument always fails, and any bucket absent from knownBuckets is then classified |
| 1943 | +// as errBucketDoesNotExist by getGatewayRegistry. Only the methods refreshNodeRegistrations |
| 1944 | +// actually calls are implemented; anything else panics via the nil embedded interface. |
| 1945 | +type fakeVanishingBootstrapConnection struct { |
| 1946 | + base.BootstrapConnection |
| 1947 | + knownBuckets []string |
| 1948 | +} |
| 1949 | + |
| 1950 | +func (f *fakeVanishingBootstrapConnection) GetConfigBuckets(_ context.Context) ([]string, error) { |
| 1951 | + return f.knownBuckets, nil |
| 1952 | +} |
| 1953 | + |
| 1954 | +func (f *fakeVanishingBootstrapConnection) GetMetadataDocument(_ context.Context, _, _ string, _ any) (uint64, error) { |
| 1955 | + return 0, errors.New("simulated: unable to read metadata document") |
| 1956 | +} |
| 1957 | + |
| 1958 | +// newTestClusterCompatManager builds a clusterCompatManager backed by a fake bootstrap |
| 1959 | +// connection, skipping the full RestTester/ServerContext setup — refreshNodeRegistrations only |
| 1960 | +// touches fields that are safe at their zero value here (NodeUID, Config, _databases(Lock)). |
| 1961 | +func newTestClusterCompatManager(knownBuckets []string, trackedBuckets ...string) *clusterCompatManager { |
| 1962 | + sc := &ServerContext{ |
| 1963 | + Config: &StartupConfig{}, |
| 1964 | + NodeUID: "test-node", |
| 1965 | + BootstrapContext: &bootstrapContext{ |
| 1966 | + Connection: &fakeVanishingBootstrapConnection{knownBuckets: knownBuckets}, |
| 1967 | + }, |
| 1968 | + } |
| 1969 | + tracked := make(map[string]struct{}, len(trackedBuckets)) |
| 1970 | + for _, b := range trackedBuckets { |
| 1971 | + tracked[b] = struct{}{} |
| 1972 | + } |
| 1973 | + return &clusterCompatManager{sc: sc, trackedBuckets: tracked} |
| 1974 | +} |
| 1975 | + |
| 1976 | +// TestClusterCompatRefreshNodeRegistrationsAllBucketsVanished verifies that when every tracked |
| 1977 | +// bucket vanishes in the same pass, refreshNodeRegistrations reports success with empty |
| 1978 | +// results rather than an error — otherwise Refresh would never clear the cache, since it stops |
| 1979 | +// calling refreshNodeRegistrations once trackedBuckets is empty. |
| 1980 | +func TestClusterCompatRefreshNodeRegistrationsAllBucketsVanished(t *testing.T) { |
| 1981 | + ctx := base.TestCtx(t) |
| 1982 | + m := newTestClusterCompatManager(nil, "vanished-bucket-1", "vanished-bucket-2") |
| 1983 | + |
| 1984 | + nodes, freeze, preCCVAwareNodes, hwm, err := m.refreshNodeRegistrations(ctx) |
| 1985 | + require.NoError(t, err, "all-vanished tracked buckets must not be reported as a failure") |
| 1986 | + assert.Empty(t, nodes) |
| 1987 | + assert.Nil(t, freeze) |
| 1988 | + assert.Empty(t, preCCVAwareNodes) |
| 1989 | + assert.Nil(t, hwm) |
| 1990 | + assert.Empty(t, m.trackedBucketList(), "vanished buckets must be untracked") |
| 1991 | +} |
| 1992 | + |
| 1993 | +// TestClusterCompatRefreshAllBucketsVanishedClearsCache verifies that Refresh actually clears a |
| 1994 | +// previously-cached, now-stale cluster compat version once every tracked bucket has vanished. |
| 1995 | +func TestClusterCompatRefreshAllBucketsVanishedClearsCache(t *testing.T) { |
| 1996 | + ctx := base.TestCtx(t) |
| 1997 | + m := newTestClusterCompatManager(nil, "vanished-bucket") |
| 1998 | + |
| 1999 | + staleVersion := base.NewClusterCompatVersion(3, 3) |
| 2000 | + m.cachedVersion = &staleVersion |
| 2001 | + m.cachedNodes = map[string]base.ClusterCompatVersion{"some-other-node": staleVersion} |
| 2002 | + |
| 2003 | + m.Refresh(ctx) |
| 2004 | + |
| 2005 | + assert.Nil(t, m.getCachedVersion(), "cache must be cleared once no buckets remain tracked") |
| 2006 | + assert.Empty(t, m.NodeVersions()) |
| 2007 | + assert.Empty(t, m.trackedBucketList()) |
| 2008 | +} |
| 2009 | + |
| 2010 | +// TestClusterCompatRefreshNodeRegistrationsPartialVanishKeepsError verifies the fix is scoped |
| 2011 | +// to the all-vanished case: if a vanished bucket is mixed with a bucket that fails for another |
| 2012 | +// reason, the result is still reported as an error so Refresh keeps the old cached state |
| 2013 | +// instead of caching an incomplete view. |
| 2014 | +func TestClusterCompatRefreshNodeRegistrationsPartialVanishKeepsError(t *testing.T) { |
| 2015 | + ctx := base.TestCtx(t) |
| 2016 | + // flaky-bucket exists (per GetConfigBuckets) but every read against it fails, so it's a |
| 2017 | + // real, non-vanished failure. vanished-bucket doesn't exist, so it's classified as vanished. |
| 2018 | + m := newTestClusterCompatManager([]string{"flaky-bucket"}, "vanished-bucket", "flaky-bucket") |
| 2019 | + |
| 2020 | + _, _, _, _, err := m.refreshNodeRegistrations(ctx) |
| 2021 | + require.Error(t, err, "a mix of vanished and non-vanished failures must still be reported as an error") |
| 2022 | + |
| 2023 | + tracked := m.trackedBucketList() |
| 2024 | + assert.NotContains(t, tracked, "vanished-bucket", "vanished bucket must still be untracked") |
| 2025 | + assert.Contains(t, tracked, "flaky-bucket", "non-vanished failures must not untrack the bucket") |
| 2026 | +} |
0 commit comments