Skip to content

Commit

Permalink
server: add pagination to hot ranges API
Browse files Browse the repository at this point in the history
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
  • Loading branch information
koorosh committed Mar 2, 2022
1 parent 35d2b78 commit af62e80
Show file tree
Hide file tree
Showing 5 changed files with 156 additions and 50 deletions.
22 changes: 21 additions & 1 deletion docs/generated/http/full.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<br><br>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) |



Expand Down Expand Up @@ -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.<br><br>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) |



Expand All @@ -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) |



Expand Down Expand Up @@ -3281,6 +3287,20 @@ HotRange message describes a single hot range, ie its QPS, node ID it belongs to



<a name="cockroach.server.serverpb.HotRangesResponseV2-cockroach.server.serverpb.HotRangesResponseV2.ErrorsByNodeIdEntry"></a>
#### HotRangesResponseV2.ErrorsByNodeIdEntry



| Field | Type | Label | Description | Support status |
| ----- | ---- | ----- | ----------- | -------------- |
| key | [int32](#cockroach.server.serverpb.HotRangesResponseV2-int32) | | | |
| value | [string](#cockroach.server.serverpb.HotRangesResponseV2-string) | | | |






## Range

Expand Down
2 changes: 2 additions & 0 deletions docs/generated/http/hotranges-request.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<br><br>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) |


12 changes: 11 additions & 1 deletion pkg/server/serverpb/status.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<int32, string> 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 {
Expand Down
143 changes: 100 additions & 43 deletions pkg/server/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,33 @@ const HotRanges = (props: HotRangesProps) => {
const [hotRanges, setHotRanges] = useState<
cockroach.server.serverpb.HotRangesResponseV2["ranges"]
>([]);
const requestHotRanges = useCallback(() => {
const [pageToken, setPageToken] = useState<string>("");
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]);
Expand All @@ -46,7 +63,7 @@ const HotRanges = (props: HotRangesProps) => {
>
<span>{`Node ID: ${nodeId ?? "All nodes"}`}</span>
<span>{`Time: ${time.toISOString()}`}</span>
<Button onClick={requestHotRanges} intent={"secondary"}>
<Button onClick={refreshHotRanges} intent={"secondary"}>
Refresh
</Button>
<pre className="state-json-box">{JSON.stringify(hotRanges, null, 2)}</pre>
Expand Down

0 comments on commit af62e80

Please sign in to comment.