xrpld
Loading...
Searching...
No Matches
IntrusiveShared_test.cpp
1
2#include <xrpl/basics/IntrusivePointer.h> // IWYU pragma: keep
3#include <xrpl/basics/IntrusivePointer.ipp> // IWYU pragma: keep
4#include <xrpl/basics/IntrusiveRefCounts.h>
5#include <xrpl/beast/unit_test/suite.h>
6
7#include <algorithm>
8#include <array>
9#include <atomic>
10#include <cassert>
11#include <chrono> // IWYU pragma: keep
12#include <condition_variable>
13#include <cstddef>
14#include <cstdint>
15#include <functional>
16#include <latch>
17#include <mutex>
18#include <optional>
19#include <random>
20#include <string>
21#include <thread>
22#include <utility>
23#include <variant>
24#include <vector>
25
26namespace xrpl::tests {
27
50struct Barrier
51{
54 int count;
55 int const initial;
56
57 Barrier(int n) : count(n), initial(n)
58 {
59 }
60
61 void
63 {
65 if (--count == 0)
66 {
67 count = initial;
68 cv.notify_all();
69 }
70 else
71 {
72 cv.wait(lock, [&] { return count == initial; });
73 }
74 }
75};
76
77namespace {
78enum class TrackedState : std::uint8_t {
79 Uninitialized,
80 Alive,
81 PartiallyDeletedStarted,
82 PartiallyDeleted,
83 DeletedStarted,
84 Deleted
85};
86
87class TIBase : public IntrusiveRefCounts
88{
89public:
90 static constexpr std::size_t kMaxStates = 128;
91 static std::array<std::atomic<TrackedState>, kMaxStates> state;
92 static std::atomic<int> nextId;
93 static TrackedState
94 getState(int id)
95 {
96 assert(id < state.size());
97 return state[id].load(std::memory_order_acquire);
98 }
99 static void
100 resetStates(bool resetCallback)
101 {
102 for (int i = 0; i < kMaxStates; ++i)
103 {
104 state[i].store(TrackedState::Uninitialized, std::memory_order_release);
105 }
106 nextId.store(0, std::memory_order_release);
107 if (resetCallback)
108 TIBase::tracingCallback = [](TrackedState, std::optional<TrackedState>) {};
109 }
110
111 struct ResetStatesGuard
112 {
113 bool resetCallback{false};
114
115 ResetStatesGuard(bool resetCallback) : resetCallback{resetCallback}
116 {
117 TIBase::resetStates(resetCallback);
118 }
119 ~ResetStatesGuard()
120 {
121 TIBase::resetStates(resetCallback);
122 }
123 };
124
125 TIBase() : id{checkoutID()}
126 {
127 assert(state.size() > id);
128 state[id].store(TrackedState::Alive, std::memory_order_relaxed);
129 }
130 ~TIBase() override
131 {
132 using enum TrackedState;
133
134 assert(state.size() > id);
135 tracingCallback(state[id].load(std::memory_order_relaxed), DeletedStarted);
136
137 assert(state.size() > id);
138 // Use relaxed memory order to try to avoid atomic operations from
139 // adding additional memory synchronizations that may hide threading
140 // errors in the underlying shared pointer class.
141 state[id].store(DeletedStarted, std::memory_order_relaxed);
142
143 tracingCallback(DeletedStarted, Deleted);
144
145 assert(state.size() > id);
146 state[id].store(TrackedState::Deleted, std::memory_order_relaxed);
147
148 tracingCallback(TrackedState::Deleted, std::nullopt);
149 }
150
151 void
152 partialDestructor() const
153 {
154 using enum TrackedState;
155
156 assert(state.size() > id);
157 tracingCallback(state[id].load(std::memory_order_relaxed), PartiallyDeletedStarted);
158
159 assert(state.size() > id);
160 state[id].store(PartiallyDeletedStarted, std::memory_order_relaxed);
161
162 tracingCallback(PartiallyDeletedStarted, PartiallyDeleted);
163
164 assert(state.size() > id);
165 state[id].store(PartiallyDeleted, std::memory_order_relaxed);
166
167 tracingCallback(PartiallyDeleted, std::nullopt);
168 }
169
170 static std::function<void(TrackedState, std::optional<TrackedState>)> tracingCallback;
171
172 int id;
173
174private:
175 static int
176 checkoutID()
177 {
178 return nextId.fetch_add(1, std::memory_order_acq_rel);
179 }
180};
181
182std::array<std::atomic<TrackedState>, TIBase::kMaxStates> TIBase::state;
183std::atomic<int> TIBase::nextId{0};
184
185std::function<void(TrackedState, std::optional<TrackedState>)> TIBase::tracingCallback =
186 [](TrackedState, std::optional<TrackedState>) {};
187
188} // namespace
189
191{
192public:
193 void
195 {
196 testcase("Basics");
197
198 {
199 TIBase::ResetStatesGuard const rsg{true};
200
201 TIBase const b;
202 BEAST_EXPECT(b.useCount() == 1);
203 b.addWeakRef();
204 BEAST_EXPECT(b.useCount() == 1);
205 auto s = b.releaseStrongRef();
207 BEAST_EXPECT(b.useCount() == 0);
208 TIBase const* pb = &b;
210 BEAST_EXPECT(!pb);
211 auto w = b.releaseWeakRef();
212 BEAST_EXPECT(w == ReleaseWeakRefAction::Destroy);
213 }
214
217 {
218 TIBase::ResetStatesGuard const rsg{true};
219
220 using enum TrackedState;
222 auto id = b->id;
223 BEAST_EXPECT(TIBase::getState(id) == Alive);
224 BEAST_EXPECT(b->useCount() == 1);
225 for (int i = 0; i < 10; ++i)
226 {
227 strong.push_back(b);
228 }
229 b.reset();
230 BEAST_EXPECT(TIBase::getState(id) == Alive);
231 strong.resize(strong.size() - 1);
232 BEAST_EXPECT(TIBase::getState(id) == Alive);
233 strong.clear();
234 BEAST_EXPECT(TIBase::getState(id) == Deleted);
235
237 id = b->id;
238 BEAST_EXPECT(TIBase::getState(id) == Alive);
239 BEAST_EXPECT(b->useCount() == 1);
240 for (int i = 0; i < 10; ++i)
241 {
242 weak.emplace_back(b);
243 BEAST_EXPECT(b->useCount() == 1);
244 }
245 BEAST_EXPECT(TIBase::getState(id) == Alive);
246 weak.resize(weak.size() - 1);
247 BEAST_EXPECT(TIBase::getState(id) == Alive);
248 b.reset();
249 BEAST_EXPECT(TIBase::getState(id) == PartiallyDeleted);
250 while (!weak.empty())
251 {
252 weak.resize(weak.size() - 1);
253 if (!weak.empty())
254 BEAST_EXPECT(TIBase::getState(id) == PartiallyDeleted);
255 }
256 BEAST_EXPECT(TIBase::getState(id) == Deleted);
257 }
258 {
259 TIBase::ResetStatesGuard const rsg{true};
260
261 using enum TrackedState;
263 auto id = b->id;
264 BEAST_EXPECT(TIBase::getState(id) == Alive);
266 BEAST_EXPECT(TIBase::getState(id) == Alive);
267 auto s = w.lock();
268 BEAST_EXPECT(s && s->useCount() == 2);
269 b.reset();
270 BEAST_EXPECT(TIBase::getState(id) == Alive);
271 BEAST_EXPECT(s && s->useCount() == 1);
272 s.reset();
273 BEAST_EXPECT(TIBase::getState(id) == PartiallyDeleted);
274 BEAST_EXPECT(w.expired());
275 s = w.lock();
276 // Cannot convert a weak pointer to a strong pointer if object is
277 // already partially deleted
278 BEAST_EXPECT(!s);
279 w.reset();
280 BEAST_EXPECT(TIBase::getState(id) == Deleted);
281 }
282 {
283 TIBase::ResetStatesGuard const rsg{true};
284
285 using enum TrackedState;
286 using swu = SharedWeakUnion<TIBase>;
288 BEAST_EXPECT(b.isStrong() && b.useCount() == 1);
289 auto id = b.get()->id;
290 BEAST_EXPECT(TIBase::getState(id) == Alive);
291 swu w = b;
292 BEAST_EXPECT(TIBase::getState(id) == Alive);
293 BEAST_EXPECT(w.isStrong() && b.useCount() == 2);
294 w.convertToWeak();
295 BEAST_EXPECT(w.isWeak() && b.useCount() == 1);
296 swu s = w;
297 BEAST_EXPECT(s.isWeak() && b.useCount() == 1);
298 s.convertToStrong();
299 BEAST_EXPECT(s.isStrong() && b.useCount() == 2);
300 b.reset();
301 BEAST_EXPECT(TIBase::getState(id) == Alive);
302 BEAST_EXPECT(s.useCount() == 1);
303 BEAST_EXPECT(!w.expired());
304 s.reset();
305 BEAST_EXPECT(TIBase::getState(id) == PartiallyDeleted);
306 BEAST_EXPECT(w.expired());
307 w.convertToStrong();
308 // Cannot convert a weak pointer to a strong pointer if object is
309 // already partially deleted
310 BEAST_EXPECT(w.isWeak());
311 w.reset();
312 BEAST_EXPECT(TIBase::getState(id) == Deleted);
313 }
314 {
315 // Testing SharedWeakUnion assignment operator
316
317 TIBase::ResetStatesGuard const rsg{true};
318
319 auto strong1 = makeSharedIntrusive<TIBase>();
320 auto strong2 = makeSharedIntrusive<TIBase>();
321
322 auto id1 = strong1->id;
323 auto id2 = strong2->id;
324
325 BEAST_EXPECT(id1 != id2);
326
327 SharedWeakUnion<TIBase> union1 = strong1;
328 SharedWeakUnion<TIBase> union2 = strong2;
329
330 BEAST_EXPECT(union1.isStrong());
331 BEAST_EXPECT(union2.isStrong());
332 BEAST_EXPECT(union1.get() == strong1.get());
333 BEAST_EXPECT(union2.get() == strong2.get());
334
335 // 1) Normal assignment: explicitly calls SharedWeakUnion assignment
336 union1 = union2;
337 BEAST_EXPECT(union1.isStrong());
338 BEAST_EXPECT(union2.isStrong());
339 BEAST_EXPECT(union1.get() == union2.get());
340 BEAST_EXPECT(TIBase::getState(id1) == TrackedState::Alive);
341 BEAST_EXPECT(TIBase::getState(id2) == TrackedState::Alive);
342
343 // 2) Test self-assignment
344 BEAST_EXPECT(union1.isStrong());
345 BEAST_EXPECT(TIBase::getState(id1) == TrackedState::Alive);
346 int const initialRefCount = strong1->useCount();
347#pragma clang diagnostic push
348#pragma clang diagnostic ignored "-Wself-assign-overloaded"
349 union1 = union1; // Self-assignment
350#pragma clang diagnostic pop
351 BEAST_EXPECT(union1.isStrong());
352 BEAST_EXPECT(TIBase::getState(id1) == TrackedState::Alive);
353 BEAST_EXPECT(strong1->useCount() == initialRefCount);
354
355 // 3) Test assignment from null union pointer
356 union1 = SharedWeakUnion<TIBase>();
357 BEAST_EXPECT(union1.get() == nullptr);
358
359 // 4) Test assignment to expired union pointer
360 strong2.reset();
361 union2.reset();
362 union1 = union2;
363 BEAST_EXPECT(union1.get() == nullptr);
364 BEAST_EXPECT(TIBase::getState(id2) == TrackedState::Deleted);
365 }
366 }
367
368 void
370 {
371 testcase("Partial Delete");
372
373 // This test creates two threads. One with a strong pointer and one
374 // with a weak pointer. The strong pointer is reset while the weak
375 // pointer still holds a reference, triggering a partial delete.
376 // While the partial delete function runs (a sleep is inserted) the
377 // weak pointer is reset. The destructor should wait to run until
378 // after the partial delete function has completed running.
379
380 using enum TrackedState;
381
382 TIBase::ResetStatesGuard const rsg{true};
383
384 auto strong = makeSharedIntrusive<TIBase>();
385 WeakIntrusive<TIBase> weak{strong};
386 bool destructorRan = false;
387 bool partialDeleteRan = false;
388 std::latch partialDeleteStartedSyncPoint{2};
389 strong->tracingCallback = [&](TrackedState cur, std::optional<TrackedState> next) {
390 using enum TrackedState;
391 if (next == DeletedStarted)
392 {
393 // strong goes out of scope while weak is still in scope
394 // This checks that partialDelete has run to completion
395 // before the destructor is called. A sleep is inserted
396 // inside the partial delete to make sure the destructor is
397 // given an opportunity to run during partial delete.
398 BEAST_EXPECT(cur == PartiallyDeleted);
399 }
400 if (next == PartiallyDeletedStarted)
401 {
402 partialDeleteStartedSyncPoint.arrive_and_wait();
403 using namespace std::chrono_literals;
404 // Sleep and let the weak pointer go out of scope,
405 // potentially triggering a destructor while partial delete
406 // is running. The test is to make sure that doesn't happen.
408 }
409 if (next == PartiallyDeleted)
410 {
411 BEAST_EXPECT(!partialDeleteRan && !destructorRan);
412 partialDeleteRan = true;
413 }
414 if (next == Deleted)
415 {
416 BEAST_EXPECT(!destructorRan);
417 destructorRan = true;
418 }
419 };
420 std::thread t1{[&] {
421 partialDeleteStartedSyncPoint.arrive_and_wait();
422 weak.reset(); // Trigger a full delete as soon as the partial
423 // delete starts
424 }};
425 std::thread t2{[&] {
426 strong.reset(); // Trigger a partial delete
427 }};
428 t1.join();
429 t2.join();
430
431 BEAST_EXPECT(destructorRan && partialDeleteRan);
432 }
433
434 void
436 {
437 testcase("Destructor");
438
439 // This test creates two threads. One with a strong pointer and one
440 // with a weak pointer. The weak pointer is reset while the strong
441 // pointer still holds a reference. Then the strong pointer is
442 // reset. Only the destructor should run. The partial destructor
443 // should not be called. Since the weak reset runs to completion
444 // before the strong pointer is reset, threading doesn't add much to
445 // this test, but there is no harm in keeping it.
446
447 using enum TrackedState;
448
449 TIBase::ResetStatesGuard const rsg{true};
450
451 auto strong = makeSharedIntrusive<TIBase>();
452 WeakIntrusive<TIBase> weak{strong};
453 bool destructorRan = false;
454 bool partialDeleteRan = false;
455 std::latch weakResetSyncPoint{2};
456 strong->tracingCallback = [&](TrackedState cur, std::optional<TrackedState> next) {
457 using enum TrackedState;
458 if (next == PartiallyDeleted)
459 {
460 BEAST_EXPECT(!partialDeleteRan && !destructorRan);
461 partialDeleteRan = true;
462 }
463 if (next == Deleted)
464 {
465 BEAST_EXPECT(!destructorRan);
466 destructorRan = true;
467 }
468 };
469 std::thread t1{[&] {
470 weak.reset();
471 weakResetSyncPoint.arrive_and_wait();
472 }};
473 std::thread t2{[&] {
474 weakResetSyncPoint.arrive_and_wait();
475 strong.reset(); // Trigger a partial delete
476 }};
477 t1.join();
478 t2.join();
479
480 BEAST_EXPECT(destructorRan && !partialDeleteRan);
481 }
482
483 void
485 {
486 testcase("Multithreaded Clear Mixed Variant");
487
488 // This test creates and destroys many strong and weak pointers in a
489 // loop. There is a random mix of strong and weak pointers stored in
490 // a vector (held as a variant). Both threads clear all the pointers
491 // and check that the invariants hold.
492
493 using enum TrackedState;
494 TIBase::ResetStatesGuard const rsg{true};
495
496 std::atomic<int> destructionState{0};
497 // returns destructorRan and partialDestructorRan (in that order)
498 auto getDestructorState = [&]() -> std::pair<bool, bool> {
499 int const s = destructionState.load(std::memory_order_relaxed);
500 return {(s & 1) != 0, (s & 2) != 0};
501 };
502 auto setDestructorRan = [&]() -> void {
503 destructionState.fetch_or(1, std::memory_order_acq_rel);
504 };
505 auto setPartialDeleteRan = [&]() -> void {
506 destructionState.fetch_or(2, std::memory_order_acq_rel);
507 };
508 auto tracingCallback = [&](TrackedState cur, std::optional<TrackedState> next) {
509 using enum TrackedState;
510 auto [destructorRan, partialDeleteRan] = getDestructorState();
511 if (next == PartiallyDeleted)
512 {
513 BEAST_EXPECT(!partialDeleteRan && !destructorRan);
514 setPartialDeleteRan();
515 }
516 if (next == Deleted)
517 {
518 BEAST_EXPECT(!destructorRan);
519 setDestructorRan();
520 }
521 };
522 auto createVecOfPointers = [&](auto const& toClone, std::default_random_engine& eng)
525 std::uniform_int_distribution<> toCreateDist(4, 64);
526 std::uniform_int_distribution<> isStrongDist(0, 1);
527 auto numToCreate = toCreateDist(eng);
528 result.reserve(numToCreate);
529 for (int i = 0; i < numToCreate; ++i)
530 {
531 if (isStrongDist(eng))
532 {
533 result.emplace_back(SharedIntrusive<TIBase>(toClone));
534 }
535 else
536 {
537 result.emplace_back(WeakIntrusive<TIBase>(toClone));
538 }
539 }
540 return result;
541 };
542 static constexpr int kLoopIters = 2 * 1024;
543 static constexpr int kNumThreads = 16;
545 Barrier loopStartSyncPoint{kNumThreads};
546 Barrier postCreateToCloneSyncPoint{kNumThreads};
547 Barrier postCreateVecOfPointersSyncPoint{kNumThreads};
548 auto engines = [&]() -> std::vector<std::default_random_engine> {
551 result.reserve(kNumThreads);
552 for (int i = 0; i < kNumThreads; ++i)
553 result.emplace_back(rd());
554 return result;
555 }();
556
557 // cloneAndDestroy clones the strong pointer into a vector of mixed
558 // strong and weak pointers and destroys them all at once.
559 // threadId==0 is special.
560 auto cloneAndDestroy = [&](int threadId) {
561 for (int i = 0; i < kLoopIters; ++i)
562 {
563 // ------ Sync Point ------
564 loopStartSyncPoint.arriveAndWait();
565
566 // only thread 0 should reset the state
568 if (threadId == 0)
569 {
570 // Thread 0 is the genesis thread. It creates the strong
571 // pointers to be cloned by the other threads. This
572 // thread will also check that the destructor ran and
573 // clear the temporary variables.
574
575 rsg.emplace(false);
576 auto [destructorRan, partialDeleteRan] = getDestructorState();
577 BEAST_EXPECT(!i || destructorRan);
578 destructionState.store(0, std::memory_order_release);
579
580 toClone.clear();
581 toClone.resize(kNumThreads);
582 auto strong = makeSharedIntrusive<TIBase>();
583 strong->tracingCallback = tracingCallback;
584 std::ranges::fill(toClone, strong);
585 }
586
587 // ------ Sync Point ------
588 postCreateToCloneSyncPoint.arriveAndWait();
589
590 auto v = createVecOfPointers(toClone[threadId], engines[threadId]);
591 toClone[threadId].reset();
592
593 // ------ Sync Point ------
594 postCreateVecOfPointersSyncPoint.arriveAndWait();
595
596 v.clear();
597 }
598 };
600 threads.reserve(kNumThreads);
601 for (int i = 0; i < kNumThreads; ++i)
602 {
603 threads.emplace_back(cloneAndDestroy, i);
604 }
605 for (int i = 0; i < kNumThreads; ++i)
606 {
607 threads[i].join();
608 }
609 }
610
611 void
613 {
614 testcase("Multithreaded Clear Mixed Union");
615
616 // This test creates and destroys many SharedWeak pointers in a
617 // loop. All the pointers start as strong and a loop randomly
618 // convert them between strong and weak pointers. Both threads clear
619 // all the pointers and check that the invariants hold.
620 //
621 // Note: This test also differs from the test above in that the pointers
622 // randomly change from strong to weak and from weak to strong in a
623 // loop. This can't be done in the variant test above because variant is
624 // not thread safe while the SharedWeakUnion is thread safe.
625
626 using enum TrackedState;
627
628 TIBase::ResetStatesGuard const rsg{true};
629
630 std::atomic<int> destructionState{0};
631 // returns destructorRan and partialDestructorRan (in that order)
632 auto getDestructorState = [&]() -> std::pair<bool, bool> {
633 int const s = destructionState.load(std::memory_order_relaxed);
634 return {(s & 1) != 0, (s & 2) != 0};
635 };
636 auto setDestructorRan = [&]() -> void {
637 destructionState.fetch_or(1, std::memory_order_acq_rel);
638 };
639 auto setPartialDeleteRan = [&]() -> void {
640 destructionState.fetch_or(2, std::memory_order_acq_rel);
641 };
642 auto tracingCallback = [&](TrackedState cur, std::optional<TrackedState> next) {
643 using enum TrackedState;
644 auto [destructorRan, partialDeleteRan] = getDestructorState();
645 if (next == PartiallyDeleted)
646 {
647 BEAST_EXPECT(!partialDeleteRan && !destructorRan);
648 setPartialDeleteRan();
649 }
650 if (next == Deleted)
651 {
652 BEAST_EXPECT(!destructorRan);
653 setDestructorRan();
654 }
655 };
656 auto createVecOfPointers =
657 [&](auto const& toClone,
660 std::uniform_int_distribution<> toCreateDist(4, 64);
661 auto numToCreate = toCreateDist(eng);
662 result.reserve(numToCreate);
663 for (int i = 0; i < numToCreate; ++i)
664 result.emplace_back(SharedIntrusive<TIBase>(toClone));
665 return result;
666 };
667 static constexpr int kLoopIters = 2 * 1024;
668 static constexpr int kFlipPointersLoopIters = 256;
669 static constexpr int kNumThreads = 16;
671 Barrier loopStartSyncPoint{kNumThreads};
672 Barrier postCreateToCloneSyncPoint{kNumThreads};
673 Barrier postCreateVecOfPointersSyncPoint{kNumThreads};
674 Barrier postFlipPointersLoopSyncPoint{kNumThreads};
675 auto engines = [&]() -> std::vector<std::default_random_engine> {
678 result.reserve(kNumThreads);
679 for (int i = 0; i < kNumThreads; ++i)
680 result.emplace_back(rd());
681 return result;
682 }();
683
684 // cloneAndDestroy clones the strong pointer into a vector of
685 // mixed strong and weak pointers, runs a loop that randomly
686 // changes strong pointers to weak pointers, and destroys them
687 // all at once.
688 auto cloneAndDestroy = [&](int threadId) {
689 for (int i = 0; i < kLoopIters; ++i)
690 {
691 // ------ Sync Point ------
692 loopStartSyncPoint.arriveAndWait();
693
694 // only thread 0 should reset the state
696 if (threadId == 0)
697 {
698 // threadId 0 is the genesis thread. It creates the
699 // strong point to be cloned by the other threads. This
700 // thread will also check that the destructor ran and
701 // clear the temporary variables.
702 rsg.emplace(false);
703 auto [destructorRan, partialDeleteRan] = getDestructorState();
704 BEAST_EXPECT(!i || destructorRan);
705 destructionState.store(0, std::memory_order_release);
706
707 toClone.clear();
708 toClone.resize(kNumThreads);
709 auto strong = makeSharedIntrusive<TIBase>();
710 strong->tracingCallback = tracingCallback;
711 std::ranges::fill(toClone, strong);
712 }
713
714 // ------ Sync Point ------
715 postCreateToCloneSyncPoint.arriveAndWait();
716
717 auto v = createVecOfPointers(toClone[threadId], engines[threadId]);
718 toClone[threadId].reset();
719
720 // ------ Sync Point ------
721 postCreateVecOfPointersSyncPoint.arriveAndWait();
722
723 std::uniform_int_distribution<> isStrongDist(0, 1);
724 for (int f = 0; f < kFlipPointersLoopIters; ++f)
725 {
726 for (auto& p : v)
727 {
728 if (isStrongDist(engines[threadId]))
729 {
730 p.convertToStrong();
731 }
732 else
733 {
734 p.convertToWeak();
735 }
736 }
737 }
738
739 // ------ Sync Point ------
740 postFlipPointersLoopSyncPoint.arriveAndWait();
741
742 v.clear();
743 }
744 };
746 threads.reserve(kNumThreads);
747 for (int i = 0; i < kNumThreads; ++i)
748 {
749 threads.emplace_back(cloneAndDestroy, i);
750 }
751 for (int i = 0; i < kNumThreads; ++i)
752 {
753 threads[i].join();
754 }
755 }
756
757 void
759 {
760 testcase("Multithreaded Locking Weak");
761
762 // This test creates a single shared atomic pointer that multiple thread
763 // create weak pointers from. The threads then lock the weak pointers.
764 // Both threads clear all the pointers and check that the invariants
765 // hold.
766
767 using enum TrackedState;
768
769 TIBase::ResetStatesGuard const rsg{true};
770
771 std::atomic<int> destructionState{0};
772 // returns destructorRan and partialDestructorRan (in that order)
773 auto getDestructorState = [&]() -> std::pair<bool, bool> {
774 int const s = destructionState.load(std::memory_order_relaxed);
775 return {(s & 1) != 0, (s & 2) != 0};
776 };
777 auto setDestructorRan = [&]() -> void {
778 destructionState.fetch_or(1, std::memory_order_acq_rel);
779 };
780 auto setPartialDeleteRan = [&]() -> void {
781 destructionState.fetch_or(2, std::memory_order_acq_rel);
782 };
783 auto tracingCallback = [&](TrackedState cur, std::optional<TrackedState> next) {
784 using enum TrackedState;
785 auto [destructorRan, partialDeleteRan] = getDestructorState();
786 if (next == PartiallyDeleted)
787 {
788 BEAST_EXPECT(!partialDeleteRan && !destructorRan);
789 setPartialDeleteRan();
790 }
791 if (next == Deleted)
792 {
793 BEAST_EXPECT(!destructorRan);
794 setDestructorRan();
795 }
796 };
797
798 static constexpr int kLoopIters = 2 * 1024;
799 static constexpr int kLockWeakLoopIters = 256;
800 static constexpr int kNumThreads = 16;
802 Barrier loopStartSyncPoint{kNumThreads};
803 Barrier postCreateToLockSyncPoint{kNumThreads};
804 Barrier postLockWeakLoopSyncPoint{kNumThreads};
805
806 // lockAndDestroy creates weak pointers from the strong pointer
807 // and runs a loop that locks the weak pointer. At the end of the loop
808 // all the pointers are destroyed all at once.
809 auto lockAndDestroy = [&](int threadId) {
810 for (int i = 0; i < kLoopIters; ++i)
811 {
812 // ------ Sync Point ------
813 loopStartSyncPoint.arriveAndWait();
814
815 // only thread 0 should reset the state
817 if (threadId == 0)
818 {
819 // threadId 0 is the genesis thread. It creates the
820 // strong point to be locked by the other threads. This
821 // thread will also check that the destructor ran and
822 // clear the temporary variables.
823 rsg.emplace(false);
824 auto [destructorRan, partialDeleteRan] = getDestructorState();
825 BEAST_EXPECT(!i || destructorRan);
826 destructionState.store(0, std::memory_order_release);
827
828 toLock.clear();
829 toLock.resize(kNumThreads);
830 auto strong = makeSharedIntrusive<TIBase>();
831 strong->tracingCallback = tracingCallback;
832 std::ranges::fill(toLock, strong);
833 }
834
835 // ------ Sync Point ------
836 postCreateToLockSyncPoint.arriveAndWait();
837
838 // Multiple threads all create a weak pointer from the same
839 // strong pointer
840 WeakIntrusive const weak{toLock[threadId]};
841 for (int wi = 0; wi < kLockWeakLoopIters; ++wi)
842 {
843 BEAST_EXPECT(!weak.expired());
844 auto strong = weak.lock();
845 BEAST_EXPECT(strong);
846 }
847
848 // ------ Sync Point ------
849 postLockWeakLoopSyncPoint.arriveAndWait();
850
851 toLock[threadId].reset();
852 }
853 };
855 threads.reserve(kNumThreads);
856 for (int i = 0; i < kNumThreads; ++i)
857 {
858 threads.emplace_back(lockAndDestroy, i);
859 }
860 for (int i = 0; i < kNumThreads; ++i)
861 {
862 threads[i].join();
863 }
864 }
865
866 void
876}; // namespace tests
877
878BEAST_DEFINE_TESTSUITE(IntrusiveShared, basics, xrpl);
879} // namespace xrpl::tests
T arrive_and_wait(T... args)
A testsuite class.
Definition suite.h:50
TestcaseT testcase
Memberspace for declaring test cases.
Definition suite.h:149
A shared intrusive pointer class that supports weak pointers.
A combination of a strong and a weak intrusive pointer stored in the space of a single pointer.
void reset()
Set the pointer to null, decrement the appropriate ref count, and run the appropriate release action.
T * get() const
If this is a strong pointer, return the raw pointer.
bool isStrong() const
Return true is this represents a strong pointer.
A weak intrusive pointer class for the SharedIntrusive pointer class.
SharedIntrusive< T > lock() const
Get a strong pointer from the weak pointer, if possible.
bool expired() const
Return true if the strong count is zero.
void reset()
Set the pointer to null and decrement the weak count.
void run() override
Runs the suite.
T clear(T... args)
T emplace_back(T... args)
T emplace(T... args)
T empty(T... args)
T fetch_or(T... args)
T fill(T... args)
T join(T... args)
T load(T... args)
BEAST_DEFINE_TESTSUITE(IntrusiveShared, basics, xrpl)
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
void partialDestructorFinished(T **o)
SharedIntrusive< TT > makeSharedIntrusive(Args &&... args)
Create a shared intrusive pointer.
T push_back(T... args)
T reserve(T... args)
T resize(T... args)
T size(T... args)
T sleep_for(T... args)
T store(T... args)
Experimentally, we discovered that using std::barrier performs extremely poorly (~1 hour vs ~1 minute...
std::condition_variable cv