diff options
Diffstat (limited to 'src/test/cls_rgw')
-rw-r--r-- | src/test/cls_rgw/CMakeLists.txt | 24 | ||||
-rw-r--r-- | src/test/cls_rgw/test_cls_rgw.cc | 1342 | ||||
-rw-r--r-- | src/test/cls_rgw/test_cls_rgw_stats.cc | 646 |
3 files changed, 2012 insertions, 0 deletions
diff --git a/src/test/cls_rgw/CMakeLists.txt b/src/test/cls_rgw/CMakeLists.txt new file mode 100644 index 000000000..67b8beb6c --- /dev/null +++ b/src/test/cls_rgw/CMakeLists.txt @@ -0,0 +1,24 @@ +if(${WITH_RADOSGW}) + add_executable(ceph_test_cls_rgw + test_cls_rgw.cc + ) + target_link_libraries(ceph_test_cls_rgw + cls_rgw_client + librados + global + ${UNITTEST_LIBS} + ${EXTRALIBS} + ${BLKID_LIBRARIES} + ${CMAKE_DL_LIBS} + radostest-cxx) + install(TARGETS + ceph_test_cls_rgw + DESTINATION ${CMAKE_INSTALL_BINDIR}) + + add_executable(ceph_test_cls_rgw_stats test_cls_rgw_stats.cc + $<TARGET_OBJECTS:unit-main>) + target_link_libraries(ceph_test_cls_rgw_stats cls_rgw_client global + librados ${UNITTEST_LIBS} radostest-cxx) + install(TARGETS ceph_test_cls_rgw_stats DESTINATION ${CMAKE_INSTALL_BINDIR}) +endif(${WITH_RADOSGW}) + diff --git a/src/test/cls_rgw/test_cls_rgw.cc b/src/test/cls_rgw/test_cls_rgw.cc new file mode 100644 index 000000000..bf60dfdd0 --- /dev/null +++ b/src/test/cls_rgw/test_cls_rgw.cc @@ -0,0 +1,1342 @@ +// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*- +// vim: ts=8 sw=2 smarttab + +#include "include/types.h" +#include "cls/rgw/cls_rgw_client.h" +#include "cls/rgw/cls_rgw_ops.h" + +#include "gtest/gtest.h" +#include "test/librados/test_cxx.h" +#include "global/global_context.h" +#include "common/ceph_context.h" + +#include <errno.h> +#include <string> +#include <vector> +#include <map> +#include <set> + +using namespace std; +using namespace librados; + +// creates a temporary pool and initializes an IoCtx shared by all tests +class cls_rgw : public ::testing::Test { + static librados::Rados rados; + static std::string pool_name; + protected: + static librados::IoCtx ioctx; + + static void SetUpTestCase() { + pool_name = get_temp_pool_name(); + /* create pool */ + ASSERT_EQ("", create_one_pool_pp(pool_name, rados)); + ASSERT_EQ(0, rados.ioctx_create(pool_name.c_str(), ioctx)); + } + static void TearDownTestCase() { + /* remove pool */ + ioctx.close(); + ASSERT_EQ(0, destroy_one_pool_pp(pool_name, rados)); + } +}; +librados::Rados cls_rgw::rados; +std::string cls_rgw::pool_name; +librados::IoCtx cls_rgw::ioctx; + + +string str_int(string s, int i) +{ + char buf[32]; + snprintf(buf, sizeof(buf), "-%d", i); + s.append(buf); + + return s; +} + +void test_stats(librados::IoCtx& ioctx, string& oid, RGWObjCategory category, uint64_t num_entries, uint64_t total_size) +{ + map<int, struct rgw_cls_list_ret> results; + map<int, string> oids; + oids[0] = oid; + ASSERT_EQ(0, CLSRGWIssueGetDirHeader(ioctx, oids, results, 8)()); + + uint64_t entries = 0; + uint64_t size = 0; + map<int, struct rgw_cls_list_ret>::iterator iter = results.begin(); + for (; iter != results.end(); ++iter) { + entries += (iter->second).dir.header.stats[category].num_entries; + size += (iter->second).dir.header.stats[category].total_size; + } + ASSERT_EQ(total_size, size); + ASSERT_EQ(num_entries, entries); +} + +void index_prepare(librados::IoCtx& ioctx, string& oid, RGWModifyOp index_op, + string& tag, const cls_rgw_obj_key& key, string& loc, + uint16_t bi_flags = 0, bool log_op = true) +{ + ObjectWriteOperation op; + rgw_zone_set zones_trace; + cls_rgw_bucket_prepare_op(op, index_op, tag, key, loc, log_op, bi_flags, zones_trace); + ASSERT_EQ(0, ioctx.operate(oid, &op)); +} + +void index_complete(librados::IoCtx& ioctx, string& oid, RGWModifyOp index_op, + string& tag, int epoch, const cls_rgw_obj_key& key, + rgw_bucket_dir_entry_meta& meta, uint16_t bi_flags = 0, + bool log_op = true) +{ + ObjectWriteOperation op; + rgw_bucket_entry_ver ver; + ver.pool = ioctx.get_id(); + ver.epoch = epoch; + meta.accounted_size = meta.size; + cls_rgw_bucket_complete_op(op, index_op, tag, ver, key, meta, nullptr, log_op, bi_flags, nullptr); + ASSERT_EQ(0, ioctx.operate(oid, &op)); + if (!key.instance.empty()) { + bufferlist olh_tag; + olh_tag.append(tag); + rgw_zone_set zone_set; + ASSERT_EQ(0, cls_rgw_bucket_link_olh(ioctx, oid, key, olh_tag, + false, tag, &meta, epoch, + ceph::real_time{}, true, true, zone_set)); + } +} + +TEST_F(cls_rgw, index_basic) +{ + string bucket_oid = str_int("bucket", 0); + + ObjectWriteOperation op; + cls_rgw_bucket_init_index(op); + ASSERT_EQ(0, ioctx.operate(bucket_oid, &op)); + + uint64_t epoch = 1; + + uint64_t obj_size = 1024; + +#define NUM_OBJS 10 + for (int i = 0; i < NUM_OBJS; i++) { + cls_rgw_obj_key obj = str_int("obj", i); + string tag = str_int("tag", i); + string loc = str_int("loc", i); + + index_prepare(ioctx, bucket_oid, CLS_RGW_OP_ADD, tag, obj, loc); + + test_stats(ioctx, bucket_oid, RGWObjCategory::None, i, obj_size * i); + + rgw_bucket_dir_entry_meta meta; + meta.category = RGWObjCategory::None; + meta.size = obj_size; + index_complete(ioctx, bucket_oid, CLS_RGW_OP_ADD, tag, epoch, obj, meta); + } + + test_stats(ioctx, bucket_oid, RGWObjCategory::None, NUM_OBJS, + obj_size * NUM_OBJS); +} + +TEST_F(cls_rgw, index_multiple_obj_writers) +{ + string bucket_oid = str_int("bucket", 1); + + ObjectWriteOperation op; + cls_rgw_bucket_init_index(op); + ASSERT_EQ(0, ioctx.operate(bucket_oid, &op)); + + uint64_t obj_size = 1024; + + cls_rgw_obj_key obj = str_int("obj", 0); + string loc = str_int("loc", 0); + /* multi prepare on a single object */ + for (int i = 0; i < NUM_OBJS; i++) { + string tag = str_int("tag", i); + + index_prepare(ioctx, bucket_oid, CLS_RGW_OP_ADD, tag, obj, loc); + + test_stats(ioctx, bucket_oid, RGWObjCategory::None, 0, 0); + } + + for (int i = NUM_OBJS; i > 0; i--) { + string tag = str_int("tag", i - 1); + + rgw_bucket_dir_entry_meta meta; + meta.category = RGWObjCategory::None; + meta.size = obj_size * i; + + index_complete(ioctx, bucket_oid, CLS_RGW_OP_ADD, tag, i, obj, meta); + + /* verify that object size doesn't change, as we went back with epoch */ + test_stats(ioctx, bucket_oid, RGWObjCategory::None, 1, + obj_size * NUM_OBJS); + } +} + +TEST_F(cls_rgw, index_remove_object) +{ + string bucket_oid = str_int("bucket", 2); + + ObjectWriteOperation op; + cls_rgw_bucket_init_index(op); + ASSERT_EQ(0, ioctx.operate(bucket_oid, &op)); + + uint64_t obj_size = 1024; + uint64_t total_size = 0; + + int epoch = 0; + + /* prepare multiple objects */ + for (int i = 0; i < NUM_OBJS; i++) { + cls_rgw_obj_key obj = str_int("obj", i); + string tag = str_int("tag", i); + string loc = str_int("loc", i); + + index_prepare(ioctx, bucket_oid, CLS_RGW_OP_ADD, tag, obj, loc); + + test_stats(ioctx, bucket_oid, RGWObjCategory::None, i, total_size); + + rgw_bucket_dir_entry_meta meta; + meta.category = RGWObjCategory::None; + meta.size = i * obj_size; + total_size += i * obj_size; + + index_complete(ioctx, bucket_oid, CLS_RGW_OP_ADD, tag, ++epoch, obj, meta); + + test_stats(ioctx, bucket_oid, RGWObjCategory::None, i + 1, total_size); + } + + int i = NUM_OBJS / 2; + string tag_remove = "tag-rm"; + string tag_modify = "tag-mod"; + cls_rgw_obj_key obj = str_int("obj", i); + string loc = str_int("loc", i); + + /* prepare both removal and modification on the same object */ + index_prepare(ioctx, bucket_oid, CLS_RGW_OP_DEL, tag_remove, obj, loc); + index_prepare(ioctx, bucket_oid, CLS_RGW_OP_ADD, tag_modify, obj, loc); + + test_stats(ioctx, bucket_oid, RGWObjCategory::None, NUM_OBJS, total_size); + + rgw_bucket_dir_entry_meta meta; + + /* complete object removal */ + index_complete(ioctx, bucket_oid, CLS_RGW_OP_DEL, tag_remove, ++epoch, obj, meta); + + /* verify stats correct */ + total_size -= i * obj_size; + test_stats(ioctx, bucket_oid, RGWObjCategory::None, NUM_OBJS - 1, total_size); + + meta.size = 512; + meta.category = RGWObjCategory::None; + + /* complete object modification */ + index_complete(ioctx, bucket_oid, CLS_RGW_OP_ADD, tag_modify, ++epoch, obj, meta); + + /* verify stats correct */ + total_size += meta.size; + test_stats(ioctx, bucket_oid, RGWObjCategory::None, NUM_OBJS, total_size); + + + /* prepare both removal and modification on the same object, this time we'll + * first complete modification then remove*/ + index_prepare(ioctx, bucket_oid, CLS_RGW_OP_DEL, tag_remove, obj, loc); + index_prepare(ioctx, bucket_oid, CLS_RGW_OP_DEL, tag_modify, obj, loc); + + /* complete modification */ + total_size -= meta.size; + meta.size = i * obj_size * 2; + meta.category = RGWObjCategory::None; + + /* complete object modification */ + index_complete(ioctx, bucket_oid, CLS_RGW_OP_ADD, tag_modify, ++epoch, obj, meta); + + /* verify stats correct */ + total_size += meta.size; + test_stats(ioctx, bucket_oid, RGWObjCategory::None, NUM_OBJS, total_size); + + /* complete object removal */ + index_complete(ioctx, bucket_oid, CLS_RGW_OP_DEL, tag_remove, ++epoch, obj, meta); + + /* verify stats correct */ + total_size -= meta.size; + test_stats(ioctx, bucket_oid, RGWObjCategory::None, NUM_OBJS - 1, + total_size); +} + +TEST_F(cls_rgw, index_suggest) +{ + string bucket_oid = str_int("suggest", 1); + { + ObjectWriteOperation op; + cls_rgw_bucket_init_index(op); + ASSERT_EQ(0, ioctx.operate(bucket_oid, &op)); + } + uint64_t total_size = 0; + + int epoch = 0; + + int num_objs = 100; + + uint64_t obj_size = 1024; + + /* create multiple objects */ + for (int i = 0; i < num_objs; i++) { + cls_rgw_obj_key obj = str_int("obj", i); + string tag = str_int("tag", i); + string loc = str_int("loc", i); + + index_prepare(ioctx, bucket_oid, CLS_RGW_OP_ADD, tag, obj, loc); + + test_stats(ioctx, bucket_oid, RGWObjCategory::None, i, total_size); + + rgw_bucket_dir_entry_meta meta; + meta.category = RGWObjCategory::None; + meta.size = obj_size; + total_size += meta.size; + + index_complete(ioctx, bucket_oid, CLS_RGW_OP_ADD, tag, ++epoch, obj, meta); + + test_stats(ioctx, bucket_oid, RGWObjCategory::None, i + 1, total_size); + } + + /* prepare (without completion) some of the objects */ + for (int i = 0; i < num_objs; i += 2) { + cls_rgw_obj_key obj = str_int("obj", i); + string tag = str_int("tag-prepare", i); + string loc = str_int("loc", i); + + index_prepare(ioctx, bucket_oid, CLS_RGW_OP_ADD, tag, obj, loc); + + test_stats(ioctx, bucket_oid, RGWObjCategory::None, num_objs, total_size); + } + + int actual_num_objs = num_objs; + /* remove half of the objects */ + for (int i = num_objs / 2; i < num_objs; i++) { + cls_rgw_obj_key obj = str_int("obj", i); + string tag = str_int("tag-rm", i); + string loc = str_int("loc", i); + + index_prepare(ioctx, bucket_oid, CLS_RGW_OP_ADD, tag, obj, loc); + + test_stats(ioctx, bucket_oid, RGWObjCategory::None, actual_num_objs, total_size); + + rgw_bucket_dir_entry_meta meta; + index_complete(ioctx, bucket_oid, CLS_RGW_OP_DEL, tag, ++epoch, obj, meta); + + total_size -= obj_size; + actual_num_objs--; + test_stats(ioctx, bucket_oid, RGWObjCategory::None, actual_num_objs, total_size); + } + + bufferlist updates; + + for (int i = 0; i < num_objs; i += 2) { + cls_rgw_obj_key obj = str_int("obj", i); + string tag = str_int("tag-rm", i); + string loc = str_int("loc", i); + + rgw_bucket_dir_entry dirent; + dirent.key.name = obj.name; + dirent.locator = loc; + dirent.exists = (i < num_objs / 2); // we removed half the objects + dirent.meta.size = 1024; + dirent.meta.accounted_size = 1024; + + char suggest_op = (i < num_objs / 2 ? CEPH_RGW_UPDATE : CEPH_RGW_REMOVE); + cls_rgw_encode_suggestion(suggest_op, dirent, updates); + } + + map<int, string> bucket_objs; + bucket_objs[0] = bucket_oid; + int r = CLSRGWIssueSetTagTimeout(ioctx, bucket_objs, 8 /* max aio */, 1)(); + ASSERT_EQ(0, r); + + sleep(1); + + /* suggest changes! */ + { + ObjectWriteOperation op; + cls_rgw_suggest_changes(op, updates); + ASSERT_EQ(0, ioctx.operate(bucket_oid, &op)); + } + /* suggest changes twice! */ + { + ObjectWriteOperation op; + cls_rgw_suggest_changes(op, updates); + ASSERT_EQ(0, ioctx.operate(bucket_oid, &op)); + } + test_stats(ioctx, bucket_oid, RGWObjCategory::None, num_objs / 2, total_size); +} + +static void list_entries(librados::IoCtx& ioctx, + const std::string& oid, + uint32_t num_entries, + std::map<int, rgw_cls_list_ret>& results) +{ + std::map<int, std::string> oids = { {0, oid} }; + cls_rgw_obj_key start_key; + string empty_prefix; + string empty_delimiter; + ASSERT_EQ(0, CLSRGWIssueBucketList(ioctx, start_key, empty_prefix, + empty_delimiter, num_entries, + true, oids, results, 1)()); +} + +TEST_F(cls_rgw, index_suggest_complete) +{ + string bucket_oid = str_int("suggest", 2); + { + ObjectWriteOperation op; + cls_rgw_bucket_init_index(op); + ASSERT_EQ(0, ioctx.operate(bucket_oid, &op)); + } + + cls_rgw_obj_key obj = str_int("obj", 0); + string tag = str_int("tag-prepare", 0); + string loc = str_int("loc", 0); + + // prepare entry + index_prepare(ioctx, bucket_oid, CLS_RGW_OP_ADD, tag, obj, loc); + + // list entry before completion + rgw_bucket_dir_entry dirent; + { + std::map<int, rgw_cls_list_ret> listing; + list_entries(ioctx, bucket_oid, 1, listing); + ASSERT_EQ(1, listing.size()); + const auto& entries = listing.begin()->second.dir.m; + ASSERT_EQ(1, entries.size()); + dirent = entries.begin()->second; + ASSERT_EQ(obj, dirent.key); + } + // complete entry + { + rgw_bucket_dir_entry_meta meta; + index_complete(ioctx, bucket_oid, CLS_RGW_OP_ADD, tag, 1, obj, meta); + } + // suggest removal of listed entry + { + bufferlist updates; + cls_rgw_encode_suggestion(CEPH_RGW_REMOVE, dirent, updates); + + ObjectWriteOperation op; + cls_rgw_suggest_changes(op, updates); + ASSERT_EQ(0, ioctx.operate(bucket_oid, &op)); + } + // list entry again, verify that suggested removal was not applied + { + std::map<int, rgw_cls_list_ret> listing; + list_entries(ioctx, bucket_oid, 1, listing); + ASSERT_EQ(1, listing.size()); + const auto& entries = listing.begin()->second.dir.m; + ASSERT_EQ(1, entries.size()); + EXPECT_TRUE(entries.begin()->second.exists); + } +} + +/* + * This case is used to test whether get_obj_vals will + * return all validate utf8 objnames and filter out those + * in BI_PREFIX_CHAR private namespace. + */ +TEST_F(cls_rgw, index_list) +{ + string bucket_oid = str_int("bucket", 4); + + ObjectWriteOperation op; + cls_rgw_bucket_init_index(op); + ASSERT_EQ(0, ioctx.operate(bucket_oid, &op)); + + uint64_t epoch = 1; + uint64_t obj_size = 1024; + const int num_objs = 4; + const string keys[num_objs] = { + /* single byte utf8 character */ + { static_cast<char>(0x41) }, + /* double byte utf8 character */ + { static_cast<char>(0xCF), static_cast<char>(0x8F) }, + /* treble byte utf8 character */ + { static_cast<char>(0xDF), static_cast<char>(0x8F), static_cast<char>(0x8F) }, + /* quadruble byte utf8 character */ + { static_cast<char>(0xF7), static_cast<char>(0x8F), static_cast<char>(0x8F), static_cast<char>(0x8F) }, + }; + + for (int i = 0; i < num_objs; i++) { + string obj = keys[i]; + string tag = str_int("tag", i); + string loc = str_int("loc", i); + + index_prepare(ioctx, bucket_oid, CLS_RGW_OP_ADD, tag, obj, loc, + 0 /* bi_flags */, false /* log_op */); + + rgw_bucket_dir_entry_meta meta; + meta.category = RGWObjCategory::None; + meta.size = obj_size; + index_complete(ioctx, bucket_oid, CLS_RGW_OP_ADD, tag, epoch, obj, meta, + 0 /* bi_flags */, false /* log_op */); + } + + map<string, bufferlist> entries; + /* insert 998 omap key starts with BI_PREFIX_CHAR, + * so bucket list first time will get one key before 0x80 and one key after */ + for (int i = 0; i < 998; ++i) { + char buf[10]; + snprintf(buf, sizeof(buf), "%c%s%d", 0x80, "1000_", i); + entries.emplace(string{buf}, bufferlist{}); + } + ioctx.omap_set(bucket_oid, entries); + + test_stats(ioctx, bucket_oid, RGWObjCategory::None, + num_objs, obj_size * num_objs); + + map<int, string> oids = { {0, bucket_oid} }; + map<int, struct rgw_cls_list_ret> list_results; + cls_rgw_obj_key start_key("", ""); + string empty_prefix; + string empty_delimiter; + int r = CLSRGWIssueBucketList(ioctx, start_key, + empty_prefix, empty_delimiter, + 1000, true, oids, list_results, 1)(); + ASSERT_EQ(r, 0); + ASSERT_EQ(1u, list_results.size()); + + auto it = list_results.begin(); + auto m = (it->second).dir.m; + + ASSERT_EQ(4u, m.size()); + int i = 0; + for(auto it2 = m.cbegin(); it2 != m.cend(); it2++, i++) { + ASSERT_EQ(it2->first.compare(keys[i]), 0); + } +} + + +/* + * This case is used to test when bucket index list that includes a + * delimiter can handle the first chunk ending in a delimiter. + */ +TEST_F(cls_rgw, index_list_delimited) +{ + string bucket_oid = str_int("bucket", 7); + + ObjectWriteOperation op; + cls_rgw_bucket_init_index(op); + ASSERT_EQ(0, ioctx.operate(bucket_oid, &op)); + + uint64_t epoch = 1; + uint64_t obj_size = 1024; + const int file_num_objs = 5; + const int dir_num_objs = 1005; + + std::vector<std::string> file_prefixes = + { "a", "c", "e", "g", "i", "k", "m", "o", "q", "s", "u" }; + std::vector<std::string> dir_prefixes = + { "b/", "d/", "f/", "h/", "j/", "l/", "n/", "p/", "r/", "t/" }; + + rgw_bucket_dir_entry_meta meta; + meta.category = RGWObjCategory::None; + meta.size = obj_size; + + // create top-level files + for (const auto& p : file_prefixes) { + for (int i = 0; i < file_num_objs; i++) { + string tag = str_int("tag", i); + string loc = str_int("loc", i); + const string obj = str_int(p, i); + + index_prepare(ioctx, bucket_oid, CLS_RGW_OP_ADD, tag, obj, loc, + 0 /* bi_flags */, false /* log_op */); + + index_complete(ioctx, bucket_oid, CLS_RGW_OP_ADD, tag, epoch, obj, meta, + 0 /* bi_flags */, false /* log_op */); + } + } + + // create large directories + for (const auto& p : dir_prefixes) { + for (int i = 0; i < dir_num_objs; i++) { + string tag = str_int("tag", i); + string loc = str_int("loc", i); + const string obj = p + str_int("f", i); + + index_prepare(ioctx, bucket_oid, CLS_RGW_OP_ADD, tag, obj, loc, + 0 /* bi_flags */, false /* log_op */); + + index_complete(ioctx, bucket_oid, CLS_RGW_OP_ADD, tag, epoch, obj, meta, + 0 /* bi_flags */, false /* log_op */); + } + } + + map<int, string> oids = { {0, bucket_oid} }; + map<int, struct rgw_cls_list_ret> list_results; + cls_rgw_obj_key start_key("", ""); + const string empty_prefix; + const string delimiter = "/"; + int r = CLSRGWIssueBucketList(ioctx, start_key, + empty_prefix, delimiter, + 1000, true, oids, list_results, 1)(); + ASSERT_EQ(r, 0); + ASSERT_EQ(1u, list_results.size()) << + "Because we only have one bucket index shard, we should " + "only get one list_result."; + + auto it = list_results.begin(); + auto id_entry_map = it->second.dir.m; + bool truncated = it->second.is_truncated; + + // the cls code will make 4 tries to get 1000 entries; however + // because each of the subdirectories is so large, each attempt will + // only retrieve the first part of the subdirectory + + ASSERT_EQ(48u, id_entry_map.size()) << + "We should get 40 top-level entries and the tops of 8 \"subdirectories\"."; + ASSERT_EQ(true, truncated) << "We did not get all entries."; + + ASSERT_EQ("a-0", id_entry_map.cbegin()->first); + ASSERT_EQ("p/", id_entry_map.crbegin()->first); + + // now let's get the rest of the entries + + list_results.clear(); + + cls_rgw_obj_key start_key2("p/", ""); + r = CLSRGWIssueBucketList(ioctx, start_key2, + empty_prefix, delimiter, + 1000, true, oids, list_results, 1)(); + ASSERT_EQ(r, 0); + + it = list_results.begin(); + id_entry_map = it->second.dir.m; + truncated = it->second.is_truncated; + + ASSERT_EQ(17u, id_entry_map.size()) << + "We should get 15 top-level entries and the tops of 2 \"subdirectories\"."; + ASSERT_EQ(false, truncated) << "We now have all entries."; + + ASSERT_EQ("q-0", id_entry_map.cbegin()->first); + ASSERT_EQ("u-4", id_entry_map.crbegin()->first); +} + + +TEST_F(cls_rgw, bi_list) +{ + string bucket_oid = str_int("bucket", 5); + + CephContext *cct = reinterpret_cast<CephContext *>(ioctx.cct()); + + ObjectWriteOperation op; + cls_rgw_bucket_init_index(op); + ASSERT_EQ(0, ioctx.operate(bucket_oid, &op)); + + const std::string empty_name_filter; + uint64_t max = 10; + std::list<rgw_cls_bi_entry> entries; + bool is_truncated; + std::string marker; + + int ret = cls_rgw_bi_list(ioctx, bucket_oid, empty_name_filter, marker, max, + &entries, &is_truncated); + ASSERT_EQ(ret, 0); + ASSERT_EQ(entries.size(), 0u) << + "The listing of an empty bucket as 0 entries."; + ASSERT_EQ(is_truncated, false) << + "The listing of an empty bucket is not truncated."; + + uint64_t epoch = 1; + uint64_t obj_size = 1024; + const uint64_t num_objs = 35; + + for (uint64_t i = 0; i < num_objs; i++) { + string obj = str_int(i % 4 ? "obj" : "об'єкт", i); + string tag = str_int("tag", i); + string loc = str_int("loc", i); + index_prepare(ioctx, bucket_oid, CLS_RGW_OP_ADD, tag, obj, loc, + RGW_BILOG_FLAG_VERSIONED_OP); + + rgw_bucket_dir_entry_meta meta; + meta.category = RGWObjCategory::None; + meta.size = obj_size; + index_complete(ioctx, bucket_oid, CLS_RGW_OP_ADD, tag, epoch, obj, meta, + RGW_BILOG_FLAG_VERSIONED_OP); + } + + ret = cls_rgw_bi_list(ioctx, bucket_oid, empty_name_filter, marker, num_objs + 10, + &entries, &is_truncated); + ASSERT_EQ(ret, 0); + if (is_truncated) { + ASSERT_LT(entries.size(), num_objs); + } else { + ASSERT_EQ(entries.size(), num_objs); + } + + uint64_t num_entries = 0; + + is_truncated = true; + marker.clear(); + while(is_truncated) { + ret = cls_rgw_bi_list(ioctx, bucket_oid, empty_name_filter, marker, max, + &entries, &is_truncated); + ASSERT_EQ(ret, 0); + if (is_truncated) { + ASSERT_LT(entries.size(), num_objs - num_entries); + } else { + ASSERT_EQ(entries.size(), num_objs - num_entries); + } + num_entries += entries.size(); + marker = entries.back().idx; + } + + // try with marker as final entry + ret = cls_rgw_bi_list(ioctx, bucket_oid, empty_name_filter, marker, max, + &entries, &is_truncated); + ASSERT_EQ(ret, 0); + ASSERT_EQ(entries.size(), 0u); + ASSERT_EQ(is_truncated, false); + + if (cct->_conf->osd_max_omap_entries_per_request < 15) { + num_entries = 0; + max = 15; + is_truncated = true; + marker.clear(); + while(is_truncated) { + ret = cls_rgw_bi_list(ioctx, bucket_oid, empty_name_filter, marker, max, + &entries, &is_truncated); + ASSERT_EQ(ret, 0); + if (is_truncated) { + ASSERT_LT(entries.size(), num_objs - num_entries); + } else { + ASSERT_EQ(entries.size(), num_objs - num_entries); + } + num_entries += entries.size(); + marker = entries.back().idx; + } + + // try with marker as final entry + ret = cls_rgw_bi_list(ioctx, bucket_oid, empty_name_filter, marker, max, + &entries, &is_truncated); + ASSERT_EQ(ret, 0); + ASSERT_EQ(entries.size(), 0u); + ASSERT_EQ(is_truncated, false); + } + + // test with name filters; pairs contain filter and expected number of elements returned + const std::list<std::pair<const std::string,unsigned>> filters_results = + { { str_int("obj", 9), 1 }, + { str_int("об'єкт", 8), 1 }, + { str_int("obj", 8), 0 } }; + for (const auto& filter_result : filters_results) { + is_truncated = true; + entries.clear(); + marker.clear(); + + ret = cls_rgw_bi_list(ioctx, bucket_oid, filter_result.first, marker, max, + &entries, &is_truncated); + + ASSERT_EQ(ret, 0) << "bi list test with name filters should succeed"; + ASSERT_EQ(entries.size(), filter_result.second) << + "bi list test with filters should return the correct number of results"; + ASSERT_EQ(is_truncated, false) << + "bi list test with filters should return correct truncation indicator"; + } + + // test whether combined segment count is correcgt + is_truncated = false; + entries.clear(); + marker.clear(); + + ret = cls_rgw_bi_list(ioctx, bucket_oid, empty_name_filter, marker, num_objs - 1, + &entries, &is_truncated); + ASSERT_EQ(ret, 0) << "combined segment count should succeed"; + ASSERT_EQ(entries.size(), num_objs - 1) << + "combined segment count should return the correct number of results"; + ASSERT_EQ(is_truncated, true) << + "combined segment count should return correct truncation indicator"; + + + marker = entries.back().idx; // advance marker + ret = cls_rgw_bi_list(ioctx, bucket_oid, empty_name_filter, marker, num_objs - 1, + &entries, &is_truncated); + ASSERT_EQ(ret, 0) << "combined segment count should succeed"; + ASSERT_EQ(entries.size(), 1) << + "combined segment count should return the correct number of results"; + ASSERT_EQ(is_truncated, false) << + "combined segment count should return correct truncation indicator"; +} + +/* test garbage collection */ +static void create_obj(cls_rgw_obj& obj, int i, int j) +{ + char buf[32]; + snprintf(buf, sizeof(buf), "-%d.%d", i, j); + obj.pool = "pool"; + obj.pool.append(buf); + obj.key.name = "oid"; + obj.key.name.append(buf); + obj.loc = "loc"; + obj.loc.append(buf); +} + +static bool cmp_objs(cls_rgw_obj& obj1, cls_rgw_obj& obj2) +{ + return (obj1.pool == obj2.pool) && + (obj1.key == obj2.key) && + (obj1.loc == obj2.loc); +} + + +TEST_F(cls_rgw, gc_set) +{ + /* add chains */ + string oid = "obj"; + for (int i = 0; i < 10; i++) { + char buf[32]; + snprintf(buf, sizeof(buf), "chain-%d", i); + string tag = buf; + librados::ObjectWriteOperation op; + cls_rgw_gc_obj_info info; + + cls_rgw_obj obj1, obj2; + create_obj(obj1, i, 1); + create_obj(obj2, i, 2); + info.chain.objs.push_back(obj1); + info.chain.objs.push_back(obj2); + + op.create(false); // create object + + info.tag = tag; + cls_rgw_gc_set_entry(op, 0, info); + + ASSERT_EQ(0, ioctx.operate(oid, &op)); + } + + bool truncated; + list<cls_rgw_gc_obj_info> entries; + string marker; + string next_marker; + + /* list chains, verify truncated */ + ASSERT_EQ(0, cls_rgw_gc_list(ioctx, oid, marker, 8, true, entries, &truncated, next_marker)); + ASSERT_EQ(8, (int)entries.size()); + ASSERT_EQ(1, truncated); + + entries.clear(); + next_marker.clear(); + + /* list all chains, verify not truncated */ + ASSERT_EQ(0, cls_rgw_gc_list(ioctx, oid, marker, 10, true, entries, &truncated, next_marker)); + ASSERT_EQ(10, (int)entries.size()); + ASSERT_EQ(0, truncated); + + /* verify all chains are valid */ + list<cls_rgw_gc_obj_info>::iterator iter = entries.begin(); + for (int i = 0; i < 10; i++, ++iter) { + cls_rgw_gc_obj_info& entry = *iter; + + /* create expected chain name */ + char buf[32]; + snprintf(buf, sizeof(buf), "chain-%d", i); + string tag = buf; + + /* verify chain name as expected */ + ASSERT_EQ(entry.tag, tag); + + /* verify expected num of objects in chain */ + ASSERT_EQ(2, (int)entry.chain.objs.size()); + + list<cls_rgw_obj>::iterator oiter = entry.chain.objs.begin(); + cls_rgw_obj obj1, obj2; + + /* create expected objects */ + create_obj(obj1, i, 1); + create_obj(obj2, i, 2); + + /* assign returned object names */ + cls_rgw_obj& ret_obj1 = *oiter++; + cls_rgw_obj& ret_obj2 = *oiter; + + /* verify objects are as expected */ + ASSERT_EQ(1, (int)cmp_objs(obj1, ret_obj1)); + ASSERT_EQ(1, (int)cmp_objs(obj2, ret_obj2)); + } +} + +TEST_F(cls_rgw, gc_list) +{ + /* add chains */ + string oid = "obj"; + for (int i = 0; i < 10; i++) { + char buf[32]; + snprintf(buf, sizeof(buf), "chain-%d", i); + string tag = buf; + librados::ObjectWriteOperation op; + cls_rgw_gc_obj_info info; + + cls_rgw_obj obj1, obj2; + create_obj(obj1, i, 1); + create_obj(obj2, i, 2); + info.chain.objs.push_back(obj1); + info.chain.objs.push_back(obj2); + + op.create(false); // create object + + info.tag = tag; + cls_rgw_gc_set_entry(op, 0, info); + + ASSERT_EQ(0, ioctx.operate(oid, &op)); + } + + bool truncated; + list<cls_rgw_gc_obj_info> entries; + list<cls_rgw_gc_obj_info> entries2; + string marker; + string next_marker; + + /* list chains, verify truncated */ + ASSERT_EQ(0, cls_rgw_gc_list(ioctx, oid, marker, 8, true, entries, &truncated, next_marker)); + ASSERT_EQ(8, (int)entries.size()); + ASSERT_EQ(1, truncated); + + marker = next_marker; + next_marker.clear(); + + ASSERT_EQ(0, cls_rgw_gc_list(ioctx, oid, marker, 8, true, entries2, &truncated, next_marker)); + ASSERT_EQ(2, (int)entries2.size()); + ASSERT_EQ(0, truncated); + + entries.splice(entries.end(), entries2); + + /* verify all chains are valid */ + list<cls_rgw_gc_obj_info>::iterator iter = entries.begin(); + for (int i = 0; i < 10; i++, ++iter) { + cls_rgw_gc_obj_info& entry = *iter; + + /* create expected chain name */ + char buf[32]; + snprintf(buf, sizeof(buf), "chain-%d", i); + string tag = buf; + + /* verify chain name as expected */ + ASSERT_EQ(entry.tag, tag); + + /* verify expected num of objects in chain */ + ASSERT_EQ(2, (int)entry.chain.objs.size()); + + list<cls_rgw_obj>::iterator oiter = entry.chain.objs.begin(); + cls_rgw_obj obj1, obj2; + + /* create expected objects */ + create_obj(obj1, i, 1); + create_obj(obj2, i, 2); + + /* assign returned object names */ + cls_rgw_obj& ret_obj1 = *oiter++; + cls_rgw_obj& ret_obj2 = *oiter; + + /* verify objects are as expected */ + ASSERT_EQ(1, (int)cmp_objs(obj1, ret_obj1)); + ASSERT_EQ(1, (int)cmp_objs(obj2, ret_obj2)); + } +} + +TEST_F(cls_rgw, gc_defer) +{ + librados::IoCtx ioctx; + librados::Rados rados; + + string gc_pool_name = get_temp_pool_name(); + /* create pool */ + ASSERT_EQ("", create_one_pool_pp(gc_pool_name, rados)); + ASSERT_EQ(0, rados.ioctx_create(gc_pool_name.c_str(), ioctx)); + + string oid = "obj"; + string tag = "mychain"; + + librados::ObjectWriteOperation op; + cls_rgw_gc_obj_info info; + + op.create(false); + + info.tag = tag; + + /* create chain */ + cls_rgw_gc_set_entry(op, 0, info); + + ASSERT_EQ(0, ioctx.operate(oid, &op)); + + bool truncated; + list<cls_rgw_gc_obj_info> entries; + string marker; + string next_marker; + + /* list chains, verify num entries as expected */ + ASSERT_EQ(0, cls_rgw_gc_list(ioctx, oid, marker, 1, true, entries, &truncated, next_marker)); + ASSERT_EQ(1, (int)entries.size()); + ASSERT_EQ(0, truncated); + + librados::ObjectWriteOperation op2; + + /* defer chain */ + cls_rgw_gc_defer_entry(op2, 5, tag); + ASSERT_EQ(0, ioctx.operate(oid, &op2)); + + entries.clear(); + next_marker.clear(); + + /* verify list doesn't show deferred entry (this may fail if cluster is thrashing) */ + ASSERT_EQ(0, cls_rgw_gc_list(ioctx, oid, marker, 1, true, entries, &truncated, next_marker)); + ASSERT_EQ(0, (int)entries.size()); + ASSERT_EQ(0, truncated); + + /* wait enough */ + sleep(5); + next_marker.clear(); + + /* verify list shows deferred entry */ + ASSERT_EQ(0, cls_rgw_gc_list(ioctx, oid, marker, 1, true, entries, &truncated, next_marker)); + ASSERT_EQ(1, (int)entries.size()); + ASSERT_EQ(0, truncated); + + librados::ObjectWriteOperation op3; + vector<string> tags; + tags.push_back(tag); + + /* remove chain */ + cls_rgw_gc_remove(op3, tags); + ASSERT_EQ(0, ioctx.operate(oid, &op3)); + + entries.clear(); + next_marker.clear(); + + /* verify entry was removed */ + ASSERT_EQ(0, cls_rgw_gc_list(ioctx, oid, marker, 1, true, entries, &truncated, next_marker)); + ASSERT_EQ(0, (int)entries.size()); + ASSERT_EQ(0, truncated); + + /* remove pool */ + ioctx.close(); + 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) +{ + 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)); + } + + return info; +} + +auto gen_usage_log_info(std::string payer, std::string bucket, int total_usage_entries) +{ + 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)); + } + + return info; +} + +TEST_F(cls_rgw, usage_basic) +{ + string oid="usage.1"; + string user="user1"; + uint64_t start_epoch{0}, end_epoch{(uint64_t) -1}; + int total_usage_entries = 512; + uint64_t max_entries = 2000; + string payer; + + auto info = populate_usage_log_info(user, payer, total_usage_entries); + ObjectWriteOperation op; + cls_rgw_usage_log_add(op, info); + ASSERT_EQ(0, ioctx.operate(oid, &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 + ASSERT_EQ(0, ret); + ASSERT_FALSE(truncated); + 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)); + + + ret = cls_rgw_usage_log_read(ioctx, oid, user, "", start_epoch, end_epoch, + max_entries, read_iter, usage2, &truncated); + ASSERT_EQ(0, ret); + ASSERT_EQ(0u, usage2.size()); + + // add and read to assert that bucket option is valid for usage reading + string bucket1 = "bucket-usage-1"; + string bucket2 = "bucket-usage-2"; + info = gen_usage_log_info(payer, bucket1, 100); + cls_rgw_usage_log_add(op, info); + ASSERT_EQ(0, ioctx.operate(oid, &op)); + + info = gen_usage_log_info(payer, bucket2, 100); + cls_rgw_usage_log_add(op, info); + ASSERT_EQ(0, ioctx.operate(oid, &op)); + ret = cls_rgw_usage_log_read(ioctx, oid, "", bucket1, start_epoch, end_epoch, + max_entries, read_iter, usage2, &truncated); + ASSERT_EQ(0, ret); + ASSERT_EQ(100u, usage2.size()); + + // delete and read to assert that bucket option is valid for usage trim + ASSERT_EQ(0, cls_rgw_usage_log_trim(ioctx, oid, "", bucket1, start_epoch, end_epoch)); + + ret = cls_rgw_usage_log_read(ioctx, oid, "", bucket1, start_epoch, end_epoch, + max_entries, read_iter, usage2, &truncated); + ASSERT_EQ(0, ret); + ASSERT_EQ(0u, usage2.size()); + ASSERT_EQ(0, cls_rgw_usage_log_trim(ioctx, oid, "", bucket2, start_epoch, end_epoch)); +} + +TEST_F(cls_rgw, usage_clear_no_obj) +{ + string user="user1"; + string oid="usage.10"; + librados::ObjectWriteOperation op; + cls_rgw_usage_log_clear(op); + int ret = ioctx.operate(oid, &op); + ASSERT_EQ(0, ret); + +} + +TEST_F(cls_rgw, usage_clear) +{ + string user="user1"; + string payer; + string oid="usage.10"; + librados::ObjectWriteOperation op; + int max_entries=2000; + + auto info = populate_usage_log_info(user, payer, max_entries); + + cls_rgw_usage_log_add(op, info); + ASSERT_EQ(0, ioctx.operate(oid, &op)); + + ObjectWriteOperation op2; + cls_rgw_usage_log_clear(op2); + int ret = ioctx.operate(oid, &op2); + ASSERT_EQ(0, ret); + + map <rgw_user_bucket, rgw_usage_log_entry> usage; + bool truncated; + uint64_t start_epoch{0}, end_epoch{(uint64_t) -1}; + string read_iter; + ret = cls_rgw_usage_log_read(ioctx, oid, user, "", start_epoch, end_epoch, + max_entries, read_iter, usage, &truncated); + ASSERT_EQ(0, ret); + ASSERT_EQ(0u, usage.size()); +} + +static int bilog_list(librados::IoCtx& ioctx, const std::string& oid, + cls_rgw_bi_log_list_ret *result) +{ + int retcode = 0; + librados::ObjectReadOperation op; + cls_rgw_bilog_list(op, "", 128, result, &retcode); + int ret = ioctx.operate(oid, &op, nullptr); + if (ret < 0) { + return ret; + } + return retcode; +} + +static int bilog_trim(librados::IoCtx& ioctx, const std::string& oid, + const std::string& start_marker, + const std::string& end_marker) +{ + librados::ObjectWriteOperation op; + cls_rgw_bilog_trim(op, start_marker, end_marker); + return ioctx.operate(oid, &op); +} + +TEST_F(cls_rgw, bi_log_trim) +{ + string bucket_oid = str_int("bucket", 6); + + ObjectWriteOperation op; + cls_rgw_bucket_init_index(op); + ASSERT_EQ(0, ioctx.operate(bucket_oid, &op)); + + // create 10 versioned entries. this generates instance and olh bi entries, + // allowing us to check that bilog trim doesn't remove any of those + for (int i = 0; i < 10; i++) { + cls_rgw_obj_key obj{str_int("obj", i), "inst"}; + string tag = str_int("tag", i); + string loc = str_int("loc", i); + + index_prepare(ioctx, bucket_oid, CLS_RGW_OP_ADD, tag, obj, loc); + rgw_bucket_dir_entry_meta meta; + index_complete(ioctx, bucket_oid, CLS_RGW_OP_ADD, tag, 1, obj, meta); + } + // bi list + { + list<rgw_cls_bi_entry> entries; + bool truncated{false}; + ASSERT_EQ(0, cls_rgw_bi_list(ioctx, bucket_oid, "", "", 128, + &entries, &truncated)); + // prepare/complete/instance/olh entry for each + EXPECT_EQ(40u, entries.size()); + EXPECT_FALSE(truncated); + } + // bilog list + vector<rgw_bi_log_entry> bilog1; + { + cls_rgw_bi_log_list_ret bilog; + ASSERT_EQ(0, bilog_list(ioctx, bucket_oid, &bilog)); + // complete/olh entry for each + EXPECT_EQ(20u, bilog.entries.size()); + + bilog1.assign(std::make_move_iterator(bilog.entries.begin()), + std::make_move_iterator(bilog.entries.end())); + } + // trim front of bilog + { + const std::string from = ""; + const std::string to = bilog1[0].id; + ASSERT_EQ(0, bilog_trim(ioctx, bucket_oid, from, to)); + cls_rgw_bi_log_list_ret bilog; + ASSERT_EQ(0, bilog_list(ioctx, bucket_oid, &bilog)); + EXPECT_EQ(19u, bilog.entries.size()); + EXPECT_EQ(bilog1[1].id, bilog.entries.begin()->id); + ASSERT_EQ(-ENODATA, bilog_trim(ioctx, bucket_oid, from, to)); + } + // trim back of bilog + { + const std::string from = bilog1[18].id; + const std::string to = "9"; + ASSERT_EQ(0, bilog_trim(ioctx, bucket_oid, from, to)); + cls_rgw_bi_log_list_ret bilog; + ASSERT_EQ(0, bilog_list(ioctx, bucket_oid, &bilog)); + EXPECT_EQ(18u, bilog.entries.size()); + EXPECT_EQ(bilog1[18].id, bilog.entries.rbegin()->id); + ASSERT_EQ(-ENODATA, bilog_trim(ioctx, bucket_oid, from, to)); + } + // trim middle of bilog + { + const std::string from = bilog1[13].id; + const std::string to = bilog1[14].id; + ASSERT_EQ(0, bilog_trim(ioctx, bucket_oid, from, to)); + cls_rgw_bi_log_list_ret bilog; + ASSERT_EQ(0, bilog_list(ioctx, bucket_oid, &bilog)); + EXPECT_EQ(17u, bilog.entries.size()); + ASSERT_EQ(-ENODATA, bilog_trim(ioctx, bucket_oid, from, to)); + } + // trim full bilog + { + const std::string from = ""; + const std::string to = "9"; + ASSERT_EQ(0, bilog_trim(ioctx, bucket_oid, from, to)); + cls_rgw_bi_log_list_ret bilog; + ASSERT_EQ(0, bilog_list(ioctx, bucket_oid, &bilog)); + EXPECT_EQ(0u, bilog.entries.size()); + ASSERT_EQ(-ENODATA, bilog_trim(ioctx, bucket_oid, from, to)); + } + // bi list should be the same + { + list<rgw_cls_bi_entry> entries; + bool truncated{false}; + ASSERT_EQ(0, cls_rgw_bi_list(ioctx, bucket_oid, "", "", 128, + &entries, &truncated)); + EXPECT_EQ(40u, entries.size()); + EXPECT_FALSE(truncated); + } +} + +TEST_F(cls_rgw, index_racing_removes) +{ + string bucket_oid = str_int("bucket", 8); + + ObjectWriteOperation op; + cls_rgw_bucket_init_index(op); + ASSERT_EQ(0, ioctx.operate(bucket_oid, &op)); + + int epoch = 0; + rgw_bucket_dir_entry dirent; + rgw_bucket_dir_entry_meta meta; + + // prepare/complete add for single object + const cls_rgw_obj_key obj{"obj"}; + std::string loc = "loc"; + { + std::string tag = "tag-add"; + index_prepare(ioctx, bucket_oid, CLS_RGW_OP_ADD, tag, obj, loc); + index_complete(ioctx, bucket_oid, CLS_RGW_OP_ADD, tag, ++epoch, obj, meta); + test_stats(ioctx, bucket_oid, RGWObjCategory::None, 1, 0); + } + + // list to verify no pending ops + { + std::map<int, rgw_cls_list_ret> results; + list_entries(ioctx, bucket_oid, 1, results); + ASSERT_EQ(1, results.size()); + const auto& entries = results.begin()->second.dir.m; + ASSERT_EQ(1, entries.size()); + dirent = std::move(entries.begin()->second); + ASSERT_EQ(obj, dirent.key); + ASSERT_TRUE(dirent.exists); + ASSERT_TRUE(dirent.pending_map.empty()); + } + + // prepare three racing removals + std::string tag1 = "tag-rm1"; + std::string tag2 = "tag-rm2"; + std::string tag3 = "tag-rm3"; + index_prepare(ioctx, bucket_oid, CLS_RGW_OP_DEL, tag1, obj, loc); + index_prepare(ioctx, bucket_oid, CLS_RGW_OP_DEL, tag2, obj, loc); + index_prepare(ioctx, bucket_oid, CLS_RGW_OP_DEL, tag3, obj, loc); + + test_stats(ioctx, bucket_oid, RGWObjCategory::None, 1, 0); + + // complete on tag2 + index_complete(ioctx, bucket_oid, CLS_RGW_OP_DEL, tag2, ++epoch, obj, meta); + { + std::map<int, rgw_cls_list_ret> results; + list_entries(ioctx, bucket_oid, 1, results); + ASSERT_EQ(1, results.size()); + const auto& entries = results.begin()->second.dir.m; + ASSERT_EQ(1, entries.size()); + dirent = std::move(entries.begin()->second); + ASSERT_EQ(obj, dirent.key); + ASSERT_FALSE(dirent.exists); + ASSERT_FALSE(dirent.pending_map.empty()); + } + + // cancel on tag1 + index_complete(ioctx, bucket_oid, CLS_RGW_OP_CANCEL, tag1, ++epoch, obj, meta); + { + std::map<int, rgw_cls_list_ret> results; + list_entries(ioctx, bucket_oid, 1, results); + ASSERT_EQ(1, results.size()); + const auto& entries = results.begin()->second.dir.m; + ASSERT_EQ(1, entries.size()); + dirent = std::move(entries.begin()->second); + ASSERT_EQ(obj, dirent.key); + ASSERT_FALSE(dirent.exists); + ASSERT_FALSE(dirent.pending_map.empty()); + } + + // final cancel on tag3 + index_complete(ioctx, bucket_oid, CLS_RGW_OP_CANCEL, tag3, ++epoch, obj, meta); + + // verify that the key was removed + { + std::map<int, rgw_cls_list_ret> results; + list_entries(ioctx, bucket_oid, 1, results); + EXPECT_EQ(1, results.size()); + const auto& entries = results.begin()->second.dir.m; + ASSERT_EQ(0, entries.size()); + } + + test_stats(ioctx, bucket_oid, RGWObjCategory::None, 0, 0); +} diff --git a/src/test/cls_rgw/test_cls_rgw_stats.cc b/src/test/cls_rgw/test_cls_rgw_stats.cc new file mode 100644 index 000000000..004ccc6d1 --- /dev/null +++ b/src/test/cls_rgw/test_cls_rgw_stats.cc @@ -0,0 +1,646 @@ +// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*- +// vim: ts=8 sw=2 smarttab + +#include <vector> +#include <boost/circular_buffer.hpp> +#include <boost/intrusive/set.hpp> +#include <gtest/gtest.h> +#include "cls/rgw/cls_rgw_client.h" +#include "common/debug.h" +#include "common/dout.h" +#include "common/errno.h" +#include "common/random_string.h" +#include "global/global_context.h" +#include "test/librados/test_cxx.h" + +#define dout_subsys ceph_subsys_rgw +#define dout_context g_ceph_context + + +// simulation parameters: + +// total number of index operations to prepare +constexpr size_t max_operations = 2048; +// total number of object names. each operation chooses one at random +constexpr size_t max_entries = 32; +// maximum number of pending operations. once the limit is reached, the oldest +// pending operation is finished before another can start +constexpr size_t max_pending = 16; +// object size is randomly distributed between 0 and 4M +constexpr size_t max_object_size = 4*1024*1024; +// multipart upload threshold +constexpr size_t max_part_size = 1024*1024; + + +// create/destroy a pool that's shared by all tests in the process +struct RadosEnv : public ::testing::Environment { + static std::optional<std::string> pool_name; + public: + static librados::Rados rados; + static librados::IoCtx ioctx; + + void SetUp() override { + // create pool + std::string name = get_temp_pool_name(); + ASSERT_EQ("", create_one_pool_pp(name, rados)); + pool_name = name; + ASSERT_EQ(rados.ioctx_create(name.c_str(), ioctx), 0); + } + void TearDown() override { + ioctx.close(); + if (pool_name) { + ASSERT_EQ(destroy_one_pool_pp(*pool_name, rados), 0); + } + } +}; +std::optional<std::string> RadosEnv::pool_name; +librados::Rados RadosEnv::rados; +librados::IoCtx RadosEnv::ioctx; + +auto *const rados_env = ::testing::AddGlobalTestEnvironment(new RadosEnv); + + +std::ostream& operator<<(std::ostream& out, const rgw_bucket_category_stats& c) { + return out << "{count=" << c.num_entries << " size=" << c.total_size << '}'; +} + + +// librados helpers +rgw_bucket_entry_ver last_version(librados::IoCtx& ioctx) +{ + rgw_bucket_entry_ver ver; + ver.pool = ioctx.get_id(); + ver.epoch = ioctx.get_last_version(); + return ver; +} + +int index_init(librados::IoCtx& ioctx, const std::string& oid) +{ + librados::ObjectWriteOperation op; + cls_rgw_bucket_init_index(op); + return ioctx.operate(oid, &op); +} + +int index_prepare(librados::IoCtx& ioctx, const std::string& oid, + const cls_rgw_obj_key& key, const std::string& tag, + RGWModifyOp type) +{ + librados::ObjectWriteOperation op; + const std::string loc; // empty + constexpr bool log_op = false; + constexpr int flags = 0; + rgw_zone_set zones; + cls_rgw_bucket_prepare_op(op, type, tag, key, loc, log_op, flags, zones); + return ioctx.operate(oid, &op); +} + +int index_complete(librados::IoCtx& ioctx, const std::string& oid, + const cls_rgw_obj_key& key, const std::string& tag, + RGWModifyOp type, const rgw_bucket_entry_ver& ver, + const rgw_bucket_dir_entry_meta& meta, + std::list<cls_rgw_obj_key>* remove_objs) +{ + librados::ObjectWriteOperation op; + constexpr bool log_op = false; + constexpr int flags = 0; + constexpr rgw_zone_set* zones = nullptr; + cls_rgw_bucket_complete_op(op, type, tag, ver, key, meta, + remove_objs, log_op, flags, zones); + return ioctx.operate(oid, &op); +} + +void read_stats(librados::IoCtx& ioctx, const std::string& oid, + rgw_bucket_dir_stats& stats) +{ + auto oids = std::map<int, std::string>{{0, oid}}; + std::map<int, rgw_cls_list_ret> results; + ASSERT_EQ(0, CLSRGWIssueGetDirHeader(ioctx, oids, results, 8)()); + ASSERT_EQ(1, results.size()); + stats = std::move(results.begin()->second.dir.header.stats); +} + +static void account_entry(rgw_bucket_dir_stats& stats, + const rgw_bucket_dir_entry_meta& meta) +{ + rgw_bucket_category_stats& c = stats[meta.category]; + c.num_entries++; + c.total_size += meta.accounted_size; + c.total_size_rounded += cls_rgw_get_rounded_size(meta.accounted_size); + c.actual_size += meta.size; +} + +static void unaccount_entry(rgw_bucket_dir_stats& stats, + const rgw_bucket_dir_entry_meta& meta) +{ + rgw_bucket_category_stats& c = stats[meta.category]; + c.num_entries--; + c.total_size -= meta.accounted_size; + c.total_size_rounded -= cls_rgw_get_rounded_size(meta.accounted_size); + c.actual_size -= meta.size; +} + + +// a map of cached dir entries representing the expected state of cls_rgw +struct object : rgw_bucket_dir_entry, boost::intrusive::set_base_hook<> { + explicit object(const cls_rgw_obj_key& key) { + this->key = key; + } +}; + +struct object_key { + using type = cls_rgw_obj_key; + const type& operator()(const object& o) const { return o.key; } +}; + +using object_map_base = boost::intrusive::set<object, + boost::intrusive::key_of_value<object_key>>; + +struct object_map : object_map_base { + ~object_map() { + clear_and_dispose(std::default_delete<object>{}); + } +}; + + +// models a bucket index operation, starting with cls_rgw_bucket_prepare_op(). +// stores all of the state necessary to complete the operation, either with +// cls_rgw_bucket_complete_op() or cls_rgw_suggest_changes(). uploads larger +// than max_part_size are modeled as multipart uploads +struct operation { + RGWModifyOp type; + cls_rgw_obj_key key; + std::string tag; + rgw_bucket_entry_ver ver; + std::string upload_id; // empty unless multipart + rgw_bucket_dir_entry_meta meta; +}; + + +class simulator { + public: + simulator(librados::IoCtx& ioctx, std::string oid) + : ioctx(ioctx), oid(std::move(oid)), pending(max_pending) + { + // generate a set of object keys. each operation chooses one at random + keys.reserve(max_entries); + for (size_t i = 0; i < max_entries; i++) { + keys.emplace_back(gen_rand_alphanumeric_upper(g_ceph_context, 12)); + } + } + + void run(); + + private: + void start(); + int try_start(const cls_rgw_obj_key& key, + const std::string& tag); + + void finish(const operation& op); + void complete(const operation& op, RGWModifyOp type); + void suggest(const operation& op, char suggestion); + + int init_multipart(const operation& op); + void complete_multipart(const operation& op); + + object_map::iterator find_or_create(const cls_rgw_obj_key& key); + + librados::IoCtx& ioctx; + std::string oid; + + std::vector<cls_rgw_obj_key> keys; + object_map objects; + boost::circular_buffer<operation> pending; + rgw_bucket_dir_stats stats; +}; + + +void simulator::run() +{ + // init the bucket index object + ASSERT_EQ(0, index_init(ioctx, oid)); + // run the simulation for N steps + for (size_t i = 0; i < max_operations; i++) { + if (pending.full()) { + // if we're at max_pending, finish the oldest operation + auto& op = pending.front(); + finish(op); + pending.pop_front(); + + // verify bucket stats + rgw_bucket_dir_stats stored_stats; + read_stats(ioctx, oid, stored_stats); + + const rgw_bucket_dir_stats& expected_stats = stats; + ASSERT_EQ(expected_stats, stored_stats); + } + + // initiate the next operation + start(); + + // verify bucket stats + rgw_bucket_dir_stats stored_stats; + read_stats(ioctx, oid, stored_stats); + + const rgw_bucket_dir_stats& expected_stats = stats; + ASSERT_EQ(expected_stats, stored_stats); + } +} + +object_map::iterator simulator::find_or_create(const cls_rgw_obj_key& key) +{ + object_map::insert_commit_data commit; + auto result = objects.insert_check(key, std::less<cls_rgw_obj_key>{}, commit); + if (result.second) { // inserting new entry + auto p = new object(key); + result.first = objects.insert_commit(*p, commit); + } + return result.first; +} + +int simulator::try_start(const cls_rgw_obj_key& key, const std::string& tag) +{ + // choose randomly betwen create and delete + const auto type = static_cast<RGWModifyOp>( + ceph::util::generate_random_number<size_t, size_t>(CLS_RGW_OP_ADD, + CLS_RGW_OP_DEL)); + auto op = operation{type, key, tag}; + + op.meta.category = RGWObjCategory::Main; + op.meta.size = op.meta.accounted_size = + ceph::util::generate_random_number(1, max_object_size); + + if (type == CLS_RGW_OP_ADD && op.meta.size > max_part_size) { + // simulate multipart for uploads over the max_part_size threshold + op.upload_id = gen_rand_alphanumeric_upper(g_ceph_context, 12); + + int r = init_multipart(op); + if (r != 0) { + derr << "> failed to prepare multipart upload key=" << key + << " upload=" << op.upload_id << " tag=" << tag + << " type=" << type << ": " << cpp_strerror(r) << dendl; + return r; + } + + dout(1) << "> prepared multipart upload key=" << key + << " upload=" << op.upload_id << " tag=" << tag + << " type=" << type << " size=" << op.meta.size << dendl; + } else { + // prepare operation + int r = index_prepare(ioctx, oid, op.key, op.tag, op.type); + if (r != 0) { + derr << "> failed to prepare operation key=" << key + << " tag=" << tag << " type=" << type + << ": " << cpp_strerror(r) << dendl; + return r; + } + + dout(1) << "> prepared operation key=" << key + << " tag=" << tag << " type=" << type + << " size=" << op.meta.size << dendl; + } + op.ver = last_version(ioctx); + + ceph_assert(!pending.full()); + pending.push_back(std::move(op)); + return 0; +} + +void simulator::start() +{ + // choose a random object key + const size_t index = ceph::util::generate_random_number(0, keys.size() - 1); + const auto& key = keys[index]; + // generate a random tag + const auto tag = gen_rand_alphanumeric_upper(g_ceph_context, 12); + + // retry until success. failures don't count towards max_operations + while (try_start(key, tag) != 0) + ; +} + +void simulator::finish(const operation& op) +{ + if (op.type == CLS_RGW_OP_ADD && !op.upload_id.empty()) { + // multipart uploads either complete or abort based on part uploads + complete_multipart(op); + return; + } + + // complete most operations, but finish some with cancel or dir suggest + constexpr int cancel_percent = 10; + constexpr int suggest_update_percent = 10; + constexpr int suggest_remove_percent = 10; + + int result = ceph::util::generate_random_number(0, 99); + if (result < cancel_percent) { + complete(op, CLS_RGW_OP_CANCEL); + return; + } + result -= cancel_percent; + if (result < suggest_update_percent) { + suggest(op, CEPH_RGW_UPDATE); + return; + } + result -= suggest_update_percent; + if (result < suggest_remove_percent) { + suggest(op, CEPH_RGW_REMOVE); + return; + } + complete(op, op.type); +} + +void simulator::complete(const operation& op, RGWModifyOp type) +{ + int r = index_complete(ioctx, oid, op.key, op.tag, type, + op.ver, op.meta, nullptr); + if (r != 0) { + derr << "< failed to complete operation key=" << op.key + << " tag=" << op.tag << " type=" << op.type + << " size=" << op.meta.size + << ": " << cpp_strerror(r) << dendl; + return; + } + + if (type == CLS_RGW_OP_CANCEL) { + dout(1) << "< canceled operation key=" << op.key + << " tag=" << op.tag << " type=" << op.type + << " size=" << op.meta.size << dendl; + } else if (type == CLS_RGW_OP_ADD) { + auto obj = find_or_create(op.key); + if (obj->exists) { + unaccount_entry(stats, obj->meta); + } + obj->exists = true; + obj->meta = op.meta; + account_entry(stats, obj->meta); + dout(1) << "< completed write operation key=" << op.key + << " tag=" << op.tag << " type=" << type + << " size=" << op.meta.size << dendl; + } else { + ceph_assert(type == CLS_RGW_OP_DEL); + auto obj = objects.find(op.key, std::less<cls_rgw_obj_key>{}); + if (obj != objects.end()) { + if (obj->exists) { + unaccount_entry(stats, obj->meta); + } + objects.erase_and_dispose(obj, std::default_delete<object>{}); + } + dout(1) << "< completed delete operation key=" << op.key + << " tag=" << op.tag << " type=" << type << dendl; + } +} + +void simulator::suggest(const operation& op, char suggestion) +{ + // read and decode the current dir entry + rgw_cls_bi_entry bi_entry; + int r = cls_rgw_bi_get(ioctx, oid, BIIndexType::Plain, op.key, &bi_entry); + if (r != 0) { + derr << "< no bi entry to suggest for operation key=" << op.key + << " tag=" << op.tag << " type=" << op.type + << " size=" << op.meta.size + << ": " << cpp_strerror(r) << dendl; + return; + } + ASSERT_EQ(bi_entry.type, BIIndexType::Plain); + + rgw_bucket_dir_entry entry; + auto p = bi_entry.data.cbegin(); + ASSERT_NO_THROW(decode(entry, p)); + + ASSERT_EQ(entry.key, op.key); + + // clear pending info and write it back; this cancels those pending + // operations (we'll see EINVAL when we try to complete them), but dir + // suggest is ignored unless the pending_map is empty + entry.pending_map.clear(); + + bi_entry.data.clear(); + encode(entry, bi_entry.data); + + r = cls_rgw_bi_put(ioctx, oid, bi_entry); + ASSERT_EQ(0, r); + + // now suggest changes for this entry + entry.ver = last_version(ioctx); + entry.exists = (suggestion == CEPH_RGW_UPDATE); + entry.meta = op.meta; + + bufferlist update; + cls_rgw_encode_suggestion(suggestion, entry, update); + + librados::ObjectWriteOperation write_op; + cls_rgw_suggest_changes(write_op, update); + r = ioctx.operate(oid, &write_op); + if (r != 0) { + derr << "< failed to suggest operation key=" << op.key + << " tag=" << op.tag << " type=" << op.type + << " size=" << op.meta.size + << ": " << cpp_strerror(r) << dendl; + return; + } + + // update our cache accordingly + if (suggestion == CEPH_RGW_UPDATE) { + auto obj = find_or_create(op.key); + if (obj->exists) { + unaccount_entry(stats, obj->meta); + } + obj->exists = true; + obj->meta = op.meta; + account_entry(stats, obj->meta); + dout(1) << "< suggested update operation key=" << op.key + << " tag=" << op.tag << " type=" << op.type + << " size=" << op.meta.size << dendl; + } else { + ceph_assert(suggestion == CEPH_RGW_REMOVE); + auto obj = objects.find(op.key, std::less<cls_rgw_obj_key>{}); + if (obj != objects.end()) { + if (obj->exists) { + unaccount_entry(stats, obj->meta); + } + objects.erase_and_dispose(obj, std::default_delete<object>{}); + } + dout(1) << "< suggested remove operation key=" << op.key + << " tag=" << op.tag << " type=" << op.type << dendl; + } +} + +int simulator::init_multipart(const operation& op) +{ + // create (not just prepare) the meta object + const auto meta_key = cls_rgw_obj_key{ + fmt::format("_multipart_{}.2~{}.meta", op.key.name, op.upload_id)}; + const std::string empty_tag; // empty tag enables complete without prepare + const rgw_bucket_entry_ver empty_ver; + rgw_bucket_dir_entry_meta meta_meta; + meta_meta.category = RGWObjCategory::MultiMeta; + int r = index_complete(ioctx, oid, meta_key, empty_tag, CLS_RGW_OP_ADD, + empty_ver, meta_meta, nullptr); + if (r != 0) { + derr << " < failed to create multipart meta key=" << meta_key + << ": " << cpp_strerror(r) << dendl; + return r; + } else { + // account for meta object + auto obj = find_or_create(meta_key); + if (obj->exists) { + unaccount_entry(stats, obj->meta); + } + obj->exists = true; + obj->meta = meta_meta; + account_entry(stats, obj->meta); + } + + // prepare part uploads + std::list<cls_rgw_obj_key> remove_objs; + size_t part_id = 0; + + size_t remaining = op.meta.size; + while (remaining > max_part_size) { + remaining -= max_part_size; + const auto part_size = std::min(remaining, max_part_size); + const auto part_key = cls_rgw_obj_key{ + fmt::format("_multipart_{}.2~{}.{}", op.key.name, op.upload_id, part_id)}; + part_id++; + + r = index_prepare(ioctx, oid, part_key, op.tag, op.type); + if (r != 0) { + // if part prepare fails, remove the meta object and remove_objs + [[maybe_unused]] int ignored = + index_complete(ioctx, oid, meta_key, empty_tag, CLS_RGW_OP_DEL, + empty_ver, meta_meta, &remove_objs); + derr << " > failed to prepare part key=" << part_key + << " size=" << part_size << dendl; + return r; // return the error from prepare + } + dout(1) << " > prepared part key=" << part_key + << " size=" << part_size << dendl; + remove_objs.push_back(part_key); + } + return 0; +} + +void simulator::complete_multipart(const operation& op) +{ + const std::string empty_tag; // empty tag enables complete without prepare + const rgw_bucket_entry_ver empty_ver; + + // try to finish part uploads + size_t part_id = 0; + std::list<cls_rgw_obj_key> remove_objs; + + RGWModifyOp type = op.type; // OP_ADD, or OP_CANCEL for abort + + size_t remaining = op.meta.size; + while (remaining > max_part_size) { + remaining -= max_part_size; + const auto part_size = std::min(remaining, max_part_size); + const auto part_key = cls_rgw_obj_key{ + fmt::format("_multipart_{}.2~{}.{}", op.key.name, op.upload_id, part_id)}; + part_id++; + + // cancel 10% of part uploads (and abort the multipart upload) + constexpr int cancel_percent = 10; + const int result = ceph::util::generate_random_number(0, 99); + if (result < cancel_percent) { + type = CLS_RGW_OP_CANCEL; // abort multipart + dout(1) << " < canceled part key=" << part_key + << " size=" << part_size << dendl; + } else { + rgw_bucket_dir_entry_meta meta; + meta.category = op.meta.category; + meta.size = meta.accounted_size = part_size; + + int r = index_complete(ioctx, oid, part_key, op.tag, op.type, + empty_ver, meta, nullptr); + if (r != 0) { + derr << " < failed to complete part key=" << part_key + << " size=" << meta.size << ": " << cpp_strerror(r) << dendl; + type = CLS_RGW_OP_CANCEL; // abort multipart + } else { + dout(1) << " < completed part key=" << part_key + << " size=" << meta.size << dendl; + // account for successful part upload + auto obj = find_or_create(part_key); + if (obj->exists) { + unaccount_entry(stats, obj->meta); + } + obj->exists = true; + obj->meta = meta; + account_entry(stats, obj->meta); + } + } + remove_objs.push_back(part_key); + } + + // delete the multipart meta object + const auto meta_key = cls_rgw_obj_key{ + fmt::format("_multipart_{}.2~{}.meta", op.key.name, op.upload_id)}; + rgw_bucket_dir_entry_meta meta_meta; + meta_meta.category = RGWObjCategory::MultiMeta; + + int r = index_complete(ioctx, oid, meta_key, empty_tag, CLS_RGW_OP_DEL, + empty_ver, meta_meta, nullptr); + if (r != 0) { + derr << " < failed to remove multipart meta key=" << meta_key + << ": " << cpp_strerror(r) << dendl; + } else { + // unaccount for meta object + auto obj = objects.find(meta_key, std::less<cls_rgw_obj_key>{}); + if (obj != objects.end()) { + if (obj->exists) { + unaccount_entry(stats, obj->meta); + } + objects.erase_and_dispose(obj, std::default_delete<object>{}); + } + } + + // create or cancel the head object + r = index_complete(ioctx, oid, op.key, empty_tag, type, + empty_ver, op.meta, &remove_objs); + if (r != 0) { + derr << "< failed to complete multipart upload key=" << op.key + << " upload=" << op.upload_id << " tag=" << op.tag + << " type=" << type << " size=" << op.meta.size + << ": " << cpp_strerror(r) << dendl; + return; + } + + if (type == CLS_RGW_OP_ADD) { + dout(1) << "< completed multipart upload key=" << op.key + << " upload=" << op.upload_id << " tag=" << op.tag + << " type=" << op.type << " size=" << op.meta.size << dendl; + + // account for head stats + auto obj = find_or_create(op.key); + if (obj->exists) { + unaccount_entry(stats, obj->meta); + } + obj->exists = true; + obj->meta = op.meta; + account_entry(stats, obj->meta); + } else { + dout(1) << "< canceled multipart upload key=" << op.key + << " upload=" << op.upload_id << " tag=" << op.tag + << " type=" << op.type << " size=" << op.meta.size << dendl; + } + + // unaccount for remove_objs + for (const auto& part_key : remove_objs) { + auto obj = objects.find(part_key, std::less<cls_rgw_obj_key>{}); + if (obj != objects.end()) { + if (obj->exists) { + unaccount_entry(stats, obj->meta); + } + objects.erase_and_dispose(obj, std::default_delete<object>{}); + } + } +} + +TEST(cls_rgw_stats, simulate) +{ + const char* bucket_oid = __func__; + auto sim = simulator{RadosEnv::ioctx, bucket_oid}; + sim.run(); +} |