#12932·neon

compute: shard misrouting for multi-block prefetch batches crossing a stripe boundary

Author: reatcatCreated Aug 11, 2026Updated Aug 11, 2026

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:

c
slot->buftag = hashkey.buftag;
slot->shard_no = get_shard_number(&tag);   // <-- `tag` never advances in the loop

tag 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

  1. 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_total and drops the connection WITHOUT sending a response (PageStreamError::Reconnect, "getpage@lsn request routed to wrong shard" in pageserver/src/page_service.rs).
  2. 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.