upleb.uk

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

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbitflash-maintainer <noreply@bitflash.invalid>2026-09-02 14:14:52 +0000
committerbitflash-maintainer <noreply@bitflash.invalid>2026-09-02 14:14:52 +0000
commit03a69ccfb36861948e11b0871bb9af8f0b280709 (patch)
tree40064bdd36597b7c6486b0efd45efbacc03bc888
parente4f898c4a6b9900553c64b9804889b5fdae3e55f (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.
-rw-r--r--src/rpc.cpp10
-rw-r--r--src/util.cpp99
-rw-r--r--src/util.h22
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 {
47static std::map<std::string, LogDedupState> g_logDedup; 47static std::map<std::string, LogDedupState> g_logDedup;
48static CCriticalSection cs_logDedup; 48static 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.
52struct 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
59static int64 g_logDedupLastSweep = 0;
60
61static 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.
82static 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
50void LogPrintDedup(const char* category, const char* file, int line, const std::string& msgIn) 105void 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.
176void 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
diff --git a/src/util.h b/src/util.h
index d72518b..fe605a1 100644
--- a/src/util.h
+++ b/src/util.h
@@ -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// ---------------------------------------------------------------------------
114bool LogAcceptsCategory(const char* category); 125bool LogAcceptsCategory(const char* category);
115void LogPrintDedup(const char* category, const char* file, int line, const std::string& msg); 126void LogPrintDedup(const char* category, const char* file, int line, const std::string& msg);
127void 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