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, destructor)
440{
441 // This test creates two threads. One with a strong pointer and one
442 // with a weak pointer. The weak pointer is reset while the strong
443 // pointer still holds a reference. Then the strong pointer is
444 // reset. Only the destructor should run. The partial destructor
445 // should not be called. Since the weak reset runs to completion
446 // before the strong pointer is reset, threading doesn't add much to
447 // this test, but there is no harm in keeping it.
448
449 using enum TrackedState;
450
451 TIBase::ResetStatesGuard const rsg{true};
452
453 auto strong = makeSharedIntrusive<TIBase>();
454 WeakIntrusive<TIBase> weak{strong};
455 std::atomic<bool> destructorRan{false};
456 std::atomic<bool> partialDeleteRan{false};
457 std::latch weakResetSyncPoint{2};
458 strong->tracingCallback = [&](TrackedState cur, std::optional<TrackedState> next) {
459 using enum TrackedState;
460 if (!next)
461 return;
462
463 switch (*next)
464 {
465 case PartiallyDeleted:
466 EXPECT_FALSE(partialDeleteRan.exchange(true) || destructorRan.load());
467 break;
468
469 case Deleted:
470 EXPECT_FALSE(destructorRan.exchange(true));
471 break;
472
473 case Uninitialized:
474 case Alive:
475 case PartiallyDeletedStarted:
476 case DeletedStarted:
477 break;
478 }
479 };
480 std::thread t1{[&] {
481 weak.reset();
482 weakResetSyncPoint.arrive_and_wait();
483 }};
484 std::thread t2{[&] {
485 weakResetSyncPoint.arrive_and_wait();
486 strong.reset(); // Trigger a partial delete
487 }};
488 t1.join();
489 t2.join();
490
491 EXPECT_TRUE(destructorRan.load() && !partialDeleteRan.load());
492}
493
494TEST(IntrusiveSharedTest, multithreaded_clear_mixed_variant)
495{
496 // This test creates and destroys many strong and weak pointers in a
497 // loop. There is a random mix of strong and weak pointers stored in
498 // a vector (held as a variant). Both threads clear all the pointers
499 // and check that the invariants hold.
500
501 using enum TrackedState;
502 TIBase::ResetStatesGuard const rsg{true};
503
504 std::atomic<int> destructionState{0};
505 // returns destructorRan and partialDestructorRan (in that order)
506 auto getDestructorState = [&]() -> std::pair<bool, bool> {
507 int const s = destructionState.load(std::memory_order_relaxed);
508 return {(s & 1) != 0, (s & 2) != 0};
509 };
510 auto setDestructorRan = [&]() -> void {
511 destructionState.fetch_or(1, std::memory_order_acq_rel);
512 };
513 auto setPartialDeleteRan = [&]() -> void {
514 destructionState.fetch_or(2, std::memory_order_acq_rel);
515 };
516 auto tracingCallback = [&](TrackedState cur, std::optional<TrackedState> next) {
517 using enum TrackedState;
518 auto [destructorRan, partialDeleteRan] = getDestructorState();
519 if (!next)
520 return;
521
522 switch (*next)
523 {
524 case PartiallyDeleted:
525 EXPECT_FALSE(partialDeleteRan || destructorRan);
526 setPartialDeleteRan();
527 break;
528
529 case Deleted:
530 EXPECT_FALSE(destructorRan);
531 setDestructorRan();
532 break;
533
534 case Uninitialized:
535 case Alive:
536 case PartiallyDeletedStarted:
537 case DeletedStarted:
538 break;
539 }
540 };
541 auto createVecOfPointers = [&](auto const& toClone, std::default_random_engine& eng)
545 std::uniform_int_distribution<> isStrongDist(0, 1);
546 auto numToCreate = toCreateDist(eng);
547 result.reserve(numToCreate);
548 for (auto i = 0uz; i < numToCreate; ++i)
549 {
550 if (isStrongDist(eng))
551 {
552 result.emplace_back(SharedIntrusive<TIBase>(toClone));
553 }
554 else
555 {
556 result.emplace_back(WeakIntrusive<TIBase>(toClone));
557 }
558 }
559 return result;
560 };
561 constexpr auto kLoopIters = 2uz * 1024;
562 constexpr auto kNumThreads = 16uz;
564 Barrier loopStartSyncPoint{kNumThreads};
565 Barrier postCreateToCloneSyncPoint{kNumThreads};
566 Barrier postCreateVecOfPointersSyncPoint{kNumThreads};
567 auto engines = [&]() -> std::vector<std::default_random_engine> {
570 result.reserve(kNumThreads);
571 for (auto i = 0uz; i < kNumThreads; ++i)
572 result.emplace_back(rd());
573 return result;
574 }();
575
576 // cloneAndDestroy clones the strong pointer into a vector of mixed
577 // strong and weak pointers and destroys them all at once.
578 // threadId==0 is special.
579 auto cloneAndDestroy = [&](std::size_t threadId) {
580 for (auto i = 0uz; i < kLoopIters; ++i)
581 {
582 // ------ Sync Point ------
583 loopStartSyncPoint.arriveAndWait();
584
585 // only thread 0 should reset the state
587 if (threadId == 0)
588 {
589 // Thread 0 is the genesis thread. It creates the strong
590 // pointers to be cloned by the other threads. This
591 // thread will also check that the destructor ran and
592 // clear the temporary variables.
593
594 rsg.emplace(false);
595 auto [destructorRan, partialDeleteRan] = getDestructorState();
596 EXPECT_TRUE(i == 0 || destructorRan);
597 destructionState.store(0, std::memory_order_release);
598
599 toClone.clear();
600 toClone.resize(kNumThreads);
601 auto strong = makeSharedIntrusive<TIBase>();
602 strong->tracingCallback = tracingCallback;
603 std::ranges::fill(toClone, strong);
604 }
605
606 // ------ Sync Point ------
607 postCreateToCloneSyncPoint.arriveAndWait();
608
609 auto v = createVecOfPointers(toClone[threadId], engines[threadId]);
610 toClone[threadId].reset();
611
612 // ------ Sync Point ------
613 postCreateVecOfPointersSyncPoint.arriveAndWait();
614
615 v.clear();
616 }
617 };
619 threads.reserve(kNumThreads);
620 for (auto i = 0uz; i < kNumThreads; ++i)
621 {
622 threads.emplace_back(cloneAndDestroy, i);
623 }
624 for (auto i = 0uz; i < kNumThreads; ++i)
625 {
626 threads[i].join();
627 }
628}
629
630TEST(IntrusiveSharedTest, multithreaded_clear_mixed_union)
631{
632 // This test creates and destroys many SharedWeak pointers in a
633 // loop. All the pointers start as strong and a loop randomly
634 // convert them between strong and weak pointers. Both threads clear
635 // all the pointers and check that the invariants hold.
636 //
637 // Note: This test also differs from the test above in that the pointers
638 // randomly change from strong to weak and from weak to strong in a
639 // loop. This can't be done in the variant test above because variant is
640 // not thread safe while the SharedWeakUnion is thread safe.
641
642 using enum TrackedState;
643
644 TIBase::ResetStatesGuard const rsg{true};
645
646 std::atomic<int> destructionState{0};
647 // returns destructorRan and partialDestructorRan (in that order)
648 auto getDestructorState = [&]() -> std::pair<bool, bool> {
649 int const s = destructionState.load(std::memory_order_relaxed);
650 return {(s & 1) != 0, (s & 2) != 0};
651 };
652 auto setDestructorRan = [&]() -> void {
653 destructionState.fetch_or(1, std::memory_order_acq_rel);
654 };
655 auto setPartialDeleteRan = [&]() -> void {
656 destructionState.fetch_or(2, std::memory_order_acq_rel);
657 };
658 auto tracingCallback = [&](TrackedState cur, std::optional<TrackedState> next) {
659 using enum TrackedState;
660 auto [destructorRan, partialDeleteRan] = getDestructorState();
661 if (!next)
662 return;
663
664 switch (*next)
665 {
666 case PartiallyDeleted:
667 EXPECT_FALSE(partialDeleteRan || destructorRan);
668 setPartialDeleteRan();
669 break;
670
671 case Deleted:
672 EXPECT_FALSE(destructorRan);
673 setDestructorRan();
674 break;
675
676 case Uninitialized:
677 case Alive:
678 case PartiallyDeletedStarted:
679 case DeletedStarted:
680 break;
681 }
682 };
683 auto createVecOfPointers =
684 [&](auto const& toClone,
688 auto numToCreate = toCreateDist(eng);
689 result.reserve(numToCreate);
690 for (auto i = 0uz; i < numToCreate; ++i)
691 result.emplace_back(SharedIntrusive<TIBase>(toClone));
692 return result;
693 };
694 constexpr auto kLoopIters = 2uz * 1024;
695 constexpr auto kFlipPointersLoopIters = 256uz;
696 constexpr auto kNumThreads = 16uz;
698 Barrier loopStartSyncPoint{kNumThreads};
699 Barrier postCreateToCloneSyncPoint{kNumThreads};
700 Barrier postCreateVecOfPointersSyncPoint{kNumThreads};
701 Barrier postFlipPointersLoopSyncPoint{kNumThreads};
702 auto engines = [&]() -> std::vector<std::default_random_engine> {
705 result.reserve(kNumThreads);
706 for (auto i = 0uz; i < kNumThreads; ++i)
707 result.emplace_back(rd());
708 return result;
709 }();
710
711 // cloneAndDestroy clones the strong pointer into a vector of
712 // mixed strong and weak pointers, runs a loop that randomly
713 // changes strong pointers to weak pointers, and destroys them
714 // all at once.
715 auto cloneAndDestroy = [&](std::size_t threadId) {
716 for (auto i = 0uz; i < kLoopIters; ++i)
717 {
718 // ------ Sync Point ------
719 loopStartSyncPoint.arriveAndWait();
720
721 // only thread 0 should reset the state
723 if (threadId == 0)
724 {
725 // threadId 0 is the genesis thread. It creates the
726 // strong point to be cloned by the other threads. This
727 // thread will also check that the destructor ran and
728 // clear the temporary variables.
729 rsg.emplace(false);
730 auto [destructorRan, partialDeleteRan] = getDestructorState();
731 EXPECT_TRUE(i == 0 || destructorRan);
732 destructionState.store(0, std::memory_order_release);
733
734 toClone.clear();
735 toClone.resize(kNumThreads);
736 auto strong = makeSharedIntrusive<TIBase>();
737 strong->tracingCallback = tracingCallback;
738 std::ranges::fill(toClone, strong);
739 }
740
741 // ------ Sync Point ------
742 postCreateToCloneSyncPoint.arriveAndWait();
743
744 auto v = createVecOfPointers(toClone[threadId], engines[threadId]);
745 toClone[threadId].reset();
746
747 // ------ Sync Point ------
748 postCreateVecOfPointersSyncPoint.arriveAndWait();
749
750 std::uniform_int_distribution<> isStrongDist(0, 1);
751 for (auto f = 0uz; f < kFlipPointersLoopIters; ++f)
752 {
753 for (auto& p : v)
754 {
755 if (isStrongDist(engines[threadId]))
756 {
757 p.convertToStrong();
758 }
759 else
760 {
761 p.convertToWeak();
762 }
763 }
764 }
765
766 // ------ Sync Point ------
767 postFlipPointersLoopSyncPoint.arriveAndWait();
768
769 v.clear();
770 }
771 };
773 threads.reserve(kNumThreads);
774 for (auto i = 0uz; i < kNumThreads; ++i)
775 {
776 threads.emplace_back(cloneAndDestroy, i);
777 }
778 for (auto i = 0uz; i < kNumThreads; ++i)
779 {
780 threads[i].join();
781 }
782}
783
784TEST(IntrusiveSharedTest, multithreaded_locking_weak)
785{
786 // This test creates a single shared atomic pointer that multiple thread
787 // create weak pointers from. The threads then lock the weak pointers.
788 // Both threads clear all the pointers and check that the invariants
789 // hold.
790
791 using enum TrackedState;
792
793 TIBase::ResetStatesGuard const rsg{true};
794
795 std::atomic<int> destructionState{0};
796 // returns destructorRan and partialDestructorRan (in that order)
797 auto getDestructorState = [&]() -> std::pair<bool, bool> {
798 int const s = destructionState.load(std::memory_order_relaxed);
799 return {(s & 1) != 0, (s & 2) != 0};
800 };
801 auto setDestructorRan = [&]() -> void {
802 destructionState.fetch_or(1, std::memory_order_acq_rel);
803 };
804 auto setPartialDeleteRan = [&]() -> void {
805 destructionState.fetch_or(2, std::memory_order_acq_rel);
806 };
807 auto tracingCallback = [&](TrackedState cur, std::optional<TrackedState> next) {
808 using enum TrackedState;
809 auto [destructorRan, partialDeleteRan] = getDestructorState();
810 if (!next)
811 return;
812
813 switch (*next)
814 {
815 case PartiallyDeleted:
816 EXPECT_FALSE(partialDeleteRan || destructorRan);
817 setPartialDeleteRan();
818 break;
819
820 case Deleted:
821 EXPECT_FALSE(destructorRan);
822 setDestructorRan();
823 break;
824
825 case Uninitialized:
826 case Alive:
827 case PartiallyDeletedStarted:
828 case DeletedStarted:
829 break;
830 }
831 };
832
833 constexpr auto kLoopIters = 2uz * 1024;
834 constexpr auto kLockWeakLoopIters = 256uz;
835 constexpr auto kNumThreads = 16uz;
837 Barrier loopStartSyncPoint{kNumThreads};
838 Barrier postCreateToLockSyncPoint{kNumThreads};
839 Barrier postLockWeakLoopSyncPoint{kNumThreads};
840
841 // lockAndDestroy creates weak pointers from the strong pointer
842 // and runs a loop that locks the weak pointer. At the end of the loop
843 // all the pointers are destroyed all at once.
844 auto lockAndDestroy = [&](std::size_t threadId) {
845 for (auto i = 0uz; i < kLoopIters; ++i)
846 {
847 // ------ Sync Point ------
848 loopStartSyncPoint.arriveAndWait();
849
850 // only thread 0 should reset the state
852 if (threadId == 0)
853 {
854 // threadId 0 is the genesis thread. It creates the
855 // strong point to be locked by the other threads. This
856 // thread will also check that the destructor ran and
857 // clear the temporary variables.
858 rsg.emplace(false);
859 auto [destructorRan, partialDeleteRan] = getDestructorState();
860 EXPECT_TRUE(i == 0 || destructorRan);
861 destructionState.store(0, std::memory_order_release);
862
863 toLock.clear();
864 toLock.resize(kNumThreads);
865 auto strong = makeSharedIntrusive<TIBase>();
866 strong->tracingCallback = tracingCallback;
867 std::ranges::fill(toLock, strong);
868 }
869
870 // ------ Sync Point ------
871 postCreateToLockSyncPoint.arriveAndWait();
872
873 // Multiple threads all create a weak pointer from the same
874 // strong pointer
875 WeakIntrusive const weak{toLock[threadId]};
876 for (auto wi = 0uz; wi < kLockWeakLoopIters; ++wi)
877 {
878 EXPECT_FALSE(weak.expired());
879 auto strong = weak.lock();
880 EXPECT_TRUE(strong);
881 }
882
883 // ------ Sync Point ------
884 postLockWeakLoopSyncPoint.arriveAndWait();
885
886 toLock[threadId].reset();
887 }
888 };
890 threads.reserve(kNumThreads);
891 for (auto i = 0uz; i < kNumThreads; ++i)
892 {
893 threads.emplace_back(lockAndDestroy, i);
894 }
895 for (auto i = 0uz; i < kNumThreads; ++i)
896 {
897 threads[i].join();
898 }
899}
900
901} // 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.
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.
T clear(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