Skip to content

Commit

Permalink
sql/server: add function to reset index usage stats
Browse files Browse the repository at this point in the history
Previously, there was no existing way to clear index usage statistics
from the crdb_internal.index_usage_statistics table. This commit adds a
function that enables developers to clear index usage metrics using RPC
fanout to reach all nodes in a cluster. We have also added a new
metadata field that tracks the last reset time. Currently, this
functionality can be accessed via the SQL shell, and is not yet
accessible from the frontend console.

Release note (sql change): Add function
crdb_internal.reset_index_usage_stats() to clear index usage stats. This
can be invoked from the SQL shell.
  • Loading branch information
lindseyjin committed Oct 29, 2021
1 parent 10e1fd9 commit f85b8ff
Show file tree
Hide file tree
Showing 18 changed files with 1,398 additions and 557 deletions.
41 changes: 41 additions & 0 deletions docs/generated/http/full.md
Original file line number Diff line number Diff line change
Expand Up @@ -3956,6 +3956,47 @@ Response object returned by IndexUsageStatistics.
| Field | Type | Label | Description | Support status |
| ----- | ---- | ----- | ----------- | -------------- |
| statistics | [cockroach.sql.CollectedIndexUsageStatistics](#cockroach.server.serverpb.IndexUsageStatisticsResponse-cockroach.sql.CollectedIndexUsageStatistics) | repeated | | [reserved](#support-status) |
| last_reset | [google.protobuf.Timestamp](#cockroach.server.serverpb.IndexUsageStatisticsResponse-google.protobuf.Timestamp) | | Timestamp of the last index usage stats reset. | [reserved](#support-status) |







## ResetIndexUsageStats

`POST /_status/resetindexusagestats`



Support status: [reserved](#support-status)

#### Request Parameters




Request object for issuing a index usage stats reset request.


| Field | Type | Label | Description | Support status |
| ----- | ---- | ----- | ----------- | -------------- |
| node_id | [string](#cockroach.server.serverpb.ResetIndexUsageStatsRequest-string) | | | [reserved](#support-status) |







#### Response Parameters




Response object returned by ResetIndexUsageStatsRequest.




Expand Down
2 changes: 2 additions & 0 deletions docs/generated/sql/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -2924,6 +2924,8 @@ SELECT * FROM crdb_internal.check_consistency(true, ‘\x02’, ‘\x04’)</p>
</span></td></tr>
<tr><td><a name="crdb_internal.range_stats"></a><code>crdb_internal.range_stats(key: <a href="bytes.html">bytes</a>) &rarr; jsonb</code></td><td><span class="funcdesc"><p>This function is used to retrieve range statistics information as a JSON object.</p>
</span></td></tr>
<tr><td><a name="crdb_internal.reset_index_usage_stats"></a><code>crdb_internal.reset_index_usage_stats() &rarr; <a href="bool.html">bool</a></code></td><td><span class="funcdesc"><p>This function is used to clear the collected index usage statistics.</p>
</span></td></tr>
<tr><td><a name="crdb_internal.reset_sql_stats"></a><code>crdb_internal.reset_sql_stats() &rarr; <a href="bool.html">bool</a></code></td><td><span class="funcdesc"><p>This function is used to clear the collected SQL statistics.</p>
</span></td></tr>
<tr><td><a name="crdb_internal.round_decimal_values"></a><code>crdb_internal.round_decimal_values(val: <a href="decimal.html">decimal</a>, scale: <a href="int.html">int</a>) &rarr; <a href="decimal.html">decimal</a></code></td><td><span class="funcdesc"><p>This function is used internally to round decimal values during mutations.</p>
Expand Down
106 changes: 106 additions & 0 deletions pkg/ccl/serverccl/tenant_status_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,112 @@ func TestResetSQLStatsRPCForTenant(t *testing.T) {
}
}

func TestResetIndexUsageStatsRPCForTenant(t *testing.T) {
defer leaktest.AfterTest(t)()
defer log.Scope(t).Close(t)

skip.UnderStressRace(t, "expensive tests")

ctx := context.Background()

statsIngestionCb, statsIngestionNotifier := idxusage.CreateIndexStatsIngestedCallbackForTest()

knobs := tests.CreateTestingKnobs()
knobs.IndexUsageStatsKnobs = &idxusage.TestingKnobs{
OnIndexUsageStatsProcessedCallback: statsIngestionCb,
}

testHelper := newTestTenantHelper(t, 3 /* tenantClusterSize */, knobs)
defer testHelper.cleanup(ctx, t)

testingCluster := testHelper.testCluster()
controlCluster := testHelper.controlCluster()

for _, cluster := range []tenantCluster{testingCluster, controlCluster} {
// Create tables and insert data.
cluster.tenantConn(0).Exec(t, `
CREATE TABLE test (
k INT PRIMARY KEY,
a INT,
b INT,
INDEX(a)
)
`)

cluster.tenantConn(0).Exec(t, `
INSERT INTO test
VALUES (1, 10, 100), (2, 20, 200), (3, 30, 300)
`)

// Record scan on primary index.
cluster.tenantConn(0).Exec(t, "SELECT * FROM test")

// Record scan on secondary index.
cluster.tenantConn(1).Exec(t, "SELECT * FROM test@test_a_idx")
testTableIDStr := cluster.tenantConn(2).QueryStr(t, "SELECT 'test'::regclass::oid")[0][0]
testTableID, err := strconv.Atoi(testTableIDStr)
require.NoError(t, err)

// Wait for the stats to be ingested.
require.NoError(t,
idxusage.WaitForIndexStatsIngestionForTest(statsIngestionNotifier, map[roachpb.IndexUsageKey]struct{}{
{
TableID: roachpb.TableID(testTableID),
IndexID: 1,
}: {},
{
TableID: roachpb.TableID(testTableID),
IndexID: 2,
}: {},
}, 2 /* expectedEventCnt*/, 5*time.Second /* timeout */),
)

query := `
SELECT
table_id,
index_id,
total_reads,
extract_duration('second', now() - last_read) < 5
FROM
crdb_internal.index_usage_statistics
WHERE
table_id = $1
`
// Assert index usage data was inserted.
actual := cluster.tenantConn(2).QueryStr(t, query, testTableID)
expected := [][]string{
{testTableIDStr, "1", "1", "true"},
{testTableIDStr, "2", "1", "true"},
}
require.Equal(t, expected, actual)
}

// Reset index usage stats.
status := testingCluster.tenantStatusSrv(1 /* idx */)
_, err := status.ResetIndexUsageStats(ctx, &serverpb.ResetIndexUsageStatsRequest{})
require.NoError(t, err)

resp, err := status.IndexUsageStatistics(ctx, &serverpb.IndexUsageStatisticsRequest{})
require.NoError(t, err)

// Require index usage metrics to be reset.
for _, stats := range resp.Statistics {
require.Equal(t, uint64(0), stats.Stats.TotalReadCount)
require.Equal(t, time.Time{}, stats.Stats.LastRead)
}

// Ensure tenant data isolation.
status = controlCluster.tenantStatusSrv(1 /* idx */)
resp, err = status.IndexUsageStatistics(ctx, &serverpb.IndexUsageStatisticsRequest{})
require.NoError(t, err)

// Require index usage metrics to not be reset.
for _, stats := range resp.Statistics {
require.NotEqual(t, uint64(0), stats.Stats.TotalReadCount)
require.NotEqual(t, time.Time{}, stats.Stats.LastRead)
}
}

func ensureExpectedStmtFingerprintExistsInRPCResponse(
t *testing.T, expectedStmts []string, resp *serverpb.StatementsResponse, clusterType string,
) {
Expand Down
66 changes: 66 additions & 0 deletions pkg/server/index_usage_stats.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ func (s *statusServer) IndexUsageStatistics(
return nil, err
}

// Append last reset time.
resp.LastReset = s.sqlServer.pgServer.SQLServer.GetLocalIndexStatistics().GetLastReset()

return resp, nil
}

Expand All @@ -108,5 +111,68 @@ func indexUsageStatsLocal(
}); err != nil {
return nil, err
}
// Append last reset time.
resp.LastReset = idxUsageStats.GetLastReset()
return resp, nil
}

func (s *statusServer) ResetIndexUsageStats(
ctx context.Context, req *serverpb.ResetIndexUsageStatsRequest,
) (*serverpb.ResetIndexUsageStatsResponse, error) {
ctx = propagateGatewayMetadata(ctx)
ctx = s.AnnotateCtx(ctx)

if _, err := s.privilegeChecker.requireAdminUser(ctx); err != nil {
return nil, err
}

localReq := &serverpb.ResetIndexUsageStatsRequest{
NodeID: "local",
}
resp := &serverpb.ResetIndexUsageStatsResponse{}

if len(req.NodeID) > 0 {
requestedNodeID, local, err := s.parseNodeID(req.NodeID)
if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}
if local {
s.sqlServer.pgServer.SQLServer.GetLocalIndexStatistics().Reset()
return resp, nil
}

statusClient, err := s.dialNode(ctx, requestedNodeID)
if err != nil {
return nil, err
}

return statusClient.ResetIndexUsageStats(ctx, localReq)
}

dialFn := func(ctx context.Context, nodeID roachpb.NodeID) (interface{}, error) {
client, err := s.dialNode(ctx, nodeID)
return client, err
}

resetIndexUsageStats := func(ctx context.Context, client interface{}, _ roachpb.NodeID) (interface{}, error) {
statusClient := client.(serverpb.StatusClient)
return statusClient.ResetIndexUsageStats(ctx, localReq)
}

aggFn := func(_ roachpb.NodeID, nodeResp interface{}) {
// Nothing to do here.
}

var combinedError error
errFn := func(_ roachpb.NodeID, nodeFnError error) {
combinedError = errors.CombineErrors(combinedError, nodeFnError)
}

if err := s.iterateNodes(ctx,
fmt.Sprintf("Resetting index usage stats for node %s", req.NodeID),
dialFn, resetIndexUsageStats, aggFn, errFn); err != nil {
return nil, err
}

return resp, nil
}
1 change: 1 addition & 0 deletions pkg/server/serverpb/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type SQLStatusServer interface {
ListLocalDistSQLFlows(context.Context, *ListDistSQLFlowsRequest) (*ListDistSQLFlowsResponse, error)
Profile(ctx context.Context, request *ProfileRequest) (*JSONResponse, error)
IndexUsageStatistics(context.Context, *IndexUsageStatisticsRequest) (*IndexUsageStatisticsResponse, error)
ResetIndexUsageStats(context.Context, *ResetIndexUsageStatsRequest) (*ResetIndexUsageStatsResponse, error)
}

// OptionalNodesStatusServer is a StatusServer that is only optionally present
Expand Down
Loading

0 comments on commit f85b8ff

Please sign in to comment.