Skip to content

refactor(cli): replace _run_login's polling loop with threading.Event.wait - #298

Merged
dhruvbatra merged 2 commits into
mainfrom
claude/admiring-hawking-ddlvid
Aug 25, 2026
Merged

refactor(cli): replace _run_login's polling loop with threading.Event.wait#298
dhruvbatra merged 2 commits into
mainfrom
claude/admiring-hawking-ddlvid

Conversation

@dhruvbatra

@dhruvbatra dhruvbatra commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

What

_run_login's CLI login flow bridges a threading.Event (done, set by either the Playwright browser-close callback _mark_browser_closed, or the background stdin-reader thread _read_stdin) into async code with a hand-rolled busy-poll loop:

while not done.is_set():
    await asyncio.sleep(0.2)

This is exactly what asyncio.to_thread + threading.Event.wait() exists for — blocking a worker thread on the event until it's set, instead of polling is_set() on a fixed 200ms cadence. Replaced with:

await asyncio.to_thread(done.wait)

Why this is an improvement

  • Reacts to either signal immediately instead of after up to ~200ms of polling latency.
  • Removes a hand-rolled poll loop in favor of the standard-library primitive purpose-built for this exact "block async code on a threading primitive" bridge.
  • No new imports — asyncio and threading were already imported in cli.py.

Why this is safe

  • Pure behavior-preserving swap: same Event, same two setters (_mark_browser_closed, _read_stdin), same eventual outcome — the coroutine still only resumes once done is set, from whichever source fires first.
  • tests/test_cli.py::test_run_login_opens_headed_persistent_browser_and_saves_profile and test_run_login_exits_cleanly_when_browser_window_closes_first exercise this exact code path (both signal sources) and pass unchanged.
  • Dropped the now-dead monkeypatch.setattr(cli.asyncio, "sleep", noop_sleep) lines from those two tests (and the now-unused noop_sleep import) since asyncio.sleep is no longer called in _run_login.
  • Full suite: 322 passed / 21 pre-existing environment-only failures (missing Chromium binary in this sandbox — documented, unrelated baseline), identical before and after.
  • ruff check src/ tests/ clean. ruff format --check on the touched files reports the same pre-existing "would reformat" lines that exist on main today (unrelated to this diff, confirmed via git stash A/B) — per AGENTS.md, existing files are not reformatted unless asked.
  • 2 files touched (+5/-5).

🤖 Generated with Claude Code

https://claude.ai/code/session_01UNp1qd4G67kz4mMxt5Z6u3


Generated by Claude Code


Note

Low Risk
Localized change to the interactive login CLI wait path; same completion signals and cleanup behavior, with no auth or verification logic touched.

Overview
The interactive login command no longer polls every 200ms for completion. _run_login now waits on an asyncio.Event with await done.wait(), while the Playwright close handler and background stdin reader signal completion via loop.call_soon_threadsafe(done.set).

This should wake immediately when the user presses Enter or closes the browser, and avoids parking a worker thread on threading.Event.wait() if the coroutine is cancelled. Login tests no longer monkeypatch asyncio.sleep or import noop_sleep, since the poll loop is gone.

Reviewed by Cursor Bugbot for commit 5e2ad2e. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • Bug Fixes
    • Improved the login flow’s responsiveness by waiting directly for completion events instead of checking repeatedly at fixed intervals.
    • Login now reacts immediately when the browser closes or input is received.

….wait

_run_login bridged a threading.Event (set by either the Playwright
browser-close callback or the background stdin-reader thread) into
async code with a hand-rolled `while not done.is_set(): await
asyncio.sleep(0.2)` loop — a busy-poll with up to 200ms of added
latency before the login flow notices either signal.

Replaced it with `await asyncio.to_thread(done.wait)`, which blocks a
worker thread on the same Event.wait() until either signal fires,
removing the polling entirely and reacting immediately. Behavior is
unchanged: same Event, same two setters, same eventual outcome.

Dropped the now-unused `monkeypatch.setattr(cli.asyncio, "sleep",
noop_sleep)` lines (and the now-unused `noop_sleep` import) from the
two tests exercising this path in tests/test_cli.py, since
asyncio.sleep is no longer called here.
@coderabbitai

coderabbitai Bot commented Aug 25, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

Next included review available in 51 minutes.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3281e2fd-684f-4649-a324-00ff0d0915cb

📥 Commits

Reviewing files that changed from the base of the PR and between 2907d6e and 5e2ad2e.

📒 Files selected for processing (1)
  • src/frontend_visualqa/cli.py

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 944b4b56-3c6e-4604-b0d0-191b9de5ab1d

📥 Commits

Reviewing files that changed from the base of the PR and between f1db584 and 2907d6e.

📒 Files selected for processing (2)
  • src/frontend_visualqa/cli.py
  • tests/test_cli.py

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The login flow now waits for the completion event with asyncio.to_thread(done.wait) instead of polling with asyncio.sleep. Login tests remove the unused sleep helper and related patches.

Changes

Login wait flow

Layer / File(s) Summary
Event-driven login wait and test updates
src/frontend_visualqa/cli.py, tests/test_cli.py
_run_login uses done.wait in a worker thread. Login tests no longer import or patch noop_sleep.

Estimated code review effort: 2 (Simple) | ~5 minutes

Merge Risk: ⚪ Minimal · up to 2907d

This localized login-wait refactor preserves the existing completion signals and has no actionable merge-blocking risk beyond normal checks and review.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 2 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: replacing _run_login's polling loop with threading.Event.wait.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/admiring-hawking-ddlvid

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cursor cursor Bot 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.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 2907d6e. Configure here.

Comment thread src/frontend_visualqa/cli.py Outdated
…run_login

Cursor Bugbot flagged that asyncio.to_thread(done.wait) parks a
default-executor thread on a threading.Event that cancellation cannot
interrupt. If _run_login is ever cancelled before done is set, that
thread keeps blocking forever, and asyncio.run()'s shutdown sequence
joins the default executor before returning — hanging process exit.

Switched to asyncio.Event, set via loop.call_soon_threadsafe from
both signal sources (the background stdin-reader thread, and the
browser's own "close" callback). This keeps the "react immediately,
no polling" property from the previous commit without ever parking a
thread: awaiting an asyncio.Event is natively cancellable, so a
cancelled _run_login unwinds cleanly with no orphaned thread.
@dhruvbatra
dhruvbatra merged commit 63bb719 into main Aug 25, 2026
4 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.

2 participants