diff options
| author | bitflash-maintainer <noreply@bitflash.invalid> | 2026-08-23 11:22:32 +0000 |
|---|---|---|
| committer | bitflash-maintainer <noreply@bitflash.invalid> | 2026-08-23 11:22:32 +0000 |
| commit | 6c0ed67a2f6b39b630c5748ecf214f86e9d26a5c (patch) | |
| tree | d08c42d440061028edc46761f568964472fc7a6f /src/rpc.cpp | |
| parent | c9353e8f8a4d95c827497abe5b30ac951dd7dda5 (diff) | |
payout: leave no temporary file behind, and say so where it will be read
Every failure path in WriteFileAtomic returned without removing
"<path>.tmp". A disk that filled up once, or a single EIO, left a short
file called pending_payouts.json.tmp sitting next to a good ledger for
as long as the node ran. That file is worse than no file: it carries the
name of a half-finished save, and the operator cleaning up after the
incident is precisely the person who will move it into place and publish
a ledger missing whatever had not been written yet.
The failures also logged under LogPrint("payout", ...). pool_status.json
is written through the same function but everything else about it is
logged under "pool", so an operator running -debug=pool was told nothing
when the status file could not be replaced -- the message existed and
was discarded for having the wrong category. They now print
unconditionally: a write failure is not a trace of normal operation, it
is the file that is about to be read not being there, and gating it on a
category chosen before the failure happened is how it went missing. The
brief's alternative, a category argument, would still have required the
right guess in advance.
Properties: no failure of WriteFileAtomic leaves a .tmp behind, and no
failure of it is invisible at default log settings.
Diffstat (limited to 'src/rpc.cpp')
| -rw-r--r-- | src/rpc.cpp | 38 |
1 files changed, 28 insertions, 10 deletions
diff --git a/src/rpc.cpp b/src/rpc.cpp index f853e78..be265db 100644 --- a/src/rpc.cpp +++ b/src/rpc.cpp | |||
| @@ -506,20 +506,35 @@ static void SyncDirectoryOf(const std::string& path) | |||
| 506 | // write mutex for `path`: the temp name is derived from the target, so two | 506 | // write mutex for `path`: the temp name is derived from the target, so two |
| 507 | // threads in here at once for one path is precisely the race this exists to | 507 | // threads in here at once for one path is precisely the race this exists to |
| 508 | // prevent. | 508 | // prevent. |
| 509 | // | ||
| 510 | // Every failure removes the temp file before returning. It used to be left | ||
| 511 | // where it fell, so a disk that filled up once left a short, valid-looking | ||
| 512 | // "pending_payouts.json.tmp" next to the good ledger for as long as the node | ||
| 513 | // ran. That file is a trap: it has the name of a half-finished save, and an | ||
| 514 | // operator cleaning up after an incident is exactly the person who will move | ||
| 515 | // it into place. | ||
| 516 | // | ||
| 517 | // Failures print unconditionally rather than through LogPrint(). These three | ||
| 518 | // files are written under two different debug categories ("payout" for the | ||
| 519 | // ledgers, "pool" for the status file), and a write failure is not a trace of | ||
| 520 | // normal operation -- it is the file the operator is about to read not being | ||
| 521 | // there. Gating it behind a category that had to be chosen before the failure | ||
| 522 | // happened is how "could not replace pool_status.json" went missing. | ||
| 509 | static bool WriteFileAtomic(const std::string& path, const std::string& data, | 523 | static bool WriteFileAtomic(const std::string& path, const std::string& data, |
| 510 | const char* what) | 524 | const char* what) |
| 511 | { | 525 | { |
| 512 | std::string tmp = path + ".tmp"; | 526 | std::string tmp = path + ".tmp"; |
| 513 | FILE* f = fopen(tmp.c_str(), "w"); | 527 | FILE* f = fopen(tmp.c_str(), "w"); |
| 514 | if (!f) { | 528 | if (!f) { |
| 515 | LogPrint("payout", "[payout] WARNING: could not write %s (%s)\n", | 529 | printf("[payout] WARNING: could not write %s (%s)\n", |
| 516 | tmp.c_str(), what); | 530 | tmp.c_str(), what); |
| 517 | return false; | 531 | return false; |
| 518 | } | 532 | } |
| 519 | if (!data.empty() && fwrite(data.c_str(), 1, data.size(), f) != data.size()) { | 533 | if (!data.empty() && fwrite(data.c_str(), 1, data.size(), f) != data.size()) { |
| 520 | LogPrint("payout", "[payout] WARNING: short write to %s (%s)\n", | 534 | printf("[payout] WARNING: short write to %s (%s)\n", |
| 521 | tmp.c_str(), what); | 535 | tmp.c_str(), what); |
| 522 | fclose(f); | 536 | fclose(f); |
| 537 | remove(tmp.c_str()); | ||
| 523 | return false; | 538 | return false; |
| 524 | } | 539 | } |
| 525 | // Before the rename, not after: publishing a name that points at nothing | 540 | // Before the rename, not after: publishing a name that points at nothing |
| @@ -527,9 +542,10 @@ static bool WriteFileAtomic(const std::string& path, const std::string& data, | |||
| 527 | // made durable, do not rename -- the previous file is still intact and | 542 | // made durable, do not rename -- the previous file is still intact and |
| 528 | // still correct, and the next write will try again. | 543 | // still correct, and the next write will try again. |
| 529 | if (!SyncFileHandle(f)) { | 544 | if (!SyncFileHandle(f)) { |
| 530 | LogPrint("payout", "[payout] WARNING: could not flush %s to disk (%s)" | 545 | printf("[payout] WARNING: could not flush %s to disk (%s)" |
| 531 | " -- keeping the previous file\n", tmp.c_str(), what); | 546 | " -- keeping the previous file\n", tmp.c_str(), what); |
| 532 | fclose(f); | 547 | fclose(f); |
| 548 | remove(tmp.c_str()); | ||
| 533 | return false; | 549 | return false; |
| 534 | } | 550 | } |
| 535 | // fclose() is where buffered data actually reaches the OS, so a failure | 551 | // fclose() is where buffered data actually reaches the OS, so a failure |
| @@ -537,8 +553,9 @@ static bool WriteFileAtomic(const std::string& path, const std::string& data, | |||
| 537 | // destroy the good copy, which is exactly what these files exist to | 553 | // destroy the good copy, which is exactly what these files exist to |
| 538 | // prevent. | 554 | // prevent. |
| 539 | if (fclose(f) != 0) { | 555 | if (fclose(f) != 0) { |
| 540 | LogPrint("payout", "[payout] WARNING: could not close %s (%s)\n", | 556 | printf("[payout] WARNING: could not close %s (%s)\n", |
| 541 | tmp.c_str(), what); | 557 | tmp.c_str(), what); |
| 558 | remove(tmp.c_str()); | ||
| 542 | return false; | 559 | return false; |
| 543 | } | 560 | } |
| 544 | #ifdef _WIN32 | 561 | #ifdef _WIN32 |
| @@ -546,8 +563,9 @@ static bool WriteFileAtomic(const std::string& path, const std::string& data, | |||
| 546 | remove(path.c_str()); | 563 | remove(path.c_str()); |
| 547 | #endif | 564 | #endif |
| 548 | if (rename(tmp.c_str(), path.c_str()) != 0) { | 565 | if (rename(tmp.c_str(), path.c_str()) != 0) { |
| 549 | LogPrint("payout", "[payout] WARNING: could not replace %s (%s)\n", | 566 | printf("[payout] WARNING: could not replace %s (%s)\n", |
| 550 | path.c_str(), what); | 567 | path.c_str(), what); |
| 568 | remove(tmp.c_str()); | ||
| 551 | return false; | 569 | return false; |
| 552 | } | 570 | } |
| 553 | SyncDirectoryOf(path); | 571 | SyncDirectoryOf(path); |