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

Support Pagination for ClusterGroupMembership #3183

Merged
merged 1 commit into from Mar 10, 2022

Conversation

qiyueyao
Copy link
Contributor

@qiyueyao qiyueyao commented Jan 10, 2022

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 curl would 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:

{
  "kind": "GroupMemberList",
  "apiVersion": "controlplane.antrea.tanzu.vmware.com/v1beta2",
  "metadata": {
    "name": <name>,
    "creationTimestamp": null
  },
  "effectiveMembers": [],
  "effectiveIPBlocks": null,
  "totalMembers": <number of members in entire list>,
  "currentPage": <page of response>,
}

Change:

  • Changed the Getter of cluster group membership querier to GetterWithOptions so it can take in more options.
  • Changed ClusterGroupMembers struct to include total members and current page info in the response.
  • Added PaginationGetOptions to both controlplane and v1beta2 types, registered in controlplane, v1beta2, and legacy APIs.
  • Changed parameterCodec from metav1 version to runtime scheme codec in pkg/apiserver/apiserver.go.
  • Added UT specifically for pagination Get.

Signed-off-by: Qiyue Yao yaoq@vmware.com

@codecov-commenter
Copy link

codecov-commenter commented Jan 10, 2022

Codecov Report

Merging #3183 (13e0f70) into main (c6926ed) will increase coverage by 2.31%.
The diff coverage is 84.21%.

Impacted file tree graph

@@            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     
Flag Coverage Δ
kind-e2e-tests 51.09% <10.52%> (+3.64%) ⬆️
unit-tests 42.45% <77.77%> (+0.03%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted Files Coverage Δ
pkg/apis/controlplane/types.go 0.00% <ø> (ø)
.../registry/networkpolicy/clustergroupmember/rest.go 84.31% <82.85%> (-3.93%) ⬇️
pkg/apis/controlplane/register.go 90.47% <100.00%> (+0.47%) ⬆️
pkg/apis/controlplane/v1beta2/register.go 95.23% <100.00%> (+0.23%) ⬆️
pkg/apiserver/apiserver.go 89.47% <100.00%> (+1.50%) ⬆️
pkg/agent/controller/networkpolicy/reject.go 78.22% <0.00%> (-9.68%) ⬇️
...agent/flowexporter/connections/deny_connections.go 83.07% <0.00%> (-3.08%) ⬇️
pkg/agent/cniserver/ipam/ipam_service.go 52.17% <0.00%> (-2.18%) ⬇️
pkg/ipam/poolallocator/allocator.go 50.00% <0.00%> (-1.16%) ⬇️
...gent/controller/noderoute/node_route_controller.go 54.91% <0.00%> (-1.10%) ⬇️
... and 26 more

@qiyueyao qiyueyao marked this pull request as draft January 10, 2022 23:56
@qiyueyao qiyueyao force-pushed the pagination branch 2 times, most recently from e05f4de to 3b80708 Compare January 11, 2022 20:01
Copy link
Contributor

@Dyanngg Dyanngg left a 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

pkg/apis/controlplane/v1beta2/generated.proto Outdated Show resolved Hide resolved
pkg/apis/controlplane/v1beta2/types.go Outdated Show resolved Hide resolved
@qiyueyao qiyueyao force-pushed the pagination branch 2 times, most recently from 4b603e9 to b7464e1 Compare January 27, 2022 07:59
@qiyueyao qiyueyao marked this pull request as ready for review January 27, 2022 18:46
@qiyueyao qiyueyao added this to the Antrea v1.6 release milestone Jan 27, 2022
@qiyueyao qiyueyao added action/backport Indicates a PR that requires backports. area/network-policy Issues or PRs related to network policies. and removed action/backport Indicates a PR that requires backports. labels Jan 27, 2022
@Dyanngg
Copy link
Contributor

Dyanngg commented Feb 9, 2022

LGTM overall, will let other reviewers take a look as well

pkg/apis/controlplane/v1beta2/conversion.go Outdated Show resolved Hide resolved

// PaginationGetOptions is used to retrieve page number and page limit info from the request.
type PaginationGetOptions struct {
metav1.TypeMeta `json:",inline"`
Copy link
Member

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

Copy link
Contributor Author

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{},
Copy link
Member

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Member

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/apis/controlplane/types.go Outdated Show resolved Hide resolved
@antoninbas antoninbas added the api-review Categorizes an issue or PR as actively needing an API review. label Feb 9, 2022
pkg/apis/controlplane/types.go Outdated Show resolved Hide resolved
@@ -58,6 +58,7 @@ func addKnownTypes(scheme *runtime.Scheme) error {
&newcontrolplane.NetworkPolicyStatus{},
&newcontrolplane.NodeStatsSummary{},
&newcontrolplane.ClusterGroupMembers{},
&newcontrolplane.PaginationGetOptions{},
Copy link
Contributor

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.

@qiyueyao qiyueyao force-pushed the pagination branch 2 times, most recently from ec7d69f to 37b123c Compare February 15, 2022 01:52
@@ -58,6 +58,7 @@ func addKnownTypes(scheme *runtime.Scheme) error {
&newcontrolplane.NetworkPolicyStatus{},
&newcontrolplane.NodeStatsSummary{},
&newcontrolplane.ClusterGroupMembers{},
&newcontrolplane.PaginationGetOptions{},
Copy link
Member

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.

@qiyueyao
Copy link
Contributor Author

/test-all

Copy link
Contributor

@antoninbas antoninbas left a 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.

@@ -162,3 +214,133 @@ func TestRESTGet(t *testing.T) {
assert.Equal(t, tt.expectedObj, actualGroupList)
}
}

func TestRESTGetPagination(t *testing.T) {
tests := []struct {
Copy link
Contributor

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?

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'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.

Copy link
Contributor

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
}
})
Copy link
Contributor

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?

Copy link
Contributor Author

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
Copy link
Contributor

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?

Copy link
Contributor Author

@qiyueyao qiyueyao Feb 25, 2022

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!

Copy link
Member

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.

Copy link
Contributor Author

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!

@qiyueyao qiyueyao force-pushed the pagination branch 2 times, most recently from 245da18 to 26adb90 Compare February 26, 2022 06:34
@tnqn
Copy link
Member

tnqn commented Mar 1, 2022

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.

@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)?

@antoninbas
Copy link
Contributor

@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 r.querier.GetGroupMembers(name) always returns members in the same order. Is that not the case?

@qiyueyao
Copy link
Contributor Author

qiyueyao commented Mar 2, 2022

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.

@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 n) instead of the entire slice (say N) to reduce the runtime from O(NlogN) to O(nlogn)?

Trying to understand the first pageNumber*pageLimit (say n), is it based on the original unsorted members? Then should there be another option for users to decide if they want continuous result (no member change) across queries?

@tnqn
Copy link
Member

tnqn commented Mar 2, 2022

@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).
The runtime should be O(N+klogN), not O(klogk) I commented.

@qiyueyao qiyueyao force-pushed the pagination branch 2 times, most recently from 5bf8d77 to c0e0f39 Compare March 4, 2022 21:52
@qiyueyao
Copy link
Contributor Author

qiyueyao commented Mar 4, 2022

@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). The runtime should be O(N+klogN), not O(klogk) I commented.

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.

@qiyueyao qiyueyao requested a review from antoninbas March 7, 2022 22:46
antoninbas
antoninbas previously approved these changes Mar 7, 2022
Copy link
Contributor

@antoninbas antoninbas left a comment

Choose a reason for hiding this comment

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

LGTM

Comment on lines 58 to 61
if !ok {
getOptions = &controlplane.PaginationGetOptions{Page: 0, Limit: 0}
}
Copy link
Contributor

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.

Copy link
Contributor Author

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 {
Copy link
Contributor

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))
Copy link
Contributor

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.

@qiyueyao qiyueyao force-pushed the pagination branch 2 times, most recently from b623283 to 9451e33 Compare March 8, 2022 02:55
} else {
require.NoError(t, err)
}
if tt.name == "default-zero-group-member-pagination" {
Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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" {
Copy link
Member

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 {
Copy link
Member

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?

Copy link
Contributor Author

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>
Copy link
Member

@tnqn tnqn left a comment

Choose a reason for hiding this comment

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

LGTM

@tnqn tnqn added the action/release-note Indicates a PR that should be included in release notes. label Mar 9, 2022
@antoninbas
Copy link
Contributor

/test-all

@tnqn tnqn merged commit cd37a9b into antrea-io:main Mar 10, 2022
@qiyueyao qiyueyao deleted the pagination branch March 10, 2022 19:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
action/release-note Indicates a PR that should be included in release notes. api-review Categorizes an issue or PR as actively needing an API review. area/network-policy Issues or PRs related to network policies.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support IP Address in ClusterGroup Membership and Association Query
6 participants