Repository navigation
feat(inkless:consolidation): bound the WAL prune per request [KC-444] - #843
Merged
Merged
Conversation
viktorsomogyi
force-pushed
the
svv/async-prune
branch
from
October 29, 2026 09:07
50c9388 to
8df1ff3
Compare
viktorsomogyi
force-pushed
the
svv/async-prune
branch
3 times, most recently
from
October 29, 2026 11:49
d1f0874 to
30af211
Compare
…444] Set `consolidation.cleanup.max.batches.per.request` to 2000 and `consolidation.cleanup.interval.ms` to 1 minute. These defaults match retention's per-minute 2000-batch rate, so consolidation sustains about 33 batches per second while each locked delete stays within retention's batch count. A value of 0 remains uncapped, and the config reference records the roughly eight-hour drain time for a 1,000,000-batch backlog. Testing - `JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:test --tests io.aiven.inkless.config.InklessConfigTest` - `PATH="$HOME/Library/Python/3.13/bin:$PATH" make vale` Assisted-by: GPT-5.6 Luna Max Assisted-by: Grok 4.7
Keep the existing v1 function available during rolling upgrades while v2 bounds each prune delete and reports deleted counts. The new function locks only partitions with work, preserves the cross-tier start bootstrap, and advances empty switched logs to the seal. The keep-offset lookup reads one covering-index entry, so the `logs` row lock covers only that read. The log start sitting at or before the tiered offset doesn't by itself mean a batch is deletable: the oldest remaining batch can still straddle the tiered offset when it's a multi-record or coalesced batch only partially tiered. v2 probes the keep-offset unlocked before falling back to the lock, mirroring retention enforcement's unlocked boundary scan, and probes it again right after acquiring the lock. Either probe finding nothing to delete returns early, so a prune with nothing to delete never takes the `logs` lock or calls `delete_records_v1` purely to re-set `log_start_offset` to its current value. Testing - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:compileTestJava --no-watch-fs --rerun-tasks - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:test --no-watch-fs --tests io.aiven.inkless.control_plane.postgres.PruneBatchesBelowHighestTieredOffsetV1Test - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:test --no-watch-fs --tests io.aiven.inkless.control_plane.postgres.PruneBatchesBelowHighestTieredOffsetV2Test - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:test --no-watch-fs --tests io.aiven.inkless.control_plane.postgres.PostgresControlPlaneTest - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:test --no-watch-fs --tests io.aiven.inkless.config.InklessConfigTest - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:test --no-watch-fs --tests io.aiven.inkless.control_plane.postgres.BatchesPlanRegressionTest - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:test --no-watch-fs --max-workers=1 Assisted-by: GPT-5.6 Luna Max Assisted-by: Grok 4.7
The control plane passes the per-request cap to the v2 prune function and returns deletion counts. The job isolates each partition in its own retried transaction so a retry doesn't repeat committed partitions. The in-memory control plane mirrors the cap, handoff, and no-op behavior. Testing - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:test --rerun-tasks --tests io.aiven.inkless.control_plane.InMemoryControlPlaneTest --tests io.aiven.inkless.control_plane.postgres.PostgresControlPlaneTest --tests io.aiven.inkless.control_plane.InMemoryControlPlanePruneDisklessLogsTest --tests io.aiven.inkless.control_plane.postgres.PruneBatchesBelowHighestTieredOffsetV1Test --tests io.aiven.inkless.control_plane.postgres.PruneBatchesBelowHighestTieredOffsetV2Test --tests io.aiven.inkless.control_plane.postgres.LogsEarliestBatchTimestampTest - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :core:test --rerun-tasks --tests io.aiven.inkless.consolidation.ConsolidatedDisklessLogPrunerTest - make vale Assisted-by: GPT-5.6 Luna Max Assisted-by: Grok 4.7
Bound each consolidation prune request with the broker cap and report successful-run deletion totals and duration. This keeps prune work bounded and makes partial runs invisible to completion metrics. The generated metrics reference now lists the per-partition `PruneDisklessLogs` query metrics that the docs describe. Testing - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :core:test --tests io.aiven.inkless.consolidation.ConsolidatedDisklessLogPrunerTest - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:test --tests io.aiven.inkless.delete.ConsolidatedDisklessLogPrunerMetricsTest --tests io.aiven.inkless.control_plane.postgres.PostgresControlPlaneMetricsTest - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:genInklessMetricsDoc - make vale Assisted-by: GPT-5.6 Luna Max Assisted-by: Grok 4.7
The bounded prune must skip a lock for completed partitions while retaining the lock for bootstrap and delete work. Postgres tests hold `logs` rows from a second connection, observe `pg_blocking_pids`, verify per-partition lock release, and exercise a transient `40P01` retry. They also check one duration sample per partition, a log soft-deleted while the prune waits for its lock, a straddling oldest batch that leaves the log start at or before the tiered offset with nothing actually deletable, and a later partition failure that leaves earlier partitions committed. The benchmark seeds an identical 200,000-tiered-batch backlog on two partitions, then compares the longest lock hold of the old unbounded `v1` call against the longest single hold across 100 capped `v2` passes that clear the same total batch count. A background thread commits continuously through each scenario's whole measurement window, so the reported commit-latency sample covers the run, not a couple of commits that happened to land inside one short pass. Testing: - `JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:test --tests io.aiven.inkless.control_plane.postgres.PruneDisklessLogsJobTest --tests io.aiven.inkless.control_plane.postgres.PruneBatchesBelowHighestTieredOffsetV2Test --tests io.aiven.inkless.control_plane.postgres.EnforceRetentionJobTest --no-daemon` passes. - `JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:benchmarkTest --tests io.aiven.inkless.control_plane.postgres.PruneDisklessLogsCommitBlockingBenchmarkTest --no-daemon` passes. Across four local runs, v1's single lock hold ran 196ms, 164ms, 179ms, and 179ms; v2's longest single pass among 100 ran 41ms, 38ms, 38ms, and 40ms, a 4.3x-4.8x gap. - `make vale` passes. Assisted-by: GPT-5.6 Luna Max Assisted-by: Grok 4.7
A completed no-op prune needs zero-valued observability, while a partial run must not look successful. Document and test the batch-boundary keep offset, per-attempt cap, empty switched-log handoff, and partition-scoped deadlock retry. Testing - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :core:test --tests io.aiven.inkless.consolidation.ConsolidatedDisklessLogPrunerTest --no-daemon --rerun-tasks - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:test --tests io.aiven.inkless.control_plane.postgres.PruneDisklessLogsJobTest --no-daemon - PATH="$HOME/Library/Python/3.13/bin:$PATH" make vale Assisted-by: GPT-5.6 Luna Max Assisted-by: Grok 4.7
Vale's reStructuredText parser calls rst2html before it applies any style rule. The runner installs Vale and its packages, and it has no docutils, so a pull request that changes an owned rst file fails the lint job with E100 before any alert is reported. Assisted-by: Grok 4.7
viktorsomogyi
force-pushed
the
svv/async-prune
branch
from
October 29, 2026 11:52
30af211 to
c19eba0
Compare
viktorsomogyi
marked this pull request as ready for review
October 29, 2026 11:56
jeqo
reviewed
Oct 30, 2026
Contributor
There was a problem hiding this comment.
LGMT. Just added a comment on a missing error metric, but should be good after that.
| } | ||
| } | ||
| } | ||
| metrics.recordConsolidationPruneFinishedSuccessfully( |
Contributor
There was a problem hiding this comment.
Could we add a an error rate metric as well?
Contributor
Author
There was a problem hiding this comment.
Ok, added it.
The consolidated diskless log pruner updates its prune totals only after a run completes. A run that throws left no trace in the metrics, so operators could not see that pruning had stopped working. A new error rate counts each run that fails with an exception. A tick that the control plane reports as unavailable does not count. The availability gate already skips that tick, so it is not a prune failure. Testing ConsolidatedDisklessLogPrunerMetricsTest covers the new counter and its removal on close. ConsolidatedDisklessLogPrunerTest checks that a failed run increments the counter and that a control-plane-unavailable run does not. Assisted-by: Auto
jeqo
approved these changes
Nov 2, 2026
gqmelo
pushed a commit
that referenced
this pull request
Nov 5, 2026
…#843) * feat(inkless:config): set a 2000-batch cap and 1-minute interval [KC-444] Set `consolidation.cleanup.max.batches.per.request` to 2000 and `consolidation.cleanup.interval.ms` to 1 minute. These defaults match retention's per-minute 2000-batch rate, so consolidation sustains about 33 batches per second while each locked delete stays within retention's batch count. A value of 0 remains uncapped, and the config reference records the roughly eight-hour drain time for a 1,000,000-batch backlog. Testing - `JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew : 8000 storage:inkless:test --tests io.aiven.inkless.config.InklessConfigTest` - `PATH="$HOME/Library/Python/3.13/bin:$PATH" make vale` Assisted-by: GPT-5.6 Luna Max Assisted-by: Grok 4.7 * feat(inkless:storage): add bounded prune v2 function [KC-444] Keep the existing v1 function available during rolling upgrades while v2 bounds each prune delete and reports deleted counts. The new function locks only partitions with work, preserves the cross-tier start bootstrap, and advances empty switched logs to the seal. The keep-offset lookup reads one covering-index entry, so the `logs` row lock covers only that read. The log start sitting at or before the tiered offset doesn't by itself mean a batch is deletable: the oldest remaining batch can still straddle the tiered offset when it's a multi-record or coalesced batch only partially tiered. v2 probes the keep-offset unlocked before falling back to the lock, mirroring retention enforcement's unlocked boundary scan, and probes it again right after acquiring the lock. Either probe finding nothing to delete returns early, so a prune with nothing to delete never takes the `logs` lock or calls `delete_records_v1` purely to re-set `log_start_offset` to its current value. Testing - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:compileTestJava --no-watch-fs --rerun-tasks - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:test --no-watch-fs --tests io.aiven.inkless.control_plane.postgres.PruneBatchesBelowHighestTieredOffsetV1Test - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:test --no-watch-fs --tests io.aiven.inkless.control_plane.postgres.PruneBatchesBelowHighestTieredOffsetV2Test - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:test --no-watch-fs --tests io.aiven.inkless.control_plane.postgres.PostgresControlPlaneTest - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:test --no-watch-fs --tests io.aiven.inkless.config.InklessConfigTest - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:test --no-watch-fs --tests io.aiven.inkless.control_plane.postgres.BatchesPlanRegressionTest - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:test --no-watch-fs --max-workers=1 Assisted-by: GPT-5.6 Luna Max Assisted-by: Grok 4.7 * feat(inkless:storage): wire bounded prune through control plane [KC-444] The control plane passes the per-request cap to the v2 prune function and returns deletion counts. The job isolates each partition in its own retried transaction so a retry doesn't repeat committed partitions. The in-memory control plane mirrors the cap, handoff, and no-op behavior. Testing - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:test --rerun-tasks --tests io.aiven.inkless.control_plane.InMemoryControlPlaneTest --tests io.aiven.inkless.control_plane.postgres.PostgresControlPlaneTest --tests io.aiven.inkless.control_plane.InMemoryControlPlanePruneDisklessLogsTest --tests io.aiven.inkless.control_plane.postgres.PruneBatchesBelowHighestTieredOffsetV1Test --tests io.aiven.inkless.control_plane.postgres.PruneBatchesBelowHighestTieredOffsetV2Test --tests io.aiven.inkless.control_plane.postgres.LogsEarliestBatchTimestampTest - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :core:test --rerun-tasks --tests io.aiven.inkless.consolidation.ConsolidatedDisklessLogPrunerTest - make vale Assisted-by: GPT-5.6 Luna Max Assisted-by: Grok 4.7 * feat(inkless:storage): wire consolidation prune cap and metrics [KC-444] Bound each consolidation prune request with the broker cap and report successful-run deletion totals and duration. This keeps prune work bounded and makes partial runs invisible to completion metrics. The generated metrics reference now lists the per-partition `PruneDisklessLogs` query metrics that the docs describe. Testing - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :core:test --tests io.aiven.inkless.consolidation.ConsolidatedDisklessLogPrunerTest - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:test --tests io.aiven.inkless.delete.ConsolidatedDisklessLogPrunerMetricsTest --tests io.aiven.inkless.control_plane.postgres.PostgresControlPlaneMetricsTest - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:genInklessMetricsDoc - make vale Assisted-by: GPT-5.6 Luna Max Assisted-by: Grok 4.7 * test(inkless:storage): verify prune lock behavior [KC-444] The bounded prune must skip a lock for completed partitions while retaining the lock for bootstrap and delete work. Postgres tests hold `logs` rows from a second connection, observe `pg_blocking_pids`, verify per-partition lock release, and exercise a transient `40P01` retry. They also check one duration sample per partition, a log soft-deleted while the prune waits for its lock, a straddling oldest batch that leaves the log start at or before the tiered offset with nothing actually deletable, and a later partition failure that leaves earlier partitions committed. The benchmark seeds an identical 200,000-tiered-batch backlog on two partitions, then compares the longest lock hold of the old unbounded `v1` call against the longest single hold across 100 capped `v2` passes that clear the same total batch count. A background thread commits continuously through each scenario's whole measurement window, so the reported commit-latency sample covers the run, not a couple of commits that happened to land inside one short pass. Testing: - `JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:test --tests io.aiven.inkless.control_plane.postgres.PruneDisklessLogsJobTest --tests io.aiven.inkless.control_plane.postgres.PruneBatchesBelowHighestTieredOffsetV2Test --tests io.aiven.inkless.control_plane.postgres.EnforceRetentionJobTest --no-daemon` passes. - `JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:benchmarkTest --tests io.aiven.inkless.control_plane.postgres.PruneDisklessLogsCommitBlockingBenchmarkTest --no-daemon` passes. Across four local runs, v1's single lock hold ran 196ms, 164ms, 179ms, and 179ms; v2's longest single pass among 100 ran 41ms, 38ms, 38ms, and 40ms, a 4.3x-4.8x gap. - `make vale` passes. Assisted-by: GPT-5.6 Luna Max Assisted-by: Grok 4.7 * fix(inkless:consolidation): finalize prune bounds behavior [KC-444] A completed no-op prune needs zero-valued observability, while a partial run must not look successful. Document and test the batch-boundary keep offset, per-attempt cap, empty switched-log handoff, and partition-scoped deadlock retry. Testing - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :core:test --tests io.aiven.inkless.consolidation.ConsolidatedDisklessLogPrunerTest --no-daemon --rerun-tasks - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:test --tests io.aiven.inkless.control_plane.postgres.PruneDisklessLogsJobTest --no-daemon - PATH="$HOME/Library/Python/3.13/bin:$PATH" make vale Assisted-by: GPT-5.6 Luna Max Assisted-by: Grok 4.7 * ci(inkless): install rst2html for the Vale runner Vale's reStructuredText parser calls rst2html before it applies any style rule. The runner installs Vale and its packages, and it has no docutils, so a pull request that changes an owned rst file fails the lint job with E100 before any alert is reported. Assisted-by: Grok 4.7 * feat(inkless:consolidation): add consolidation prune error rate [KC-444] The consolidated diskless log pruner updates its prune totals only after a run completes. A run that throws left no trace in the metrics, so operators could not see that pruning had stopped working. A new error rate counts each run that fails with an exception. A tick that the control plane reports as unavailable does not count. The availability gate already skips that tick, so it is not a prune failure. Testing ConsolidatedDisklessLogPrunerMetricsTest covers the new counter and its removal on close. ConsolidatedDisklessLogPrunerTest checks that a failed run increments the counter and that a control-plane-unavailable run does not. Assisted-by: Auto
gqmelo
pushed a commit
that referenced
this pull request
Nov 5, 2026
…#843) * feat(inkless:config): set a 2000-batch cap and 1-minute interval [KC-444] Set `consolidation.cleanup.max.batches.per.request` to 2000 and `consolidation.cleanup.interval.ms` to 1 minute. These defaults match retention's per-minute 2000-batch rate, so consolidation sustains about 33 batches per second while each locked delete stays within retention's batch count. A value of 0 remains uncapped, and the config reference records the roughly eight-hour drain time for a 1,000,000-batch backlog. Testing - `JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:test --tests io.aiven.inkless.config.InklessConfigTest` - `PATH="$HOME/Library/Python/3.13/bin:$PATH" make vale` Assisted-by: GPT-5.6 Luna Max Assisted-by: Grok 4.7 * feat(inkless:storage): add bounded prune v2 function [KC-444] Keep the existing v1 function available during rolling upgrades while v2 bounds each prune delete and reports deleted counts. The new function locks only partitions with work, preserves the cross-tier start bootstrap, and advances empty switched logs to the seal. The keep-offset lookup reads one covering-index entry, so the `logs` row lock covers only that read. The log start sitting at or before the tiered offset doesn't by itself mean a batch is deletable: the oldest remaining batch can still straddle the tiered offset when it's a multi-record or coalesced batch only partially tiered. v2 probes the keep-offset unlocked before falling back to the lock, mirroring retention enforcement's unlocked boundary scan, and probes it again right after acquiring the lock. Either probe finding nothing to delete returns early, so a prune with nothing to delete never takes the `logs` lock or calls `delete_records_v1` purely to re-set `log_start_offset` to its current value. Testing - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:compileTestJava --no-watch-fs --rerun-tasks - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:test --no-watch-fs --tests io.aiven.inkless.control_plane.postgres.PruneBatchesBelowHighestTieredOffsetV1Test - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:test --no-watch-fs --tests io.aiven.inkless.control_plane.postgres.PruneBatchesBelowHighestTieredOffsetV2Test - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:test --no-watch-fs --tests io.aiven.inkless.control_plane.postgres.PostgresControlPlaneTest - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:test --no-watch-fs --tests io.aiven.inkless.config.InklessConfigTest - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:test --no-watch-fs --tests io.aiven.inkless.control_plane.postgres.BatchesPlanRegressionTest - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:test --no-watch-fs --max-workers=1 Assisted-by: GPT-5.6 Luna Max Assisted-by: Grok 4.7 * feat(inkless:storage): wire bounded prune through control plane [KC-444] The control plane passes the per-request cap to the v2 prune function and returns deletion counts. The job isolates each partition in its own retried transaction so a retry doesn't repeat committed partitions. The in-memory control plane mirrors the cap, handoff, and no-op behavior. Testing - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:test --rerun-tasks --tests io.aiven.inkless.control_plane.InMemoryControlPlaneTest --tests io.aiven.inkless.control_plane.postgres.PostgresControlPlaneTest --tests io.aiven.inkless.control_plane.InMemoryControlPlanePruneDisklessLogsTest --tests io.aiven.inkless.control_plane.postgres.PruneBatchesBelowHighestTieredOffsetV1Test --tests io.aiven.inkless.control_plane.postgres.PruneBatchesBelowHighestTieredOffsetV2Test --tests io.aiven.inkless.control_plane.postgres.LogsEarliestBatchTimestampTest - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :core:test --rerun-tasks --tests io.aiven.inkless.consolidation.ConsolidatedDisklessLogPrunerTest - make vale Assisted-by: GPT-5.6 Luna Max Assisted-by: Grok 4.7 * feat(inkless:storage): wire consolidation prune cap and metrics [KC-444] Bound each consolidation prune request with the broker cap and report successful-run deletion totals and duration. This keeps prune work bounded and makes partial runs invisible to completion metrics. The generated metrics reference now lists the per-partition `PruneDisklessLogs` query metrics that the docs describe. Testing - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :core:test --tests io.aiven.inkless.consolidation.ConsolidatedDisklessLogPrunerTest - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:test --tests io.aiven.inkless.delete.ConsolidatedDisklessLogPrunerMetricsTest --tests io.aiven.inkless.control_plane.postgres.PostgresControlPlaneMetricsTest - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:genInklessMetricsDoc - make vale Assisted-by: GPT-5.6 Luna Max Assisted-by: Grok 4.7 * test(inkless:storage): verify prune lock behavior [KC-444] The bounded prune must skip a lock for completed partitions while retaining the lock for bootstrap and delete work. Postgres tests hold `logs` rows from a second connection, observe `pg_blocking_pids`, verify per-partition lock release, and exercise a transient `40P01` retry. They also check one duration sample per partition, a log soft-deleted while the prune waits for its lock, a straddling oldest batch that leaves the log start at or before the tiered offset with nothing actually deletable, and a later partition failure that leaves earlier partitions committed. The benchmark seeds an identical 200,000-tiered-batch backlog on two partitions, then compares the longest lock hold of the old unbounded `v1` call against the longest single hold across 100 capped `v2` passes that clear the same total batch count. A background thread commits continuously through each scenario's whole measurement window, so the reported commit-latency sample covers the run, not a couple of commits that happened to land inside one short pass. Testing: - `JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:test --tests io.aiven.inkless.control_plane.postgres.PruneDisklessLogsJobTest --tests io.aiven.inkless.control_plane.postgres.PruneBatchesBelowHighestTieredOffsetV2Test --tests io.aiven.inkless.control_plane.postgres.EnforceRetentionJobTest --no-daemon` passes. - `JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:benchmarkTest --tests io.aiven.inkless.control_plane.postgres.PruneDisklessLogsCommitBlockingBenchmarkTest --no-daemon` passes. Across four local runs, v1's single lock hold ran 196ms, 164ms, 179ms, and 179ms; v2's longest single pass among 100 ran 41ms, 38ms, 38ms, and 40ms, a 4.3x-4.8x gap. - `make vale` passes. Assisted-by: GPT-5.6 Luna Max Assisted-by: Grok 4.7 * fix(inkless:consolidation): finalize prune bounds behavior [KC-444] A completed no-op prune needs zero-valued observability, while a partial run must not look successful. Document and test the batch-boundary keep offset, per-attempt cap, empty switched-log handoff, and partition-scoped deadlock retry. Testing - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :core:test --tests io.aiven.inkless.consolidation.ConsolidatedDisklessLogPrunerTest --no-daemon --rerun-tasks - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:test --tests io.aiven.inkless.control_plane.postgres.PruneDisklessLogsJobTest --no-daemon - PATH="$HOME/Library/Python/3.13/bin:$PATH" make vale Assisted-by: GPT-5.6 Luna Max Assisted-by: Grok 4.7 * ci(inkless): install rst2html for the Vale runner Vale's reStructuredText parser calls rst2html before it applies any style rule. The runner installs Vale and its packages, and it has no docutils, so a pull request that changes an owned rst file fails the lint job with E100 before any alert is reported. Assisted-by: Grok 4.7 * feat(inkless:consolidation): add consolidation prune error rate [KC-444] The consolidated diskless log pruner updates its prune totals only after a run completes. A run that throws left no trace in the metrics, so operators could not see that pruning had stopped working. A new error rate counts each run that fails with an exception. A tick that the control plane reports as unavailable does not count. The availability gate already skips that tick, so it is not a prune failure. Testing ConsolidatedDisklessLogPrunerMetricsTest covers the new counter and its removal on close. ConsolidatedDisklessLogPrunerTest checks that a failed run increments the counter and that a control-plane-unavailable run does not. Assisted-by: Auto
gqmelo
pushed a commit
that referenced
this pull request
Nov 5, 2026
…#843) * feat(inkless:config): set a 2000-batch cap and 1-minute interval [KC-444] Set `consolidation.cleanup.max.batches.per.request` to 2000 and `consolidation.cleanup.interval.ms` to 1 minute. These defaults match retention's per-minute 2000-batch rate, so consolidation sustains about 33 batches per second while each locked delete stays within retention's batch count. A value of 0 remains uncapped, and the config reference records the roughly eight-hour drain time for a 1,000,000-batch backlog. Testing - `JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:test --tests io.aiven.inkless.config.InklessConfigTest` - `PATH="$HOME/Library/Python/3.13/bin:$PATH" make vale` Assisted-by: GPT-5.6 Luna Max Assisted-by: Grok 4.7 * feat(inkless:storage): add bounded prune v2 function [KC-444] Keep the existing v1 function available during rolling upgrades while v2 bounds each prune delete and reports deleted counts. The new function locks only partitions with work, preserves the cross-tier start bootstrap, and advances empty switched logs to the seal. The keep-offset lookup reads one covering-index entry, so the `logs` row lock covers only that read. The log start sitting at or before the tiered offset doesn't by itself mean a batch is deletable: the oldest remaining batch can still straddle the tiered offset when it's a multi-record or coalesced batch only partially tiered. v2 probes the keep-offset unlocked before falling back to the lock, mirroring retention enforcement's unlocked boundary scan, and probes it again right after acquiring the lock. Either probe finding nothing to delete returns early, so a prune with nothing to delete never takes the `logs` lock or calls `delete_records_v1` purely to re-set `log_start_offset` to its current value. Testing - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:compileTestJava --no-watch-fs --rerun-tasks - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:test --no-watch-fs --tests io.aiven.inkless.control_plane.postgres.PruneBatchesBelowHighestTieredOffsetV1Test - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:test --no-watch-fs --tests io.aiven.inkless.control_plane.postgres.PruneBatchesBelowHighestTieredOffsetV2Test - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:test --no-watch-fs --tests io.aiven.inkless.control_plane.postgres.PostgresControlPlaneTest - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:test --no-watch-fs --tests io.aiven.inkless.config.InklessConfigTest - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:test --no-watch-fs --tests io.aiven.inkless.control_plane.postgres.BatchesPlanRegressionTest - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:test --no-watch-fs --max-workers=1 Assisted-by: GPT-5.6 Luna Max Assisted-by: Grok 4.7 * feat(inkless:storage): wire bounded prune through control plane [KC-444] The control plane passes the per-request cap to the v2 prune function and returns deletion counts. The job isolates each partition in its own retried transaction so a retry doesn't repeat committed partitions. The in-memory control plane mirrors the cap, handoff, and no-op behavior. Testing - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:test --rerun-tasks --tests io.aiven.inkless.control_plane.InMemoryControlPlaneTest --tests io.aiven.inkless.control_plane.postgres.PostgresControlPlaneTest --tests io.aiven.inkless.control_plane.InMemoryControlPlanePruneDisklessLogsTest --tests io.aiven.inkless.control_plane.postgres.PruneBatchesBelowHighestTieredOffsetV1Test --tests io.aiven.inkless.control_plane.postgres.PruneBatchesBelowHighestTieredOffsetV2Test --tests io.aiven.inkless.control_plane.postgres.LogsEarliestBatchTimestampTest - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :core:test --rerun-tasks --tests io.aiven.inkless.consolidation.ConsolidatedDisklessLogPrunerTest - make vale Assisted-by: GPT-5.6 Luna Max Assisted-by: Grok 4.7 * feat(inkless:storage): wire consolidation prune cap and metrics [KC-444] Bound each consolidation prune request with the broker cap and report successful-run deletion totals and duration. This keeps prune work bounded and makes partial runs invisible to completion metrics. The generated metrics reference now lists the per-partition `PruneDisklessLogs` query metrics that the docs describe. Testing - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :core:test --tests io.aiven.inkless.consolidation.ConsolidatedDisklessLogPrunerTest - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:test --tests io.aiven.inkless.delete.ConsolidatedDisklessLogPrunerMetricsTest --tests io.aiven.inkless.control_plane.postgres.PostgresControlPlaneMetricsTest - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:genInklessMetricsDoc - make vale Assisted-by: GPT-5.6 Luna Max Assisted-by: Grok 4.7 * test(inkless:storage): verify prune lock behavior [KC-444] The bounded prune must skip a lock for completed partitions while retaining the lock for bootstrap and delete work. Postgres tests hold `logs` rows from a second connection, observe `pg_blocking_pids`, verify per-partition lock release, and exercise a transient `40P01` retry. They also check one duration sample per partition, a log soft-deleted while the prune waits for its lock, a straddling oldest batch that leaves the log start at or before the tiered offset with nothing actually deletable, and a later partition failure that leaves earlier partitions committed. The benchmark seeds an identical 200,000-tiered-batch backlog on two partitions, then compares the longest lock hold of the old unbounded `v1` call against the longest single hold across 100 capped `v2` passes that clear the same total batch count. A background thread commits continuously through each scenario's whole measurement window, so the reported commit-latency sample covers the run, not a couple of commits that happened to land inside one short pass. Testing: - `JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:test --tests io.aiven.inkless.control_plane.postgres.PruneDisklessLogsJobTest --tests io.aiven.inkless.control_plane.postgres.PruneBatchesBelowHighestTieredOffsetV2Test --tests io.aiven.inkless.control_plane.postgres.EnforceRetentionJobTest --no-daemon` passes. - `JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:benchmarkTest --tests io.aiven.inkless.control_plane.postgres.PruneDisklessLogsCommitBlockingBenchmarkTest --no-daemon` passes. Across four local runs, v1's single lock hold ran 196ms, 164ms, 179ms, and 179ms; v2's longest single pass among 100 ran 41ms, 38ms, 38ms, and 40ms, a 4.3x-4.8x gap. - `make vale` passes. Assisted-by: GPT-5.6 Luna Max Assisted-by: Grok 4.7 * fix(inkless:consolidation): finalize prune bounds behavior [KC-444] A completed no-op prune needs zero-valued observability, while a partial run must not look successful. Document and test the batch-boundary keep offset, per-attempt cap, empty switched-log handoff, and partition-scoped deadlock retry. Testing - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :core:test --tests io.aiven.inkless.consolidation.ConsolidatedDisklessLogPrunerTest --no-daemon --rerun-tasks - JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :storage:inkless:test --tests io.aiven.inkless.control_plane.postgres.PruneDisklessLogsJobTest --no-daemon - PATH="$HOME/Library/Python/3.13/bin:$PATH" make vale Assisted-by: GPT-5.6 Luna Max Assisted-by: Grok 4.7 * ci(inkless): install rst2html for the Vale runner Vale's reStructuredText parser calls rst2html before it applies any style rule. The runner installs Vale and its packages, and it has no docutils, so a pull request that changes an owned rst file fails the lint job with E100 before any alert is reported. Assisted-by: Grok 4.7 * feat(inkless:consolidation): add consolidation prune error rate [KC-444] The consolidated diskless log pruner updates its prune totals only after a run completes. A run that throws left no trace in the metrics, so operators could not see that pruning had stopped working. A new error rate counts each run that fails with an exception. A tick that the control plane reports as unavailable does not count. The availability gate already skips that tick, so it is not a prune failure. Testing ConsolidatedDisklessLogPrunerMetricsTest covers the new counter and its removal on close. ConsolidatedDisklessLogPrunerTest checks that a failed run increments the counter and that a control-plane-unavailable run does not. Assisted-by: Auto
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A consolidation prune of a deep already-tiered WAL backlog held every requested partition's
logsrow for an unbounded delete, in one transaction for the whole run.prune_batches_below_highest_tiered_offset_v2caps each delete and takes the lock only when that partition has work.v1stays, so a broker still on the old build keeps a function that resolves during a rolling upgrade.A successful run records
ConsolidationPruneTotalTime,ConsolidationPruneTotalBatchesDeleted, andConsolidationPruneTotalBytesDeleted. A thrown run records none of them.PruneDisklessLogsQueryTimeandPruneDisklessLogsQueryRaterecord one sample per partition.Evidence
prune_batches_below_highest_tiered_offset_v1holds thelogslock for one pass over the whole tiered backlog. The only bound isstatement_timeout, which aborts the transaction, and the lock hold grows with the backlog depth.After:
v2holds the lock for one capped pass at a time and releases it between passes, so the longest hold is bounded by the cap, not by backlog depth.A benchmark clears the same 200,000-batch tiered backlog (plus a 200,000-batch untiered tail) two ways: once under
v1in a single unbounded pass, and once underv2through 100 capped 2,000-batch passes. A background thread commits continuously through each scenario, so the commit-latency sample covers the whole run, not a couple of lucky data points.Both scenarios' worst-case commit latency sits inside the 5-second control-plane socket timeout on this fixture. Postgres clears an indexed range fast either way, so total prune time doesn't tell the real story here. The longest lock hold does: it sets the worst case a produce commit can stall behind.
v2's 100 passes take longer in total thanv1's one pass from per-transaction overhead, yet across four local runsv1's lo 8000 ck hold ran 4.3x-5.0x as long asv2's longest pass, for the same total deleted volume. A deeper backlog growsv1's lock hold further;v2's worst case stays capped regardless of backlog depth.PruneBatchesBelowHighestTieredOffsetV1Testcallsv1directly.PruneBatchesBelowHighestTieredOffsetV2Test,PruneDisklessLogsJobTest,ConsolidatedDisklessLogPrunerTest, andConsolidatedDisklessLogPrunerMetricsTestpass.BatchesPlanRegressionTestchecks the keep-offset lookup is one index entry.Merge Danger
Door: two-way
v1stays in the schema, so a broker rolled back to the old build still calls a function that resolves.V31only addsv2. The new interval and cap defaults apply to brokers on this build and can be set back in config.Blast Radius: produce
The prune lock sits on the same
logsrow a produce commit needs. A wrong keep-offset can delete WAL batches that are not fully tiered, or leave a switched topic's log start behind the seal.