]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
Fix usage log issues caused by incorrect form of name-by-user keys
authorYixin Jin <yjin@akamai.com>
Mon, 25 Aug 2025 20:38:57 +0000 (20:38 +0000)
committermheler <mheler@akamai.com>
Tue, 30 Sep 2025 16:01:35 +0000 (11:01 -0500)
1. The old name-by-user keys start with user/payer IDs. If those IDs
   start with '0', the resulting name-by-user keys could fall within
   the range of name-by-time keys, which has the prefix of '0' followed
   by 11 digits of the epoch. The intermingle of name-by-user and
   name-by-time keys causes the usage function to mistakenly treat
   name-by-user usage logs as name-by-time logs. This leads issues like
   failure to trim old logs because some name-by-user logs with later
   epoch listed BEFORE name-by-time logs with earlier epoch, causing
   the trim logic to stop prematurally.
2. To fix the above issue, adding prefix '~' in name-by-user keys when
   the user/payer IDs starting with '0'.
3. Introduce a rgw config option to indicate the need to deal with
   the co-existence of old and new name-by-user keys. Since handling
   both keys incurs performance penalty, this option helps avoid it
   once the cluster no longer has the old keys.
4. usage_iterate_range() is splitted to usage_iterate_range_by_user()
   and usage_iterate_range_by_time() as only the former needs to deal
   with the potential co-existence of old and new name-by-user keys.
   This split intends to simplify the logic.
5. "usage_basic" test in test_cls_rgw.cc is adapted slightly to reproduce
   the issue mentioned above.
6. A new test case "usage_key_transition" is added to test the handling
   of the co-existence of old and new keys.

Signed-off-by: Matthew N Heler <mheler@akamai.com>
PendingReleaseNotes
doc/radosgw/admin.rst
src/cls/rgw/cls_rgw.cc
src/common/options/rgw.yaml.in
src/test/cls_rgw/test_cls_rgw.cc

index d03bbb75af171b260d99e08fff922f39159d5336..dfb6440a65e116ea488fcb92ef2ee92d6664aa7e 100644 (file)
 * Dashboard: RGW granular bucket replication
 * Dashboard: SMB monitoring and management
 * Monitoring: New monitoring Dashboards for Application, NVMe, CephFS, and SMB Overview
+* RGW: Introduce the `rgw_usage_log_key_transition` configuration option to handle
+  the co-existence of old and new usage log keys. This option is enabled by default
+  to ensure compatibility during upgrades, but can be disabled once old usage logs
+  are no longer present to avoid performance overhead.
 
 >=19.2.1
 
index 2740a287a11cd2946b9503aebbeb7b7bae1350ab..cb405d797f7d99ebc770486c335aaae519ed0df2 100644 (file)
@@ -890,7 +890,6 @@ Options include:
 .. note:: You can specify time to a precision of minutes and seconds, but the
    specified time is stored only with a one-hour resolution.
 
-
 Show Usage
 ----------
 
@@ -925,3 +924,32 @@ example commands:
    radosgw-admin usage trim --uid=johndoe      
    radosgw-admin usage trim --uid=johndoe --end-date=2013-12-31
 
+Usage Log Key Transition
+-------------------------
+
+.. versionadded:: Umbrella
+
+The ``rgw_usage_log_key_transition`` configuration option controls how RGW handles
+usage log keys in the Umbrella release. This option is enabled by default to ensure
+compatibility with existing usage logs.
+
+In previous versions, usage log keys for user/payer IDs starting with '0' could interfere
+with time-based log keys, causing issues with log trimming and iteration. The new key format
+adds a '~' prefix to prevent this conflict.
+
+When ``rgw_usage_log_key_transition`` is enabled (default: ``true``), RGW will:
+
+- Handle both old and new usage log key formats
+- Automatically migrate old keys to the new format during normal operations
+- Ensure proper trimming and reading of usage logs during the transition period
+
+You can disable this option once all old usage logs have been migrated to improve performance:
+
+.. prompt:: bash #
+
+   ceph config set client.rgw rgw_usage_log_key_transition false
+
+.. note:: Only disable ``rgw_usage_log_key_transition`` after confirming that no old
+   usage log entries remain in your system, as this will prevent RGW from handling
+   the old key format and may result in incomplete usage statistics or failed trim operations.
+
index 673f218e3c4912957d63d862e94eedfe8745236d..984f46458a59d7c014b4381173e00cea29aef5ea 100644 (file)
@@ -3962,13 +3962,23 @@ static void usage_record_prefix_by_time(uint64_t epoch, string& key)
   key = buf;
 }
 
-static void usage_record_prefix_by_user(const string& user, uint64_t epoch, string& key)
+static void usage_record_prefix_by_user_old(const string& user, uint64_t epoch, string& key)
 {
   char buf[user.size() + 32];
   snprintf(buf, sizeof(buf), "%s_%011llu_", user.c_str(), (long long unsigned)epoch);
   key = buf;
 }
 
+static void usage_record_prefix_by_user(const string& user, uint64_t epoch, string& key)
+{
+  usage_record_prefix_by_user_old(user, epoch, key);
+  if (user.starts_with('0')) {
+    // Use ~ prefix for name_by_user records that have keys that could potentially
+    // put them among name_by_time records.
+    key.insert(0, 1, '~');
+  }
+}
+
 static void usage_record_name_by_time(uint64_t epoch, const string& user, const string& bucket, string& key)
 {
   char buf[32 + user.size() + bucket.size()];
@@ -3976,13 +3986,23 @@ static void usage_record_name_by_time(uint64_t epoch, const string& user, const
   key = buf;
 }
 
-static void usage_record_name_by_user(const string& user, uint64_t epoch, const string& bucket, string& key)
+static void usage_record_name_by_user_old(const string& user, uint64_t epoch, const string& bucket, string& key)
 {
   char buf[32 + user.size() + bucket.size()];
   snprintf(buf, sizeof(buf), "%s_%011llu_%s", user.c_str(), (long long unsigned)epoch, bucket.c_str());
   key = buf;
 }
 
+static void usage_record_name_by_user(const string& user, uint64_t epoch, const string& bucket, string& key)
+{
+  usage_record_name_by_user_old(user, epoch, bucket, key);
+  if (user.starts_with('0')) {
+    // Use ~ prefix for name_by_user records that have keys that could potentially
+    // put them among name_by_time records.
+    key.insert(0, 1, '~');
+  }
+}
+
 static int usage_record_decode(bufferlist& record_bl, rgw_usage_log_entry& e)
 {
   auto kiter = record_bl.cbegin();
@@ -4010,6 +4030,9 @@ static int rgw_user_usage_log_add(cls_method_context_t hctx, bufferlist *in, buf
     return -EINVAL;
   }
 
+  const ConfigProxy& conf = cls_get_config(hctx);
+  const bool key_transition = conf->rgw_usage_log_key_transition;
+
   rgw_usage_log_info& info = op.info;
 
   for (auto iter = info.entries.begin(); iter != info.entries.end(); ++iter) {
@@ -4048,97 +4071,171 @@ static int rgw_user_usage_log_add(cls_method_context_t hctx, bufferlist *in, buf
     ret = cls_cxx_map_set_val(hctx, key_by_user, &new_record_bl);
     if (ret < 0)
       return ret;
+
+    if (key_transition && puser->to_str().starts_with('0')) {
+      string key_by_user_old;
+      usage_record_name_by_user_old(puser->to_str(), entry.epoch, entry.bucket, key_by_user_old);
+      (void)cls_cxx_map_remove_key(hctx, key_by_user_old);
+    }
   }
 
   return 0;
 }
 
-static int usage_iterate_range(cls_method_context_t hctx, uint64_t start, uint64_t end, const string& user,
-                            const string& bucket, string& key_iter, uint32_t max_entries, bool *truncated,
-                            int (*cb)(cls_method_context_t, const string&, rgw_usage_log_entry&, void *),
-                            void *param)
+static int usage_handle_range(cls_method_context_t hctx, uint64_t start_epoch, uint64_t end_epoch,
+                              const string& start_key, const string& end_key, const string& user_key,
+                              const string& bucket, string& key_iter, uint32_t max_entries, bool& truncated,
+                              int (*cb)(cls_method_context_t, const string&, rgw_usage_log_entry&, void *),
+                              void *param)
 {
-  CLS_LOG(10, "entered %s", __func__);
-
-  map<string, bufferlist> keys;
   string filter_prefix;
-  string start_key, end_key;
-  bool by_user = !user.empty();
-  string user_key;
   bool truncated_status = false;
+  map<string, bufferlist> usage_logs;
+  bool by_user = !user_key.empty();
+  int cnt = 0;
 
-  ceph_assert(truncated != nullptr);
-
-  if (!by_user) {
-    usage_record_prefix_by_time(end, end_key);
-  } else {
-    user_key = user;
-    user_key.append("_");
-  }
-
-  if (key_iter.empty()) {
-    if (by_user) {
-      usage_record_prefix_by_user(user, start, start_key);
-    } else {
-      usage_record_prefix_by_time(start, start_key);
-    }
-  } else {
-    start_key = key_iter;
-  }
-
-  CLS_LOG(20, "usage_iterate_range start_key=%s", start_key.c_str());
-  int ret = cls_cxx_map_get_vals(hctx, start_key, filter_prefix, max_entries, &keys, &truncated_status);
+  CLS_LOG(20, "usage_handle_range start_key=%s", start_key.c_str());
+  int ret = cls_cxx_map_get_vals(hctx, start_key, filter_prefix, max_entries, &usage_logs, &truncated_status);
   if (ret < 0)
     return ret;
 
-  *truncated = truncated_status;
-
-  auto iter = keys.begin();
-  if (iter == keys.end())
-    return 0;
+  truncated = truncated_status;
 
-  for (; iter != keys.end(); ++iter) {
-    const string& key = iter->first;
+  for (auto log : usage_logs) {
+    const string& key = log.first;
     rgw_usage_log_entry e;
 
     key_iter = key;
     if (!by_user && key.compare(end_key) >= 0) {
-      CLS_LOG(20, "usage_iterate_range reached key=%s, done", key.c_str());
-      *truncated = false;
-      key_iter = key;
-      return 0;
+      CLS_LOG(20, "usage_handle_range reached key=%s, done", key.c_str());
+      truncated = false;
+      break;
     }
 
     if (by_user && key.compare(0, user_key.size(), user_key) != 0) {
-      CLS_LOG(20, "usage_iterate_range reached key=%s, done", key.c_str());
-      *truncated = false;
-      key_iter = key;
-      return 0;
+      CLS_LOG(20, "usage_handle_range reached key=%s, done", key.c_str());
+      truncated = false;
+      break;
     }
 
-    ret = usage_record_decode(iter->second, e);
+    ret = usage_record_decode(log.second, e);
     if (ret < 0)
       return ret;
 
     if (!bucket.empty() && bucket.compare(e.bucket))
       continue;
 
-    if (e.epoch < start)
+    if (e.epoch < start_epoch)
       continue;
 
     /* keys are sorted by epoch, so once we're past end we're done */
-    if (e.epoch >= end) {
-      *truncated = false;
-      return 0;
+    if (e.epoch >= end_epoch) {
+      truncated = false;
+      break;
     }
 
     ret = cb(hctx, key, e, param);
     if (ret < 0)
       return ret;
+
+    ++cnt;
+  }
+
+  return cnt;
+}
+
+static int usage_iterate_range_by_user(cls_method_context_t hctx, uint64_t start_epoch, uint64_t end_epoch,
+                            const string& user, const string& bucket, string& key_iter, uint32_t max_entries,
+                            bool& truncated, int (*cb)(cls_method_context_t, const string&, rgw_usage_log_entry&, void *),
+                            void *param)
+{
+  ceph_assert(!user.empty());
+
+  const ConfigProxy& conf = cls_get_config(hctx);
+  const bool key_transition = conf->rgw_usage_log_key_transition;
+
+  if (key_transition &&
+      user.starts_with('0') &&
+      (key_iter.empty() || key_iter.starts_with('0'))) {
+    // During key transition for records that could have keys that fall within name_by_time records,
+    // we need to go 2 passes to cover both the old keys and the new keys.
+    // Remove this block when key_transition is deprecated.
+    string user_key = user + "_";
+    string start_key;
+    string old_key_iter = key_iter;
+    if (key_iter.empty()) {
+      usage_record_prefix_by_user_old(user, start_epoch, start_key);
+    } else {
+      start_key = key_iter;
+    }
+    int ret = usage_handle_range(hctx, start_epoch, end_epoch, start_key, "", user_key,
+                                 bucket, old_key_iter, max_entries, truncated, cb, param);
+    if (ret < 0) {
+      return ret;
+    }
+
+    if (truncated) {
+      key_iter = old_key_iter;
+      return 0;
+    } else {
+      // All old keys have been handled, start fresh on the new keys
+      key_iter.clear();
+    }
+    max_entries -= ret;
+  }
+
+  if (max_entries > 0) {
+    string start_key;
+    string user_key = user.starts_with('0') ? (std::string("~") + user + "_") : (user + "_");
+    // Handle new keys
+    if (key_iter.empty()) {
+      usage_record_prefix_by_user(user, start_epoch, start_key);
+    } else {
+      start_key = key_iter;
+    }
+
+    return usage_handle_range(hctx, start_epoch, end_epoch, start_key, "", user_key,
+                              bucket, key_iter, max_entries, truncated, cb, param);
   }
+
   return 0;
 }
 
+static int usage_iterate_range_by_time(cls_method_context_t hctx, uint64_t start_epoch, uint64_t end_epoch,
+                            const string& bucket, string& key_iter, uint32_t max_entries, bool& truncated,
+                            int (*cb)(cls_method_context_t, const string&, rgw_usage_log_entry&, void *),
+                            void *param)
+{
+  CLS_LOG(10, "entered %s", __func__);
+
+  string start_key, end_key;
+
+  usage_record_prefix_by_time(end_epoch, end_key);
+
+  if (key_iter.empty()) {
+    usage_record_prefix_by_time(start_epoch, start_key);
+  } else {
+    start_key = key_iter;
+  }
+
+  return usage_handle_range(hctx, start_epoch, end_epoch, start_key, end_key, "",
+                            bucket, key_iter, max_entries, truncated, cb, param);
+}
+
+static int usage_iterate_range(cls_method_context_t hctx, uint64_t start_epoch, uint64_t end_epoch, const string& user,
+                            const string& bucket, string& key_iter, uint32_t max_entries, bool& truncated,
+                            int (*cb)(cls_method_context_t, const string&, rgw_usage_log_entry&, void *),
+                            void *param)
+{
+  CLS_LOG(10, "entered %s", __func__);
+
+  if (user.empty()) {
+    return usage_iterate_range_by_time(hctx, start_epoch, end_epoch, bucket, key_iter, max_entries, truncated, cb, param);
+  } else {
+    return usage_iterate_range_by_user(hctx, start_epoch, end_epoch, user, bucket, key_iter, max_entries, truncated, cb, param);
+  }
+}
+
 static int usage_log_read_cb(cls_method_context_t hctx, const string& key, rgw_usage_log_entry& entry, void *param)
 {
   map<rgw_user_bucket, rgw_usage_log_entry> *usage = (map<rgw_user_bucket, rgw_usage_log_entry> *)param;
@@ -4174,7 +4271,8 @@ int rgw_user_usage_log_read(cls_method_context_t hctx, bufferlist *in, bufferlis
   string iter = op.iter;
 #define MAX_ENTRIES 1000
   uint32_t max_entries = (op.max_entries ? op.max_entries : MAX_ENTRIES);
-  int ret = usage_iterate_range(hctx, op.start_epoch, op.end_epoch, op.owner, op.bucket, iter, max_entries, &ret_info.truncated, usage_log_read_cb, (void *)usage);
+
+  int ret = usage_iterate_range(hctx, op.start_epoch, op.end_epoch, op.owner, op.bucket, iter, max_entries, ret_info.truncated, usage_log_read_cb, (void *)usage);
   if (ret < 0)
     return ret;
 
@@ -4185,16 +4283,24 @@ int rgw_user_usage_log_read(cls_method_context_t hctx, bufferlist *in, bufferlis
   return 0;
 }
 
+struct usage_trim_param
+{
+  bool found = false;
+  const bool key_transition = false;
+};
+
 static int usage_log_trim_cb(cls_method_context_t hctx, const string& key, rgw_usage_log_entry& entry, void *param)
 {
-  bool *found = (bool *)param;
-  if (found) {
-    *found = true;
-  }
+  usage_trim_param *trim_param = (usage_trim_param *)param;
+
+  ceph_assert(trim_param != nullptr);
+
+  trim_param->found = true;
+
   string key_by_time;
   string key_by_user;
 
-  string o = entry.owner.to_str();
+  string o = entry.payer.empty() ? entry.owner.to_str() : entry.payer.to_str();
   usage_record_name_by_time(entry.epoch, o, entry.bucket, key_by_time);
   usage_record_name_by_user(o, entry.epoch, entry.bucket, key_by_user);
 
@@ -4202,6 +4308,12 @@ static int usage_log_trim_cb(cls_method_context_t hctx, const string& key, rgw_u
   if (ret < 0)
     return ret;
 
+  if (trim_param->key_transition && o.starts_with('0')) {
+    string key_by_user_old;
+    usage_record_name_by_user_old(o, entry.epoch, entry.bucket, key_by_user_old);
+    (void)cls_cxx_map_remove_key(hctx, key_by_user_old);
+  }
+
   return cls_cxx_map_remove_key(hctx, key_by_user);
 }
 
@@ -4226,13 +4338,16 @@ int rgw_user_usage_log_trim(cls_method_context_t hctx, bufferlist *in, bufferlis
 
   string iter;
   bool more;
-  bool found = false;
+  const ConfigProxy& conf = cls_get_config(hctx);
+  usage_trim_param trim_param{false, conf->rgw_usage_log_key_transition};
+
 #define MAX_USAGE_TRIM_ENTRIES 1000
-  ret = usage_iterate_range(hctx, op.start_epoch, op.end_epoch, op.user, op.bucket, iter, MAX_USAGE_TRIM_ENTRIES, &more, usage_log_trim_cb, (void *)&found);
+  ret = usage_iterate_range(hctx, op.start_epoch, op.end_epoch, op.user, op.bucket, iter, MAX_USAGE_TRIM_ENTRIES, more, usage_log_trim_cb, (void *)&trim_param);
+
   if (ret < 0)
     return ret;
 
-  if (!more && !found)
+  if (!more && !trim_param.found)
     return -ENODATA;
 
   return 0;
index 41c9a61127046ed16b44f93818420e98840253ad..31e4ffdb57da9322d3a2faea5bbd34b51caaaf11 100644 (file)
@@ -4523,3 +4523,19 @@ options:
   default: 11
   services: 
     - rgw
+- name: rgw_usage_log_key_transition
+  type: bool
+  level: advanced
+  desc: Hhandle the co-existence of both old and new name-by-user keys
+  long_desc: The new usage log keyed by owner/payer ID has the prefix of "~"
+    for IDs starting with '0'.  This prefix is absent from the old scheme.
+    This option instructs RGW to handle the old keys if they still exist.
+    It should be set false when the old keys no longer exist to avoid
+    performance penalty.
+  fmt_desc: To handle the co-existence of both old and new name-by-user keys
+  default: true
+  services:
+  - rgw
+  see_also:
+  - rgw_enable_usage_log
+  with_legacy: true
index 904593b1ea926bddb205879eb9f45f3078302d4c..60134c8e6e375ae97a55efe396b58e43758efc1f 100644 (file)
@@ -1000,48 +1000,159 @@ TEST_F(cls_rgw, gc_defer)
   ASSERT_EQ(0, destroy_one_pool_pp(gc_pool_name, rados));
 }
 
-auto populate_usage_log_info(std::string user, std::string payer, int total_usage_entries)
+auto populate_usage_log_info(std::string user, std::string payer, int total_usage_entries, uint64_t epoch = 0)
 {
   rgw_usage_log_info info;
 
   for (int i=0; i < total_usage_entries; i++){
     auto bucket = str_int("bucket", i);
     info.entries.emplace_back(rgw_usage_log_entry(user, payer, bucket));
+    info.entries.back().epoch = epoch;
   }
 
   return info;
 }
 
-auto gen_usage_log_info(std::string payer, std::string bucket, int total_usage_entries)
+auto gen_usage_log_info(std::string payer, std::string bucket, int total_usage_entries, uint64_t epoch = 0)
 {
   rgw_usage_log_info info;
   for (int i=0; i < total_usage_entries; i++){
     auto user = str_int("user", i);
     info.entries.emplace_back(rgw_usage_log_entry(user, payer, bucket));
+    info.entries.back().epoch = epoch;
   }
 
   return info;
 }
 
+// Copied from cls_rgw.cc in order to populate usage logs with old keys
+static void usage_record_name_by_time(uint64_t epoch, const std::string& user, const std::string& bucket, std::string& key)
+{
+    char buf[32 + user.size() + bucket.size()];
+    snprintf(buf, sizeof(buf), "%011llu_%s_%s", (long long unsigned)epoch, user.c_str(), bucket.c_str());
+    key = buf;
+}
+
+static void usage_record_name_by_user_old(const std::string& user, uint64_t epoch, const std::string& bucket, std::string& key)
+{
+    char buf[32 + user.size() + bucket.size()];
+    snprintf(buf, sizeof(buf), "%s_%011llu_%s", user.c_str(), (long long unsigned)epoch, bucket.c_str());
+    key = buf;
+}
+
+void populate_old_usage_log_info(librados::IoCtx &ioctx,
+                                 std::string& oid,
+                                 std::string& user,
+                                 std::string& payer,
+                                 int total_usage_entries,
+                                 uint64_t epoch)
+{
+  for (int i=0; i < total_usage_entries; ++i) {
+    auto bucket = str_int("bucket", i);
+    rgw_usage_log_entry entry(user, payer, bucket);
+    entry.epoch = epoch;
+    bufferlist bl;
+    encode(entry, bl);
+
+    std::string key_by_time;
+    std::string key_by_user;
+    std::string owner = payer.empty() ? user : payer;
+
+    usage_record_name_by_time(epoch, owner, bucket, key_by_time);
+    usage_record_name_by_user_old(owner, epoch, bucket, key_by_user);
+
+    map<std::string, bufferlist> omap_records{
+      {key_by_time, bl},
+      {key_by_user, bl}
+    };
+
+    librados::ObjectWriteOperation op;
+    op.omap_set(omap_records);
+    ASSERT_EQ(0, ioctx.operate(oid, &op));
+  }
+}
+
+TEST_F(cls_rgw, usage_key_transition)
+{
+  string oid="usage.1";
+  string user="012-345-678";
+  uint64_t an_hour{3600}, start_epoch{0}, log_epoch{1755892800}, end_epoch{log_epoch + an_hour * 3};
+  int total_usage_entries = 8, extra_entries = 4;
+  uint64_t max_entries = 12;
+  string payer;
+
+  // Populate logs with old keys
+  populate_old_usage_log_info(ioctx, oid, user, payer, total_usage_entries, log_epoch);
+  populate_old_usage_log_info(ioctx, oid, user, payer, total_usage_entries, log_epoch + an_hour);
+
+  // Populate logs with new keys with the same epoch as the last old ones. They override the old ones.
+  auto info = populate_usage_log_info(user, payer, total_usage_entries / 2, log_epoch + an_hour);
+  ObjectWriteOperation op;
+  cls_rgw_usage_log_add(op, info);
+  ASSERT_EQ(0, ioctx.operate(oid, &op));
+
+  // Populate logs with new keys with the new epoch
+  info = populate_usage_log_info(user, payer, total_usage_entries + extra_entries, log_epoch + an_hour * 2);
+  ObjectWriteOperation another_op;
+  cls_rgw_usage_log_add(another_op, info);
+  ASSERT_EQ(0, ioctx.operate(oid, &another_op));
+
+  string read_iter;
+  map <rgw_user_bucket, rgw_usage_log_entry> usage, usage2, usage3;
+  bool truncated;
+  int ret = cls_rgw_usage_log_read(ioctx, oid, user, payer, start_epoch, end_epoch, max_entries, read_iter, usage, &truncated);
+  ASSERT_EQ(ret, 0);
+  ASSERT_TRUE(truncated);
+  ASSERT_TRUE(!read_iter.empty() && read_iter.at(0) == '0'); // Old key;
+  ASSERT_EQ(usage.size(), total_usage_entries);
+
+  ret = cls_rgw_usage_log_read(ioctx, oid, user, payer, start_epoch, end_epoch, max_entries, read_iter, usage2, &truncated);
+  ASSERT_EQ(ret, 0);
+  ASSERT_TRUE(truncated); // Still have more left
+  ASSERT_TRUE(!read_iter.empty() && read_iter.at(0) == '~'); // New key;
+  ASSERT_EQ(usage2.size(), total_usage_entries);
+
+  ret = cls_rgw_usage_log_read(ioctx, oid, user, payer, start_epoch, end_epoch, max_entries, read_iter, usage3, &truncated);
+  ASSERT_EQ(ret, 0);
+  ASSERT_FALSE(truncated); // Nothing left
+  ASSERT_TRUE(read_iter.empty()); // Done
+  ASSERT_EQ(usage3.size(), extra_entries);
+
+  ret = cls_rgw_usage_log_trim(ioctx, oid, user, payer, start_epoch, end_epoch);
+  ASSERT_EQ(ret, 0);
+
+  usage.clear();
+  ret = cls_rgw_usage_log_read(ioctx, oid, user, payer, start_epoch, end_epoch, max_entries, read_iter, usage, &truncated);
+  ASSERT_EQ(ret, 0);
+  ASSERT_FALSE(truncated); // Nothing left
+  ASSERT_EQ(usage.size(), 0); // Got nothing
+}
+
 TEST_F(cls_rgw, usage_basic)
 {
   string oid="usage.1";
   string user="user1";
-  uint64_t start_epoch{0}, end_epoch{(uint64_t) -1};
+  uint64_t a_minute{60}, an_hour{3600};
+  uint64_t start_epoch{0}, log_epoch{1755892800}, end_epoch{log_epoch + a_minute};
   int total_usage_entries = 512;
   uint64_t max_entries = 2000;
   string payer;
 
-  auto info = populate_usage_log_info(user, payer, total_usage_entries);
+  auto info = populate_usage_log_info(user, payer, total_usage_entries, log_epoch);
   ObjectWriteOperation op;
   cls_rgw_usage_log_add(op, info);
   ASSERT_EQ(0, ioctx.operate(oid, &op));
 
+  // Slip in some records of a user whose name could cause those records to be among key_by_time ones.
+  auto extra_info = populate_usage_log_info("01234", payer, total_usage_entries, log_epoch + an_hour);
+  ObjectWriteOperation extra_op;
+  cls_rgw_usage_log_add(extra_op, extra_info);
+  ASSERT_EQ(0, ioctx.operate(oid, &extra_op));
+
   string read_iter;
   map <rgw_user_bucket, rgw_usage_log_entry> usage, usage2;
   bool truncated;
 
-
   int ret = cls_rgw_usage_log_read(ioctx, oid, user, "", start_epoch, end_epoch,
                                   max_entries, read_iter, usage, &truncated);
   // read the entries, and see that we have all the added entries
@@ -1050,8 +1161,7 @@ TEST_F(cls_rgw, usage_basic)
   ASSERT_EQ(static_cast<uint64_t>(total_usage_entries), usage.size());
 
   // delete and read to assert that we've deleted all the values
-  ASSERT_EQ(0, cls_rgw_usage_log_trim(ioctx, oid, user, "", start_epoch, end_epoch));
-
+  ASSERT_EQ(0, cls_rgw_usage_log_trim(ioctx, oid, "", "", start_epoch, end_epoch));
 
   ret = cls_rgw_usage_log_read(ioctx, oid, user, "", start_epoch, end_epoch,
                               max_entries, read_iter, usage2, &truncated);