From af62e80448d9f9855c23111d76dcb09091727eb2 Mon Sep 17 00:00:00 2001 From: Andrii Vorobiov Date: Wed, 16 Feb 2022 10:21:26 +0200 Subject: [PATCH] server: add pagination to hot ranges API This change extends hot ranges API to support pagination of responses. It is also possible to avoid pagination when page size is set to 0. Release note: None Release justification: bug fixes and low-risk updates to new functionality --- docs/generated/http/full.md | 22 ++- docs/generated/http/hotranges-request.md | 2 + pkg/server/serverpb/status.proto | 12 +- pkg/server/status.go | 143 ++++++++++++------ .../reports/containers/hotranges/index.tsx | 27 +++- 5 files changed, 156 insertions(+), 50 deletions(-) diff --git a/docs/generated/http/full.md b/docs/generated/http/full.md index 5438796a7431..977ff4d45f99 100644 --- a/docs/generated/http/full.md +++ b/docs/generated/http/full.md @@ -3130,6 +3130,8 @@ of ranges currently considered “hot” by the node(s). | Field | Type | Label | Description | Support status | | ----- | ---- | ----- | ----------- | -------------- | | node_id | [string](#cockroach.server.serverpb.HotRangesRequest-string) | | NodeID indicates which node to query for a hot range report. It is possible to populate any node ID; if the node receiving the request is not the target node, it will forward the request to the target node.

If left empty, the request is forwarded to every node in the cluster. | [alpha](#support-status) | +| page_size | [int32](#cockroach.server.serverpb.HotRangesRequest-int32) | | | [reserved](#support-status) | +| page_token | [string](#cockroach.server.serverpb.HotRangesRequest-string) | | | [reserved](#support-status) | @@ -3236,6 +3238,8 @@ of ranges currently considered “hot” by the node(s). | Field | Type | Label | Description | Support status | | ----- | ---- | ----- | ----------- | -------------- | | node_id | [string](#cockroach.server.serverpb.HotRangesRequest-string) | | NodeID indicates which node to query for a hot range report. It is possible to populate any node ID; if the node receiving the request is not the target node, it will forward the request to the target node.

If left empty, the request is forwarded to every node in the cluster. | [alpha](#support-status) | +| page_size | [int32](#cockroach.server.serverpb.HotRangesRequest-int32) | | | [reserved](#support-status) | +| page_token | [string](#cockroach.server.serverpb.HotRangesRequest-string) | | | [reserved](#support-status) | @@ -3253,7 +3257,9 @@ HotRangesResponseV2 is a response payload returned by `HotRangesV2` service. | Field | Type | Label | Description | Support status | | ----- | ---- | ----- | ----------- | -------------- | -| ranges | [HotRangesResponseV2.HotRange](#cockroach.server.serverpb.HotRangesResponseV2-cockroach.server.serverpb.HotRangesResponseV2.HotRange) | repeated | ranges contain list of hot ranges info that has highest number of QPS. | [reserved](#support-status) | +| ranges | [HotRangesResponseV2.HotRange](#cockroach.server.serverpb.HotRangesResponseV2-cockroach.server.serverpb.HotRangesResponseV2.HotRange) | repeated | Ranges contain list of hot ranges info that has highest number of QPS. | [reserved](#support-status) | +| errors_by_node_id | [HotRangesResponseV2.ErrorsByNodeIdEntry](#cockroach.server.serverpb.HotRangesResponseV2-cockroach.server.serverpb.HotRangesResponseV2.ErrorsByNodeIdEntry) | repeated | errors contains any errors that occurred during fan-out calls to other nodes. | [reserved](#support-status) | +| next_page_token | [string](#cockroach.server.serverpb.HotRangesResponseV2-string) | | NextPageToken represents next pagination token to request next slice of data. | [reserved](#support-status) | @@ -3281,6 +3287,20 @@ HotRange message describes a single hot range, ie its QPS, node ID it belongs to + +#### HotRangesResponseV2.ErrorsByNodeIdEntry + + + +| Field | Type | Label | Description | Support status | +| ----- | ---- | ----- | ----------- | -------------- | +| key | [int32](#cockroach.server.serverpb.HotRangesResponseV2-int32) | | | | +| value | [string](#cockroach.server.serverpb.HotRangesResponseV2-string) | | | | + + + + + ## Range diff --git a/docs/generated/http/hotranges-request.md b/docs/generated/http/hotranges-request.md index 6c542161d115..c7ccfd28d4e7 100644 --- a/docs/generated/http/hotranges-request.md +++ b/docs/generated/http/hotranges-request.md @@ -12,5 +12,7 @@ Support status: [alpha](#support-status) | Field | Type | Label | Description | Support status | | ----- | ---- | ----- | ----------- | -------------- | | node_id | [string](#string) | | NodeID indicates which node to query for a hot range report. It is possible to populate any node ID; if the node receiving the request is not the target node, it will forward the request to the target node.

If left empty, the request is forwarded to every node in the cluster. | [alpha](#support-status) | +| page_size | [int32](#int32) | | | [reserved](#support-status) | +| page_token | [string](#string) | | | [reserved](#support-status) | diff --git a/pkg/server/serverpb/status.proto b/pkg/server/serverpb/status.proto index 49d381736922..aa73cd4f9b46 100644 --- a/pkg/server/serverpb/status.proto +++ b/pkg/server/serverpb/status.proto @@ -1110,6 +1110,8 @@ message HotRangesRequest { // in the cluster. // API: PUBLIC ALPHA string node_id = 1 [(gogoproto.customname) = "NodeID"]; + int32 page_size = 2 [(gogoproto.nullable) = true]; + string page_token = 3 [(gogoproto.nullable) = true]; } // HotRangesResponse is the payload produced in response @@ -1233,8 +1235,16 @@ message HotRangesResponseV2 { // schema_name provides the name of schema (if exists) for table in current range. string schema_name = 9; } - // ranges contain list of hot ranges info that has highest number of QPS. + // Ranges contain list of hot ranges info that has highest number of QPS. repeated HotRange ranges = 1; + // errors contains any errors that occurred during fan-out calls to other nodes. + map errors_by_node_id = 2 [ + (gogoproto.castkey) = "github.com/cockroachdb/cockroach/pkg/roachpb.NodeID", + (gogoproto.customname) = "ErrorsByNodeID", + (gogoproto.nullable) = false + ]; + // NextPageToken represents next pagination token to request next slice of data. + string next_page_token = 3 [(gogoproto.nullable) = true]; } message RangeRequest { diff --git a/pkg/server/status.go b/pkg/server/status.go index 44a0accec471..917d8140d0ff 100644 --- a/pkg/server/status.go +++ b/pkg/server/status.go @@ -2121,14 +2121,23 @@ type hotRangeReportMeta struct { func (s *statusServer) HotRangesV2( ctx context.Context, req *serverpb.HotRangesRequest, ) (*serverpb.HotRangesResponseV2, error) { - resp, err := s.HotRanges(ctx, req) - if err != nil { + if _, err := s.privilegeChecker.requireAdminUser(ctx); err != nil { return nil, err } + size := int(req.PageSize) + start := paginationState{} + + if len(req.PageToken) > 0 { + if err := start.UnmarshalText([]byte(req.PageToken)); err != nil { + return nil, err + } + } + rangeReportMetas := make(map[uint32]hotRangeReportMeta) var descrs []catalog.Descriptor - if err = s.sqlServer.distSQLServer.CollectionFactory.Txn( + var err error + if err := s.sqlServer.distSQLServer.CollectionFactory.Txn( ctx, s.sqlServer.internalExecutor, s.db, func(ctx context.Context, txn *kv.Txn, descriptors *descs.Collection) error { all, err := descriptors.GetAllDescriptors(ctx, txn) @@ -2162,51 +2171,99 @@ func (s *statusServer) HotRangesV2( rangeReportMetas[id] = meta } - var ranges []*serverpb.HotRangesResponseV2_HotRange - for nodeID, hr := range resp.HotRangesByNodeID { - for _, store := range hr.Stores { - for _, r := range store.HotRanges { - var ( - dbName, tableName, indexName, schemaName string - replicaNodeIDs []roachpb.NodeID - ) - _, tableID, err := s.sqlServer.execCfg.Codec.DecodeTablePrefix(r.Desc.StartKey.AsRawKey()) - if err != nil { - log.Warningf(ctx, "cannot decode tableID for range descriptor: %s. %s", r.Desc.String(), err.Error()) - continue - } - parent := rangeReportMetas[tableID].parentID - if parent != 0 { - tableName = rangeReportMetas[tableID].tableName - dbName = rangeReportMetas[parent].dbName - } else { - dbName = rangeReportMetas[tableID].dbName - } - schemaParent := rangeReportMetas[tableID].schemaParentID - schemaName = rangeReportMetas[schemaParent].schemaName - _, _, idxID, err := s.sqlServer.execCfg.Codec.DecodeIndexPrefix(r.Desc.StartKey.AsRawKey()) - if err == nil { - indexName = rangeReportMetas[tableID].indexNames[idxID] - } - for _, repl := range r.Desc.Replicas().Descriptors() { - replicaNodeIDs = append(replicaNodeIDs, repl.NodeID) + response := &serverpb.HotRangesResponseV2{ + ErrorsByNodeID: make(map[roachpb.NodeID]string), + } + + var requestedNodes []roachpb.NodeID + if len(req.NodeID) > 0 { + requestedNodeID, _, err := s.parseNodeID(req.NodeID) + if err != nil { + return nil, err + } + requestedNodes = []roachpb.NodeID{requestedNodeID} + } + + dialFn := func(ctx context.Context, nodeID roachpb.NodeID) (interface{}, error) { + client, err := s.dialNode(ctx, nodeID) + return client, err + } + remoteRequest := serverpb.HotRangesRequest{NodeID: "local"} + nodeFn := func(ctx context.Context, client interface{}, nodeID roachpb.NodeID) (interface{}, error) { + status := client.(serverpb.StatusClient) + resp, err := status.HotRanges(ctx, &remoteRequest) + if err != nil || resp == nil { + return nil, err + } + var ranges []*serverpb.HotRangesResponseV2_HotRange + for nodeID, hr := range resp.HotRangesByNodeID { + for _, store := range hr.Stores { + for _, r := range store.HotRanges { + var ( + dbName, tableName, indexName, schemaName string + replicaNodeIDs []roachpb.NodeID + ) + _, tableID, err := s.sqlServer.execCfg.Codec.DecodeTablePrefix(r.Desc.StartKey.AsRawKey()) + if err != nil { + log.Warningf(ctx, "cannot decode tableID for range descriptor: %s. %s", r.Desc.String(), err.Error()) + continue + } + parent := rangeReportMetas[tableID].parentID + if parent != 0 { + tableName = rangeReportMetas[tableID].tableName + dbName = rangeReportMetas[parent].dbName + } else { + dbName = rangeReportMetas[tableID].dbName + } + schemaParent := rangeReportMetas[tableID].schemaParentID + schemaName = rangeReportMetas[schemaParent].schemaName + _, _, idxID, err := s.sqlServer.execCfg.Codec.DecodeIndexPrefix(r.Desc.StartKey.AsRawKey()) + if err == nil { + indexName = rangeReportMetas[tableID].indexNames[idxID] + } + for _, repl := range r.Desc.Replicas().Descriptors() { + replicaNodeIDs = append(replicaNodeIDs, repl.NodeID) + } + ranges = append(ranges, &serverpb.HotRangesResponseV2_HotRange{ + RangeID: r.Desc.RangeID, + NodeID: nodeID, + QPS: r.QueriesPerSecond, + TableName: tableName, + SchemaName: schemaName, + DatabaseName: dbName, + IndexName: indexName, + ReplicaNodeIds: replicaNodeIDs, + LeaseholderNodeID: r.LeaseholderNodeID, + }) } - ranges = append(ranges, &serverpb.HotRangesResponseV2_HotRange{ - RangeID: r.Desc.RangeID, - NodeID: nodeID, - QPS: r.QueriesPerSecond, - TableName: tableName, - SchemaName: schemaName, - DatabaseName: dbName, - IndexName: indexName, - ReplicaNodeIds: replicaNodeIDs, - LeaseholderNodeID: r.LeaseholderNodeID, - }) } } + return ranges, nil + } + responseFn := func(nodeID roachpb.NodeID, resp interface{}) { + if resp == nil { + return + } + hotRanges := resp.([]*serverpb.HotRangesResponseV2_HotRange) + response.Ranges = append(response.Ranges, hotRanges...) } + errorFn := func(nodeID roachpb.NodeID, err error) { + response.ErrorsByNodeID[nodeID] = err.Error() + } + + next, err := s.paginatedIterateNodes( + ctx, "hotRanges", size, start, requestedNodes, dialFn, + nodeFn, responseFn, errorFn) - return &serverpb.HotRangesResponseV2{Ranges: ranges}, nil + if err != nil { + return nil, err + } + var nextBytes []byte + if nextBytes, err = next.MarshalText(); err != nil { + return nil, err + } + response.NextPageToken = string(nextBytes) + return response, nil } func (s *statusServer) localHotRanges(ctx context.Context) serverpb.HotRangesResponse_NodeResponse { diff --git a/pkg/ui/workspaces/db-console/src/views/reports/containers/hotranges/index.tsx b/pkg/ui/workspaces/db-console/src/views/reports/containers/hotranges/index.tsx index 934d6862010e..4533a36b31c8 100644 --- a/pkg/ui/workspaces/db-console/src/views/reports/containers/hotranges/index.tsx +++ b/pkg/ui/workspaces/db-console/src/views/reports/containers/hotranges/index.tsx @@ -24,16 +24,33 @@ const HotRanges = (props: HotRangesProps) => { const [hotRanges, setHotRanges] = useState< cockroach.server.serverpb.HotRangesResponseV2["ranges"] >([]); - const requestHotRanges = useCallback(() => { + const [pageToken, setPageToken] = useState(""); + const pageSize = 50; + + const refreshHotRanges = useCallback(() => { + setHotRanges([]); + setPageToken(""); + }, []); + + useEffect(() => { const request = cockroach.server.serverpb.HotRangesRequest.create({ node_id: nodeId, + page_size: pageSize, + page_token: pageToken, }); getHotRanges(request).then(response => { - setHotRanges(response.ranges); + if (response.ranges.length == 0) { + return; + } + setPageToken(response.next_page_token); + setHotRanges([...hotRanges, ...response.ranges]); setTime(moment()); }); - }, [nodeId]); - useEffect(requestHotRanges, [requestHotRanges, nodeId]); + // Avoid dispatching request when `hotRanges` list is updated. + // This effect should be triggered only when pageToken is changed. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [pageToken]); + useEffect(() => { setNodeId(nodeIdParam); }, [nodeIdParam]); @@ -46,7 +63,7 @@ const HotRanges = (props: HotRangesProps) => { > {`Node ID: ${nodeId ?? "All nodes"}`} {`Time: ${time.toISOString()}`} -
{JSON.stringify(hotRanges, null, 2)}