From 94e245a14d2a3c57214bb884b843d7f89f5a1bf6 Mon Sep 17 00:00:00 2001 From: Yanqin Jin Date: Wed, 27 Apr 2022 17:50:54 -0700 Subject: [PATCH 1/4] Improve stress test for MultiOpsTxnsStressTest (#9829) Summary: Adds more coverage to `MultiOpsTxnsStressTest` with a focus on write-prepared transactions. 1. Add a hack to manually evict commit cache entries. We currently cannot assign small values to `wp_commit_cache_bits` because it requires a prepared transaction to commit within a certain range of sequence numbers, otherwise it will throw. 2. Add coverage for commit-time-write-batch. If write policy is write-prepared, we need to set `use_only_the_last_commit_time_batch_for_recovery` to true. 3. After each flush/compaction, verify data consistency. This is possible since data size can be small: default numbers of primary/secondary keys are just 1000. Pull Request resolved: https://github.com/facebook/rocksdb/pull/9829 Test Plan: ``` TEST_TMPDIR=/dev/shm/rocksdb_crashtest_blackbox/ make blackbox_crash_test_with_multiops_wp_txn ``` Reviewed By: pdillinger Differential Revision: D35806678 Pulled By: riversand963 fbshipit-source-id: d7fde7a29fda0fb481a61f553e0ca0c47da93616 --- db_stress_tool/db_stress_test_base.cc | 2 + db_stress_tool/db_stress_test_base.h | 7 + db_stress_tool/multi_ops_txns_stress.cc | 383 +++++++++++++----- db_stress_tool/multi_ops_txns_stress.h | 62 +++ tools/db_crashtest.py | 5 + .../transactions/write_prepared_txn_db.cc | 18 + .../transactions/write_prepared_txn_db.h | 1 + 7 files changed, 371 insertions(+), 107 deletions(-) diff --git a/db_stress_tool/db_stress_test_base.cc b/db_stress_tool/db_stress_test_base.cc index 9fde2ecd7..443b825bc 100644 --- a/db_stress_tool/db_stress_test_base.cc +++ b/db_stress_tool/db_stress_test_base.cc @@ -2613,6 +2613,7 @@ void StressTest::Open() { options_.listeners.emplace_back(new DbStressListener( FLAGS_db, options_.db_paths, cf_descriptors, db_stress_listener_env)); #endif // !ROCKSDB_LITE + RegisterAdditionalListeners(); options_.create_missing_column_families = true; if (!FLAGS_use_txn) { #ifndef NDEBUG @@ -2751,6 +2752,7 @@ void StressTest::Open() { static_cast(FLAGS_wp_snapshot_cache_bits); txn_db_options.wp_commit_cache_bits = static_cast(FLAGS_wp_commit_cache_bits); + PrepareTxnDbOptions(txn_db_options); s = TransactionDB::Open(options_, txn_db_options, FLAGS_db, cf_descriptors, &column_families_, &txn_db_); if (!s.ok()) { diff --git a/db_stress_tool/db_stress_test_base.h b/db_stress_tool/db_stress_test_base.h index 786ace4ee..ee37eb101 100644 --- a/db_stress_tool/db_stress_test_base.h +++ b/db_stress_tool/db_stress_test_base.h @@ -16,6 +16,7 @@ namespace ROCKSDB_NAMESPACE { class SystemClock; class Transaction; class TransactionDB; +struct TransactionDBOptions; class StressTest { public: @@ -224,6 +225,12 @@ class StressTest { void CheckAndSetOptionsForUserTimestamp(); + virtual void RegisterAdditionalListeners() {} + +#ifndef ROCKSDB_LITE + virtual void PrepareTxnDbOptions(TransactionDBOptions& /*txn_db_opts*/) {} +#endif + std::shared_ptr cache_; std::shared_ptr compressed_cache_; std::shared_ptr filter_policy_; diff --git a/db_stress_tool/multi_ops_txns_stress.cc b/db_stress_tool/multi_ops_txns_stress.cc index 985af56ac..619a119cb 100644 --- a/db_stress_tool/multi_ops_txns_stress.cc +++ b/db_stress_tool/multi_ops_txns_stress.cc @@ -15,6 +15,7 @@ #ifndef NDEBUG #include "utilities/fault_injection_fs.h" #endif // NDEBUG +#include "utilities/transactions/write_prepared_txn_db.h" namespace ROCKSDB_NAMESPACE { @@ -31,6 +32,21 @@ DEFINE_int32(delay_snapshot_read_one_in, 0, "With a chance of 1/N, inject a random delay between taking " "snapshot and read."); +DEFINE_int32(rollback_one_in, 0, + "If non-zero, rollback non-read-only transactions with a " + "probability of 1/N."); + +DEFINE_int32(clear_wp_commit_cache_one_in, 0, + "If non-zero, evict all commit entries from commit cache with a " + "probability of 1/N. This options applies to write-prepared and " + "write-unprepared transactions."); + +extern "C" bool rocksdb_write_prepared_TEST_ShouldClearCommitCache(void) { + static Random rand(static_cast(db_stress_env->NowMicros())); + return FLAGS_clear_wp_commit_cache_one_in > 0 && + rand.OneIn(FLAGS_clear_wp_commit_cache_one_in); +} + // MultiOpsTxnsStressTest can either operate on a database with pre-populated // data (possibly from previous ones), or create a new db and preload it with // data specified via `-lb_a`, `-ub_a`, `-lb_c`, `-ub_c`, etc. Among these, we @@ -75,8 +91,9 @@ void MultiOpsTxnsStressTest::KeyGenerator::FinishInit() { "Cannot allocate key in [%u, %u)\nStart with a new DB or try change " "the number of threads for testing via -threads=<#threads>\n", static_cast(low_), static_cast(high_)); + fflush(stdout); fflush(stderr); - std::terminate(); + assert(false); } initialized_ = true; } @@ -131,33 +148,43 @@ void MultiOpsTxnsStressTest::KeyGenerator::UndoAllocation(uint32_t new_val) { } std::string MultiOpsTxnsStressTest::Record::EncodePrimaryKey(uint32_t a) { - char buf[8]; - EncodeFixed32(buf, kPrimaryIndexId); - std::reverse(buf, buf + 4); - EncodeFixed32(buf + 4, a); - std::reverse(buf + 4, buf + 8); - return std::string(buf, sizeof(buf)); + std::string ret; + PutFixed32(&ret, kPrimaryIndexId); + PutFixed32(&ret, a); + + char* const buf = &ret[0]; + std::reverse(buf, buf + sizeof(kPrimaryIndexId)); + std::reverse(buf + sizeof(kPrimaryIndexId), + buf + sizeof(kPrimaryIndexId) + sizeof(a)); + return ret; } std::string MultiOpsTxnsStressTest::Record::EncodeSecondaryKey(uint32_t c) { - char buf[8]; - EncodeFixed32(buf, kSecondaryIndexId); - std::reverse(buf, buf + 4); - EncodeFixed32(buf + 4, c); - std::reverse(buf + 4, buf + 8); - return std::string(buf, sizeof(buf)); + std::string ret; + PutFixed32(&ret, kSecondaryIndexId); + PutFixed32(&ret, c); + + char* const buf = &ret[0]; + std::reverse(buf, buf + sizeof(kSecondaryIndexId)); + std::reverse(buf + sizeof(kSecondaryIndexId), + buf + sizeof(kSecondaryIndexId) + sizeof(c)); + return ret; } std::string MultiOpsTxnsStressTest::Record::EncodeSecondaryKey(uint32_t c, uint32_t a) { - char buf[12]; - EncodeFixed32(buf, kSecondaryIndexId); - std::reverse(buf, buf + 4); - EncodeFixed32(buf + 4, c); - EncodeFixed32(buf + 8, a); - std::reverse(buf + 4, buf + 8); - std::reverse(buf + 8, buf + 12); - return std::string(buf, sizeof(buf)); + std::string ret; + PutFixed32(&ret, kSecondaryIndexId); + PutFixed32(&ret, c); + PutFixed32(&ret, a); + + char* const buf = &ret[0]; + std::reverse(buf, buf + sizeof(kSecondaryIndexId)); + std::reverse(buf + sizeof(kSecondaryIndexId), + buf + sizeof(kSecondaryIndexId) + sizeof(c)); + std::reverse(buf + sizeof(kSecondaryIndexId) + sizeof(c), + buf + sizeof(kSecondaryIndexId) + sizeof(c) + sizeof(a)); + return ret; } std::tuple @@ -201,40 +228,26 @@ std::string MultiOpsTxnsStressTest::Record::EncodePrimaryKey() const { } std::string MultiOpsTxnsStressTest::Record::EncodePrimaryIndexValue() const { - char buf[8]; - EncodeFixed32(buf, b_); - EncodeFixed32(buf + 4, c_); - return std::string(buf, sizeof(buf)); + std::string ret; + PutFixed32(&ret, b_); + PutFixed32(&ret, c_); + return ret; } std::pair MultiOpsTxnsStressTest::Record::EncodeSecondaryIndexEntry() const { - std::string secondary_index_key; - char buf[12]; - EncodeFixed32(buf, kSecondaryIndexId); - std::reverse(buf, buf + 4); - EncodeFixed32(buf + 4, c_); - EncodeFixed32(buf + 8, a_); - std::reverse(buf + 4, buf + 8); - std::reverse(buf + 8, buf + 12); - secondary_index_key.assign(buf, sizeof(buf)); + std::string secondary_index_key = EncodeSecondaryKey(c_, a_); // Secondary index value is always 4-byte crc32 of the secondary key std::string secondary_index_value; - uint32_t crc = crc32c::Value(buf, sizeof(buf)); + uint32_t crc = + crc32c::Value(secondary_index_key.data(), secondary_index_key.size()); PutFixed32(&secondary_index_value, crc); - return std::make_pair(secondary_index_key, secondary_index_value); + return std::make_pair(std::move(secondary_index_key), secondary_index_value); } std::string MultiOpsTxnsStressTest::Record::EncodeSecondaryKey() const { - char buf[12]; - EncodeFixed32(buf, kSecondaryIndexId); - std::reverse(buf, buf + 4); - EncodeFixed32(buf + 4, c_); - EncodeFixed32(buf + 8, a_); - std::reverse(buf + 4, buf + 8); - std::reverse(buf + 8, buf + 12); - return std::string(buf, sizeof(buf)); + return EncodeSecondaryKey(c_, a_); } Status MultiOpsTxnsStressTest::Record::DecodePrimaryIndexEntry( @@ -244,27 +257,22 @@ Status MultiOpsTxnsStressTest::Record::DecodePrimaryIndexEntry( return Status::Corruption("Primary index key length is not 8"); } - const char* const index_id_buf = primary_index_key.data(); - uint32_t index_id = - static_cast(static_cast(index_id_buf[0])) << 24; - index_id += static_cast(static_cast(index_id_buf[1])) - << 16; - index_id += static_cast(static_cast(index_id_buf[2])) - << 8; - index_id += - static_cast(static_cast(index_id_buf[3])); - primary_index_key.remove_prefix(sizeof(uint32_t)); + uint32_t index_id = 0; + + [[maybe_unused]] bool res = GetFixed32(&primary_index_key, &index_id); + assert(res); + index_id = EndianSwapValue(index_id); + if (index_id != kPrimaryIndexId) { std::ostringstream oss; oss << "Unexpected primary index id: " << index_id; return Status::Corruption(oss.str()); } - const char* const buf = primary_index_key.data(); - a_ = static_cast(static_cast(buf[0])) << 24; - a_ += static_cast(static_cast(buf[1])) << 16; - a_ += static_cast(static_cast(buf[2])) << 8; - a_ += static_cast(static_cast(buf[3])); + res = GetFixed32(&primary_index_key, &a_); + assert(res); + a_ = EndianSwapValue(a_); + assert(primary_index_key.empty()); if (primary_index_value.size() != 8) { return Status::Corruption("Primary index value length is not 8"); @@ -282,33 +290,28 @@ Status MultiOpsTxnsStressTest::Record::DecodeSecondaryIndexEntry( uint32_t crc = crc32c::Value(secondary_index_key.data(), secondary_index_key.size()); - const char* const index_id_buf = secondary_index_key.data(); - uint32_t index_id = - static_cast(static_cast(index_id_buf[0])) << 24; - index_id += static_cast(static_cast(index_id_buf[1])) - << 16; - index_id += static_cast(static_cast(index_id_buf[2])) - << 8; - index_id += - static_cast(static_cast(index_id_buf[3])); - secondary_index_key.remove_prefix(sizeof(uint32_t)); + uint32_t index_id = 0; + + [[maybe_unused]] bool res = GetFixed32(&secondary_index_key, &index_id); + assert(res); + index_id = EndianSwapValue(index_id); + if (index_id != kSecondaryIndexId) { std::ostringstream oss; oss << "Unexpected secondary index id: " << index_id; return Status::Corruption(oss.str()); } - const char* const buf = secondary_index_key.data(); assert(secondary_index_key.size() == 8); - c_ = static_cast(static_cast(buf[0])) << 24; - c_ += static_cast(static_cast(buf[1])) << 16; - c_ += static_cast(static_cast(buf[2])) << 8; - c_ += static_cast(static_cast(buf[3])); + res = GetFixed32(&secondary_index_key, &c_); + assert(res); + c_ = EndianSwapValue(c_); - a_ = static_cast(static_cast(buf[4])) << 24; - a_ += static_cast(static_cast(buf[5])) << 16; - a_ += static_cast(static_cast(buf[6])) << 8; - a_ += static_cast(static_cast(buf[7])); + assert(secondary_index_key.size() == 4); + res = GetFixed32(&secondary_index_key, &a_); + assert(res); + a_ = EndianSwapValue(a_); + assert(secondary_index_key.empty()); if (secondary_index_value.size() != 4) { return Status::Corruption("Secondary index value length is not 4"); @@ -520,9 +523,35 @@ Status MultiOpsTxnsStressTest::TestCustomOperations( // Should never reach here. assert(false); } + return s; } +void MultiOpsTxnsStressTest::RegisterAdditionalListeners() { + options_.listeners.emplace_back(new MultiOpsTxnsStressListener(this)); +} + +#ifndef ROCKSDB_LITE +void MultiOpsTxnsStressTest::PrepareTxnDbOptions( + TransactionDBOptions& txn_db_opts) { + // MultiOpsTxnStressTest uses SingleDelete to delete secondary keys, thus we + // register this callback to let TxnDb know that when rolling back + // a transaction, use only SingleDelete to cancel prior Put from the same + // transaction if applicable. + txn_db_opts.rollback_deletion_type_callback = + [](TransactionDB* /*db*/, ColumnFamilyHandle* /*column_family*/, + const Slice& key) { + Slice ks = key; + uint32_t index_id = 0; + [[maybe_unused]] bool res = GetFixed32(&ks, &index_id); + assert(res); + index_id = EndianSwapValue(index_id); + assert(index_id <= Record::kSecondaryIndexId); + return index_id == Record::kSecondaryIndexId; + }; +} +#endif // !ROCKSDB_LITE + Status MultiOpsTxnsStressTest::PrimaryKeyUpdateTxn(ThreadState* thread, uint32_t old_a, uint32_t old_a_pos, @@ -561,8 +590,10 @@ Status MultiOpsTxnsStressTest::PrimaryKeyUpdateTxn(ThreadState* thread, } if (s.IsNotFound()) { thread->stats.AddGets(/*ngets=*/1, /*nfounds=*/0); - } else if (s.IsBusy()) { + } else if (s.IsBusy() || s.IsIncomplete()) { // ignore. + // Incomplete also means rollback by application. See the transaction + // implementations. } else { thread->stats.AddErrors(1); } @@ -631,6 +662,16 @@ Status MultiOpsTxnsStressTest::PrimaryKeyUpdateTxn(ThreadState* thread, return s; } + if (FLAGS_rollback_one_in > 0 && thread->rand.OneIn(FLAGS_rollback_one_in)) { + s = Status::Incomplete(); + return s; + } + + s = WriteToCommitTimeWriteBatch(*txn); + if (!s.ok()) { + return s; + } + s = txn->Commit(); auto& key_gen = key_gen_for_a_.at(thread->tid); @@ -677,11 +718,12 @@ Status MultiOpsTxnsStressTest::SecondaryKeyUpdateTxn(ThreadState* thread, Record::kPrimaryIndexEntrySize + Record::kSecondaryIndexEntrySize); return; } else if (s.IsBusy() || s.IsTimedOut() || s.IsTryAgain() || - s.IsMergeInProgress()) { + s.IsMergeInProgress() || s.IsIncomplete()) { // ww-conflict detected, or // lock cannot be acquired, or // memtable history is not large enough for conflict checking, or - // Merge operation cannot be resolved. + // Merge operation cannot be resolved, or + // application rollback. // TODO (yanqin) add stats for other cases? } else if (s.IsNotFound()) { // ignore. @@ -727,8 +769,9 @@ Status MultiOpsTxnsStressTest::SecondaryKeyUpdateTxn(ThreadState* thread, Record record; s = record.DecodeSecondaryIndexEntry(it->key(), it->value()); if (!s.ok()) { - fprintf(stderr, "Cannot decode secondary key: %s\n", - s.ToString().c_str()); + fprintf(stderr, "Cannot decode secondary key (%s => %s): %s\n", + it->key().ToString(true).c_str(), + it->value().ToString(true).c_str(), s.ToString().c_str()); assert(false); break; } @@ -749,21 +792,31 @@ Status MultiOpsTxnsStressTest::SecondaryKeyUpdateTxn(ThreadState* thread, } else if (s.IsNotFound()) { // We can also fail verification here. std::ostringstream oss; - oss << "pk should exist: " << Slice(pk).ToString(true); + auto* dbimpl = static_cast_with_check(db_->GetRootDB()); + assert(dbimpl); + oss << "snap " << read_opts.snapshot->GetSequenceNumber() + << " (published " << dbimpl->GetLastPublishedSequence() + << "), pk should exist: " << Slice(pk).ToString(true); fprintf(stderr, "%s\n", oss.str().c_str()); assert(false); break; } if (!s.ok()) { - fprintf(stderr, "%s\n", s.ToString().c_str()); + std::ostringstream oss; + auto* dbimpl = static_cast_with_check(db_->GetRootDB()); + assert(dbimpl); + oss << "snap " << read_opts.snapshot->GetSequenceNumber() + << " (published " << dbimpl->GetLastPublishedSequence() << "), " + << s.ToString(); + fprintf(stderr, "%s\n", oss.str().c_str()); assert(false); break; } auto result = Record::DecodePrimaryIndexValue(value); s = std::get<0>(result); if (!s.ok()) { - fprintf(stderr, "Cannot decode primary index value: %s\n", - s.ToString().c_str()); + fprintf(stderr, "Cannot decode primary index value %s: %s\n", + Slice(value).ToString(true).c_str(), s.ToString().c_str()); assert(false); break; } @@ -771,8 +824,12 @@ Status MultiOpsTxnsStressTest::SecondaryKeyUpdateTxn(ThreadState* thread, uint32_t c = std::get<2>(result); if (c != old_c) { std::ostringstream oss; - oss << "c in primary index does not match secondary index: " << c - << " != " << old_c; + auto* dbimpl = static_cast_with_check(db_->GetRootDB()); + assert(dbimpl); + oss << "snap " << read_opts.snapshot->GetSequenceNumber() + << " (published " << dbimpl->GetLastPublishedSequence() + << "), pk/sk mismatch. pk: (a=" << record.a_value() << ", " + << "c=" << c << "), sk: (c=" << old_c << ")"; s = Status::Corruption(); fprintf(stderr, "%s\n", oss.str().c_str()); assert(false); @@ -811,6 +868,16 @@ Status MultiOpsTxnsStressTest::SecondaryKeyUpdateTxn(ThreadState* thread, return s; } + if (FLAGS_rollback_one_in > 0 && thread->rand.OneIn(FLAGS_rollback_one_in)) { + s = Status::Incomplete(); + return s; + } + + s = WriteToCommitTimeWriteBatch(*txn); + if (!s.ok()) { + return s; + } + s = txn->Commit(); if (s.ok()) { @@ -856,7 +923,7 @@ Status MultiOpsTxnsStressTest::UpdatePrimaryIndexValueTxn(ThreadState* thread, } else if (s.IsInvalidArgument()) { // ignored. } else if (s.IsBusy() || s.IsTimedOut() || s.IsTryAgain() || - s.IsMergeInProgress()) { + s.IsMergeInProgress() || s.IsIncomplete()) { // ignored. } else { thread->stats.AddErrors(1); @@ -874,8 +941,8 @@ Status MultiOpsTxnsStressTest::UpdatePrimaryIndexValueTxn(ThreadState* thread, auto result = Record::DecodePrimaryIndexValue(value); if (!std::get<0>(result).ok()) { s = std::get<0>(result); - fprintf(stderr, "Cannot decode primary index value: %s\n", - s.ToString().c_str()); + fprintf(stderr, "Cannot decode primary index value %s: %s\n", + Slice(value).ToString(true).c_str(), s.ToString().c_str()); assert(false); return s; } @@ -892,6 +959,17 @@ Status MultiOpsTxnsStressTest::UpdatePrimaryIndexValueTxn(ThreadState* thread, if (!s.ok()) { return s; } + + if (FLAGS_rollback_one_in > 0 && thread->rand.OneIn(FLAGS_rollback_one_in)) { + s = Status::Incomplete(); + return s; + } + + s = WriteToCommitTimeWriteBatch(*txn); + if (!s.ok()) { + return s; + } + s = txn->Commit(); if (s.ok()) { delete txn; @@ -1050,12 +1128,15 @@ void MultiOpsTxnsStressTest::VerifyDb(ThreadState* thread) const { // First, iterate primary index. size_t primary_index_entries_count = 0; { - char buf[4]; - EncodeFixed32(buf, Record::kPrimaryIndexId + 1); - std::reverse(buf, buf + sizeof(buf)); - std::string iter_ub_str(buf, sizeof(buf)); + std::string iter_ub_str; + PutFixed32(&iter_ub_str, Record::kPrimaryIndexId + 1); + std::reverse(iter_ub_str.begin(), iter_ub_str.end()); Slice iter_ub = iter_ub_str; + std::string start_key; + PutFixed32(&start_key, Record::kPrimaryIndexId); + std::reverse(start_key.begin(), start_key.end()); + // This `ReadOptions` is for validation purposes. Ignore // `FLAGS_rate_limit_user_ops` to avoid slowing any validation. ReadOptions ropts; @@ -1064,7 +1145,7 @@ void MultiOpsTxnsStressTest::VerifyDb(ThreadState* thread) const { ropts.iterate_upper_bound = &iter_ub; std::unique_ptr it(db_->NewIterator(ropts)); - for (it->SeekToFirst(); it->Valid(); it->Next()) { + for (it->Seek(start_key); it->Valid(); it->Next()) { Record record; Status s = record.DecodePrimaryIndexEntry(it->key(), it->value()); if (!s.ok()) { @@ -1101,10 +1182,9 @@ void MultiOpsTxnsStressTest::VerifyDb(ThreadState* thread) const { // Second, iterate secondary index. size_t secondary_index_entries_count = 0; { - char buf[4]; - EncodeFixed32(buf, Record::kSecondaryIndexId); - std::reverse(buf, buf + sizeof(buf)); - const std::string start_key(buf, sizeof(buf)); + std::string start_key; + PutFixed32(&start_key, Record::kSecondaryIndexId); + std::reverse(start_key.begin(), start_key.end()); // This `ReadOptions` is for validation purposes. Ignore // `FLAGS_rate_limit_user_ops` to avoid slowing any validation. @@ -1118,7 +1198,8 @@ void MultiOpsTxnsStressTest::VerifyDb(ThreadState* thread) const { Record record; Status s = record.DecodeSecondaryIndexEntry(it->key(), it->value()); if (!s.ok()) { - oss << "Cannot decode secondary index entry"; + oss << "Cannot decode secondary index entry " + << it->key().ToString(true) << "=>" << it->value().ToString(true); VerificationAbort(thread->shared, oss.str(), s); assert(false); return; @@ -1132,7 +1213,7 @@ void MultiOpsTxnsStressTest::VerifyDb(ThreadState* thread) const { s = db_->Get(ropts, pk, &value); if (!s.ok()) { oss << "Error searching pk " << Slice(pk).ToString(true) << ". " - << s.ToString(); + << s.ToString() << ". sk " << it->key().ToString(true); VerificationAbort(thread->shared, oss.str(), s); assert(false); return; @@ -1148,8 +1229,10 @@ void MultiOpsTxnsStressTest::VerifyDb(ThreadState* thread) const { } uint32_t c_in_primary = std::get<2>(result); if (c_in_primary != record.c_value()) { - oss << "Pk/sk mismatch. pk: (c=" << c_in_primary - << "), sk: (c=" << record.c_value() << ")"; + oss << "Pk/sk mismatch. pk: " << Slice(pk).ToString(true) << "=>" + << Slice(value).ToString(true) << " (a=" << record.a_value() + << ", c=" << c_in_primary << "), sk: " << it->key().ToString(true) + << " (c=" << record.c_value() << ")"; VerificationAbort(thread->shared, oss.str(), s); assert(false); return; @@ -1167,6 +1250,75 @@ void MultiOpsTxnsStressTest::VerifyDb(ThreadState* thread) const { } } +void MultiOpsTxnsStressTest::VerifyPkSkFast(int job_id) { + const Snapshot* const snapshot = db_->GetSnapshot(); + assert(snapshot); + ManagedSnapshot snapshot_guard(db_, snapshot); + + std::ostringstream oss; + auto* dbimpl = static_cast_with_check(db_->GetRootDB()); + assert(dbimpl); + + oss << "Job " << job_id << ": [" << snapshot->GetSequenceNumber() << "," + << dbimpl->GetLastPublishedSequence() << "] "; + + std::string start_key; + PutFixed32(&start_key, Record::kSecondaryIndexId); + std::reverse(start_key.begin(), start_key.end()); + + // This `ReadOptions` is for validation purposes. Ignore + // `FLAGS_rate_limit_user_ops` to avoid slowing any validation. + ReadOptions ropts; + ropts.snapshot = snapshot; + ropts.total_order_seek = true; + + std::unique_ptr it(db_->NewIterator(ropts)); + for (it->Seek(start_key); it->Valid(); it->Next()) { + Record record; + Status s = record.DecodeSecondaryIndexEntry(it->key(), it->value()); + if (!s.ok()) { + oss << "Cannot decode secondary index entry " << it->key().ToString(true) + << "=>" << it->value().ToString(true); + fprintf(stderr, "%s\n", oss.str().c_str()); + fflush(stderr); + assert(false); + } + // After decoding secondary index entry, we know a and c. Crc is verified + // in decoding phase. + // + // Form a primary key and search in the primary index. + std::string pk = Record::EncodePrimaryKey(record.a_value()); + std::string value; + s = db_->Get(ropts, pk, &value); + if (!s.ok()) { + oss << "Error searching pk " << Slice(pk).ToString(true) << ". " + << s.ToString() << ". sk " << it->key().ToString(true); + fprintf(stderr, "%s\n", oss.str().c_str()); + fflush(stderr); + assert(false); + } + auto result = Record::DecodePrimaryIndexValue(value); + s = std::get<0>(result); + if (!s.ok()) { + oss << "Error decoding primary index value " + << Slice(value).ToString(true) << ". " << s.ToString(); + fprintf(stderr, "%s\n", oss.str().c_str()); + fflush(stderr); + assert(false); + } + uint32_t c_in_primary = std::get<2>(result); + if (c_in_primary != record.c_value()) { + oss << "Pk/sk mismatch. pk: " << Slice(pk).ToString(true) << "=>" + << Slice(value).ToString(true) << " (a=" << record.a_value() + << ", c=" << c_in_primary << "), sk: " << it->key().ToString(true) + << " (c=" << record.c_value() << ")"; + fprintf(stderr, "%s\n", oss.str().c_str()); + fflush(stderr); + assert(false); + } + } +} + std::pair MultiOpsTxnsStressTest::ChooseExistingA( ThreadState* thread) { uint32_t tid = thread->tid; @@ -1193,6 +1345,22 @@ uint32_t MultiOpsTxnsStressTest::GenerateNextC(ThreadState* thread) { return key_gen->Allocate(); } +#ifndef ROCKSDB_LITE +Status MultiOpsTxnsStressTest::WriteToCommitTimeWriteBatch(Transaction& txn) { + WriteBatch* ctwb = txn.GetCommitTimeWriteBatch(); + assert(ctwb); + // Do not change the content in key_buf. + static constexpr char key_buf[sizeof(Record::kMetadataPrefix) + 4] = { + '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\xff'}; + + uint64_t counter_val = counter_.Next(); + char val_buf[sizeof(counter_val)]; + EncodeFixed64(val_buf, counter_val); + return ctwb->Put(Slice(key_buf, sizeof(key_buf)), + Slice(val_buf, sizeof(val_buf))); +} +#endif // !ROCKSDB_LITE + std::string MultiOpsTxnsStressTest::KeySpaces::EncodeTo() const { std::string result; PutFixed32(&result, lb_a); @@ -1428,8 +1596,9 @@ void MultiOpsTxnsStressTest::ScanExistingDb(SharedState* shared, int threads) { Record record; Status s = record.DecodePrimaryIndexEntry(it->key(), it->value()); if (!s.ok()) { - fprintf(stderr, "Cannot decode primary index entry: %s\n", - s.ToString().c_str()); + fprintf(stderr, "Cannot decode primary index entry (%s => %s): %s\n", + it->key().ToString(true).c_str(), + it->value().ToString(true).c_str(), s.ToString().c_str()); assert(false); } uint32_t a = record.a_value(); diff --git a/db_stress_tool/multi_ops_txns_stress.h b/db_stress_tool/multi_ops_txns_stress.h index 47b438875..a7db1c69e 100644 --- a/db_stress_tool/multi_ops_txns_stress.h +++ b/db_stress_tool/multi_ops_txns_stress.h @@ -111,6 +111,7 @@ class MultiOpsTxnsStressTest : public StressTest { public: class Record { public: + static constexpr uint32_t kMetadataPrefix = 0; static constexpr uint32_t kPrimaryIndexId = 1; static constexpr uint32_t kSecondaryIndexId = 2; @@ -261,6 +262,12 @@ class MultiOpsTxnsStressTest : public StressTest { ThreadState* thread, const std::vector& rand_column_families) override; + void RegisterAdditionalListeners() override; + +#ifndef ROCKSDB_LITE + void PrepareTxnDbOptions(TransactionDBOptions& txn_db_opts) override; +#endif // !ROCKSDB_LITE + Status PrimaryKeyUpdateTxn(ThreadState* thread, uint32_t old_a, uint32_t old_a_pos, uint32_t new_a); @@ -280,7 +287,17 @@ class MultiOpsTxnsStressTest : public StressTest { VerifyDb(thread); } + void VerifyPkSkFast(int job_id); + protected: + class Counter { + public: + uint64_t Next() { return value_.fetch_add(1); } + + private: + std::atomic value_ = Env::Default()->NowNanos(); + }; + using KeySet = std::set; class KeyGenerator { public: @@ -330,9 +347,21 @@ class MultiOpsTxnsStressTest : public StressTest { uint32_t GenerateNextC(ThreadState* thread); +#ifndef ROCKSDB_LITE + // Some applications, e.g. MyRocks writes a KV pair to the database via + // commit-time-write-batch (ctwb) in additional to the transaction's regular + // write batch. The key is usually constant representing some system + // metadata, while the value is monoticailly increasing which represents the + // actual value of the metadata. Method WriteToCommitTimeWriteBatch() + // emulates this scenario. + Status WriteToCommitTimeWriteBatch(Transaction& txn); +#endif //! ROCKSDB_LITE + std::vector> key_gen_for_a_; std::vector> key_gen_for_c_; + Counter counter_{}; + private: struct KeySpaces { uint32_t lb_a = 0; @@ -370,5 +399,38 @@ class InvariantChecker { "MultiOpsTxnsStressTest::Record::c_ must be 4 bytes"); }; +class MultiOpsTxnsStressListener : public EventListener { + public: + explicit MultiOpsTxnsStressListener(MultiOpsTxnsStressTest* stress_test) + : stress_test_(stress_test) { + assert(stress_test_); + } + +#ifndef ROCKSDB_LITE + ~MultiOpsTxnsStressListener() override {} + + void OnFlushCompleted(DB* db, const FlushJobInfo& info) override { + assert(db); +#ifdef NDEBUG + (void)db; +#endif + assert(info.cf_id == 0); + stress_test_->VerifyPkSkFast(info.job_id); + } + + void OnCompactionCompleted(DB* db, const CompactionJobInfo& info) override { + assert(db); +#ifdef NDEBUG + (void)db; +#endif + assert(info.cf_id == 0); + stress_test_->VerifyPkSkFast(info.job_id); + } +#endif //! ROCKSDB_LITE + + private: + MultiOpsTxnsStressTest* const stress_test_ = nullptr; +}; + } // namespace ROCKSDB_NAMESPACE #endif // GFLAGS diff --git a/tools/db_crashtest.py b/tools/db_crashtest.py index 231f79372..696448989 100644 --- a/tools/db_crashtest.py +++ b/tools/db_crashtest.py @@ -383,6 +383,7 @@ multiops_txn_default_params = { # compactions. "flush_one_in": 1000, "key_spaces_path": setup_multiops_txn_key_spaces_file(), + "rollback_one_in": 4, } multiops_wc_txn_params = { @@ -401,6 +402,10 @@ multiops_wp_txn_params = { "enable_pipelined_write": 0, # OpenReadOnly after checkpoint is not currnetly compatible with WritePrepared txns "checkpoint_one_in": 0, + # Required to be 1 in order to use commit-time-batch + "use_only_the_last_commit_time_batch_for_recovery": 1, + "recycle_log_file_num": 0, + "clear_wp_commit_cache_one_in": 10, } def finalize_and_sanitize(src_params): diff --git a/utilities/transactions/write_prepared_txn_db.cc b/utilities/transactions/write_prepared_txn_db.cc index 3b6985737..139afc37a 100644 --- a/utilities/transactions/write_prepared_txn_db.cc +++ b/utilities/transactions/write_prepared_txn_db.cc @@ -26,6 +26,18 @@ #include "utilities/transactions/pessimistic_transaction.h" #include "utilities/transactions/transaction_db_mutex_impl.h" +// This function is for testing only. If it returns true, then all entries in +// the commit cache will be evicted. Unit and/or stress tests (db_stress) +// can implement this function and customize how frequently commit cache +// eviction occurs. +// TODO: remove this function once we can configure commit cache to be very +// small so that eviction occurs very frequently. This requires the commit +// cache entry to be able to encode prepare and commit sequence numbers so that +// the commit sequence number does not have to be within a certain range of +// prepare sequence number. +extern "C" bool rocksdb_write_prepared_TEST_ShouldClearCommitCache(void) + __attribute__((__weak__)); + namespace ROCKSDB_NAMESPACE { Status WritePreparedTxnDB::Initialize( @@ -505,6 +517,12 @@ void WritePreparedTxnDB::AddCommitted(uint64_t prepare_seq, uint64_t commit_seq, // legit when a commit entry in a write batch overwrite the previous one max_evicted_seq = evicted.commit_seq; } +#ifdef OS_LINUX + if (rocksdb_write_prepared_TEST_ShouldClearCommitCache && + rocksdb_write_prepared_TEST_ShouldClearCommitCache()) { + max_evicted_seq = last; + } +#endif // OS_LINUX ROCKS_LOG_DETAILS(info_log_, "%lu Evicting %" PRIu64 ",%" PRIu64 " with max %" PRIu64 " => %lu", diff --git a/utilities/transactions/write_prepared_txn_db.h b/utilities/transactions/write_prepared_txn_db.h index 502fea56a..5ae29b3f3 100644 --- a/utilities/transactions/write_prepared_txn_db.h +++ b/utilities/transactions/write_prepared_txn_db.h @@ -513,6 +513,7 @@ class WritePreparedTxnDB : public PessimisticTransactionDB { friend class WriteUnpreparedTxn; friend class WriteUnpreparedTxnDB; friend class WriteUnpreparedTransactionTest_RecoveryTest_Test; + friend class MultiOpsTxnsStressTest; void Init(const TransactionDBOptions& txn_db_opts); From fce65e7e4f01074d78a742157dc6c5979d12dc78 Mon Sep 17 00:00:00 2001 From: Akanksha Mahajan <43301668+akankshamahajan15@users.noreply.github.com> Date: Wed, 27 Apr 2022 22:33:29 -0700 Subject: [PATCH 2/4] Fix bug in async_io path which reads incorrect length (#9916) Summary: In FilePrefetchBuffer, in case data is overlapping between two buffers and more data is required to read and copy that to third buffer, incorrect length was updated resulting in ``` Iterator diverged from control iterator which has value 00000000000310C3000000000000012B0000000000000274 total_order_seek: 1 auto_prefix_mode: 0 S 000000000002C37F000000000000012B000000000000001C NNNPPPPPNN; total_order_seek: 1 auto_prefix_mode: 0 S 000000000002F10B00000000000000BF78787878787878 NNNPNNNNPN; total_order_seek: 1 auto_prefix_mode: 0 S 00000000000310C3000000000000012B000000000000026B iterator is not valid Control CF default db_stress: db_stress_tool/db_stress_test_base.cc:1388: void rocksdb::StressTest::VerifyIterator(rocksdb::ThreadState*, rocksdb::ColumnFamilyHandle*, const rocksdb::ReadOptions&, rocksdb::Iterator*, rocksdb::Iterator*, rocksdb::StressTest::LastIterateOp, const rocksdb::Slice&, const string&, bool*): Assertion `false' failed. Aborted (core dumped) ``` Pull Request resolved: https://github.com/facebook/rocksdb/pull/9916 Test Plan: ``` - CircleCI jobs - Ran db_stress with OPTIONS file which caught the bug ./db_stress --acquire_snapshot_one_in=10000 --adaptive_readahead=0 --async_io=1 --avoid_flush_during_recovery=0 --avoid_unnecessary_blocking_io=1 --backup_max_size=104857600 --backup_one_in=0 --batch_protection_bytes_per_key=0 --block_size=16384 --bloom_bits=42.26248932628998 --bottommost_compression_type=disable --cache_index_and_filter_blocks=0 --cache_size=8388608 --checkpoint_one_in=0 --checksum_type=kxxHash --clear_column_family_one_in=0 --compact_files_one_in=1000000 --compact_range_one_in=1000000 --compaction_ttl=0 --compression_max_dict_buffer_bytes=1073741823 --compression_max_dict_bytes=16384 --compression_parallel_threads=1 --compression_type=zstd --compression_zstd_max_train_bytes=65536 --continuous_verification_interval=0 --db=/dev/shm/rocksdb/ --db_write_buffer_size=134217728 --delpercent=5 --delrangepercent=0 --destroy_db_initially=0 --detect_filter_construct_corruption=0 --disable_wal=0 --enable_blob_files=0 --enable_compaction_filter=0 --enable_pipelined_write=0 --fail_if_options_file_error=0 --file_checksum_impl=none --flush_one_in=1000000 --format_version=4 --get_current_wal_file_one_in=0 --get_live_files_one_in=1000000 --get_property_one_in=1000000 --get_sorted_wal_files_one_in=0 --index_block_restart_interval=12 --index_type=2 --ingest_external_file_one_in=0 --iterpercent=10 --key_len_percent_dist=1,30,69 --level_compaction_dynamic_level_bytes=True --long_running_snapshots=0 --mark_for_compaction_one_file_in=0 --max_background_compactions=20 --max_bytes_for_level_base=10485760 --max_key=25000000 --max_key_len=3 --max_manifest_file_size=1073741824 --max_write_batch_group_size_bytes=1048576 --max_write_buffer_number=3 --max_write_buffer_size_to_maintain=8388608 --memtable_prefix_bloom_size_ratio=0.001 --memtable_whole_key_filtering=1 --memtablerep=skip_list --mmap_read=0 --mock_direct_io=False --nooverwritepercent=1 --open_files=100 --open_metadata_write_fault_one_in=0 --open_read_fault_one_in=0 --open_write_fault_one_in=16 --ops_per_thread=100000000 --optimize_filters_for_memory=0 --paranoid_file_checks=1 --partition_filters=0 --partition_pinning=1 --pause_background_one_in=1000000 --periodic_compaction_seconds=0 --prefix_size=-1 --prefixpercent=0 --prepopulate_block_cache=0 --progress_reports=0 --read_fault_one_in=0 --read_only=0 --readpercent=50 --recycle_log_file_num=1 --reopen=0 --reserve_table_reader_memory=0 --ribbon_starting_level=999 --secondary_cache_fault_one_in=0 --secondary_catch_up_one_in=0 --set_options_one_in=10000 --snapshot_hold_ops=100000 --sst_file_manager_bytes_per_sec=104857600 --sst_file_manager_bytes_per_truncate=0 --subcompactions=2 --sync=0 --sync_fault_injection=False --target_file_size_base=2097152 --target_file_size_multiplier=2 --test_batches_snapshots=0 --test_cf_consistency=0 --top_level_index_pinning=3 --unpartitioned_pinning=3 --use_blob_db=0 --use_block_based_filter=0 --use_clock_cache=0 --use_direct_io_for_flush_and_compaction=1 --use_direct_reads=0 --use_full_merge_v1=0 --use_merge=0 --use_multiget=0 --use_txn=0 --user_timestamp_size=0 --value_size_mult=32 --verify_checksum=1 --verify_checksum_one_in=1000000 --verify_db_one_in=100000 --wal_compression=zstd --write_buffer_size=4194304 --write_dbid_to_manifest=0 --writepercent=35 --options_file=/home/akankshamahajan/OPTIONS.orig -column_families=1 db_bench with async_io enabled to make sure db_bench completes successfully without any failure. - ./db_bench -use_existing_db=true -db=/tmp/prefix_scan_prefetch_main -benchmarks="seekrandom" -key_size=32 -value_size=512 -num=5000000 -use_direct_reads=true -seek_nexts=327680 -duration=120 -ops_between_duration_checks=1 -async_io=1 ``` crash_test in progress Reviewed By: anand1976 Differential Revision: D35985789 Pulled By: akankshamahajan15 fbshipit-source-id: 5abe185f34caa99ca587d4bdc8954bd0802b1bf9 --- HISTORY.md | 1 + env/fs_posix.cc | 8 +++++++- file/file_prefetch_buffer.cc | 5 +++-- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 725e2c129..7d622caa9 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -3,6 +3,7 @@ ### Bug Fixes * Fixed a bug where manual flush would block forever even though flush options had wait=false. * Fixed a bug where RocksDB could corrupt DBs with `avoid_flush_during_recovery == true` by removing valid WALs, leading to `Status::Corruption` with message like "SST file is ahead of WALs" when attempting to reopen. +* Fixed a bug in async_io path where incorrect length of data is read by FilePrefetchBuffer if data is consumed from two populated buffers and request for more data is sent. ### New Features * DB::GetLiveFilesStorageInfo is ready for production use. diff --git a/env/fs_posix.cc b/env/fs_posix.cc index 629ebe5b8..057058d24 100644 --- a/env/fs_posix.cc +++ b/env/fs_posix.cc @@ -1122,7 +1122,13 @@ class PosixFileSystem : public FileSystem { // in posix for IOUring requests. Currently it calls Poll to wait for requests // to complete the request. virtual IOStatus AbortIO(std::vector& io_handles) override { - return Poll(io_handles, io_handles.size()); + IOStatus s = Poll(io_handles, io_handles.size()); + // If Poll is not supported then it didn't submit any request and it should + // return OK. + if (s.IsNotSupported()) { + return IOStatus::OK(); + } + return s; } #if defined(ROCKSDB_IOURING_PRESENT) diff --git a/file/file_prefetch_buffer.cc b/file/file_prefetch_buffer.cc index ec92758fe..222166876 100644 --- a/file/file_prefetch_buffer.cc +++ b/file/file_prefetch_buffer.cc @@ -282,7 +282,7 @@ Status FilePrefetchBuffer::PrefetchAsync(const IOOptions& opts, bufs_[curr_].offset_ + bufs_[curr_].buffer_.CurrentSize()) { offset += length; length = 0; - prefetch_size -= length; + prefetch_size = readahead_size; } // Data is overlapping i.e. some of the data is in curr_ buffer and remaining // in second buffer. @@ -311,7 +311,8 @@ Status FilePrefetchBuffer::PrefetchAsync(const IOOptions& opts, // sync prefetching and copy the remaining data to third buffer in the end. // swap the buffers. curr_ = curr_ ^ 1; - prefetch_size -= length; + // Update prefetch_size as length has been updated in CopyDataToBuffer. + prefetch_size = length + readahead_size; } // Update second again if swap happened. From aafb377bb5f999d4e065995437cc01627289f904 Mon Sep 17 00:00:00 2001 From: Anvesh Komuravelli Date: Thu, 28 Apr 2022 14:42:00 -0700 Subject: [PATCH 3/4] Update protection info on recovered logs data (#9875) Summary: Update protection info on recovered logs data Pull Request resolved: https://github.com/facebook/rocksdb/pull/9875 Test Plan: - Benchmark setup: `TEST_TMPDIR=/dev/shm/100MB_WAL_DB/ ./db_bench -benchmarks=fillrandom -write_buffer_size=1048576000` - Benchmark command: `TEST_TMPDIR=/dev/shm/100MB_WAL_DB/ /usr/bin/time ./db_bench -use_existing_db=true -benchmarks=overwrite -write_buffer_size=1048576000 -writes=1 -report_open_timing=true` - Results before this PR ``` OpenDb: 2350.14 milliseconds OpenDb: 2296.94 milliseconds OpenDb: 2184.29 milliseconds OpenDb: 2167.59 milliseconds OpenDb: 2231.24 milliseconds OpenDb: 2109.57 milliseconds OpenDb: 2197.71 milliseconds OpenDb: 2120.8 milliseconds OpenDb: 2148.12 milliseconds OpenDb: 2207.95 milliseconds ``` - Results after this PR ``` OpenDb: 2424.52 milliseconds OpenDb: 2359.84 milliseconds OpenDb: 2317.68 milliseconds OpenDb: 2339.4 milliseconds OpenDb: 2325.36 milliseconds OpenDb: 2321.06 milliseconds OpenDb: 2353.98 milliseconds OpenDb: 2344.64 milliseconds OpenDb: 2384.09 milliseconds OpenDb: 2428.58 milliseconds ``` Mean regressed 7.2% (2201.4 -> 2359.9) Reviewed By: ajkr Differential Revision: D36012787 Pulled By: akomurav fbshipit-source-id: d2aba09f29c6beb2fd0fe8e1e359be910b4ef02a --- db/db_impl/db_impl_open.cc | 6 +- db/write_batch.cc | 196 +++++++++++++++--- include/rocksdb/write_batch.h | 20 +- tools/ldb_cmd.cc | 5 +- utilities/transactions/write_prepared_txn.cc | 4 +- .../transactions/write_prepared_txn_db.h | 4 +- 6 files changed, 204 insertions(+), 31 deletions(-) diff --git a/db/db_impl/db_impl_open.cc b/db/db_impl/db_impl_open.cc index 0377a7a5c..7a2ff8f49 100644 --- a/db/db_impl/db_impl_open.cc +++ b/db/db_impl/db_impl_open.cc @@ -947,7 +947,6 @@ Status DBImpl::RecoverLogFiles(const std::vector& wal_numbers, // Read all the records and add to a memtable std::string scratch; Slice record; - WriteBatch batch; TEST_SYNC_POINT_CALLBACK("DBImpl::RecoverLogFiles:BeforeReadWal", /*arg=*/nullptr); @@ -961,10 +960,15 @@ Status DBImpl::RecoverLogFiles(const std::vector& wal_numbers, continue; } + // We create a new batch and initialize with a valid prot_info_ to store + // the data checksums + WriteBatch batch(0, 0, 8, 0); + status = WriteBatchInternal::SetContents(&batch, record); if (!status.ok()) { return status; } + SequenceNumber sequence = WriteBatchInternal::Sequence(&batch); if (immutable_db_options_.wal_recovery_mode == diff --git a/db/write_batch.cc b/db/write_batch.cc index 8a590a7f2..77e91504e 100644 --- a/db/write_batch.cc +++ b/db/write_batch.cc @@ -152,14 +152,6 @@ struct SavePoints { std::stack> stack; }; -WriteBatch::WriteBatch(size_t reserved_bytes, size_t max_bytes) - : content_flags_(0), max_bytes_(max_bytes), rep_() { - rep_.reserve((reserved_bytes > WriteBatchInternal::kHeader) - ? reserved_bytes - : WriteBatchInternal::kHeader); - rep_.resize(WriteBatchInternal::kHeader); -} - WriteBatch::WriteBatch(size_t reserved_bytes, size_t max_bytes, size_t protection_bytes_per_key, size_t default_cf_ts_sz) : content_flags_(0), @@ -580,14 +572,16 @@ Status WriteBatchInternal::Iterate(const WriteBatch* wb, s = handler->MarkBeginPrepare(); assert(s.ok()); empty_batch = false; - if (!handler->WriteAfterCommit()) { + if (handler->WriteAfterCommit() == + WriteBatch::Handler::OptionState::kDisabled) { s = Status::NotSupported( "WriteCommitted txn tag when write_after_commit_ is disabled (in " "WritePrepared/WriteUnprepared mode). If it is not due to " "corruption, the WAL must be emptied before changing the " "WritePolicy."); } - if (handler->WriteBeforePrepare()) { + if (handler->WriteBeforePrepare() == + WriteBatch::Handler::OptionState::kEnabled) { s = Status::NotSupported( "WriteCommitted txn tag when write_before_prepare_ is enabled " "(in WriteUnprepared mode). If it is not due to corruption, the " @@ -600,7 +594,8 @@ Status WriteBatchInternal::Iterate(const WriteBatch* wb, s = handler->MarkBeginPrepare(); assert(s.ok()); empty_batch = false; - if (handler->WriteAfterCommit()) { + if (handler->WriteAfterCommit() == + WriteBatch::Handler::OptionState::kEnabled) { s = Status::NotSupported( "WritePrepared/WriteUnprepared txn tag when write_after_commit_ " "is enabled (in default WriteCommitted mode). If it is not due " @@ -614,13 +609,15 @@ Status WriteBatchInternal::Iterate(const WriteBatch* wb, s = handler->MarkBeginPrepare(true /* unprepared */); assert(s.ok()); empty_batch = false; - if (handler->WriteAfterCommit()) { + if (handler->WriteAfterCommit() == + WriteBatch::Handler::OptionState::kEnabled) { s = Status::NotSupported( "WriteUnprepared txn tag when write_after_commit_ is enabled (in " "default WriteCommitted mode). If it is not due to corruption, " "the WAL must be emptied before changing the WritePolicy."); } - if (!handler->WriteBeforePrepare()) { + if (handler->WriteBeforePrepare() == + WriteBatch::Handler::OptionState::kDisabled) { s = Status::NotSupported( "WriteUnprepared txn tag when write_before_prepare_ is disabled " "(in WriteCommitted/WritePrepared mode). If it is not due to " @@ -1494,6 +1491,8 @@ Status WriteBatch::UpdateTimestamps( return s; } +namespace { + class MemTableInserter : public WriteBatch::Handler { SequenceNumber sequence_; @@ -1581,9 +1580,24 @@ class MemTableInserter : public WriteBatch::Handler { return res; } + void DecrementProtectionInfoIdxForTryAgain() { + if (prot_info_ != nullptr) --prot_info_idx_; + } + + void ResetProtectionInfo() { + prot_info_idx_ = 0; + prot_info_ = nullptr; + } + protected: - bool WriteBeforePrepare() const override { return write_before_prepare_; } - bool WriteAfterCommit() const override { return write_after_commit_; } + Handler::OptionState WriteBeforePrepare() const override { + return write_before_prepare_ ? Handler::OptionState::kEnabled + : Handler::OptionState::kDisabled; + } + Handler::OptionState WriteAfterCommit() const override { + return write_after_commit_ ? Handler::OptionState::kEnabled + : Handler::OptionState::kDisabled; + } public: // cf_mems should not be shared with concurrent inserters @@ -1871,15 +1885,25 @@ class MemTableInserter : public WriteBatch::Handler { Status PutCF(uint32_t column_family_id, const Slice& key, const Slice& value) override { const auto* kv_prot_info = NextProtectionInfo(); + Status ret_status; if (kv_prot_info != nullptr) { // Memtable needs seqno, doesn't need CF ID auto mem_kv_prot_info = kv_prot_info->StripC(column_family_id).ProtectS(sequence_); - return PutCFImpl(column_family_id, key, value, kTypeValue, - &mem_kv_prot_info); + ret_status = PutCFImpl(column_family_id, key, value, kTypeValue, + &mem_kv_prot_info); + } else { + ret_status = PutCFImpl(column_family_id, key, value, kTypeValue, + nullptr /* kv_prot_info */); } - return PutCFImpl(column_family_id, key, value, kTypeValue, - nullptr /* kv_prot_info */); + // TODO: this assumes that if TryAgain status is returned to the caller, + // the operation is actually tried again. The proper way to do this is to + // pass a `try_again` parameter to the operation itself and decrement + // prot_info_idx_ based on that + if (UNLIKELY(ret_status.IsTryAgain())) { + DecrementProtectionInfoIdxForTryAgain(); + } + return ret_status; } Status DeleteImpl(uint32_t /*column_family_id*/, const Slice& key, @@ -1926,6 +1950,9 @@ class MemTableInserter : public WriteBatch::Handler { } else if (ret_status.ok()) { MaybeAdvanceSeq(false /* batch_boundary */); } + if (UNLIKELY(ret_status.IsTryAgain())) { + DecrementProtectionInfoIdxForTryAgain(); + } return ret_status; } @@ -1957,6 +1984,9 @@ class MemTableInserter : public WriteBatch::Handler { ret_status = WriteBatchInternal::Delete(rebuilding_trx_, column_family_id, key); } + if (UNLIKELY(ret_status.IsTryAgain())) { + DecrementProtectionInfoIdxForTryAgain(); + } return ret_status; } @@ -1985,6 +2015,9 @@ class MemTableInserter : public WriteBatch::Handler { } else if (ret_status.ok()) { MaybeAdvanceSeq(false /* batch_boundary */); } + if (UNLIKELY(ret_status.IsTryAgain())) { + DecrementProtectionInfoIdxForTryAgain(); + } return ret_status; } assert(ret_status.ok()); @@ -2009,6 +2042,9 @@ class MemTableInserter : public WriteBatch::Handler { ret_status = WriteBatchInternal::SingleDelete(rebuilding_trx_, column_family_id, key); } + if (UNLIKELY(ret_status.IsTryAgain())) { + DecrementProtectionInfoIdxForTryAgain(); + } return ret_status; } @@ -2038,6 +2074,9 @@ class MemTableInserter : public WriteBatch::Handler { } else if (ret_status.ok()) { MaybeAdvanceSeq(false /* batch_boundary */); } + if (UNLIKELY(ret_status.IsTryAgain())) { + DecrementProtectionInfoIdxForTryAgain(); + } return ret_status; } assert(ret_status.ok()); @@ -2092,6 +2131,9 @@ class MemTableInserter : public WriteBatch::Handler { ret_status = WriteBatchInternal::DeleteRange( rebuilding_trx_, column_family_id, begin_key, end_key); } + if (UNLIKELY(ret_status.IsTryAgain())) { + DecrementProtectionInfoIdxForTryAgain(); + } return ret_status; } @@ -2121,6 +2163,9 @@ class MemTableInserter : public WriteBatch::Handler { } else if (ret_status.ok()) { MaybeAdvanceSeq(false /* batch_boundary */); } + if (UNLIKELY(ret_status.IsTryAgain())) { + DecrementProtectionInfoIdxForTryAgain(); + } return ret_status; } assert(ret_status.ok()); @@ -2242,23 +2287,31 @@ class MemTableInserter : public WriteBatch::Handler { ret_status = WriteBatchInternal::Merge(rebuilding_trx_, column_family_id, key, value); } + if (UNLIKELY(ret_status.IsTryAgain())) { + DecrementProtectionInfoIdxForTryAgain(); + } return ret_status; } Status PutBlobIndexCF(uint32_t column_family_id, const Slice& key, const Slice& value) override { const auto* kv_prot_info = NextProtectionInfo(); + Status ret_status; if (kv_prot_info != nullptr) { // Memtable needs seqno, doesn't need CF ID auto mem_kv_prot_info = kv_prot_info->StripC(column_family_id).ProtectS(sequence_); // Same as PutCF except for value type. - return PutCFImpl(column_family_id, key, value, kTypeBlobIndex, - &mem_kv_prot_info); + ret_status = PutCFImpl(column_family_id, key, value, kTypeBlobIndex, + &mem_kv_prot_info); } else { - return PutCFImpl(column_family_id, key, value, kTypeBlobIndex, - nullptr /* kv_prot_info */); + ret_status = PutCFImpl(column_family_id, key, value, kTypeBlobIndex, + nullptr /* kv_prot_info */); } + if (UNLIKELY(ret_status.IsTryAgain())) { + DecrementProtectionInfoIdxForTryAgain(); + } + return ret_status; } void CheckMemtableFull() { @@ -2401,6 +2454,7 @@ class MemTableInserter : public WriteBatch::Handler { const auto& batch_info = trx->batches_.begin()->second; // all inserts must reference this trx log number log_number_ref_ = batch_info.log_number_; + ResetProtectionInfo(); s = batch_info.batch_->Iterate(this); log_number_ref_ = 0; } @@ -2422,6 +2476,10 @@ class MemTableInserter : public WriteBatch::Handler { const bool batch_boundry = true; MaybeAdvanceSeq(batch_boundry); + if (UNLIKELY(s.IsTryAgain())) { + DecrementProtectionInfoIdxForTryAgain(); + } + return s; } @@ -2466,6 +2524,7 @@ class MemTableInserter : public WriteBatch::Handler { return ucmp->timestamp_size(); }); if (s.ok()) { + ResetProtectionInfo(); s = batch_info.batch_->Iterate(this); log_number_ref_ = 0; } @@ -2488,6 +2547,10 @@ class MemTableInserter : public WriteBatch::Handler { constexpr bool batch_boundary = true; MaybeAdvanceSeq(batch_boundary); + if (UNLIKELY(s.IsTryAgain())) { + DecrementProtectionInfoIdxForTryAgain(); + } + return s; } @@ -2523,6 +2586,8 @@ class MemTableInserter : public WriteBatch::Handler { } }; +} // namespace + // This function can only be called in these conditions: // 1) During Recovery() // 2) During Write(), in a single-threaded write thread @@ -2613,11 +2678,94 @@ Status WriteBatchInternal::InsertInto( return s; } +namespace { + +// This class updates protection info for a WriteBatch. +class ProtectionInfoUpdater : public WriteBatch::Handler { + public: + explicit ProtectionInfoUpdater(WriteBatch::ProtectionInfo* prot_info) + : prot_info_(prot_info) {} + + ~ProtectionInfoUpdater() override {} + + Status PutCF(uint32_t cf, const Slice& key, const Slice& val) override { + return UpdateProtInfo(cf, key, val, kTypeValue); + } + + Status DeleteCF(uint32_t cf, const Slice& key) override { + return UpdateProtInfo(cf, key, "", kTypeDeletion); + } + + Status SingleDeleteCF(uint32_t cf, const Slice& key) override { + return UpdateProtInfo(cf, key, "", kTypeSingleDeletion); + } + + Status DeleteRangeCF(uint32_t cf, const Slice& begin_key, + const Slice& end_key) override { + return UpdateProtInfo(cf, begin_key, end_key, kTypeRangeDeletion); + } + + Status MergeCF(uint32_t cf, const Slice& key, const Slice& val) override { + return UpdateProtInfo(cf, key, val, kTypeMerge); + } + + Status PutBlobIndexCF(uint32_t cf, const Slice& key, + const Slice& val) override { + return UpdateProtInfo(cf, key, val, kTypeBlobIndex); + } + + Status MarkBeginPrepare(bool /* unprepare */) override { + return Status::OK(); + } + + Status MarkEndPrepare(const Slice& /* xid */) override { + return Status::OK(); + } + + Status MarkCommit(const Slice& /* xid */) override { return Status::OK(); } + + Status MarkCommitWithTimestamp(const Slice& /* xid */, + const Slice& /* ts */) override { + return Status::OK(); + } + + Status MarkRollback(const Slice& /* xid */) override { return Status::OK(); } + + Status MarkNoop(bool /* empty_batch */) override { return Status::OK(); } + + private: + Status UpdateProtInfo(uint32_t cf, const Slice& key, const Slice& val, + const ValueType op_type) { + if (prot_info_) { + prot_info_->entries_.emplace_back( + ProtectionInfo64().ProtectKVO(key, val, op_type).ProtectC(cf)); + } + return Status::OK(); + } + + // No copy or move. + ProtectionInfoUpdater(const ProtectionInfoUpdater&) = delete; + ProtectionInfoUpdater(ProtectionInfoUpdater&&) = delete; + ProtectionInfoUpdater& operator=(const ProtectionInfoUpdater&) = delete; + ProtectionInfoUpdater& operator=(ProtectionInfoUpdater&&) = delete; + + WriteBatch::ProtectionInfo* const prot_info_ = nullptr; +}; + +} // namespace + Status WriteBatchInternal::SetContents(WriteBatch* b, const Slice& contents) { assert(contents.size() >= WriteBatchInternal::kHeader); - assert(b->prot_info_ == nullptr); + b->rep_.assign(contents.data(), contents.size()); b->content_flags_.store(ContentFlags::DEFERRED, std::memory_order_relaxed); + + // If we have a prot_info_, update protection info entries for the batch. + if (b->prot_info_) { + ProtectionInfoUpdater prot_info_updater(b->prot_info_.get()); + return b->Iterate(&prot_info_updater); + } + return Status::OK(); } diff --git a/include/rocksdb/write_batch.h b/include/rocksdb/write_batch.h index d39727fac..4078ceeaa 100644 --- a/include/rocksdb/write_batch.h +++ b/include/rocksdb/write_batch.h @@ -63,7 +63,9 @@ struct SavePoint { class WriteBatch : public WriteBatchBase { public: - explicit WriteBatch(size_t reserved_bytes = 0, size_t max_bytes = 0); + explicit WriteBatch(size_t reserved_bytes = 0, size_t max_bytes = 0) + : WriteBatch(reserved_bytes, max_bytes, 0, 0) {} + // `protection_bytes_per_key` is the number of bytes used to store // protection information for each key entry. Currently supported values are // zero (disabled) and eight. @@ -318,8 +320,17 @@ class WriteBatch : public WriteBatchBase { protected: friend class WriteBatchInternal; - virtual bool WriteAfterCommit() const { return true; } - virtual bool WriteBeforePrepare() const { return false; } + enum class OptionState { + kUnknown, + kDisabled, + kEnabled, + }; + virtual OptionState WriteAfterCommit() const { + return OptionState::kUnknown; + } + virtual OptionState WriteBeforePrepare() const { + return OptionState::kUnknown; + } }; Status Iterate(Handler* handler) const; @@ -402,6 +413,9 @@ class WriteBatch : public WriteBatchBase { struct ProtectionInfo; size_t GetProtectionBytesPerKey() const; + // Clears prot_info_ if there are no entries. + void ClearProtectionInfoIfEmpty(); + private: friend class WriteBatchInternal; friend class LocalSavePoint; diff --git a/tools/ldb_cmd.cc b/tools/ldb_cmd.cc index 638fbe262..2228ea47e 100644 --- a/tools/ldb_cmd.cc +++ b/tools/ldb_cmd.cc @@ -2538,7 +2538,10 @@ class InMemoryHandler : public WriteBatch::Handler { ~InMemoryHandler() override {} protected: - bool WriteAfterCommit() const override { return write_after_commit_; } + Handler::OptionState WriteAfterCommit() const override { + return write_after_commit_ ? Handler::OptionState::kEnabled + : Handler::OptionState::kDisabled; + } private: std::stringstream& row_; diff --git a/utilities/transactions/write_prepared_txn.cc b/utilities/transactions/write_prepared_txn.cc index 97a964a23..ce2975354 100644 --- a/utilities/transactions/write_prepared_txn.cc +++ b/utilities/transactions/write_prepared_txn.cc @@ -374,7 +374,9 @@ Status WritePreparedTxn::RollbackInternal() { } protected: - bool WriteAfterCommit() const override { return false; } + Handler::OptionState WriteAfterCommit() const override { + return Handler::OptionState::kDisabled; + } } rollback_handler(db_impl_, wpt_db_, read_at_seq, &rollback_batch, *cf_comp_map_shared_ptr.get(), *cf_map_shared_ptr.get(), wpt_db_->txn_db_options_.rollback_merge_operands, diff --git a/utilities/transactions/write_prepared_txn_db.h b/utilities/transactions/write_prepared_txn_db.h index 5ae29b3f3..105dfe09f 100644 --- a/utilities/transactions/write_prepared_txn_db.h +++ b/utilities/transactions/write_prepared_txn_db.h @@ -1082,7 +1082,9 @@ struct SubBatchCounter : public WriteBatch::Handler { } Status MarkBeginPrepare(bool) override { return Status::OK(); } Status MarkRollback(const Slice&) override { return Status::OK(); } - bool WriteAfterCommit() const override { return false; } + Handler::OptionState WriteAfterCommit() const override { + return Handler::OptionState::kDisabled; + } }; SnapshotBackup WritePreparedTxnDB::AssignMinMaxSeqs(const Snapshot* snapshot, From 2b5c29f9f3a5c622031368bf3bf4566f5c590ce5 Mon Sep 17 00:00:00 2001 From: Yanqin Jin Date: Thu, 28 Apr 2022 14:48:27 -0700 Subject: [PATCH 4/4] Enforce the contract of SingleDelete (#9888) Summary: Enforce the contract of SingleDelete so that they are not mixed with Delete for the same key. Otherwise, it will lead to undefined behavior. See https://github.com/facebook/rocksdb/wiki/Single-Delete#notes. Also fix unit tests and write-unprepared. Pull Request resolved: https://github.com/facebook/rocksdb/pull/9888 Test Plan: make check Reviewed By: ajkr Differential Revision: D35837817 Pulled By: riversand963 fbshipit-source-id: acd06e4dcba8cb18df92b44ed18c57e10e5a7635 --- HISTORY.md | 3 +++ db/compaction/compaction_iterator.cc | 26 +++++++++++++------ db/compaction/compaction_job_test.cc | 24 +++++++++-------- test_util/transaction_test_util.cc | 8 ++++-- test_util/transaction_test_util.h | 17 ++++++++++++ utilities/transactions/transaction_test.cc | 4 +++ .../write_prepared_transaction_test.cc | 2 ++ .../transactions/write_unprepared_txn.cc | 6 ++++- 8 files changed, 68 insertions(+), 22 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 7d622caa9..2055e6d74 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -17,6 +17,9 @@ * RocksDB calls FileSystem::Poll API during FilePrefetchBuffer destruction which impacts performance as it waits for read requets completion which is not needed anymore. Calling FileSystem::AbortIO to abort those requests instead fixes that performance issue. * Fixed unnecessary block cache contention when queries within a MultiGet batch and across parallel batches access the same data block, which previously could cause severely degraded performance in this unusual case. (In more typical MultiGet cases, this fix is expected to yield a small or negligible performance improvement.) +### Behavior changes +* Enforce the existing contract of SingleDelete so that SingleDelete cannot be mixed with Delete because it leads to undefined behavior. Fix a number of unit tests that violate the contract but happen to pass. + ## 7.2.0 (04/15/2022) ### Bug Fixes * Fixed bug which caused rocksdb failure in the situation when rocksdb was accessible using UNC path diff --git a/db/compaction/compaction_iterator.cc b/db/compaction/compaction_iterator.cc index a9e1990b8..9f5bc4bc8 100644 --- a/db/compaction/compaction_iterator.cc +++ b/db/compaction/compaction_iterator.cc @@ -625,24 +625,34 @@ void CompactionIterator::NextFromInput() { TEST_SYNC_POINT_CALLBACK( "CompactionIterator::NextFromInput:SingleDelete:2", nullptr); - if (next_ikey.type == kTypeSingleDeletion || - next_ikey.type == kTypeDeletion) { + if (next_ikey.type == kTypeSingleDeletion) { // We encountered two SingleDeletes for same key in a row. This // could be due to unexpected user input. If write-(un)prepared // transaction is used, this could also be due to releasing an old // snapshot between a Put and its matching SingleDelete. - // Furthermore, if write-(un)prepared transaction is rolled back - // after prepare, we will write a Delete to cancel a prior Put. If - // old snapshot is released between a later Put and its matching - // SingleDelete, we will end up with a Delete followed by - // SingleDelete. // Skip the first SingleDelete and let the next iteration decide - // how to handle the second SingleDelete or Delete. + // how to handle the second SingleDelete. // First SingleDelete has been skipped since we already called // input_.Next(). ++iter_stats_.num_record_drop_obsolete; ++iter_stats_.num_single_del_mismatch; + } else if (next_ikey.type == kTypeDeletion) { + std::ostringstream oss; + oss << "Found SD and type: " << static_cast(next_ikey.type) + << " on the same key, violating the contract " + "of SingleDelete. Check your application to make sure the " + "application does not mix SingleDelete and Delete for " + "the same key. If you are using " + "write-prepared/write-unprepared transactions, and use " + "SingleDelete to delete certain keys, then make sure " + "TransactionDBOptions::rollback_deletion_type_callback is " + "configured properly. Mixing SD and DEL can lead to " + "undefined behaviors"; + ROCKS_LOG_ERROR(info_log_, "%s", oss.str().c_str()); + valid_ = false; + status_ = Status::Corruption(oss.str()); + return; } else if (!is_timestamp_eligible_for_gc) { // We cannot drop the SingleDelete as timestamp is enabled, and // timestamp of this key is greater than or equal to diff --git a/db/compaction/compaction_job_test.cc b/db/compaction/compaction_job_test.cc index 7d2564695..2032af44a 100644 --- a/db/compaction/compaction_job_test.cc +++ b/db/compaction/compaction_job_test.cc @@ -890,10 +890,10 @@ TEST_F(CompactionJobTest, MultiSingleDelete) { // -> Snapshot Put // K: SDel SDel Put SDel Put Put Snapshot SDel Put SDel SDel Put SDel // -> Snapshot Put Snapshot SDel - // L: SDel Put Del Put SDel Snapshot Del Put Del SDel Put SDel - // -> Snapshot SDel - // M: (Put) SDel Put Del Put SDel Snapshot Put Del SDel Put SDel Del - // -> SDel Snapshot Del + // L: SDel Put SDel Put SDel Snapshot SDel Put SDel SDel Put SDel + // -> Snapshot SDel Put SDel + // M: (Put) SDel Put SDel Put SDel Snapshot Put SDel SDel Put SDel SDel + // -> SDel Snapshot Put SDel NewDB(); auto file1 = mock::MakeMockFile({ @@ -924,14 +924,14 @@ TEST_F(CompactionJobTest, MultiSingleDelete) { {KeyStr("L", 16U, kTypeSingleDeletion), ""}, {KeyStr("L", 15U, kTypeValue), "val"}, {KeyStr("L", 14U, kTypeSingleDeletion), ""}, - {KeyStr("L", 13U, kTypeDeletion), ""}, + {KeyStr("L", 13U, kTypeSingleDeletion), ""}, {KeyStr("L", 12U, kTypeValue), "val"}, - {KeyStr("L", 11U, kTypeDeletion), ""}, - {KeyStr("M", 16U, kTypeDeletion), ""}, + {KeyStr("L", 11U, kTypeSingleDeletion), ""}, + {KeyStr("M", 16U, kTypeSingleDeletion), ""}, {KeyStr("M", 15U, kTypeSingleDeletion), ""}, {KeyStr("M", 14U, kTypeValue), "val"}, {KeyStr("M", 13U, kTypeSingleDeletion), ""}, - {KeyStr("M", 12U, kTypeDeletion), ""}, + {KeyStr("M", 12U, kTypeSingleDeletion), ""}, {KeyStr("M", 11U, kTypeValue), "val"}, }); AddMockFile(file1); @@ -972,12 +972,12 @@ TEST_F(CompactionJobTest, MultiSingleDelete) { {KeyStr("K", 1U, kTypeSingleDeletion), ""}, {KeyStr("L", 5U, kTypeSingleDeletion), ""}, {KeyStr("L", 4U, kTypeValue), "val"}, - {KeyStr("L", 3U, kTypeDeletion), ""}, + {KeyStr("L", 3U, kTypeSingleDeletion), ""}, {KeyStr("L", 2U, kTypeValue), "val"}, {KeyStr("L", 1U, kTypeSingleDeletion), ""}, {KeyStr("M", 10U, kTypeSingleDeletion), ""}, {KeyStr("M", 7U, kTypeValue), "val"}, - {KeyStr("M", 5U, kTypeDeletion), ""}, + {KeyStr("M", 5U, kTypeSingleDeletion), ""}, {KeyStr("M", 4U, kTypeValue), "val"}, {KeyStr("M", 3U, kTypeSingleDeletion), ""}, }); @@ -1019,7 +1019,9 @@ TEST_F(CompactionJobTest, MultiSingleDelete) { {KeyStr("K", 8U, kTypeValue), "val3"}, {KeyStr("L", 16U, kTypeSingleDeletion), ""}, {KeyStr("L", 15U, kTypeValue), ""}, - {KeyStr("M", 16U, kTypeDeletion), ""}, + {KeyStr("L", 11U, kTypeSingleDeletion), ""}, + {KeyStr("M", 15U, kTypeSingleDeletion), ""}, + {KeyStr("M", 14U, kTypeValue), ""}, {KeyStr("M", 3U, kTypeSingleDeletion), ""}}); SetLastSequence(22U); diff --git a/test_util/transaction_test_util.cc b/test_util/transaction_test_util.cc index 36b2e9ec4..3eaa7fd6f 100644 --- a/test_util/transaction_test_util.cc +++ b/test_util/transaction_test_util.cc @@ -91,7 +91,7 @@ Status RandomTransactionInserter::DBGet( Status s; // Five digits (since the largest uint16_t is 65535) plus the NUL // end char. - char prefix_buf[6]; + char prefix_buf[6] = {0}; // Pad prefix appropriately so we can iterate over each set assert(set_i + 1 <= 9999); snprintf(prefix_buf, sizeof(prefix_buf), "%.4u", set_i + 1); @@ -165,7 +165,11 @@ bool RandomTransactionInserter::DoInsert(DB* db, Transaction* txn, // Increment key std::string sum = ToString(int_value + incr); if (txn != nullptr) { - s = txn->SingleDelete(key); + if ((set_i % 4) != 0) { + s = txn->SingleDelete(key); + } else { + s = txn->Delete(key); + } if (!get_for_update && (s.IsBusy() || s.IsTimedOut())) { // If the initial get was not for update, then the key is not locked // before put and put could fail due to concurrent writes. diff --git a/test_util/transaction_test_util.h b/test_util/transaction_test_util.h index 086b0ea6f..175376f5f 100644 --- a/test_util/transaction_test_util.h +++ b/test_util/transaction_test_util.h @@ -33,6 +33,23 @@ class Random64; // RandomTransactionInserter with similar arguments using the same DB. class RandomTransactionInserter { public: + static bool RollbackDeletionTypeCallback(const Slice& key) { + // These are hard-coded atm. See how RandomTransactionInserter::DoInsert() + // determines whether to use SingleDelete or Delete for a key. + assert(key.size() >= 4); + const char* ptr = key.data(); + assert(ptr); + while (ptr && ptr < key.data() + 4 && *ptr == '0') { + ++ptr; + } + std::string prefix(ptr, 4 - (ptr - key.data())); + unsigned long set_i = std::stoul(prefix); + assert(set_i > 0); + assert(set_i <= 9999); + --set_i; + return ((set_i % 4) != 0); + } + // num_keys is the number of keys in each set. // num_sets is the number of sets of keys. // cmt_delay_ms is the delay between prepare (if there is any) and commit diff --git a/utilities/transactions/transaction_test.cc b/utilities/transactions/transaction_test.cc index 798fb2ad0..20ee73e27 100644 --- a/utilities/transactions/transaction_test.cc +++ b/utilities/transactions/transaction_test.cc @@ -5469,6 +5469,10 @@ Status TransactionStressTestInserter( TEST_P(MySQLStyleTransactionTest, TransactionStressTest) { // Small write buffer to trigger more compactions options.write_buffer_size = 1024; + txn_db_options.rollback_deletion_type_callback = + [](TransactionDB*, ColumnFamilyHandle*, const Slice& key) { + return RandomTransactionInserter::RollbackDeletionTypeCallback(key); + }; ASSERT_OK(ReOpenNoDelete()); constexpr size_t num_workers = 4; // worker threads count constexpr size_t num_checkers = 2; // checker threads count diff --git a/utilities/transactions/write_prepared_transaction_test.cc b/utilities/transactions/write_prepared_transaction_test.cc index 5a90bab88..299b8b332 100644 --- a/utilities/transactions/write_prepared_transaction_test.cc +++ b/utilities/transactions/write_prepared_transaction_test.cc @@ -3202,6 +3202,8 @@ TEST_P(WritePreparedTransactionTest, ReleaseEarliestSnapshotAfterSeqZeroing2) { TEST_P(WritePreparedTransactionTest, SingleDeleteAfterRollback) { constexpr size_t kSnapshotCacheBits = 7; // same as default constexpr size_t kCommitCacheBits = 0; // minimum commit cache + txn_db_options.rollback_deletion_type_callback = + [](TransactionDB*, ColumnFamilyHandle*, const Slice&) { return true; }; UpdateTransactionDBOptions(kSnapshotCacheBits, kCommitCacheBits); options.disable_auto_compactions = true; ASSERT_OK(ReOpen()); diff --git a/utilities/transactions/write_unprepared_txn.cc b/utilities/transactions/write_unprepared_txn.cc index 6cc1603d1..7623c6d73 100644 --- a/utilities/transactions/write_unprepared_txn.cc +++ b/utilities/transactions/write_unprepared_txn.cc @@ -675,7 +675,11 @@ Status WriteUnpreparedTxn::WriteRollbackKeys( s = rollback_batch->Put(cf_handle, key, pinnable_val); assert(s.ok()); } else if (s.IsNotFound()) { - s = rollback_batch->Delete(cf_handle, key); + if (wupt_db_->ShouldRollbackWithSingleDelete(cf_handle, key)) { + s = rollback_batch->SingleDelete(cf_handle, key); + } else { + s = rollback_batch->Delete(cf_handle, key); + } assert(s.ok()); } else { return s;