upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/rpc.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/rpc.cpp')
-rw-r--r--src/rpc.cpp290
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
2103static 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.
2129static 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.
2224static 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.
2947static void RefreshStats(int blocksFoundThisSession, uint64 roundShareTotal);
2948
2910int RunPayoutLedgerSelfTest() 2949int 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// ---------------------------------------------------------------------------
3616static void RefreshStats(int blocksFoundThisSession, uint64 roundShareTotal) 3785static 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// ---------------------------------------------------------------------------