Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add pagination, filtering and sort to more API endpoints #12186

Merged
merged 13 commits into from
Mar 9, 2022

Conversation

lgfa29
Copy link
Contributor

@lgfa29 lgfa29 commented Mar 3, 2022

This PR builds on previous PR s(#11648, #12034, #12128, #12090) to add pagination, filtering, and sorting to the following endpoints:

  • /v1/acl/tokens
  • /v1/allocations
  • /v1/jobs
  • /v1/volumes

Adding sort turned out to be more complex for job and volumes because their IDs are not globally unique, they need to use a CompoundIndex with namespace and ID. Adding CreateIndex as an additional component of this index resulted in an error when trying to do prefix query, so reverse sorting is not supported /v1/jobs and /v1/volumes.

This PR also renamed the Ascending flag to Reverse so its meaning is more generic. Ascending assumes that the default order is descending, but that may not always be the case.

It also refactors the paginator (more details in 1a5d07c) and the implementation of wildcard namespace listing for /v1/jobs and /v1/allocations to avoid having two different methods that are almost the same.

Documentation will be provided in #12094

Closes #11742
Closes #7118


Note to reviewers: it's probably better to review by commit, the work of implementing additional endpoints is somewhat repetitive, and some of the changes resulted in a significant diff, but are easier to understand in isolation.

lgfa29 and others added 7 commits March 3, 2022 17:27
As more endpoints started using the API response paginator, new patterns
started to emerge that required either more flexible configuration or a
lot of re-used code.

This commit splits the paginator into three logical components:

- The Paginator is reponsible for splitting the values of an iterator
  into chunks of the requested size.
- The Tokenizer returns a page token that is used to uniquely identify
  elements of the iterator.
- The Filter is used to decide if a given element should be added to the
  page or skipped altogether.

The Paginator implementation has returned, more or less, to its initial
clean design and implementation.

Additional logic and flow customization can be provided by implementing
the Tokenizer and Filter interfaces.

One Tokenizer implementation is provided (StructsTokenizer) which can be
configured to emit tokens using common fields found in structs, such as
ID, Namespace, and CreateIndex.

The go-bexpr Evaluator implements the Filter interface, so it can be
used directly. Another implementation provided is the NamespaceFilter,
which can be used to apply filtering logic based on the namespaces the
ACL token used for the request is allowed to access.
It's currently not possible to provide consistent sorting by CreateIndex
to this endpoint because it requires a Namespace and ID to be able to
uniquely identify a volume.

This reduces the type of go-memdb indexes that can be created where the
raft CreateIndex would be in a position that would allow sorting with
prefix match.
It's currently not possible to provide consistent sorting by CreateIndex
to this endpoint because it requires a Namespace and ID to be able to
uniquely identify a job.

This reduces the type of go-memdb indexes that can be created where the
raft CreateIndex would be in a position that would allow sorting with
prefix match.
Co-authored-by: Seth Hoenig <seth.a.hoenig@gmail.com>
Refactor the allocations list endpoint to merge the listAllNamespaces
special case into the normal List function.

Co-authored-by: Seth Hoenig <seth.a.hoenig@gmail.com>
Refactor the jobs list endpoint to merge the listAllNamespaces special
case into the normal List function.
Using reverse provides a more generic intention that the results should
be returned in the reverse of the default order instead of assuming any
particular order.
Copy link
Member

@shoenig shoenig left a comment

Choose a reason for hiding this comment

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

LGTM - no blockers, just rambling thoughts. We could refactor this until the end of time, and then start all over once generics get here.

.changelog/12186.txt Show resolved Hide resolved
// Currently only supported by evaluations.List and deployments.list endpoints.
Ascending bool
// Currently only supported by specific endpoints.
Reverse bool
Copy link
Member

Choose a reason for hiding this comment

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

Thanks for knocking this out!

}
}

func (it StructsTokenizer) GetToken(raw interface{}) string {
Copy link
Member

Choose a reason for hiding this comment

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

Not a blocker, just thinking: it seems like we could boil this down to a GetToken() string interface that is implemented by the types that otherwise implement GetID, GetNamespace, and/or GetCreateIndex. And then we'd just call that, instead of building the same value dynamically via StructsTokenizer.

Copy link
Member

Choose a reason for hiding this comment

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

I floated something similar to @lgfa29 in slack and he pointed out that the specific token we want is set by the caller and not by the type. So you'd end up with something like GetToken(byNamespace, byID, byCreateIndex bool) string or GetToken(tokenOpts) which doesn't feel a lot better.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yup, I couldn't find a nice interface since the token format depends on the query params used in the request. The struct doesn't know which format it should use by itself.

nomad/state/paginator/tokenizer.go Outdated Show resolved Hide resolved
Comment on lines 3217 to 3219
// Evals returns an iterator over all the evaluations in ascending or descending
// order of CreationIndex as determined by the ascending parameter.
func (s *StateStore) Evals(ws memdb.WatchSet, ascending bool) (memdb.ResultIterator, error) {
func (s *StateStore) Evals(ws memdb.WatchSet, reverse bool) (memdb.ResultIterator, error) {
Copy link
Member

Choose a reason for hiding this comment

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

For the docstring, it's the reverse parameter now.

But seeing this API makes me wonder if we should have a const we can use for clarity at the call-site. Something like:

const SortedReverse = true
const Sorted = false

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah good call, that would avoid mysterious boolean args, thanks! 🙂

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I called it SortedDefault because just store.Evals(ws, state.Sorted) felt like there would be a Unsorted option 😅

I also didn't create a custom type because this is just a boolean that would often be loaded from somewhere else (like a request). So it felt a bit much to require type casting.

Copy link
Member

@schmichael schmichael Mar 8, 2022

Choose a reason for hiding this comment

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

Not to nitpick, but I do hate bool parameters as you can rarely tell what they're for at the call site. Here's an alternative similar to Tim's if you still have energy left to mess with it:

type SortOrder bool
const SortAscending  SortOrder = true
const SortDescending SortOrder = false

func (s *StateStore) Eval(ws memdb.WatchSet, sort SortOrder) (memdb.ResultIterator, error) { /* ... */ }

func SomeCallSite() {
  iter, err := state.Eval(ws, SortAscending)

  // ...as opposed to this without the const & type def:
  iter, err := state.Eval(ws, true)
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, that's what I started doing. I can follow through with that.

I also notice while writing the docs that it's super confusing that reverse only works with certain combination of query params, so I'm going to make it work in all cases. The good thing about renaming it to reverse is that it just means using GetReverse instead of Get, no need to mess with the index schema to get some kind of specific order out 🙂

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually, scratch that last part. It would add a lot of extra code (mainly tests, but still) to this PR that is already pretty big. I will raise smaller follow-up PRs for each endpoint.

@tgross @schmichael I used a named type value for setting the order in 6784b79 if you would like to take another look.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ugh..sorry for the early ping, I'm working on fixing the tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, tests are passing now. I accidentally inverted the values of SortDefault and SortReverse 😅

New commit with the changes is cb8c6d0

}
}

func (it StructsTokenizer) GetToken(raw interface{}) string {
Copy link
Member

Choose a reason for hiding this comment

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

I floated something similar to @lgfa29 in slack and he pointed out that the specific token we want is set by the caller and not by the type. So you'd end up with something like GetToken(byNamespace, byID, byCreateIndex bool) string or GetToken(tokenOpts) which doesn't feel a lot better.

@@ -1,4 +1,4 @@
package state
package paginator
Copy link
Member

Choose a reason for hiding this comment

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

Good call moving this out to its own package

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I debated whether or not it should still be in nomad/state or just nomad, but I figure there isn't really much difference for now.

nomad/state/paginator/paginator.go Outdated Show resolved Hide resolved
@github-actions
Copy link

I'm going to lock this pull request because it has been closed for 120 days ⏳. This helps our maintainers find and focus on the active contributions.
If you have found a problem that seems related to this change, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Oct 25, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
theme/api HTTP API and SDK issues
Projects
None yet
Development

Successfully merging this pull request may close these issues.

sort-by fields for paginated List RPCs Paging for CSI volumes endpoint
4 participants