Skip to content

Commit

Permalink
cluster-ui: add sql api request wrapper and clusterLocks request
Browse files Browse the repository at this point in the history
This commit allows DB Console to use the SQL over HTTP API from
`/api/v2/sql/`. A new fetch wrapper providing the custom header
necessary for the API and using content type JSON has been added.

The clusterLocksApi components added in this commit use the above
SQL api functions to query from the `crdb_internal.cluster_locks`
table.

Release note: None
  • Loading branch information
xinhaoz committed Aug 3, 2022
1 parent 05e3d5d commit b75a704
Show file tree
Hide file tree
Showing 4 changed files with 235 additions and 0 deletions.
118 changes: 118 additions & 0 deletions pkg/ui/workspaces/cluster-ui/src/api/clusterLocksApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Copyright 2022 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

import moment from "moment";
import { executeSql, SqlExecutionRequest } from "./sqlApi";

export type ClusterLockState = {
databaseName?: string;
schemaName?: string;
tableName?: string;
indexName?: string;
lockHolderTxnID?: string; // Excecution ID of txn holding this lock.
holdTime: moment.Duration;
waiters?: LockWaiter[]; // List of waiting transaction execution IDs.
};

export type LockWaiter = {
id: string; // Txn execution ID.
waitTime: moment.Duration;
};

export type ClusterLocksResponse = ClusterLockState[];

type ClusterLockColumns = {
lock_key_pretty: string;
database_name: string;
schema_name: string;
table_name: string;
index_name: string;
txn_id: string;
duration: string;
granted: boolean;
};

/**
* getClusterLocksState returns information from crdb_internal.cluster_locks
* regarding the state of range locks in the cluster.
*/
export function getClusterLocksState(): Promise<ClusterLocksResponse> {
const request: SqlExecutionRequest = {
statements: [
{
sql: `
SELECT
lock_key_pretty,
database_name,
schema_name,
table_name,
index_name,
txn_id,
duration,
granted
FROM
crdb_internal.cluster_locks
WHERE
contended = true
`,
},
],
execute: true,
};
return executeSql<ClusterLockColumns>(request).then(result => {
if (
result.execution.txn_results.length === 0 ||
!result.execution.txn_results[0].rows
) {
// No data.
return [];
}

const locks: Record<string, ClusterLockState> = {};

// If all the lock keys are blank, then the user has VIEWACTIVITYREDACTED
// role. We won't be able to group the resulting rows by lock key to get
// correlated transactions, but we can still surface wait time information.
// To do this, we treat each entry as a unique lock entry with a single
// txn.
const noLockKeys = result.execution.txn_results[0].rows.every(
row => !row.lock_key_pretty,
);

let counter = 0;
result.execution.txn_results[0].rows.forEach(row => {
const key = noLockKeys ? `entry_${counter++}` : row.lock_key_pretty;

if (!locks[key]) {
locks[key] = {
databaseName: row.database_name,
schemaName: row.schema_name,
tableName: row.table_name,
indexName: row.index_name,
waiters: [],
holdTime: moment.duration(),
};
}

const duration = moment.duration(row.duration);
if (row.granted) {
locks[key].lockHolderTxnID = row.txn_id;
locks[key].holdTime = duration;
} else {
locks[key].waiters.push({
id: row.txn_id,
waitTime: duration,
});
}
});

return Object.values(locks);
});
}
38 changes: 38 additions & 0 deletions pkg/ui/workspaces/cluster-ui/src/api/fetchData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,41 @@ export const fetchData = <P extends ProtoBuilder<P>, T extends ProtoBuilder<T>>(
})
.then(buffer => RespBuilder.decode(new Uint8Array(buffer)));
};

/**
* fetchDataJSON makes a request for /api/v2 which uses content type JSON.
* @param path relative path for requested resource.
* @param reqPayload request payload object.
*/
export function fetchDataJSON<ResponseType, RequestType>(
path: string,
reqPayload?: RequestType,
): Promise<ResponseType> {
const params: RequestInit = {
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"X-Cockroach-API-Session": "cookie",
},
credentials: "same-origin",
};

if (reqPayload) {
params.method = "POST";
params.body = JSON.stringify(reqPayload);
}

const basePath = getBasePath();

return fetch(`${basePath}${path}`, params).then(response => {
if (!response.ok) {
throw new RequestError(
response.statusText,
response.status,
response.statusText,
);
}

return response.json();
});
}
1 change: 1 addition & 0 deletions pkg/ui/workspaces/cluster-ui/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ export * from "./statementDiagnosticsApi";
export * from "./statementsApi";
export * from "./basePath";
export * from "./nodesApi";
export * from "./clusterLocksApi";
78 changes: 78 additions & 0 deletions pkg/ui/workspaces/cluster-ui/src/api/sqlApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright 2022 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

import { fetchDataJSON } from "./fetchData";

export type SqlExecutionRequest = {
statements: SqlStatement[];
execute?: boolean;
timeout?: string; // Default 5s
application_name?: string; // Defaults to '$ api-v2-sql'
database_name?: string; // Defaults to defaultDb
max_result_size?: number; // Default 10kib
};

export type SqlStatement = {
sql: string;
arguments?: unknown[];
};

export type SqlExecutionResponse<T> = {
num_statements?: number;
execution?: SqlExecutionExecResult<T>;
error?: SqlExecutionErrorMessage;
request?: SqlExecutionRequest;
};

export interface SqlExecutionExecResult<T> {
retries: number;
txn_results: SqlTxnResult<T>[];
}

export type SqlTxnResult<RowType> = {
statement: number; // Statement index from input array
tag: string; // Short stmt tag
start: string; // Start timestamp, encoded as RFC3339
end: string; // End timestamp, encoded as RFC3339
rows_affected: number;
columns?: SqlResultColumn[];
rows?: RowType[];
error?: Error;
};

export type SqlResultColumn = {
name: string;
type: string;
oid: number;
};

export type SqlExecutionErrorMessage = {
message: string;
code: string;
severity: string;
source: { file: string; line: number; function: "string" };
};

export const SQL_API_PATH = "/api/v2/sql/";

/**
* executeSql executes the provided SQL statements in a single transaction
* over HTTP.
*
* @param req execution request details
*/
export function executeSql<RowType>(
req: SqlExecutionRequest,
): Promise<SqlExecutionResponse<RowType>> {
return fetchDataJSON<SqlExecutionResponse<RowType>, SqlExecutionRequest>(
SQL_API_PATH,
req,
);
}

0 comments on commit b75a704

Please sign in to comment.