diff options
| author | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-02 14:14:52 +0000 |
|---|---|---|
| committer | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-02 14:14:52 +0000 |
| commit | 03a69ccfb36861948e11b0871bb9af8f0b280709 (patch) | |
| tree | 40064bdd36597b7c6486b0efd45efbacc03bc888 /src | |
| parent | e4f898c4a6b9900553c64b9804889b5fdae3e55f (diff) | |
payout: never collapse a log line that reports money moving
LogPrint()'s burst collapse is right for share and peer chatter and wrong
for anything that reports an irreversible change to money. A reorg voided
100 ledger entries totalling 4,950 BTF and the log carried one line, naming
one block and 49.49 BTF. The other 99 were counted and never printed, and
no "(N similar)" summary appeared either -- a summary was only ever flushed
when that same call site fired again, and after the reorg it never did. The
missing 4,950 BTF was found by counting entries in pending_payouts.json.
Add LogPrintAlways(), which skips the collapse entirely, and move the four
payout lines that report money to it: an obligation voided, an obligation
dropped, a payment sent, a payment that failed. All four are bursty by
nature -- one reorg voids every entry funded by the orphaned block, from a
single source line -- which is precisely why one line must not stand in for
all of them. Their volume is bounded by how much money moved, which is a
number worth the lines.
Also sweep expired windows for every call site rather than only the one
currently logging, so a burst that stops is still summarised, and report
the window's real elapsed time instead of a hardcoded 3s.
Diffstat (limited to 'src')
| -rw-r--r-- | src/rpc.cpp | 10 | ||||
| -rw-r--r-- | src/util.cpp | 99 | ||||
| -rw-r--r-- | src/util.h | 22 |
3 files changed, 114 insertions, 17 deletions
diff --git a/src/rpc.cpp b/src/rpc.cpp index a4b53ca..2e8f4b8 100644 --- a/src/rpc.cpp +++ b/src/rpc.cpp | |||
| @@ -1732,7 +1732,9 @@ static void FlushMaturePayouts() | |||
| 1732 | } | 1732 | } |
| 1733 | int64 total = 0; | 1733 | int64 total = 0; |
| 1734 | for (auto& kv : it->amounts) total += kv.second; | 1734 | for (auto& kv : it->amounts) total += kv.second; |
| 1735 | LogPrint("payout", "[payout] block %d (%s) was orphaned -- voiding %zu" | 1735 | // Always: one line per voided entry, never a collapsed summary. |
| 1736 | // A reorg voids these in a burst from this one source line. | ||
| 1737 | LogPrintAlways("payout", "[payout] block %d (%s) was orphaned -- voiding %zu" | ||
| 1736 | " payment(s) totalling %s\n", | 1738 | " payment(s) totalling %s\n", |
| 1737 | it->blockHeight, it->blockHash.ToString().substr(0, 14).c_str(), | 1739 | it->blockHeight, it->blockHash.ToString().substr(0, 14).c_str(), |
| 1738 | it->amounts.size(), FormatMoney(total).c_str()); | 1740 | it->amounts.size(), FormatMoney(total).c_str()); |
| @@ -2036,7 +2038,7 @@ static void PayoutThreadFn(void*) | |||
| 2036 | // funding block can land at any point in that window. Upstream has | 2038 | // funding block can land at any point in that window. Upstream has |
| 2037 | // no equivalent -- it decides once, when the entry matures. | 2039 | // no equivalent -- it decides once, when the entry matures. |
| 2038 | if (!PayoutBlockStillOnChain(r.blockHash)) { | 2040 | if (!PayoutBlockStillOnChain(r.blockHash)) { |
| 2039 | LogPrint("payout", "[payout] block %d (%s) orphaned while queued" | 2041 | LogPrintAlways("payout", "[payout] block %d (%s) orphaned while queued" |
| 2040 | " -- dropping %s to %s\n", | 2042 | " -- dropping %s to %s\n", |
| 2041 | r.blockHeight, r.blockHash.ToString().substr(0, 14).c_str(), | 2043 | r.blockHeight, r.blockHash.ToString().substr(0, 14).c_str(), |
| 2042 | FormatMoney(r.amount).c_str(), r.address.c_str()); | 2044 | FormatMoney(r.amount).c_str(), r.address.c_str()); |
| @@ -2205,7 +2207,7 @@ static void PayoutThreadFn(void*) | |||
| 2205 | // described by at least one of them at every instant. | 2207 | // described by at least one of them at every instant. |
| 2206 | RecordPayoutTxid(r, wtx.GetHash().GetHex()); | 2208 | RecordPayoutTxid(r, wtx.GetHash().GetHex()); |
| 2207 | ForgetPayment(r); | 2209 | ForgetPayment(r); |
| 2208 | LogPrint("payout", "[payout] paid %s to %s (tx %s)\n", | 2210 | LogPrintAlways("payout", "[payout] paid %s to %s (tx %s)\n", |
| 2209 | FormatMoney(r.amount).c_str(), r.address.c_str(), | 2211 | FormatMoney(r.amount).c_str(), r.address.c_str(), |
| 2210 | wtx.GetHash().ToString().substr(0, 12).c_str()); | 2212 | wtx.GetHash().ToString().substr(0, 12).c_str()); |
| 2211 | } else { | 2213 | } else { |
| @@ -2263,7 +2265,7 @@ static void PayoutThreadFn(void*) | |||
| 2263 | // going away must not turn the log into one line a minute | 2265 | // going away must not turn the log into one line a minute |
| 2264 | // forever, and must not go quiet either. | 2266 | // forever, and must not go quiet either. |
| 2265 | if (r.retries <= 5 || r.retries % 60 == 0) | 2267 | if (r.retries <= 5 || r.retries % 60 == 0) |
| 2266 | LogPrint("payout", "[payout] FAILED to pay %s to %s" | 2268 | LogPrintAlways("payout", "[payout] FAILED to pay %s to %s" |
| 2267 | " (%s spendable, wallet unlocked, block %d on" | 2269 | " (%s spendable, wallet unlocked, block %d on" |
| 2268 | " the main chain) -- attempt %d, retrying in" | 2270 | " the main chain) -- attempt %d, retrying in" |
| 2269 | " 60s; this debt stands until it is paid\n", | 2271 | " 60s; this debt stands until it is paid\n", |
diff --git a/src/util.cpp b/src/util.cpp index 0cd0133..b5b145f 100644 --- a/src/util.cpp +++ b/src/util.cpp | |||
| @@ -47,6 +47,61 @@ struct LogDedupState { | |||
| 47 | static std::map<std::string, LogDedupState> g_logDedup; | 47 | static std::map<std::string, LogDedupState> g_logDedup; |
| 48 | static CCriticalSection cs_logDedup; | 48 | static CCriticalSection cs_logDedup; |
| 49 | 49 | ||
| 50 | // A pending "(N similar)" summary, lifted out of the table while the lock is | ||
| 51 | // held and written after it is released. | ||
| 52 | struct LogDedupFlush { | ||
| 53 | std::string category; | ||
| 54 | int suppressed; | ||
| 55 | int64 elapsed; // how long the window that counted them ran | ||
| 56 | std::string lastMsg; | ||
| 57 | }; | ||
| 58 | |||
| 59 | static int64 g_logDedupLastSweep = 0; | ||
| 60 | |||
| 61 | static void EmitDedupSummary(const LogDedupFlush& f) | ||
| 62 | { | ||
| 63 | OutputDebugStringF("[%s] (%d similar in %llds, last: %s)\n", | ||
| 64 | f.category.c_str(), f.suppressed, | ||
| 65 | (long long)f.elapsed, f.lastMsg.c_str()); | ||
| 66 | } | ||
| 67 | |||
| 68 | // A window is otherwise only closed out when its own call site fires again, so | ||
| 69 | // a burst that simply stops -- 100 ledger entries voided in one pass and then | ||
| 70 | // silence -- leaves its count sitting in the table and the summary is never | ||
| 71 | // written at all. That is how 99 of 100 voids went unreported, with no "(N | ||
| 72 | // similar)" line to say anything was missing (issue #3). Sweep every expired | ||
| 73 | // window instead, so a finished burst is summarised as soon as anything at all | ||
| 74 | // is logged. | ||
| 75 | // | ||
| 76 | // Expired entries are erased rather than reopened: the window is over, so there | ||
| 77 | // is no state left worth keeping, and the next call from that site then takes | ||
| 78 | // the "first time" path and prints directly -- exactly what the rollover branch | ||
| 79 | // below would have done. It also stops the table growing without bound. | ||
| 80 | // | ||
| 81 | // Caller holds cs_logDedup; the summaries are written after it is released. | ||
| 82 | static void SweepExpiredWindowsLocked(int64 now, std::vector<LogDedupFlush>& out) | ||
| 83 | { | ||
| 84 | for (std::map<std::string, LogDedupState>::iterator it = g_logDedup.begin(); | ||
| 85 | it != g_logDedup.end(); ) | ||
| 86 | { | ||
| 87 | if (now - it->second.windowStart < DEDUP_WINDOW_SECS) | ||
| 88 | { | ||
| 89 | ++it; | ||
| 90 | continue; | ||
| 91 | } | ||
| 92 | if (it->second.suppressed > 0) | ||
| 93 | { | ||
| 94 | LogDedupFlush f; | ||
| 95 | f.category = it->first.substr(0, it->first.find('|')); | ||
| 96 | f.suppressed = it->second.suppressed; | ||
| 97 | f.elapsed = now - it->second.windowStart; | ||
| 98 | f.lastMsg = it->second.lastMsg; | ||
| 99 | out.push_back(f); | ||
| 100 | } | ||
| 101 | g_logDedup.erase(it++); | ||
| 102 | } | ||
| 103 | } | ||
| 104 | |||
| 50 | void LogPrintDedup(const char* category, const char* file, int line, const std::string& msgIn) | 105 | void LogPrintDedup(const char* category, const char* file, int line, const std::string& msgIn) |
| 51 | { | 106 | { |
| 52 | // Call sites follow the existing printf() convention of ending their | 107 | // Call sites follow the existing printf() convention of ending their |
| @@ -61,11 +116,19 @@ void LogPrintDedup(const char* category, const char* file, int line, const std:: | |||
| 61 | 116 | ||
| 62 | int64 now = GetTime(); | 117 | int64 now = GetTime(); |
| 63 | bool fEmitNow = false; | 118 | bool fEmitNow = false; |
| 64 | int suppressedToFlush = 0; | 119 | std::vector<LogDedupFlush> flushes; |
| 65 | std::string flushedMsg; | ||
| 66 | 120 | ||
| 67 | CRITICAL_BLOCK(cs_logDedup) | 121 | CRITICAL_BLOCK(cs_logDedup) |
| 68 | { | 122 | { |
| 123 | // At most once a second. The table holds one entry per call site that | ||
| 124 | // has fired in the last few seconds, so the walk is short, but it must | ||
| 125 | // not run once per line of a burst. | ||
| 126 | if (now != g_logDedupLastSweep) | ||
| 127 | { | ||
| 128 | g_logDedupLastSweep = now; | ||
| 129 | SweepExpiredWindowsLocked(now, flushes); | ||
| 130 | } | ||
| 131 | |||
| 69 | std::map<std::string, LogDedupState>::iterator it = g_logDedup.find(key); | 132 | std::map<std::string, LogDedupState>::iterator it = g_logDedup.find(key); |
| 70 | if (it == g_logDedup.end()) | 133 | if (it == g_logDedup.end()) |
| 71 | { | 134 | { |
| @@ -76,10 +139,18 @@ void LogPrintDedup(const char* category, const char* file, int line, const std:: | |||
| 76 | } | 139 | } |
| 77 | else if (now - it->second.windowStart >= DEDUP_WINDOW_SECS) | 140 | else if (now - it->second.windowStart >= DEDUP_WINDOW_SECS) |
| 78 | { | 141 | { |
| 79 | // Window rolled over: flush what was suppressed during it (if | 142 | // Window rolled over within the same second as the last sweep: |
| 80 | // anything), then start a new window with this call printed. | 143 | // flush what was suppressed during it (if anything), then start a |
| 81 | suppressedToFlush = it->second.suppressed; | 144 | // new window with this call printed. |
| 82 | flushedMsg = it->second.lastMsg; | 145 | if (it->second.suppressed > 0) |
| 146 | { | ||
| 147 | LogDedupFlush f; | ||
| 148 | f.category = category; | ||
| 149 | f.suppressed = it->second.suppressed; | ||
| 150 | f.elapsed = now - it->second.windowStart; | ||
| 151 | f.lastMsg = it->second.lastMsg; | ||
| 152 | flushes.push_back(f); | ||
| 153 | } | ||
| 83 | it->second.windowStart = now; | 154 | it->second.windowStart = now; |
| 84 | it->second.suppressed = 0; | 155 | it->second.suppressed = 0; |
| 85 | it->second.lastMsg = msg; | 156 | it->second.lastMsg = msg; |
| @@ -94,13 +165,23 @@ void LogPrintDedup(const char* category, const char* file, int line, const std:: | |||
| 94 | } | 165 | } |
| 95 | } | 166 | } |
| 96 | 167 | ||
| 97 | if (suppressedToFlush > 0) | 168 | for (size_t i = 0; i < flushes.size(); i++) |
| 98 | OutputDebugStringF("[%s] (%d similar in %llds, last: %s)\n", | 169 | EmitDedupSummary(flushes[i]); |
| 99 | category, suppressedToFlush, (long long)DEDUP_WINDOW_SECS, flushedMsg.c_str()); | ||
| 100 | if (fEmitNow) | 170 | if (fEmitNow) |
| 101 | OutputDebugStringF("[%s] %s\n", category, msg.c_str()); | 171 | OutputDebugStringF("[%s] %s\n", category, msg.c_str()); |
| 102 | } | 172 | } |
| 103 | 173 | ||
| 174 | // LogPrintAlways() -- see the doc comment in util.h. No window, no counting, | ||
| 175 | // no table entry: every call writes its own line. | ||
| 176 | void LogPrintNoCollapse(const char* category, const std::string& msgIn) | ||
| 177 | { | ||
| 178 | std::string msg = msgIn; | ||
| 179 | while (!msg.empty() && (msg.back() == '\n' || msg.back() == '\r')) | ||
| 180 | msg.pop_back(); | ||
| 181 | |||
| 182 | OutputDebugStringF("[%s] %s\n", category, msg.c_str()); | ||
| 183 | } | ||
| 184 | |||
| 104 | 185 | ||
| 105 | 186 | ||
| 106 | 187 | ||
| @@ -106,15 +106,29 @@ extern bool fDebug; | |||
| 106 | // 2. Burst collapse: repeated LogPrint() calls from the *same source line* | 106 | // 2. Burst collapse: repeated LogPrint() calls from the *same source line* |
| 107 | // only emit one line per DEDUP_WINDOW_SECS; if more calls land inside | 107 | // only emit one line per DEDUP_WINDOW_SECS; if more calls land inside |
| 108 | // that window they're counted instead of reprinted, and the count is | 108 | // that window they're counted instead of reprinted, and the count is |
| 109 | // flushed as a single "(xN similar in Ws)" line the moment the window | 109 | // flushed as a single "(xN similar in Ws)" line once the window expires |
| 110 | // rolls over. A loop discovering 500 peers by address now produces a | 110 | // -- whether or not that call site ever fires again. A loop discovering |
| 111 | // small, bounded number of lines instead of 500 -- log growth is capped | 111 | // 500 peers by address now produces a small, bounded number of lines |
| 112 | // by elapsed time, not by how many similar events happen to fire. | 112 | // instead of 500 -- log growth is capped by elapsed time, not by how |
| 113 | // many similar events happen to fire. | ||
| 114 | // | ||
| 115 | // Use LogPrintAlways() instead for anything that reports an irreversible | ||
| 116 | // change to money: an obligation voided, a payment sent, a payment that | ||
| 117 | // failed. One line standing in for an unknown number of identical events is | ||
| 118 | // a tidy report of peer chatter and a false report of a ledger. A reorg | ||
| 119 | // voiding 100 entries hits one source line 100 times inside one window; | ||
| 120 | // under LogPrint() that read as a single block losing a single 49.49 BTF | ||
| 121 | // payment, while 4,950 BTF of obligations were actually destroyed, and the | ||
| 122 | // discrepancy was only found by counting entries in pending_payouts.json | ||
| 123 | // (issue #3). Bounded log growth is not worth that ambiguity here. | ||
| 113 | // --------------------------------------------------------------------------- | 124 | // --------------------------------------------------------------------------- |
| 114 | bool LogAcceptsCategory(const char* category); | 125 | bool LogAcceptsCategory(const char* category); |
| 115 | void LogPrintDedup(const char* category, const char* file, int line, const std::string& msg); | 126 | void LogPrintDedup(const char* category, const char* file, int line, const std::string& msg); |
| 127 | void LogPrintNoCollapse(const char* category, const std::string& msg); | ||
| 116 | #define LogPrint(category, ...) \ | 128 | #define LogPrint(category, ...) \ |
| 117 | do { if (LogAcceptsCategory(category)) LogPrintDedup((category), __FILE__, __LINE__, strprintf(__VA_ARGS__)); } while (0) | 129 | do { if (LogAcceptsCategory(category)) LogPrintDedup((category), __FILE__, __LINE__, strprintf(__VA_ARGS__)); } while (0) |
| 130 | #define LogPrintAlways(category, ...) \ | ||
| 131 | do { if (LogAcceptsCategory(category)) LogPrintNoCollapse((category), strprintf(__VA_ARGS__)); } while (0) | ||
| 118 | 132 | ||
| 119 | // Every format string in this project goes through one of the four functions | 133 | // Every format string in this project goes through one of the four functions |
| 120 | // below -- `printf` itself is remapped to OutputDebugStringF just above, and | 134 | // below -- `printf` itself is remapped to OutputDebugStringF just above, and |