Skip to content

Add sort index trees to unified resource cache#32709

Merged
avatus merged 1 commit intomasterfrom
avatus/unified_resource_indexes
Oct 5, 2023
Merged

Add sort index trees to unified resource cache#32709
avatus merged 1 commit intomasterfrom
avatus/unified_resource_indexes

Conversation

@avatus
Copy link
Copy Markdown
Contributor

@avatus avatus commented Sep 28, 2023

This adds sort indexes to not only improve the performance of getting resources from our unified cache, but also to support #32077 being able to get a resource from the cache by the keys we use on the frontend (name/type) of the "contained" resource instead of the container resources we store them as (appserver, dbserver, etc).

I'm not married to any implementations here but the main parts I tried to solve were:

  1. store our resources in a map instead with the same unique key we use in the current btree, name/type (derived from resourceKey).
  2. Create a tree for each sort type we have ("name" and "type" currently). These items will be sorted by their key (generated by resourceSortKey depending on the type) with the value being the resourceKey from above so we can pull from the resources map.
  3. be able to update our sorted trees that have the 'contained' keys when we receive an event that doesn't include the entire resource (like opDelete)

this removes the need for FakePaginate by passing in our matchers and rbac to each iteration of Ascend/Descend

// benchmarkListUnifiedResources
-// FakePaginate
-// ok      github.com/gravitational/teleport/lib/auth      25.135s

+// IterateUnifiedResources
+// ok      github.com/gravitational/teleport/lib/auth      2.878s

So far, all my tests pass with minimal changes (mostly to the input instead of the output) and the web doesn't even have to care about these changes.

i might be lost in the sauce on some parts

Comment thread lib/auth/auth_with_roles.go Outdated
Comment on lines 1638 to 1639
switch r := resource.(type) {
case types.Server:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of using this switch statement can we create an access checker that works for all resources via newResourceAccessChecker like we do in ListResources?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah thats way better. Will update

Comment thread lib/services/unified_resource.go Outdated
Comment on lines +154 to +155
nameSortKey := resourceSortKey(resource, SortByName)
typeSortKey := resourceSortKey(resource, SortByKind)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like resourceSortKey is always called twice in a row, once for each sort option, should we update it to just return both keys in a single invocation?

Comment thread lib/services/unified_resource.go Outdated
Comment on lines 158 to 167
if _, ok := cache.nameTree.Delete(&item{Key: nameSortKey}); !ok {
return trace.NotFound("key %q is not found in unified cache name sort tree", string(nameSortKey))
}
if _, ok := cache.typeTree.Delete(&item{Key: typeSortKey}); !ok {
return trace.NotFound("key %q is not found in unified cache type sort tree", string(typeSortKey))
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we return early if one of the deletes fails? Should we attempt all deletes and return an aggregated error?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah i suppose if the resource isn't found in one it probably won't be found in the other (my guess is it wouldn't be found due to name change and they both include the name).
I think I'm more a fan of returning early but I want to noodle on it for a bit

Comment thread lib/services/unified_resource.go Outdated
Comment on lines +192 to +198
err := c.read(ctx, func(cache *UnifiedResourceCache) error {
tree, err := cache.getSortTree(req.SortBy.Field)
if err != nil {
return trace.Wrap(err, "getting sort tree")
}

iterateFunc := func(item *item) bool {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this makes it any more/less readable but you could store the correct function in a variable first and then call it inline with the iterate function.

Suggested change
err := c.read(ctx, func(cache *UnifiedResourceCache) error {
tree, err := cache.getSortTree(req.SortBy.Field)
if err != nil {
return trace.Wrap(err, "getting sort tree")
}
iterateFunc := func(item *item) bool {
err := c.read(ctx, func(cache *UnifiedResourceCache) error {
tree, err := cache.getSortTree(req.SortBy.Field)
if err != nil {
return trace.Wrap(err, "getting sort tree")
}
var iterateRange func(lessOrEqual, greaterThan *item, iterator btree.ItemIteratorG[*item])
var endKey []byte
if req.SortBy.IsDesc {
iterateRange = tree.DescendRange
endKey = backend.Key(prefix)
} else {
iterateRange = tree.AscendRange
endKey = backend.RangeEnd(backend.Key(prefix))
}
iterateRange(&item{Key: startKey}, &item{Key: endKey}, func(item *item) bool {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd argue your suggestion is more readable if only for the fact that the code is read in the order it'd be executed (instead of my way which you'd see the iterate func and then it actually executes underneath it).

I was going for something like this anyway.

@rosstimothy rosstimothy self-requested a review September 28, 2023 15:53
@avatus avatus marked this pull request as ready for review September 29, 2023 01:05
Comment thread lib/services/unified_resource.go Outdated
// get resource from resource map
resourceFromMap := cache.resources[item.Value]
if resourceFromMap == nil {
// continue the ascend. maybe we should throw an error?
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If nil entries are not valid you could instead check if the resource exists in the map and if not return and keep ascending? And if the resource is nil then return an error

name = r.GetHostname() + "/" + r.GetName()
kind = types.KindNode
case types.AppServer:
app := r.GetApp()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this and the types.DatabaseServer case, should app or db ever be nil or would that be invalid?

Copy link
Copy Markdown
Contributor Author

@avatus avatus Oct 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was originally written to make "delete" not break if we received a resource header instead of the resource. It's vestigial and we can remove it, as our resource map will always have the contained resource available. (Or I suppose the check it fine to make sure we don't get a panic)

Comment thread lib/auth/auth_with_roles.go Outdated
return false, trace.Wrap(err)
}
match, err := services.MatchResourceByFilters(resource, filter, nil)
return match, err
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return match, err
return match, trace.Wrap(err)

Comment thread lib/services/unified_resource.go Outdated
c.nameTree.Clear(false)
c.typeTree.Clear(false)
// clear the resource map as well
c.resources = make(map[string]resource)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is only going to be backported to v14 we could use the new clear builtin

Suggested change
c.resources = make(map[string]resource)
clear(c.resources)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh thats sick

Copy link
Copy Markdown
Contributor Author

@avatus avatus Oct 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this, it actually exposed a bug. When I was putting resources, using that make was building a map that I had forgotten to build in our fallbackCache so it paniced after this change because it couldn't clear something that didnt exist. Now I added the map during the fallbackCache creation and it works great!

Comment on lines 697 to +703
const (
prefix = "unified_resource"
)

const (
SortByName string = "name"
SortByKind string = "kind"
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: group the const blocks. Are SortByName and SortByKind used by anything external? If not can we unexport them. If they are can we add go docs for them?

Suggested change
const (
prefix = "unified_resource"
)
const (
SortByName string = "name"
SortByKind string = "kind"
)
const (
prefix = "unified_resource"
SortByName string = "name"
SortByKind string = "kind"
)

Comment on lines +325 to +328
type resourceSortKey struct {
ByName []byte
ByType []byte
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: You could unexport the fields since the type isn't exported

Suggested change
type resourceSortKey struct {
ByName []byte
ByType []byte
}
type resourceSortKey struct {
byName []byte
byType []byte
}

@public-teleport-github-review-bot public-teleport-github-review-bot Bot removed the request for review from fspmarshall October 5, 2023 12:41
@avatus avatus force-pushed the avatus/unified_resource_indexes branch from 5518954 to d84bb77 Compare October 5, 2023 15:50
@avatus avatus enabled auto-merge October 5, 2023 15:51
@avatus avatus added this pull request to the merge queue Oct 5, 2023
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 5, 2023
@avatus avatus added this pull request to the merge queue Oct 5, 2023
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 5, 2023
@avatus avatus added this pull request to the merge queue Oct 5, 2023
Merged via the queue into master with commit 58b8a16 Oct 5, 2023
@avatus avatus deleted the avatus/unified_resource_indexes branch October 5, 2023 17:12
@public-teleport-github-review-bot
Copy link
Copy Markdown

@avatus See the table below for backport results.

Branch Result
branch/v14 Failed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants