diff options
Diffstat (limited to 'src/rpc.cpp')
| -rw-r--r-- | src/rpc.cpp | 118 |
1 files changed, 97 insertions, 21 deletions
diff --git a/src/rpc.cpp b/src/rpc.cpp index b6b26f1..7badb7c 100644 --- a/src/rpc.cpp +++ b/src/rpc.cpp | |||
| @@ -680,6 +680,18 @@ static void LoadPendingPayouts() | |||
| 680 | // and reconciling against the wallet on startup, is what makes a restart | 680 | // and reconciling against the wallet on startup, is what makes a restart |
| 681 | // survivable. | 681 | // survivable. |
| 682 | struct PayingRecord { | 682 | struct PayingRecord { |
| 683 | // Identity of this SLOT, distinct from the identity of the payment it | ||
| 684 | // describes. ForgetPayment and ReschedulePayment used to find their record | ||
| 685 | // by comparing every field, which is only an identity while no two records | ||
| 686 | // can be equal -- and a restart used to produce byte-identical duplicates | ||
| 687 | // (see FlushMaturePayouts), so the first match absorbed every update and | ||
| 688 | // the second was never touched again. | ||
| 689 | // | ||
| 690 | // Process-local and deliberately not persisted: gPaying is rebuilt from | ||
| 691 | // disk at every start, so an id only ever has to name a slot within one | ||
| 692 | // run, and reading ids back would mean trusting a file to contain no | ||
| 693 | // duplicates. Assigned under gPayoutExecMutex from gPayingNextId. | ||
| 694 | uint64 id = 0; | ||
| 683 | int blockHeight = 0; | 695 | int blockHeight = 0; |
| 684 | uint256 blockHash = 0; // zero = predates hash keying, see PendingPayout | 696 | uint256 blockHash = 0; // zero = predates hash keying, see PendingPayout |
| 685 | int matureAtHeight = 0; | 697 | int matureAtHeight = 0; |
| @@ -702,11 +714,37 @@ struct PayingRecord { | |||
| 702 | }; | 714 | }; |
| 703 | static std::vector<PayingRecord> gPaying; // guarded by gPayoutExecMutex | 715 | static std::vector<PayingRecord> gPaying; // guarded by gPayoutExecMutex |
| 704 | static std::mutex gPayoutExecMutex; | 716 | static std::mutex gPayoutExecMutex; |
| 717 | static uint64 gPayingNextId = 0; // guarded by gPayoutExecMutex | ||
| 705 | static uint64 gPayingSeq = 0; // guarded by gPayoutExecMutex | 718 | static uint64 gPayingSeq = 0; // guarded by gPayoutExecMutex |
| 706 | static std::mutex gPayingFileMutex; | 719 | static std::mutex gPayingFileMutex; |
| 707 | static uint64 gPayingPublishedSeq = 0; // guarded by gPayingFileMutex | 720 | static uint64 gPayingPublishedSeq = 0; // guarded by gPayingFileMutex |
| 708 | static std::atomic<bool> gPayoutThreadRunning{false}; | 721 | static std::atomic<bool> gPayoutThreadRunning{false}; |
| 709 | 722 | ||
| 723 | // Upstream's gPayoutExecQueue/gPayoutExecMutex/gPayoutThreadRunning are | ||
| 724 | // declared above with gPaying: the in-flight list IS the exec queue in this | ||
| 725 | // tree, at per-recipient rather than per-round granularity, so a second queue | ||
| 726 | // would be a second answer to "who is owed money". Only the dedupe set is new. | ||
| 727 | // | ||
| 728 | // Ids of rounds currently exploded into gPaying, so a flush cannot queue the | ||
| 729 | // same round twice. Guarded by gPayoutExecMutex. Seeded from the restored | ||
| 730 | // in-flight list by ReconcilePayouts -- it is memory-only, and leaving it | ||
| 731 | // empty at startup was half of why a restart duplicated everything in flight. | ||
| 732 | static std::set<std::string> gQueuedPayouts; | ||
| 733 | |||
| 734 | // Is this exact payment already in flight? gPaying is keyed on | ||
| 735 | // (round, address): the ledger holds one amount per address per round, so two | ||
| 736 | // records for that pair are two standing orders to send the same money. | ||
| 737 | // Caller must hold gPayoutExecMutex. | ||
| 738 | static bool PaymentInFlightLocked(int blockHeight, const uint256& blockHash, | ||
| 739 | const std::string& address) | ||
| 740 | { | ||
| 741 | for (const PayingRecord& e : gPaying) | ||
| 742 | if (e.address == address | ||
| 743 | && SamePayoutRound(e.blockHeight, e.blockHash, blockHeight, blockHash)) | ||
| 744 | return true; | ||
| 745 | return false; | ||
| 746 | } | ||
| 747 | |||
| 710 | static std::string PayingFilePath() | 748 | static std::string PayingFilePath() |
| 711 | { | 749 | { |
| 712 | return GetAppDir() + "/paying_payouts.json"; | 750 | return GetAppDir() + "/paying_payouts.json"; |
| @@ -774,6 +812,30 @@ static void LoadPaying() | |||
| 774 | r.retries = obj["retries"].get<int>(); | 812 | r.retries = obj["retries"].get<int>(); |
| 775 | if (obj.contains("deferrals") && obj["deferrals"].is_number()) | 813 | if (obj.contains("deferrals") && obj["deferrals"].is_number()) |
| 776 | r.deferrals = obj["deferrals"].get<int>(); | 814 | r.deferrals = obj["deferrals"].get<int>(); |
| 815 | // Collapse duplicates as they are read. A build without the | ||
| 816 | // ReconcilePayouts seeding below wrote a second (and after another | ||
| 817 | // restart, a third) byte-identical record for every unpaid | ||
| 818 | // recipient, so a file written by one of those has to be cleaned | ||
| 819 | // up here or the pathology is inherited. The retry and deferral | ||
| 820 | // counts are merged upwards so collapsing does not hand the | ||
| 821 | // survivor a fresh budget. | ||
| 822 | PayingRecord* dup = NULL; | ||
| 823 | for (PayingRecord& e : gPaying) | ||
| 824 | if (e.address == r.address | ||
| 825 | && SamePayoutRound(e.blockHeight, e.blockHash, | ||
| 826 | r.blockHeight, r.blockHash)) { | ||
| 827 | dup = &e; | ||
| 828 | break; | ||
| 829 | } | ||
| 830 | if (dup) { | ||
| 831 | dup->retries = std::max(dup->retries, r.retries); | ||
| 832 | dup->deferrals = std::max(dup->deferrals, r.deferrals); | ||
| 833 | LogPrint("payout", "[payout] collapsing duplicate in-flight" | ||
| 834 | " record for %s to %s -- one payment, one record\n", | ||
| 835 | FormatMoney(r.amount).c_str(), r.address.c_str()); | ||
| 836 | continue; | ||
| 837 | } | ||
| 838 | r.id = ++gPayingNextId; | ||
| 777 | gPaying.push_back(r); | 839 | gPaying.push_back(r); |
| 778 | } | 840 | } |
| 779 | LogPrint("payout", "[payout] loaded %zu in-flight payment(s) from disk\n", | 841 | LogPrint("payout", "[payout] loaded %zu in-flight payment(s) from disk\n", |
| @@ -962,6 +1024,15 @@ static std::string PayoutComment(int blockHeight) | |||
| 962 | // What is left is the useful part: the in-flight list restored from disk may | 1024 | // What is left is the useful part: the in-flight list restored from disk may |
| 963 | // contain records the ledger already shows as paid (we died between the send | 1025 | // contain records the ledger already shows as paid (we died between the send |
| 964 | // and the in-flight file being rewritten). The ledger wins. | 1026 | // and the in-flight file being rewritten). The ledger wins. |
| 1027 | // | ||
| 1028 | // And the part that was missing: gQueuedPayouts is memory-only, so it came up | ||
| 1029 | // empty while gPaying came up full. FlushMaturePayouts deduped against that | ||
| 1030 | // empty set and against the ledger's paidTxids, and against nothing else -- | ||
| 1031 | // so a round that was exploded but not yet paid got a SECOND record per | ||
| 1032 | // unpaid recipient on the first flush after every restart, deterministically. | ||
| 1033 | // (Observed live: two byte-identical records for one (block, address).) The | ||
| 1034 | // surviving in-flight list is exactly the statement of which rounds are | ||
| 1035 | // already exploded, so it is what the set is rebuilt from. | ||
| 965 | static void ReconcilePayouts() | 1036 | static void ReconcilePayouts() |
| 966 | { | 1037 | { |
| 967 | VersionedBlob payingSnap; | 1038 | VersionedBlob payingSnap; |
| @@ -997,6 +1068,13 @@ static void ReconcilePayouts() | |||
| 997 | " already recorded paid in the ledger -- not resending\n", | 1068 | " already recorded paid in the ledger -- not resending\n", |
| 998 | settled, FormatMoney(settledAmount).c_str()); | 1069 | settled, FormatMoney(settledAmount).c_str()); |
| 999 | } | 1070 | } |
| 1071 | |||
| 1072 | // Rebuild the "already exploded" set from what survived. Without this the | ||
| 1073 | // first flush after a restart re-explodes every round still in flight. | ||
| 1074 | gQueuedPayouts.clear(); | ||
| 1075 | for (const PayingRecord& r : gPaying) | ||
| 1076 | gQueuedPayouts.insert(PayoutIdForBlock(r.blockHeight, r.blockHash)); | ||
| 1077 | |||
| 1000 | payingSnap = SerializePayingLocked(); | 1078 | payingSnap = SerializePayingLocked(); |
| 1001 | 1079 | ||
| 1002 | if (!gPaying.empty()) { | 1080 | if (!gPaying.empty()) { |
| @@ -1054,15 +1132,6 @@ static void MarkPoolRoundPaid(const std::string& id, | |||
| 1054 | PublishPoolRounds(snap); | 1132 | PublishPoolRounds(snap); |
| 1055 | } | 1133 | } |
| 1056 | 1134 | ||
| 1057 | // Upstream's gPayoutExecQueue/gPayoutExecMutex/gPayoutThreadRunning are | ||
| 1058 | // declared above with gPaying: the in-flight list IS the exec queue in this | ||
| 1059 | // tree, at per-recipient rather than per-round granularity, so a second queue | ||
| 1060 | // would be a second answer to "who is owed money". Only the dedupe set is new. | ||
| 1061 | // | ||
| 1062 | // Ids of rounds currently exploded into gPaying, so a flush cannot queue the | ||
| 1063 | // same round twice. Guarded by gPayoutExecMutex. | ||
| 1064 | static std::set<std::string> gQueuedPayouts; | ||
| 1065 | |||
| 1066 | static bool FindRecordedPoolPayout(const std::string& id, | 1135 | static bool FindRecordedPoolPayout(const std::string& id, |
| 1067 | const std::string& address, | 1136 | const std::string& address, |
| 1068 | int64 amount, | 1137 | int64 amount, |
| @@ -1311,7 +1380,17 @@ static void FlushMaturePayouts() | |||
| 1311 | // txid is done, even though the entry as a whole is not. | 1380 | // txid is done, even though the entry as a whole is not. |
| 1312 | if (pp.paidTxids.count(kv.first)) | 1381 | if (pp.paidTxids.count(kv.first)) |
| 1313 | continue; | 1382 | continue; |
| 1383 | // The invariant, checked against the thing it is an invariant of: | ||
| 1384 | // at most one in-flight record per (round, address). The round-level | ||
| 1385 | // set above is an optimisation that can be wrong -- it is memory | ||
| 1386 | // only, it is rebuilt from gPaying at startup, and a stale `ready` | ||
| 1387 | // snapshot can race a round finishing -- whereas gPaying is the | ||
| 1388 | // list being protected. A second record here is a second standing | ||
| 1389 | // order to send the same money. | ||
| 1390 | if (PaymentInFlightLocked(pp.blockHeight, pp.blockHash, kv.first)) | ||
| 1391 | continue; | ||
| 1314 | PayingRecord r; | 1392 | PayingRecord r; |
| 1393 | r.id = ++gPayingNextId; | ||
| 1315 | r.blockHeight = pp.blockHeight; | 1394 | r.blockHeight = pp.blockHeight; |
| 1316 | r.blockHash = pp.blockHash; | 1395 | r.blockHash = pp.blockHash; |
| 1317 | r.matureAtHeight = pp.matureAtHeight; | 1396 | r.matureAtHeight = pp.matureAtHeight; |
| @@ -1331,27 +1410,24 @@ static void FlushMaturePayouts() | |||
| 1331 | PublishPaying(payingSnap); | 1410 | PublishPaying(payingSnap); |
| 1332 | } | 1411 | } |
| 1333 | 1412 | ||
| 1334 | // Identity of a payment, for finding its record again after SendMoney. | ||
| 1335 | // amounts is a map keyed by address, so (block, address) is already unique. | ||
| 1336 | static bool SamePayment(const PayingRecord& a, const PayingRecord& b) | ||
| 1337 | { | ||
| 1338 | return SamePayoutRound(a.blockHeight, a.blockHash, b.blockHeight, b.blockHash) | ||
| 1339 | && a.matureAtHeight == b.matureAtHeight | ||
| 1340 | && a.address == b.address | ||
| 1341 | && a.amount == b.amount; | ||
| 1342 | } | ||
| 1343 | |||
| 1344 | // Settle a payment: it is either done or abandoned, either way it stops being | 1413 | // Settle a payment: it is either done or abandoned, either way it stops being |
| 1345 | // in flight. Note this only retires the working record -- whether the miner is | 1414 | // in flight. Note this only retires the working record -- whether the miner is |
| 1346 | // still OWED the money is the ledger's business, and the ledger is updated | 1415 | // still OWED the money is the ledger's business, and the ledger is updated |
| 1347 | // separately by RecordPayoutTxid(). | 1416 | // separately by RecordPayoutTxid(). |
| 1417 | // | ||
| 1418 | // Erases by record id, not by comparing fields. Value-matching was only an | ||
| 1419 | // identity for as long as no two records could be equal, and the restart bug | ||
| 1420 | // above produced records that were equal in every field: the first match then | ||
| 1421 | // absorbed every erase and every state update, and the second was never | ||
| 1422 | // touched again. The id names the slot the caller took its copy from, so the | ||
| 1423 | // update lands where it belongs whatever else is in the list. | ||
| 1348 | static void ForgetPayment(const PayingRecord& r) | 1424 | static void ForgetPayment(const PayingRecord& r) |
| 1349 | { | 1425 | { |
| 1350 | VersionedBlob snap; | 1426 | VersionedBlob snap; |
| 1351 | { | 1427 | { |
| 1352 | std::lock_guard<std::mutex> lk(gPayoutExecMutex); | 1428 | std::lock_guard<std::mutex> lk(gPayoutExecMutex); |
| 1353 | for (auto it = gPaying.begin(); it != gPaying.end(); ++it) | 1429 | for (auto it = gPaying.begin(); it != gPaying.end(); ++it) |
| 1354 | if (SamePayment(*it, r)) { gPaying.erase(it); break; } | 1430 | if (it->id == r.id) { gPaying.erase(it); break; } |
| 1355 | 1431 | ||
| 1356 | // When the last record for a round is gone the round is no longer in | 1432 | // When the last record for a round is gone the round is no longer in |
| 1357 | // flight, so a later flush may explode it again if the ledger still | 1433 | // flight, so a later flush may explode it again if the ledger still |
| @@ -1379,7 +1455,7 @@ static void ReschedulePayment(const PayingRecord& r) | |||
| 1379 | { | 1455 | { |
| 1380 | std::lock_guard<std::mutex> lk(gPayoutExecMutex); | 1456 | std::lock_guard<std::mutex> lk(gPayoutExecMutex); |
| 1381 | for (auto& e : gPaying) | 1457 | for (auto& e : gPaying) |
| 1382 | if (SamePayment(e, r)) { | 1458 | if (e.id == r.id) { |
| 1383 | e.retries = r.retries; | 1459 | e.retries = r.retries; |
| 1384 | e.deferrals = r.deferrals; | 1460 | e.deferrals = r.deferrals; |
| 1385 | e.retryAfter = r.retryAfter; | 1461 | e.retryAfter = r.retryAfter; |