From ff6ac088c5e1810fb26d04f59667c9b8da601cfb Mon Sep 17 00:00:00 2001 From: Noble Mittal Date: Tue, 24 Sep 2024 19:47:43 +0530 Subject: [PATCH] VTAdmin: Support for conclude txn Signed-off-by: Noble Mittal --- go/vt/vtadmin/api.go | 22 +++++ go/vt/vtadmin/http/transactions.go | 11 +++ web/vtadmin/src/api/http.ts | 13 +++ .../src/components/routes/Transactions.tsx | 36 +++++++- .../routes/transactions/TransactionAction.tsx | 87 +++++++++++++++++++ .../transactions/TransactionActions.tsx | 45 ++++++++++ web/vtadmin/src/hooks/api.ts | 13 +++ 7 files changed, 224 insertions(+), 3 deletions(-) create mode 100644 web/vtadmin/src/components/routes/transactions/TransactionAction.tsx create mode 100644 web/vtadmin/src/components/routes/transactions/TransactionActions.tsx diff --git a/go/vt/vtadmin/api.go b/go/vt/vtadmin/api.go index 2dc83b6e233..f36e1fd0e99 100644 --- a/go/vt/vtadmin/api.go +++ b/go/vt/vtadmin/api.go @@ -415,6 +415,7 @@ func (api *API) Handler() http.Handler { router.HandleFunc("/tablet/{tablet}/stop_replication", httpAPI.Adapt(vtadminhttp.StopReplication)).Name("API.StopReplication").Methods("PUT", "OPTIONS") router.HandleFunc("/tablet/{tablet}/externally_promoted", httpAPI.Adapt(vtadminhttp.TabletExternallyPromoted)).Name("API.TabletExternallyPromoted").Methods("POST") router.HandleFunc("/transactions/{cluster_id}/{keyspace}", httpAPI.Adapt(vtadminhttp.GetUnresolvedTransactions)).Name("API.GetUnresolvedTransactions").Methods("GET") + router.HandleFunc("/transaction/{cluster_id}/{dtid}/conclude", httpAPI.Adapt(vtadminhttp.ConcludeTransaction)).Name("API.ConcludeTransaction") router.HandleFunc("/vschema/{cluster_id}/{keyspace}", httpAPI.Adapt(vtadminhttp.GetVSchema)).Name("API.GetVSchema") router.HandleFunc("/vschemas", httpAPI.Adapt(vtadminhttp.GetVSchemas)).Name("API.GetVSchemas") router.HandleFunc("/vtctlds", httpAPI.Adapt(vtadminhttp.GetVtctlds)).Name("API.GetVtctlds") @@ -531,6 +532,27 @@ func (api *API) CompleteSchemaMigration(ctx context.Context, req *vtadminpb.Comp return c.CompleteSchemaMigration(ctx, req.Request) } +// ConcludeTransaction is part of the vtadminpb.VTAdminServer interface. +func (api *API) ConcludeTransaction(ctx context.Context, req *vtadminpb.ConcludeTransactionRequest) (*vtctldatapb.ConcludeTransactionResponse, error) { + span, ctx := trace.NewSpan(ctx, "API.ConcludeTransaction") + defer span.Finish() + + c, err := api.getClusterForRequest(req.ClusterId) + if err != nil { + return nil, err + } + + cluster.AnnotateSpan(c, span) + + if !api.authz.IsAuthorized(ctx, c.ID, rbac.KeyspaceResource, rbac.GetAction) { + return nil, nil + } + + return c.Vtctld.ConcludeTransaction(ctx, &vtctldatapb.ConcludeTransactionRequest{ + Dtid: req.Dtid, + }) +} + // CreateKeyspace is part of the vtadminpb.VTAdminServer interface. func (api *API) CreateKeyspace(ctx context.Context, req *vtadminpb.CreateKeyspaceRequest) (*vtadminpb.CreateKeyspaceResponse, error) { span, ctx := trace.NewSpan(ctx, "API.CreateKeyspace") diff --git a/go/vt/vtadmin/http/transactions.go b/go/vt/vtadmin/http/transactions.go index 2bcf4ee7e2c..12359863677 100644 --- a/go/vt/vtadmin/http/transactions.go +++ b/go/vt/vtadmin/http/transactions.go @@ -34,3 +34,14 @@ func GetUnresolvedTransactions(ctx context.Context, r Request, api *API) *JSONRe return NewJSONResponse(res, err) } + +func ConcludeTransaction(ctx context.Context, r Request, api *API) *JSONResponse { + vars := r.Vars() + + res, err := api.server.ConcludeTransaction(ctx, &vtadminpb.ConcludeTransactionRequest{ + ClusterId: vars["cluster_id"], + Dtid: vars["dtid"], + }) + + return NewJSONResponse(res, err) +} diff --git a/web/vtadmin/src/api/http.ts b/web/vtadmin/src/api/http.ts index 529cb3c4e3c..1e01cd03b83 100644 --- a/web/vtadmin/src/api/http.ts +++ b/web/vtadmin/src/api/http.ts @@ -435,6 +435,19 @@ export const fetchTransactions = async ({ clusterID, keyspace }: FetchTransactio return vtctldata.GetUnresolvedTransactionsResponse.create(result); }; +export interface ConcludeTransactionParams { + clusterID: string; + dtid: string; +} + +export const concludeTransaction = async ({ clusterID, dtid }: ConcludeTransactionParams) => { + const { result } = await vtfetch(`/api/transaction/${clusterID}/${dtid}/conclude`); + const err = vtctldata.ConcludeTransactionResponse.verify(result); + if (err) throw Error(err); + + return vtctldata.ConcludeTransactionResponse.create(result); +}; + export const fetchWorkflows = async () => { const { result } = await vtfetch(`/api/workflows`); diff --git a/web/vtadmin/src/components/routes/Transactions.tsx b/web/vtadmin/src/components/routes/Transactions.tsx index 8a40a039394..fb80c286e57 100644 --- a/web/vtadmin/src/components/routes/Transactions.tsx +++ b/web/vtadmin/src/components/routes/Transactions.tsx @@ -29,8 +29,12 @@ import { formatTransactionState } from '../../util/transactions'; import { ShardLink } from '../links/ShardLink'; import { formatDateTime, formatRelativeTimeInSeconds } from '../../util/time'; import { orderBy } from 'lodash-es'; +import { ReadOnlyGate } from '../ReadOnlyGate'; +import TransactionActions from './transactions/TransactionActions'; +import { isReadOnlyMode } from '../../util/env'; -const COLUMNS = ['ID', 'State', 'Participants', 'Time Created']; +const COLUMNS = ['ID', 'State', 'Participants', 'Time Created', 'Actions']; +const READ_ONLY_COLUMNS = ['ID', 'State', 'Participants', 'Time Created']; export const Transactions = () => { useDocumentTitle('In Flight Distributed Transactions'); @@ -82,6 +86,15 @@ export const Transactions = () => { {formatRelativeTimeInSeconds(row.time_created)} + + + + + ); }); @@ -94,8 +107,9 @@ export const Transactions = () => { +
setParams({ clusterID: ks?.cluster?.id!, keyspace: ks?.keyspace?.name! })} + placeholder={ + keyspacesQuery.isLoading + ? 'Loading keyspaces...' + : 'Select a keyspace to view unresolved transactions' + } + renderItem={(ks) => `${ks?.keyspace?.name} (${ks?.cluster?.id})`} + selectedItem={selectedKeyspace} + /> +
+
diff --git a/web/vtadmin/src/components/routes/transactions/TransactionAction.tsx b/web/vtadmin/src/components/routes/transactions/TransactionAction.tsx new file mode 100644 index 00000000000..bfb91569421 --- /dev/null +++ b/web/vtadmin/src/components/routes/transactions/TransactionAction.tsx @@ -0,0 +1,87 @@ +import React from 'react'; +import { Icon, Icons } from '../../Icon'; +import Dialog from '../../dialog/Dialog'; +import { UseMutationResult } from 'react-query'; + +interface TransactionActionProps { + isOpen: boolean; + mutation: UseMutationResult; + title: string; + confirmText: string; + successText: string; + errorText: string; + loadingText: string; + description?: string; + body?: JSX.Element; + refetchTransactions: Function; + closeDialog: () => void; +} + +const TransactionAction: React.FC = ({ + isOpen, + closeDialog, + mutation, + title, + confirmText, + description, + successText, + loadingText, + errorText, + refetchTransactions, + body, +}) => { + const onCloseDialog = () => { + setTimeout(mutation.reset, 500); + closeDialog(); + }; + + const hasRun = mutation.data || mutation.error; + const onConfirm = () => { + mutation.mutate( + {}, + { + onSuccess: () => { + refetchTransactions(); + }, + } + ); + }; + return ( + +
+ {!hasRun && body} + {mutation.data && !mutation.error && ( +
+ + + +
{successText}
+
+ )} + {mutation.error && ( +
+ + + +
{errorText}
+
{mutation.error.message}
+
+ )} +
+
+ ); +}; + +export default TransactionAction; diff --git a/web/vtadmin/src/components/routes/transactions/TransactionActions.tsx b/web/vtadmin/src/components/routes/transactions/TransactionActions.tsx new file mode 100644 index 00000000000..a9b71a010dc --- /dev/null +++ b/web/vtadmin/src/components/routes/transactions/TransactionActions.tsx @@ -0,0 +1,45 @@ +import React, { useState } from 'react'; +import Dropdown from '../../dropdown/Dropdown'; +import MenuItem from '../../dropdown/MenuItem'; +import { Icons } from '../../Icon'; +import TransactionAction from './TransactionAction'; +import { useConcludeTransaction } from '../../../hooks/api'; + +interface TransactionActionsProps { + refetchTransactions: Function; + clusterID: string; + dtid: string; +} + +const TransactionActions: React.FC = ({ refetchTransactions, clusterID, dtid }) => { + const [currentDialog, setCurrentDialog] = useState(''); + const closeDialog = () => setCurrentDialog(''); + + const concludeTransactionMutation = useConcludeTransaction({ clusterID, dtid }); + + return ( +
+ + setCurrentDialog('Conclude Transaction')}>Conclude Transaction + + + Conclude the transaction with id: {dtid}. +
+ } + /> + + ); +}; + +export default TransactionActions; diff --git a/web/vtadmin/src/hooks/api.ts b/web/vtadmin/src/hooks/api.ts index 1a552dad167..673ac19f413 100644 --- a/web/vtadmin/src/hooks/api.ts +++ b/web/vtadmin/src/hooks/api.ts @@ -84,6 +84,7 @@ import { stopWorkflow, FetchTransactionsParams, fetchTransactions, + concludeTransaction, } from '../api/http'; import { vtadmin as pb, vtctldata } from '../proto/vtadmin'; import { formatAlias } from '../util/tablets'; @@ -418,6 +419,18 @@ export const useTransactions = ( return useQuery(['transactions', params], () => fetchTransactions(params), { ...options }); }; +/** + * useConcludeTransaction is a mutate hook that concludes a transaction. + */ +export const useConcludeTransaction = ( + params: Parameters[0], + options?: UseMutationOptions>, Error> +) => { + return useMutation>, Error>(() => { + return concludeTransaction(params); + }, options); +}; + export const useVTExplain = ( params: Parameters[0], options?: UseQueryOptions | undefined