Skip to content

Commit 913988a

Browse files
committed
[#28956] YSQL: Fix transaction self abort with rollback to savepoint
Summary: With DDL savepoint support, users can rollback DDL operations as part of rollback to savepoint operations. When a table is created as part of a savepoint and then the savepoint is rolled back, the table must get deleted. In such scenarios, statements executed after the rollback to savepoint operation are receiving the transaction expired / conflict error. Example: ``` CREATE TABLE projects (id INT PRIMARY KEY, project_name VARCHAR(100)); INSERT INTO projects (id, project_name) VALUES (101, 'Project Alpha'); -- Conflict BEGIN ISOLATION LEVEL REPEATABLE READ; UPDATE projects SET project_name = 'Project Phoenix' WHERE id = 101; SAVEPOINT sp_mixed; CREATE TABLE project_logs (log_id INT, entry TEXT); INSERT INTO project_logs VALUES (1, 'Initial log'); ROLLBACK TO SAVEPOINT sp_mixed; SELECT project_name FROM projects WHERE id = 101; -- gets expired/conflict error ``` ``` ERROR: current transaction is expired or aborted (query layer retry isn't possible because this is not the first command in the transaction. Consider using READ COMMITTED isolation level.) ``` #### Issue When the table is deleted, we make DeleteTabletRequestPB request to delete all tablets of the table. As part of the tablet deletion process there is logic to abort all transactions that are active on the tablet. This leads to the transaction self-abort i.e. the in-flight transaction that initiated the delete table is also aborted. This leads to future statements within the transaction receiving the transaction expired / conflict error. #### Why not earlier This is the first time in YSQL where a tablet deletion is done with an active transaction. Before ddl savepoints, we always deleted tables after the transaction committed (roll-forward). So the transaction abort during tablet deletion was a no-op. #### Fix This revision fixes the issue by not aborting the current transaction if the table is being deleted due to rollback to savepoint operation. We populate the transaction_id in the DeleteTablet request to the tablet server. In the tablet server, we exclude aborting the transaction_id if provided in the request. **Upgrade/Rollback safety:** New field is only populated during rollback to savepoint operation which is a new feature and protected under the TEST flag `FLAGS_TEST_ysql_yb_enable_ddl_savepoint_support`. Jira: DB-18693 Test Plan: ./yb_build.sh --java-test 'org.yb.pgsql.TestDdlSavepoints' Reviewers: bkolagani, amitanand Reviewed By: bkolagani Subscribers: myang, hsunder, svc_phabricator, yql, ybase Differential Revision: https://phorge.dev.yugabyte.com/D47796
1 parent cfeed59 commit 913988a

File tree

15 files changed

+145
-13
lines changed

15 files changed

+145
-13
lines changed

src/postgres/src/test/regress/expected/yb.orig.ddl_savepoint.out

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,19 @@ ROLLBACK;
118118
-- #28955: CREATE INDEX (separate DDL transaction) works when savepoint is enabled.
119119
CREATE TABLE employees(id INT PRIMARY KEY, code VARCHAR(20) UNIQUE, department VARCHAR(50));
120120
CREATE INDEX idx_department ON employees(department);
121+
-- #28956: Transaction doesn't self abort
122+
CREATE TABLE projects (id INT PRIMARY KEY, project_name VARCHAR(100));
123+
INSERT INTO projects (id, project_name) VALUES (101, 'Project Alpha');
124+
BEGIN ISOLATION LEVEL REPEATABLE READ;
125+
UPDATE projects SET project_name = 'Project Phoenix' WHERE id = 101;
126+
SAVEPOINT sp_mixed;
127+
CREATE TABLE project_logs (log_id INT, entry TEXT);
128+
INSERT INTO project_logs VALUES (1, 'Initial log');
129+
-- This will drop table project_logs but shouldn't abort the entire transaction.
130+
ROLLBACK TO SAVEPOINT sp_mixed;
131+
SELECT project_name FROM projects WHERE id = 101;
132+
project_name
133+
-----------------
134+
Project Phoenix
135+
(1 row)
136+

src/postgres/src/test/regress/sql/yb.orig.ddl_savepoint.sql

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,15 @@ ROLLBACK;
6969
-- #28955: CREATE INDEX (separate DDL transaction) works when savepoint is enabled.
7070
CREATE TABLE employees(id INT PRIMARY KEY, code VARCHAR(20) UNIQUE, department VARCHAR(50));
7171
CREATE INDEX idx_department ON employees(department);
72+
73+
-- #28956: Transaction doesn't self abort
74+
CREATE TABLE projects (id INT PRIMARY KEY, project_name VARCHAR(100));
75+
INSERT INTO projects (id, project_name) VALUES (101, 'Project Alpha');
76+
BEGIN ISOLATION LEVEL REPEATABLE READ;
77+
UPDATE projects SET project_name = 'Project Phoenix' WHERE id = 101;
78+
SAVEPOINT sp_mixed;
79+
CREATE TABLE project_logs (log_id INT, entry TEXT);
80+
INSERT INTO project_logs VALUES (1, 'Initial log');
81+
-- This will drop table project_logs but shouldn't abort the entire transaction.
82+
ROLLBACK TO SAVEPOINT sp_mixed;
83+
SELECT project_name FROM projects WHERE id = 101;

src/yb/master/async_rpc_tasks.cc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,9 @@ bool AsyncDeleteReplica::SendRequest(int attempt) {
467467
bool should_abort_active_txns = !table() ||
468468
table()->LockForRead()->started_deleting();
469469
req.set_should_abort_active_txns(should_abort_active_txns);
470+
if (should_abort_active_txns && exclude_aborting_transaction_id_.has_value()) {
471+
req.set_transaction_id(exclude_aborting_transaction_id_->ToString());
472+
}
470473

471474
ts_admin_proxy_->DeleteTabletAsync(req, &resp_, &rpc_, BindRpcCallback());
472475
VLOG_WITH_PREFIX(1) << "Send delete tablet request to " << permanent_uuid_

src/yb/master/async_rpc_tasks.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,10 @@ class AsyncDeleteReplica : public RetrySpecificTSRpcTaskWithTable {
217217
keep_data_ = value;
218218
}
219219

220+
void set_exclude_aborting_transaction_id(TransactionId value) {
221+
exclude_aborting_transaction_id_ = value;
222+
}
223+
220224
TabletId tablet_id() const override { return tablet_id_; }
221225

222226
protected:
@@ -232,6 +236,7 @@ class AsyncDeleteReplica : public RetrySpecificTSRpcTaskWithTable {
232236
tserver::DeleteTabletResponsePB resp_;
233237
bool hide_only_ = false;
234238
bool keep_data_ = false;
239+
std::optional<TransactionId> exclude_aborting_transaction_id_{std::nullopt};
235240

236241
private:
237242
Status SetPendingDelete(AddPendingDelete add_pending_delete);

src/yb/master/catalog_manager.cc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11007,6 +11007,10 @@ void CatalogManager::SendDeleteTabletRequest(
1100711007
}
1100811008
if (table != nullptr) {
1100911009
table->AddTask(call);
11010+
TransactionId txn_id = TransactionId::Nil();
11011+
if (IsTableDeletionDueToRollbackToSubTxn(table, txn_id)) {
11012+
call->set_exclude_aborting_transaction_id(txn_id);
11013+
}
1101011014
}
1101111015

1101211016
auto status = ScheduleTask(call);

src/yb/master/catalog_manager.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,14 @@ class CatalogManager : public CatalogManagerIf, public SnapshotCoordinatorContex
531531
const SubTransactionId sub_txn_id,
532532
const LeaderEpoch& epoch);
533533

534+
// Returns true if the table deletion is happening due to a rollback to sub-transaction operation.
535+
// If returning true, the txn_id field is populated with the transaction that it undergoing the
536+
// rollback to sub-transaction operation.
537+
// NOTE: This function must only be called when we know that the table is being deleted i.e. it
538+
// assumes that the table is being deleted.
539+
bool IsTableDeletionDueToRollbackToSubTxn(
540+
const scoped_refptr<TableInfo>& table, TransactionId& txn_id);
541+
534542
// Rollback all the DDL state changes made by the YSQL transaction from the end till
535543
// rollback_till_ddl_state_index of ysql_ddl_txn_verifier_state i.e.
536544
// ysql_ddl_txn_verifier_state[rollback_till_ddl_state_index, end)

src/yb/master/ysql_ddl_handler.cc

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1210,4 +1210,70 @@ Status CatalogManager::IsRollbackDocdbSchemaToSubtxnDone(
12101210
: Status::OK();
12111211
}
12121212

1213+
bool CatalogManager::IsTableDeletionDueToRollbackToSubTxn(
1214+
const scoped_refptr<TableInfo>& table, TransactionId& txn_id) {
1215+
if (!FLAGS_TEST_ysql_yb_enable_ddl_savepoint_support) {
1216+
return false;
1217+
}
1218+
1219+
// Get the transaction id (if exists) that is modifying the table.
1220+
{
1221+
auto l = table->LockForRead();
1222+
auto txn_id_res = l->GetCurrentDdlTransactionId();
1223+
if (!txn_id_res.ok() || txn_id_res->IsNil()) {
1224+
// Transaction ID will always be present in case of rollback to sub-transaction operation.
1225+
return false;
1226+
}
1227+
txn_id = *txn_id_res;
1228+
}
1229+
1230+
SubTransactionId rolled_back_sub_transaction_id;
1231+
{
1232+
LockGuard lock(ddl_txn_verifier_mutex_);
1233+
const auto rollback_to_subtxn_state =
1234+
FindOrNull(ysql_ddl_txn_undergoing_subtransaction_rollback_map_, txn_id);
1235+
if (rollback_to_subtxn_state == nullptr) {
1236+
// No rollback to sub-txn operation is in progress for this transaction.
1237+
return false;
1238+
}
1239+
1240+
auto tables_itr = std::find_if(
1241+
rollback_to_subtxn_state->tables.begin(), rollback_to_subtxn_state->tables.end(),
1242+
[&table](const TableInfoPtr& table_info) {
1243+
return table_info->id() == table->id(); });
1244+
if (tables_itr == rollback_to_subtxn_state->tables.end()) {
1245+
// This table is not undergoing rollback to sub-transaction operation.
1246+
// Either it has already completed or it wasn't affected.
1247+
return false;
1248+
}
1249+
1250+
rolled_back_sub_transaction_id = rollback_to_subtxn_state->sub_txn;
1251+
}
1252+
1253+
auto l = table->LockForRead();
1254+
// Ensure this table is still being modified by the same transaction id.
1255+
auto txn_id_res = l->GetCurrentDdlTransactionId();
1256+
if (!txn_id_res.ok() || txn_id_res->IsNil() || txn_id != *txn_id_res) {
1257+
// Table is now:
1258+
// 1. Not under any DDL anymore i.e. completed
1259+
// 2. Undergoing DDL via a different transaction
1260+
// None of these cases should happen because this would indicate that the transaction finished
1261+
// and got cleaned up even before the table deletion finished.
1262+
// Crash in DEBUG so that we can find out cases where this is happening.
1263+
// Return false in RELEASE to avoid crash. There is no correctness issue with returning false as
1264+
// this function is used to avoid self-abort of transaction. When we return false, no txn are
1265+
// excluded i.e. all get aborted as part of tablet deletion.
1266+
LOG(DFATAL) << "DDL transaction id for table: " << table
1267+
<< " changed while DeleteTable was in progress. old: " << txn_id
1268+
<< ", new: " << txn_id_res;
1269+
return false;
1270+
}
1271+
1272+
// The table must be created within or after the rolled back sub-transaction.
1273+
const YsqlDdlTxnVerifierStatePB& first_ddl_state = l->ysql_ddl_txn_verifier_state_first();
1274+
return first_ddl_state.contains_create_table_op() &&
1275+
first_ddl_state.has_sub_transaction_id() &&
1276+
first_ddl_state.sub_transaction_id() >= rolled_back_sub_transaction_id;
1277+
}
1278+
12131279
} // namespace yb::master

src/yb/tablet/tablet.cc

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5319,14 +5319,16 @@ std::string IncrementedCopy(Slice key) {
53195319

53205320
} // namespace
53215321

5322-
Status Tablet::AbortActiveTransactions(CoarseTimePoint deadline) const {
5322+
Status Tablet::AbortActiveTransactions(
5323+
CoarseTimePoint deadline, std::optional<TransactionId>&& exclude_txn_id) const {
53235324
if (transaction_participant() == nullptr) {
53245325
return Status::OK();
53255326
}
53265327
HybridTime max_cutoff = HybridTime::kMax;
53275328
LOG(INFO) << "Aborting transactions that started prior to " << max_cutoff << " for tablet id "
53285329
<< tablet_id();
5329-
return transaction_participant()->StopActiveTxnsPriorTo(max_cutoff, deadline);
5330+
return transaction_participant()->StopActiveTxnsPriorTo(
5331+
max_cutoff, deadline, exclude_txn_id.has_value() ? &*exclude_txn_id : nullptr);
53305332
}
53315333

53325334
Status Tablet::GetTabletKeyRanges(

src/yb/tablet/tablet.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -971,7 +971,8 @@ class Tablet : public AbstractTablet,
971971
max_key_length, std::move(callback), colocated_table_id);
972972
}
973973

974-
Status AbortActiveTransactions(CoarseTimePoint deadline) const;
974+
Status AbortActiveTransactions(
975+
CoarseTimePoint deadline, std::optional<TransactionId>&& exclude_txn_id = std::nullopt) const;
975976

976977
// TODO: Move mutex to private section.
977978
// Lock used to serialize the creation of RocksDB checkpoints.

src/yb/tablet/tablet_peer.cc

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -614,14 +614,15 @@ void TabletPeer::WaitUntilShutdown() {
614614

615615
Status TabletPeer::Shutdown(
616616
ShouldAbortActiveTransactions should_abort_active_txns,
617-
DisableFlushOnShutdown disable_flush_on_shutdown) {
617+
DisableFlushOnShutdown disable_flush_on_shutdown,
618+
std::optional<TransactionId>&& exclude_aborting_txn_id) {
618619
auto is_shutdown_initiated = StartShutdown();
619620

620621
if (should_abort_active_txns) {
621622
// Once raft group state enters QUIESCING state,
622623
// new queries cannot be processed from then onwards.
623624
// Aborting any remaining active transactions in the tablet.
624-
AbortActiveTransactions();
625+
AbortActiveTransactions(std::move(exclude_aborting_txn_id));
625626
}
626627

627628
if (is_shutdown_initiated) {
@@ -632,14 +633,15 @@ Status TabletPeer::Shutdown(
632633
return Status::OK();
633634
}
634635

635-
void TabletPeer::AbortActiveTransactions() const {
636+
void TabletPeer::AbortActiveTransactions(
637+
std::optional<TransactionId>&& exclude_aborting_txn_id) const {
636638
if (!tablet_) {
637639
return;
638640
}
639641
auto deadline =
640642
CoarseMonoClock::Now() + MonoDelta::FromMilliseconds(FLAGS_ysql_transaction_abort_timeout_ms);
641643
WARN_NOT_OK(
642-
tablet_->AbortActiveTransactions(deadline),
644+
tablet_->AbortActiveTransactions(deadline, std::move(exclude_aborting_txn_id)),
643645
"Cannot abort transactions for tablet " + tablet_->tablet_id());
644646
}
645647

0 commit comments

Comments
 (0)