upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/rpc.cpp128
1 files changed, 96 insertions, 32 deletions
diff --git a/src/rpc.cpp b/src/rpc.cpp
index 7ea0597..79947df 100644
--- a/src/rpc.cpp
+++ b/src/rpc.cpp
@@ -3542,54 +3542,118 @@ int RunPayoutLedgerSelfTest()
3542 3542
3543 // -- 6. Concurrent publishers cannot interleave a file ------------------- 3543 // -- 6. Concurrent publishers cannot interleave a file -------------------
3544 // 3544 //
3545 // Two threads renaming the same "<path>.tmp" over the same target was the 3545 // Two threads opening and renaming the same "<path>.tmp" over the same
3546 // original defect. The images are made big so a torn write shows up as 3546 // target was the original defect. This test was mutation-tested and did
3547 // unparseable JSON rather than by luck. 3547 // not catch it: with the file mutex removed the suite still went green 2
3548 // runs in 5, and the "never leave it unparseable" assertion fired 0 times
3549 // in 5. Two reasons, both fixed here.
3550 //
3551 // First, every image had the same structure and near enough the same
3552 // length, so an interleaving of two of them was still valid JSON. The
3553 // images now alternate between ONE entry and FIVE THOUSAND, so a write
3554 // that lands on top of another leaves either a truncated array or a small
3555 // image followed by the tail of a large one -- neither of which parses.
3556 //
3557 // Second, and worse, almost nothing was being written. Each thread walked
3558 // a permutation of the images, and PublishVersioned drops any blob that is
3559 // not newer than what is on disk, so only the record highs in each
3560 // permutation ever reached the disk -- on the order of ten writes in
3561 // total, four threads, no contention to speak of. Every publish here is
3562 // stamped from a shared counter at the moment it is made, so every one of
3563 // them is newer than the last and every one of them writes.
3564 //
3565 // And the file is watched while the writers run, not only inspected after
3566 // they finish. "The published file is never observed unparseable" is the
3567 // actual invariant -- rename(2) is atomic, so a reader can only ever see a
3568 // completely written image -- and checking it once at the end samples the
3569 // one moment when a torn file is least likely to still be there.
3548 { 3570 {
3549 const std::string racePath = dir + "/publish_race_selftest.json"; 3571 const std::string racePath = dir + "/publish_race_selftest.json";
3550 std::mutex raceMutex; 3572 std::mutex raceMutex;
3551 uint64 racePublished = 0; 3573 uint64 racePublished = 0;
3552 3574
3553 std::vector<VersionedBlob> images; 3575 std::string tiny, huge;
3554 for (int i = 0; i < 64; i++) { 3576 {
3555 nlohmann::json arr = nlohmann::json::array(); 3577 nlohmann::json one = nlohmann::json::array();
3556 for (int k = 0; k < 200; k++) { 3578 nlohmann::json o;
3557 nlohmann::json o; 3579 o["image"] = 0;
3558 o["image"] = i; 3580 o["entry"] = 0;
3559 o["entry"] = k; 3581 o["address"] = addrA;
3560 o["address"] = addrA; 3582 o["amount"] = (int64)COIN;
3561 o["amount"] = (int64)(k + 1) * COIN; 3583 one.push_back(o);
3562 arr.push_back(o); 3584 tiny = one.dump(2);
3585
3586 nlohmann::json many = nlohmann::json::array();
3587 for (int k = 0; k < 5000; k++) {
3588 nlohmann::json e;
3589 e["image"] = 1;
3590 e["entry"] = k;
3591 e["address"] = addrA;
3592 e["amount"] = (int64)(k + 1) * COIN;
3593 many.push_back(e);
3563 } 3594 }
3564 VersionedBlob v; 3595 huge = many.dump(2);
3565 v.blob = arr.dump(2);
3566 v.seq = (uint64)i + 1;
3567 images.push_back(v);
3568 } 3596 }
3569 3597
3598 const int kThreads = 4;
3599 const int kPerThread = 100;
3600 const uint64 kTotal = (uint64)kThreads * kPerThread;
3601
3602 std::atomic<uint64> nextSeq(0);
3603 std::atomic<bool> racing(true);
3604 std::atomic<int> tornReads(0);
3605 std::atomic<int> goodReads(0);
3606
3607 std::thread watcher([&]() {
3608 bool seenOnce = false;
3609 while (racing) {
3610 std::string s;
3611 if (!PayoutTestReadFile(racePath, s)) {
3612 // Only meaningful once the file exists at all.
3613 if (seenOnce) tornReads++;
3614 continue;
3615 }
3616 seenOnce = true;
3617 try {
3618 if (nlohmann::json::parse(s).is_array()) goodReads++;
3619 else tornReads++;
3620 } catch (...) {
3621 tornReads++;
3622 }
3623 }
3624 });
3625
3570 std::vector<std::thread> threads; 3626 std::vector<std::thread> threads;
3571 for (int t = 0; t < 4; t++) { 3627 for (int t = 0; t < kThreads; t++) {
3572 threads.push_back(std::thread([&, t]() { 3628 threads.push_back(std::thread([&]() {
3573 for (size_t i = 0; i < images.size(); i++) { 3629 for (int i = 0; i < kPerThread; i++) {
3574 size_t idx = (i * 7 + (size_t)t * 13) % images.size(); 3630 VersionedBlob v;
3575 PublishVersioned(racePath, images[idx], raceMutex, 3631 v.seq = ++nextSeq;
3576 racePublished, "publish race selftest"); 3632 v.blob = (v.seq & 1) ? tiny : huge;
3633 PublishVersioned(racePath, v, raceMutex, racePublished,
3634 "publish race selftest");
3577 } 3635 }
3578 })); 3636 }));
3579 } 3637 }
3580 for (std::thread& th : threads) 3638 for (std::thread& th : threads)
3581 th.join(); 3639 th.join();
3582 3640 racing = false;
3641 watcher.join();
3642
3643 check(goodReads > 0,
3644 "the race test actually observed the file while it was being"
3645 " written");
3646 check(tornReads == 0,
3647 "four threads publishing the same file never leave it"
3648 " unparseable, at any instant while they are doing it");
3649
3650 // The highest sequence number issued is the one the file must settle
3651 // on: PublishVersioned refuses anything older, so whichever order the
3652 // writes complete in, that image is the last one that can land.
3653 const std::string& expected = (kTotal & 1) ? tiny : huge;
3583 std::string onDisk; 3654 std::string onDisk;
3584 bool read = PayoutTestReadFile(racePath, onDisk); 3655 bool read = PayoutTestReadFile(racePath, onDisk);
3585 bool parses = false; 3656 check(read && onDisk == expected,
3586 if (read) {
3587 try { parses = nlohmann::json::parse(onDisk).is_array(); }
3588 catch (...) { parses = false; }
3589 }
3590 check(read && parses,
3591 "four threads publishing the same file never leave it unparseable");
3592 check(read && onDisk == images.back().blob,
3593 "the published file settles on the highest version, whatever the" 3657 "the published file settles on the highest version, whatever the"
3594 " completion order"); 3658 " completion order");
3595 remove(racePath.c_str()); 3659 remove(racePath.c_str());