-
Notifications
You must be signed in to change notification settings - Fork 3.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
cluster-ui: add sql api request wrapper and clusterLocks request
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
Showing
4 changed files
with
235 additions
and
0 deletions.
There are no files selected for viewing
118 changes: 118 additions & 0 deletions
118
pkg/ui/workspaces/cluster-ui/src/api/clusterLocksApi.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
); | ||
} |