diff options
| -rw-r--r-- | src/rpc.cpp | 128 |
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()); |