Skip to content

Commit 66166bb

Browse files
committed
bch(m5): in-flight timeout/eviction for the block-download window
The windowed block download (2cc7de4) deferred stalling-peer handling until block bodies actually flowed end-to-end -- they now do (NodeP2P enqueues each synced headers batch and getdatas through the bounded window). This adds the eviction leg: BlockDownloadWindow now records the issue tick per in-flight request (next_requests(now_tick)) and exposes expire(now, timeout), which drops any request older than the timeout, frees its window slot, and re-queues the stalled hash at the FRONT so it is retried ahead of not-yet-requested tip blocks. A block delivered after its eviction is reported unsolicited but still applied. Without this a single non-delivering peer could pin a window slot forever and wedge IBD. Periodic-tick wiring into the NodeP2P poll loop is the next micro-slice; this lands the pure, peer-free state machine + unit coverage first (same shape as the windowed-download slice). bch_block_download_test: +2 cases (full-window expiry + re-queue ahead of fresh hashes; partial expiry leaving fresh requests; post-eviction delivery reported unsolicited). ctest 11/11 green. Header-only, build-inert (bch skip-green). Zero p2pool-merged-v36 surface -- pure SPV/IBD wire-sync; per-coin isolation.
1 parent 2cc7de4 commit 66166bb

2 files changed

Lines changed: 78 additions & 3 deletions

File tree

src/impl/bch/coin/block_download.hpp

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@
3939
// ---------------------------------------------------------------------------
4040

4141
#include <cstddef>
42+
#include <cstdint>
4243
#include <deque>
44+
#include <unordered_map>
4345
#include <unordered_set>
4446
#include <vector>
4547

@@ -85,13 +87,13 @@ class BlockDownloadWindow {
8587
/// Pop hashes to request now, up to the free window slots
8688
/// (max_in_flight - in_flight). Marks them in flight. Oldest-first
8789
/// (chain order) so block bodies are pulled in the order they were learned.
88-
std::vector<uint256> next_requests()
90+
std::vector<uint256> next_requests(uint64_t now_tick = 0)
8991
{
9092
std::vector<uint256> out;
9193
while (m_in_flight.size() < m_max_in_flight && !m_queue.empty()) {
9294
uint256 h = m_queue.front();
9395
m_queue.pop_front();
94-
m_in_flight.insert(h);
96+
m_in_flight.emplace(h, now_tick);
9597
out.push_back(h);
9698
}
9799
return out;
@@ -114,6 +116,34 @@ class BlockDownloadWindow {
114116
return true;
115117
}
116118

119+
/// Evict in-flight requests that have gone stale -- a block we getdata'd but a
120+
/// stalling peer never delivered. now_tick and timeout_ticks are in the caller's
121+
/// monotonic unit (the same unit passed to next_requests()); any request issued
122+
/// at issue_tick with now_tick - issue_tick >= timeout_ticks is pushed back to
123+
/// the FRONT of the queue (retry oldest-first, ahead of not-yet-requested hashes)
124+
/// and dropped from the in-flight set, freeing its window slot. The hash stays in
125+
/// m_known (still wanted, still deduped). Returns the evicted hashes so the caller
126+
/// can log / consider peer demotion. A block that arrives after eviction is handled
127+
/// normally (on_block_received reports it unsolicited, but emit_full_block still
128+
/// applies it). Deferred from the windowed-download slice (2cc7de44) until block
129+
/// bodies actually flowed end-to-end.
130+
std::vector<uint256> expire(uint64_t now_tick, uint64_t timeout_ticks)
131+
{
132+
std::vector<uint256> evicted;
133+
for (auto it = m_in_flight.begin(); it != m_in_flight.end(); ) {
134+
if (now_tick - it->second >= timeout_ticks) {
135+
evicted.push_back(it->first);
136+
it = m_in_flight.erase(it);
137+
} else {
138+
++it;
139+
}
140+
}
141+
// Re-queue the stalled hashes ahead of not-yet-requested ones so a stuck
142+
// block is retried before fresh tip blocks.
143+
for (const auto& h : evicted) m_queue.push_front(h);
144+
return evicted;
145+
}
146+
117147
std::size_t in_flight() const { return m_in_flight.size(); }
118148
std::size_t queued() const { return m_queue.size(); }
119149
std::size_t max_in_flight() const { return m_max_in_flight; }
@@ -125,7 +155,7 @@ class BlockDownloadWindow {
125155
private:
126156
std::size_t m_max_in_flight;
127157
std::deque<uint256> m_queue; // pending, chain order
128-
std::unordered_set<uint256, HashHasher> m_in_flight; // outstanding getdata
158+
std::unordered_map<uint256, uint64_t, HashHasher> m_in_flight; // hash -> tick getdata issued
129159
std::unordered_set<uint256, HashHasher> m_known; // queued ∪ inflight ∪ received
130160
};
131161

src/impl/bch/test/block_download_test.cpp

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,51 @@ int main()
131131
assert(DEFAULT_MAX_BLOCKS_IN_FLIGHT == 16);
132132
}
133133

134+
// ---- expire(): stalled in-flight requests are evicted + re-queued ------
135+
{
136+
BlockDownloadWindow w(2);
137+
w.enqueue(hashes(0, 4)); // [H0 H1 H2 H3]
138+
auto req = w.next_requests(/*now=*/100); // H0,H1 issued @100
139+
assert(req.size() == 2);
140+
assert(w.in_flight() == 2 && w.queued() == 2);
141+
142+
// Not yet past the timeout: nothing evicted.
143+
assert(w.expire(/*now=*/105, /*timeout=*/10).empty());
144+
assert(w.in_flight() == 2);
145+
146+
// Past the timeout (110-100 >= 10): both stalled requests evicted,
147+
// window freed, and they go back to the FRONT of the queue.
148+
auto evicted = w.expire(/*now=*/110, /*timeout=*/10);
149+
assert(evicted.size() == 2);
150+
assert(w.in_flight() == 0);
151+
assert(w.queued() == 4); // H0,H1 re-queued ahead of H2,H3
152+
153+
// Retried oldest-first: the next window pulls exactly the two stalled
154+
// hashes (ahead of the never-requested H2/H3).
155+
auto retry = w.next_requests(/*now=*/200);
156+
assert(retry.size() == 2);
157+
std::vector<uint256> rset(retry.begin(), retry.end());
158+
assert((rset[0] == H(0) || rset[0] == H(1)));
159+
assert((rset[1] == H(0) || rset[1] == H(1)) && rset[0] != rset[1]);
160+
}
161+
162+
// ---- expire(): only the stale request goes, fresh ones stay -----------
163+
{
164+
BlockDownloadWindow w(2);
165+
w.enqueue(hashes(0, 3)); // [H0 H1 H2]
166+
w.next_requests(/*now=*/100); // H0,H1 issued @100
167+
assert(w.on_block_received(H(1)) == true); // H1 arrives, frees a slot
168+
w.next_requests(/*now=*/150); // H2 issued @150
169+
// in flight: H0@100, H2@150. timeout 50 at now=155 => only H0 is stale.
170+
auto evicted = w.expire(/*now=*/155, /*timeout=*/50);
171+
assert(evicted.size() == 1 && evicted[0] == H(0));
172+
assert(w.in_flight() == 1); // H2 still outstanding
173+
174+
// A block that arrives AFTER its eviction is reported unsolicited
175+
// (no longer in flight) -- caller still applies it; window unaffected.
176+
assert(w.on_block_received(H(0)) == false);
177+
}
178+
134179
std::cout << "bch block_download window: ALL PASS\n";
135180
return 0;
136181
}

0 commit comments

Comments
 (0)