1 // -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
2 // vim: ts=8 sw=2 smarttab
5 #include <boost/circular_buffer.hpp>
6 #include <boost/intrusive/set.hpp>
7 #include <gtest/gtest.h>
8 #include "cls/rgw/cls_rgw_client.h"
9 #include "common/debug.h"
10 #include "common/dout.h"
11 #include "common/errno.h"
12 #include "common/random_string.h"
13 #include "global/global_context.h"
14 #include "test/librados/test_cxx.h"
16 #define dout_subsys ceph_subsys_rgw
17 #define dout_context g_ceph_context
20 // simulation parameters:
22 // total number of index operations to prepare
23 constexpr size_t max_operations
= 2048;
24 // total number of object names. each operation chooses one at random
25 constexpr size_t max_entries
= 32;
26 // maximum number of pending operations. once the limit is reached, the oldest
27 // pending operation is finished before another can start
28 constexpr size_t max_pending
= 16;
29 // object size is randomly distributed between 0 and 4M
30 constexpr size_t max_object_size
= 4*1024*1024;
31 // multipart upload threshold
32 constexpr size_t max_part_size
= 1024*1024;
35 // create/destroy a pool that's shared by all tests in the process
36 struct RadosEnv
: public ::testing::Environment
{
37 static std::optional
<std::string
> pool_name
;
39 static librados::Rados rados
;
40 static librados::IoCtx ioctx
;
42 void SetUp() override
{
44 std::string name
= get_temp_pool_name();
45 ASSERT_EQ("", create_one_pool_pp(name
, rados
));
47 ASSERT_EQ(rados
.ioctx_create(name
.c_str(), ioctx
), 0);
49 void TearDown() override
{
52 ASSERT_EQ(destroy_one_pool_pp(*pool_name
, rados
), 0);
56 std::optional
<std::string
> RadosEnv::pool_name
;
57 librados::Rados
RadosEnv::rados
;
58 librados::IoCtx
RadosEnv::ioctx
;
60 auto *const rados_env
= ::testing::AddGlobalTestEnvironment(new RadosEnv
);
63 std::ostream
& operator<<(std::ostream
& out
, const rgw_bucket_category_stats
& c
) {
64 return out
<< "{count=" << c
.num_entries
<< " size=" << c
.total_size
<< '}';
69 rgw_bucket_entry_ver
last_version(librados::IoCtx
& ioctx
)
71 rgw_bucket_entry_ver ver
;
72 ver
.pool
= ioctx
.get_id();
73 ver
.epoch
= ioctx
.get_last_version();
77 int index_init(librados::IoCtx
& ioctx
, const std::string
& oid
)
79 librados::ObjectWriteOperation op
;
80 cls_rgw_bucket_init_index(op
);
81 return ioctx
.operate(oid
, &op
);
84 int index_prepare(librados::IoCtx
& ioctx
, const std::string
& oid
,
85 const cls_rgw_obj_key
& key
, const std::string
& tag
,
88 librados::ObjectWriteOperation op
;
89 const std::string loc
; // empty
90 constexpr bool log_op
= false;
91 constexpr int flags
= 0;
93 cls_rgw_bucket_prepare_op(op
, type
, tag
, key
, loc
, log_op
, flags
, zones
);
94 return ioctx
.operate(oid
, &op
);
97 int index_complete(librados::IoCtx
& ioctx
, const std::string
& oid
,
98 const cls_rgw_obj_key
& key
, const std::string
& tag
,
99 RGWModifyOp type
, const rgw_bucket_entry_ver
& ver
,
100 const rgw_bucket_dir_entry_meta
& meta
,
101 std::list
<cls_rgw_obj_key
>* remove_objs
)
103 librados::ObjectWriteOperation op
;
104 constexpr bool log_op
= false;
105 constexpr int flags
= 0;
106 constexpr rgw_zone_set
* zones
= nullptr;
107 cls_rgw_bucket_complete_op(op
, type
, tag
, ver
, key
, meta
,
108 remove_objs
, log_op
, flags
, zones
);
109 return ioctx
.operate(oid
, &op
);
112 void read_stats(librados::IoCtx
& ioctx
, const std::string
& oid
,
113 rgw_bucket_dir_stats
& stats
)
115 auto oids
= std::map
<int, std::string
>{{0, oid
}};
116 std::map
<int, rgw_cls_list_ret
> results
;
117 ASSERT_EQ(0, CLSRGWIssueGetDirHeader(ioctx
, oids
, results
, 8)());
118 ASSERT_EQ(1, results
.size());
119 stats
= std::move(results
.begin()->second
.dir
.header
.stats
);
122 static void account_entry(rgw_bucket_dir_stats
& stats
,
123 const rgw_bucket_dir_entry_meta
& meta
)
125 rgw_bucket_category_stats
& c
= stats
[meta
.category
];
127 c
.total_size
+= meta
.accounted_size
;
128 c
.total_size_rounded
+= cls_rgw_get_rounded_size(meta
.accounted_size
);
129 c
.actual_size
+= meta
.size
;
132 static void unaccount_entry(rgw_bucket_dir_stats
& stats
,
133 const rgw_bucket_dir_entry_meta
& meta
)
135 rgw_bucket_category_stats
& c
= stats
[meta
.category
];
137 c
.total_size
-= meta
.accounted_size
;
138 c
.total_size_rounded
-= cls_rgw_get_rounded_size(meta
.accounted_size
);
139 c
.actual_size
-= meta
.size
;
143 // a map of cached dir entries representing the expected state of cls_rgw
144 struct object
: rgw_bucket_dir_entry
, boost::intrusive::set_base_hook
<> {
145 explicit object(const cls_rgw_obj_key
& key
) {
151 using type
= cls_rgw_obj_key
;
152 const type
& operator()(const object
& o
) const { return o
.key
; }
155 using object_map_base
= boost::intrusive::set
<object
,
156 boost::intrusive::key_of_value
<object_key
>>;
158 struct object_map
: object_map_base
{
160 clear_and_dispose(std::default_delete
<object
>{});
165 // models a bucket index operation, starting with cls_rgw_bucket_prepare_op().
166 // stores all of the state necessary to complete the operation, either with
167 // cls_rgw_bucket_complete_op() or cls_rgw_suggest_changes(). uploads larger
168 // than max_part_size are modeled as multipart uploads
173 rgw_bucket_entry_ver ver
;
174 std::string upload_id
; // empty unless multipart
175 rgw_bucket_dir_entry_meta meta
;
181 simulator(librados::IoCtx
& ioctx
, std::string oid
)
182 : ioctx(ioctx
), oid(std::move(oid
)), pending(max_pending
)
184 // generate a set of object keys. each operation chooses one at random
185 keys
.reserve(max_entries
);
186 for (size_t i
= 0; i
< max_entries
; i
++) {
187 keys
.emplace_back(gen_rand_alphanumeric_upper(g_ceph_context
, 12));
195 int try_start(const cls_rgw_obj_key
& key
,
196 const std::string
& tag
);
198 void finish(const operation
& op
);
199 void complete(const operation
& op
, RGWModifyOp type
);
200 void suggest(const operation
& op
, char suggestion
);
202 int init_multipart(const operation
& op
);
203 void complete_multipart(const operation
& op
);
205 object_map::iterator
find_or_create(const cls_rgw_obj_key
& key
);
207 librados::IoCtx
& ioctx
;
210 std::vector
<cls_rgw_obj_key
> keys
;
212 boost::circular_buffer
<operation
> pending
;
213 rgw_bucket_dir_stats stats
;
217 void simulator::run()
219 // init the bucket index object
220 ASSERT_EQ(0, index_init(ioctx
, oid
));
221 // run the simulation for N steps
222 for (size_t i
= 0; i
< max_operations
; i
++) {
223 if (pending
.full()) {
224 // if we're at max_pending, finish the oldest operation
225 auto& op
= pending
.front();
229 // verify bucket stats
230 rgw_bucket_dir_stats stored_stats
;
231 read_stats(ioctx
, oid
, stored_stats
);
233 const rgw_bucket_dir_stats
& expected_stats
= stats
;
234 ASSERT_EQ(expected_stats
, stored_stats
);
237 // initiate the next operation
240 // verify bucket stats
241 rgw_bucket_dir_stats stored_stats
;
242 read_stats(ioctx
, oid
, stored_stats
);
244 const rgw_bucket_dir_stats
& expected_stats
= stats
;
245 ASSERT_EQ(expected_stats
, stored_stats
);
249 object_map::iterator
simulator::find_or_create(const cls_rgw_obj_key
& key
)
251 object_map::insert_commit_data commit
;
252 auto result
= objects
.insert_check(key
, std::less
<cls_rgw_obj_key
>{}, commit
);
253 if (result
.second
) { // inserting new entry
254 auto p
= new object(key
);
255 result
.first
= objects
.insert_commit(*p
, commit
);
260 int simulator::try_start(const cls_rgw_obj_key
& key
, const std::string
& tag
)
262 // choose randomly betwen create and delete
263 const auto type
= static_cast<RGWModifyOp
>(
264 ceph::util::generate_random_number
<size_t, size_t>(CLS_RGW_OP_ADD
,
266 auto op
= operation
{type
, key
, tag
};
268 op
.meta
.category
= RGWObjCategory::Main
;
269 op
.meta
.size
= op
.meta
.accounted_size
=
270 ceph::util::generate_random_number(1, max_object_size
);
272 if (type
== CLS_RGW_OP_ADD
&& op
.meta
.size
> max_part_size
) {
273 // simulate multipart for uploads over the max_part_size threshold
274 op
.upload_id
= gen_rand_alphanumeric_upper(g_ceph_context
, 12);
276 int r
= init_multipart(op
);
278 derr
<< "> failed to prepare multipart upload key=" << key
279 << " upload=" << op
.upload_id
<< " tag=" << tag
280 << " type=" << type
<< ": " << cpp_strerror(r
) << dendl
;
284 dout(1) << "> prepared multipart upload key=" << key
285 << " upload=" << op
.upload_id
<< " tag=" << tag
286 << " type=" << type
<< " size=" << op
.meta
.size
<< dendl
;
289 int r
= index_prepare(ioctx
, oid
, op
.key
, op
.tag
, op
.type
);
291 derr
<< "> failed to prepare operation key=" << key
292 << " tag=" << tag
<< " type=" << type
293 << ": " << cpp_strerror(r
) << dendl
;
297 dout(1) << "> prepared operation key=" << key
298 << " tag=" << tag
<< " type=" << type
299 << " size=" << op
.meta
.size
<< dendl
;
301 op
.ver
= last_version(ioctx
);
303 ceph_assert(!pending
.full());
304 pending
.push_back(std::move(op
));
308 void simulator::start()
310 // choose a random object key
311 const size_t index
= ceph::util::generate_random_number(0, keys
.size() - 1);
312 const auto& key
= keys
[index
];
313 // generate a random tag
314 const auto tag
= gen_rand_alphanumeric_upper(g_ceph_context
, 12);
316 // retry until success. failures don't count towards max_operations
317 while (try_start(key
, tag
) != 0)
321 void simulator::finish(const operation
& op
)
323 if (op
.type
== CLS_RGW_OP_ADD
&& !op
.upload_id
.empty()) {
324 // multipart uploads either complete or abort based on part uploads
325 complete_multipart(op
);
329 // complete most operations, but finish some with cancel or dir suggest
330 constexpr int cancel_percent
= 10;
331 constexpr int suggest_update_percent
= 10;
332 constexpr int suggest_remove_percent
= 10;
334 int result
= ceph::util::generate_random_number(0, 99);
335 if (result
< cancel_percent
) {
336 complete(op
, CLS_RGW_OP_CANCEL
);
339 result
-= cancel_percent
;
340 if (result
< suggest_update_percent
) {
341 suggest(op
, CEPH_RGW_UPDATE
);
344 result
-= suggest_update_percent
;
345 if (result
< suggest_remove_percent
) {
346 suggest(op
, CEPH_RGW_REMOVE
);
349 complete(op
, op
.type
);
352 void simulator::complete(const operation
& op
, RGWModifyOp type
)
354 int r
= index_complete(ioctx
, oid
, op
.key
, op
.tag
, type
,
355 op
.ver
, op
.meta
, nullptr);
357 derr
<< "< failed to complete operation key=" << op
.key
358 << " tag=" << op
.tag
<< " type=" << op
.type
359 << " size=" << op
.meta
.size
360 << ": " << cpp_strerror(r
) << dendl
;
364 if (type
== CLS_RGW_OP_CANCEL
) {
365 dout(1) << "< canceled operation key=" << op
.key
366 << " tag=" << op
.tag
<< " type=" << op
.type
367 << " size=" << op
.meta
.size
<< dendl
;
368 } else if (type
== CLS_RGW_OP_ADD
) {
369 auto obj
= find_or_create(op
.key
);
371 unaccount_entry(stats
, obj
->meta
);
375 account_entry(stats
, obj
->meta
);
376 dout(1) << "< completed write operation key=" << op
.key
377 << " tag=" << op
.tag
<< " type=" << type
378 << " size=" << op
.meta
.size
<< dendl
;
380 ceph_assert(type
== CLS_RGW_OP_DEL
);
381 auto obj
= objects
.find(op
.key
, std::less
<cls_rgw_obj_key
>{});
382 if (obj
!= objects
.end()) {
384 unaccount_entry(stats
, obj
->meta
);
386 objects
.erase_and_dispose(obj
, std::default_delete
<object
>{});
388 dout(1) << "< completed delete operation key=" << op
.key
389 << " tag=" << op
.tag
<< " type=" << type
<< dendl
;
393 void simulator::suggest(const operation
& op
, char suggestion
)
395 // read and decode the current dir entry
396 rgw_cls_bi_entry bi_entry
;
397 int r
= cls_rgw_bi_get(ioctx
, oid
, BIIndexType::Plain
, op
.key
, &bi_entry
);
399 derr
<< "< no bi entry to suggest for operation key=" << op
.key
400 << " tag=" << op
.tag
<< " type=" << op
.type
401 << " size=" << op
.meta
.size
402 << ": " << cpp_strerror(r
) << dendl
;
405 ASSERT_EQ(bi_entry
.type
, BIIndexType::Plain
);
407 rgw_bucket_dir_entry entry
;
408 auto p
= bi_entry
.data
.cbegin();
409 ASSERT_NO_THROW(decode(entry
, p
));
411 ASSERT_EQ(entry
.key
, op
.key
);
413 // clear pending info and write it back; this cancels those pending
414 // operations (we'll see EINVAL when we try to complete them), but dir
415 // suggest is ignored unless the pending_map is empty
416 entry
.pending_map
.clear();
418 bi_entry
.data
.clear();
419 encode(entry
, bi_entry
.data
);
421 r
= cls_rgw_bi_put(ioctx
, oid
, bi_entry
);
424 // now suggest changes for this entry
425 entry
.ver
= last_version(ioctx
);
426 entry
.exists
= (suggestion
== CEPH_RGW_UPDATE
);
427 entry
.meta
= op
.meta
;
430 cls_rgw_encode_suggestion(suggestion
, entry
, update
);
432 librados::ObjectWriteOperation write_op
;
433 cls_rgw_suggest_changes(write_op
, update
);
434 r
= ioctx
.operate(oid
, &write_op
);
436 derr
<< "< failed to suggest operation key=" << op
.key
437 << " tag=" << op
.tag
<< " type=" << op
.type
438 << " size=" << op
.meta
.size
439 << ": " << cpp_strerror(r
) << dendl
;
443 // update our cache accordingly
444 if (suggestion
== CEPH_RGW_UPDATE
) {
445 auto obj
= find_or_create(op
.key
);
447 unaccount_entry(stats
, obj
->meta
);
451 account_entry(stats
, obj
->meta
);
452 dout(1) << "< suggested update operation key=" << op
.key
453 << " tag=" << op
.tag
<< " type=" << op
.type
454 << " size=" << op
.meta
.size
<< dendl
;
456 ceph_assert(suggestion
== CEPH_RGW_REMOVE
);
457 auto obj
= objects
.find(op
.key
, std::less
<cls_rgw_obj_key
>{});
458 if (obj
!= objects
.end()) {
460 unaccount_entry(stats
, obj
->meta
);
462 objects
.erase_and_dispose(obj
, std::default_delete
<object
>{});
464 dout(1) << "< suggested remove operation key=" << op
.key
465 << " tag=" << op
.tag
<< " type=" << op
.type
<< dendl
;
469 int simulator::init_multipart(const operation
& op
)
471 // create (not just prepare) the meta object
472 const auto meta_key
= cls_rgw_obj_key
{
473 fmt::format("_multipart_{}.2~{}.meta", op
.key
.name
, op
.upload_id
)};
474 const std::string empty_tag
; // empty tag enables complete without prepare
475 const rgw_bucket_entry_ver empty_ver
;
476 rgw_bucket_dir_entry_meta meta_meta
;
477 meta_meta
.category
= RGWObjCategory::MultiMeta
;
478 int r
= index_complete(ioctx
, oid
, meta_key
, empty_tag
, CLS_RGW_OP_ADD
,
479 empty_ver
, meta_meta
, nullptr);
481 derr
<< " < failed to create multipart meta key=" << meta_key
482 << ": " << cpp_strerror(r
) << dendl
;
485 // account for meta object
486 auto obj
= find_or_create(meta_key
);
488 unaccount_entry(stats
, obj
->meta
);
491 obj
->meta
= meta_meta
;
492 account_entry(stats
, obj
->meta
);
495 // prepare part uploads
496 std::list
<cls_rgw_obj_key
> remove_objs
;
499 size_t remaining
= op
.meta
.size
;
500 while (remaining
> max_part_size
) {
501 remaining
-= max_part_size
;
502 const auto part_size
= std::min(remaining
, max_part_size
);
503 const auto part_key
= cls_rgw_obj_key
{
504 fmt::format("_multipart_{}.2~{}.{}", op
.key
.name
, op
.upload_id
, part_id
)};
507 r
= index_prepare(ioctx
, oid
, part_key
, op
.tag
, op
.type
);
509 // if part prepare fails, remove the meta object and remove_objs
510 [[maybe_unused
]] int ignored
=
511 index_complete(ioctx
, oid
, meta_key
, empty_tag
, CLS_RGW_OP_DEL
,
512 empty_ver
, meta_meta
, &remove_objs
);
513 derr
<< " > failed to prepare part key=" << part_key
514 << " size=" << part_size
<< dendl
;
515 return r
; // return the error from prepare
517 dout(1) << " > prepared part key=" << part_key
518 << " size=" << part_size
<< dendl
;
519 remove_objs
.push_back(part_key
);
524 void simulator::complete_multipart(const operation
& op
)
526 const std::string empty_tag
; // empty tag enables complete without prepare
527 const rgw_bucket_entry_ver empty_ver
;
529 // try to finish part uploads
531 std::list
<cls_rgw_obj_key
> remove_objs
;
533 RGWModifyOp type
= op
.type
; // OP_ADD, or OP_CANCEL for abort
535 size_t remaining
= op
.meta
.size
;
536 while (remaining
> max_part_size
) {
537 remaining
-= max_part_size
;
538 const auto part_size
= std::min(remaining
, max_part_size
);
539 const auto part_key
= cls_rgw_obj_key
{
540 fmt::format("_multipart_{}.2~{}.{}", op
.key
.name
, op
.upload_id
, part_id
)};
543 // cancel 10% of part uploads (and abort the multipart upload)
544 constexpr int cancel_percent
= 10;
545 const int result
= ceph::util::generate_random_number(0, 99);
546 if (result
< cancel_percent
) {
547 type
= CLS_RGW_OP_CANCEL
; // abort multipart
548 dout(1) << " < canceled part key=" << part_key
549 << " size=" << part_size
<< dendl
;
551 rgw_bucket_dir_entry_meta meta
;
552 meta
.category
= op
.meta
.category
;
553 meta
.size
= meta
.accounted_size
= part_size
;
555 int r
= index_complete(ioctx
, oid
, part_key
, op
.tag
, op
.type
,
556 empty_ver
, meta
, nullptr);
558 derr
<< " < failed to complete part key=" << part_key
559 << " size=" << meta
.size
<< ": " << cpp_strerror(r
) << dendl
;
560 type
= CLS_RGW_OP_CANCEL
; // abort multipart
562 dout(1) << " < completed part key=" << part_key
563 << " size=" << meta
.size
<< dendl
;
564 // account for successful part upload
565 auto obj
= find_or_create(part_key
);
567 unaccount_entry(stats
, obj
->meta
);
571 account_entry(stats
, obj
->meta
);
574 remove_objs
.push_back(part_key
);
577 // delete the multipart meta object
578 const auto meta_key
= cls_rgw_obj_key
{
579 fmt::format("_multipart_{}.2~{}.meta", op
.key
.name
, op
.upload_id
)};
580 rgw_bucket_dir_entry_meta meta_meta
;
581 meta_meta
.category
= RGWObjCategory::MultiMeta
;
583 int r
= index_complete(ioctx
, oid
, meta_key
, empty_tag
, CLS_RGW_OP_DEL
,
584 empty_ver
, meta_meta
, nullptr);
586 derr
<< " < failed to remove multipart meta key=" << meta_key
587 << ": " << cpp_strerror(r
) << dendl
;
589 // unaccount for meta object
590 auto obj
= objects
.find(meta_key
, std::less
<cls_rgw_obj_key
>{});
591 if (obj
!= objects
.end()) {
593 unaccount_entry(stats
, obj
->meta
);
595 objects
.erase_and_dispose(obj
, std::default_delete
<object
>{});
599 // create or cancel the head object
600 r
= index_complete(ioctx
, oid
, op
.key
, empty_tag
, type
,
601 empty_ver
, op
.meta
, &remove_objs
);
603 derr
<< "< failed to complete multipart upload key=" << op
.key
604 << " upload=" << op
.upload_id
<< " tag=" << op
.tag
605 << " type=" << type
<< " size=" << op
.meta
.size
606 << ": " << cpp_strerror(r
) << dendl
;
610 if (type
== CLS_RGW_OP_ADD
) {
611 dout(1) << "< completed multipart upload key=" << op
.key
612 << " upload=" << op
.upload_id
<< " tag=" << op
.tag
613 << " type=" << op
.type
<< " size=" << op
.meta
.size
<< dendl
;
615 // account for head stats
616 auto obj
= find_or_create(op
.key
);
618 unaccount_entry(stats
, obj
->meta
);
622 account_entry(stats
, obj
->meta
);
624 dout(1) << "< canceled multipart upload key=" << op
.key
625 << " upload=" << op
.upload_id
<< " tag=" << op
.tag
626 << " type=" << op
.type
<< " size=" << op
.meta
.size
<< dendl
;
629 // unaccount for remove_objs
630 for (const auto& part_key
: remove_objs
) {
631 auto obj
= objects
.find(part_key
, std::less
<cls_rgw_obj_key
>{});
632 if (obj
!= objects
.end()) {
634 unaccount_entry(stats
, obj
->meta
);
636 objects
.erase_and_dispose(obj
, std::default_delete
<object
>{});
641 TEST(cls_rgw_stats
, simulate
)
643 const char* bucket_oid
= __func__
;
644 auto sim
= simulator
{RadosEnv::ioctx
, bucket_oid
};