Skip to content

Bound the idle wait so a busy page cannot hang select() - #87

Merged
nathanfallet merged 1 commit into
mainfrom
fix/bounded-idle-wait
Aug 31, 2026
Merged

Bound the idle wait so a busy page cannot hang select()#87
nathanfallet merged 1 commit into
mainfrom
fix/bounded-idle-wait

Conversation

@nathanfallet

Copy link
Copy Markdown
Member

DefaultTab.select() / find() / findAll() all open with wait(), which lands in the t == null branch of DefaultConnection.wait() — the one commented "Wait indefinitely". Its only exit is 100 ms without a single CDP event.

On a page that streams continuously — polling, server-sent events, ads, a refreshing interstitial — that quiet window never comes, so wait() never returns and select() blocks before it ever evaluates its own timeout. The timeout parameter is inert in that state, timeout = 0 included.

Changes

wait() takes an idleTimeout, capping how long it watches for the connection to settle. Waiting is best-effort — a page that never settles is not an error — so past the bound it stops watching and returns.

t stays a floor and keeps its meaning. Splitting the two makes the signature say what the body used to hide: one number served as both cap and floor, which is why the two branches existed. They are now a single path.

⚠️ Note for anyone tempted by the smaller change: giving t a non-null default does not work. The t != null branch guarantees a wait of at least t, and select() calls wait() once per poll — every selector would cost that much. It would trade a hang for a slowdown.

Idleness is now read, not guessed. The receive loop timestamps the last frame it saw, and isIdle() compares against it. The previous check re-subscribed to events on every poll and only observed the 100 ms window it had just opened, so a connection quiet for a while still looked busy, and a busy one could look quiet.

Tests

A clock seam mirrors the existing createTransport() seam, so the tests decide "traffic kept arriving" and "nothing arrived" off the scheduler's virtual clock rather than off how fast the machine runs.

  • a page that never goes quiet — wait() returns, and only at the bound
  • a silent connection — idle at once, no polling window spent rediscovering it
  • wait(t) on an already-idle connection — the floor is still honoured

All three fail on main. Mutation-checked: removing the bound, removing the floor, never writing the timestamp, treating "nothing received" as busy, and dropping the idle threshold each turn one red.

Full :core:jvmTest + :opentelemetry:jvmTest green (184 tests). :cdp:detekt and kotlinStoreYarnLock fail identically on main and are untouched; :core detekt violations are unchanged at 142.

DefaultTab.select()/find()/findAll() open with wait(), which lands in the
t == null branch of DefaultConnection.wait() — the one commented "Wait
indefinitely". Its only exit is 100 ms without a single CDP event, so on a
page that streams continuously (polling, SSE, ads, a refreshing interstitial)
it never returns, and select() blocks before it ever evaluates its own
timeout. The timeout parameter is inert in that state, including timeout = 0.

Two changes:

- wait() takes an idleTimeout, capping how long it watches for the connection
  to settle. t stays a floor and keeps its meaning, so callers asking for
  breathing room still get it. Splitting the two makes the signature say what
  the body used to hide: one number served as both cap and floor, which is why
  the two branches existed at all. They are now one path.

- Idleness is read from the timestamp of the last frame the receive loop saw,
  instead of re-subscribing to events on every poll. The old check only ever
  observed the 100 ms window it had just opened, so a connection that had been
  quiet for a while still looked busy, and a busy one could look quiet.

A clock seam mirrors the existing createTransport() seam so the tests decide
"traffic kept arriving" and "nothing arrived" off the scheduler's virtual
clock rather than off how fast the machine runs.
@codecov

codecov Bot commented Aug 31, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.66667% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...n/dev/kdriver/core/connection/DefaultConnection.kt 88.88% 0 Missing and 1 partial ⚠️
...tlin/dev/kdriver/opentelemetry/OpenTelemetryTab.kt 0.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@nathanfallet
nathanfallet merged commit 3e78da3 into main Aug 31, 2026
5 checks passed
@nathanfallet
nathanfallet deleted the fix/bounded-idle-wait branch August 31, 2026 14:57
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.

1 participant