]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
rgw/multi: Add GenCache to ShardCache
authorAdam C. Emerson <aemerson@redhat.com>
Thu, 2 Apr 2026 19:31:16 +0000 (15:31 -0400)
committerAdam C. Emerson <aemerson@redhat.com>
Thu, 14 May 2026 23:26:05 +0000 (19:26 -0400)
The GenCache will keep track of per-bucket sync information, like
if we have had to synthesize a change-per-shard as a result of hitting
the wrong generation.

Templatize tests.

Fixes: https://tracker.ceph.com/issues/75786
Signed-off-by: Adam C. Emerson <aemerson@redhat.com>
src/rgw/driver/rados/rgw_data_sync.cc
src/rgw/rgw_bucket_sync_cache.h
src/test/rgw/test_rgw_bucket_sync_cache.cc

index 3b742b8570616e6b97c3a5e34b1bd4d02d8f9a12..abe1e611ada4270e4b6d97dc702fd4ecf9a6891c 100644 (file)
@@ -1426,7 +1426,7 @@ public:
 class RGWDataSyncSingleEntryCR : public RGWCoroutine {
   RGWDataSyncCtx *sc;
   RGWDataSyncEnv *sync_env;
-  rgw::bucket_sync::Handle state; // cached bucket-shard state
+  rgw::bucket_sync::ShardHandle state; // cached bucket-shard state
   rgw_data_sync_obligation obligation; // input obligation
   std::optional<rgw_data_sync_obligation> complete; // obligation to complete
   uint32_t obligation_counter = 0;
@@ -1438,7 +1438,7 @@ class RGWDataSyncSingleEntryCR : public RGWCoroutine {
   ceph::real_time progress;
   int sync_status = 0;
 public:
-  RGWDataSyncSingleEntryCR(RGWDataSyncCtx *_sc, rgw::bucket_sync::Handle state,
+  RGWDataSyncSingleEntryCR(RGWDataSyncCtx *_sc, rgw::bucket_sync::ShardHandle state,
                            rgw_data_sync_obligation _obligation,
                            RGWDataSyncShardMarkerTrack *_marker_tracker,
                            const rgw_raw_obj& error_repo,
@@ -1641,7 +1641,7 @@ RGWCoroutine* data_sync_single_entry(RGWDataSyncCtx *sc, const rgw_bucket_shard&
                                 const std::string marker,
                                 ceph::real_time timestamp,
                                 boost::intrusive_ptr<const RGWContinuousLeaseCR> lease_cr,
-                                boost::intrusive_ptr<rgw::bucket_sync::Cache> bucket_shard_cache,
+                                boost::intrusive_ptr<rgw::bucket_sync::ShardCache> bucket_shard_cache,
                                 RGWDataSyncShardMarkerTrack* marker_tracker,
                                 rgw_raw_obj error_repo,
                                 RGWSyncTraceNodeRef& tn,
@@ -1674,7 +1674,7 @@ class RGWDataFullSyncSingleEntryCR : public RGWCoroutine {
   rgw_raw_obj error_repo;
   ceph::real_time timestamp;
   boost::intrusive_ptr<const RGWContinuousLeaseCR> lease_cr;
-  boost::intrusive_ptr<rgw::bucket_sync::Cache> bucket_shard_cache;
+  boost::intrusive_ptr<rgw::bucket_sync::ShardCache> bucket_shard_cache;
   RGWDataSyncShardMarkerTrack* marker_tracker;
   RGWSyncTraceNodeRef tn;
   rgw_bucket_index_marker_info remote_info;
@@ -1689,7 +1689,7 @@ public:
   RGWDataFullSyncSingleEntryCR(RGWDataSyncCtx *_sc, const rgw_pool& _pool, const rgw_bucket_shard& _source_bs,
                       const std::string& _key, const rgw_data_sync_status& _sync_status, const rgw_raw_obj& _error_repo,
                       ceph::real_time _timestamp, boost::intrusive_ptr<const RGWContinuousLeaseCR> _lease_cr,
-                      boost::intrusive_ptr<rgw::bucket_sync::Cache> _bucket_shard_cache,
+                      boost::intrusive_ptr<rgw::bucket_sync::ShardCache> _bucket_shard_cache,
                       RGWDataSyncShardMarkerTrack* _marker_tracker,
                       RGWSyncTraceNodeRef& _tn)
     : RGWCoroutine(_sc->cct), sc(_sc), sync_env(_sc->env), pool(_pool), source_bs(_source_bs), key(_key),
@@ -1791,7 +1791,7 @@ protected:
   boost::intrusive_ptr<const RGWContinuousLeaseCR> lease_cr;
   const rgw_data_sync_status& sync_status;
   RGWObjVersionTracker& objv;
-  boost::intrusive_ptr<rgw::bucket_sync::Cache> bucket_shard_cache;
+  boost::intrusive_ptr<rgw::bucket_sync::ShardCache> bucket_shard_cache;
 
   std::optional<RGWDataSyncShardMarkerTrack> marker_tracker;
   RGWRadosGetOmapValsCR::ResultPtr omapvals;
@@ -1818,7 +1818,7 @@ protected:
     boost::intrusive_ptr<const RGWContinuousLeaseCR> lease_cr,
     const rgw_data_sync_status& sync_status,
     RGWObjVersionTracker& objv,
-    const boost::intrusive_ptr<rgw::bucket_sync::Cache>& bucket_shard_cache)
+    const boost::intrusive_ptr<rgw::bucket_sync::ShardCache>& bucket_shard_cache)
     : RGWCoroutine(_sc->cct), sc(_sc), pool(pool), shard_id(shard_id),
       sync_marker(sync_marker), tn(tn), status_oid(status_oid),
       error_repo(error_repo), lease_cr(std::move(lease_cr)),
@@ -1846,7 +1846,7 @@ public:
     const string& status_oid, const rgw_raw_obj& error_repo,
     boost::intrusive_ptr<const RGWContinuousLeaseCR> lease_cr,
     const rgw_data_sync_status& sync_status, RGWObjVersionTracker& objv,
-    const boost::intrusive_ptr<rgw::bucket_sync::Cache>& bucket_shard_cache)
+    const boost::intrusive_ptr<rgw::bucket_sync::ShardCache>& bucket_shard_cache)
     : RGWDataBaseSyncShardCR(sc, pool, shard_id, sync_marker, tn,
                             status_oid, error_repo, std::move(lease_cr),
                             sync_status, objv, bucket_shard_cache) {}
@@ -1999,7 +1999,7 @@ public:
     const string& status_oid, const rgw_raw_obj& error_repo,
     boost::intrusive_ptr<const RGWContinuousLeaseCR> lease_cr,
     const rgw_data_sync_status& sync_status, RGWObjVersionTracker& objv,
-    const boost::intrusive_ptr<rgw::bucket_sync::Cache>& bucket_shard_cache,
+    const boost::intrusive_ptr<rgw::bucket_sync::ShardCache>& bucket_shard_cache,
     ceph::mutex& inc_lock,
     bc::flat_set<rgw_data_notify_entry>& modified_shards)
     : RGWDataBaseSyncShardCR(sc, pool, shard_id, sync_marker, tn,
@@ -2221,8 +2221,8 @@ class RGWDataSyncShardCR : public RGWCoroutine {
 
   // target number of entries to cache before recycling idle ones
   static constexpr size_t target_cache_size = 256;
-  boost::intrusive_ptr<rgw::bucket_sync::Cache> bucket_shard_cache {
-    rgw::bucket_sync::Cache::create(target_cache_size) };
+  boost::intrusive_ptr<rgw::bucket_sync::ShardCache> bucket_shard_cache {
+    rgw::bucket_sync::ShardCache::create(target_cache_size) };
 
   boost::intrusive_ptr<RGWContinuousLeaseCR> lease_cr;
   boost::intrusive_ptr<RGWCoroutinesStack> lease_stack;
index 01ebafe0727c1cbba2acbdbd04f9c81858f07424..7b206994d89db59c653ab38441404f0eeae07db4 100644 (file)
 #include <boost/smart_ptr/intrusive_ref_counter.hpp>
 #include "common/intrusive_lru.h"
 #include "rgw_data_sync.h"
+#include "common/ceph_time.h"
 
 namespace rgw::bucket_sync {
 
-// per bucket-shard state cached by DataSyncShardCR
-struct State {
+// per bucket-shard (dimensioned by generation) state cached by DataSyncShardCR
+struct ShardState {
+  using key_type = std::pair<rgw_bucket_shard, std::optional<uint64_t>>;
   // the source bucket shard to sync
-  std::pair<rgw_bucket_shard, std::optional<uint64_t>> key;
+  key_type key;
   // current sync obligation being processed by DataSyncSingleEntry
   std::optional<rgw_data_sync_obligation> obligation;
   // incremented with each new obligation
@@ -31,27 +33,47 @@ struct State {
   // highest timestamp applied by all sources
   ceph::real_time progress_timestamp;
 
-  State(const std::pair<rgw_bucket_shard, std::optional<uint64_t>>& key ) noexcept
-    : key(key) {}
-  State(const rgw_bucket_shard& shard, std::optional<uint64_t> gen) noexcept
+  ShardState(const key_type& key) noexcept
+  : key(key) {}
+  ShardState(const rgw_bucket_shard& shard, std::optional<uint64_t> gen) noexcept
     : key(shard, gen) {}
 };
 
+// per bucket (dimensioned by generation) state cached by DataSyncShardCR
+struct GenState {
+  using key_type = std::pair<std::string, std::optional<uint64_t>>;
+  // the source bucket/generation to sync
+  key_type key;
+  // Last future generation recovery timestamp
+  ceph::coarse_mono_time last_future_generation_recovery = ceph::coarse_mono_clock::zero();
+
+  GenState(const key_type& key) noexcept
+    : key(key) {}
+  GenState(std::string bucket, std::optional<uint64_t> gen) noexcept
+  : key(std::move(bucket), gen) {}
+};
+
+template<typename State>
 struct Entry;
+template<typename State>
 struct EntryToKey;
+template<typename State>
 class Handle;
 
+template<typename State>
 using lru_config = ceph::common::intrusive_lru_config<
-  std::pair<rgw_bucket_shard, std::optional<uint64_t>>, Entry, EntryToKey>;
+    typename State::key_type, Entry<State>, EntryToKey<State>>;
 
 // a recyclable cache entry
-struct Entry : State, ceph::common::intrusive_lru_base<lru_config> {
+template<typename State>
+struct Entry : State, ceph::common::intrusive_lru_base<lru_config<State>> {
   using State::State;
 };
 
+template<typename State>
 struct EntryToKey {
-  using type = std::pair<rgw_bucket_shard, std::optional<uint64_t>>;
-  const type& operator()(const Entry& e) { return e.key; }
+  using type = typename State::key_type;
+  const type& operator()(const Entry<State>& e) { return e.key; }
 };
 
 // use a non-atomic reference count since these aren't shared across threads
@@ -59,9 +81,10 @@ template <typename T>
 using thread_unsafe_ref_counter = boost::intrusive_ref_counter<
     T, boost::thread_unsafe_counter>;
 
-// a state cache for entries within a single datalog shard
-class Cache : public thread_unsafe_ref_counter<Cache> {
-  ceph::common::intrusive_lru<lru_config> cache;
+// A state cache for entries within a single datalog shard
+template<typename State>
+class Cache : public thread_unsafe_ref_counter<Cache<State>> {
+  ceph::common::intrusive_lru<lru_config<State>> cache;
  protected:
   // protected ctor to enforce the use of factory function create()
   explicit Cache(size_t target_size) {
@@ -74,18 +97,19 @@ class Cache : public thread_unsafe_ref_counter<Cache> {
 
   // find or create a cache entry for the given key, and return a Handle that
   // keeps it lru-pinned until destruction
-  Handle get(const rgw_bucket_shard& shard, std::optional<uint64_t> gen);
+  Handle<State> get(const auto& ...args);
 };
 
 // a State handle that keeps the Cache referenced
+template<typename State>
 class Handle {
-  boost::intrusive_ptr<Cache> cache;
-  boost::intrusive_ptr<Entry> entry;
+  boost::intrusive_ptr<Cache<State>> cache;
+  boost::intrusive_ptr<Entry<State>> entry;
  public:
   Handle() noexcept = default;
   ~Handle() = default;
-  Handle(boost::intrusive_ptr<Cache> cache,
-         boost::intrusive_ptr<Entry> entry) noexcept
+  Handle(boost::intrusive_ptr<Cache<State>> cache,
+         boost::intrusive_ptr<Entry<State>> entry) noexcept
     : cache(std::move(cache)), entry(std::move(entry)) {}
   Handle(Handle&&) = default;
   Handle(const Handle&) = default;
@@ -107,10 +131,21 @@ class Handle {
   State* operator->() const noexcept { return entry.get(); }
 };
 
-inline Handle Cache::get(const rgw_bucket_shard& shard, std::optional<uint64_t> gen)
+template<typename State>
+inline Handle<State> Cache<State>::get(const auto& ...args)
 {
-  auto result = cache.get_or_create({ shard, gen });
+  static_assert(
+      std::is_constructible_v<typename State::key_type, decltype(args)...>,
+      "The arguments to Cache<State>::get must be arguments to construct "
+      "State");
+  auto result = cache.get_or_create({args...});
   return {this, std::move(result.first)};
 }
 
+using ShardHandle = Handle<ShardState>;
+using ShardCache = Cache<ShardState>;
+
+using GenHandle = Handle<GenState>;
+using GenCache = Cache<GenState>;
+
 } // namespace rgw::bucket_sync
index ea285c6655ea9db758ac31dd46cd8a6942fc947e..59060847269699d748419bb0a2bfd4b301ecf6fe 100644 (file)
  * Foundation.  See file COPYING.
  */
 
-#include "rgw_bucket_sync_cache.h"
+#include <common/ceph_time.h>
 #include <gtest/gtest.h>
 
+#include "rgw_bucket_sync_cache.h"
+
 using namespace rgw::bucket_sync;
 
-// helper function to construct rgw_bucket_shard
-static rgw_bucket_shard make_key(const std::string& tenant,
-                                 const std::string& bucket, int shard)
+using Shard = ShardState;
+using Gen = GenState;
+
+namespace {
+/// Create a key suitable for a given cache
+///
+/// \tparam State One of `Shard` or `Gen`.
+///
+/// \param[in] tenant Owning tenant
+/// \param[in] bucket Bucket name
+/// \param[in] shard Bucket shard, ignored if `State` is `Gen`.
+///
+/// \return A key for the given bucket
+template <typename State>
+auto make_key(const std::string& tenant, const std::string& bucket, int shard) =
+    delete;
+
+template <>
+auto
+make_key<Shard>(const std::string& tenant, const std::string& bucket, int shard)
 {
   auto key = rgw_bucket_key{tenant, bucket};
   return rgw_bucket_shard{std::move(key), shard};
 }
 
-TEST(BucketSyncCache, ReturnCachedPinned)
+template <>
+auto
+make_key<Gen>(
+    const std::string& tenant,
+    const std::string& bucket,
+    // Dummy parameter for overload
+    int)
+{
+  auto key = rgw_bucket_key{tenant, bucket};
+  rgw_bucket b{std::move(key)};
+  return b.get_key();
+}
+
+/// Stick an integer into a state
+///
+/// \tparam State One of `Shard` or `Gen`.
+///
+/// \param[inout] state State to modify
+/// \param[in] value Value to store
+template <typename State>
+void mutate(State&, unsigned int value) = delete;
+
+template <>
+void
+mutate<Shard>(ShardState& state, unsigned int value)
+{
+  state.counter = value;
+}
+
+template <>
+void
+mutate<Gen>(GenState& state, unsigned int value)
+{
+  state.last_future_generation_recovery = ceph::coarse_mono_time{
+      ceph::timespan{value}};
+}
+
+/// Retrieve an integer from a state
+///
+/// \note Intended only for values stored with `mutate`, anything else
+/// will be truncated to the capacity of an `unsigned int`.
+///
+/// \tparam State One of `Shard` or `Gen`.
+///
+/// \param[in] state State to modify
+///
+/// \return The integer previously stored in the state
+template <typename State>
+unsigned int extract(const State&) = delete;
+
+template <>
+unsigned int
+extract<Shard>(const Shard& state)
+{
+  return static_cast<unsigned int>(state.counter);
+}
+
+template <>
+unsigned int
+extract<Gen>(const Gen& state)
 {
-  auto cache = Cache::create(0);
-  const auto key = make_key("", "1", 0);
+  return static_cast<unsigned int>(
+      state.last_future_generation_recovery.time_since_epoch().count());
+}
+} // namespace
+
+template <typename State>
+void
+ReturnCachedPinned()
+{
+  auto cache = Cache<State>::create(0);
+  const auto key = make_key<State>("", "1", 0);
   auto h1 = cache->get(key, std::nullopt); // pin
-  h1->counter = 1;
+  mutate(*h1, 1);
   auto h2 = cache->get(key, std::nullopt);
-  EXPECT_EQ(1, h2->counter);
+  EXPECT_EQ(1, extract(*h2));
 }
 
-TEST(BucketSyncCache, ReturnNewUnpinned)
+TEST(BucketShardSyncCache, ReturnCachedPinned) { ReturnCachedPinned<Shard>(); }
+
+TEST(BucketGenSyncCache, ReturnCachedPinned) { ReturnCachedPinned<Gen>(); }
+
+template <typename State>
+void
+ReturnNewUnpinned()
 {
-  auto cache = Cache::create(0);
-  const auto key = make_key("", "1", 0);
-  cache->get(key, std::nullopt)->counter = 1; // pin+unpin
-  EXPECT_EQ(0, cache->get(key, std::nullopt)->counter);
+  auto cache = Cache<State>::create(0);
+  const auto key = make_key<State>("", "1", 0);
+  mutate(*cache->get(key, std::nullopt), 1); // pin+unpin
+  EXPECT_EQ(0, extract(*cache->get(key, std::nullopt)));
 }
 
-TEST(BucketSyncCache, DistinctTenant)
+TEST(BucketShardSyncCache, ReturnNewUnpinned) { ReturnNewUnpinned<Shard>(); }
+
+TEST(BucketGenSyncCache, ReturnNewUnpinned) { ReturnNewUnpinned<Gen>(); }
+
+template <typename State>
+void
+DistinctTenant()
 {
-  auto cache = Cache::create(2);
-  const auto key1 = make_key("a", "bucket", 0);
-  const auto key2 = make_key("b", "bucket", 0);
-  cache->get(key1, std::nullopt)->counter = 1;
-  EXPECT_EQ(0, cache->get(key2, std::nullopt)->counter);
+  auto cache = Cache<State>::create(2);
+  const auto key1 = make_key<State>("a", "bucket", 0);
+  const auto key2 = make_key<State>("b", "bucket", 0);
+  mutate(*cache->get(key1, std::nullopt), 1);
+  EXPECT_EQ(0, extract(*cache->get(key2, std::nullopt)));
 }
 
-TEST(BucketSyncCache, DistinctShards)
+TEST(BucketShardSyncCache, DistinctTenant) { DistinctTenant<Shard>(); }
+
+TEST(BucketGenSyncCache, DistinctTenant) { DistinctTenant<Gen>(); }
+
+TEST(BucketShardSyncCache, DistinctShards)
 {
-  auto cache = Cache::create(2);
-  const auto key1 = make_key("", "bucket", 0);
-  const auto key2 = make_key("", "bucket", 1);
+  auto cache = ShardCache::create(2);
+  const auto key1 = make_key<Shard>("", "bucket", 0);
+  const auto key2 = make_key<Shard>("", "bucket", 1);
   cache->get(key1, std::nullopt)->counter = 1;
   EXPECT_EQ(0, cache->get(key2, std::nullopt)->counter);
 }
 
-TEST(BucketSyncCache, DistinctGen)
+template <typename State>
+void
+DistinctGen()
 {
-  auto cache = Cache::create(2);
-  const auto key = make_key("", "bucket", 0);
+  auto cache = Cache<State>::create(2);
+  const auto key = make_key<State>("", "bucket", 0);
   std::optional<uint64_t> gen1; // empty
   std::optional<uint64_t> gen2 = 5;
-  cache->get(key, gen1)->counter = 1;
-  EXPECT_EQ(0, cache->get(key, gen2)->counter);
+  mutate(*cache->get(key, gen1), 1);
+  EXPECT_EQ(0, extract(*cache->get(key, gen2)));
 }
 
-TEST(BucketSyncCache, DontEvictPinned)
+TEST(BucketShardSyncCache, DistinctGen) { DistinctGen<Shard>(); }
+
+TEST(BucketGenSyncCache, DistinctGen) { DistinctGen<Gen>(); }
+
+template <typename State>
+void
+DontEvictPinned()
 {
-  auto cache = Cache::create(0);
+  auto cache = Cache<State>::create(0);
 
-  const auto key1 = make_key("", "1", 0);
-  const auto key2 = make_key("", "2", 0);
+  const auto key1 = make_key<State>("", "1", 0);
+  const auto key2 = make_key<State>("", "2", 0);
 
   auto h1 = cache->get(key1, std::nullopt);
   EXPECT_EQ(key1, h1->key.first);
@@ -85,46 +196,64 @@ TEST(BucketSyncCache, DontEvictPinned)
   EXPECT_EQ(key1, h1->key.first); // h1 unchanged
 }
 
-TEST(BucketSyncCache, HandleLifetime)
+TEST(BucketShardSyncCache, DontEvictPinned) { DontEvictPinned<Shard>(); }
+
+TEST(BucketGenSyncCache, DontEvictPinned) { DontEvictPinned<Gen>(); }
+
+template <typename State>
+void
+HandleLifetime()
 {
-  const auto key = make_key("", "1", 0);
+  const auto key = make_key<State>("", "1", 0);
 
-  Handle h; // test that handles keep the cache referenced
+  Handle<State> h; // test that handles keep the cache referenced
   {
-    auto cache = Cache::create(0);
+    auto cache = Cache<State>::create(0);
     h = cache->get(key, std::nullopt);
   }
   EXPECT_EQ(key, h->key.first);
 }
 
-TEST(BucketSyncCache, TargetSize)
+TEST(BucketShardSyncCache, HandleLifetime) { HandleLifetime<Shard>(); }
+
+TEST(BucketGenSyncCache, HandleLifetime) { HandleLifetime<Gen>(); }
+
+template <typename State>
+void
+TargetSize()
 {
-  auto cache = Cache::create(2);
+  auto cache = Cache<State>::create(2);
 
-  const auto key1 = make_key("", "1", 0);
-  const auto key2 = make_key("", "2", 0);
-  const auto key3 = make_key("", "3", 0);
+  const auto key1 = make_key<State>("", "1", 0);
+  const auto key2 = make_key<State>("", "2", 0);
+  const auto key3 = make_key<State>("", "3", 0);
 
   // fill cache up to target_size=2
-  cache->get(key1, std::nullopt)->counter = 1;
-  cache->get(key2, std::nullopt)->counter = 2;
+  mutate(*cache->get(key1, std::nullopt), 1);
+  mutate(*cache->get(key2, std::nullopt), 2);
   // test that each unpinned entry is still cached
-  EXPECT_EQ(1, cache->get(key1, std::nullopt)->counter);
-  EXPECT_EQ(2, cache->get(key2, std::nullopt)->counter);
+  EXPECT_EQ(1, extract(*cache->get(key1, std::nullopt)));
+  EXPECT_EQ(2, extract(*cache->get(key2, std::nullopt)));
   // overflow the cache and recycle key1
-  cache->get(key3, std::nullopt)->counter = 3;
+  mutate(*cache->get(key3, std::nullopt), 3);
   // test that the oldest entry was recycled
-  EXPECT_EQ(0, cache->get(key1, std::nullopt)->counter);
+  EXPECT_EQ(0, extract(*cache->get(key1, std::nullopt)));
 }
 
-TEST(BucketSyncCache, HandleMoveAssignEmpty)
+TEST(BucketShardSyncCache, TargetSize) { TargetSize<Shard>(); }
+
+TEST(BucketGenSyncCache, TargetSize) { TargetSize<Gen>(); }
+
+template <typename State>
+void
+HandleMoveAssignEmpty()
 {
-  auto cache = Cache::create(0);
+  auto cache = Cache<State>::create(0);
 
-  const auto key1 = make_key("", "1", 0);
-  const auto key2 = make_key("", "2", 0);
+  const auto key1 = make_key<State>("", "1", 0);
+  const auto key2 = make_key<State>("", "2", 0);
 
-  Handle j1;
+  Handle<State> j1;
   {
     auto h1 = cache->get(key1, std::nullopt);
     j1 = std::move(h1); // assign over empty handle
@@ -134,32 +263,56 @@ TEST(BucketSyncCache, HandleMoveAssignEmpty)
   EXPECT_EQ(key1, j1->key.first); // j1 stays pinned
 }
 
-TEST(BucketSyncCache, HandleMoveAssignExisting)
+TEST(BucketShardSyncCache, HandleMoveAssignEmpty)
 {
-  const auto key1 = make_key("", "1", 0);
-  const auto key2 = make_key("", "2", 0);
+  HandleMoveAssignEmpty<Shard>();
+}
+
+TEST(BucketGenSyncCache, HandleMoveAssignEmpty)
+{
+  HandleMoveAssignEmpty<Gen>();
+}
 
-  Handle h1;
+template <typename State>
+void
+HandleMoveAssignExisting()
+{
+  const auto key1 = make_key<State>("", "1", 0);
+  const auto key2 = make_key<State>("", "2", 0);
+
+  Handle<State> h1;
   {
-    auto cache1 = Cache::create(0);
+    auto cache1 = Cache<State>::create(0);
     h1 = cache1->get(key1, std::nullopt);
-  } // j1 has the last ref to cache1
+  } // h1 has the last ref to cache1
   {
-    auto cache2 = Cache::create(0);
+    auto cache2 = Cache<State>::create(0);
     auto h2 = cache2->get(key2, std::nullopt);
     h1 = std::move(h2); // assign over existing handle
   }
   EXPECT_EQ(key2, h1->key.first);
 }
 
-TEST(BucketSyncCache, HandleCopyAssignEmpty)
+TEST(BucketShardSyncCache, HandleMoveAssignExisting)
+{
+  HandleMoveAssignExisting<Shard>();
+}
+
+TEST(BucketGenSyncCache, HandleMoveAssignExisting)
+{
+  HandleMoveAssignExisting<Gen>();
+}
+
+template <typename State>
+void
+HandleCopyAssignEmpty()
 {
-  auto cache = Cache::create(0);
+  auto cache = Cache<State>::create(0);
 
-  const auto key1 = make_key("", "1", 0);
-  const auto key2 = make_key("", "2", 0);
+  const auto key1 = make_key<State>("", "1", 0);
+  const auto key2 = make_key<State>("", "2", 0);
 
-  Handle j1;
+  Handle<State> j1;
   {
     auto h1 = cache->get(key1, std::nullopt);
     j1 = h1; // assign over empty handle
@@ -169,21 +322,43 @@ TEST(BucketSyncCache, HandleCopyAssignEmpty)
   EXPECT_EQ(key1, j1->key.first); // j1 stays pinned
 }
 
-TEST(BucketSyncCache, HandleCopyAssignExisting)
+TEST(BucketShardSyncCache, HandleCopyAssignEmpty)
+{
+  HandleCopyAssignEmpty<Shard>();
+}
+
+TEST(BucketGenSyncCache, HandleCopyAssignEmpty)
+{
+  HandleCopyAssignEmpty<Gen>();
+}
+
+template <typename State>
+void
+HandleCopyAssignExisting()
 {
-  const auto key1 = make_key("", "1", 0);
-  const auto key2 = make_key("", "2", 0);
+  const auto key1 = make_key<State>("", "1", 0);
+  const auto key2 = make_key<State>("", "2", 0);
 
-  Handle h1;
+  Handle<State> h1;
   {
-    auto cache1 = Cache::create(0);
+    auto cache1 = Cache<State>::create(0);
     h1 = cache1->get(key1, std::nullopt);
-  } // j1 has the last ref to cache1
+  } // h1 has the last ref to cache1
   {
-    auto cache2 = Cache::create(0);
+    auto cache2 = Cache<State>::create(0);
     auto h2 = cache2->get(key2, std::nullopt);
     h1 = h2; // assign over existing handle
     EXPECT_EQ(&*h1, &*h2);
   }
   EXPECT_EQ(key2, h1->key.first);
 }
+
+TEST(BucketShardSyncCache, HandleCopyAssignExisting)
+{
+  HandleCopyAssignExisting<Shard>();
+}
+
+TEST(BucketGenSyncCache, HandleCopyAssignExisting)
+{
+  HandleCopyAssignExisting<Gen>();
+}