From 98aca73ae2632c2fa4441c694cdf6d319b03f2de Mon Sep 17 00:00:00 2001 From: Xin Hao Zhang Date: Thu, 5 Oct 2023 13:47:13 -0400 Subject: [PATCH] ui: include SERIALIZATION_CONFLICT events in txn contention details This commit introduces `SERIALIZATION_CONFLICT` contention event details to the txn insights details page. If a transaction insight failed with a 40001 error code, we will query `crdb_internal.transaction_contention_events` to see if there is any serialization conflict event containing the blocking txn exec id, fingerprint id and conflict location information. A section called `Failed Execution` will appear when this information is available with the following info: - blocking txn execution id - blocking txn finggerprint id - conflict location - db, table and index names Epic: none Closes: #111650 Release note (ui change): Txn insight details will show the following details when we have information on a txn execution with a 40001 error code and we have captured the conflicting txn meta (only available if the txn had not yet committed at the time of execution). A section called `Failed Execution` will appear when this information is available with the following info: - blocking txn execution id - blocking txn finggerprint id - conflict location - db, table and index names --- .../cluster-ui/src/api/contentionApi.ts | 10 +- .../src/api/txnInsightDetailsApi.ts | 15 ++- .../cluster-ui/src/api/txnInsightsApi.spec.ts | 4 + .../cluster-ui/src/insights/types.ts | 12 +++ .../cluster-ui/src/insights/utils.spec.ts | 1 + .../failedInsightDetailsPanel.tsx | 102 ++++++++++++++++++ .../transactionInsightDetails.tsx | 9 +- .../transactionInsightDetailsOverviewTab.tsx | 16 ++- 8 files changed, 160 insertions(+), 9 deletions(-) create mode 100644 pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/failedInsightDetailsPanel.tsx diff --git a/pkg/ui/workspaces/cluster-ui/src/api/contentionApi.ts b/pkg/ui/workspaces/cluster-ui/src/api/contentionApi.ts index e1a676d5559c..5961e2561bfa 100644 --- a/pkg/ui/workspaces/cluster-ui/src/api/contentionApi.ts +++ b/pkg/ui/workspaces/cluster-ui/src/api/contentionApi.ts @@ -22,6 +22,7 @@ import { } from "./sqlApi"; import { ContentionDetails, + ContentionTypeKey, InsightExecEnum, InsightNameEnum, TxnContentionInsightDetails, @@ -57,6 +58,7 @@ export type ContentionResponseColumns = { table_name: string; index_name: string; key: string; + contention_type: ContentionTypeKey; }; export async function getContentionDetailsApi( @@ -95,7 +97,9 @@ export async function getContentionDetailsApi( x.rows.forEach(row => { contentionDetails.push({ blockingExecutionID: row.blocking_txn_id, - blockingTxnFingerprintID: row.blocking_txn_fingerprint_id, + blockingTxnFingerprintID: FixFingerprintHexValue( + row.blocking_txn_fingerprint_id, + ), blockingTxnQuery: null, waitingTxnID: row.waiting_txn_id, waitingTxnFingerprintID: row.waiting_txn_fingerprint_id, @@ -113,6 +117,7 @@ export async function getContentionDetailsApi( row.index_name && row.index_name !== "" ? row.index_name : "index not found", + contentionType: row.contention_type, }); }); }); @@ -190,7 +195,8 @@ function contentionDetailsQuery(filters?: ContentionFilters) { database_name, schema_name, table_name, - index_name + index_name, + contention_type FROM crdb_internal.transaction_contention_events ${whereClause} diff --git a/pkg/ui/workspaces/cluster-ui/src/api/txnInsightDetailsApi.ts b/pkg/ui/workspaces/cluster-ui/src/api/txnInsightDetailsApi.ts index 7c83e51b36be..133221027599 100644 --- a/pkg/ui/workspaces/cluster-ui/src/api/txnInsightDetailsApi.ts +++ b/pkg/ui/workspaces/cluster-ui/src/api/txnInsightDetailsApi.ts @@ -15,7 +15,11 @@ import { sqlApiErrorMessage, SqlApiResponse, } from "./sqlApi"; -import { InsightNameEnum, TxnInsightDetails } from "../insights"; +import { + InsightNameEnum, + StmtFailureCodesStr, + TxnInsightDetails, +} from "../insights"; import { formatStmtInsights, stmtInsightsByTxnExecutionQuery, @@ -137,8 +141,15 @@ export async function getTxnInsightDetailsApi( insight => insight.name === InsightNameEnum.highContention, ); + const isRetrySerializableFailure = + txnInsightDetails.txnDetails?.errorCode === + StmtFailureCodesStr.RETRY_SERIALIZABLE; + try { - if (!req.excludeContention && highContention) { + if ( + !req.excludeContention && + (highContention || isRetrySerializableFailure) + ) { const contentionInfo = await getTxnInsightsContentionDetailsApi(req); txnInsightDetails.blockingContentionDetails = contentionInfo?.blockingContentionDetails; diff --git a/pkg/ui/workspaces/cluster-ui/src/api/txnInsightsApi.spec.ts b/pkg/ui/workspaces/cluster-ui/src/api/txnInsightsApi.spec.ts index 9ff5e97a58bd..c0327ead4c68 100644 --- a/pkg/ui/workspaces/cluster-ui/src/api/txnInsightsApi.spec.ts +++ b/pkg/ui/workspaces/cluster-ui/src/api/txnInsightsApi.spec.ts @@ -67,6 +67,7 @@ describe("test txn insights api functions", () => { blocking_txn_fingerprint_id: "4329ab5f4493f82d", waiting_txn_id: waitingTxnID, waiting_txn_fingerprint_id: "1831d909096f992c", + contention_type: "LOCK_WAIT", }; afterEach(() => { @@ -111,6 +112,7 @@ describe("test txn insights api functions", () => { waitingTxnFingerprintID: contentionDetailsMock.waiting_txn_fingerprint_id, waitingTxnID: contentionDetailsMock.waiting_txn_id, + contentionType: "LOCK_WAIT", }, ], execType: InsightExecEnum.TRANSACTION, @@ -157,6 +159,7 @@ describe("test txn insights api functions", () => { waitingTxnFingerprintID: contentionDetailsMock.waiting_txn_fingerprint_id, waitingTxnID: contentionDetailsMock.waiting_txn_id, + contentionType: "LOCK_WAIT", }, ], execType: InsightExecEnum.TRANSACTION, @@ -212,6 +215,7 @@ describe("test txn insights api functions", () => { waitingTxnFingerprintID: contentionDetailsMock.waiting_txn_fingerprint_id, waitingTxnID: contentionDetailsMock.waiting_txn_id, + contentionType: "LOCK_WAIT", }, ], execType: InsightExecEnum.TRANSACTION, diff --git a/pkg/ui/workspaces/cluster-ui/src/insights/types.ts b/pkg/ui/workspaces/cluster-ui/src/insights/types.ts index d331c872320e..c769903962b6 100644 --- a/pkg/ui/workspaces/cluster-ui/src/insights/types.ts +++ b/pkg/ui/workspaces/cluster-ui/src/insights/types.ts @@ -10,6 +10,13 @@ import moment, { Moment } from "moment-timezone"; import { Filters } from "../queryFilter"; +import { cockroach } from "@cockroachlabs/crdb-protobuf-client"; + +const ContentionTypeEnum = cockroach.sql.contentionpb.ContentionType; + +export type ContentionTypeKey = { + [K in keyof typeof ContentionTypeEnum]: K; +}[keyof typeof ContentionTypeEnum]; // This enum corresponds to the string enum for `problems` in `cluster_execution_insights` export enum InsightNameEnum { @@ -83,6 +90,7 @@ export type ContentionDetails = { tableName: string; indexName: string; contentionTimeMs: number; + contentionType: ContentionTypeKey; }; // The return type of getTxnInsightsContentionDetailsApi. @@ -354,3 +362,7 @@ export interface insightDetails { duration?: number; description: string; } + +export enum StmtFailureCodesStr { + RETRY_SERIALIZABLE = "40001", +} diff --git a/pkg/ui/workspaces/cluster-ui/src/insights/utils.spec.ts b/pkg/ui/workspaces/cluster-ui/src/insights/utils.spec.ts index 555568f3fb7f..ef27b66445db 100644 --- a/pkg/ui/workspaces/cluster-ui/src/insights/utils.spec.ts +++ b/pkg/ui/workspaces/cluster-ui/src/insights/utils.spec.ts @@ -51,6 +51,7 @@ const blockedContentionMock: ContentionDetails = { tableName: "table", indexName: "index", contentionTimeMs: 500, + contentionType: "LOCK_WAIT", }; const statementInsightMock: StmtInsightEvent = { diff --git a/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/failedInsightDetailsPanel.tsx b/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/failedInsightDetailsPanel.tsx new file mode 100644 index 000000000000..1188a7374881 --- /dev/null +++ b/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/failedInsightDetailsPanel.tsx @@ -0,0 +1,102 @@ +// Copyright 2023 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 React from "react"; +import { Col, Row } from "antd"; +import { ContentionDetails } from "../types"; +import { SummaryCard, SummaryCardItem } from "../../summaryCard"; +import { Heading } from "@cockroachlabs/ui-components"; +import classNames from "classnames/bind"; + +// TODO (xinhaoz) we should organize these common page details styles into its own file. +import styles from "../../statementDetails/statementDetails.module.scss"; + +import "antd/lib/row/style"; +import "antd/lib/col/style"; +import { TransactionDetailsLink } from "../workloadInsights/util"; + +const cx = classNames.bind(styles); + +type Props = { + conflictDetails: ContentionDetails; +}; + +const FailedInsightDetailsPanelLabels = { + SECTION_HEADER: "Failed Execution", + CONFLICTING_TRANSACTION_HEADER: "Conflicting Transaction", + CONFLICTING_TRANSACTION_EXEC_ID: "Transaction Execution", + CONFLICTING_TRANSACTION_FINGERPRINT: "Transaction Fingerprint", + CONFLICT_LOCATION_HEADER: "Conflict Location", + DATABASE_NAME: "Database", + TABLE_NAME: "Table", + INDEX_NAME: "Index", +}; + +export const FailedInsightDetailsPanel: React.FC = ({ + conflictDetails, +}) => { + return ( +
+ + + + {FailedInsightDetailsPanelLabels.SECTION_HEADER} + + + + + + { + FailedInsightDetailsPanelLabels.CONFLICTING_TRANSACTION_HEADER + } + + + + + + + + + {FailedInsightDetailsPanelLabels.CONFLICT_LOCATION_HEADER} + + + + + + + + + +
+ ); +}; diff --git a/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/transactionInsightDetails.tsx b/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/transactionInsightDetails.tsx index 16d0b95782d3..5c18c5a04089 100644 --- a/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/transactionInsightDetails.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/transactionInsightDetails.tsx @@ -18,7 +18,11 @@ import "antd/lib/tabs/style"; import { Button } from "src/button"; import { getMatchParamByName } from "src/util/query"; import { TxnInsightDetailsRequest, TxnInsightDetailsReqErrs } from "src/api"; -import { InsightNameEnum, TxnInsightDetails } from "../types"; +import { + InsightNameEnum, + StmtFailureCodesStr, + TxnInsightDetails, +} from "../types"; import { commonStyles } from "src/common"; import { TimeScale } from "../../timeScaleDropdown"; @@ -94,7 +98,8 @@ export const TransactionInsightDetails: React.FC< (txnDetails != null && txnDetails.insights.find( i => i.name === InsightNameEnum.highContention, - ) == null); + ) == null && + txnDetails.errorCode !== StmtFailureCodesStr.RETRY_SERIALIZABLE); if (!stmtsComplete || !contentionComplete || txnDetails == null) { // Only fetch if we are missing some information. diff --git a/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/transactionInsightDetailsOverviewTab.tsx b/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/transactionInsightDetailsOverviewTab.tsx index c0a3b9f25386..95c3237cb921 100644 --- a/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/transactionInsightDetailsOverviewTab.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/transactionInsightDetailsOverviewTab.tsx @@ -49,6 +49,7 @@ import insightTableStyles from "src/insightsTable/insightsTable.module.scss"; import insightsDetailsStyles from "src/insights/workloadInsightDetails/insightsDetails.module.scss"; import { InsightsError } from "../insightsErrorComponent"; import { Timestamp } from "../../timestamp"; +import { FailedInsightDetailsPanel } from "./failedInsightDetailsPanel"; const cx = classNames.bind(insightsDetailsStyles); const tableCx = classNames.bind(insightTableStyles); @@ -93,8 +94,9 @@ the maximum number of statements was reached in the console.`; true, ); - const blockingExecutions: ContentionEvent[] = contentionDetails?.map( - event => { + const blockingExecutions: ContentionEvent[] = contentionDetails + ?.filter(e => e.contentionType === "LOCK_WAIT") + .map(event => { const stmtInsight = statements.find( stmt => stmt.statementExecutionID === event.waitingStmtID, ); @@ -113,7 +115,12 @@ the maximum number of statements was reached in the console.`; indexName: event.indexName, stmtInsightEvent: stmtInsight, }; - }, + }); + + // We only expect up to 1 serialization conflict since only 1 can be recorded + // per execution. + const serializationConflict = contentionDetails?.find( + e => e.contentionType === "SERIALIZATION_CONFLICT", ); const insightRecs = getTxnInsightRecommendations(txnDetails); @@ -233,6 +240,9 @@ the maximum number of statements was reached in the console.`; )} + {serializationConflict && ( + + )} {hasContentionInsights && (