xrpld
Loading...
Searching...
No Matches
IntrusiveShared.cpp
1#include <xrpl/basics/IntrusivePointer.h> // IWYU pragma: keep
2#include <xrpl/basics/IntrusivePointer.ipp> // IWYU pragma: keep
3#include <xrpl/basics/IntrusiveRefCounts.h>
4
5#include <gtest/gtest.h>
6
7#include <algorithm>
8#include <array>
9#include <atomic>
10#include <chrono> // IWYU pragma: keep
11#include <condition_variable>
12#include <cstddef>
13#include <cstdint>
14#include <functional>
15#include <latch>
16#include <mutex>
17#include <optional>
18#include <random>
19#include <stdexcept>
20#include <thread>
21#include <utility>
22#include <variant>
23#include <vector>
24
25namespace xrpl::tests {
26
27/*
28 * Experimentally, we discovered that using std::barrier performs extremely
29 * poorly (~1 hour vs ~1 minute to run the test suite) in certain macOS
30 * environments. To unblock our macOS CI pipeline, we replaced std::barrier with a
31 * custom mutex-based barrier (Barrier) that significantly improves performance
32 * without compromising correctness. For future reference, if we ever consider
33 * reintroducing std::barrier, the following configuration is known to exhibit the
34 * problem:
35 *
36 * Model Name: Mac mini
37 * Model Identifier: Mac14,3
38 * Model Number: Z16K000R4LL/A
39 * Chip: Apple M2
40 * Total Number of Cores: 8 (4 performance and 4 efficiency)
41 * Memory: 24 GB
42 * System Firmware Version: 11881.41.5
43 * OS Loader Version: 11881.1.1
44 * Apple clang version 16.0.0 (clang-1600.0.26.3)
45 * Target: arm64-apple-darwin24.0.0
46 * Thread model: posix
47 *
48 */
49struct Barrier
50{
56
57 explicit Barrier(std::size_t n) : count(n), initial(n)
58 {
59 }
60
61 void
63 {
65 auto const currentGeneration = generation;
66 if (--count == 0)
67 {
68 ++generation;
69 count = initial;
70 cv.notify_all();
71 }
72 else
73 {
74 cv.wait(lock, [&] { return generation != currentGeneration; });
75 }
76 }
77};
78
79namespace {
80enum class TrackedState : std::uint8_t {
81 Uninitialized,
82 Alive,
83 PartiallyDeletedStarted,
84 PartiallyDeleted,
85 DeletedStarted,
86 Deleted
87};
88
89class TIBase : public IntrusiveRefCounts
90{
91public:
92 static constexpr std::size_t kMaxStates = 128;
93 static std::array<std::atomic<TrackedState>, kMaxStates> state;
94 static std::atomic<std::size_t> nextId;
95
96 static TrackedState
97 getState(std::size_t id)
98 {
99 if (id >= state.size())
100 throw std::out_of_range("TIBase state id out of range");
101
102 return state[id].load(std::memory_order_acquire);
103 }
104
105 static void
106 resetStates(bool resetCallback)
107 {
108 for (std::size_t i = 0; i < kMaxStates; ++i)
109 state[i].store(TrackedState::Uninitialized, std::memory_order_release);
110 nextId.store(0, std::memory_order_release);
111 if (resetCallback)
112 TIBase::tracingCallback = [](TrackedState, std::optional<TrackedState>) {};
113 }
114
115 struct ResetStatesGuard
116 {
117 bool resetCallback{false};
118
119 ResetStatesGuard(bool resetCallback) : resetCallback{resetCallback}
120 {
121 TIBase::resetStates(resetCallback);
122 }
123
124 ~ResetStatesGuard()
125 {
126 TIBase::resetStates(resetCallback);
127 }
128 };
129
130 TIBase() : id{checkoutID()}
131 {
132 state[id].store(TrackedState::Alive, std::memory_order_relaxed);
133 }
134
135 ~TIBase() override
136 {
137 using enum TrackedState;
138
139 tracingCallback(state[id].load(std::memory_order_relaxed), DeletedStarted);
140
141 // Use relaxed memory order to try to avoid atomic operations from
142 // adding additional memory synchronizations that may hide threading
143 // errors in the underlying shared pointer class.
144 state[id].store(DeletedStarted, std::memory_order_relaxed);
145
146 tracingCallback(DeletedStarted, Deleted);
147
148 state[id].store(TrackedState::Deleted, std::memory_order_relaxed);
149
150 tracingCallback(TrackedState::Deleted, std::nullopt);
151 }
152
153 void
154 partialDestructor() const
155 {
156 using enum TrackedState;
157
158 tracingCallback(state[id].load(std::memory_order_relaxed), PartiallyDeletedStarted);
159
160 state[id].store(PartiallyDeletedStarted, std::memory_order_relaxed);
161
162 tracingCallback(PartiallyDeletedStarted, PartiallyDeleted);
163
164 state[id].store(PartiallyDeleted, std::memory_order_relaxed);
165
166 tracingCallback(PartiallyDeleted, std::nullopt);
167 }
168
169 static std::function<void(TrackedState, std::optional<TrackedState>)> tracingCallback;
170
171 std::size_t const id;
172
173private:
174 static std::size_t
175 checkoutID()
176 {
177 auto const id = nextId.fetch_add(1, std::memory_order_acq_rel);
178 if (id >= state.size())
179 throw std::out_of_range("TIBase state capacity exceeded");
180
181 return id;
182 }
183};
184
185std::array<std::atomic<TrackedState>, TIBase::kMaxStates> TIBase::state;
186std::atomic<std::size_t> TIBase::nextId{0};
187
188std::function<void(TrackedState, std::optional<TrackedState>)> TIBase::tracingCallback =
189 [](TrackedState, std::optional<TrackedState>) {};
190
191} // namespace
192
193TEST(IntrusiveSharedTest, basics)
194{
195 {
196 TIBase::ResetStatesGuard const rsg{true};
197
198 TIBase const b;
199 EXPECT_EQ(b.useCount(), 1);
200 b.addWeakRef();
201 EXPECT_EQ(b.useCount(), 1);
202 auto s = b.releaseStrongRef();
204 EXPECT_EQ(b.useCount(), 0);
205 TIBase const* pb = &b;
207 EXPECT_FALSE(pb);
208 auto w = b.releaseWeakRef();
209 EXPECT_EQ(w, ReleaseWeakRefAction::Destroy);
210 }
211
214 {
215 TIBase::ResetStatesGuard const rsg{true};
216
217 using enum TrackedState;
219 auto id = b->id;
220 EXPECT_EQ(TIBase::getState(id), Alive);
221 EXPECT_EQ(b->useCount(), 1);
222 for (auto i = 0uz; i < 10; ++i)
223 strong.push_back(b);
224 b.reset();
225 EXPECT_EQ(TIBase::getState(id), Alive);
226 strong.resize(strong.size() - 1);
227 EXPECT_EQ(TIBase::getState(id), Alive);
228 strong.clear();
229 EXPECT_EQ(TIBase::getState(id), Deleted);
230
232 id = b->id;
233 EXPECT_EQ(TIBase::getState(id), Alive);
234 EXPECT_EQ(b->useCount(), 1);
235 for (auto i = 0uz; i < 10; ++i)
236 {
237 weak.emplace_back(b);
238 EXPECT_EQ(b->useCount(), 1);
239 }
240 EXPECT_EQ(TIBase::getState(id), Alive);
241 weak.resize(weak.size() - 1);
242 EXPECT_EQ(TIBase::getState(id), Alive);
243 b.reset();
244 EXPECT_EQ(TIBase::getState(id), PartiallyDeleted);
245 while (!weak.empty())
246 {
247 if (weak.resize(weak.size() - 1); !weak.empty())
248 {
249 EXPECT_EQ(TIBase::getState(id), PartiallyDeleted);
250 }
251 }
252 EXPECT_EQ(TIBase::getState(id), Deleted);
253 }
254 {
255 TIBase::ResetStatesGuard const rsg{true};
256
257 using enum TrackedState;
259 auto id = b->id;
260 EXPECT_EQ(TIBase::getState(id), Alive);
262 EXPECT_EQ(TIBase::getState(id), Alive);
263 auto s = w.lock();
264 EXPECT_TRUE(s && s->useCount() == 2);
265 b.reset();
266 EXPECT_TRUE(TIBase::getState(id) == Alive);
267 EXPECT_TRUE(s && s->useCount() == 1);
268 s.reset();
269 EXPECT_EQ(TIBase::getState(id), PartiallyDeleted);
270 EXPECT_TRUE(w.expired());
271 s = w.lock();
272 // Cannot convert a weak pointer to a strong pointer if object is
273 // already partially deleted
274 EXPECT_FALSE(s);
275 w.reset();
276 EXPECT_EQ(TIBase::getState(id), Deleted);
277 }
278 {
279 TIBase::ResetStatesGuard const rsg{true};
280
281 using enum TrackedState;
282 using SharedWeak = SharedWeakUnion<TIBase>;
283 SharedWeak b = makeSharedIntrusive<TIBase>();
284 EXPECT_TRUE(b.isStrong() && b.useCount() == 1);
285 auto id = b.get()->id;
286 EXPECT_EQ(TIBase::getState(id), Alive);
287 SharedWeak w = b;
288 EXPECT_TRUE(TIBase::getState(id) == Alive);
289 EXPECT_TRUE(w.isStrong() && b.useCount() == 2);
290 w.convertToWeak();
291 EXPECT_TRUE(w.isWeak() && b.useCount() == 1);
292 SharedWeak s = w;
293 EXPECT_TRUE(s.isWeak() && b.useCount() == 1);
294 s.convertToStrong();
295 EXPECT_TRUE(s.isStrong() && b.useCount() == 2);
296 b.reset();
297 EXPECT_EQ(TIBase::getState(id), Alive);
298 EXPECT_EQ(s.useCount(), 1);
299 EXPECT_FALSE(w.expired());
300 s.reset();
301 EXPECT_EQ(TIBase::getState(id), PartiallyDeleted);
302 EXPECT_TRUE(w.expired());
303 w.convertToStrong();
304 // Cannot convert a weak pointer to a strong pointer if object is
305 // already partially deleted
306 EXPECT_TRUE(w.isWeak());
307 w.reset();
308 EXPECT_EQ(TIBase::getState(id), Deleted);
309 }
310 {
311 // Testing SharedWeakUnion assignment operator
312
313 TIBase::ResetStatesGuard const rsg{true};
314
315 auto strong1 = makeSharedIntrusive<TIBase>();
316 auto strong2 = makeSharedIntrusive<TIBase>();
317
318 auto id1 = strong1->id;
319 auto id2 = strong2->id;
320
321 EXPECT_NE(id1, id2);
322
323 SharedWeakUnion<TIBase> union1 = strong1;
324 SharedWeakUnion<TIBase> union2 = strong2;
325
326 EXPECT_TRUE(union1.isStrong());
327 EXPECT_TRUE(union2.isStrong());
328 EXPECT_EQ(union1.get(), strong1.get());
329 EXPECT_EQ(union2.get(), strong2.get());
330
331 // 1) Normal assignment: explicitly calls SharedWeakUnion assignment
332 union1 = union2;
333 EXPECT_TRUE(union1.isStrong());
334 EXPECT_TRUE(union2.isStrong());
335 EXPECT_EQ(union1.get(), union2.get());
336 EXPECT_EQ(TIBase::getState(id1), TrackedState::Alive);
337 EXPECT_EQ(TIBase::getState(id2), TrackedState::Alive);
338
339 // 2) Test self-assignment
340 EXPECT_TRUE(union1.isStrong());
341 EXPECT_EQ(TIBase::getState(id1), TrackedState::Alive);
342 int const initialRefCount = strong1->useCount();
343#pragma clang diagnostic push
344#pragma clang diagnostic ignored "-Wself-assign-overloaded"
345 union1 = union1; // Self-assignment
346#pragma clang diagnostic pop
347 EXPECT_TRUE(union1.isStrong());
348 EXPECT_EQ(TIBase::getState(id1), TrackedState::Alive);
349 EXPECT_EQ(strong1->useCount(), initialRefCount);
350
351 // 3) Test assignment from null union pointer
352 union1 = SharedWeakUnion<TIBase>();
353 EXPECT_EQ(union1.get(), nullptr);
354
355 // 4) Test assignment to expired union pointer
356 strong2.reset();
357 union2.reset();
358 union1 = union2;
359 EXPECT_EQ(union1.get(), nullptr);
360 EXPECT_EQ(TIBase::getState(id2), TrackedState::Deleted);
361 }
362}
363
364TEST(IntrusiveSharedTest, partial_delete)
365{
366 // This test creates two threads. One with a strong pointer and one
367 // with a weak pointer. The strong pointer is reset while the weak
368 // pointer still holds a reference, triggering a partial delete.
369 // While the partial delete function runs (a sleep is inserted) the
370 // weak pointer is reset. The destructor should wait to run until
371 // after the partial delete function has completed running.
372
373 using enum TrackedState;
374
375 TIBase::ResetStatesGuard const rsg{true};
376
377 auto strong = makeSharedIntrusive<TIBase>();
378 WeakIntrusive<TIBase> weak{strong};
379 std::atomic<bool> destructorRan{false};
380 std::atomic<bool> partialDeleteRan{false};
381 std::latch partialDeleteStartedSyncPoint{2};
382
383 strong->tracingCallback = [&](TrackedState cur, std::optional<TrackedState> next) {
384 using enum TrackedState;
385 if (!next)
386 return;
387
388 switch (*next)
389 {
390 case DeletedStarted:
391 // strong goes out of scope while weak is still in scope
392 // This checks that partialDelete has run to completion
393 // before the destructor is called. A sleep is inserted
394 // inside the partial delete to make sure the destructor is
395 // given an opportunity to run during partial delete.
396 EXPECT_EQ(cur, PartiallyDeleted);
397 break;
398
399 case PartiallyDeletedStarted: {
400 partialDeleteStartedSyncPoint.arrive_and_wait();
401 using namespace std::chrono_literals;
402 // Sleep and let the weak pointer go out of scope,
403 // potentially triggering a destructor while partial delete
404 // is running. The test is to make sure that doesn't happen.
406 break;
407 }
408
409 case PartiallyDeleted:
410 EXPECT_FALSE(partialDeleteRan.exchange(true) || destructorRan.load());
411 break;
412
413 case Deleted:
414 EXPECT_FALSE(destructorRan.exchange(true));
415 break;
416
417 case Uninitialized:
418 case Alive:
419 break;
420 }
421 };
422
423 std::thread t1{[&] {
424 partialDeleteStartedSyncPoint.arrive_and_wait();
425 weak.reset(); // Trigger a full delete as soon as the partial
426 // delete starts
427 }};
428
429 std::thread t2{[&] {
430 strong.reset(); // Trigger a partial delete
431 }};
432
433 t1.join();
434 t2.join();
435
436 EXPECT_TRUE(destructorRan.load() && partialDeleteRan.load());
437}
438
439TEST(IntrusiveSharedTest, convert_last_strong_to_weak)
440{
441 using enum TrackedState;
442
443 TIBase::ResetStatesGuard const rsg{true};
444
446 auto const id = p.get()->id;
447 EXPECT_EQ(TIBase::getState(id), Alive);
448
449 EXPECT_TRUE(p.convertToWeak());
450 EXPECT_TRUE(p.isWeak());
451 EXPECT_TRUE(p.expired());
452 EXPECT_EQ(TIBase::getState(id), PartiallyDeleted);
453
454 p.reset();
455 EXPECT_EQ(TIBase::getState(id), Deleted);
456}
457
458TEST(IntrusiveSharedTest, multithreaded_convert_to_weak_partial_delete)
459{
460 using enum TrackedState;
461
462 TIBase::ResetStatesGuard const rsg{true};
463
465 SharedWeakUnion<TIBase> other = converted;
466 EXPECT_TRUE(other.convertToWeak());
467 EXPECT_EQ(converted.useCount(), 1);
468 auto const id = converted.get()->id;
469
470 std::atomic<bool> destructorRan{false};
471 std::atomic<bool> partialDeleteRan{false};
472 std::latch partialDeleteStartedSyncPoint{1};
473 std::latch otherReleasedSyncPoint{1};
474
475 TIBase::tracingCallback = [&](TrackedState cur, std::optional<TrackedState> next) {
476 if (!next)
477 return;
478
479 switch (*next)
480 {
481 case DeletedStarted:
482 EXPECT_EQ(cur, PartiallyDeleted);
483 break;
484
485 case PartiallyDeletedStarted:
486 partialDeleteStartedSyncPoint.count_down();
487 // Keep the partial delete running until the other weak pointer has been released.
488 otherReleasedSyncPoint.wait();
489 break;
490
491 case PartiallyDeleted:
492 EXPECT_FALSE(partialDeleteRan.exchange(true) || destructorRan.load());
493 break;
494
495 case Deleted:
496 EXPECT_FALSE(destructorRan.exchange(true));
497 break;
498
499 case Uninitialized:
500 case Alive:
501 break;
502 }
503 };
504
505 std::thread t1{[&] {
506 partialDeleteStartedSyncPoint.wait();
507 other.reset(); // Not the last weak ref, so must not trigger a delete
508 otherReleasedSyncPoint.count_down();
509 }};
510
511 std::thread t2{[&] {
512 EXPECT_TRUE(converted.convertToWeak()); // Trigger a partial delete
513 }};
514
515 t1.join();
516 t2.join();
517
518 EXPECT_TRUE(partialDeleteRan.load());
519 EXPECT_FALSE(destructorRan.load());
520 EXPECT_TRUE(converted.isWeak());
521 EXPECT_EQ(TIBase::getState(id), PartiallyDeleted);
522
523 converted.reset(); // Release the last weak ref
524 EXPECT_TRUE(destructorRan.load());
525 EXPECT_EQ(TIBase::getState(id), Deleted);
526}
527
528TEST(IntrusiveSharedTest, destructor)
529{
530 // This test creates two threads. One with a strong pointer and one
531 // with a weak pointer. The weak pointer is reset while the strong
532 // pointer still holds a reference. Then the strong pointer is
533 // reset. Only the destructor should run. The partial destructor
534 // should not be called. Since the weak reset runs to completion
535 // before the strong pointer is reset, threading doesn't add much to
536 // this test, but there is no harm in keeping it.
537
538 using enum TrackedState;
539
540 TIBase::ResetStatesGuard const rsg{true};
541
542 auto strong = makeSharedIntrusive<TIBase>();
543 WeakIntrusive<TIBase> weak{strong};
544 std::atomic<bool> destructorRan{false};
545 std::atomic<bool> partialDeleteRan{false};
546 std::latch weakResetSyncPoint{2};
547 strong->tracingCallback = [&](TrackedState cur, std::optional<TrackedState> next) {
548 using enum TrackedState;
549 if (!next)
550 return;
551
552 switch (*next)
553 {
554 case PartiallyDeleted:
555 EXPECT_FALSE(partialDeleteRan.exchange(true) || destructorRan.load());
556 break;
557
558 case Deleted:
559 EXPECT_FALSE(destructorRan.exchange(true));
560 break;
561
562 case Uninitialized:
563 case Alive:
564 case PartiallyDeletedStarted:
565 case DeletedStarted:
566 break;
567 }
568 };
569 std::thread t1{[&] {
570 weak.reset();
571 weakResetSyncPoint.arrive_and_wait();
572 }};
573 std::thread t2{[&] {
574 weakResetSyncPoint.arrive_and_wait();
575 strong.reset(); // Trigger a partial delete
576 }};
577 t1.join();
578 t2.join();
579
580 EXPECT_TRUE(destructorRan.load() && !partialDeleteRan.load());
581}
582
583TEST(IntrusiveSharedTest, multithreaded_clear_mixed_variant)
584{
585 // This test creates and destroys many strong and weak pointers in a
586 // loop. There is a random mix of strong and weak pointers stored in
587 // a vector (held as a variant). Both threads clear all the pointers
588 // and check that the invariants hold.
589
590 using enum TrackedState;
591 TIBase::ResetStatesGuard const rsg{true};
592
593 std::atomic<int> destructionState{0};
594 // returns destructorRan and partialDestructorRan (in that order)
595 auto getDestructorState = [&]() -> std::pair<bool, bool> {
596 int const s = destructionState.load(std::memory_order_relaxed);
597 return {(s & 1) != 0, (s & 2) != 0};
598 };
599 auto setDestructorRan = [&]() -> void {
600 destructionState.fetch_or(1, std::memory_order_acq_rel);
601 };
602 auto setPartialDeleteRan = [&]() -> void {
603 destructionState.fetch_or(2, std::memory_order_acq_rel);
604 };
605 auto tracingCallback = [&](TrackedState cur, std::optional<TrackedState> next) {
606 using enum TrackedState;
607 auto [destructorRan, partialDeleteRan] = getDestructorState();
608 if (!next)
609 return;
610
611 switch (*next)
612 {
613 case PartiallyDeleted:
614 EXPECT_FALSE(partialDeleteRan || destructorRan);
615 setPartialDeleteRan();
616 break;
617
618 case Deleted:
619 EXPECT_FALSE(destructorRan);
620 setDestructorRan();
621 break;
622
623 case Uninitialized:
624 case Alive:
625 case PartiallyDeletedStarted:
626 case DeletedStarted:
627 break;
628 }
629 };
630 auto createVecOfPointers = [&](auto const& toClone, std::default_random_engine& eng)
634 std::uniform_int_distribution<> isStrongDist(0, 1);
635 auto numToCreate = toCreateDist(eng);
636 result.reserve(numToCreate);
637 for (auto i = 0uz; i < numToCreate; ++i)
638 {
639 if (isStrongDist(eng))
640 {
641 result.emplace_back(SharedIntrusive<TIBase>(toClone));
642 }
643 else
644 {
645 result.emplace_back(WeakIntrusive<TIBase>(toClone));
646 }
647 }
648 return result;
649 };
650 constexpr auto kLoopIters = 2uz * 1024;
651 constexpr auto kNumThreads = 16uz;
653 Barrier loopStartSyncPoint{kNumThreads};
654 Barrier postCreateToCloneSyncPoint{kNumThreads};
655 Barrier postCreateVecOfPointersSyncPoint{kNumThreads};
656 auto engines = [&]() -> std::vector<std::default_random_engine> {
659 result.reserve(kNumThreads);
660 for (auto i = 0uz; i < kNumThreads; ++i)
661 result.emplace_back(rd());
662 return result;
663 }();
664
665 // cloneAndDestroy clones the strong pointer into a vector of mixed
666 // strong and weak pointers and destroys them all at once.
667 // threadId==0 is special.
668 auto cloneAndDestroy = [&](std::size_t threadId) {
669 for (auto i = 0uz; i < kLoopIters; ++i)
670 {
671 // ------ Sync Point ------
672 loopStartSyncPoint.arriveAndWait();
673
674 // only thread 0 should reset the state
676 if (threadId == 0)
677 {
678 // Thread 0 is the genesis thread. It creates the strong
679 // pointers to be cloned by the other threads. This
680 // thread will also check that the destructor ran and
681 // clear the temporary variables.
682
683 rsg.emplace(false);
684 auto [destructorRan, partialDeleteRan] = getDestructorState();
685 EXPECT_TRUE(i == 0 || destructorRan);
686 destructionState.store(0, std::memory_order_release);
687
688 toClone.clear();
689 toClone.resize(kNumThreads);
690 auto strong = makeSharedIntrusive<TIBase>();
691 strong->tracingCallback = tracingCallback;
692 std::ranges::fill(toClone, strong);
693 }
694
695 // ------ Sync Point ------
696 postCreateToCloneSyncPoint.arriveAndWait();
697
698 auto v = createVecOfPointers(toClone[threadId], engines[threadId]);
699 toClone[threadId].reset();
700
701 // ------ Sync Point ------
702 postCreateVecOfPointersSyncPoint.arriveAndWait();
703
704 v.clear();
705 }
706 };
708 threads.reserve(kNumThreads);
709 for (auto i = 0uz; i < kNumThreads; ++i)
710 {
711 threads.emplace_back(cloneAndDestroy, i);
712 }
713 for (auto i = 0uz; i < kNumThreads; ++i)
714 {
715 threads[i].join();
716 }
717}
718
719TEST(IntrusiveSharedTest, multithreaded_clear_mixed_union)
720{
721 // This test creates and destroys many SharedWeak pointers in a
722 // loop. All the pointers start as strong and a loop randomly
723 // convert them between strong and weak pointers. Both threads clear
724 // all the pointers and check that the invariants hold.
725 //
726 // Note: This test also differs from the test above in that the pointers
727 // randomly change from strong to weak and from weak to strong in a
728 // loop. This can't be done in the variant test above because variant is
729 // not thread safe while the SharedWeakUnion is thread safe.
730
731 using enum TrackedState;
732
733 TIBase::ResetStatesGuard const rsg{true};
734
735 std::atomic<int> destructionState{0};
736 // returns destructorRan and partialDestructorRan (in that order)
737 auto getDestructorState = [&]() -> std::pair<bool, bool> {
738 int const s = destructionState.load(std::memory_order_relaxed);
739 return {(s & 1) != 0, (s & 2) != 0};
740 };
741 auto setDestructorRan = [&]() -> void {
742 destructionState.fetch_or(1, std::memory_order_acq_rel);
743 };
744 auto setPartialDeleteRan = [&]() -> void {
745 destructionState.fetch_or(2, std::memory_order_acq_rel);
746 };
747 auto tracingCallback = [&](TrackedState cur, std::optional<TrackedState> next) {
748 using enum TrackedState;
749 auto [destructorRan, partialDeleteRan] = getDestructorState();
750 if (!next)
751 return;
752
753 switch (*next)
754 {
755 case PartiallyDeleted:
756 EXPECT_FALSE(partialDeleteRan || destructorRan);
757 setPartialDeleteRan();
758 break;
759
760 case Deleted:
761 EXPECT_FALSE(destructorRan);
762 setDestructorRan();
763 break;
764
765 case Uninitialized:
766 case Alive:
767 case PartiallyDeletedStarted:
768 case DeletedStarted:
769 break;
770 }
771 };
772 auto createVecOfPointers =
773 [&](auto const& toClone,
777 auto numToCreate = toCreateDist(eng);
778 result.reserve(numToCreate);
779 for (auto i = 0uz; i < numToCreate; ++i)
780 result.emplace_back(SharedIntrusive<TIBase>(toClone));
781 return result;
782 };
783 constexpr auto kLoopIters = 2uz * 1024;
784 constexpr auto kFlipPointersLoopIters = 256uz;
785 constexpr auto kNumThreads = 16uz;
787 Barrier loopStartSyncPoint{kNumThreads};
788 Barrier postCreateToCloneSyncPoint{kNumThreads};
789 Barrier postCreateVecOfPointersSyncPoint{kNumThreads};
790 Barrier postFlipPointersLoopSyncPoint{kNumThreads};
791 auto engines = [&]() -> std::vector<std::default_random_engine> {
794 result.reserve(kNumThreads);
795 for (auto i = 0uz; i < kNumThreads; ++i)
796 result.emplace_back(rd());
797 return result;
798 }();
799
800 // cloneAndDestroy clones the strong pointer into a vector of
801 // mixed strong and weak pointers, runs a loop that randomly
802 // changes strong pointers to weak pointers, and destroys them
803 // all at once.
804 auto cloneAndDestroy = [&](std::size_t threadId) {
805 for (auto i = 0uz; i < kLoopIters; ++i)
806 {
807 // ------ Sync Point ------
808 loopStartSyncPoint.arriveAndWait();
809
810 // only thread 0 should reset the state
812 if (threadId == 0)
813 {
814 // threadId 0 is the genesis thread. It creates the
815 // strong point to be cloned by the other threads. This
816 // thread will also check that the destructor ran and
817 // clear the temporary variables.
818 rsg.emplace(false);
819 auto [destructorRan, partialDeleteRan] = getDestructorState();
820 EXPECT_TRUE(i == 0 || destructorRan);
821 destructionState.store(0, std::memory_order_release);
822
823 toClone.clear();
824 toClone.resize(kNumThreads);
825 auto strong = makeSharedIntrusive<TIBase>();
826 strong->tracingCallback = tracingCallback;
827 std::ranges::fill(toClone, strong);
828 }
829
830 // ------ Sync Point ------
831 postCreateToCloneSyncPoint.arriveAndWait();
832
833 auto v = createVecOfPointers(toClone[threadId], engines[threadId]);
834 toClone[threadId].reset();
835
836 // ------ Sync Point ------
837 postCreateVecOfPointersSyncPoint.arriveAndWait();
838
839 std::uniform_int_distribution<> isStrongDist(0, 1);
840 for (auto f = 0uz; f < kFlipPointersLoopIters; ++f)
841 {
842 for (auto& p : v)
843 {
844 if (isStrongDist(engines[threadId]))
845 {
846 p.convertToStrong();
847 }
848 else
849 {
850 p.convertToWeak();
851 }
852 }
853 }
854
855 // ------ Sync Point ------
856 postFlipPointersLoopSyncPoint.arriveAndWait();
857
858 v.clear();
859 }
860 };
862 threads.reserve(kNumThreads);
863 for (auto i = 0uz; i < kNumThreads; ++i)
864 {
865 threads.emplace_back(cloneAndDestroy, i);
866 }
867 for (auto i = 0uz; i < kNumThreads; ++i)
868 {
869 threads[i].join();
870 }
871}
872
873TEST(IntrusiveSharedTest, multithreaded_locking_weak)
874{
875 // This test creates a single shared atomic pointer that multiple thread
876 // create weak pointers from. The threads then lock the weak pointers.
877 // Both threads clear all the pointers and check that the invariants
878 // hold.
879
880 using enum TrackedState;
881
882 TIBase::ResetStatesGuard const rsg{true};
883
884 std::atomic<int> destructionState{0};
885 // returns destructorRan and partialDestructorRan (in that order)
886 auto getDestructorState = [&]() -> std::pair<bool, bool> {
887 int const s = destructionState.load(std::memory_order_relaxed);
888 return {(s & 1) != 0, (s & 2) != 0};
889 };
890 auto setDestructorRan = [&]() -> void {
891 destructionState.fetch_or(1, std::memory_order_acq_rel);
892 };
893 auto setPartialDeleteRan = [&]() -> void {
894 destructionState.fetch_or(2, std::memory_order_acq_rel);
895 };
896 auto tracingCallback = [&](TrackedState cur, std::optional<TrackedState> next) {
897 using enum TrackedState;
898 auto [destructorRan, partialDeleteRan] = getDestructorState();
899 if (!next)
900 return;
901
902 switch (*next)
903 {
904 case PartiallyDeleted:
905 EXPECT_FALSE(partialDeleteRan || destructorRan);
906 setPartialDeleteRan();
907 break;
908
909 case Deleted:
910 EXPECT_FALSE(destructorRan);
911 setDestructorRan();
912 break;
913
914 case Uninitialized:
915 case Alive:
916 case PartiallyDeletedStarted:
917 case DeletedStarted:
918 break;
919 }
920 };
921
922 constexpr auto kLoopIters = 2uz * 1024;
923 constexpr auto kLockWeakLoopIters = 256uz;
924 constexpr auto kNumThreads = 16uz;
926 Barrier loopStartSyncPoint{kNumThreads};
927 Barrier postCreateToLockSyncPoint{kNumThreads};
928 Barrier postLockWeakLoopSyncPoint{kNumThreads};
929
930 // lockAndDestroy creates weak pointers from the strong pointer
931 // and runs a loop that locks the weak pointer. At the end of the loop
932 // all the pointers are destroyed all at once.
933 auto lockAndDestroy = [&](std::size_t threadId) {
934 for (auto i = 0uz; i < kLoopIters; ++i)
935 {
936 // ------ Sync Point ------
937 loopStartSyncPoint.arriveAndWait();
938
939 // only thread 0 should reset the state
941 if (threadId == 0)
942 {
943 // threadId 0 is the genesis thread. It creates the
944 // strong point to be locked by the other threads. This
945 // thread will also check that the destructor ran and
946 // clear the temporary variables.
947 rsg.emplace(false);
948 auto [destructorRan, partialDeleteRan] = getDestructorState();
949 EXPECT_TRUE(i == 0 || destructorRan);
950 destructionState.store(0, std::memory_order_release);
951
952 toLock.clear();
953 toLock.resize(kNumThreads);
954 auto strong = makeSharedIntrusive<TIBase>();
955 strong->tracingCallback = tracingCallback;
956 std::ranges::fill(toLock, strong);
957 }
958
959 // ------ Sync Point ------
960 postCreateToLockSyncPoint.arriveAndWait();
961
962 // Multiple threads all create a weak pointer from the same
963 // strong pointer
964 WeakIntrusive const weak{toLock[threadId]};
965 for (auto wi = 0uz; wi < kLockWeakLoopIters; ++wi)
966 {
967 EXPECT_FALSE(weak.expired());
968 auto strong = weak.lock();
969 EXPECT_TRUE(strong);
970 }
971
972 // ------ Sync Point ------
973 postLockWeakLoopSyncPoint.arriveAndWait();
974
975 toLock[threadId].reset();
976 }
977 };
979 threads.reserve(kNumThreads);
980 for (auto i = 0uz; i < kNumThreads; ++i)
981 {
982 threads.emplace_back(lockAndDestroy, i);
983 }
984 for (auto i = 0uz; i < kNumThreads; ++i)
985 {
986 threads[i].join();
987 }
988}
989
990} // namespace xrpl::tests
T arrive_and_wait(T... args)
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.
bool expired() const
Return true if there is a non-zero strong count.
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 convertToWeak()
If this is a strong pointer, attempt to convert it to a weak pointer.
bool isStrong() const
Return true is this represents a strong pointer.
std::size_t useCount() const
If this is a strong pointer, return the strong count.
bool isWeak() const
Return true is this represents a weak 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.
T clear(T... args)
T count_down(T... args)
T emplace_back(T... args)
T emplace(T... args)
T empty(T... args)
T exchange(T... args)
T fetch_or(T... args)
T fill(T... args)
T join(T... args)
T load(T... args)
TEST(IntrusiveSharedTest, basics)
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)
std::condition_variable cv
std::size_t const initial
T wait(T... args)