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:
authorbitflash-maintainer <noreply@bitflash.invalid>2026-08-23 07:45:56 +0000
committerbitflash-maintainer <noreply@bitflash.invalid>2026-08-23 07:45:56 +0000
commit86b39b952d81c8aa6db56beb293e08befdca296e (patch)
treef51a3f046fc728560e770040512617e32b6a1235 /src/rpc.cpp
parentf8c0b6d9b477739e0699c4972adba68c5ea20000 (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.
Diffstat (limited to 'src/rpc.cpp')
-rw-r--r--src/rpc.cpp61
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. 465static 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.
487static 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.
463static bool WriteFileAtomic(const std::string& path, const std::string& data, 507static 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