Skip to content

⚡ Bolt: Offload blocking deezer network calls to thread pool#19

Open
davidjuarezdev wants to merge 1 commit intomainfrom
bolt/deezer-asyncio-to-thread-4659567636530970392
Open

⚡ Bolt: Offload blocking deezer network calls to thread pool#19
davidjuarezdev wants to merge 1 commit intomainfrom
bolt/deezer-asyncio-to-thread-4659567636530970392

Conversation

@davidjuarezdev
Copy link
Copy Markdown
Owner

@davidjuarezdev davidjuarezdev commented Mar 20, 2026

💡 What:
Wrapped the synchronous self.client.gw.get_track and self.client.get_track_url method calls inside streamrip/client/deezer.py with await asyncio.to_thread.

🎯 Why:
The application heavily uses asyncio for managing concurrency. deezer-python relies on synchronous HTTP requests. By calling these functions directly in the get_downloadable method, the main asyncio event loop was being blocked, causing all other asynchronous operations (like other concurrent downloads or client fetches) to pause until the network requests finished.

📊 Impact:
Greatly improves concurrent downloading speed for Deezer when resolving multiple tracks or albums at once. The main event loop is no longer stalled by waiting on Deezer's servers to respond for individual track URLs.

🔬 Measurement:
Run concurrent downloads from Deezer (e.g., download a playlist). The application should be able to continue downloading and processing other items while waiting for the Deezer API to respond to track metadata and URL requests.


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

Summary by Sourcery

Enhancements:

  • Run synchronous Deezer track metadata and URL lookups in a thread pool to improve concurrency and responsiveness when downloading multiple tracks or albums.

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 Mar 20, 2026

📝 Walkthrough

Summary by CodeRabbit

  • Refactor
    • Improved application responsiveness when downloading tracks from Deezer by optimizing how API requests are processed.

Walkthrough

The get_downloadable() method in streamrip/client/deezer.py was updated to execute two synchronous Deezer client calls (gw.get_track() and get_track_url()) on worker threads via asyncio.to_thread, preventing event loop blocking during these I/O operations.

Changes

Cohort / File(s) Summary
Async Threading Optimization
streamrip/client/deezer.py
Wrapped two blocking Deezer API calls with asyncio.to_thread() to offload execution to worker threads and prevent event loop blocking. Downstream logic remains unchanged.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Possibly related PRs

Poem

🐇 A rabbit hops through async threads so fine,
No blocking waits on Deezer's client line,
Two calls now dance on worker threads so spry,
The event loop stays free to soar and fly! ✨

🚥 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 clearly references the main change: offloading blocking Deezer network calls to a thread pool via asyncio, which matches the PR's core objective.
Description check ✅ Passed The description is directly related to the changeset, explaining both what was changed (wrapping synchronous calls with asyncio.to_thread) and why (preventing event loop blocking).

✏️ 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/deezer-asyncio-to-thread-4659567636530970392
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch bolt/deezer-asyncio-to-thread-4659567636530970392

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.

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.

No issues found across 1 file

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

@davidjuarezdev davidjuarezdev marked this pull request as ready for review March 21, 2026 07:32
Copilot AI review requested due to automatic review settings March 21, 2026 07:32
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Mar 21, 2026

Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

Wraps synchronous Deezer client network calls in asyncio.to_thread within get_downloadable to avoid blocking the event loop and slightly tidies surrounding whitespace.

Sequence diagram for offloading Deezer network calls with asyncio.to_thread

sequenceDiagram
    actor Caller
    participant DeezerClient
    participant AsyncioEventLoop
    participant ThreadPoolWorker
    participant GwClient
    participant DeezerHttpClient

    Caller->>DeezerClient: get_downloadable(item_id, quality)
    activate DeezerClient

    DeezerClient->>AsyncioEventLoop: await asyncio.to_thread(self.client.gw.get_track, item_id)
    activate AsyncioEventLoop
    AsyncioEventLoop-->>ThreadPoolWorker: schedule get_track(item_id)
    activate ThreadPoolWorker
    ThreadPoolWorker->>GwClient: get_track(item_id)
    activate GwClient
    GwClient->>DeezerHttpClient: HTTP request get_track
    DeezerHttpClient-->>GwClient: track_info
    GwClient-->>ThreadPoolWorker: track_info
    deactivate GwClient
    ThreadPoolWorker-->>AsyncioEventLoop: track_info
    deactivate ThreadPoolWorker
    AsyncioEventLoop-->>DeezerClient: track_info
    deactivate AsyncioEventLoop

    DeezerClient->>DeezerClient: compute quality_to_size, select quality

    DeezerClient->>AsyncioEventLoop: await asyncio.to_thread(self.client.get_track_url, token, format_str)
    activate AsyncioEventLoop
    AsyncioEventLoop-->>ThreadPoolWorker: schedule get_track_url(token, format_str)
    activate ThreadPoolWorker
    ThreadPoolWorker->>GwClient: get_track_url(token, format_str)
    activate GwClient
    GwClient->>DeezerHttpClient: HTTP request get_track_url
    DeezerHttpClient-->>GwClient: url
    GwClient-->>ThreadPoolWorker: url
    deactivate GwClient
    ThreadPoolWorker-->>AsyncioEventLoop: url
    deactivate ThreadPoolWorker
    AsyncioEventLoop-->>DeezerClient: url
    deactivate AsyncioEventLoop

    DeezerClient-->>Caller: dl_info including url
    deactivate DeezerClient
Loading

File-Level Changes

Change Details Files
Offload blocking Deezer track metadata and URL lookups to a thread pool within the async get_downloadable flow.
  • Wrap synchronous track metadata lookup in asyncio.to_thread so it runs in a thread pool instead of blocking the event loop.
  • Wrap synchronous track URL lookup in asyncio.to_thread to keep the async event loop responsive during network I/O.
  • Normalize minor whitespace around existing quality/availability handling logic.
streamrip/client/deezer.py

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

@sourcery-ai sourcery-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.

Hey - I've left some high level feedback:

  • Since these network calls may be executed in high concurrency, consider explicitly configuring or reusing a dedicated ThreadPoolExecutor instead of relying on the default executor to better control resource usage and avoid starving other asyncio.to_thread work.
  • If you find yourself offloading more Deezer client methods in the future, you may want to introduce a small helper (e.g., _run_in_thread(self, fn, *args)) to centralize the asyncio.to_thread pattern and keep error handling and logging consistent.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Since these network calls may be executed in high concurrency, consider explicitly configuring or reusing a dedicated ThreadPoolExecutor instead of relying on the default executor to better control resource usage and avoid starving other `asyncio.to_thread` work.
- If you find yourself offloading more Deezer client methods in the future, you may want to introduce a small helper (e.g., `_run_in_thread(self, fn, *args)`) to centralize the `asyncio.to_thread` pattern and keep error handling and logging consistent.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the Deezer client’s async download resolution path to avoid blocking the main asyncio event loop by offloading Deezer’s synchronous network calls to the default thread pool.

Changes:

  • Wrap self.client.gw.get_track(...) in await asyncio.to_thread(...) inside DeezerClient.get_downloadable.
  • Wrap self.client.get_track_url(...) in await asyncio.to_thread(...) inside DeezerClient.get_downloadable.
  • Minor whitespace cleanup in the modified section.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

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.

🧹 Nitpick comments (1)
streamrip/client/deezer.py (1)

133-133: Consider wrapping search_function call for consistency (optional follow-up).

The search_function(query, limit=limit) call on line 133 is also a synchronous API call that could benefit from asyncio.to_thread wrapping. Similarly, self.client.login_via_arl(arl) in the login method (line 51) is another candidate. These are outside the scope of this PR but could be addressed in a follow-up.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@streamrip/client/deezer.py` at line 133, Wrap synchronous calls to avoid
blocking the event loop: replace direct calls to search_function(query,
limit=limit) and self.client.login_via_arl(arl) with asyncio.to_thread calls so
they run in a thread pool (e.g., await asyncio.to_thread(search_function, query,
limit=limit) and await asyncio.to_thread(self.client.login_via_arl, arl));
update the async functions/methods that call these (the method containing
search_function and the login method using self.client.login_via_arl) to await
the to_thread result and import asyncio if not already present.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@streamrip/client/deezer.py`:
- Line 133: Wrap synchronous calls to avoid blocking the event loop: replace
direct calls to search_function(query, limit=limit) and
self.client.login_via_arl(arl) with asyncio.to_thread calls so they run in a
thread pool (e.g., await asyncio.to_thread(search_function, query, limit=limit)
and await asyncio.to_thread(self.client.login_via_arl, arl)); update the async
functions/methods that call these (the method containing search_function and the
login method using self.client.login_via_arl) to await the to_thread result and
import asyncio if not already present.

ℹ️ Review info
⚙️ Run configuration

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

Review profile: CHILL

Plan: Pro

Run ID: 8d89686c-a59f-4128-8467-93d8be63686e

📥 Commits

Reviewing files that changed from the base of the PR and between 0967a3e and ebefa51.

📒 Files selected for processing (1)
  • streamrip/client/deezer.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: Cleanup artifacts
🧰 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/deezer.py
🧠 Learnings (2)
📓 Common learnings
Learnt from: CR
Repo: davidjuarezdev/streamrip_RipDL PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-18T03:25:38.225Z
Learning: Applies to streamrip/**/*.py : Use async/await for asynchronous operations instead of blocking I/O
📚 Learning: 2026-03-18T03:25:38.225Z
Learnt from: CR
Repo: davidjuarezdev/streamrip_RipDL PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-18T03:25:38.225Z
Learning: Applies to streamrip/**/*.py : Use async/await for asynchronous operations instead of blocking I/O

Applied to files:

  • streamrip/client/deezer.py
🔇 Additional comments (2)
streamrip/client/deezer.py (2)

151-151: LGTM! Correctly offloads synchronous gateway call to thread pool.

This change aligns with the existing pattern used throughout this file (e.g., get_track, get_album, get_playlist methods) and properly prevents the blocking gw.get_track call from stalling the event loop. Based on learnings: "Use async/await for asynchronous operations instead of blocking I/O."


190-190: LGTM! Correctly offloads URL fetch to thread pool.

This properly wraps the synchronous get_track_url call, and the existing exception handling for deezer.WrongLicense and deezer.WrongGeolocation will continue to work correctly since exceptions propagate from the thread context.

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