]> git.proxmox.com Git - ceph.git/blobdiff - ceph/src/rocksdb/utilities/ttl/ttl_test.cc
update ceph source to reef 18.1.2
[ceph.git] / ceph / src / rocksdb / utilities / ttl / ttl_test.cc
index 015f67cc4d82a25192ac2b8b53414f3a50c5952a..a42e0acb4ea1855482e6dfd1ca519d931d7ba13c 100644 (file)
@@ -7,10 +7,16 @@
 
 #include <map>
 #include <memory>
+
 #include "rocksdb/compaction_filter.h"
+#include "rocksdb/convenience.h"
+#include "rocksdb/merge_operator.h"
 #include "rocksdb/utilities/db_ttl.h"
+#include "rocksdb/utilities/object_registry.h"
 #include "test_util/testharness.h"
 #include "util/string_util.h"
+#include "utilities/merge_operators/bytesxor.h"
+#include "utilities/ttl/db_ttl_impl.h"
 #ifndef OS_WIN
 #include <unistd.h>
 #endif
@@ -19,17 +25,17 @@ namespace ROCKSDB_NAMESPACE {
 
 namespace {
 
-typedef std::map<std::string, std::string> KVMap;
+using KVMap = std::map<std::string, std::string>;
 
 enum BatchOperation { OP_PUT = 0, OP_DELETE = 1 };
-}
+}  // namespace
 
 class SpecialTimeEnv : public EnvWrapper {
  public:
   explicit SpecialTimeEnv(Env* base) : EnvWrapper(base) {
-    base->GetCurrentTime(&current_time_);
+    EXPECT_OK(base->GetCurrentTime(&current_time_));
   }
-
+  const char* Name() const override { return "SpecialTimeEnv"; }
   void Sleep(int64_t sleep_time) { current_time_ += sleep_time; }
   Status GetCurrentTime(int64_t* current_time) override {
     *current_time = current_time_;
@@ -51,12 +57,12 @@ class TtlTest : public testing::Test {
     options_.max_compaction_bytes = 1;
     // compaction should take place always from level0 for determinism
     db_ttl_ = nullptr;
-    DestroyDB(dbname_, Options());
+    EXPECT_OK(DestroyDB(dbname_, Options()));
   }
 
   ~TtlTest() override {
     CloseTtl();
-    DestroyDB(dbname_, Options());
+    EXPECT_OK(DestroyDB(dbname_, Options()));
   }
 
   // Open database with TTL support when TTL not provided with db_ttl_ pointer
@@ -75,8 +81,8 @@ class TtlTest : public testing::Test {
   // Open with TestFilter compaction filter
   void OpenTtlWithTestCompaction(int32_t ttl) {
     options_.compaction_filter_factory =
-      std::shared_ptr<CompactionFilterFactory>(
-          new TestFilterFactory(kSampleSize_, kNewValue_));
+        std::shared_ptr<CompactionFilterFactory>(
+            new TestFilterFactory(kSampleSize_, kNewValue_));
     OpenTtl(ttl);
   }
 
@@ -95,7 +101,7 @@ class TtlTest : public testing::Test {
   void CloseTtlHelper(bool close_db) {
     if (db_ttl_ != nullptr) {
       if (close_db) {
-        db_ttl_->Close();
+        EXPECT_OK(db_ttl_->Close());
       }
       delete db_ttl_;
       db_ttl_ = nullptr;
@@ -115,7 +121,7 @@ class TtlTest : public testing::Test {
       if (i % 10 == 0) {
         digits_in_i++;
       }
-      for(int j = digits_in_i; j < digits; j++) {
+      for (int j = digits_in_i; j < digits; j++) {
         key.append("0");
         value.append("0");
       }
@@ -137,17 +143,17 @@ class TtlTest : public testing::Test {
     for (int64_t i = 0; i < num_ops && kv_it_ != kvmap_.end(); i++, ++kv_it_) {
       switch (batch_ops[i]) {
         case OP_PUT:
-          batch.Put(kv_it_->first, kv_it_->second);
+          ASSERT_OK(batch.Put(kv_it_->first, kv_it_->second));
           break;
         case OP_DELETE:
-          batch.Delete(kv_it_->first);
+          ASSERT_OK(batch.Delete(kv_it_->first));
           break;
         default:
           FAIL();
       }
     }
-    db_ttl_->Write(wopts, &batch);
-    db_ttl_->Flush(flush_opts);
+    ASSERT_OK(db_ttl_->Write(wopts, &batch));
+    ASSERT_OK(db_ttl_->Flush(flush_opts));
   }
 
   // Puts num_entries starting from start_pos_map from kvmap_ into the database
@@ -170,37 +176,53 @@ class TtlTest : public testing::Test {
                             : db_ttl_->Put(wopts, cf, "keymock", "valuemock"));
     if (flush) {
       if (cf == nullptr) {
-        db_ttl_->Flush(flush_opts);
+        ASSERT_OK(db_ttl_->Flush(flush_opts));
       } else {
-        db_ttl_->Flush(flush_opts, cf);
+        ASSERT_OK(db_ttl_->Flush(flush_opts, cf));
       }
     }
   }
 
   // Runs a manual compaction
-  void ManualCompact(ColumnFamilyHandle* cf = nullptr) {
+  Status ManualCompact(ColumnFamilyHandle* cf = nullptr) {
+    assert(db_ttl_);
     if (cf == nullptr) {
-      db_ttl_->CompactRange(CompactRangeOptions(), nullptr, nullptr);
+      return db_ttl_->CompactRange(CompactRangeOptions(), nullptr, nullptr);
     } else {
-      db_ttl_->CompactRange(CompactRangeOptions(), cf, nullptr, nullptr);
+      return db_ttl_->CompactRange(CompactRangeOptions(), cf, nullptr, nullptr);
     }
   }
 
+  // Runs a DeleteRange
+  void MakeDeleteRange(std::string start, std::string end,
+                       ColumnFamilyHandle* cf = nullptr) {
+    ASSERT_TRUE(db_ttl_);
+    static WriteOptions wops;
+    WriteBatch wb;
+    ASSERT_OK(cf == nullptr
+                  ? wb.DeleteRange(db_ttl_->DefaultColumnFamily(), start, end)
+                  : wb.DeleteRange(cf, start, end));
+    ASSERT_OK(db_ttl_->Write(wops, &wb));
+  }
+
   // checks the whole kvmap_ to return correct values using KeyMayExist
   void SimpleKeyMayExistCheck() {
     static ReadOptions ropts;
     bool value_found;
     std::string val;
-    for(auto &kv : kvmap_) {
+    for (auto& kv : kvmap_) {
       bool ret = db_ttl_->KeyMayExist(ropts, kv.first, &val, &value_found);
       if (ret == false || value_found == false) {
-        fprintf(stderr, "KeyMayExist could not find key=%s in the database but"
-                        " should have\n", kv.first.c_str());
+        fprintf(stderr,
+                "KeyMayExist could not find key=%s in the database but"
+                " should have\n",
+                kv.first.c_str());
         FAIL();
       } else if (val.compare(kv.second) != 0) {
-        fprintf(stderr, " value for key=%s present in database is %s but"
-                        " should be %s\n", kv.first.c_str(), val.c_str(),
-                        kv.second.c_str());
+        fprintf(stderr,
+                " value for key=%s present in database is %s but"
+                " should be %s\n",
+                kv.first.c_str(), val.c_str(), kv.second.c_str());
         FAIL();
       }
     }
@@ -225,18 +247,9 @@ class TtlTest : public testing::Test {
     }
   }
 
-  // Sleeps for slp_tim then runs a manual compaction
-  // Checks span starting from st_pos from kvmap_ in the db and
-  // Gets should return true if check is true and false otherwise
-  // Also checks that value that we got is the same as inserted; and =kNewValue
-  //   if test_compaction_change is true
-  void SleepCompactCheck(int slp_tim, int64_t st_pos, int64_t span,
-                         bool check = true, bool test_compaction_change = false,
-                         ColumnFamilyHandle* cf = nullptr) {
-    ASSERT_TRUE(db_ttl_);
-
-    env_->Sleep(slp_tim);
-    ManualCompact(cf);
+  void CompactCheck(int64_t st_pos, int64_t span, bool check = true,
+                    bool test_compaction_change = false,
+                    ColumnFamilyHandle* cf = nullptr) {
     static ReadOptions ropts;
     kv_it_ = kvmap_.begin();
     advance(kv_it_, st_pos);
@@ -253,29 +266,45 @@ class TtlTest : public testing::Test {
         }
         FAIL();
       } else if (s.ok()) {
-          if (test_compaction_change && v.compare(kNewValue_) != 0) {
-            fprintf(stderr, " value for key=%s present in database is %s but "
-                            " should be %s\n", kv_it_->first.c_str(), v.c_str(),
-                            kNewValue_.c_str());
-            FAIL();
-          } else if (!test_compaction_change && v.compare(kv_it_->second) !=0) {
-            fprintf(stderr, " value for key=%s present in database is %s but "
-                            " should be %s\n", kv_it_->first.c_str(), v.c_str(),
-                            kv_it_->second.c_str());
-            FAIL();
-          }
+        if (test_compaction_change && v.compare(kNewValue_) != 0) {
+          fprintf(stderr,
+                  " value for key=%s present in database is %s but "
+                  " should be %s\n",
+                  kv_it_->first.c_str(), v.c_str(), kNewValue_.c_str());
+          FAIL();
+        } else if (!test_compaction_change && v.compare(kv_it_->second) != 0) {
+          fprintf(stderr,
+                  " value for key=%s present in database is %s but "
+                  " should be %s\n",
+                  kv_it_->first.c_str(), v.c_str(), kv_it_->second.c_str());
+          FAIL();
+        }
       }
     }
   }
+  // Sleeps for slp_tim then runs a manual compaction
+  // Checks span starting from st_pos from kvmap_ in the db and
+  // Gets should return true if check is true and false otherwise
+  // Also checks that value that we got is the same as inserted; and =kNewValue
+  //   if test_compaction_change is true
+  void SleepCompactCheck(int slp_tim, int64_t st_pos, int64_t span,
+                         bool check = true, bool test_compaction_change = false,
+                         ColumnFamilyHandle* cf = nullptr) {
+    ASSERT_TRUE(db_ttl_);
+
+    env_->Sleep(slp_tim);
+    ASSERT_OK(ManualCompact(cf));
+    CompactCheck(st_pos, span, check, test_compaction_change, cf);
+  }
 
   // Similar as SleepCompactCheck but uses TtlIterator to read from db
   void SleepCompactCheckIter(int slp, int st_pos, int64_t span,
                              bool check = true) {
     ASSERT_TRUE(db_ttl_);
     env_->Sleep(slp);
-    ManualCompact();
+    ASSERT_OK(ManualCompact());
     static ReadOptions ropts;
-    Iterator *dbiter = db_ttl_->NewIterator(ropts);
+    Iteratordbiter = db_ttl_->NewIterator(ropts);
     kv_it_ = kvmap_.begin();
     advance(kv_it_, st_pos);
 
@@ -292,6 +321,7 @@ class TtlTest : public testing::Test {
         dbiter->Next();
       }
     }
+    ASSERT_OK(dbiter->status());
     delete dbiter;
   }
 
@@ -304,9 +334,7 @@ class TtlTest : public testing::Test {
   class TestFilter : public CompactionFilter {
    public:
     TestFilter(const int64_t kSampleSize, const std::string& kNewValue)
-      : kSampleSize_(kSampleSize),
-        kNewValue_(kNewValue) {
-    }
+        : kSampleSize_(kSampleSize), kNewValue_(kNewValue) {}
 
     // Works on keys of the form "key<number>"
     // Drops key if number at the end of key is in [0, kSampleSize_/3),
@@ -330,7 +358,7 @@ class TtlTest : public testing::Test {
 #endif
 
       } else {
-        return false; // Keep keys not matching the format "key<NUMBER>"
+        return false;  // Keep keys not matching the format "key<NUMBER>"
       }
 
       int64_t partition = kSampleSize_ / 3;
@@ -353,26 +381,23 @@ class TtlTest : public testing::Test {
   };
 
   class TestFilterFactory : public CompactionFilterFactory {
-    public:
-      TestFilterFactory(const int64_t kSampleSize, const std::string& kNewValue)
-        : kSampleSize_(kSampleSize),
-          kNewValue_(kNewValue) {
-      }
+   public:
+    TestFilterFactory(const int64_t kSampleSize, const std::string& kNewValue)
+        : kSampleSize_(kSampleSize), kNewValue_(kNewValue) {}
 
-      std::unique_ptr<CompactionFilter> CreateCompactionFilter(
-          const CompactionFilter::Context& /*context*/) override {
-        return std::unique_ptr<CompactionFilter>(
-            new TestFilter(kSampleSize_, kNewValue_));
-      }
+    std::unique_ptr<CompactionFilter> CreateCompactionFilter(
+        const CompactionFilter::Context& /*context*/) override {
+      return std::unique_ptr<CompactionFilter>(
+          new TestFilter(kSampleSize_, kNewValue_));
+    }
 
-      const char* Name() const override { return "TestFilterFactory"; }
+    const char* Name() const override { return "TestFilterFactory"; }
 
-     private:
-      const int64_t kSampleSize_;
-      const std::string kNewValue_;
+   private:
+    const int64_t kSampleSize_;
+    const std::string kNewValue_;
   };
 
-
   // Choose carefully so that Put, Gets & Compaction complete in 1 second buffer
   static const int64_t kSampleSize_ = 100;
   std::string dbname_;
@@ -385,7 +410,7 @@ class TtlTest : public testing::Test {
   KVMap::iterator kv_it_;
   const std::string kNewValue_ = "new_value";
   std::unique_ptr<CompactionFilter> test_comp_filter_;
-}; // class TtlTest
+};  // class TtlTest
 
 // If TTL is non positive or not provided, the behaviour is TTL = infinity
 // This test opens the db 3 times with such default behavior and inserts a
@@ -397,18 +422,18 @@ TEST_F(TtlTest, NoEffect) {
   int64_t boundary2 = 2 * boundary1;
 
   OpenTtl();
-  PutValues(0, boundary1);                       //T=0: Set1 never deleted
-  SleepCompactCheck(1, 0, boundary1);            //T=1: Set1 still there
+  PutValues(0, boundary1);             // T=0: Set1 never deleted
+  SleepCompactCheck(1, 0, boundary1);  // T=1: Set1 still there
   CloseTtl();
 
   OpenTtl(0);
-  PutValues(boundary1, boundary2 - boundary1);   //T=1: Set2 never deleted
-  SleepCompactCheck(1, 0, boundary2);            //T=2: Sets1 & 2 still there
+  PutValues(boundary1, boundary2 - boundary1);  // T=1: Set2 never deleted
+  SleepCompactCheck(1, 0, boundary2);           // T=2: Sets1 & 2 still there
   CloseTtl();
 
   OpenTtl(-1);
-  PutValues(boundary2, kSampleSize_ - boundary2); //T=3: Set3 never deleted
-  SleepCompactCheck(1, 0, kSampleSize_, true);    //T=4: Sets 1,2,3 still there
+  PutValues(boundary2, kSampleSize_ - boundary2);  // T=3: Set3 never deleted
+  SleepCompactCheck(1, 0, kSampleSize_, true);  // T=4: Sets 1,2,3 still there
   CloseTtl();
 }
 
@@ -439,9 +464,10 @@ TEST_F(TtlTest, DestructWithoutClose) {
 TEST_F(TtlTest, PresentDuringTTL) {
   MakeKVMap(kSampleSize_);
 
-  OpenTtl(2);                                 // T=0:Open the db with ttl = 2
-  PutValues(0, kSampleSize_);                  // T=0:Insert Set1. Delete at t=2
-  SleepCompactCheck(1, 0, kSampleSize_, true); // T=1:Set1 should still be there
+  OpenTtl(2);                  // T=0:Open the db with ttl = 2
+  PutValues(0, kSampleSize_);  // T=0:Insert Set1. Delete at t=2
+  SleepCompactCheck(1, 0, kSampleSize_,
+                    true);  // T=1:Set1 should still be there
   CloseTtl();
 }
 
@@ -449,9 +475,9 @@ TEST_F(TtlTest, PresentDuringTTL) {
 TEST_F(TtlTest, AbsentAfterTTL) {
   MakeKVMap(kSampleSize_);
 
-  OpenTtl(1);                                  // T=0:Open the db with ttl = 2
-  PutValues(0, kSampleSize_);                  // T=0:Insert Set1. Delete at t=2
-  SleepCompactCheck(2, 0, kSampleSize_, false); // T=2:Set1 should not be there
+  OpenTtl(1);                  // T=0:Open the db with ttl = 2
+  PutValues(0, kSampleSize_);  // T=0:Insert Set1. Delete at t=2
+  SleepCompactCheck(2, 0, kSampleSize_, false);  // T=2:Set1 should not be there
   CloseTtl();
 }
 
@@ -461,10 +487,10 @@ TEST_F(TtlTest, ResetTimestamp) {
   MakeKVMap(kSampleSize_);
 
   OpenTtl(3);
-  PutValues(0, kSampleSize_);            // T=0: Insert Set1. Delete at t=3
-  env_->Sleep(2);                        // T=2
-  PutValues(0, kSampleSize_);            // T=2: Insert Set1. Delete at t=5
-  SleepCompactCheck(2, 0, kSampleSize_); // T=4: Set1 should still be there
+  PutValues(0, kSampleSize_);             // T=0: Insert Set1. Delete at t=3
+  env_->Sleep(2);                         // T=2
+  PutValues(0, kSampleSize_);             // T=2: Insert Set1. Delete at t=5
+  SleepCompactCheck(2, 0, kSampleSize_);  // T=4: Set1 should still be there
   CloseTtl();
 }
 
@@ -483,8 +509,8 @@ TEST_F(TtlTest, IterAbsentAfterTTL) {
   MakeKVMap(kSampleSize_);
 
   OpenTtl(1);
-  PutValues(0, kSampleSize_);                      // T=0: Insert. Delete at t=1
-  SleepCompactCheckIter(2, 0, kSampleSize_, false); // T=2: Should not be there
+  PutValues(0, kSampleSize_);  // T=0: Insert. Delete at t=1
+  SleepCompactCheckIter(2, 0, kSampleSize_, false);  // T=2: Should not be there
   CloseTtl();
 }
 
@@ -494,11 +520,11 @@ TEST_F(TtlTest, MultiOpenSamePresent) {
   MakeKVMap(kSampleSize_);
 
   OpenTtl(2);
-  PutValues(0, kSampleSize_);                   // T=0: Insert. Delete at t=2
+  PutValues(0, kSampleSize_);  // T=0: Insert. Delete at t=2
   CloseTtl();
 
-  OpenTtl(2);                                  // T=0. Delete at t=2
-  SleepCompactCheck(1, 0, kSampleSize_);        // T=1: Set should be there
+  OpenTtl(2);                             // T=0. Delete at t=2
+  SleepCompactCheck(1, 0, kSampleSize_);  // T=1: Set should be there
   CloseTtl();
 }
 
@@ -508,11 +534,11 @@ TEST_F(TtlTest, MultiOpenSameAbsent) {
   MakeKVMap(kSampleSize_);
 
   OpenTtl(1);
-  PutValues(0, kSampleSize_);                   // T=0: Insert. Delete at t=1
+  PutValues(0, kSampleSize_);  // T=0: Insert. Delete at t=1
   CloseTtl();
 
-  OpenTtl(1);                                  // T=0.Delete at t=1
-  SleepCompactCheck(2, 0, kSampleSize_, false); // T=2: Set should not be there
+  OpenTtl(1);                                    // T=0.Delete at t=1
+  SleepCompactCheck(2, 0, kSampleSize_, false);  // T=2: Set should not be there
   CloseTtl();
 }
 
@@ -521,11 +547,11 @@ TEST_F(TtlTest, MultiOpenDifferent) {
   MakeKVMap(kSampleSize_);
 
   OpenTtl(1);
-  PutValues(0, kSampleSize_);            // T=0: Insert. Delete at t=1
+  PutValues(0, kSampleSize_);  // T=0: Insert. Delete at t=1
   CloseTtl();
 
-  OpenTtl(3);                           // T=0: Set deleted at t=3
-  SleepCompactCheck(2, 0, kSampleSize_); // T=2: Set should be there
+  OpenTtl(3);                             // T=0: Set deleted at t=3
+  SleepCompactCheck(2, 0, kSampleSize_);  // T=2: Set should be there
   CloseTtl();
 }
 
@@ -533,12 +559,17 @@ TEST_F(TtlTest, MultiOpenDifferent) {
 TEST_F(TtlTest, ReadOnlyPresentForever) {
   MakeKVMap(kSampleSize_);
 
-  OpenTtl(1);                                 // T=0:Open the db normally
-  PutValues(0, kSampleSize_);                  // T=0:Insert Set1. Delete at t=1
+  OpenTtl(1);                  // T=0:Open the db normally
+  PutValues(0, kSampleSize_);  // T=0:Insert Set1. Delete at t=1
   CloseTtl();
 
   OpenReadOnlyTtl(1);
-  SleepCompactCheck(2, 0, kSampleSize_);       // T=2:Set1 should still be there
+  ASSERT_TRUE(db_ttl_);
+
+  env_->Sleep(2);
+  Status s = ManualCompact();  // T=2:Set1 should still be there
+  ASSERT_TRUE(s.IsNotSupported());
+  CompactCheck(0, kSampleSize_);
   CloseTtl();
 }
 
@@ -567,17 +598,17 @@ TEST_F(TtlTest, CompactionFilter) {
   MakeKVMap(kSampleSize_);
 
   OpenTtlWithTestCompaction(1);
-  PutValues(0, kSampleSize_);                  // T=0:Insert Set1. Delete at t=1
+  PutValues(0, kSampleSize_);  // T=0:Insert Set1. Delete at t=1
   // T=2: TTL logic takes precedence over TestFilter:-Set1 should not be there
   SleepCompactCheck(2, 0, kSampleSize_, false);
   CloseTtl();
 
   OpenTtlWithTestCompaction(3);
-  PutValues(0, kSampleSize_);                   // T=0:Insert Set1.
+  PutValues(0, kSampleSize_);  // T=0:Insert Set1.
   int64_t partition = kSampleSize_ / 3;
-  SleepCompactCheck(1, 0, partition, false);                  // Part dropped
-  SleepCompactCheck(0, partition, partition);                 // Part kept
-  SleepCompactCheck(0, 2 * partition, partition, true, true); // Part changed
+  SleepCompactCheck(1, 0, partition, false);                   // Part dropped
+  SleepCompactCheck(0, partition, partition);                  // Part kept
+  SleepCompactCheck(0, 2 * partition, partition, true, true);  // Part changed
   CloseTtl();
 }
 
@@ -666,17 +697,206 @@ TEST_F(TtlTest, ColumnFamiliesTest) {
 TEST_F(TtlTest, ChangeTtlOnOpenDb) {
   MakeKVMap(kSampleSize_);
 
-  OpenTtl(1);                                  // T=0:Open the db with ttl = 2
+  OpenTtl(1);  // T=0:Open the db with ttl = 2
   SetTtl(3);
-  PutValues(0, kSampleSize_);                  // T=0:Insert Set1. Delete at t=2
-  SleepCompactCheck(2, 0, kSampleSize_, true); // T=2:Set1 should be there
+  PutValues(0, kSampleSize_);  // T=0:Insert Set1. Delete at t=2
+  SleepCompactCheck(2, 0, kSampleSize_, true);  // T=2:Set1 should be there
   CloseTtl();
 }
 
+// Test DeleteRange for DBWithTtl
+TEST_F(TtlTest, DeleteRangeTest) {
+  OpenTtl();
+  ASSERT_OK(db_ttl_->Put(WriteOptions(), "a", "val"));
+  MakeDeleteRange("a", "b");
+  ASSERT_OK(db_ttl_->Put(WriteOptions(), "c", "val"));
+  MakeDeleteRange("b", "d");
+  ASSERT_OK(db_ttl_->Put(WriteOptions(), "e", "val"));
+  MakeDeleteRange("d", "e");
+  // first iteration verifies query correctness in memtable, second verifies
+  // query correctness for a single SST file
+  for (int i = 0; i < 2; i++) {
+    if (i > 0) {
+      ASSERT_OK(db_ttl_->Flush(FlushOptions()));
+    }
+    std::string value;
+    ASSERT_TRUE(db_ttl_->Get(ReadOptions(), "a", &value).IsNotFound());
+    ASSERT_TRUE(db_ttl_->Get(ReadOptions(), "c", &value).IsNotFound());
+    ASSERT_OK(db_ttl_->Get(ReadOptions(), "e", &value));
+  }
+  CloseTtl();
+}
+
+class DummyFilter : public CompactionFilter {
+ public:
+  bool Filter(int /*level*/, const Slice& /*key*/, const Slice& /*value*/,
+              std::string* /*new_value*/,
+              bool* /*value_changed*/) const override {
+    return false;
+  }
+
+  const char* Name() const override { return kClassName(); }
+  static const char* kClassName() { return "DummyFilter"; }
+};
+
+class DummyFilterFactory : public CompactionFilterFactory {
+ public:
+  const char* Name() const override { return kClassName(); }
+  static const char* kClassName() { return "DummyFilterFactory"; }
+
+  std::unique_ptr<CompactionFilter> CreateCompactionFilter(
+      const CompactionFilter::Context&) override {
+    std::unique_ptr<CompactionFilter> f(new DummyFilter());
+    return f;
+  }
+};
+
+static int RegisterTestObjects(ObjectLibrary& library,
+                               const std::string& /*arg*/) {
+  library.AddFactory<CompactionFilter>(
+      "DummyFilter", [](const std::string& /*uri*/,
+                        std::unique_ptr<CompactionFilter>* /*guard*/,
+                        std::string* /* errmsg */) {
+        static DummyFilter dummy;
+        return &dummy;
+      });
+  library.AddFactory<CompactionFilterFactory>(
+      "DummyFilterFactory", [](const std::string& /*uri*/,
+                               std::unique_ptr<CompactionFilterFactory>* guard,
+                               std::string* /* errmsg */) {
+        guard->reset(new DummyFilterFactory());
+        return guard->get();
+      });
+  return 2;
+}
+
+class TtlOptionsTest : public testing::Test {
+ public:
+  TtlOptionsTest() {
+    config_options_.registry->AddLibrary("RegisterTtlObjects",
+                                         RegisterTtlObjects, "");
+    config_options_.registry->AddLibrary("RegisterTtlTestObjects",
+                                         RegisterTestObjects, "");
+  }
+  ConfigOptions config_options_;
+};
+
+TEST_F(TtlOptionsTest, LoadTtlCompactionFilter) {
+  const CompactionFilter* filter = nullptr;
+
+  ASSERT_OK(CompactionFilter::CreateFromString(
+      config_options_, TtlCompactionFilter::kClassName(), &filter));
+  ASSERT_NE(filter, nullptr);
+  ASSERT_STREQ(filter->Name(), TtlCompactionFilter::kClassName());
+  auto ttl = filter->GetOptions<int32_t>("TTL");
+  ASSERT_NE(ttl, nullptr);
+  ASSERT_EQ(*ttl, 0);
+  ASSERT_OK(filter->ValidateOptions(DBOptions(), ColumnFamilyOptions()));
+  delete filter;
+  filter = nullptr;
+
+  ASSERT_OK(CompactionFilter::CreateFromString(
+      config_options_, "id=TtlCompactionFilter; ttl=123", &filter));
+  ASSERT_NE(filter, nullptr);
+  ttl = filter->GetOptions<int32_t>("TTL");
+  ASSERT_NE(ttl, nullptr);
+  ASSERT_EQ(*ttl, 123);
+  ASSERT_OK(filter->ValidateOptions(DBOptions(), ColumnFamilyOptions()));
+  delete filter;
+  filter = nullptr;
+
+  ASSERT_OK(CompactionFilter::CreateFromString(
+      config_options_,
+      "id=TtlCompactionFilter; ttl=456; user_filter=DummyFilter;", &filter));
+  ASSERT_NE(filter, nullptr);
+  auto inner = filter->CheckedCast<DummyFilter>();
+  ASSERT_NE(inner, nullptr);
+  ASSERT_OK(filter->ValidateOptions(DBOptions(), ColumnFamilyOptions()));
+  std::string mismatch;
+  std::string opts_str = filter->ToString(config_options_);
+  const CompactionFilter* copy = nullptr;
+  ASSERT_OK(
+      CompactionFilter::CreateFromString(config_options_, opts_str, &copy));
+  ASSERT_TRUE(filter->AreEquivalent(config_options_, copy, &mismatch));
+  delete filter;
+  delete copy;
+}
+
+TEST_F(TtlOptionsTest, LoadTtlCompactionFilterFactory) {
+  std::shared_ptr<CompactionFilterFactory> cff;
+
+  ASSERT_OK(CompactionFilterFactory::CreateFromString(
+      config_options_, TtlCompactionFilterFactory::kClassName(), &cff));
+  ASSERT_NE(cff.get(), nullptr);
+  ASSERT_STREQ(cff->Name(), TtlCompactionFilterFactory::kClassName());
+  auto ttl = cff->GetOptions<int32_t>("TTL");
+  ASSERT_NE(ttl, nullptr);
+  ASSERT_EQ(*ttl, 0);
+  ASSERT_OK(cff->ValidateOptions(DBOptions(), ColumnFamilyOptions()));
+
+  ASSERT_OK(CompactionFilterFactory::CreateFromString(
+      config_options_, "id=TtlCompactionFilterFactory; ttl=123", &cff));
+  ASSERT_NE(cff.get(), nullptr);
+  ASSERT_STREQ(cff->Name(), TtlCompactionFilterFactory::kClassName());
+  ttl = cff->GetOptions<int32_t>("TTL");
+  ASSERT_NE(ttl, nullptr);
+  ASSERT_EQ(*ttl, 123);
+  ASSERT_OK(cff->ValidateOptions(DBOptions(), ColumnFamilyOptions()));
+
+  ASSERT_OK(CompactionFilterFactory::CreateFromString(
+      config_options_,
+      "id=TtlCompactionFilterFactory; ttl=456; "
+      "user_filter_factory=DummyFilterFactory;",
+      &cff));
+  ASSERT_NE(cff.get(), nullptr);
+  auto filter = cff->CreateCompactionFilter(CompactionFilter::Context());
+  ASSERT_NE(filter.get(), nullptr);
+  auto ttlf = filter->CheckedCast<TtlCompactionFilter>();
+  ASSERT_EQ(filter.get(), ttlf);
+  auto user = filter->CheckedCast<DummyFilter>();
+  ASSERT_NE(user, nullptr);
+  ASSERT_OK(cff->ValidateOptions(DBOptions(), ColumnFamilyOptions()));
+
+  std::string opts_str = cff->ToString(config_options_);
+  std::string mismatch;
+  std::shared_ptr<CompactionFilterFactory> copy;
+  ASSERT_OK(CompactionFilterFactory::CreateFromString(config_options_, opts_str,
+                                                      &copy));
+  ASSERT_TRUE(cff->AreEquivalent(config_options_, copy.get(), &mismatch));
+}
+
+TEST_F(TtlOptionsTest, LoadTtlMergeOperator) {
+  std::shared_ptr<MergeOperator> mo;
+
+  config_options_.invoke_prepare_options = false;
+  ASSERT_OK(MergeOperator::CreateFromString(
+      config_options_, TtlMergeOperator::kClassName(), &mo));
+  ASSERT_NE(mo.get(), nullptr);
+  ASSERT_STREQ(mo->Name(), TtlMergeOperator::kClassName());
+  ASSERT_NOK(mo->ValidateOptions(DBOptions(), ColumnFamilyOptions()));
+
+  config_options_.invoke_prepare_options = true;
+  ASSERT_OK(MergeOperator::CreateFromString(
+      config_options_, "id=TtlMergeOperator; user_operator=bytesxor", &mo));
+  ASSERT_NE(mo.get(), nullptr);
+  ASSERT_STREQ(mo->Name(), TtlMergeOperator::kClassName());
+  ASSERT_OK(mo->ValidateOptions(DBOptions(), ColumnFamilyOptions()));
+  auto ttl_mo = mo->CheckedCast<TtlMergeOperator>();
+  ASSERT_EQ(mo.get(), ttl_mo);
+  auto user = ttl_mo->CheckedCast<BytesXOROperator>();
+  ASSERT_NE(user, nullptr);
+
+  std::string mismatch;
+  std::string opts_str = mo->ToString(config_options_);
+  std::shared_ptr<MergeOperator> copy;
+  ASSERT_OK(MergeOperator::CreateFromString(config_options_, opts_str, &copy));
+  ASSERT_TRUE(mo->AreEquivalent(config_options_, copy.get(), &mismatch));
+}
 }  // namespace ROCKSDB_NAMESPACE
 
 // A black-box test for the ttl wrapper around rocksdb
 int main(int argc, char** argv) {
+  ROCKSDB_NAMESPACE::port::InstallStackTraceHandler();
   ::testing::InitGoogleTest(&argc, argv);
   return RUN_ALL_TESTS();
 }