Skip to content

⚡ Bolt: Short-circuit Qobuz secret testing using as_completed#67

Draft
davidjuarezdev wants to merge 1 commit intomainfrom
bolt-qobuz-as-completed-11812820950230207374
Draft

⚡ Bolt: Short-circuit Qobuz secret testing using as_completed#67
davidjuarezdev wants to merge 1 commit intomainfrom
bolt-qobuz-as-completed-11812820950230207374

Conversation

@davidjuarezdev
Copy link
Copy Markdown
Owner

@davidjuarezdev davidjuarezdev commented Apr 4, 2026

💡 What: Refactored _get_valid_secret in streamrip/client/qobuz.py to use asyncio.as_completed() and a try...finally block for task cancellation, rather than asyncio.gather().

🎯 Why: Previously, asyncio.gather sent concurrent requests for all available secrets but forced the event loop to wait until every single request completed before returning a result. By using asyncio.as_completed(), we process the tasks as they finish. Once the first valid secret responds, we immediately return it and explicitly cancel the rest of the pending requests.

📊 Impact: Significantly reduces latency when fetching a valid Qobuz API secret, especially if there are slow or timing-out secrets in the list. Reduces unneeded network I/O and prevents background task leakage.

🔬 Measurement: Run the test suite (PYTHONPATH=. poetry run pytest tests) to ensure Qobuz authentication and secret retrieval still pass successfully.


PR created automatically by Jules for task 11812820950230207374 started by @davidjuarezdev

Summary by Sourcery

Short-circuit Qobuz API secret validation to return the first working secret and cancel remaining checks to reduce latency and unnecessary I/O.

Enhancements:

  • Refine Qobuz client secret validation to use task-wise completion and cancellation instead of waiting for all concurrent checks to finish.

Documentation:

  • Document the Qobuz secret validation optimization and guidance on using asyncio.as_completed with cancellation in the Bolt engineering notes.

Summary by cubic

Short-circuits Qobuz secret validation by switching _get_valid_secret to asyncio.as_completed() and canceling pending checks to return the first working secret immediately. This reduces tail latency and avoids unnecessary network I/O when some secrets are slow or time out.

  • Refactors
    • Replaces asyncio.gather() with an asyncio.as_completed() loop that returns on the first non-None result; raises InvalidAppSecretError if none succeed.
    • Adds a try...finally to cancel remaining tasks and prevent background task leakage.

Written for commit fd23f95. Summary will update on new commits.

Co-authored-by: davidjuarezdev <230496599+davidjuarezdev@users.noreply.github.com>
@google-labs-jules
Copy link
Copy Markdown

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 4, 2026

📝 Walkthrough

Walkthrough

Modified the _get_valid_secret method in the Qobuz client to use asyncio.as_completed() instead of asyncio.gather(), enabling early return upon finding the first valid secret and canceling remaining in-flight tasks via a try...finally block. Added documentation of this improvement.

Changes

Cohort / File(s) Summary
Documentation
.jules/bolt.md
New entry documenting the async control-flow optimization: replacing gather with as_completed to stop early when the first valid secret is found.
Async Control Flow Optimization
streamrip/client/qobuz.py
Refactored _get_valid_secret to create concurrent tasks for all secret candidates, iterate over completions as they arrive, return the first non-None result, and cancel pending tasks in a finally block to prevent unnecessary waiting.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 Hoppity-skip through async streams so fleet,
No more waiting for the slowest beat!
As tasks complete, we snatch the prize,
Canceling stragglers—oh so wise! 🚀

🚥 Pre-merge checks | ✅ 2 | ❌ 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%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately captures the main change: refactoring Qobuz secret testing to use asyncio.as_completed for short-circuiting, which is the core improvement described in the changeset.
Description check ✅ Passed The pull request description clearly explains the refactoring of _get_valid_secret, the motivation (reducing latency and unnecessary I/O), and the impact on performance.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bolt-qobuz-as-completed-11812820950230207374
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch bolt-qobuz-as-completed-11812820950230207374

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 and usage tips.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Apr 4, 2026

Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

Refactors Qobuz secret validation to short-circuit on the first successful result using asyncio.as_completed with explicit task cancellation, and documents this concurrency pattern in the Bolt learning log.

Updated class diagram for QobuzClient secret validation methods

classDiagram
    class QobuzClient {
        +_test_secret(secret: str) Optional_str
        +_get_valid_secret(secrets: list_str) str
    }
Loading

File-Level Changes

Change Details Files
Short-circuit Qobuz secret validation to return the first working secret and cancel remaining tests.
  • Replace asyncio.gather-based fan-out/fan-in secret testing with a task list consumed via asyncio.as_completed
  • Use asyncio.create_task for each _test_secret invocation to allow explicit lifecycle control
  • Wrap the as_completed loop in a try...finally block and cancel any still-pending tasks to avoid background task leakage
  • Raise InvalidAppSecretError only after all tasks complete without yielding a valid secret
streamrip/client/qobuz.py
Document the new async concurrency pattern and lesson in the Bolt engineering log.
  • Add a Bolt entry describing the performance issues with asyncio.gather when only the first successful result is needed
  • Describe the as_completed + cancellation pattern as the preferred approach for this use case
.jules/bolt.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@streamrip/client/qobuz.py`:
- Around line 417-425: The as_completed loop currently lets any task exception
escape and abort the search: wrap the await coro inside a try/except so per-task
exceptions are caught/logged and the loop continues to the next completed task
(refer to the asyncio.as_completed(tasks) loop and the local variable
coro/result handling). In the finally block, after cancelling unfinished tasks
(tasks.cancel() calls), await/drain all tasks to completion using
asyncio.gather(*tasks, return_exceptions=True) and suppress or handle
CancelledError/other exceptions so there are no pending-task warnings (refer to
the tasks list and the finally cleanup). Ensure the successful result is still
returned immediately when found.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 7055f137-a53b-4203-b41f-46584de17caa

📥 Commits

Reviewing files that changed from the base of the PR and between 6fb4162 and fd23f95.

📒 Files selected for processing (2)
  • .jules/bolt.md
  • streamrip/client/qobuz.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: cubic · AI code reviewer
🧰 Additional context used
📓 Path-based instructions (1)
streamrip/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

streamrip/**/*.py: Use Black-compatible code formatting with double quotes and spaces via ruff format
Lint Python code with ruff using rules: E4, E7, E9, F, I, ASYNC, N, RUF, ERA001
Use async/await for asynchronous operations instead of blocking I/O
Implement Windows compatibility by using WindowsSelectorEventLoopPolicy on Windows and the pick library instead of simple-term-menu

Files:

  • streamrip/client/qobuz.py
🧠 Learnings (2)
📚 Learning: 2026-03-18T03:25:38.238Z
Learnt from: CR
Repo: davidjuarezdev/streamrip_RipDL PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-18T03:25:38.238Z
Learning: Applies to streamrip/**/*.py : Use async/await for asynchronous operations instead of blocking I/O

Applied to files:

  • .jules/bolt.md
📚 Learning: 2026-03-18T03:25:38.238Z
Learnt from: CR
Repo: davidjuarezdev/streamrip_RipDL PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-18T03:25:38.238Z
Learning: Use abstract base classes for Client implementations, with each streaming service (Qobuz, Tidal, Deezer, SoundCloud) having its own concrete implementation handling login/auth, metadata fetching, search, and Downloadable object production

Applied to files:

  • .jules/bolt.md
🔇 Additional comments (1)
.jules/bolt.md (1)

9-11: Nice addition—this documents the async fast-path clearly.

The note accurately captures the as_completed early-return behavior and the try...finally cancellation cleanup rationale.

Comment thread streamrip/client/qobuz.py
Comment on lines +417 to +425
for coro in asyncio.as_completed(tasks):
result = await coro
if result is not None:
return result
finally:
for task in tasks:
if not task.done():
task.cancel()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read-only verification: inspect current cancellation/error-handling behavior
rg -n -A20 -B5 'async def _get_valid_secret|as_completed\(|task\.cancel\(|gather\(' streamrip/client/qobuz.py

Repository: davidjuarezdev/streamrip_RipDL

Length of output: 3609


Handle per-task failures and drain cancelled tasks before exit.

At line 418, any exception from one secret-check task aborts the whole loop, even if another candidate would succeed.
At line 424, tasks are cancelled but never awaited, leaving cleanup non-deterministic and risking pending-task warnings.

Proposed fix
     tasks = [asyncio.create_task(self._test_secret(secret)) for secret in secrets]
     try:
         for coro in asyncio.as_completed(tasks):
-            result = await coro
+            try:
+                result = await coro
+            except Exception as exc:
+                logger.warning("Secret check task failed: %s", exc)
+                continue
             if result is not None:
                 return result
     finally:
         for task in tasks:
             if not task.done():
                 task.cancel()
+        await asyncio.gather(*tasks, return_exceptions=True)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@streamrip/client/qobuz.py` around lines 417 - 425, The as_completed loop
currently lets any task exception escape and abort the search: wrap the await
coro inside a try/except so per-task exceptions are caught/logged and the loop
continues to the next completed task (refer to the asyncio.as_completed(tasks)
loop and the local variable coro/result handling). In the finally block, after
cancelling unfinished tasks (tasks.cancel() calls), await/drain all tasks to
completion using asyncio.gather(*tasks, return_exceptions=True) and suppress or
handle CancelledError/other exceptions so there are no pending-task warnings
(refer to the tasks list and the finally cleanup). Ensure the successful result
is still returned immediately when found.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

2 issues found across 2 files

Confidence score: 3/5

  • There is a concrete reliability risk in streamrip/client/qobuz.py: an unexpected exception from one _test_secret task can escape the await loop and prevent remaining candidates from being tested, which may cause valid secrets to be skipped.
  • Task lifecycle handling in streamrip/client/qobuz.py is incomplete on early return; cancelled pending/done tasks are not awaited, so exceptions can go uncollected and surface later as noisy async warnings or hidden failures.
  • Given two medium-severity, high-confidence async-control-flow issues in the same path, this carries some merge risk and is worth addressing before relying on it in production.
  • Pay close attention to streamrip/client/qobuz.py - exception handling and task cleanup around secret-check coroutines need to be made robust.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="streamrip/client/qobuz.py">

<violation number="1" location="streamrip/client/qobuz.py:418">
P2: Wrap `await coro` in a `try/except` to handle individual task failures gracefully. If one `_test_secret` call raises an unexpected exception, it will propagate out of the loop and skip remaining candidates that might return a valid secret. Catch and log the exception, then `continue` to the next completed task.</violation>

<violation number="2" location="streamrip/client/qobuz.py:420">
P2: Pending/done secret-check tasks are cancelled but never awaited after early return, so task exceptions can go uncollected.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread streamrip/client/qobuz.py
tasks = [asyncio.create_task(self._test_secret(secret)) for secret in secrets]
try:
for coro in asyncio.as_completed(tasks):
result = await coro
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 4, 2026

Choose a reason for hiding this comment

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

P2: Wrap await coro in a try/except to handle individual task failures gracefully. If one _test_secret call raises an unexpected exception, it will propagate out of the loop and skip remaining candidates that might return a valid secret. Catch and log the exception, then continue to the next completed task.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At streamrip/client/qobuz.py, line 418:

<comment>Wrap `await coro` in a `try/except` to handle individual task failures gracefully. If one `_test_secret` call raises an unexpected exception, it will propagate out of the loop and skip remaining candidates that might return a valid secret. Catch and log the exception, then `continue` to the next completed task.</comment>

<file context>
@@ -409,14 +409,21 @@ async def _test_secret(self, secret: str) -> Optional[str]:
+        tasks = [asyncio.create_task(self._test_secret(secret)) for secret in secrets]
+        try:
+            for coro in asyncio.as_completed(tasks):
+                result = await coro
+                if result is not None:
+                    return result
</file context>
Suggested change
result = await coro
try:
result = await coro
except Exception as exc:
logger.warning("Secret check task failed: %s", exc)
continue
Fix with Cubic

Comment thread streamrip/client/qobuz.py
for coro in asyncio.as_completed(tasks):
result = await coro
if result is not None:
return result
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 4, 2026

Choose a reason for hiding this comment

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

P2: Pending/done secret-check tasks are cancelled but never awaited after early return, so task exceptions can go uncollected.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At streamrip/client/qobuz.py, line 420:

<comment>Pending/done secret-check tasks are cancelled but never awaited after early return, so task exceptions can go uncollected.</comment>

<file context>
@@ -409,14 +409,21 @@ async def _test_secret(self, secret: str) -> Optional[str]:
+            for coro in asyncio.as_completed(tasks):
+                result = await coro
+                if result is not None:
+                    return result
+        finally:
+            for task in tasks:
</file context>
Fix with Cubic

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