Allow reuse of cache indexes (#20562)

Previously calling `index.New` would return an object with the index information such as the Indexer, whether it was required, and the name of the index as well as a radix tree to store indexed data.

Now the main `Index` type doesn’t contain the radix tree for indexed data. Instead the `IndexedData` method can be used to combine the main `Index` with a radix tree in the `IndexedData` structure.

The cache still only allows configuring the `Index` type and will invoke the `IndexedData` method on the provided indexes to get the structure that the cache can use for actual data management.

All of this makes it now safe to reuse the `index.Index` types.
This commit is contained in:
Matt Keeler 2024-02-09 13:00:21 -05:00 committed by GitHub
parent 6708e88ec9
commit 6c4b83c119
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 135 additions and 22 deletions

View File

@ -243,6 +243,55 @@ func TestPrefixReferenceOrIDFromArgs(t *testing.T) {
}) })
} }
func TestMaybePrefixReferenceOrIDFromArgs(t *testing.T) {
ref := &pbresource.Reference{
Type: &pbresource.Type{
Group: "test",
GroupVersion: "v1",
Kind: "fake",
},
Tenancy: &pbresource.Tenancy{
Partition: "default",
},
}
t.Run("invalid length", func(t *testing.T) {
// the second arg will cause a validation error
val, err := MaybePrefixReferenceOrIDFromArgs(ref, IndexQueryOptions{Prefix: false}, 3)
require.Nil(t, val)
require.Error(t, err)
})
t.Run("invalid type arg 1", func(t *testing.T) {
val, err := MaybePrefixReferenceOrIDFromArgs("string type unexpected", IndexQueryOptions{Prefix: true})
require.Nil(t, val)
require.Error(t, err)
})
t.Run("invalid type arg 2", func(t *testing.T) {
val, err := MaybePrefixReferenceOrIDFromArgs(ref, "string type unexpected")
require.Nil(t, val)
require.Error(t, err)
})
t.Run("ok single arg", func(t *testing.T) {
val, err := MaybePrefixReferenceOrIDFromArgs(ref)
require.NoError(t, err)
require.Equal(t, IndexFromRefOrID(ref), val)
})
t.Run("ok two args no prefix", func(t *testing.T) {
val, err := MaybePrefixReferenceOrIDFromArgs(ref, IndexQueryOptions{Prefix: false})
require.NoError(t, err)
require.Equal(t, IndexFromRefOrID(ref), val)
})
t.Run("ok two args with prefix", func(t *testing.T) {
val, err := MaybePrefixReferenceOrIDFromArgs(ref, IndexQueryOptions{Prefix: true})
require.NoError(t, err)
require.Equal(t, PrefixIndexFromRefOrID(ref), val)
})
}
func TestSingleValueFromArgs(t *testing.T) { func TestSingleValueFromArgs(t *testing.T) {
injectedError := errors.New("injected test error") injectedError := errors.New("injected test error")

View File

@ -12,7 +12,6 @@ type Index struct {
name string name string
required bool required bool
indexer MultiIndexer indexer MultiIndexer
tree *iradix.Tree[[]*pbresource.Resource]
} }
func New(name string, i Indexer, opts ...IndexOption) *Index { func New(name string, i Indexer, opts ...IndexOption) *Index {
@ -36,7 +35,6 @@ func New(name string, i Indexer, opts ...IndexOption) *Index {
idx := &Index{ idx := &Index{
name: name, name: name,
indexer: multiIndexer, indexer: multiIndexer,
tree: iradix.New[[]*pbresource.Resource](),
} }
for _, opt := range opts { for _, opt := range opts {
@ -50,7 +48,21 @@ func (i *Index) Name() string {
return i.name return i.name
} }
func (i *Index) Txn() Txn { // IndexedData combines the Index with an radix tree for index and resource storage.
func (i *Index) IndexedData() *IndexedData {
return &IndexedData{
Index: i,
tree: iradix.New[[]*pbresource.Resource](),
}
}
// IndexedData is a wrapper around an Index and an radix tree for index and resource storage.
type IndexedData struct {
*Index
tree *iradix.Tree[[]*pbresource.Resource]
}
func (i *IndexedData) Txn() Txn {
return &txn{ return &txn{
inner: i.tree.Txn(), inner: i.tree.Txn(),
index: i, index: i,

View File

@ -10,23 +10,24 @@ import (
"github.com/hashicorp/consul/internal/controller/cache/index/indexmock" "github.com/hashicorp/consul/internal/controller/cache/index/indexmock"
"github.com/hashicorp/consul/internal/resource/resourcetest" "github.com/hashicorp/consul/internal/resource/resourcetest"
"github.com/hashicorp/consul/proto-public/pbresource" "github.com/hashicorp/consul/proto-public/pbresource"
"github.com/hashicorp/consul/proto/private/prototest"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
type testSingleIndexer struct{} type testSingleIndexer struct{}
func (testSingleIndexer) FromArgs(args ...any) ([]byte, error) { func (testSingleIndexer) FromArgs(args ...any) ([]byte, error) {
return ReferenceOrIDFromArgs(args) return ReferenceOrIDFromArgs(args...)
} }
func (testSingleIndexer) FromResource(*pbresource.Resource) (bool, []byte, error) { func (testSingleIndexer) FromResource(res *pbresource.Resource) (bool, []byte, error) {
return false, nil, nil return true, IndexFromRefOrID(res.Id), nil
} }
type testMultiIndexer struct{} type testMultiIndexer struct{}
func (testMultiIndexer) FromArgs(args ...any) ([]byte, error) { func (testMultiIndexer) FromArgs(args ...any) ([]byte, error) {
return ReferenceOrIDFromArgs(args) return ReferenceOrIDFromArgs(args...)
} }
func (testMultiIndexer) FromResource(*pbresource.Resource) (bool, [][]byte, error) { func (testMultiIndexer) FromResource(*pbresource.Resource) (bool, [][]byte, error) {
@ -135,3 +136,54 @@ func TestSingleIndexWrapper(t *testing.T) {
require.Equal(t, []byte{1, 2, 3}, vals[0]) require.Equal(t, []byte{1, 2, 3}, vals[0])
}) })
} }
func TestIndexReuse(t *testing.T) {
rtype := &pbresource.Type{
Group: "test",
GroupVersion: "v1",
Kind: "fake",
}
id := resourcetest.Resource(rtype, "foo").ID()
res1 := resourcetest.ResourceID(id).Build()
res2 := resourcetest.ResourceID(id).WithStatus("foo", &pbresource.Status{
ObservedGeneration: "woo",
}).Build()
indexer := testSingleIndexer{}
// Verify that the indexer produces an identical index for both resources. If this
// isn't true then the rest of the checks we do don't actually prove that the
// two IndexedData objects have independent resource storage.
_, idx1, _ := indexer.FromResource(res1)
_, idx2, _ := indexer.FromResource(res2)
require.Equal(t, idx1, idx2)
// Create the index and two indepent indexed data storage objects
idx := New("test", indexer)
data1 := idx.IndexedData()
data2 := idx.IndexedData()
// Push 1 resource into each
txn := data1.Txn()
txn.Insert(res1)
txn.Commit()
txn = data2.Txn()
txn.Insert(res2)
txn.Commit()
// Verify that querying the first indexed data can only return the first resource
iter, err := data1.Txn().ListIterator(id)
require.NoError(t, err)
res := iter.Next()
prototest.AssertDeepEqual(t, res1, res)
require.Nil(t, iter.Next())
// Verify that querying the second indexed data can only return the second resource
iter, err = data2.Txn().ListIterator(id)
require.NoError(t, err)
res = iter.Next()
prototest.AssertDeepEqual(t, res2, res)
require.Nil(t, iter.Next())
}

View File

@ -12,7 +12,7 @@ import (
type txn struct { type txn struct {
inner *iradix.Txn[[]*pbresource.Resource] inner *iradix.Txn[[]*pbresource.Resource]
index *Index index *IndexedData
dirty bool dirty bool
} }

View File

@ -24,7 +24,7 @@ type txnSuite struct {
suite.Suite suite.Suite
indexer *indexmock.SingleIndexer indexer *indexmock.SingleIndexer
index *Index index *IndexedData
r1 *pbresource.Resource r1 *pbresource.Resource
r2 *pbresource.Resource r2 *pbresource.Resource
@ -34,7 +34,7 @@ type txnSuite struct {
func (suite *txnSuite) SetupTest() { func (suite *txnSuite) SetupTest() {
suite.indexer = indexmock.NewSingleIndexer(suite.T()) suite.indexer = indexmock.NewSingleIndexer(suite.T())
suite.index = New("test", suite.indexer, IndexRequired) suite.index = New("test", suite.indexer, IndexRequired).IndexedData()
suite.r1 = testResource("r1") suite.r1 = testResource("r1")
suite.r2 = testResource("r2") suite.r2 = testResource("r2")

View File

@ -131,7 +131,7 @@ func (suite *decodedSingleIndexerSuite) TestIntegration() {
dec := resourcetest.MustDecode[*pbdemo.Album](suite.T(), res) dec := resourcetest.MustDecode[*pbdemo.Album](suite.T(), res)
idx := DecodedSingleIndexer("test", suite.args.Execute, suite.indexer.Execute) idx := DecodedSingleIndexer("test", suite.args.Execute, suite.indexer.Execute).IndexedData()
suite.indexer.EXPECT(). suite.indexer.EXPECT().
Execute(dec). Execute(dec).
@ -263,7 +263,7 @@ func (suite *decodedMultiIndexerSuite) TestIntegration() {
dec := resourcetest.MustDecode[*pbdemo.Album](suite.T(), res) dec := resourcetest.MustDecode[*pbdemo.Album](suite.T(), res)
idx := DecodedMultiIndexer("test", suite.args.Execute, suite.indexer.Execute) idx := DecodedMultiIndexer("test", suite.args.Execute, suite.indexer.Execute).IndexedData()
suite.indexer.EXPECT(). suite.indexer.EXPECT().
Execute(dec). Execute(dec).

View File

@ -17,7 +17,7 @@ import (
) )
func TestIDIndex(t *testing.T) { func TestIDIndex(t *testing.T) {
idx := IDIndex("test", index.IndexRequired) idx := IDIndex("test", index.IndexRequired).IndexedData()
r1 := resourcetest.Resource(pbdemo.AlbumType, "foo"). r1 := resourcetest.Resource(pbdemo.AlbumType, "foo").
WithTenancy(&pbresource.Tenancy{ WithTenancy(&pbresource.Tenancy{
@ -39,7 +39,7 @@ func TestIDIndex(t *testing.T) {
} }
func TestOwnerIndex(t *testing.T) { func TestOwnerIndex(t *testing.T) {
idx := OwnerIndex("test", index.IndexRequired) idx := OwnerIndex("test", index.IndexRequired).IndexedData()
r1 := resourcetest.Resource(pbdemo.AlbumType, "foo"). r1 := resourcetest.Resource(pbdemo.AlbumType, "foo").
WithTenancy(&pbresource.Tenancy{ WithTenancy(&pbresource.Tenancy{
@ -70,7 +70,7 @@ func TestOwnerIndex(t *testing.T) {
func TestSingleIDOrRefIndex(t *testing.T) { func TestSingleIDOrRefIndex(t *testing.T) {
getRef := indexersmock.NewGetSingleRefOrID(t) getRef := indexersmock.NewGetSingleRefOrID(t)
idx := SingleIDOrRefIndex("test", getRef.Execute) idx := SingleIDOrRefIndex("test", getRef.Execute).IndexedData()
r1 := resourcetest.Resource(pbdemo.AlbumType, "foo").Build() r1 := resourcetest.Resource(pbdemo.AlbumType, "foo").Build()
ref := &pbresource.Reference{ ref := &pbresource.Reference{

View File

@ -43,7 +43,7 @@ func TestRefOrIDIndex(t *testing.T) {
refs := indexersmock.NewRefOrIDFetcher[*pbdemo.Album, *pbresource.Reference](t) refs := indexersmock.NewRefOrIDFetcher[*pbdemo.Album, *pbresource.Reference](t)
idx := RefOrIDIndex("test", refs.Execute) idx := RefOrIDIndex("test", refs.Execute).IndexedData()
refs.EXPECT().Execute(dec). refs.EXPECT().Execute(dec).
Return([]*pbresource.Reference{ref1, ref2}). Return([]*pbresource.Reference{ref1, ref2}).
@ -90,7 +90,7 @@ func TestBoundRefsIndex(t *testing.T) {
}). }).
Build() Build()
idx := BoundRefsIndex[*pbmesh.ComputedRoutes]("test") idx := BoundRefsIndex[*pbmesh.ComputedRoutes]("test").IndexedData()
txn := idx.Txn() txn := idx.Txn()
require.NoError(t, txn.Insert(r1)) require.NoError(t, txn.Insert(r1))

View File

@ -18,16 +18,16 @@ type kindIndices struct {
it unversionedType it unversionedType
indices map[string]*index.Index indices map[string]*index.IndexedData
} }
func newKindIndices() *kindIndices { func newKindIndices() *kindIndices {
kind := &kindIndices{ kind := &kindIndices{
indices: make(map[string]*index.Index), indices: make(map[string]*index.IndexedData),
} }
// add the id index // add the id index
kind.indices[IDIndex] = indexers.IDIndex(IDIndex, index.IndexRequired) kind.indices[IDIndex] = indexers.IDIndex(IDIndex, index.IndexRequired).IndexedData()
return kind return kind
} }
@ -38,7 +38,7 @@ func (k *kindIndices) addIndex(i *index.Index) error {
return DuplicateIndexError{name: i.Name()} return DuplicateIndexError{name: i.Name()}
} }
k.indices[i.Name()] = i k.indices[i.Name()] = i.IndexedData()
return nil return nil
} }
@ -178,7 +178,7 @@ func (k *kindIndices) delete(r *pbresource.Resource) error {
return nil return nil
} }
func (k *kindIndices) getIndex(name string) (*index.Index, error) { func (k *kindIndices) getIndex(name string) (*index.IndexedData, error) {
idx, ok := k.indices[name] idx, ok := k.indices[name]
if !ok { if !ok {
return nil, CacheTypeError{err: IndexNotFoundError{name: name}, it: k.it} return nil, CacheTypeError{err: IndexNotFoundError{name: name}, it: k.it}