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
Support Pagination for ClusterGroupMembership #3183
Conversation
Codecov Report
@@ Coverage Diff @@
## main #3183 +/- ##
==========================================
+ Coverage 61.03% 63.34% +2.31%
==========================================
Files 268 268
Lines 26872 26907 +35
==========================================
+ Hits 16400 17043 +643
+ Misses 8654 7999 -655
- Partials 1818 1865 +47
Flags with carried forward coverage won't be shown. Click here to find out more.
|
e05f4de
to
3b80708
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Implementation looks good overall
4b603e9
to
b7464e1
Compare
pkg/apiserver/registry/networkpolicy/clustergroupmember/rest.go
Outdated
Show resolved
Hide resolved
pkg/apiserver/registry/networkpolicy/clustergroupmember/rest.go
Outdated
Show resolved
Hide resolved
|
LGTM overall, will let other reviewers take a look as well |
pkg/apis/controlplane/types.go
Outdated
|
|
||
| // PaginationGetOptions is used to retrieve page number and page limit info from the request. | ||
| type PaginationGetOptions struct { | ||
| metav1.TypeMeta `json:",inline"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
internal type doesn't need json tag as it will not be serialized
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keeping the jason tag as needed by +k8s:conversion-gen:explicit-from=net/url.Values to autogenerate.🤔
| @@ -58,6 +58,7 @@ func addKnownTypes(scheme *runtime.Scheme) error { | |||
| &newcontrolplane.NetworkPolicyStatus{}, | |||
| &newcontrolplane.NodeStatsSummary{}, | |||
| &newcontrolplane.ClusterGroupMembers{}, | |||
| &newcontrolplane.PaginationGetOptions{}, | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't need to add support for legacy APIs. They will be removed soon.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1. Actually I think we can remove them for v1.6. Let me see if I can open a PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👌 Sure, though keeping this before the removal PR gets merged to build successfully. Because during API initialization it looks for this type in group version. Tested that w/o this registration, controller would throw an error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
legacyAPI has been removed, you could rebase on main now.
pkg/apiserver/registry/networkpolicy/clustergroupmember/rest.go
Outdated
Show resolved
Hide resolved
pkg/apiserver/registry/networkpolicy/clustergroupmember/rest.go
Outdated
Show resolved
Hide resolved
pkg/apiserver/registry/networkpolicy/clustergroupmember/rest.go
Outdated
Show resolved
Hide resolved
| @@ -58,6 +58,7 @@ func addKnownTypes(scheme *runtime.Scheme) error { | |||
| &newcontrolplane.NetworkPolicyStatus{}, | |||
| &newcontrolplane.NodeStatsSummary{}, | |||
| &newcontrolplane.ClusterGroupMembers{}, | |||
| &newcontrolplane.PaginationGetOptions{}, | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1. Actually I think we can remove them for v1.6. Let me see if I can open a PR.
ec7d69f
to
37b123c
Compare
| @@ -58,6 +58,7 @@ func addKnownTypes(scheme *runtime.Scheme) error { | |||
| &newcontrolplane.NetworkPolicyStatus{}, | |||
| &newcontrolplane.NodeStatsSummary{}, | |||
| &newcontrolplane.ClusterGroupMembers{}, | |||
| &newcontrolplane.PaginationGetOptions{}, | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
legacyAPI has been removed, you could rebase on main now.
|
/test-all |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That doesn't have to be addressed in this PR, but it seems to me that we are doing a lot of repeated work every time a new page is requested: we build the entire slice of members, then sort it and then we index into it using the page number. I wonder if there is a way to implement it differently, while remaining stateless. Adding @tnqn to see if he has any insight on whether this is possible.
pkg/apiserver/registry/networkpolicy/clustergroupmember/rest.go
Outdated
Show resolved
Hide resolved
pkg/apiserver/registry/networkpolicy/clustergroupmember/rest.go
Outdated
Show resolved
Hide resolved
| @@ -162,3 +214,133 @@ func TestRESTGet(t *testing.T) { | |||
| assert.Equal(t, tt.expectedObj, actualGroupList) | |||
| } | |||
| } | |||
|
|
|||
| func TestRESTGetPagination(t *testing.T) { | |||
| tests := []struct { | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems you have no negative test cases (invalid query parameters for which the server must return an error). Would it make sense to add one or two of them?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've modified the test cases, (total 3 members) one of page 5, limit 2 where page index exceeds actual, one of page 1, limit -2 where limit is invalid.
If I understand your comment correctly, in both cases, the current design is to return full list instead of throwing an error. Let me know if returning an error works better here, but I checked K8s List pagination it only pages for limit>0, otherwise it's treated as normal.
For invalid parameters like "providing string for int"/"key not found", these are handled by the conversion function generated by +k8s:conversion-gen:explicit-from=net/url.Values, not tested in UT.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So there is no error either if page < 0?
| } else { | ||
| return true | ||
| } | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question about behaviour: If the user does not require pagination, the response is not sorted. On the other hand, if the user requires pagination, the response is sorted by Pod name or External entity. Is my understanding correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes that is the case, to reduce extra sorting when user does not require pagination.
| memberList.EffectiveMembers = memberList.EffectiveMembers[:pageInfo.pageLimit] | ||
| } | ||
| } else { | ||
| pageInfo.pageNumber = 0 // set to 0 in case given page number exceeds total pages |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I understand correctly if a user requires a page whose number is higher than the total number of pages they will get back the first page of results? Why not an empty page?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They will get back the entire result of members, as if pagination is not requested. In the response, totalPages is the number of pages based on user's correct limit, but pageNumber is set to 0 to indicate that no specific page is returned.
Currently to keep error handling consistent, all errors are handled by returning full list (added comment for func), let me know if full list or an empty page works best here. Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel this may cause client a bit complex to handle this situation. For example, if some members are removed between two page queries, leading to the second query get empty result, and we return the entire result of members, it would waste the bandwidth spent on transfering the whole data and client has to add a page number check to distinguish whether this is a legal response before displaying it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That use case makes total sense, I'll change it to empty result, thanks!
245da18
to
26adb90
Compare
@antoninbas I thought about caching the result but it seems too complicated to associate queries and clean up cache. But If we assume the pagination will be used in a situation that only the first few pages will be queried quite often, maybe an optimization worth to do is sorting only the first pageNumber*pageLimit (say k) instead of the entire slice (say N) to reduce the runtime from O(NlogN) to O(klogN)? |
|
@tnqn and is the sorting actually required? Given that for this API, the user wouldn't expect any specific ordering of the results, there should be no need to sort as long as |
Trying to understand |
|
@antoninbas I suppose the API will be used for listing members of a group in web application, it's possible that there are some updates to the members when user requests next page. If we don't sort it at all, the result of next page may be totally discontinuous from previous page even there is just one member removed/added to the group because the map may be resized. If we sort it, the pages could be continuous overall with a few duplicate or missing members, which is very common in list page (For example, if someone opens a new PR when I'm viewing page 1, I may still see the last PR of page 1 when I view page 2). @qiyueyao I mean sorting the requested members only via something like heap sort. For example, when user requests page 1, pageLimit is 10, and the total members is 1000. In current implementation, the cost is sorting 1000 items => O(1000 * log1000). If we use heap sort, we only need to build a heap and get the least or largest 10 items => O(1000+10*log1000). |
5bf8d77
to
c0e0f39
Compare
Understood. If we have the use case for heap sort, I could open an issue to track, and follow up with a new PR. Let me know if that works, thanks. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
| if !ok { | ||
| getOptions = &controlplane.PaginationGetOptions{Page: 0, Limit: 0} | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO, if ok is false, it could be treated as an internal server error. It should not be possible based on the K8s code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated the error handling cases including this ok and limit | page < 0.
| @@ -162,3 +214,133 @@ func TestRESTGet(t *testing.T) { | |||
| assert.Equal(t, tt.expectedObj, actualGroupList) | |||
| } | |||
| } | |||
|
|
|||
| func TestRESTGetPagination(t *testing.T) { | |||
| tests := []struct { | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So there is no error either if page < 0?
| return nil | ||
| } | ||
| if pageInfo.Page < 0 || pageInfo.Limit < 0 { | ||
| return errors.NewInternalError(fmt.Errorf("received invalid page number %d or limit %d for pagination", pageInfo.Page, pageInfo.Limit)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's not the right HTTP error code
You need NewBadRequest here. You can also return a different error message based on which parameter is wrong.
b623283
to
9451e33
Compare
| } else { | ||
| require.NoError(t, err) | ||
| } | ||
| if tt.name == "default-zero-group-member-pagination" { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why does it need to be checked specially? doesn't assert.Equal(t, tt.expectedObj, actualGroupList) work?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because in default zero case, no pagination is performed thus the three returned members are sorted. assert.ElementsMatch is needed separately to resolve flakiness.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we should sort in the non-pagination case as well for consistency. I don't think the performance argument in that case is a very valid one.
| assert.Equal(t, tt.expectedObj.(*controlplane.ClusterGroupMembers).TotalPages, actualGroupList.(*controlplane.ClusterGroupMembers).TotalPages) | ||
| assert.Equal(t, tt.expectedObj.(*controlplane.ClusterGroupMembers).CurrentPage, actualGroupList.(*controlplane.ClusterGroupMembers).CurrentPage) | ||
| assert.ElementsMatch(t, tt.expectedObj.(*controlplane.ClusterGroupMembers).EffectiveMembers, actualGroupList.(*controlplane.ClusterGroupMembers).EffectiveMembers) | ||
| } else if tt.name == "err-page-group-member-pagination" || tt.name == "err-limit-group-member-pagination" { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you could return early when expectedErr is true, as actualGroupList and err is mutually exclusive.
| if pageInfo.Limit < int64(len(memberList.EffectiveMembers)) { | ||
| memberList.EffectiveMembers = memberList.EffectiveMembers[:pageInfo.Limit] | ||
| } | ||
| } else if totalPages < pageInfo.Page { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if Page is 0, it seems it will get all members?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it will be treated as no pagination option is provided, same as limit = 0.
Signed-off-by: Qiyue Yao <yaoq@vmware.com> Investigation Signed-off-by: Qiyue Yao <yaoq@vmware.com> add UT Signed-off-by: Qiyue Yao <yaoq@vmware.com> update case page exceeds Signed-off-by: Qiyue Yao <yaoq@vmware.com> update error handling Signed-off-by: Qiyue Yao <yaoq@vmware.com> update HTTP error Signed-off-by: Qiyue Yao <yaoq@vmware.com> change sort Signed-off-by: Qiyue Yao <yaoq@vmware.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
|
/test-all |
To resolve #2492 .
Pagination:
API query:
kubectl proxy &curl "<k8s-apiserver>:8001/apis/controlplane.antrea.io/v1beta2/clustergroupmembers/<name>?page=<page#>&limit=<limit#>"!! Notice the double quote, it's very important as
curlwould not interpret the&otherwise.Page: the specific page to return in the sorted member list
Limit: max number of members to return per page
Response:
Change:
Getterof cluster group membership querier toGetterWithOptionsso it can take in more options.ClusterGroupMembersstruct to include total members and current page info in the response.PaginationGetOptionsto bothcontrolplaneandv1beta2types, registered in controlplane, v1beta2, and legacy APIs.metav1version to runtime scheme codec inpkg/apiserver/apiserver.go.Signed-off-by: Qiyue Yao yaoq@vmware.com