Skip to content

fix(inkless:consume): bound find_batches_v2 scan to the fetch budget (O(result), not O(read-depth)) [KC-327] - #701

Merged
ivanyu merged 2 commits into
mainfrom
jeqo/fix-find-batches
Jul 16, 2026
Merged

fix(inkless:consume): bound find_batches_v2 scan to the fetch budget (O(result), not O(read-depth)) [KC-327]#701
ivanyu merged 2 commits into
mainfrom
jeqo/fix-find-batches

Conversation

@jeqo

@jeqo jeqo commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

find_batches_v2 (the diskless consumer read path) scaled with read-depth, not result size. It computed ROW_NUMBER() / SUM() OVER windows across the entire [starting_offset, high_watermark) range and only then trimmed to the fetch byte budget, so a single fetch cost O(high_watermark - starting_offset). A lagging consumer therefore paid for its whole lag on every fetch. This creates an unstable feedback loop: the further a consumer falls behind, the more expensive each fetch, so its drain rate drops below the produce rate and lag runs away rather than recovering.

Change

Rewrite find_batches_v2 (migration V19) as a per-partition plpgsql loop that streams batches in last_offset order and stops as soon as the per-partition (max_partition_fetch_bytes) or global (fetch_max_bytes) byte budget is crossed. Cost is now proportional to the bytes returned, not the backlog depth. The function signature and return type are unchanged, so the jOOQ binding and the FindBatchesJob caller are untouched; the only generated-code change is the schema-version stamp bump to 19.

Read results are identical to the previous version (V15): same error handling (unknown_topic_or_partition, offset_out_of_range), always at least one batch per partition, the budget-crossing batch is included, and responses stay in request order.

Why the plan is stable (and why an implicit loop, not an explicit cursor)

The O(result) win depends on the planner choosing an Index Scan on batches_by_last_offset_covering_idx (yielding last_offset order, no Sort) that the loop can abandon early. That plan is chosen reliably because the WHERE predicates are on domain-typed columns the query casts (topic_id::uuid, partition::integer, last_offset::bigint), which defeats column statistics: the planner estimates rows=1 and always picks the Index Scan + Nested Loop.

We evaluated an explicit OPEN/FETCH/CLOSE cursor (which adds cursor_tuple_fraction fast-start planning bias) as insurance, and rejected it: that bias is dormant today (the rows=1 estimate already forces the desired plan), while its single-row FETCH costs a consistent ~10-15% more than the implicit loop's 50-row batched fetch at large result sizes. Paying a live tax for a dormant benefit is not worth it. The migration comment records the full rationale and the exact future change (the rows=1 estimate becoming accurate: a PG upgrade, stats change, or dropping the domain casts) that would flip the plan to a Sort/Hash and make the explicit cursor worth revisiting.

Verification

  • Equivalence: the existing FindBatchesJobTest (Postgres, testcontainers) runs against the new function and covers the >=1-batch, budget-crossing, combined per-partition/global-budget, max_batches_per_partition cap, ordering, and error paths. It is the regression oracle.
  • Scan-depth benchmark (FindBatchesScanDepthBenchmarkTest, @Tag("benchmark")): at a fixed fetch budget, find_batches latency stays flat (~50-90 ms) as partition depth grows 25k -> 400k, versus a roughly linear rise before. EXPLAIN of the inner scan confirms Index Scan on the covering index with no Sort.
  • Result-size sweep: at fixed depth, per-row cost is flat as the returned batch count grows to 128k (linear O(k) build, no O(k^2) array append; plpgsql mutates the array variable in place).
  • Worst-case sizing (128 MiB request / 64 MiB-per-partition budget with 1 KiB batches ~= 131k metadata rows): measured ~0.9 s for the find_batches call, ~26 MB metadata array. This is metadata only; the record data never enters the control plane. O(result), no runaway.

Follow-ups (out of scope, separate items)

  • Apply the same O(depth) -> O(result) treatment to the retention/write path (enforce_retention_v2), which additionally holds logs FOR UPDATE across a full-depth boundary scan (commit-latency tail): compute the boundary lock-free, take the row lock only for the DELETE.
  • Optional covering-index widening: the scan is now O(result) rows, but each returned row still does a heap fetch + files lookup; index-only is a constant-factor win, benchmark-gated (costs commit-path WAL).
  • A representative CI plan-guard (assert no Sort above the scan) as a stronger regression detector than the current benchmark-only EXPLAIN.

How to run the benchmark

./gradlew :storage:inkless:benchmarkTest --tests '*FindBatchesScanDepthBenchmarkTest*'

@jeqo
jeqo requested a review from Copilot July 15, 2026 12:00
@jeqo jeqo changed the title fix(inkless:consume): bound find_batches_v2 scan to the fetch budget (O(result), not O(read-depth)) fix(inkless:consume): bound find_batches_v2 scan to the fetch budget (O(result), not O(read-depth)) [KC-327] Jul 15, 2026

Copilot AI 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.

Pull request overview

This PR updates Inkless’s Postgres control-plane read path by rewriting find_batches_v2 (migration V19) so it stops scanning as soon as fetch byte budgets are crossed, making the query cost proportional to bytes returned rather than partition backlog depth. It also adds an opt-in benchmark to validate scan-depth scaling behavior and bumps the jOOQ-generated schema version stamp to 19.

Changes:

  • Rewrite find_batches_v2 as a per-partition PL/pgSQL streaming loop that exits early once per-partition or global fetch budgets are reached (migration V19).
  • Add a @Tag("benchmark") JUnit test to measure find_batches_v2 latency vs partition scan depth and to print an EXPLAIN plan for the inner scan.
  • Regenerate jOOQ artifacts to reflect schema version 19.

Reviewed changes

Copilot reviewed 2 out of 120 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
storage/inkless/src/test/java/io/aiven/inkless/control_plane/postgres/FindBatchesScanDepthBenchmarkTest.java Adds an opt-in benchmark test for scan-depth vs latency and an EXPLAIN helper.
storage/inkless/src/main/resources/db/migration/V19__Find_batches_bounded_scan.sql Replaces find_batches_v2 implementation with a bounded streaming scan in PL/pgSQL.
storage/inkless/src/main/jooq/org/jooq/generated/UDTs.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/RepairDisklessLogResponseV1.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/RepairDisklessLogRequestV1.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/records/RepairDisklessLogResponseV1Record.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/records/RepairDisklessLogRequestV1Record.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/records/PruneBatchesBelowHighestTieredOffsetResponseV1Record.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/records/PruneBatchesBelowHighestTieredOffsetRequestV1Record.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/records/ListOffsetsResponseV1Record.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/records/ListOffsetsRequestV1Record.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/records/InitDisklessLogResponseV1Record.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/records/InitDisklessLogRequestV1Record.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/records/InitDisklessLogProducerStateV1Record.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/records/FindBatchesResponseV1Record.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/records/FindBatchesRequestV1Record.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/records/EnforceRetentionResponseV1Record.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/records/EnforceRetentionRequestV1Record.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/records/DeleteRecordsResponseV1Record.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/records/DeleteRecordsRequestV1Record.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/records/CommitBatchResponseV1Record.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/records/CommitBatchRequestV1Record.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/records/BatchMetadataV1Record.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/records/BatchInfoV1Record.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/records/AdvanceCrossTierLogStartResponseV1Record.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/records/AdvanceCrossTierLogStartRequestV1Record.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/PruneBatchesBelowHighestTieredOffsetResponseV1.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/PruneBatchesBelowHighestTieredOffsetRequestV1.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/paths/RepairDisklessLogResponseV1Path.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/paths/RepairDisklessLogRequestV1Path.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/paths/PruneBatchesBelowHighestTieredOffsetResponseV1Path.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/paths/PruneBatchesBelowHighestTieredOffsetRequestV1Path.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/paths/ListOffsetsResponseV1Path.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/paths/ListOffsetsRequestV1Path.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/paths/InitDisklessLogResponseV1Path.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/paths/InitDisklessLogRequestV1Path.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/paths/InitDisklessLogProducerStateV1Path.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/paths/FindBatchesResponseV1Path.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/paths/FindBatchesRequestV1Path.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/paths/EnforceRetentionResponseV1Path.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/paths/EnforceRetentionRequestV1Path.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/paths/DeleteRecordsResponseV1Path.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/paths/DeleteRecordsRequestV1Path.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/paths/CommitBatchResponseV1Path.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/paths/CommitBatchRequestV1Path.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/paths/BatchMetadataV1Path.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/paths/BatchInfoV1Path.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/paths/AdvanceCrossTierLogStartResponseV1Path.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/paths/AdvanceCrossTierLogStartRequestV1Path.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/ListOffsetsResponseV1.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/ListOffsetsRequestV1.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/InitDisklessLogResponseV1.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/InitDisklessLogRequestV1.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/InitDisklessLogProducerStateV1.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/FindBatchesResponseV1.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/FindBatchesRequestV1.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/EnforceRetentionResponseV1.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/EnforceRetentionRequestV1.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/DeleteRecordsResponseV1.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/DeleteRecordsRequestV1.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/CommitBatchResponseV1.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/CommitBatchRequestV1.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/BatchMetadataV1.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/BatchInfoV1.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/AdvanceCrossTierLogStartResponseV1.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/udt/AdvanceCrossTierLogStartRequestV1.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/tables/RepairDisklessLogV1.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/tables/records/RepairDisklessLogV1Record.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/tables/records/PruneBatchesBelowHighestTieredOffsetV1Record.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/tables/records/ProducerStateRecord.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/tables/records/LogsRecord.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/tables/records/ListOffsetsV1Record.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/tables/records/InitDisklessLogV1Record.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/tables/records/FindBatchesV2Record.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/tables/records/FindBatchesV1Record.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/tables/records/FilesRecord.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/tables/records/EnforceRetentionV2Record.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/tables/records/EnforceRetentionV1Record.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/tables/records/DeleteRecordsV1Record.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/tables/records/CommitFileV2Record.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/tables/records/CommitFileV1Record.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/tables/records/BatchesRecord.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/tables/records/AdvanceCrossTierLogStartV1Record.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/tables/PruneBatchesBelowHighestTieredOffsetV1.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/tables/ProducerState.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/tables/Logs.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/tables/ListOffsetsV1.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/tables/InitDisklessLogV1.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/tables/FindBatchesV2.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/tables/FindBatchesV1.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/tables/Files.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/tables/EnforceRetentionV2.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/tables/EnforceRetentionV1.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/tables/DeleteRecordsV1.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/tables/CommitFileV2.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/tables/CommitFileV1.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/tables/Batches.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/tables/AdvanceCrossTierLogStartV1.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/Tables.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/routines/MarkFileToDeleteV1.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/routines/FlushCommitRunV2.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/routines/DeleteTopicV1.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/routines/DeleteFilesV1.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/routines/DeleteBatchV1.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/routines/BatchTimestamp.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/Routines.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/Keys.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/Indexes.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/enums/PruneBatchesBelowHighestTieredOffsetErrorV1.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/enums/ListOffsetsResponseErrorV1.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/enums/InitDisklessLogErrorV1.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/enums/FindBatchesResponseErrorV1.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/enums/FileStateT.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/enums/FileReasonT.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/enums/EnforceRetentionResponseErrorV1.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/enums/DeleteRecordsResponseErrorV1.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/enums/CommitBatchResponseErrorV1.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/enums/AdvanceCrossTierLogStartResponseErrorV1.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/Domains.java jOOQ regen: schema version stamp bump to 19.
storage/inkless/src/main/jooq/org/jooq/generated/DefaultSchema.java jOOQ regen: schema version stamp bump to 19.

@jeqo
jeqo force-pushed the jeqo/fix-find-batches branch from 2e351ca to e0ec43a Compare July 15, 2026 12:46
@jeqo
jeqo requested a review from Copilot July 15, 2026 12:47

Copilot AI 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.

Pull request overview

Copilot reviewed 2 out of 120 changed files in this pull request and generated 1 comment.

jeqo added 2 commits July 15, 2026 15:56
@tag("benchmark") harness measuring find_batches_v2 latency vs partition scan
depth at a fixed fetch budget, to expose the O(read-depth) query cost: a
lagging consumer's per-fetch latency grows with its lag while the returned
result stays constant. Seeds one deep partition and measures at depth
checkpoints in a single pass, and dumps an EXPLAIN of the inner scan to check
whether the plan can terminate early (index-ordered, no sort). Also sweeps
result size at fixed depth to confirm the per-row build cost is flat (O(k),
no quadratic array append).
find_batches_v2 computed ROW_NUMBER()/SUM() OVER windows over the whole
[starting_offset, high_watermark) range before trimming to the byte budget,
so a fetch cost O(read-depth): a lagging consumer paid for its entire lag on
every call. V19 rewrites it as a per-partition plpgsql loop that streams
batches in last_offset order (index scan, no sort) and stops once the
per-partition or global byte budget is crossed, making cost O(result).
Semantics are unchanged from V15 (error handling, >=1 batch per partition,
budget-crossing batch included, request-order responses).

The generated jOOQ sources carry only the schema-version stamp bump to 19.
@jeqo
jeqo force-pushed the jeqo/fix-find-batches branch from e0ec43a to 16016bc Compare July 15, 2026 12:56
@jeqo
jeqo requested a review from Copilot July 15, 2026 12:56

Copilot AI 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.

Pull request overview

Copilot reviewed 2 out of 120 changed files in this pull request and generated no new comments.

@jeqo
jeqo marked this pull request as ready for review July 15, 2026 13:07
@ivanyu
ivanyu merged commit e69ef85 into main Jul 16, 2026
11 of 12 checks passed
@ivanyu
ivanyu deleted the jeqo/fix-find-batches branch July 16, 2026 09:23
jeqo added a commit that referenced this pull request Jul 20, 2026
…(O(result), not O(read-depth)) [KC-327] (#701)

* test(inkless:consume): add find_batches scan-depth benchmark

@tag("benchmark") harness measuring find_batches_v2 latency vs partition scan
depth at a fixed fetch budget, to expose the O(read-depth) query cost: a
lagging consumer's per-fetch latency grows with its lag while the returned
result stays constant. Seeds one deep partition and measures at depth
checkpoints in a single pass, and dumps an EXPLAIN of the inner scan to check
whether the plan can terminate early (index-ordered, no sort). Also sweeps
result size at fixed depth to confirm the per-row build cost is flat (O(k),
no quadratic array append).

* fix(inkless:consume): bound find_batches_v2 scan to the fetch budget

find_batches_v2 computed ROW_NUMBER()/SUM() OVER windows over the whole
[starting_offset, high_watermark) range before trimming to the byte budget,
so a fetch cost O(read-depth): a lagging consumer paid for its entire lag on
every call. V19 rewrites it as a per-partition plpgsql loop that streams
batches in last_offset order (index scan, no sort) and stops once the
per-partition or global byte budget is crossed, making cost O(result).
Semantics are unchanged from V15 (error handling, >=1 batch per partition,
budget-crossing batch included, request-order responses).

The generated jOOQ sources carry only the schema-version stamp bump to 19.
jeqo added a commit that referenced this pull request Jul 20, 2026
…(O(result), not O(read-depth)) [KC-327] (#701)

* test(inkless:consume): add find_batches scan-depth benchmark

@tag("benchmark") harness measuring find_batches_v2 latency vs partition scan
depth at a fixed fetch budget, to expose the O(read-depth) query cost: a
lagging consumer's per-fetch latency grows with its lag while the returned
result stays constant. Seeds one deep partition and measures at depth
checkpoints in a single pass, and dumps an EXPLAIN of the inner scan to check
whether the plan can terminate early (index-ordered, no sort). Also sweeps
result size at fixed depth to confirm the per-row build cost is flat (O(k),
no quadratic array append).

* fix(inkless:consume): bound find_batches_v2 scan to the fetch budget

find_batches_v2 computed ROW_NUMBER()/SUM() OVER windows over the whole
[starting_offset, high_watermark) range before trimming to the byte budget,
so a fetch cost O(read-depth): a lagging consumer paid for its entire lag on
every call. V19 rewrites it as a per-partition plpgsql loop that streams
batches in last_offset order (index scan, no sort) and stops once the
per-partition or global byte budget is crossed, making cost O(result).
Semantics are unchanged from V15 (error handling, >=1 batch per partition,
budget-crossing batch included, request-order responses).

The generated jOOQ sources carry only the schema-version stamp bump to 19.
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