diff options
Diffstat (limited to 'src/rpc.cpp')
| -rw-r--r-- | src/rpc.cpp | 290 |
1 files changed, 237 insertions, 53 deletions
diff --git a/src/rpc.cpp b/src/rpc.cpp index be265db..403cb14 100644 --- a/src/rpc.cpp +++ b/src/rpc.cpp | |||
| @@ -451,9 +451,20 @@ static std::string PoolRoundsFilePath() | |||
| 451 | // number; publishing happens with the data mutex released and refuses any | 451 | // number; publishing happens with the data mutex released and refuses any |
| 452 | // blob whose sequence number is not newer than what is already on disk. So | 452 | // blob whose sequence number is not newer than what is already on disk. So |
| 453 | // writers can queue in any order and finish in any order -- the file only | 453 | // writers can queue in any order and finish in any order -- the file only |
| 454 | // ever moves forwards. Crucially, no thread ever waits for I/O while holding | 454 | // ever moves forwards. |
| 455 | // gPayoutLedgerMutex / gPayoutExecMutex / gPoolRoundsMutex, which is the | 455 | // |
| 456 | // contention the merge was right to remove from the event loop. | 456 | // The other half of the rule is that no thread waits for I/O while holding |
| 457 | // gPayoutLedgerMutex, gPayoutExecMutex or gPoolRoundsMutex, which is the | ||
| 458 | // contention the merge was right to remove from the event loop. That was | ||
| 459 | // claimed here before it was true: pool_status.json is written by the same | ||
| 460 | // WriteFileAtomic and RefreshStats held gPayoutLedgerMutex at function scope | ||
| 461 | // across the call, so once the writes gained fsync() the event loop was doing | ||
| 462 | // two of them every ten seconds with the payout thread's mutex held -- worse | ||
| 463 | // than the buffered write the merge replaced. The status file now goes through | ||
| 464 | // the same serialise-under-the-lock / publish-outside-it split as the ledgers, | ||
| 465 | // and the rule holds for all four files. It is checked, not asserted: see the | ||
| 466 | // "no payout mutex is held across a publish" section of the payout-ledger | ||
| 467 | // self-test. | ||
| 457 | 468 | ||
| 458 | // A serialised file image plus the version of the in-memory state it was | 469 | // A serialised file image plus the version of the in-memory state it was |
| 459 | // taken from. | 470 | // taken from. |
| @@ -2100,7 +2111,22 @@ static std::string PoolStatusFilePath() | |||
| 2100 | return GetAppDir() + "/pool_status.json"; | 2111 | return GetAppDir() + "/pool_status.json"; |
| 2101 | } | 2112 | } |
| 2102 | 2113 | ||
| 2103 | static void WritePoolStatusJsonLocked() | 2114 | // Serialise the operator status file. Caller must hold gStatsMutex (this reads |
| 2115 | // gStats / gWorkerStats / gPayoutViews); gPoolRoundsMutex is taken and | ||
| 2116 | // released inside. | ||
| 2117 | // | ||
| 2118 | // Split from the write for the same reason as the three ledgers, and for a | ||
| 2119 | // worse instance of the same problem. This used to be called from RefreshStats | ||
| 2120 | // with gPayoutLedgerMutex held at function scope, and once the ledger writes | ||
| 2121 | // gained fsync() that call became two fsyncs -- one on the file, one on its | ||
| 2122 | // directory -- performed by the event loop, the thread that owns every miner's | ||
| 2123 | // socket, with the mutex the payout thread needs in its hand. Measured on the | ||
| 2124 | // build host (ext4 on LVM on SSD): 3.78 ms per publish, every 10 seconds, and | ||
| 2125 | // unbounded on a shared virtual disk under load. | ||
| 2126 | // | ||
| 2127 | // Nothing here needs the ledger mutex at all: the pending-payout rows were | ||
| 2128 | // already copied into gPayoutViews under it, further up RefreshStats. | ||
| 2129 | static std::string SerializePoolStatusLocked() | ||
| 2104 | { | 2130 | { |
| 2105 | nlohmann::json j; | 2131 | nlohmann::json j; |
| 2106 | j["schema"] = "bitflash-pool-status-1"; | 2132 | j["schema"] = "bitflash-pool-status-1"; |
| @@ -2184,11 +2210,20 @@ static void WritePoolStatusJsonLocked() | |||
| 2184 | } | 2210 | } |
| 2185 | } | 2211 | } |
| 2186 | 2212 | ||
| 2187 | // No version guard and no write mutex here, deliberately: RefreshStats is | 2213 | return j.dump(2); |
| 2188 | // the only caller and it only ever runs on the event loop, so there is a | 2214 | } |
| 2189 | // single writer of pool_status.json by construction. It shares | 2215 | |
| 2190 | // WriteFileAtomic so the durability rules stay in one place. | 2216 | // Publish the operator status file. Call with every payout mutex released. |
| 2191 | WriteFileAtomic(PoolStatusFilePath(), j.dump(2), "pool status"); | 2217 | // |
| 2218 | // Still no version guard and no write mutex, deliberately, and the split does | ||
| 2219 | // not change the argument: RefreshStats remains the only caller, it still runs | ||
| 2220 | // only on the event loop, and it now calls this after leaving the scope that | ||
| 2221 | // serialised -- so the serialise and the write stay strictly ordered on one | ||
| 2222 | // thread and pool_status.json still has a single writer by construction. It | ||
| 2223 | // shares WriteFileAtomic so the durability rules stay in one place. | ||
| 2224 | static void PublishPoolStatus(const std::string& blob) | ||
| 2225 | { | ||
| 2226 | WriteFileAtomic(PoolStatusFilePath(), blob, "pool status"); | ||
| 2192 | } | 2227 | } |
| 2193 | 2228 | ||
| 2194 | // --------------------------------------------------------------------------- | 2229 | // --------------------------------------------------------------------------- |
| @@ -2907,6 +2942,10 @@ static std::string PayoutTestAddWalletTx(const std::string& comment, | |||
| 2907 | return hash.GetHex(); | 2942 | return hash.GetHex(); |
| 2908 | } | 2943 | } |
| 2909 | 2944 | ||
| 2945 | // Defined below with the event loop; the status-file publication test drives | ||
| 2946 | // it directly. | ||
| 2947 | static void RefreshStats(int blocksFoundThisSession, uint64 roundShareTotal); | ||
| 2948 | |||
| 2910 | int RunPayoutLedgerSelfTest() | 2949 | int RunPayoutLedgerSelfTest() |
| 2911 | { | 2950 | { |
| 2912 | int nFail = 0; | 2951 | int nFail = 0; |
| @@ -3455,6 +3494,136 @@ int RunPayoutLedgerSelfTest() | |||
| 3455 | strPoolRoundsFile = savedRoundsFile; | 3494 | strPoolRoundsFile = savedRoundsFile; |
| 3456 | } | 3495 | } |
| 3457 | 3496 | ||
| 3497 | // -- 11. No payout mutex is held across the status-file write ------------ | ||
| 3498 | // | ||
| 3499 | // RefreshStats runs on the event loop -- the thread that owns every | ||
| 3500 | // miner's socket -- and used to hold gStatsMutex and gPayoutLedgerMutex at | ||
| 3501 | // function scope across WriteFileAtomic. Once that write gained fsync() on | ||
| 3502 | // the file and on its directory, publishing pool_status.json parked the | ||
| 3503 | // payout thread behind two disk flushes every ten seconds. | ||
| 3504 | // | ||
| 3505 | // Timing assertions would be flaky, so this does not measure anything. It | ||
| 3506 | // makes the write block for as long as the test likes: strPoolStatusFile | ||
| 3507 | // points at a path whose ".tmp" is a FIFO, and open(2) for writing on a | ||
| 3508 | // FIFO does not return until a reader arrives. While RefreshStats is | ||
| 3509 | // parked in there, every mutex it must NOT be holding has to be | ||
| 3510 | // acquirable. Against the previous code this test does not merely fail, it | ||
| 3511 | // cannot pass: the writer is inside fopen() with both mutexes held and | ||
| 3512 | // nothing will release them until the test lets the write finish. | ||
| 3513 | #ifndef _WIN32 | ||
| 3514 | { | ||
| 3515 | PayoutTestResetState(); | ||
| 3516 | const std::string statusPath = dir + "/pool_status_selftest.json"; | ||
| 3517 | const std::string statusTmp = statusPath + ".tmp"; | ||
| 3518 | const std::string savedStatusFile = strPoolStatusFile; | ||
| 3519 | const int64 savedLastWrite = gLastPoolStatusWrite; | ||
| 3520 | remove(statusPath.c_str()); | ||
| 3521 | remove(statusTmp.c_str()); | ||
| 3522 | strPoolStatusFile = statusPath; | ||
| 3523 | gLastPoolStatusWrite = 0; // due a publish on the next refresh | ||
| 3524 | |||
| 3525 | // Give the status file something to report, so the ledger mutex is | ||
| 3526 | // genuinely taken on the way through rather than skipped over. | ||
| 3527 | { | ||
| 3528 | std::lock_guard<std::mutex> lk(gPayoutLedgerMutex); | ||
| 3529 | PendingPayout pp; | ||
| 3530 | pp.blockHeight = 909; pp.blockHash = 0; pp.matureAtHeight = 909; | ||
| 3531 | pp.amounts[addrA] = 3 * COIN; | ||
| 3532 | gPendingPayouts.push_back(pp); | ||
| 3533 | } | ||
| 3534 | |||
| 3535 | bool fifoOk = (mkfifo(statusTmp.c_str(), 0600) == 0); | ||
| 3536 | check(fifoOk, "a FIFO stands in for the status file's temp file, so the" | ||
| 3537 | " write can be held open"); | ||
| 3538 | if (fifoOk) { | ||
| 3539 | std::atomic<bool> done(false); | ||
| 3540 | std::thread writer([&done]() { | ||
| 3541 | RefreshStats(0, 0); | ||
| 3542 | done = true; | ||
| 3543 | }); | ||
| 3544 | |||
| 3545 | // Settle: the work before the write is a handful of vector | ||
| 3546 | // operations, so anything past this point is inside fopen(). The | ||
| 3547 | // !done check below is what actually proves it -- with no reader | ||
| 3548 | // there is no other way for this thread to still be running. | ||
| 3549 | Sleep(500); | ||
| 3550 | bool blocked = !done; | ||
| 3551 | bool gotLedger = false, gotStats = false, gotExec = false, | ||
| 3552 | gotRounds = false; | ||
| 3553 | for (int i = 0; i < 1000 && blocked; i++) { | ||
| 3554 | if (!gotLedger && gPayoutLedgerMutex.try_lock()) { | ||
| 3555 | gotLedger = true; gPayoutLedgerMutex.unlock(); | ||
| 3556 | } | ||
| 3557 | if (!gotStats && gStatsMutex.try_lock()) { | ||
| 3558 | gotStats = true; gStatsMutex.unlock(); | ||
| 3559 | } | ||
| 3560 | if (!gotExec && gPayoutExecMutex.try_lock()) { | ||
| 3561 | gotExec = true; gPayoutExecMutex.unlock(); | ||
| 3562 | } | ||
| 3563 | if (!gotRounds && gPoolRoundsMutex.try_lock()) { | ||
| 3564 | gotRounds = true; gPoolRoundsMutex.unlock(); | ||
| 3565 | } | ||
| 3566 | if (gotLedger && gotStats && gotExec && gotRounds) | ||
| 3567 | break; | ||
| 3568 | Sleep(2); | ||
| 3569 | } | ||
| 3570 | |||
| 3571 | check(blocked, | ||
| 3572 | "the status-file write is held open for the duration of this" | ||
| 3573 | " test"); | ||
| 3574 | check(!blocked || gotLedger, | ||
| 3575 | "gPayoutLedgerMutex is free while pool_status.json is being" | ||
| 3576 | " written"); | ||
| 3577 | check(!blocked || (gotExec && gotRounds), | ||
| 3578 | "gPayoutExecMutex and gPoolRoundsMutex are free while" | ||
| 3579 | " pool_status.json is being written"); | ||
| 3580 | check(!blocked || gotStats, | ||
| 3581 | "gStatsMutex is free while pool_status.json is being written"); | ||
| 3582 | |||
| 3583 | // Let the write finish. Drain to EOF rather than closing early: | ||
| 3584 | // the writer is still filling the pipe and a closed read end would | ||
| 3585 | // deliver it SIGPIPE. | ||
| 3586 | std::thread reader([&statusTmp]() { | ||
| 3587 | FILE* rf = fopen(statusTmp.c_str(), "r"); | ||
| 3588 | if (!rf) return; | ||
| 3589 | char buf[4096]; | ||
| 3590 | while (fread(buf, 1, sizeof(buf), rf) > 0) { } | ||
| 3591 | fclose(rf); | ||
| 3592 | }); | ||
| 3593 | writer.join(); | ||
| 3594 | reader.join(); | ||
| 3595 | } | ||
| 3596 | remove(statusTmp.c_str()); | ||
| 3597 | |||
| 3598 | // And the ordinary path still produces the file it always did. | ||
| 3599 | gLastPoolStatusWrite = 0; | ||
| 3600 | RefreshStats(0, 0); | ||
| 3601 | std::string onDisk; | ||
| 3602 | bool statusOk = false; | ||
| 3603 | if (PayoutTestReadFile(statusPath, onDisk)) { | ||
| 3604 | try { | ||
| 3605 | nlohmann::json j = nlohmann::json::parse(onDisk); | ||
| 3606 | statusOk = j.value("schema", "") == "bitflash-pool-status-1" && | ||
| 3607 | j["pendingPayouts"].is_array() && | ||
| 3608 | j["pendingPayouts"].size() == 1 && | ||
| 3609 | j["pendingPayouts"][0].value("matureAtHeight", 0) == 909 && | ||
| 3610 | j["pendingPayouts"][0].value("totalAmountSatoshis", | ||
| 3611 | (int64)0) == 3 * COIN; | ||
| 3612 | } catch (...) { | ||
| 3613 | statusOk = false; | ||
| 3614 | } | ||
| 3615 | } | ||
| 3616 | check(statusOk, | ||
| 3617 | "splitting the serialise from the write leaves pool_status.json" | ||
| 3618 | " saying the same thing"); | ||
| 3619 | |||
| 3620 | remove(statusPath.c_str()); | ||
| 3621 | remove((statusPath + ".tmp").c_str()); | ||
| 3622 | strPoolStatusFile = savedStatusFile; | ||
| 3623 | gLastPoolStatusWrite = savedLastWrite; | ||
| 3624 | } | ||
| 3625 | #endif | ||
| 3626 | |||
| 3458 | // -- clean up ------------------------------------------------------------ | 3627 | // -- clean up ------------------------------------------------------------ |
| 3459 | PayoutTestResetState(); | 3628 | PayoutTestResetState(); |
| 3460 | CRITICAL_BLOCK(cs_mapWallet) | 3629 | CRITICAL_BLOCK(cs_mapWallet) |
| @@ -3615,56 +3784,71 @@ int RunPoolStratumSelfTest() | |||
| 3615 | // --------------------------------------------------------------------------- | 3784 | // --------------------------------------------------------------------------- |
| 3616 | static void RefreshStats(int blocksFoundThisSession, uint64 roundShareTotal) | 3785 | static void RefreshStats(int blocksFoundThisSession, uint64 roundShareTotal) |
| 3617 | { | 3786 | { |
| 3618 | std::lock_guard<std::mutex> lk(gStatsMutex); | 3787 | std::string statusBlob; |
| 3788 | bool fPublish = false; | ||
| 3789 | { | ||
| 3790 | std::lock_guard<std::mutex> lk(gStatsMutex); | ||
| 3619 | 3791 | ||
| 3620 | gStats.authorizedMiners = 0; | 3792 | gStats.authorizedMiners = 0; |
| 3621 | gStats.blocksFound = blocksFoundThisSession; | 3793 | gStats.blocksFound = blocksFoundThisSession; |
| 3622 | gStats.roundShares = roundShareTotal; | 3794 | gStats.roundShares = roundShareTotal; |
| 3623 | gStats.totalHashRate = 0.0; | 3795 | gStats.totalHashRate = 0.0; |
| 3624 | for (Miner* m : gMiners) | 3796 | for (Miner* m : gMiners) |
| 3625 | if (m->authorised) gStats.authorizedMiners++; | 3797 | if (m->authorised) gStats.authorizedMiners++; |
| 3626 | 3798 | ||
| 3627 | gWorkerStats.clear(); | 3799 | gWorkerStats.clear(); |
| 3628 | for (Miner* m : gMiners) { | 3800 | for (Miner* m : gMiners) { |
| 3629 | if (!m->authorised || m->lastSeen <= 0) continue; | 3801 | if (!m->authorised || m->lastSeen <= 0) continue; |
| 3630 | PoolWorkerStatView v; | 3802 | PoolWorkerStatView v; |
| 3631 | v.address = m->address; | 3803 | v.address = m->address; |
| 3632 | v.worker = m->worker; | 3804 | v.worker = m->worker; |
| 3633 | v.roundShares = m->roundShares; | 3805 | v.roundShares = m->roundShares; |
| 3634 | v.totalShares = m->totalShares; | 3806 | v.totalShares = m->totalShares; |
| 3635 | v.lastSeen = m->lastSeen; | 3807 | v.lastSeen = m->lastSeen; |
| 3636 | v.hashRate = m->hashRate; | 3808 | v.hashRate = m->hashRate; |
| 3637 | gWorkerStats.push_back(v); | 3809 | gWorkerStats.push_back(v); |
| 3638 | gStats.totalHashRate += m->hashRate; | 3810 | gStats.totalHashRate += m->hashRate; |
| 3639 | } | 3811 | } |
| 3640 | std::sort(gWorkerStats.begin(), gWorkerStats.end(), | 3812 | std::sort(gWorkerStats.begin(), gWorkerStats.end(), |
| 3641 | [](const PoolWorkerStatView& a, const PoolWorkerStatView& b) { | 3813 | [](const PoolWorkerStatView& a, const PoolWorkerStatView& b) { |
| 3642 | return a.roundShares != b.roundShares ? a.roundShares > b.roundShares | 3814 | return a.roundShares != b.roundShares ? a.roundShares > b.roundShares |
| 3643 | : a.lastSeen > b.lastSeen; | 3815 | : a.lastSeen > b.lastSeen; |
| 3644 | }); | 3816 | }); |
| 3645 | 3817 | ||
| 3646 | gPayoutViews.clear(); | 3818 | // Scoped, not held to the end of the function. At function scope this |
| 3647 | std::lock_guard<std::mutex> ledger(gPayoutLedgerMutex); | 3819 | // lock covered the status-file write below, so the event loop did two |
| 3648 | for (const PendingPayout& pp : gPendingPayouts) { | 3820 | // fsyncs holding the mutex the payout thread takes to record that a |
| 3649 | PendingPayoutView pv; | 3821 | // miner has been paid. |
| 3650 | pv.matureAtHeight = pp.matureAtHeight; | 3822 | gPayoutViews.clear(); |
| 3651 | pv.recipients = 0; | 3823 | { |
| 3652 | pv.totalAmount = 0; | 3824 | std::lock_guard<std::mutex> ledger(gPayoutLedgerMutex); |
| 3653 | for (auto& kv : pp.amounts) { | 3825 | for (const PendingPayout& pp : gPendingPayouts) { |
| 3654 | if (pp.paidTxids.count(kv.first)) | 3826 | PendingPayoutView pv; |
| 3655 | continue; | 3827 | pv.matureAtHeight = pp.matureAtHeight; |
| 3656 | pv.recipients++; | 3828 | pv.recipients = 0; |
| 3657 | pv.totalAmount += kv.second; | 3829 | pv.totalAmount = 0; |
| 3830 | for (auto& kv : pp.amounts) { | ||
| 3831 | if (pp.paidTxids.count(kv.first)) | ||
| 3832 | continue; | ||
| 3833 | pv.recipients++; | ||
| 3834 | pv.totalAmount += kv.second; | ||
| 3835 | } | ||
| 3836 | if (pv.recipients > 0) | ||
| 3837 | gPayoutViews.push_back(pv); | ||
| 3838 | } | ||
| 3658 | } | 3839 | } |
| 3659 | if (pv.recipients > 0) | ||
| 3660 | gPayoutViews.push_back(pv); | ||
| 3661 | } | ||
| 3662 | 3840 | ||
| 3663 | int64 now = GetTime(); | 3841 | int64 now = GetTime(); |
| 3664 | if (now - gLastPoolStatusWrite >= 10) { | 3842 | if (now - gLastPoolStatusWrite >= 10) { |
| 3665 | WritePoolStatusJsonLocked(); | 3843 | statusBlob = SerializePoolStatusLocked(); |
| 3666 | gLastPoolStatusWrite = now; | 3844 | gLastPoolStatusWrite = now; |
| 3845 | fPublish = true; | ||
| 3846 | } | ||
| 3667 | } | 3847 | } |
| 3848 | // Every payout mutex is released here, and gStatsMutex with them. This is | ||
| 3849 | // where the two fsyncs happen, and no other thread is waiting behind them. | ||
| 3850 | if (fPublish) | ||
| 3851 | PublishPoolStatus(statusBlob); | ||
| 3668 | } | 3852 | } |
| 3669 | 3853 | ||
| 3670 | // --------------------------------------------------------------------------- | 3854 | // --------------------------------------------------------------------------- |