Coverage Report

Created: 2026-11-12 08:06

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/rocksdb/util/udt_util.cc
Line
Count
Source
1
//  Copyright (c) Meta Platforms, Inc. and affiliates.
2
//
3
//  This source code is licensed under both the GPLv2 (found in the
4
//  COPYING file in the root directory) and Apache 2.0 License
5
//  (found in the LICENSE.Apache file in the root directory).
6
7
#include "util/udt_util.h"
8
9
#include "db/dbformat.h"
10
#include "rocksdb/types.h"
11
#include "util/coding.h"
12
#include "util/write_batch_util.h"
13
14
namespace ROCKSDB_NAMESPACE {
15
namespace {
16
enum class RecoveryType {
17
  kNoop,
18
  kUnrecoverable,
19
  kStripTimestamp,
20
  kPadTimestamp,
21
};
22
23
RecoveryType GetRecoveryType(const size_t running_ts_sz,
24
825k
                             const std::optional<size_t>& recorded_ts_sz) {
25
825k
  if (running_ts_sz == 0) {
26
825k
    if (!recorded_ts_sz.has_value()) {
27
      // A column family id not recorded is equivalent to that column family has
28
      // zero timestamp size.
29
825k
      return RecoveryType::kNoop;
30
825k
    }
31
0
    return RecoveryType::kStripTimestamp;
32
825k
  }
33
34
825k
  assert(running_ts_sz != 0);
35
36
0
  if (!recorded_ts_sz.has_value()) {
37
0
    return RecoveryType::kPadTimestamp;
38
0
  }
39
40
0
  if (running_ts_sz != *recorded_ts_sz) {
41
0
    return RecoveryType::kUnrecoverable;
42
0
  }
43
44
0
  return RecoveryType::kNoop;
45
0
}
46
47
bool AllRunningColumnFamiliesConsistent(
48
    const UnorderedMap<uint32_t, size_t>& running_ts_sz,
49
809k
    const UnorderedMap<uint32_t, size_t>& record_ts_sz) {
50
825k
  for (const auto& [cf_id, ts_sz] : running_ts_sz) {
51
825k
    auto record_it = record_ts_sz.find(cf_id);
52
825k
    RecoveryType recovery_type =
53
825k
        GetRecoveryType(ts_sz, record_it != record_ts_sz.end()
54
825k
                                   ? std::optional<size_t>(record_it->second)
55
825k
                                   : std::nullopt);
56
825k
    if (recovery_type != RecoveryType::kNoop) {
57
0
      return false;
58
0
    }
59
825k
  }
60
809k
  return true;
61
809k
}
62
63
Status CheckWriteBatchTimestampSizeConsistency(
64
    const WriteBatch* batch,
65
    const UnorderedMap<uint32_t, size_t>& running_ts_sz,
66
    const UnorderedMap<uint32_t, size_t>& record_ts_sz,
67
0
    TimestampSizeConsistencyMode check_mode, bool* ts_need_recovery) {
68
0
  std::vector<uint32_t> column_family_ids;
69
0
  Status status =
70
0
      CollectColumnFamilyIdsFromWriteBatch(*batch, &column_family_ids);
71
0
  if (!status.ok()) {
72
0
    return status;
73
0
  }
74
0
  for (const auto& cf_id : column_family_ids) {
75
0
    auto running_iter = running_ts_sz.find(cf_id);
76
0
    if (running_iter == running_ts_sz.end()) {
77
      // Ignore dropped column family referred to in a WriteBatch regardless of
78
      // its consistency.
79
0
      continue;
80
0
    }
81
0
    auto record_iter = record_ts_sz.find(cf_id);
82
0
    RecoveryType recovery_type = GetRecoveryType(
83
0
        running_iter->second, record_iter != record_ts_sz.end()
84
0
                                  ? std::optional<size_t>(record_iter->second)
85
0
                                  : std::nullopt);
86
0
    if (recovery_type != RecoveryType::kNoop) {
87
0
      if (check_mode == TimestampSizeConsistencyMode::kVerifyConsistency) {
88
0
        return Status::InvalidArgument(
89
0
            "WriteBatch contains timestamp size inconsistency.");
90
0
      }
91
92
0
      if (recovery_type == RecoveryType::kUnrecoverable) {
93
0
        return Status::InvalidArgument(
94
0
            "WriteBatch contains unrecoverable timestamp size inconsistency.");
95
0
      }
96
97
      // If any column family needs reconciliation, it will mark the whole
98
      // WriteBatch to need recovery and rebuilt.
99
0
      *ts_need_recovery = true;
100
0
    }
101
0
  }
102
0
  return Status::OK();
103
0
}
104
105
enum class ToggleUDT {
106
  kUnchanged,
107
  kEnableUDT,
108
  kDisableUDT,
109
  kInvalidChange,
110
};
111
112
ToggleUDT CompareComparator(const Comparator* new_comparator,
113
63.7k
                            const std::string& old_comparator_name) {
114
63.7k
  static const char* kUDTSuffix = ".u64ts";
115
63.7k
  static const Slice kSuffixSlice = kUDTSuffix;
116
63.7k
  static const size_t kSuffixSize = 6;
117
63.7k
  size_t ts_sz = new_comparator->timestamp_size();
118
63.7k
  (void)ts_sz;
119
63.7k
  Slice new_ucmp_name(new_comparator->Name());
120
63.7k
  Slice old_ucmp_name(old_comparator_name);
121
63.7k
  if (new_ucmp_name.compare(old_ucmp_name) == 0) {
122
63.7k
    return ToggleUDT::kUnchanged;
123
63.7k
  }
124
0
  if (new_ucmp_name.size() == old_ucmp_name.size() + kSuffixSize &&
125
0
      new_ucmp_name.starts_with(old_ucmp_name) &&
126
0
      new_ucmp_name.ends_with(kSuffixSlice)) {
127
0
    assert(ts_sz == 8);
128
0
    return ToggleUDT::kEnableUDT;
129
0
  }
130
0
  if (old_ucmp_name.size() == new_ucmp_name.size() + kSuffixSize &&
131
0
      old_ucmp_name.starts_with(new_ucmp_name) &&
132
0
      old_ucmp_name.ends_with(kSuffixSlice)) {
133
0
    assert(ts_sz == 0);
134
0
    return ToggleUDT::kDisableUDT;
135
0
  }
136
0
  return ToggleUDT::kInvalidChange;
137
0
}
138
}  // namespace
139
140
TimestampRecoveryHandler::TimestampRecoveryHandler(
141
    const UnorderedMap<uint32_t, size_t>& running_ts_sz,
142
    const UnorderedMap<uint32_t, size_t>& record_ts_sz, bool seq_per_batch,
143
    bool batch_per_txn)
144
0
    : running_ts_sz_(running_ts_sz),
145
0
      record_ts_sz_(record_ts_sz),
146
      // Write after commit currently uses one seq per key (instead of per
147
      // batch). So seq_per_batch being false indicates write_after_commit
148
      // approach.
149
0
      write_after_commit_(!seq_per_batch),
150
      // WriteUnprepared can write multiple WriteBatches per transaction, so
151
      // batch_per_txn being false indicates write_before_prepare.
152
0
      write_before_prepare_(!batch_per_txn),
153
0
      new_batch_(new WriteBatch()),
154
0
      handler_valid_(true),
155
0
      new_batch_diff_from_orig_batch_(false) {}
156
157
Status TimestampRecoveryHandler::PutCF(uint32_t cf, const Slice& key,
158
0
                                       const Slice& value) {
159
0
  std::string new_key_buf;
160
0
  Slice new_key;
161
0
  Status status =
162
0
      ReconcileTimestampDiscrepancy(cf, key, &new_key_buf, &new_key);
163
0
  if (!status.ok()) {
164
0
    return status;
165
0
  }
166
0
  return WriteBatchInternal::Put(new_batch_.get(), cf, new_key, value);
167
0
}
168
169
Status TimestampRecoveryHandler::PutEntityCF(uint32_t cf, const Slice& key,
170
0
                                             const Slice& entity) {
171
0
  std::string new_key_buf;
172
0
  Slice new_key;
173
0
  Status status = TimestampRecoveryHandler::ReconcileTimestampDiscrepancy(
174
0
      cf, key, &new_key_buf, &new_key);
175
0
  if (!status.ok()) {
176
0
    return status;
177
0
  }
178
0
  Slice entity_copy = entity;
179
0
  WideColumns columns;
180
0
  if (!WideColumnSerialization::DeserializeSimple(entity_copy, columns).ok()) {
181
0
    return Status::Corruption("Unable to deserialize entity",
182
0
                              entity.ToString(/* hex */ true));
183
0
  }
184
185
0
  return WriteBatchInternal::PutEntity(new_batch_.get(), cf, new_key, columns);
186
0
}
187
188
Status TimestampRecoveryHandler::TimedPutCF(uint32_t cf, const Slice& key,
189
                                            const Slice& value,
190
0
                                            uint64_t write_time) {
191
0
  std::string new_key_buf;
192
0
  Slice new_key;
193
0
  Status status =
194
0
      ReconcileTimestampDiscrepancy(cf, key, &new_key_buf, &new_key);
195
0
  if (!status.ok()) {
196
0
    return status;
197
0
  }
198
0
  return WriteBatchInternal::TimedPut(new_batch_.get(), cf, new_key, value,
199
0
                                      write_time);
200
0
}
201
202
0
Status TimestampRecoveryHandler::DeleteCF(uint32_t cf, const Slice& key) {
203
0
  std::string new_key_buf;
204
0
  Slice new_key;
205
0
  Status status =
206
0
      ReconcileTimestampDiscrepancy(cf, key, &new_key_buf, &new_key);
207
0
  if (!status.ok()) {
208
0
    return status;
209
0
  }
210
0
  return WriteBatchInternal::Delete(new_batch_.get(), cf, new_key);
211
0
}
212
213
0
Status TimestampRecoveryHandler::SingleDeleteCF(uint32_t cf, const Slice& key) {
214
0
  std::string new_key_buf;
215
0
  Slice new_key;
216
0
  Status status =
217
0
      ReconcileTimestampDiscrepancy(cf, key, &new_key_buf, &new_key);
218
0
  if (!status.ok()) {
219
0
    return status;
220
0
  }
221
0
  return WriteBatchInternal::SingleDelete(new_batch_.get(), cf, new_key);
222
0
}
223
224
Status TimestampRecoveryHandler::DeleteRangeCF(uint32_t cf,
225
                                               const Slice& begin_key,
226
0
                                               const Slice& end_key) {
227
0
  std::string new_begin_key_buf;
228
0
  Slice new_begin_key;
229
0
  std::string new_end_key_buf;
230
0
  Slice new_end_key;
231
0
  Status status = ReconcileTimestampDiscrepancy(
232
0
      cf, begin_key, &new_begin_key_buf, &new_begin_key);
233
0
  if (!status.ok()) {
234
0
    return status;
235
0
  }
236
0
  status = ReconcileTimestampDiscrepancy(cf, end_key, &new_end_key_buf,
237
0
                                         &new_end_key);
238
0
  if (!status.ok()) {
239
0
    return status;
240
0
  }
241
0
  return WriteBatchInternal::DeleteRange(new_batch_.get(), cf, new_begin_key,
242
0
                                         new_end_key);
243
0
}
244
245
Status TimestampRecoveryHandler::MergeCF(uint32_t cf, const Slice& key,
246
0
                                         const Slice& value) {
247
0
  std::string new_key_buf;
248
0
  Slice new_key;
249
0
  Status status =
250
0
      ReconcileTimestampDiscrepancy(cf, key, &new_key_buf, &new_key);
251
0
  if (!status.ok()) {
252
0
    return status;
253
0
  }
254
0
  return WriteBatchInternal::Merge(new_batch_.get(), cf, new_key, value);
255
0
}
256
257
Status TimestampRecoveryHandler::PutBlobIndexCF(uint32_t cf, const Slice& key,
258
0
                                                const Slice& value) {
259
0
  std::string new_key_buf;
260
0
  Slice new_key;
261
0
  Status status =
262
0
      ReconcileTimestampDiscrepancy(cf, key, &new_key_buf, &new_key);
263
0
  if (!status.ok()) {
264
0
    return status;
265
0
  }
266
0
  return WriteBatchInternal::PutBlobIndex(new_batch_.get(), cf, new_key, value);
267
0
}
268
269
0
Status TimestampRecoveryHandler::MarkBeginPrepare(bool unprepare) {
270
  // Transaction policy change requires empty WAL and User-defined timestamp is
271
  // only supported for write committed txns.
272
  // WriteBatch::Iterate has will handle this based on
273
  // handler->WriteAfterCommit() and handler->WriteBeforePrepare().
274
0
  if (unprepare) {
275
0
    return Status::InvalidArgument(
276
0
        "Handle user defined timestamp setting change is not supported for"
277
0
        "write unprepared policy. The WAL must be emptied.");
278
0
  }
279
0
  return WriteBatchInternal::InsertBeginPrepare(new_batch_.get(),
280
0
                                                write_after_commit_,
281
0
                                                /* unprepared_batch */ false);
282
0
}
283
284
0
Status TimestampRecoveryHandler::MarkEndPrepare(const Slice& name) {
285
0
  return WriteBatchInternal::InsertEndPrepare(new_batch_.get(), name);
286
0
}
287
288
0
Status TimestampRecoveryHandler::MarkCommit(const Slice& name) {
289
0
  return WriteBatchInternal::MarkCommit(new_batch_.get(), name);
290
0
}
291
292
Status TimestampRecoveryHandler::MarkCommitWithTimestamp(
293
0
    const Slice& name, const Slice& commit_ts) {
294
0
  return WriteBatchInternal::MarkCommitWithTimestamp(new_batch_.get(), name,
295
0
                                                     commit_ts);
296
0
}
297
298
0
Status TimestampRecoveryHandler::MarkRollback(const Slice& name) {
299
0
  return WriteBatchInternal::MarkRollback(new_batch_.get(), name);
300
0
}
301
302
0
Status TimestampRecoveryHandler::MarkNoop(bool /*empty_batch*/) {
303
0
  return WriteBatchInternal::InsertNoop(new_batch_.get());
304
0
}
305
306
Status TimestampRecoveryHandler::ReconcileTimestampDiscrepancy(
307
0
    uint32_t cf, const Slice& key, std::string* new_key_buf, Slice* new_key) {
308
0
  assert(handler_valid_);
309
0
  auto running_iter = running_ts_sz_.find(cf);
310
0
  if (running_iter == running_ts_sz_.end()) {
311
    // The column family referred to by the WriteBatch is no longer running.
312
    // Copy over the entry as is to the new WriteBatch.
313
0
    *new_key = key;
314
0
    return Status::OK();
315
0
  }
316
0
  size_t running_ts_sz = running_iter->second;
317
0
  auto record_iter = record_ts_sz_.find(cf);
318
0
  std::optional<size_t> record_ts_sz =
319
0
      record_iter != record_ts_sz_.end()
320
0
          ? std::optional<size_t>(record_iter->second)
321
0
          : std::nullopt;
322
0
  RecoveryType recovery_type = GetRecoveryType(running_ts_sz, record_ts_sz);
323
324
0
  switch (recovery_type) {
325
0
    case RecoveryType::kNoop:
326
0
      *new_key = key;
327
0
      break;
328
0
    case RecoveryType::kStripTimestamp:
329
0
      assert(record_ts_sz.has_value());
330
0
      *new_key = StripTimestampFromUserKey(key, *record_ts_sz);
331
0
      new_batch_diff_from_orig_batch_ = true;
332
0
      break;
333
0
    case RecoveryType::kPadTimestamp:
334
0
      AppendKeyWithMinTimestamp(new_key_buf, key, running_ts_sz);
335
0
      *new_key = *new_key_buf;
336
0
      new_batch_diff_from_orig_batch_ = true;
337
0
      break;
338
0
    case RecoveryType::kUnrecoverable:
339
0
      return Status::InvalidArgument(
340
0
          "Unrecoverable timestamp size inconsistency encountered by "
341
0
          "TimestampRecoveryHandler.");
342
0
    default:
343
0
      assert(false);
344
0
  }
345
0
  return Status::OK();
346
0
}
347
348
Status HandleWriteBatchTimestampSizeDifference(
349
    const WriteBatch* batch,
350
    const UnorderedMap<uint32_t, size_t>& running_ts_sz,
351
    const UnorderedMap<uint32_t, size_t>& record_ts_sz,
352
    TimestampSizeConsistencyMode check_mode, bool seq_per_batch,
353
809k
    bool batch_per_txn, std::unique_ptr<WriteBatch>* new_batch) {
354
  // Quick path to bypass checking the WriteBatch.
355
809k
  if (AllRunningColumnFamiliesConsistent(running_ts_sz, record_ts_sz)) {
356
809k
    return Status::OK();
357
809k
  }
358
0
  bool need_recovery = false;
359
0
  Status status = CheckWriteBatchTimestampSizeConsistency(
360
0
      batch, running_ts_sz, record_ts_sz, check_mode, &need_recovery);
361
0
  if (!status.ok()) {
362
0
    return status;
363
0
  } else if (need_recovery) {
364
0
    assert(new_batch);
365
0
    SequenceNumber sequence = WriteBatchInternal::Sequence(batch);
366
0
    TimestampRecoveryHandler recovery_handler(running_ts_sz, record_ts_sz,
367
0
                                              seq_per_batch, batch_per_txn);
368
0
    status = batch->Iterate(&recovery_handler);
369
0
    if (!status.ok()) {
370
0
      return status;
371
0
    } else {
372
0
      *new_batch = recovery_handler.TransferNewBatch();
373
0
      WriteBatchInternal::SetSequence(new_batch->get(), sequence);
374
0
    }
375
0
  }
376
0
  return Status::OK();
377
0
}
378
379
Status ValidateUserDefinedTimestampsOptions(
380
    const Comparator* new_comparator, const std::string& old_comparator_name,
381
    bool new_persist_udt, bool old_persist_udt,
382
63.7k
    bool* mark_sst_files_has_no_udt) {
383
63.7k
  size_t ts_sz = new_comparator->timestamp_size();
384
63.7k
  ToggleUDT res = CompareComparator(new_comparator, old_comparator_name);
385
63.7k
  switch (res) {
386
63.7k
    case ToggleUDT::kUnchanged:
387
63.7k
      if (old_persist_udt == new_persist_udt) {
388
63.7k
        return Status::OK();
389
63.7k
      }
390
0
      if (ts_sz == 0) {
391
0
        return Status::OK();
392
0
      }
393
0
      return Status::InvalidArgument(
394
0
          "Cannot toggle the persist_user_defined_timestamps flag for a column "
395
0
          "family with user-defined timestamps feature enabled.");
396
0
    case ToggleUDT::kEnableUDT:
397
0
      if (!new_persist_udt) {
398
0
        *mark_sst_files_has_no_udt = true;
399
0
        return Status::OK();
400
0
      }
401
0
      return Status::InvalidArgument(
402
0
          "Cannot open a column family and enable user-defined timestamps "
403
0
          "feature without setting persist_user_defined_timestamps flag to "
404
0
          "false.");
405
0
    case ToggleUDT::kDisableUDT:
406
0
      if (!old_persist_udt) {
407
0
        return Status::OK();
408
0
      }
409
0
      return Status::InvalidArgument(
410
0
          "Cannot open a column family and disable user-defined timestamps "
411
0
          "feature if its existing persist_user_defined_timestamps flag is not "
412
0
          "false.");
413
0
    case ToggleUDT::kInvalidChange:
414
0
      return Status::InvalidArgument(
415
0
          new_comparator->Name(),
416
0
          "does not match existing comparator " + old_comparator_name);
417
0
    default:
418
0
      break;
419
63.7k
  }
420
0
  return Status::InvalidArgument(
421
0
      "Unsupported user defined timestamps settings change.");
422
63.7k
}
423
424
void GetFullHistoryTsLowFromU64CutoffTs(Slice* cutoff_ts,
425
0
                                        std::string* full_history_ts_low) {
426
0
  uint64_t cutoff_udt_ts = 0;
427
0
  [[maybe_unused]] bool format_res = GetFixed64(cutoff_ts, &cutoff_udt_ts);
428
0
  assert(format_res);
429
0
  PutFixed64(full_history_ts_low, cutoff_udt_ts + 1);
430
0
}
431
432
void GetU64CutoffTsFromFullHistoryTsLow(Slice* full_history_ts_low,
433
0
                                        std::string* cutoff_ts) {
434
0
  uint64_t full_history_ts_low_int = 0;
435
0
  [[maybe_unused]] bool format_res =
436
0
      GetFixed64(full_history_ts_low, &full_history_ts_low_int);
437
0
  assert(format_res);
438
0
  assert(full_history_ts_low_int > 0);
439
0
  if (full_history_ts_low_int > 0) {
440
0
    PutFixed64(cutoff_ts, full_history_ts_low_int - 1);
441
0
  } else {
442
0
    PutFixed64(cutoff_ts, 0);
443
0
  }
444
0
}
445
446
std::tuple<OptSlice, OptSlice> MaybeAddTimestampsToRange(
447
    const OptSlice& start, const OptSlice& end, size_t ts_sz,
448
0
    std::string* start_with_ts, std::string* end_with_ts, bool exclusive_end) {
449
0
  OptSlice ret_start;
450
0
  OptSlice ret_end;
451
0
  if (start) {
452
0
    if (ts_sz == 0) {
453
0
      ret_start = *start;
454
0
    } else {
455
      // Maximum timestamp means including all keys with any timestamp for start
456
0
      AppendKeyWithMaxTimestamp(start_with_ts, *start, ts_sz);
457
0
      ret_start = Slice(*start_with_ts);
458
0
    }
459
0
  }
460
0
  if (end) {
461
0
    if (ts_sz == 0) {
462
0
      ret_end = *end;
463
0
    } else {
464
0
      if (exclusive_end) {
465
        // Append a maximum timestamp as the range limit is exclusive:
466
        // [start, end)
467
0
        AppendKeyWithMaxTimestamp(end_with_ts, *end, ts_sz);
468
0
      } else {
469
        // Append a minimum timestamp to end so the range limit is inclusive:
470
        // [start, end]
471
0
        AppendKeyWithMinTimestamp(end_with_ts, *end, ts_sz);
472
0
      }
473
0
      ret_end = Slice(*end_with_ts);
474
0
    }
475
0
  }
476
0
  return std::make_tuple(ret_start, ret_end);
477
0
}
478
}  // namespace ROCKSDB_NAMESPACE