Skip to content

compute: add configurable peek row iteration limit - #38506

Merged
antiguru merged 5 commits into
mainfrom
peek/structured-errors
Aug 27, 2026
Merged

compute: add configurable peek row iteration limit#38506
antiguru merged 5 commits into
mainfrom
peek/structured-errors

Conversation

@antiguru

Copy link
Copy Markdown
Member

Introduces PeekError as a structured peek failure and adds a configurable limit on how many rows a peek may iterate.

The first commit is @aljoscha's, taken from #38158 and authored by him. It carries the uncommitted fixes that accompanied that patch, because the mailbox commit alone leaves a state where the golden in src/environmentd/tests/testdata/http/ws contradicts its own code: the double-wrap in src/environmentd/src/http/sql.rs re-flattens the structured error to XX000. Splitting the fixes out would have attributed his work to someone else and left a red intermediate commit. The version gate was moved to this branch's release.

PeekError::Dataflow is produced unconditionally, so structured error frames flow on the wire regardless of whether the row-iteration limit is enabled.

The second commit resets enable_compute_peek_row_iteration_limit after use in the two slt files that set it.

🤖 Opened by Claude Code on behalf of @antiguru

Replaces #38451, which GitHub closed when the design document moved from the bottom of the stack to the top and its branch was force-pushed past these commits. Same content, new base.

@antiguru
antiguru requested review from a team and ggevay as code owners August 27, 2026 11:44
@antiguru

Copy link
Copy Markdown
Member Author

Posted by Claude Code on behalf of @antiguru.

Carrying over the review comments left on #38451 before the restack closed it. Both are unanswered and neither is addressed in this branch yet.

src/compute-client/src/protocol/response.rs, on the WirePeekResponseRef mirror type

The CTP doesn't need to be compatible across versions. Would it allow us to skip the custom serializer?

src/compute-types/src/dyncfgs.rs, on peek_row_iteration_limit_defaults

No need for this test.

@antiguru
antiguru marked this pull request as draft August 27, 2026 11:47
@antiguru
antiguru marked this pull request as ready for review August 27, 2026 11:59
@antiguru
antiguru requested a review from petrosagg August 27, 2026 11:59
@antiguru

Copy link
Copy Markdown
Member Author

Posted by Claude Code on behalf of @antiguru. Both comments addressed in f736cfc.

The custom serializer is gone. You are right that the CTP does not need it. mz_service::transport's handshake bails on peer_version != version, so the two ends of a connection are always built from the same declaration of the enum and there is no reader to keep the old byte layout for. PeekResponse now just derives Serialize/Deserialize, and WirePeekResponseRef, WirePeekResponse and both hand-written impls are deleted, along with the test that pinned the byte-for-byte legacy encoding.

The roundtrip test went too. It only existed because the mirror stood between the type and the wire; with the derive it exercises serde rather than anything of ours. The reason the encoding is free to change is now recorded on the type, since that is what a future variant needs to know.

One thing the handshake does not cover, worth knowing rather than acting on: it compares semantic versions, not builds, so two binaries at the same -dev.0 semver but different shas do connect. That is the local and staging skew where mzbuild reuses an unchanged image. Such a pairing now fails to decode rather than silently misreading an error, which is the better of the two.

The dyncfg defaults test is gone as well. While rebasing the branches above this one I applied the same reading to their defaults tests: the assertions that restate a literal three lines below the declaration are removed, and the ones that state a contract are kept. What survives is that the switch defaults off (production safety and CI coverage are separate settings), that INDEX_PEEK_PERMITS defaults to 0 as a sentinel meaning one per worker rather than none, that the activation budget admits at least one full inline slice, and that the yield granularity is coarser than the inline budget. Say the word if you want those gone as well.

@petrosagg petrosagg left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Base automatically changed from peek/fueled-iterator to main August 27, 2026 13:31
aljoscha and others added 5 commits August 27, 2026 15:31
Compute workers iterate arrangements synchronously while serving
index-backed peeks, so a query that scans far more rows than it returns
can hold a worker for a long time and delay everything else on the
cluster. Persist fast-path peeks have the same shape: filtering happens
after the rows have been read.

Add an off-by-default failsafe that bounds how many rows a worker may
examine for one peek. Two dyncfgs, a feature gate and a threshold that
defaults to 1000 rows, both read through handles so that an
`UpdateConfiguration` reaches peeks that are already in flight. The
budget covers the index result trace, the index error trace and the
Persist fast path, and counts rows before literal and MFP filtering,
because a row that is read and then discarded costs the same scan time
as one that is returned. Exactly the configured number of rows may be
examined. A peek fails only when it asks for the row after that.

The limit deliberately stops at the peek stash. A stashed peek restarts
its scan and produces in bounded bursts, so bounding it needs the count
to survive the hand-off, and the restart makes that count charge the
same rows twice. Leaving it out keeps this change small. The peeks that
motivate the failsafe, large filtered scans, fail before they ever reach
the stash threshold.

Reporting the limit needs an error type that survives the trip from the
worker. `PeekResponse::Error` carried a bare `String`, so every peek
failure reached the adapter as `AdapterError::Unstructured` and was
reported as XX000. Give it a `PeekError` of `Dataflow`, `Unstructured`
or `RowIterationLimitExceeded`, and let `PeekResponseUnary::Error` carry
an `AdapterError`, so the conversion happens once instead of once per
frontend. The limit then reports SQLSTATE 54000 with a hint naming the
threshold parameter, and worker responses merge by error precedence:
cancellation, then ordinary errors, then the limit.

Carrying the dataflow error structurally also fixes the SQLSTATE of
evaluation errors raised while reading a collection: `SELECT a / b FROM
t` now reports 22012 like its constant-folded counterpart. Such an error
keeps the message `DataflowError` renders, so one that used to come back
bare from an index or Persist fast-path MFP now carries the `Evaluation
error:` prefix the error-trace path already used.

The wire encoding is bincode, which cannot skip a variant it does not
know, so `PeekResponse` serializes through a mirror type that keeps
`Error(String)` where it was for the unstructured case and appends the
structured one. Existing frames, `Canceled` in particular, encode
exactly as before.

The test and CI configuration enables the feature with a high threshold,
so the guarded path is exercised broadly without constraining ordinary
queries.
persist-fast-path.slt and max_result_size.slt reset
compute_peek_row_iteration_limit at the end but leave the feature
flag itself enabled. That is harmless under bin/sqllogictest, whose
suite default for the limit is very high, but wrong against a plain
environmentd where the code default is 1000.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The limit is enforced per worker, so a multi-worker cluster walks the same
rows a share at a time and no single walk reaches it. That made the
assertion depend on the cluster's worker count, and it made the one-shot
query disagree with the indexed view --auto-index-selects wraps it in: the
view holds only the three rows the LIMIT produced, spread across workers,
so it answered where the one-shot query raised. Pin the block to a
single-worker cluster, which is what max_result_size.slt already does for
the same reason.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Giving `PeekResponse::Error` a payload type arrived with a mirrored wire
enum and hand-written `Serialize`/`Deserialize` impls, so that the bytes on
a CTP connection kept `Error(String)` where it was and carried structured
errors in an appended variant. That guarded against a peer decoding a shape
it does not declare, which the compute protocol does not have to survive:
`mz_service::transport`'s handshake refuses a peer whose version differs
from its own, so a replica never speaks to a controller built from another
declaration of this enum.

Derive the two impls and delete the mirror, the two enums it needed and the
test that pinned the byte-for-byte legacy encoding. The roundtrip test goes
with them, since it exercised serde's derive once the mirror stopped
standing between the type and the wire. Record on the type itself why the
encoding is free to change, because that is the fact a future variant has to
check rather than rediscover.

Drop the dyncfg test asserting the row-iteration limit's own defaults. It
restates the two literals a reader finds three lines up in the declaration.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The row-iteration limit's tests arrived as an inline `mod tests` in the
middle of `compute_state.rs`, between a free function and the struct that
follows it, and the response-merging tests did the same at the end of
`service.rs`. Both files are ones a reader opens for the implementation.

Move them to `compute_state/tests.rs` and `service/tests.rs`, which a plain
`mod tests;` finds without a `#[path]` attribute. `use super::*` still
reaches everything the tests read.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@antiguru
antiguru force-pushed the peek/structured-errors branch from 7e33dcc to f300405 Compare August 27, 2026 13:31
@antiguru
antiguru merged commit c69fde3 into main Aug 27, 2026
82 checks passed
@antiguru
antiguru deleted the peek/structured-errors branch August 27, 2026 14:35
antiguru added a commit that referenced this pull request Aug 28, 2026
Undoes the part of #38506 that pinned
`enable_compute_peek_row_iteration_limit` and
`compute_peek_row_iteration_limit` as system parameter defaults inside
the sqllogictest binary. A test binary that hardcodes parameter values
holds a second copy of the test configuration, invisible to any operator
or workflow and free to drift from `get_minimal_system_parameters`,
which is where the suite's values belong. `mz-compute-types` becomes
unused by the crate and goes with them.

The guard defaults off in code, so no query the suite runs changes. What
is lost is that the suite no longer exercises the row iteration limit:
the composition passes the binary no `--system-parameter-default`, so
the values in `get_minimal_system_parameters` never reach it. Wiring
that through restores the coverage and is not part of this change.

The two sqllogictest files that exercise the limit set both parameters
themselves and reset them afterwards, and neither runs a query between
resetting the limit and disabling the guard.

🤖 Opened by [Claude Code](https://claude.com/claude-code) on behalf of
@antiguru

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants