diff options
| author | bitflash-maintainer <noreply@bitflash.invalid> | 2026-08-23 07:45:56 +0000 |
|---|---|---|
| committer | bitflash-maintainer <noreply@bitflash.invalid> | 2026-08-23 07:45:56 +0000 |
| commit | 86b39b952d81c8aa6db56beb293e08befdca296e (patch) | |
| tree | f51a3f046fc728560e770040512617e32b6a1235 | |
| parent | f8c0b6d9b477739e0699c4972adba68c5ea20000 (diff) | |
payout: flush the ledger to the device before the rename that publishes it
WritePendingPayoutsFile, SavePayingLocked and SavePoolRoundsLocked all wrote a
temp file and renamed it, with no fsync anywhere. That is atomic against the
process dying -- which is what the crash test exercised -- and not against the
machine dying. A power cut or a hard VM reset can land the rename on the
platter ahead of the data it points at, and what comes back is a short or
empty file.
For pending_payouts.json that is not a corrupt ledger, it is an absent one:
LoadPendingPayouts catches the parse error, logs one warning, and the node
starts up owing nobody anything. Total silent loss of the unpaid ledger, from
a clean-looking boot.
WriteFileAtomic now fflush()es and fsync()s the temp file before renaming it,
and fsync()s the containing directory afterwards so the new directory entry is
durable as well. If the data cannot be made durable the rename does not happen
at all: the previous file is still intact and still correct, and the next
write will try again. Ordering the sync before the rename is the point --
publishing a name that points at nothing is the exact failure being prevented.
The directory sync is best effort; a filesystem that will not let the
directory be opened for it is not a reason to refuse to write the ledger.
This is only affordable because the preceding commit moved all three writes
out from under gPayoutLedgerMutex / gPayoutExecMutex / gPoolRoundsMutex. An
fsync under any of those would park the pool event loop on the payout thread's
disk, which is the contention the merge was right to remove.
Applies to upstream too and is being reported there.
No on-disk format change.
| -rw-r--r-- | src/rpc.cpp | 61 |
1 files changed, 58 insertions, 3 deletions
diff --git a/src/rpc.cpp b/src/rpc.cpp index d655ad5..45130ff 100644 --- a/src/rpc.cpp +++ b/src/rpc.cpp | |||
| @@ -31,6 +31,9 @@ | |||
| 31 | #include <netinet/in.h> | 31 | #include <netinet/in.h> |
| 32 | #include <arpa/inet.h> | 32 | #include <arpa/inet.h> |
| 33 | #include <unistd.h> | 33 | #include <unistd.h> |
| 34 | #include <fcntl.h> | ||
| 35 | #else | ||
| 36 | #include <io.h> | ||
| 34 | #endif | 37 | #endif |
| 35 | 38 | ||
| 36 | #include <atomic> | 39 | #include <atomic> |
| @@ -457,9 +460,50 @@ struct VersionedBlob { | |||
| 457 | uint64 seq = 0; | 460 | uint64 seq = 0; |
| 458 | }; | 461 | }; |
| 459 | 462 | ||
| 460 | // Replace `path` with `data` atomically. Caller must hold the write mutex for | 463 | // Push a file's contents out of the C library's buffer AND out of the kernel's |
| 461 | // `path`: the temp name is derived from the target, so two threads in here at | 464 | // page cache, so the bytes exist on the device before anything points at them. |
| 462 | // once for one path is precisely the race this exists to prevent. | 465 | static bool SyncFileHandle(FILE* f) |
| 466 | { | ||
| 467 | if (fflush(f) != 0) | ||
| 468 | return false; | ||
| 469 | #ifdef _WIN32 | ||
| 470 | return _commit(_fileno(f)) == 0; | ||
| 471 | #else | ||
| 472 | return fsync(fileno(f)) == 0; | ||
| 473 | #endif | ||
| 474 | } | ||
| 475 | |||
| 476 | // Make the directory entry that rename() created durable too. | ||
| 477 | // | ||
| 478 | // A tmp+rename is only atomic with respect to a crash of the process. Against | ||
| 479 | // a power cut or a hard VM reset the rename can reach the platter while the | ||
| 480 | // data it points at has not, and the file that comes back is short or empty. | ||
| 481 | // For pending_payouts.json that is not a corrupt file, it is a silent one: | ||
| 482 | // LoadPendingPayouts catches the parse error, logs a single warning, and the | ||
| 483 | // node comes up owing nobody anything. | ||
| 484 | // | ||
| 485 | // Best effort -- a filesystem that does not allow the directory to be opened | ||
| 486 | // for this is not a reason to refuse to write the ledger. | ||
| 487 | static void SyncDirectoryOf(const std::string& path) | ||
| 488 | { | ||
| 489 | #ifndef _WIN32 | ||
| 490 | size_t slash = path.find_last_of('/'); | ||
| 491 | std::string dir = (slash == std::string::npos) ? std::string(".") | ||
| 492 | : path.substr(0, slash); | ||
| 493 | if (dir.empty()) | ||
| 494 | dir = "/"; | ||
| 495 | int fd = open(dir.c_str(), O_RDONLY); | ||
| 496 | if (fd < 0) | ||
| 497 | return; | ||
| 498 | fsync(fd); | ||
| 499 | close(fd); | ||
| 500 | #endif | ||
| 501 | } | ||
| 502 | |||
| 503 | // Replace `path` with `data` atomically and durably. Caller must hold the | ||
| 504 | // write mutex for `path`: the temp name is derived from the target, so two | ||
| 505 | // threads in here at once for one path is precisely the race this exists to | ||
| 506 | // prevent. | ||
| 463 | static bool WriteFileAtomic(const std::string& path, const std::string& data, | 507 | static bool WriteFileAtomic(const std::string& path, const std::string& data, |
| 464 | const char* what) | 508 | const char* what) |
| 465 | { | 509 | { |
| @@ -476,6 +520,16 @@ static bool WriteFileAtomic(const std::string& path, const std::string& data, | |||
| 476 | fclose(f); | 520 | fclose(f); |
| 477 | return false; | 521 | return false; |
| 478 | } | 522 | } |
| 523 | // Before the rename, not after: publishing a name that points at nothing | ||
| 524 | // is the failure this ordering exists to prevent. If the data cannot be | ||
| 525 | // made durable, do not rename -- the previous file is still intact and | ||
| 526 | // still correct, and the next write will try again. | ||
| 527 | if (!SyncFileHandle(f)) { | ||
| 528 | LogPrint("payout", "[payout] WARNING: could not flush %s to disk (%s)" | ||
| 529 | " -- keeping the previous file\n", tmp.c_str(), what); | ||
| 530 | fclose(f); | ||
| 531 | return false; | ||
| 532 | } | ||
| 479 | // fclose() is where buffered data actually reaches the OS, so a failure | 533 | // fclose() is where buffered data actually reaches the OS, so a failure |
| 480 | // here means the .tmp is short -- renaming it over the real file would | 534 | // here means the .tmp is short -- renaming it over the real file would |
| 481 | // destroy the good copy, which is exactly what these files exist to | 535 | // destroy the good copy, which is exactly what these files exist to |
| @@ -494,6 +548,7 @@ static bool WriteFileAtomic(const std::string& path, const std::string& data, | |||
| 494 | path.c_str(), what); | 548 | path.c_str(), what); |
| 495 | return false; | 549 | return false; |
| 496 | } | 550 | } |
| 551 | SyncDirectoryOf(path); | ||
| 497 | return true; | 552 | return true; |
| 498 | } | 553 | } |
| 499 | 554 | ||