This is an automated email from the ASF dual-hosted git repository. awong pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/kudu.git
commit b2633cac57e171e1ee0d98e1ecc3e41b8a2f8cd2 Author: Andrew Wong <aw...@cloudera.com> AuthorDate: Wed May 12 16:16:21 2021 -0700 [txns][tools] show details of a transaction This adds a tool to show information related to a single transaction, including metadata from all of its participants. The goal of this tool is to be useful in debugging slow commits or inconsistencies with a transaction. Currently this outputs info stored in participant's metadata for the transaction. $ kudu txn show 0.0.0.0:7051 0 txn_id | user | state | commit_datetime --------+-------+-----------+------------------------------- 0 | awong | COMMITTED | Fri, 14 May 2021 19:15:18 GMT tablet_id | begin_commit_datetime | commit_datetime ----------------------------------+-------------------------------+------------------------------- 9204d93c0af14292843b6d432d281fb5 | Fri, 14 May 2021 19:15:18 GMT | Fri, 14 May 2021 19:15:18 GMT The new tool abides by the --columns flag, as done with the `kudu txn list` tool, with some additional options for participant-specific columns. Change-Id: I9cc5c23b6b46ee75e38aaffe4773881a1ece7294 Reviewed-on: http://gerrit.cloudera.org:8080/17391 Tested-by: Kudu Jenkins Reviewed-by: Alexey Serbin <aser...@cloudera.com> --- src/kudu/tools/kudu-txn-cli-test.cc | 140 +++++++++++++++- src/kudu/tools/tool_action_txn.cc | 308 +++++++++++++++++++++++++++++------- 2 files changed, 392 insertions(+), 56 deletions(-) diff --git a/src/kudu/tools/kudu-txn-cli-test.cc b/src/kudu/tools/kudu-txn-cli-test.cc index bf03e92..502565b 100644 --- a/src/kudu/tools/kudu-txn-cli-test.cc +++ b/src/kudu/tools/kudu-txn-cli-test.cc @@ -15,6 +15,7 @@ // specific language governing permissions and limitations // under the License. +#include <functional> #include <memory> #include <string> #include <vector> @@ -31,6 +32,7 @@ #include "kudu/util/monotime.h" #include "kudu/util/net/net_util.h" #include "kudu/util/test_macros.h" +#include "kudu/util/test_util.h" using kudu::itest::TabletServerMap; using kudu::itest::TServerDetails; @@ -45,7 +47,10 @@ namespace tools { class KuduTxnsCliTest : public ExternalMiniClusterITestBase { void SetUp() override { NO_FATALS(ExternalMiniClusterITestBase::SetUp()); - NO_FATALS(StartCluster({}, {"--txn_manager_enabled=true"})); + // Some tests will depend on flushing MRSs quickly, so ensure flushes + // happen quickly. + NO_FATALS(StartCluster({ "--flush_threshold_mb=1", "--flush_threshold_secs=1" }, + {"--txn_manager_enabled=true"})); } }; @@ -167,7 +172,138 @@ TEST_F(KuduTxnsCliTest, TestTxnsListHybridTimestamps) { ASSERT_STR_MATCHES(out, R"( txn_id \| *user *\| state \| *commit_datetime *| *commit_hybridtime --------\+-*\+-----------\+------------------------------- - 0 \| *[a-z]* *\| COMMITTED \| .* GMT | P: .* usec, L: .*)"); + 0 \| *[a-z]* *\| COMMITTED \| .* GMT \| P: .* usec, L: .*)"); +} + +TEST_F(KuduTxnsCliTest, TestBasicShowTxn) { + TestWorkload w(cluster_.get()); + w.set_begin_txn(); + w.set_commit_txn(); + w.Setup(); + w.Start(); + while (w.rows_inserted() < 10) { + SleepFor(MonoDelta::FromMilliseconds(10)); + } + w.StopAndJoin(); + + // Check the output of the tool with no arguments. + string out; + ASSERT_OK(RunKuduTool({ "txn", "show", cluster_->master_rpc_addrs()[0].ToString(), "0" }, &out)); + ASSERT_STR_MATCHES(out, R"( txn_id \| *user *\| state \| *commit_datetime +--------\+-*\+-----------\+-* + 0 \| *[a-z]* *\| COMMITTED \| .* GMT + + tablet_id \| *begin_commit_datetime *\| *commit_datetime +----------------------------------\+-*\+-* + [a-z0-9]* \| .*GMT \| .*GMT)"); + + // Check the output specifying none of the transaction status columns. We + // shouldn't display the status table at all. + ASSERT_OK(RunKuduTool({ "txn", "show", cluster_->master_rpc_addrs()[0].ToString(), "0", + "--columns=participant_tablet_id,participant_begin_commit_datetime," + "participant_commit_datetime" }, &out)); + ASSERT_STR_NOT_CONTAINS(out, "txn_id"); + ASSERT_STR_MATCHES(out, + R"( tablet_id \| *begin_commit_datetime *\| *commit_datetime +----------------------------------\+-*\+-* + [a-z0-9]* \| .*GMT \| .*GMT)"); + + // Check the output specifying none of the participant columns. We shouldn't + // display the participant table at all. + ASSERT_OK(RunKuduTool({ "txn", "show", cluster_->master_rpc_addrs()[0].ToString(), "0", + "--columns=txn_id,user,state,commit_datetime" }, &out)); + ASSERT_STR_NOT_CONTAINS(out, "tablet_id"); + ASSERT_STR_MATCHES(out, R"( txn_id \| *user *\| state \| *commit_datetime +--------\+-*\+-----------\+-* + 0 \| *[a-z]* *\| COMMITTED \| .* GMT)"); +} + +TEST_F(KuduTxnsCliTest, TestShowTxnHybridTimestamps) { + TestWorkload w(cluster_.get()); + w.set_begin_txn(); + w.set_commit_txn(); + w.Setup(); + w.Start(); + while (w.rows_inserted() < 10) { + SleepFor(MonoDelta::FromMilliseconds(10)); + } + w.StopAndJoin(); + string out; + // Check that we can specify date time in the transaction status display and + // hybrid time in the participant status display. + ASSERT_OK(RunKuduTool({ "txn", "show", cluster_->master_rpc_addrs()[0].ToString(), "0", + "--columns=txn_id,user,state,commit_datetime,participant_tablet_id," + "participant_begin_commit_hybridtime," + "participant_commit_hybridtime"}, &out)); + ASSERT_STR_MATCHES(out, R"( txn_id \| *user *\| state \| *commit_datetime +--------\+-*\+-----------\+-* + 0 \| *[a-z]* *\| COMMITTED \| .* GMT + + tablet_id \| *begin_commit_hybridtime *\| *commit_hybridtime +----------------------------------\+-*\+-* + [a-z0-9]* \| P: .* usec, L: .* \| P: .* usec, L: .*)"); + + // Check that we can specify hybrid time in the transaction status display and + // date time in the participant status display. + ASSERT_OK(RunKuduTool({ "txn", "show", cluster_->master_rpc_addrs()[0].ToString(), "0", + "--columns=txn_id,user,state,commit_hybridtime,participant_tablet_id," + "participant_begin_commit_datetime," + "participant_commit_datetime"}, &out)); + ASSERT_STR_MATCHES(out, R"( txn_id \| *user *\| state \| *commit_hybridtime +--------\+-*\+-----------\+-* + 0 \| *[a-z]* *\| COMMITTED \| P: .* usec, L: .* + + tablet_id \| *begin_commit_datetime *\| *commit_datetime +----------------------------------\+-*\+-* + [a-z0-9]* \| .* GMT \| .* GMT)"); +} + +TEST_F(KuduTxnsCliTest, TestShowTxnFlushedMRS) { + { + TestWorkload w(cluster_.get()); + w.set_begin_txn(); + w.set_commit_txn(); + w.Setup(); + w.Start(); + while (w.rows_inserted() < 10) { + SleepFor(MonoDelta::FromMilliseconds(10)); + } + w.StopAndJoin(); + } + // Our cluster is set up to flush MRSs quickly, so we should eventually + // report that we've flushed the committed MRS. + ASSERT_EVENTUALLY([&] { + string out; + ASSERT_OK(RunKuduTool({ "txn", "show", cluster_->master_rpc_addrs()[0].ToString(), "0", + "--columns=participant_tablet_id,participant_flushed_committed_mrs" }, + &out)); + ASSERT_STR_NOT_CONTAINS(out, "txn_id"); + ASSERT_STR_MATCHES(out, + R"( tablet_id \| *flushed_committed_mrs +----------------------------------\+-* + [a-z0-9]* \| true)"); + }); + + // In an aborted workload, the report should always report we didn't flush + // the transactional MRS. + TestWorkload w(cluster_.get()); + w.set_begin_txn(); + w.set_rollback_txn(); + w.Setup(); + w.Start(); + while (w.rows_inserted() < 10) { + SleepFor(MonoDelta::FromMilliseconds(10)); + } + w.StopAndJoin(); + string out; + ASSERT_OK(RunKuduTool({ "txn", "show", cluster_->master_rpc_addrs()[0].ToString(), "1", + "--columns=participant_tablet_id,participant_flushed_committed_mrs" }, + &out)); + ASSERT_STR_NOT_CONTAINS(out, "txn_id"); + ASSERT_STR_MATCHES(out, + R"( tablet_id \| *flushed_committed_mrs +----------------------------------\+-* + [a-z0-9]* \| false)"); } } // namespace tools diff --git a/src/kudu/tools/tool_action_txn.cc b/src/kudu/tools/tool_action_txn.cc index c0799cb..ab9d201 100644 --- a/src/kudu/tools/tool_action_txn.cc +++ b/src/kudu/tools/tool_action_txn.cc @@ -18,6 +18,7 @@ #include <sys/types.h> #include <cstdint> +#include <cstring> #include <functional> #include <iostream> #include <memory> @@ -41,10 +42,17 @@ #include "kudu/gutil/strings/split.h" #include "kudu/gutil/strings/stringpiece.h" #include "kudu/gutil/strings/substitute.h" +#include "kudu/gutil/strings/util.h" +#include "kudu/master/master.h" +#include "kudu/tablet/metadata.pb.h" #include "kudu/tools/tool_action.h" #include "kudu/tools/tool_action_common.h" #include "kudu/transactions/transactions.pb.h" #include "kudu/transactions/txn_status_tablet.h" +#include "kudu/transactions/txn_system_client.h" +#include "kudu/tserver/tserver_admin.pb.h" +#include "kudu/util/monotime.h" +#include "kudu/util/net/net_util.h" #include "kudu/util/pb_util.h" #include "kudu/util/slice.h" #include "kudu/util/status.h" @@ -52,9 +60,6 @@ DECLARE_string(columns); -DEFINE_bool(show_hybrid_timestamps, false, - "Whether to show commit timestamps as hybrid timestamps in " - "addition to datetimes."); DEFINE_int64(min_txn_id, -1, "Inclusive minimum transaction ID to display, or -1 for no minimum."); DEFINE_int64(max_txn_id, -1, "Inclusive maximum transaction ID to display, or -1 for no maximum."); DEFINE_string(included_states, "open,abort_in_progress,commit_in_progress,finalize_in_progress", @@ -63,6 +68,8 @@ DEFINE_string(included_states, "open,abort_in_progress,commit_in_progress,finali "'finalize_in_progress', and 'committed', or '*' for all states. By default, shows " "currently active transactions."); +DECLARE_int64(timeout_ms); + using kudu::client::sp::shared_ptr; using kudu::client::KuduClient; using kudu::client::KuduPredicate; @@ -71,9 +78,12 @@ using kudu::client::KuduScanBatch; using kudu::client::KuduTable; using kudu::client::KuduValue; using kudu::clock::HybridClock; +using kudu::tablet::TxnMetadataPB; using kudu::transactions::TxnStatePB; using kudu::transactions::TxnStatusEntryPB; using kudu::transactions::TxnStatusTablet; +using kudu::transactions::TxnSystemClient; +using kudu::tserver::ParticipantOpPB; using std::cout; using std::string; using std::unique_ptr; @@ -88,7 +98,7 @@ namespace tools { namespace { -enum ListTxnsField : int { +enum class ListTxnsField : int { kTxnId, kUser, kState, @@ -96,14 +106,36 @@ enum ListTxnsField : int { kCommitHybridTime, }; -const unordered_map<string, ListTxnsField> kStrToListField { - { "txn_id", kTxnId }, - { "user", kUser }, - { "state", kState }, - { "commit_datetime", kCommitDateTime }, - { "commit_hybridtime", kCommitHybridTime }, +const unordered_map<string, ListTxnsField> kStrToTxnField { + { "txn_id", ListTxnsField::kTxnId }, + { "user", ListTxnsField::kUser }, + { "state", ListTxnsField::kState }, + { "commit_datetime", ListTxnsField::kCommitDateTime }, + { "commit_hybridtime", ListTxnsField::kCommitHybridTime }, +}; + +enum class ParticipantField : int { + kTabletId, + kAborted, + kFlushedCommittedMrs, + kBeginCommitDateTime, + kBeginCommitHybridTime, + kCommitDateTime, + kCommitHybridTime, }; +constexpr const char* kParticipantPrefix = "participant_"; +const unordered_map<string, ParticipantField> kStrToParticipantFields { + { "tablet_id", ParticipantField::kTabletId }, + { "aborted", ParticipantField::kAborted }, + { "flushed_committed_mrs", ParticipantField::kFlushedCommittedMrs }, + { "begin_commit_datetime", ParticipantField::kBeginCommitDateTime }, + { "begin_commit_hybridtime", ParticipantField::kBeginCommitHybridTime }, + { "commit_datetime", ParticipantField::kCommitDateTime }, + { "commit_hybridtime", ParticipantField::kCommitHybridTime }, +}; + +constexpr const char* kTxnIdArg = "txn_id"; const unordered_map<string, TxnStatePB> kStrToState = { { "open", TxnStatePB::OPEN }, { "abort_in_progress", TxnStatePB::ABORT_IN_PROGRESS }, @@ -138,28 +170,87 @@ bool ValidateStatesList(const char* /*flag_name*/, const string& flag_val) { } DEFINE_validator(included_states, &ValidateStatesList); -} // anonymous namespace +string HybridTimeToDateTime(int64_t ht) { + const auto physical_micros = HybridClock::GetPhysicalValueMicros(Timestamp(ht)); + time_t physical_secs(physical_micros / 1000000); + char buf[kFastToBufferSize]; + return FastTimeToBuffer(physical_secs, buf); +} -Status ListTxns(const RunnerContext& context) { - vector<ListTxnsField> fields; - vector<string> col_names; +Status GetFields(vector<string>* txn_field_names, vector<ListTxnsField>* txn_fields, + vector<string>* participant_field_names = nullptr, + vector<ParticipantField>* participant_fields = nullptr) { for (const auto& column : strings::Split(FLAGS_columns, ",", strings::SkipEmpty())) { string column_lower; ToLowerCase(column.ToString(), &column_lower); - auto* field = FindOrNull(kStrToListField, column_lower); + if (participant_field_names && participant_fields) { + // If the field is prefixed with "participant_", get the suffix and look + // through the participant fields. + const char* participant_field_suffix = + strnprefix(column_lower.c_str(), column_lower.size(), + kParticipantPrefix, strlen(kParticipantPrefix)); + if (participant_field_suffix) { + auto* field = FindOrNull(kStrToParticipantFields, participant_field_suffix); + if (field) { + participant_fields->emplace_back(*field); + participant_field_names->emplace_back(participant_field_suffix); + continue; + } + } + } + // Otherwise, check the transaction fields. + auto* field = FindOrNull(kStrToTxnField, column_lower); if (!field) { - return Status::InvalidArgument("unknown column (--columns)", column); + return Status::InvalidArgument("unknown column specified in --columns", column); + } + txn_fields->emplace_back(*field); + txn_field_names->emplace_back(std::move(column_lower)); + } + return Status::OK(); +} + +void AddTxnStatusRow(const vector<ListTxnsField>& fields, int64_t txn_id, + const TxnStatusEntryPB& txn_entry_pb, DataTable* data_table) { + string commit_ts_ht_str = "<none>"; + string commit_ts_date_str = "<none>"; + vector<string> col_vals; + for (const auto& field : fields) { + switch (field) { + case ListTxnsField::kTxnId: + col_vals.emplace_back(SimpleItoa(txn_id)); + break; + case ListTxnsField::kUser: + col_vals.emplace_back(txn_entry_pb.user()); + break; + case ListTxnsField::kState: + col_vals.emplace_back(TxnStatePB_Name(txn_entry_pb.state())); + break; + case ListTxnsField::kCommitDateTime: + col_vals.emplace_back(txn_entry_pb.has_commit_timestamp() ? + HybridTimeToDateTime(txn_entry_pb.commit_timestamp()) : "<none>"); + break; + case ListTxnsField::kCommitHybridTime: + col_vals.emplace_back(txn_entry_pb.has_commit_timestamp() ? + HybridClock::StringifyTimestamp(Timestamp(txn_entry_pb.commit_timestamp())) : "<none>"); + break; } - fields.emplace_back(*field); - col_names.emplace_back(std::move(column_lower)); } + data_table->AddRow(std::move(col_vals)); +} +} // anonymous namespace + +Status ListTxns(const RunnerContext& context) { shared_ptr<KuduClient> client; RETURN_NOT_OK(CreateKuduClient(context, &client)); shared_ptr<KuduTable> table; RETURN_NOT_OK(client->OpenTable(TxnStatusTablet::kTxnStatusTableName, &table)); + vector<string> field_names; + vector<ListTxnsField> fields; + RETURN_NOT_OK(GetFields(&field_names, &fields)); + KuduScanner scanner(table.get()); RETURN_NOT_OK(scanner.SetFaultTolerant()); RETURN_NOT_OK(scanner.SetSelection(KuduClient::LEADER_ONLY)); @@ -181,7 +272,7 @@ Status ListTxns(const RunnerContext& context) { } RETURN_NOT_OK(scanner.Open()); - DataTable data_table(col_names); + DataTable data_table(field_names); KuduScanBatch batch; while (scanner.HasMoreRows()) { RETURN_NOT_OK(scanner.NextBatch(&batch)); @@ -200,46 +291,134 @@ Status ListTxns(const RunnerContext& context) { RETURN_NOT_OK(iter.GetInt8(TxnStatusTablet::kEntryTypeColName, &entry_type)); CHECK_EQ(TxnStatusTablet::TRANSACTION, entry_type); RETURN_NOT_OK(iter.GetInt64(TxnStatusTablet::kTxnIdColName, &txn_id)); - string commit_ts_ht_str = "<none>"; - string commit_ts_date_str = "<none>"; - vector<string> col_vals; - for (const auto& field : fields) { - switch (field) { - case kTxnId: - col_vals.emplace_back(SimpleItoa(txn_id)); - break; - case kUser: - col_vals.emplace_back(txn_entry_pb.user()); - break; - case kState: - col_vals.emplace_back(TxnStatePB_Name(txn_entry_pb.state())); - break; - case kCommitDateTime: { - if (txn_entry_pb.has_commit_timestamp()) { - const auto commit_ts = Timestamp(txn_entry_pb.commit_timestamp()); - auto physical_micros = HybridClock::GetPhysicalValueMicros(commit_ts); - time_t physical_secs(physical_micros / 1000000); - char buf[kFastToBufferSize]; - commit_ts_date_str = FastTimeToBuffer(physical_secs, buf); - } - col_vals.emplace_back(commit_ts_date_str); - break; - } - case kCommitHybridTime: - if (txn_entry_pb.has_commit_timestamp()) { - const auto commit_ts = Timestamp(txn_entry_pb.commit_timestamp()); - commit_ts_ht_str = HybridClock::StringifyTimestamp(commit_ts); - } - col_vals.emplace_back(commit_ts_ht_str); - break; - } - } - data_table.AddRow(col_vals); + AddTxnStatusRow(fields, txn_id, txn_entry_pb, &data_table); } } return data_table.PrintTo(std::cout); } +Status ShowTxn(const RunnerContext& context) { + const auto& txn_id_str = FindOrDie(context.required_args, kTxnIdArg); + int64_t txn_id = -1; + if (!SimpleAtoi(txn_id_str, &txn_id) || txn_id < 0) { + return Status::InvalidArgument( + Substitute("Must supply a valid transaction ID: $0", txn_id_str)); + } + vector<string> txn_field_names; + vector<ListTxnsField> txn_fields; + vector<string> participant_field_names; + vector<ParticipantField> participant_fields; + RETURN_NOT_OK(GetFields(&txn_field_names, &txn_fields, + &participant_field_names, &participant_fields)); + DCHECK_EQ(txn_field_names.size(), txn_fields.size()); + DCHECK_EQ(participant_field_names.size(), participant_fields.size()); + + // First set up our clients so we can be sure we can connect to the cluster. + std::unique_ptr<TxnSystemClient> txn_client; + vector<HostPort> master_hps; + vector<string> master_addresses; + RETURN_NOT_OK(ParseMasterAddresses(context, kMasterAddressesArg, &master_addresses)); + for (const auto& m : master_addresses) { + HostPort hp; + hp.ParseString(m, master::Master::kDefaultPort); + master_hps.emplace_back(hp); + } + RETURN_NOT_OK(TxnSystemClient::Create(master_hps, &txn_client)); + RETURN_NOT_OK(txn_client->OpenTxnStatusTable()); + shared_ptr<KuduClient> client; + RETURN_NOT_OK(CreateKuduClient(context, &client)); + + // Scan the transaction status table for its table entries. + shared_ptr<KuduTable> table; + RETURN_NOT_OK(client->OpenTable(TxnStatusTablet::kTxnStatusTableName, &table)); + KuduScanner scanner(table.get()); + RETURN_NOT_OK(scanner.SetFaultTolerant()); + RETURN_NOT_OK(scanner.SetSelection(KuduClient::LEADER_ONLY)); + auto* pred = table->NewComparisonPredicate( + TxnStatusTablet::kTxnIdColName, KuduPredicate::EQUAL, KuduValue::FromInt(txn_id)); + RETURN_NOT_OK(scanner.AddConjunctPredicate(pred)); + RETURN_NOT_OK(scanner.Open()); + KuduScanBatch batch; + + // Extract transaction statuses and participant IDs from the results. + vector<string> participant_ids; + DataTable txn_table(txn_field_names); + while (scanner.HasMoreRows()) { + RETURN_NOT_OK(scanner.NextBatch(&batch)); + for (const auto& iter : batch) { + Slice metadata_bytes; + string metadata; + RETURN_NOT_OK(iter.GetString(TxnStatusTablet::kMetadataColName, &metadata_bytes)); + int8_t entry_type; + int64_t fetched_txn_id; + RETURN_NOT_OK(iter.GetInt8(TxnStatusTablet::kEntryTypeColName, &entry_type)); + if (entry_type == TxnStatusTablet::TRANSACTION && !txn_fields.empty()) { + TxnStatusEntryPB txn_entry_pb; + RETURN_NOT_OK(pb_util::ParseFromArray(&txn_entry_pb, metadata_bytes.data(), + metadata_bytes.size())); + RETURN_NOT_OK(iter.GetInt64(TxnStatusTablet::kTxnIdColName, &fetched_txn_id)); + DCHECK_EQ(txn_id, fetched_txn_id); + AddTxnStatusRow(txn_fields, fetched_txn_id, txn_entry_pb, &txn_table); + } else if (entry_type == TxnStatusTablet::PARTICIPANT && !participant_fields.empty()) { + Slice participant_id; + RETURN_NOT_OK(iter.GetString(TxnStatusTablet::kIdentifierColName, &participant_id)); + participant_ids.emplace_back(participant_id.ToString()); + } + } + } + if (!txn_fields.empty()) { + RETURN_NOT_OK(txn_table.PrintTo(std::cout)); + std::cout << std::endl; + } + if (participant_field_names.empty()) { + return Status::OK(); + } + + // Get further details from the participants. + DataTable participant_table(participant_field_names); + for (const auto& id : participant_ids) { + TxnMetadataPB meta_pb; + ParticipantOpPB pb; + pb.set_txn_id(txn_id); + pb.set_type(ParticipantOpPB::GET_METADATA); + RETURN_NOT_OK(txn_client->ParticipateInTransaction( + id, pb, MonoDelta::FromMilliseconds(FLAGS_timeout_ms), nullptr, &meta_pb)); + vector<string> col_vals; + for (const auto& field : participant_fields) { + switch (field) { + case ParticipantField::kTabletId: + col_vals.emplace_back(id); + break; + case ParticipantField::kAborted: + col_vals.emplace_back(meta_pb.aborted() ? "true" : "false"); + break; + case ParticipantField::kFlushedCommittedMrs: + col_vals.emplace_back(meta_pb.flushed_committed_mrs() ? "true" : "false"); + break; + case ParticipantField::kBeginCommitDateTime: + col_vals.emplace_back(meta_pb.has_commit_mvcc_op_timestamp() ? + HybridTimeToDateTime(meta_pb.commit_mvcc_op_timestamp()) : "<none>"); + break; + case ParticipantField::kBeginCommitHybridTime: + col_vals.emplace_back(meta_pb.has_commit_mvcc_op_timestamp() ? + HybridClock::StringifyTimestamp(Timestamp(meta_pb.commit_mvcc_op_timestamp())) : + "<none>"); + break; + case ParticipantField::kCommitDateTime: + col_vals.emplace_back(meta_pb.has_commit_timestamp() ? + HybridTimeToDateTime(meta_pb.commit_timestamp()) : "<none>"); + break; + case ParticipantField::kCommitHybridTime: + col_vals.emplace_back(meta_pb.has_commit_timestamp() ? + HybridClock::StringifyTimestamp(Timestamp(meta_pb.commit_timestamp())) : "<none>"); + break; + } + } + participant_table.AddRow(std::move(col_vals)); + } + return participant_table.PrintTo(std::cout); +} + unique_ptr<Mode> BuildTxnMode() { unique_ptr<Action> list = ClusterActionBuilder("list", &ListTxns) @@ -254,9 +433,30 @@ unique_ptr<Mode> BuildTxnMode() { .AddOptionalParameter("min_txn_id") .AddOptionalParameter("included_states") .Build(); + + // TODO(awong): extend the GET_METADATA call for participants to also return + // table ID, partition info, etc. so we can display it. + unique_ptr<Action> show = + ClusterActionBuilder("show", &ShowTxn) + .AddRequiredParameter({ kTxnIdArg, "Transaction ID on which to operate" }) + .AddOptionalParameter( + "columns", + string("txn_id,user,state,commit_datetime,participant_tablet_id," + "participant_begin_commit_datetime,participant_commit_datetime"), + string("Comma-separated list of transaction-related info fields to include " + "in the output.\nPossible values: txn_id, user, state, commit_datetime, " + "commit_hybridtime, participant_tablet_id, participant_is_aborted, " + "participant_flushed_committed_mrs, participant_begin_commit_datetime, " + "participant_begin_commit_hybridtime, participant_commit_datetime, " + "participant_commit_hybridtime")) + .AddOptionalParameter("timeout_ms") + .Description("Show details of a specific transaction") + .Build(); + return ModeBuilder("txn") .Description("Operate on multi-row transactions") .AddAction(std::move(list)) + .AddAction(std::move(show)) .Build(); }