Skip to content

Keep reconnect state out of the next_event future - #152

Merged
isdaniel merged 1 commit into
mainfrom
perf/slim-next-event-future
Nov 1, 2026
Merged

isdaniel merged 1 commit into
mainfrom
perf/slim-next-event-future

Conversation

@isdaniel

Copy link
Copy Markdown
Owner

Summary

After f96c6d2 (non-blocking startup), the future returned by EventStream::next_event() grew from 4.5 KB to 8.4 KB on the default rustls backend. recover_connection() was awaited inline, so its whole connect/TLS state machine became part of every read future, even though reconnecting is cold.

Two changes, both in src/stream.rs:

  1. Box the two recover_connection() awaits (check_connection_health, next_event_with_retry_until). Reconnecting is cold; the cost is one allocation per reconnect.
  2. Bind only the error out of the retry loop's match. The loop awaited inside the Err arm of match self.next_event_until(..).await, which keeps the match scrutinee (Result<Option<ChangeEvent>>, ~1 KB) alive across those awaits, so the future carried a ChangeEvent-sized slot. Boxing alone left the future at 1,768 B; with both changes it is 728 B on rustls (4,504 B on v0.9.0). Most of the diff is the resulting de-indent; git diff -w is +35/-6.

Also adds read_path_futures_stay_small, which asserts the next_event / next_event_with_retry / EventStream::next_event futures stay under 2 KB. stream.rs is excluded from the CodSpeed build, so nothing else would notice this growing again.

No behavior change: same retry, backoff and recovery logic.

Verification

  • CI's non-SSL integration loop run locally on PG17: 34 suite runs, 115 tests, 0 failures.
  • Terminated the walsender twice during a 2M-row replay on both backends: the stream reconnected and finished.
  • The new test fails on current main (next_event_with_retry future grew to 8392 bytes) and passes here.

Related issues

n/a. Found while reviewing v0.9.0..main.

Type of change

  • Bug fix
  • New feature
  • Performance improvement
  • Refactor / internal cleanup
  • Documentation
  • CI / build

Checklist

  • make before-git-push passes locally (check, build, format, audit, test, doc-check)
  • Tests added/updated for the change (coverage stays ≥ 90%)
  • Both backends build (--features rustls-tls if connection/streaming code changed)
  • Docs / README updated if public API changed

@codspeed

codspeed Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 50 untouched benchmarks


Comparing perf/slim-next-event-future (b372e9f) with main (bc4a2df)

Open in CodSpeed

@codecov

codecov Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.00990% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 96.37%. Comparing base (bc4a2df) to head (b372e9f).

Files with missing lines Patch % Lines
src/stream.rs 98.96% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #152      +/-   ##
==========================================
+ Coverage   96.27%   96.37%   +0.10%     
==========================================
  Files          36       36              
  Lines       28507    28657     +150     
==========================================
+ Hits        27444    27619     +175     
+ Misses       1063     1038      -25     
Flag Coverage Δ
integration 63.62% <66.07%> (+0.02%) ⬆️
unit 92.98% <97.52%> (+0.22%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@isdaniel
isdaniel force-pushed the perf/slim-next-event-future branch from 2da414e to b76ac5f Compare October 31, 2026 00:24
@isdaniel
isdaniel requested a balanced review from Copilot October 31, 2026 01:01

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟢 Approval recommended

No active review issues were identified for the WAL streaming future-size reduction.

No open issues 🎉

What changed in this PR

This PR reduces read-future sizes in the WAL streaming layer by keeping reconnect state and large temporary values out of the retry future, while preserving retry and recovery behavior.

Changes:

  • Boxes both cold reconnect awaits and limits the retry loop’s retained state to the error.
  • Adds a 1 KB future-size regression guard and tests for retry backoff and cancellation.
  • Enables Tokio’s test utilities for paused-time tests.
File Description
src/​stream.rs Reduces read-future sizes and adds size, backoff, and cancellation regression tests.
Cargo.toml Enables Tokio’s test-util feature for deterministic timer tests.

Review effort: Balanced

a memcpy of it each time: +7% CPU per event on the rustls backend, +4% on libpq.

Box the two recover_connection awaits, since reconnecting is cold. The retry
loop also held its whole match scrutinee, a Result<Option<ChangeEvent>>, across
the awaits in its error arm, a 1 KB slot in the future; bind only the error out
of the match.

Keep the next_event_before future small and guard it

EventStream::next_event_before matched `self.held.take()` and
`resume_parked_poll().await` as nested scrutinees, so both Option<ChangeEvent>
temporaries lived across the final read: its future was 2,800 B against 728 B
for next_event. It runs only while an aligned snapshot catches up, so the cost
is small, but it is the same pattern the previous commit removes from the retry
loop, and the guard could not cover it.

Take each early read in its own `if let` that returns, so its event ends before
the next await. The future is now 720 B. Behaviour is unchanged.

Extend read_path_futures_stay_small to both next_event_before futures.

Add the two cases no test reached: a parked poll that ended in an error hands it
to the next read, and a commit exactly at the boundary is held while one LSN
below is delivered (the boundary is a copy's consistent point, which contains
everything before it and nothing at or after it).

Test the retry loop's permanent-error and live-connection arms

Two arms of next_event_with_retry_until had no test: a permanent read error
ends the loop at once, and a decode error on a connection that is still up is
surfaced, not retried, because the bad frame is already consumed and a retry
would silently skip a change. A null test connection never reports alive and
never carries a server error, so neither was reachable.

Add two test-only seams on the native connection: one in COPY mode whose
server end the test holds, so an ErrorResponse written to it comes out of
get_copy_data_async through the real worker; and a setter for the liveness
flag the worker normally keeps. The permanent case runs on a real worker and
socket with a hard timeout; the live-connection case uses the paused clock,
which shows that no backoff started.

Both run on the rustls backend; the lines they cover are shared.
@isdaniel
isdaniel force-pushed the perf/slim-next-event-future branch from b76ac5f to b372e9f Compare October 31, 2026 03:00
@isdaniel
isdaniel merged commit 2b35e0e into main Nov 1, 2026
24 checks passed
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