Skip to content

Commit ff6ac08

Browse files
committed
VTAdmin: Support for conclude txn
Signed-off-by: Noble Mittal <noblemittal@outlook.com>
1 parent c74db2b commit ff6ac08

File tree

7 files changed

+224
-3
lines changed

7 files changed

+224
-3
lines changed

go/vt/vtadmin/api.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,7 @@ func (api *API) Handler() http.Handler {
415415
router.HandleFunc("/tablet/{tablet}/stop_replication", httpAPI.Adapt(vtadminhttp.StopReplication)).Name("API.StopReplication").Methods("PUT", "OPTIONS")
416416
router.HandleFunc("/tablet/{tablet}/externally_promoted", httpAPI.Adapt(vtadminhttp.TabletExternallyPromoted)).Name("API.TabletExternallyPromoted").Methods("POST")
417417
router.HandleFunc("/transactions/{cluster_id}/{keyspace}", httpAPI.Adapt(vtadminhttp.GetUnresolvedTransactions)).Name("API.GetUnresolvedTransactions").Methods("GET")
418+
router.HandleFunc("/transaction/{cluster_id}/{dtid}/conclude", httpAPI.Adapt(vtadminhttp.ConcludeTransaction)).Name("API.ConcludeTransaction")
418419
router.HandleFunc("/vschema/{cluster_id}/{keyspace}", httpAPI.Adapt(vtadminhttp.GetVSchema)).Name("API.GetVSchema")
419420
router.HandleFunc("/vschemas", httpAPI.Adapt(vtadminhttp.GetVSchemas)).Name("API.GetVSchemas")
420421
router.HandleFunc("/vtctlds", httpAPI.Adapt(vtadminhttp.GetVtctlds)).Name("API.GetVtctlds")
@@ -531,6 +532,27 @@ func (api *API) CompleteSchemaMigration(ctx context.Context, req *vtadminpb.Comp
531532
return c.CompleteSchemaMigration(ctx, req.Request)
532533
}
533534

535+
// ConcludeTransaction is part of the vtadminpb.VTAdminServer interface.
536+
func (api *API) ConcludeTransaction(ctx context.Context, req *vtadminpb.ConcludeTransactionRequest) (*vtctldatapb.ConcludeTransactionResponse, error) {
537+
span, ctx := trace.NewSpan(ctx, "API.ConcludeTransaction")
538+
defer span.Finish()
539+
540+
c, err := api.getClusterForRequest(req.ClusterId)
541+
if err != nil {
542+
return nil, err
543+
}
544+
545+
cluster.AnnotateSpan(c, span)
546+
547+
if !api.authz.IsAuthorized(ctx, c.ID, rbac.KeyspaceResource, rbac.GetAction) {
548+
return nil, nil
549+
}
550+
551+
return c.Vtctld.ConcludeTransaction(ctx, &vtctldatapb.ConcludeTransactionRequest{
552+
Dtid: req.Dtid,
553+
})
554+
}
555+
534556
// CreateKeyspace is part of the vtadminpb.VTAdminServer interface.
535557
func (api *API) CreateKeyspace(ctx context.Context, req *vtadminpb.CreateKeyspaceRequest) (*vtadminpb.CreateKeyspaceResponse, error) {
536558
span, ctx := trace.NewSpan(ctx, "API.CreateKeyspace")

go/vt/vtadmin/http/transactions.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,14 @@ func GetUnresolvedTransactions(ctx context.Context, r Request, api *API) *JSONRe
3434

3535
return NewJSONResponse(res, err)
3636
}
37+
38+
func ConcludeTransaction(ctx context.Context, r Request, api *API) *JSONResponse {
39+
vars := r.Vars()
40+
41+
res, err := api.server.ConcludeTransaction(ctx, &vtadminpb.ConcludeTransactionRequest{
42+
ClusterId: vars["cluster_id"],
43+
Dtid: vars["dtid"],
44+
})
45+
46+
return NewJSONResponse(res, err)
47+
}

web/vtadmin/src/api/http.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,19 @@ export const fetchTransactions = async ({ clusterID, keyspace }: FetchTransactio
435435
return vtctldata.GetUnresolvedTransactionsResponse.create(result);
436436
};
437437

438+
export interface ConcludeTransactionParams {
439+
clusterID: string;
440+
dtid: string;
441+
}
442+
443+
export const concludeTransaction = async ({ clusterID, dtid }: ConcludeTransactionParams) => {
444+
const { result } = await vtfetch(`/api/transaction/${clusterID}/${dtid}/conclude`);
445+
const err = vtctldata.ConcludeTransactionResponse.verify(result);
446+
if (err) throw Error(err);
447+
448+
return vtctldata.ConcludeTransactionResponse.create(result);
449+
};
450+
438451
export const fetchWorkflows = async () => {
439452
const { result } = await vtfetch(`/api/workflows`);
440453

web/vtadmin/src/components/routes/Transactions.tsx

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,12 @@ import { formatTransactionState } from '../../util/transactions';
2929
import { ShardLink } from '../links/ShardLink';
3030
import { formatDateTime, formatRelativeTimeInSeconds } from '../../util/time';
3131
import { orderBy } from 'lodash-es';
32+
import { ReadOnlyGate } from '../ReadOnlyGate';
33+
import TransactionActions from './transactions/TransactionActions';
34+
import { isReadOnlyMode } from '../../util/env';
3235

33-
const COLUMNS = ['ID', 'State', 'Participants', 'Time Created'];
36+
const COLUMNS = ['ID', 'State', 'Participants', 'Time Created', 'Actions'];
37+
const READ_ONLY_COLUMNS = ['ID', 'State', 'Participants', 'Time Created'];
3438

3539
export const Transactions = () => {
3640
useDocumentTitle('In Flight Distributed Transactions');
@@ -82,6 +86,15 @@ export const Transactions = () => {
8286
{formatRelativeTimeInSeconds(row.time_created)}
8387
</div>
8488
</DataCell>
89+
<ReadOnlyGate>
90+
<DataCell>
91+
<TransactionActions
92+
refetchTransactions={transactionsQuery.refetch}
93+
clusterID={params.clusterID as string}
94+
dtid={row.dtid as string}
95+
/>
96+
</DataCell>
97+
</ReadOnlyGate>
8598
</tr>
8699
);
87100
});
@@ -94,8 +107,9 @@ export const Transactions = () => {
94107
</WorkspaceHeader>
95108

96109
<ContentContainer>
110+
<div className='flex flex-row gap-1 max-w-[740px]'>
97111
<Select
98-
className="block w-full max-w-[740px]"
112+
className="block grow-1 min-w-[300px]"
99113
disabled={keyspacesQuery.isLoading}
100114
inputClassName="block w-full"
101115
items={keyspaces}
@@ -109,7 +123,23 @@ export const Transactions = () => {
109123
renderItem={(ks) => `${ks?.keyspace?.name} (${ks?.cluster?.id})`}
110124
selectedItem={selectedKeyspace}
111125
/>
112-
<DataTable columns={COLUMNS} data={transactions} renderRows={renderRows} />
126+
<Select
127+
className="block grow-1 min-w-[300px]"
128+
disabled={keyspacesQuery.isLoading}
129+
inputClassName="block w-full"
130+
items={keyspaces}
131+
label="Keyspace"
132+
onChange={(ks) => setParams({ clusterID: ks?.cluster?.id!, keyspace: ks?.keyspace?.name! })}
133+
placeholder={
134+
keyspacesQuery.isLoading
135+
? 'Loading keyspaces...'
136+
: 'Select a keyspace to view unresolved transactions'
137+
}
138+
renderItem={(ks) => `${ks?.keyspace?.name} (${ks?.cluster?.id})`}
139+
selectedItem={selectedKeyspace}
140+
/>
141+
</div>
142+
<DataTable columns={isReadOnlyMode() ? READ_ONLY_COLUMNS : COLUMNS} data={transactions} renderRows={renderRows} />
113143
<QueryLoadingPlaceholder query={transactionsQuery} />
114144
</ContentContainer>
115145
</div>
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import React from 'react';
2+
import { Icon, Icons } from '../../Icon';
3+
import Dialog from '../../dialog/Dialog';
4+
import { UseMutationResult } from 'react-query';
5+
6+
interface TransactionActionProps {
7+
isOpen: boolean;
8+
mutation: UseMutationResult<any, any, any>;
9+
title: string;
10+
confirmText: string;
11+
successText: string;
12+
errorText: string;
13+
loadingText: string;
14+
description?: string;
15+
body?: JSX.Element;
16+
refetchTransactions: Function;
17+
closeDialog: () => void;
18+
}
19+
20+
const TransactionAction: React.FC<TransactionActionProps> = ({
21+
isOpen,
22+
closeDialog,
23+
mutation,
24+
title,
25+
confirmText,
26+
description,
27+
successText,
28+
loadingText,
29+
errorText,
30+
refetchTransactions,
31+
body,
32+
}) => {
33+
const onCloseDialog = () => {
34+
setTimeout(mutation.reset, 500);
35+
closeDialog();
36+
};
37+
38+
const hasRun = mutation.data || mutation.error;
39+
const onConfirm = () => {
40+
mutation.mutate(
41+
{},
42+
{
43+
onSuccess: () => {
44+
refetchTransactions();
45+
},
46+
}
47+
);
48+
};
49+
return (
50+
<Dialog
51+
isOpen={isOpen}
52+
confirmText={hasRun ? 'Close' : confirmText}
53+
cancelText="Cancel"
54+
onConfirm={hasRun ? onCloseDialog : onConfirm}
55+
loadingText={loadingText}
56+
loading={mutation.isLoading}
57+
onCancel={onCloseDialog}
58+
onClose={onCloseDialog}
59+
hideCancel={hasRun}
60+
title={hasRun ? undefined : title}
61+
description={hasRun ? undefined : description}
62+
>
63+
<div className="w-full">
64+
{!hasRun && body}
65+
{mutation.data && !mutation.error && (
66+
<div className="w-full flex flex-col justify-center items-center">
67+
<span className="flex h-12 w-12 relative items-center justify-center">
68+
<Icon className="fill-current text-green-500" icon={Icons.checkSuccess} />
69+
</span>
70+
<div className="text-lg mt-3 font-bold text-center">{successText}</div>
71+
</div>
72+
)}
73+
{mutation.error && (
74+
<div className="w-full flex flex-col justify-center items-center">
75+
<span className="flex h-12 w-12 relative items-center justify-center">
76+
<Icon className="fill-current text-red-500" icon={Icons.alertFail} />
77+
</span>
78+
<div className="text-lg mt-3 font-bold text-center">{errorText}</div>
79+
<div className="text-sm">{mutation.error.message}</div>
80+
</div>
81+
)}
82+
</div>
83+
</Dialog>
84+
);
85+
};
86+
87+
export default TransactionAction;
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import React, { useState } from 'react';
2+
import Dropdown from '../../dropdown/Dropdown';
3+
import MenuItem from '../../dropdown/MenuItem';
4+
import { Icons } from '../../Icon';
5+
import TransactionAction from './TransactionAction';
6+
import { useConcludeTransaction } from '../../../hooks/api';
7+
8+
interface TransactionActionsProps {
9+
refetchTransactions: Function;
10+
clusterID: string;
11+
dtid: string;
12+
}
13+
14+
const TransactionActions: React.FC<TransactionActionsProps> = ({ refetchTransactions, clusterID, dtid }) => {
15+
const [currentDialog, setCurrentDialog] = useState<string>('');
16+
const closeDialog = () => setCurrentDialog('');
17+
18+
const concludeTransactionMutation = useConcludeTransaction({ clusterID, dtid });
19+
20+
return (
21+
<div className="w-min inline-block">
22+
<Dropdown dropdownButton={Icons.info}>
23+
<MenuItem onClick={() => setCurrentDialog('Conclude Transaction')}>Conclude Transaction</MenuItem>
24+
</Dropdown>
25+
<TransactionAction
26+
title="Conclude Transaction"
27+
confirmText="Conclude"
28+
loadingText="Concluding"
29+
mutation={concludeTransactionMutation}
30+
successText="Concluded transaction"
31+
errorText={`Error occured while concluding the transaction (ID: ${dtid})`}
32+
closeDialog={closeDialog}
33+
isOpen={currentDialog === 'Conclude Transaction'}
34+
refetchTransactions={refetchTransactions}
35+
body={
36+
<div className="text-sm mt-3">
37+
Conclude the transaction with id: <span className="font-mono bg-gray-300">{dtid}</span>.
38+
</div>
39+
}
40+
/>
41+
</div>
42+
);
43+
};
44+
45+
export default TransactionActions;

web/vtadmin/src/hooks/api.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ import {
8484
stopWorkflow,
8585
FetchTransactionsParams,
8686
fetchTransactions,
87+
concludeTransaction,
8788
} from '../api/http';
8889
import { vtadmin as pb, vtctldata } from '../proto/vtadmin';
8990
import { formatAlias } from '../util/tablets';
@@ -418,6 +419,18 @@ export const useTransactions = (
418419
return useQuery(['transactions', params], () => fetchTransactions(params), { ...options });
419420
};
420421

422+
/**
423+
* useConcludeTransaction is a mutate hook that concludes a transaction.
424+
*/
425+
export const useConcludeTransaction = (
426+
params: Parameters<typeof concludeTransaction>[0],
427+
options?: UseMutationOptions<Awaited<ReturnType<typeof concludeTransaction>>, Error>
428+
) => {
429+
return useMutation<Awaited<ReturnType<typeof concludeTransaction>>, Error>(() => {
430+
return concludeTransaction(params);
431+
}, options);
432+
};
433+
421434
export const useVTExplain = (
422435
params: Parameters<typeof fetchVTExplain>[0],
423436
options?: UseQueryOptions<pb.VTExplainResponse, Error> | undefined

0 commit comments

Comments
 (0)