compute: shard misrouting for multi-block prefetch batches crossing a stripe boundary
Problem
In prefetch_register_bufferv() (pgxn/neon/communicator.c), when registering a multi-block batch, the shard number of every block is computed from the base block's tag:
slot->buftag = hashkey.buftag;
slot->shard_no = get_shard_number(&tag); // <-- `tag` never advances in the looptag is the tag of the first block of the batch and is never advanced in the registration loop, while get_shard_number() hashes blockNum / stripe_size. Whenever a multi-block request batch crosses a shard stripe boundary, the blocks beyond the boundary are assigned the shard number of the base block and sent to the wrong pageserver shard connection.
Trigger path (PG 17 read-stream API)
sequential scan
-> read_stream_begin_relation() [access/heap/heapam.c]
-> StartReadBuffers() / WaitReadBuffers() [storage/buffer/bufmgr.c]
-> smgrreadv(nblocks > 1)
-> neon_readv() [pgxn/neon/pagestore_smgr.c]
-> communicator_read_at_lsnv() [pgxn/neon/communicator.c]
-> prefetch_register_bufferv(..., is_prefetch=false)
-> prefetch_do_request() -> page_server->send(wrong_shard_no)Observable symptoms
- The misrouted request reaches a pageserver that does not host the tenant shard owning the page. The pageserver counts it in
pageserver_misrouted_pagestream_requests_totaland drops the connection WITHOUT sending a response (PageStreamError::Reconnect, "getpage@lsn request routed to wrong shard" inpageserver/src/page_service.rs). - The compute detects the disconnect, discards all in-flight prefetches (
getpage_prefetch_discards_total), and retries the read one block at a time. The single-block retry computes the shard from the correct block number, so it self-heals: the query succeeds despite the misrouting.
The net effect is repeated connection teardown/reconnect and wasted round trips whenever a vectorized multi-block read crosses a stripe boundary — i.e. during ordinary sequential scans of sharded tenants.
Reproduction
Shard a tenant across two pageservers with a stripe size that is not a multiple of the read batch size (e.g. prime 71 vs up to 16-block batches with io_combine_limit), write a table spanning many stripes, then read it back cold with shared_buffers large enough for 16-block read-stream batches. Both pageserver_misrouted_pagestream_requests_total and getpage_prefetch_discards_total grow during the scan. A machine-verifiable version of this reproduction is included as a regression test in #12931.
Fix
Compute the shard from the per-block tag: get_shard_number(&hashkey.buftag) — see #12931.
Source: neondatabase/neon