/Users/deen/code/yugabyte-db/src/yb/docdb/docdb_test_util.cc
Line | Count | Source (jump to first uncovered line) |
1 | | // Copyright (c) YugaByte, Inc. |
2 | | // |
3 | | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except |
4 | | // in compliance with the License. You may obtain a copy of the License at |
5 | | // |
6 | | // http://www.apache.org/licenses/LICENSE-2.0 |
7 | | // |
8 | | // Unless required by applicable law or agreed to in writing, software distributed under the License |
9 | | // is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express |
10 | | // or implied. See the License for the specific language governing permissions and limitations |
11 | | // under the License. |
12 | | // |
13 | | |
14 | | #include "yb/docdb/docdb_test_util.h" |
15 | | |
16 | | #include <algorithm> |
17 | | #include <memory> |
18 | | #include <sstream> |
19 | | |
20 | | #include "yb/common/hybrid_time.h" |
21 | | |
22 | | #include "yb/docdb/doc_key.h" |
23 | | #include "yb/docdb/doc_reader.h" |
24 | | #include "yb/docdb/docdb-internal.h" |
25 | | #include "yb/docdb/docdb.h" |
26 | | #include "yb/docdb/docdb_compaction_filter.h" |
27 | | #include "yb/docdb/docdb_debug.h" |
28 | | #include "yb/docdb/in_mem_docdb.h" |
29 | | |
30 | | #include "yb/gutil/strings/substitute.h" |
31 | | |
32 | | #include "yb/rocksdb/db/filename.h" |
33 | | |
34 | | #include "yb/rocksutil/write_batch_formatter.h" |
35 | | |
36 | | #include "yb/util/bytes_formatter.h" |
37 | | #include "yb/util/env.h" |
38 | | #include "yb/util/path_util.h" |
39 | | #include "yb/util/scope_exit.h" |
40 | | #include "yb/util/status.h" |
41 | | #include "yb/util/string_trim.h" |
42 | | #include "yb/util/test_macros.h" |
43 | | #include "yb/util/tostring.h" |
44 | | |
45 | | using std::endl; |
46 | | using std::make_shared; |
47 | | using std::string; |
48 | | using std::unique_ptr; |
49 | | using std::vector; |
50 | | using std::stringstream; |
51 | | |
52 | | using strings::Substitute; |
53 | | |
54 | | using yb::util::ApplyEagerLineContinuation; |
55 | | using yb::FormatBytesAsStr; |
56 | | using yb::util::TrimStr; |
57 | | using yb::util::LeftShiftTextBlock; |
58 | | using yb::util::TrimCppComments; |
59 | | |
60 | | namespace yb { |
61 | | namespace docdb { |
62 | | |
63 | | namespace { |
64 | | |
65 | | class NonTransactionalStatusProvider: public TransactionStatusManager { |
66 | | public: |
67 | 0 | HybridTime LocalCommitTime(const TransactionId& id) override { |
68 | 0 | Fail(); |
69 | 0 | return HybridTime::kInvalid; |
70 | 0 | } |
71 | | |
72 | 0 | boost::optional<CommitMetadata> LocalCommitData(const TransactionId& id) override { |
73 | 0 | Fail(); |
74 | 0 | return boost::none; |
75 | 0 | } |
76 | | |
77 | 0 | void RequestStatusAt(const StatusRequest& request) override { |
78 | 0 | Fail(); |
79 | 0 | } |
80 | | |
81 | 0 | Result<TransactionMetadata> PrepareMetadata(const TransactionMetadataPB& pb) override { |
82 | 0 | Fail(); |
83 | 0 | return STATUS(Expired, ""); |
84 | 0 | } |
85 | | |
86 | 0 | int64_t RegisterRequest() override { |
87 | 0 | Fail(); |
88 | 0 | return 0; |
89 | 0 | } |
90 | | |
91 | 0 | void UnregisterRequest(int64_t) override { |
92 | 0 | Fail(); |
93 | 0 | } |
94 | | |
95 | 0 | void Abort(const TransactionId& id, TransactionStatusCallback callback) override { |
96 | 0 | Fail(); |
97 | 0 | } |
98 | | |
99 | 0 | void Cleanup(TransactionIdSet&& set) override { |
100 | 0 | Fail(); |
101 | 0 | } |
102 | | |
103 | | void FillPriorities( |
104 | 0 | boost::container::small_vector_base<std::pair<TransactionId, uint64_t>>* inout) override { |
105 | 0 | Fail(); |
106 | 0 | } |
107 | | |
108 | 0 | HybridTime MinRunningHybridTime() const override { |
109 | 0 | return HybridTime::kMax; |
110 | 0 | } |
111 | | |
112 | 0 | Result<HybridTime> WaitForSafeTime(HybridTime safe_time, CoarseTimePoint deadline) override { |
113 | 0 | return STATUS(NotSupported, "WaitForSafeTime not implemented"); |
114 | 0 | } |
115 | | |
116 | 0 | const TabletId& tablet_id() const override { |
117 | 0 | static TabletId result; |
118 | 0 | return result; |
119 | 0 | } |
120 | | |
121 | | private: |
122 | 0 | static void Fail() { |
123 | 0 | LOG(FATAL) << "Internal error: trying to get transaction status for non transactional table"; |
124 | 0 | } |
125 | | }; |
126 | | |
127 | | NonTransactionalStatusProvider kNonTransactionalStatusProvider; |
128 | | |
129 | | } // namespace |
130 | | |
131 | | const TransactionOperationContext kNonTransactionalOperationContext = { |
132 | | TransactionId::Nil(), &kNonTransactionalStatusProvider |
133 | | }; |
134 | | |
135 | 0 | PrimitiveValue GenRandomPrimitiveValue(RandomNumberGenerator* rng) { |
136 | 0 | static vector<string> kFruit = { |
137 | 0 | "Apple", |
138 | 0 | "Apricot", |
139 | 0 | "Avocado", |
140 | 0 | "Banana", |
141 | 0 | "Bilberry", |
142 | 0 | "Blackberry", |
143 | 0 | "Blackcurrant", |
144 | 0 | "Blood orange", |
145 | 0 | "Blueberry", |
146 | 0 | "Boysenberry", |
147 | 0 | "Cantaloupe", |
148 | 0 | "Cherimoya", |
149 | 0 | "Cherry", |
150 | 0 | "Clementine", |
151 | 0 | "Cloudberry", |
152 | 0 | "Coconut", |
153 | 0 | "Cranberry", |
154 | 0 | "Cucumber", |
155 | 0 | "Currant", |
156 | 0 | "Custard apple", |
157 | 0 | "Damson", |
158 | 0 | "Date", |
159 | 0 | "Decaisnea Fargesii", |
160 | 0 | "Dragonfruit", |
161 | 0 | "Durian", |
162 | 0 | "Elderberry", |
163 | 0 | "Feijoa", |
164 | 0 | "Fig", |
165 | 0 | "Goji berry", |
166 | 0 | "Gooseberry", |
167 | 0 | "Grape", |
168 | 0 | "Grapefruit", |
169 | 0 | "Guava", |
170 | 0 | "Honeyberry", |
171 | 0 | "Honeydew", |
172 | 0 | "Huckleberry", |
173 | 0 | "Jabuticaba", |
174 | 0 | "Jackfruit", |
175 | 0 | "Jambul", |
176 | 0 | "Jujube", |
177 | 0 | "Juniper berry", |
178 | 0 | "Kiwifruit", |
179 | 0 | "Kumquat", |
180 | 0 | "Lemon", |
181 | 0 | "Lime", |
182 | 0 | "Longan", |
183 | 0 | "Loquat", |
184 | 0 | "Lychee", |
185 | 0 | "Mandarine", |
186 | 0 | "Mango", |
187 | 0 | "Marionberry", |
188 | 0 | "Melon", |
189 | 0 | "Miracle fruit", |
190 | 0 | "Mulberry", |
191 | 0 | "Nance", |
192 | 0 | "Nectarine", |
193 | 0 | "Olive", |
194 | 0 | "Orange", |
195 | 0 | "Papaya", |
196 | 0 | "Passionfruit", |
197 | 0 | "Peach", |
198 | 0 | "Pear", |
199 | 0 | "Persimmon", |
200 | 0 | "Physalis", |
201 | 0 | "Pineapple", |
202 | 0 | "Plantain", |
203 | 0 | "Plum", |
204 | 0 | "Plumcot (or Pluot)", |
205 | 0 | "Pomegranate", |
206 | 0 | "Pomelo", |
207 | 0 | "Prune (dried plum)", |
208 | 0 | "Purple mangosteen", |
209 | 0 | "Quince", |
210 | 0 | "Raisin", |
211 | 0 | "Rambutan", |
212 | 0 | "Raspberry", |
213 | 0 | "Redcurrant", |
214 | 0 | "Salak", |
215 | 0 | "Salal berry", |
216 | 0 | "Salmonberry", |
217 | 0 | "Satsuma", |
218 | 0 | "Star fruit", |
219 | 0 | "Strawberry", |
220 | 0 | "Tamarillo", |
221 | 0 | "Tamarind", |
222 | 0 | "Tangerine", |
223 | 0 | "Tomato", |
224 | 0 | "Ugli fruit", |
225 | 0 | "Watermelon", |
226 | 0 | "Yuzu" |
227 | 0 | }; |
228 | 0 | switch ((*rng)() % 6) { |
229 | 0 | case 0: |
230 | 0 | return PrimitiveValue(static_cast<int64_t>((*rng)())); |
231 | 0 | case 1: { |
232 | 0 | string s; |
233 | 0 | for (size_t j = 0; j < (*rng)() % 50; ++j) { |
234 | 0 | s.push_back((*rng)() & 0xff); |
235 | 0 | } |
236 | 0 | return PrimitiveValue(s); |
237 | 0 | } |
238 | 0 | case 2: return PrimitiveValue(ValueType::kNullLow); |
239 | 0 | case 3: return PrimitiveValue(ValueType::kTrue); |
240 | 0 | case 4: return PrimitiveValue(ValueType::kFalse); |
241 | 0 | case 5: return PrimitiveValue(kFruit[(*rng)() % kFruit.size()]); |
242 | 0 | } |
243 | 0 | LOG(FATAL) << "Should never get here"; |
244 | 0 | return PrimitiveValue(); // to make the compiler happy |
245 | 0 | } |
246 | | |
247 | | |
248 | | // Generate a vector of random primitive values. |
249 | 0 | vector<PrimitiveValue> GenRandomPrimitiveValues(RandomNumberGenerator* rng, int max_num) { |
250 | 0 | vector<PrimitiveValue> result; |
251 | 0 | for (size_t i = 0; i < (*rng)() % (max_num + 1); ++i) { |
252 | 0 | result.push_back(GenRandomPrimitiveValue(rng)); |
253 | 0 | } |
254 | 0 | return result; |
255 | 0 | } |
256 | | |
257 | 0 | DocKey CreateMinimalDocKey(RandomNumberGenerator* rng, UseHash use_hash) { |
258 | 0 | return use_hash ? DocKey(static_cast<DocKeyHash>((*rng)()), std::vector<PrimitiveValue>(), |
259 | 0 | std::vector<PrimitiveValue>()) : DocKey(); |
260 | 0 | } |
261 | | |
262 | 0 | DocKey GenRandomDocKey(RandomNumberGenerator* rng, UseHash use_hash) { |
263 | 0 | if (use_hash) { |
264 | 0 | return DocKey( |
265 | 0 | static_cast<uint32_t>((*rng)()), // this is just a random value, not a hash function result |
266 | 0 | GenRandomPrimitiveValues(rng), |
267 | 0 | GenRandomPrimitiveValues(rng)); |
268 | 0 | } else { |
269 | 0 | return DocKey(GenRandomPrimitiveValues(rng)); |
270 | 0 | } |
271 | 0 | } |
272 | | |
273 | 0 | vector<DocKey> GenRandomDocKeys(RandomNumberGenerator* rng, UseHash use_hash, int num_keys) { |
274 | 0 | vector<DocKey> result; |
275 | 0 | result.push_back(CreateMinimalDocKey(rng, use_hash)); |
276 | 0 | for (int iteration = 0; iteration < num_keys; ++iteration) { |
277 | 0 | result.push_back(GenRandomDocKey(rng, use_hash)); |
278 | 0 | } |
279 | 0 | return result; |
280 | 0 | } |
281 | | |
282 | 0 | vector<SubDocKey> GenRandomSubDocKeys(RandomNumberGenerator* rng, UseHash use_hash, int num_keys) { |
283 | 0 | vector<SubDocKey> result; |
284 | 0 | result.push_back(SubDocKey(CreateMinimalDocKey(rng, use_hash), HybridTime((*rng)()))); |
285 | 0 | for (int iteration = 0; iteration < num_keys; ++iteration) { |
286 | 0 | result.push_back(SubDocKey(GenRandomDocKey(rng, use_hash))); |
287 | 0 | for (size_t i = 0; i < (*rng)() % (kMaxNumRandomSubKeys + 1); ++i) { |
288 | 0 | result.back().AppendSubKeysAndMaybeHybridTime(GenRandomPrimitiveValue(rng)); |
289 | 0 | } |
290 | 0 | const IntraTxnWriteId write_id = static_cast<IntraTxnWriteId>( |
291 | 0 | (*rng)() % 2 == 0 ? 0 : (*rng)() % 1000000); |
292 | 0 | result.back().set_hybrid_time(DocHybridTime(HybridTime((*rng)()), write_id)); |
293 | 0 | } |
294 | 0 | return result; |
295 | 0 | } |
296 | | // ------------------------------------------------------------------------------------------------ |
297 | | |
298 | 0 | void LogicalRocksDBDebugSnapshot::Capture(rocksdb::DB* rocksdb) { |
299 | 0 | kvs.clear(); |
300 | 0 | rocksdb::ReadOptions read_options; |
301 | 0 | auto iter = unique_ptr<rocksdb::Iterator>(rocksdb->NewIterator(read_options)); |
302 | 0 | iter->SeekToFirst(); |
303 | 0 | while (iter->Valid()) { |
304 | 0 | kvs.emplace_back(iter->key().ToBuffer(), iter->value().ToBuffer()); |
305 | 0 | iter->Next(); |
306 | 0 | } |
307 | | // Save the DocDB debug dump as a string so we can check that we've properly restored the snapshot |
308 | | // in RestoreTo. |
309 | 0 | docdb_debug_dump_str = DocDBDebugDumpToStr(rocksdb); |
310 | 0 | } |
311 | | |
312 | 0 | void LogicalRocksDBDebugSnapshot::RestoreTo(rocksdb::DB *rocksdb) const { |
313 | 0 | rocksdb::ReadOptions read_options; |
314 | 0 | rocksdb::WriteOptions write_options; |
315 | 0 | auto iter = unique_ptr<rocksdb::Iterator>(rocksdb->NewIterator(read_options)); |
316 | 0 | iter->SeekToFirst(); |
317 | 0 | while (iter->Valid()) { |
318 | 0 | ASSERT_OK(rocksdb->Delete(write_options, iter->key())); |
319 | 0 | iter->Next(); |
320 | 0 | } |
321 | 0 | for (const auto& kv : kvs) { |
322 | 0 | ASSERT_OK(rocksdb->Put(write_options, kv.first, kv.second)); |
323 | 0 | } |
324 | 0 | ASSERT_OK(FullyCompactDB(rocksdb)); |
325 | 0 | ASSERT_EQ(docdb_debug_dump_str, DocDBDebugDumpToStr(rocksdb)); |
326 | 0 | } |
327 | | |
328 | | // ------------------------------------------------------------------------------------------------ |
329 | | |
330 | | DocDBLoadGenerator::DocDBLoadGenerator(DocDBRocksDBFixture* fixture, |
331 | | const int num_doc_keys, |
332 | | const int num_unique_subkeys, |
333 | | const UseHash use_hash, |
334 | | const ResolveIntentsDuringRead resolve_intents, |
335 | | const int deletion_chance, |
336 | | const int max_nesting_level, |
337 | | const uint64 random_seed, |
338 | | const int verification_frequency) |
339 | | : fixture_(fixture), |
340 | | doc_keys_(GenRandomDocKeys(&random_, use_hash, num_doc_keys)), |
341 | | resolve_intents_(resolve_intents), |
342 | | possible_subkeys_(GenRandomPrimitiveValues(&random_, num_unique_subkeys)), |
343 | | iteration_(1), |
344 | | deletion_chance_(deletion_chance), |
345 | | max_nesting_level_(max_nesting_level), |
346 | 0 | verification_frequency_(verification_frequency) { |
347 | 0 | CHECK_GE(max_nesting_level_, 1); |
348 | | // Use a fixed seed so that tests are deterministic. |
349 | 0 | random_.seed(random_seed); |
350 | | |
351 | | // This is done so we can use VerifySnapshot with in_mem_docdb_. That should preform a "latest" |
352 | | // read. |
353 | 0 | in_mem_docdb_.SetCaptureHybridTime(HybridTime::kMax); |
354 | 0 | } |
355 | | |
356 | 0 | DocDBLoadGenerator::~DocDBLoadGenerator() = default; |
357 | | |
358 | 0 | void DocDBLoadGenerator::PerformOperation(bool compact_history) { |
359 | | // Increment the iteration right away so we can return from the function at any time. |
360 | 0 | const int current_iteration = iteration_; |
361 | 0 | ++iteration_; |
362 | |
|
363 | 0 | DOCDB_DEBUG_LOG("Starting iteration i=$0", current_iteration); |
364 | 0 | auto dwb = fixture_->MakeDocWriteBatch(); |
365 | 0 | const auto& doc_key = RandomElementOf(doc_keys_, &random_); |
366 | 0 | const KeyBytes encoded_doc_key(doc_key.Encode()); |
367 | |
|
368 | 0 | const SubDocument* current_doc = in_mem_docdb_.GetDocument(doc_key); |
369 | |
|
370 | 0 | bool is_deletion = false; |
371 | 0 | if (current_doc != nullptr && |
372 | 0 | current_doc->value_type() != ValueType::kObject) { |
373 | | // The entire document is not an object, let's delete it. |
374 | 0 | is_deletion = true; |
375 | 0 | } |
376 | |
|
377 | 0 | vector<PrimitiveValue> subkeys; |
378 | 0 | if (!is_deletion) { |
379 | | // Add up to (max_nesting_level_ - 1) subkeys. Combined with the document key itself, this |
380 | | // gives us the desired maximum nesting level. |
381 | 0 | for (size_t j = 0; j < random_() % max_nesting_level_; ++j) { |
382 | 0 | if (current_doc != nullptr && current_doc->value_type() != ValueType::kObject) { |
383 | | // We can't add any more subkeys because we've found a primitive subdocument. |
384 | 0 | break; |
385 | 0 | } |
386 | 0 | subkeys.emplace_back(RandomElementOf(possible_subkeys_, &random_)); |
387 | 0 | if (current_doc != nullptr) { |
388 | 0 | current_doc = current_doc->GetChild(subkeys.back()); |
389 | 0 | } |
390 | 0 | } |
391 | 0 | } |
392 | |
|
393 | 0 | const DocPath doc_path(encoded_doc_key, subkeys); |
394 | 0 | const auto value = GenRandomPrimitiveValue(&random_); |
395 | 0 | const HybridTime hybrid_time(current_iteration); |
396 | 0 | last_operation_ht_ = hybrid_time; |
397 | |
|
398 | 0 | if (random_() % deletion_chance_ == 0) { |
399 | 0 | is_deletion = true; |
400 | 0 | } |
401 | |
|
402 | 0 | const bool doc_already_exists_in_mem = |
403 | 0 | in_mem_docdb_.GetDocument(doc_key) != nullptr; |
404 | |
|
405 | 0 | if (is_deletion) { |
406 | 0 | DOCDB_DEBUG_LOG("Iteration $0: deleting doc path $1", current_iteration, doc_path.ToString()); |
407 | 0 | ASSERT_OK(dwb.DeleteSubDoc(doc_path, ReadHybridTime::Max())); |
408 | 0 | ASSERT_OK(in_mem_docdb_.DeleteSubDoc(doc_path)); |
409 | 0 | } else { |
410 | 0 | DOCDB_DEBUG_LOG("Iteration $0: setting value at doc path $1 to $2", |
411 | 0 | current_iteration, doc_path.ToString(), value.ToString()); |
412 | 0 | ASSERT_OK(in_mem_docdb_.SetPrimitive(doc_path, value)); |
413 | 0 | const auto set_primitive_status = dwb.SetPrimitive(doc_path, value); |
414 | 0 | if (!set_primitive_status.ok()) { |
415 | 0 | DocDBDebugDump(rocksdb(), std::cerr, StorageDbType::kRegular); |
416 | 0 | LOG(INFO) << "doc_path=" << doc_path.ToString(); |
417 | 0 | } |
418 | 0 | ASSERT_OK(set_primitive_status); |
419 | 0 | } |
420 | | |
421 | | // We perform our randomly chosen operation first, both on the production version of DocDB |
422 | | // sitting on top of RocksDB, and on the in-memory single-threaded debug version used for |
423 | | // validation. |
424 | 0 | ASSERT_OK(fixture_->WriteToRocksDB(dwb, hybrid_time)); |
425 | 0 | const SubDocument* const subdoc_from_mem = in_mem_docdb_.GetDocument(doc_key); |
426 | |
|
427 | 0 | TransactionOperationContext txn_op_context = GetReadOperationTransactionContext(); |
428 | | |
429 | | // In case we are asked to compact history, we read the document from RocksDB before and after the |
430 | | // compaction, and expect to get the same result in both cases. |
431 | 0 | for (int do_compaction_now = 0; do_compaction_now <= compact_history; ++do_compaction_now) { |
432 | 0 | if (do_compaction_now) { |
433 | | // This will happen between the two iterations of the loop. If compact_history is false, |
434 | | // there is only one iteration and the compaction does not happen. |
435 | 0 | fixture_->FullyCompactHistoryBefore(hybrid_time); |
436 | 0 | } |
437 | 0 | SubDocKey sub_doc_key(doc_key); |
438 | 0 | auto encoded_sub_doc_key = sub_doc_key.EncodeWithoutHt(); |
439 | 0 | auto doc_from_rocksdb_opt = ASSERT_RESULT(TEST_GetSubDocument( |
440 | 0 | encoded_sub_doc_key, doc_db(), rocksdb::kDefaultQueryId, txn_op_context, |
441 | 0 | CoarseTimePoint::max() /* deadline */)); |
442 | 0 | if (is_deletion && ( |
443 | 0 | doc_path.num_subkeys() == 0 || // Deleted the entire sub-document, |
444 | 0 | !doc_already_exists_in_mem)) { // or the document did not exist in the first place. |
445 | | // In this case, after performing the deletion operation, we definitely should not see the |
446 | | // top-level document in RocksDB or in the in-memory database. |
447 | 0 | ASSERT_FALSE(doc_from_rocksdb_opt); |
448 | 0 | ASSERT_EQ(nullptr, subdoc_from_mem); |
449 | 0 | } else { |
450 | | // This is not a deletion, or we've deleted a sub-key from a document, but the top-level |
451 | | // document should still be there in RocksDB. |
452 | 0 | ASSERT_TRUE(doc_from_rocksdb_opt); |
453 | 0 | ASSERT_NE(nullptr, subdoc_from_mem); |
454 | |
|
455 | 0 | ASSERT_EQ(*subdoc_from_mem, *doc_from_rocksdb_opt); |
456 | 0 | DOCDB_DEBUG_LOG("Retrieved a document from RocksDB: $0", doc_from_rocksdb_opt->ToString()); |
457 | 0 | ASSERT_STR_EQ_VERBOSE_TRIMMED(subdoc_from_mem->ToString(), doc_from_rocksdb_opt->ToString()); |
458 | 0 | } |
459 | 0 | } |
460 | |
|
461 | 0 | if (current_iteration % verification_frequency_ == 0) { |
462 | | // in_mem_docdb_ has its captured_at() hybrid_time set to HybridTime::kMax, so the following |
463 | | // will result in checking the latest state of DocDB stored in RocksDB against in_mem_docdb_. |
464 | 0 | ASSERT_NO_FATALS(VerifySnapshot(in_mem_docdb_)) |
465 | 0 | << "Discrepancy between RocksDB-based and in-memory DocDB state found after iteration " |
466 | 0 | << current_iteration; |
467 | 0 | } |
468 | 0 | } |
469 | | |
470 | 0 | HybridTime DocDBLoadGenerator::last_operation_ht() const { |
471 | 0 | CHECK(last_operation_ht_.is_valid()); |
472 | 0 | return last_operation_ht_; |
473 | 0 | } |
474 | | |
475 | 0 | void DocDBLoadGenerator::FlushRocksDB() { |
476 | 0 | LOG(INFO) << "Forcing a RocksDB flush after hybrid_time " << last_operation_ht().value(); |
477 | 0 | ASSERT_OK(fixture_->FlushRocksDbAndWait()); |
478 | 0 | } |
479 | | |
480 | 0 | void DocDBLoadGenerator::CaptureDocDbSnapshot() { |
481 | | // Capture snapshots from time to time. |
482 | 0 | docdb_snapshots_.emplace_back(); |
483 | 0 | docdb_snapshots_.back().CaptureAt(doc_db(), HybridTime::kMax); |
484 | 0 | docdb_snapshots_.back().SetCaptureHybridTime(last_operation_ht_); |
485 | 0 | } |
486 | | |
487 | 0 | void DocDBLoadGenerator::VerifyOldestSnapshot() { |
488 | 0 | if (!docdb_snapshots_.empty()) { |
489 | 0 | ASSERT_NO_FATALS(VerifySnapshot(GetOldestSnapshot())); |
490 | 0 | } |
491 | 0 | } |
492 | | |
493 | 0 | void DocDBLoadGenerator::CheckIfOldestSnapshotIsStillValid(const HybridTime cleanup_ht) { |
494 | 0 | if (docdb_snapshots_.empty()) { |
495 | 0 | return; |
496 | 0 | } |
497 | | |
498 | 0 | const InMemDocDbState* latest_snapshot_before_ht = nullptr; |
499 | 0 | for (const auto& snapshot : docdb_snapshots_) { |
500 | 0 | const HybridTime snap_ht = snapshot.captured_at(); |
501 | 0 | if (snap_ht.CompareTo(cleanup_ht) < 0 && |
502 | 0 | (latest_snapshot_before_ht == nullptr || |
503 | 0 | latest_snapshot_before_ht->captured_at().CompareTo(snap_ht) < 0)) { |
504 | 0 | latest_snapshot_before_ht = &snapshot; |
505 | 0 | } |
506 | 0 | } |
507 | |
|
508 | 0 | if (latest_snapshot_before_ht == nullptr) { |
509 | 0 | return; |
510 | 0 | } |
511 | | |
512 | 0 | const auto& snapshot = *latest_snapshot_before_ht; |
513 | 0 | LOG(INFO) << "Checking whether snapshot at hybrid_time " |
514 | 0 | << snapshot.captured_at().ToDebugString() |
515 | 0 | << " is no longer valid after history cleanup for hybrid_times before " |
516 | 0 | << cleanup_ht.ToDebugString() |
517 | 0 | << ", last operation hybrid_time: " << last_operation_ht() << "."; |
518 | 0 | RecordSnapshotDivergence(snapshot, cleanup_ht); |
519 | 0 | } |
520 | | |
521 | 0 | void DocDBLoadGenerator::VerifyRandomDocDbSnapshot() { |
522 | 0 | if (!docdb_snapshots_.empty()) { |
523 | 0 | const int snapshot_idx = NextRandomInt(narrow_cast<int>(docdb_snapshots_.size())); |
524 | 0 | ASSERT_NO_FATALS(VerifySnapshot(docdb_snapshots_[snapshot_idx])); |
525 | 0 | } |
526 | 0 | } |
527 | | |
528 | 0 | void DocDBLoadGenerator::RemoveSnapshotsBefore(HybridTime ht) { |
529 | 0 | docdb_snapshots_.erase( |
530 | 0 | std::remove_if(docdb_snapshots_.begin(), |
531 | 0 | docdb_snapshots_.end(), |
532 | 0 | [=](const InMemDocDbState& entry) { return entry.captured_at() < ht; }), |
533 | 0 | docdb_snapshots_.end()); |
534 | | // Double-check that there is no state corruption in any of the snapshots. Such corruption |
535 | | // happened when I (Mikhail) initially forgot to add the "erase" call above (as per the |
536 | | // "erase/remove" C++ idiom), and ended up with a bunch of moved-from objects still in the |
537 | | // snapshots array. |
538 | 0 | for (const auto& snapshot : docdb_snapshots_) { |
539 | 0 | snapshot.SanityCheck(); |
540 | 0 | } |
541 | 0 | } |
542 | | |
543 | 0 | const InMemDocDbState& DocDBLoadGenerator::GetOldestSnapshot() { |
544 | 0 | CHECK(!docdb_snapshots_.empty()); |
545 | 0 | return *std::min_element( |
546 | 0 | docdb_snapshots_.begin(), |
547 | 0 | docdb_snapshots_.end(), |
548 | 0 | [](const InMemDocDbState& a, const InMemDocDbState& b) { |
549 | 0 | return a.captured_at() < b.captured_at(); |
550 | 0 | }); |
551 | 0 | } |
552 | | |
553 | 0 | void DocDBLoadGenerator::VerifySnapshot(const InMemDocDbState& snapshot) { |
554 | 0 | const HybridTime snap_ht = snapshot.captured_at(); |
555 | 0 | InMemDocDbState flashback_state; |
556 | |
|
557 | 0 | string details_msg; |
558 | 0 | { |
559 | 0 | stringstream details_ss; |
560 | 0 | details_ss << "After operation at hybrid_time " << last_operation_ht().value() << ": " |
561 | 0 | << "performing a flashback query at hybrid_time " << snap_ht.ToDebugString() << " " |
562 | 0 | << "(last operation's hybrid_time: " << last_operation_ht() << ") " |
563 | 0 | << "and verifying it against the snapshot captured at that hybrid_time."; |
564 | 0 | details_msg = details_ss.str(); |
565 | 0 | } |
566 | 0 | LOG(INFO) << details_msg; |
567 | |
|
568 | 0 | flashback_state.CaptureAt(doc_db(), snap_ht); |
569 | 0 | const bool is_match = flashback_state.EqualsAndLogDiff(snapshot); |
570 | 0 | if (!is_match) { |
571 | 0 | LOG(ERROR) << details_msg << "\nDOCDB SNAPSHOT VERIFICATION FAILED, DOCDB STATE:"; |
572 | 0 | fixture_->DocDBDebugDumpToConsole(); |
573 | 0 | } |
574 | 0 | ASSERT_TRUE(is_match) << details_msg; |
575 | 0 | } |
576 | | |
577 | | void DocDBLoadGenerator::RecordSnapshotDivergence(const InMemDocDbState &snapshot, |
578 | 0 | const HybridTime cleanup_ht) { |
579 | 0 | InMemDocDbState flashback_state; |
580 | 0 | const auto snap_ht = snapshot.captured_at(); |
581 | 0 | flashback_state.CaptureAt(doc_db(), snap_ht); |
582 | 0 | if (!flashback_state.EqualsAndLogDiff(snapshot, /* log_diff = */ false)) { |
583 | | // Implicitly converting hybrid_times to ints. That's OK, because we're using small enough |
584 | | // integer values for hybrid_times. |
585 | 0 | divergent_snapshot_ht_and_cleanup_ht_.emplace_back(snapshot.captured_at().value(), |
586 | 0 | cleanup_ht.value()); |
587 | 0 | } |
588 | 0 | } |
589 | | |
590 | 0 | TransactionOperationContext DocDBLoadGenerator::GetReadOperationTransactionContext() { |
591 | 0 | if (resolve_intents_) { |
592 | 0 | return kNonTransactionalOperationContext; |
593 | 0 | } |
594 | 0 | return TransactionOperationContext(); |
595 | 0 | } |
596 | | |
597 | | // ------------------------------------------------------------------------------------------------ |
598 | | |
599 | 0 | void DocDBRocksDBFixture::AssertDocDbDebugDumpStrEq(const string &expected) { |
600 | 0 | const string debug_dump_str = TrimDocDbDebugDumpStr(DocDBDebugDumpToStr()); |
601 | 0 | const string expected_str = TrimDocDbDebugDumpStr(expected); |
602 | 0 | if (expected_str != debug_dump_str) { |
603 | 0 | auto expected_lines = StringSplit(expected_str, '\n'); |
604 | 0 | auto actual_lines = StringSplit(debug_dump_str, '\n'); |
605 | 0 | vector<size_t> mismatch_line_numbers; |
606 | 0 | for (size_t i = 0; i < std::min(expected_lines.size(), actual_lines.size()); ++i) { |
607 | 0 | if (expected_lines[i] != actual_lines[i]) { |
608 | 0 | mismatch_line_numbers.push_back(i + 1); |
609 | 0 | } |
610 | 0 | } |
611 | 0 | LOG(ERROR) << "Assertion failure" |
612 | 0 | << "\nExpected DocDB contents:\n\n" << expected_str << "\n" |
613 | 0 | << "\nActual DocDB contents:\n\n" << debug_dump_str << "\n" |
614 | 0 | << "\nExpected # of lines: " << expected_lines.size() |
615 | 0 | << ", actual # of lines: " << actual_lines.size() |
616 | 0 | << "\nLines not matching: " << AsString(mismatch_line_numbers) |
617 | 0 | << "\nPlease check if source files have trailing whitespace and remove it."; |
618 | |
|
619 | 0 | FAIL(); |
620 | 0 | } |
621 | 0 | } |
622 | | |
623 | 0 | void DocDBRocksDBFixture::FullyCompactHistoryBefore(HybridTime history_cutoff) { |
624 | 0 | LOG(INFO) << "Major-compacting history before hybrid_time " << history_cutoff; |
625 | 0 | SetHistoryCutoffHybridTime(history_cutoff); |
626 | 0 | auto se = ScopeExit([this] { |
627 | 0 | SetHistoryCutoffHybridTime(HybridTime::kMin); |
628 | 0 | }); |
629 | |
|
630 | 0 | ASSERT_OK(FlushRocksDbAndWait()); |
631 | 0 | ASSERT_OK(FullyCompactDB(regular_db_.get())); |
632 | 0 | } |
633 | | |
634 | | void DocDBRocksDBFixture::MinorCompaction( |
635 | | HybridTime history_cutoff, |
636 | | size_t num_files_to_compact, |
637 | 0 | ssize_t start_index) { |
638 | |
|
639 | 0 | ASSERT_OK(FlushRocksDbAndWait()); |
640 | 0 | SetHistoryCutoffHybridTime(history_cutoff); |
641 | 0 | auto se = ScopeExit([this] { |
642 | 0 | SetHistoryCutoffHybridTime(HybridTime::kMin); |
643 | 0 | }); |
644 | |
|
645 | 0 | rocksdb::ColumnFamilyMetaData cf_meta; |
646 | 0 | regular_db_->GetColumnFamilyMetaData(&cf_meta); |
647 | |
|
648 | 0 | vector<string> compaction_input_file_names; |
649 | 0 | vector<string> remaining_file_names; |
650 | |
|
651 | 0 | size_t initial_num_files = 0; |
652 | 0 | { |
653 | 0 | const auto& files = cf_meta.levels[0].files; |
654 | 0 | initial_num_files = files.size(); |
655 | 0 | ASSERT_LE(num_files_to_compact, files.size()); |
656 | 0 | vector<string> file_names; |
657 | 0 | for (const auto& sst_meta : files) { |
658 | 0 | file_names.push_back(sst_meta.name); |
659 | 0 | } |
660 | 0 | SortByKey(file_names.begin(), file_names.end(), rocksdb::TableFileNameToNumber); |
661 | |
|
662 | 0 | if (start_index < 0) { |
663 | 0 | start_index = file_names.size() - num_files_to_compact; |
664 | 0 | } |
665 | |
|
666 | 0 | for (size_t i = 0; i < file_names.size(); ++i) { |
667 | 0 | if (implicit_cast<size_t>(start_index) <= i && |
668 | 0 | compaction_input_file_names.size() < num_files_to_compact) { |
669 | 0 | compaction_input_file_names.push_back(file_names[i]); |
670 | 0 | } else { |
671 | 0 | remaining_file_names.push_back(file_names[i]); |
672 | 0 | } |
673 | 0 | } |
674 | 0 | ASSERT_EQ(num_files_to_compact, compaction_input_file_names.size()) |
675 | 0 | << "Tried to add " << num_files_to_compact << " files starting with index " << start_index |
676 | 0 | << ", ended up adding " << yb::ToString(compaction_input_file_names) |
677 | 0 | << " and leaving " << yb::ToString(remaining_file_names) << " out. All files: " |
678 | 0 | << yb::ToString(file_names); |
679 | | |
680 | 0 | LOG(INFO) << "Minor-compacting history before hybrid_time " << history_cutoff << ":\n" |
681 | 0 | << " files being compacted: " << yb::ToString(compaction_input_file_names) << "\n" |
682 | 0 | << " other files: " << yb::ToString(remaining_file_names); |
683 | |
|
684 | 0 | ASSERT_OK(regular_db_->CompactFiles( |
685 | 0 | rocksdb::CompactionOptions(), |
686 | 0 | compaction_input_file_names, |
687 | 0 | /* output_level */ 0)); |
688 | 0 | const auto sstables_after_compaction = SSTableFileNames(); |
689 | 0 | LOG(INFO) << "SSTable files after compaction: " << sstables_after_compaction.size() |
690 | 0 | << " (" << yb::ToString(sstables_after_compaction) << ")"; |
691 | 0 | for (const auto& remaining_file : remaining_file_names) { |
692 | 0 | ASSERT_TRUE( |
693 | 0 | std::find(sstables_after_compaction.begin(), sstables_after_compaction.end(), |
694 | 0 | remaining_file) != sstables_after_compaction.end() |
695 | 0 | ) << "File " << remaining_file << " not found in file list after compaction: " |
696 | 0 | << yb::ToString(sstables_after_compaction) << ", even though none of these files were " |
697 | 0 | << "supposed to be compacted: " << yb::ToString(remaining_file_names); |
698 | 0 | } |
699 | 0 | } |
700 | |
|
701 | 0 | regular_db_->GetColumnFamilyMetaData(&cf_meta); |
702 | 0 | vector<string> files_after_compaction; |
703 | 0 | for (const auto& sst_meta : cf_meta.levels[0].files) { |
704 | 0 | files_after_compaction.push_back(sst_meta.name); |
705 | 0 | } |
706 | 0 | const int64_t expected_resulting_num_files = initial_num_files - num_files_to_compact + 1; |
707 | 0 | ASSERT_EQ(expected_resulting_num_files, |
708 | 0 | static_cast<int64_t>(cf_meta.levels[0].files.size())) |
709 | 0 | << "Files after compaction: " << yb::ToString(files_after_compaction); |
710 | 0 | } |
711 | | |
712 | 0 | size_t DocDBRocksDBFixture::NumSSTableFiles() { |
713 | 0 | rocksdb::ColumnFamilyMetaData cf_meta; |
714 | 0 | regular_db_->GetColumnFamilyMetaData(&cf_meta); |
715 | 0 | return cf_meta.levels[0].files.size(); |
716 | 0 | } |
717 | | |
718 | 0 | StringVector DocDBRocksDBFixture::SSTableFileNames() { |
719 | 0 | rocksdb::ColumnFamilyMetaData cf_meta; |
720 | 0 | regular_db_->GetColumnFamilyMetaData(&cf_meta); |
721 | 0 | StringVector files; |
722 | 0 | for (const auto& sstable_meta : cf_meta.levels[0].files) { |
723 | 0 | files.push_back(sstable_meta.name); |
724 | 0 | } |
725 | 0 | SortByKey(files.begin(), files.end(), rocksdb::TableFileNameToNumber); |
726 | 0 | return files; |
727 | 0 | } |
728 | | |
729 | 0 | Status DocDBRocksDBFixture::FormatDocWriteBatch(const DocWriteBatch &dwb, string* dwb_str) { |
730 | 0 | WriteBatchFormatter formatter; |
731 | 0 | rocksdb::WriteBatch rocksdb_write_batch; |
732 | 0 | RETURN_NOT_OK(PopulateRocksDBWriteBatch(dwb, &rocksdb_write_batch)); |
733 | 0 | RETURN_NOT_OK(rocksdb_write_batch.Iterate(&formatter)); |
734 | 0 | *dwb_str = formatter.str(); |
735 | 0 | return Status::OK(); |
736 | 0 | } |
737 | | |
738 | 0 | Status FullyCompactDB(rocksdb::DB* rocksdb) { |
739 | 0 | rocksdb::CompactRangeOptions compact_range_options; |
740 | 0 | return rocksdb->CompactRange(compact_range_options, nullptr, nullptr); |
741 | 0 | } |
742 | | |
743 | 0 | Status DocDBRocksDBFixture::InitRocksDBDir() { |
744 | 0 | string test_dir; |
745 | 0 | RETURN_NOT_OK(Env::Default()->GetTestDirectory(&test_dir)); |
746 | 0 | rocksdb_dir_ = JoinPathSegments(test_dir, StringPrintf("mytestdb-%d", rand())); |
747 | 0 | CHECK(!rocksdb_dir_.empty()); // Check twice before we recursively delete anything. |
748 | 0 | CHECK_NE(rocksdb_dir_, "/"); |
749 | 0 | RETURN_NOT_OK(Env::Default()->DeleteRecursively(rocksdb_dir_)); |
750 | 0 | RETURN_NOT_OK(Env::Default()->DeleteRecursively(IntentsDBDir())); |
751 | 0 | return Status::OK(); |
752 | 0 | } |
753 | | |
754 | 0 | string DocDBRocksDBFixture::tablet_id() { |
755 | 0 | return "mytablet"; |
756 | 0 | } |
757 | | |
758 | 0 | Status DocDBRocksDBFixture::InitRocksDBOptions() { |
759 | 0 | RETURN_NOT_OK(InitCommonRocksDBOptionsForTests()); |
760 | 0 | return Status::OK(); |
761 | 0 | } |
762 | | |
763 | 0 | string TrimDocDbDebugDumpStr(const string& debug_dump_str) { |
764 | 0 | return TrimStr(ApplyEagerLineContinuation(LeftShiftTextBlock(TrimCppComments(debug_dump_str)))); |
765 | 0 | } |
766 | | |
767 | | } // namespace docdb |
768 | | } // namespace yb |