Skip to content

⚡ Bolt: Prevent Deezer client from blocking async event loop#23

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

⚡ Bolt: Prevent Deezer client from blocking async event loop#23
davidjuarezdev wants to merge 1 commit intomainfrom
bolt/deezer-asyncio-to-thread-6835358796840750250

Conversation

@davidjuarezdev
Copy link
Copy Markdown
Owner

@davidjuarezdev davidjuarezdev commented Mar 20, 2026

💡 What: Wrapped synchronous calls to the deezer-python API (like client.gw.get_track and client.get_track_url) in await asyncio.to_thread(...) within the streamrip/client/deezer.py client.

🎯 Why: The deezer-python library performs synchronous I/O. When these methods are called directly inside async functions, they completely block the main asyncio event loop. This prevents the application from processing other concurrent async tasks (like downloading other tracks in a playlist) while waiting for the Deezer API to respond.

📊 Impact: Significantly improves concurrency when downloading tracks or playlists from Deezer. The application will no longer freeze or stall other operations while waiting for API responses, resulting in faster overall execution times for bulk downloads.

🔬 Measurement: This can be verified by observing CPU and network utilization during a bulk download of a large Deezer playlist. There should be a noticeable reduction in stalled tasks and more consistent concurrent downloads compared to the previous implementation. All existing tests in tests/test_deezer.py continue to pass, proving correctness.


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

Summary by Sourcery

Offload blocking Deezer client operations to background threads to keep the asyncio event loop responsive.

Enhancements:

  • Wrap Deezer authentication, search, and track retrieval calls in asyncio.to_thread to avoid blocking async workflows and improve concurrency.

Documentation:

  • Document the event-loop blocking issue and the to_thread mitigation in the Bolt notes for future reference.

@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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

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

Review profile: CHILL

Plan: Pro

Run ID: b79c2330-0747-421e-9842-6ed5bc050f5b

📥 Commits

Reviewing files that changed from the base of the PR and between 4b61fd6 and 91a005e.

📒 Files selected for processing (2)
  • .jules/bolt.md
  • streamrip/client/deezer.py
📜 Recent 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: Agent
🧰 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 (3)
📓 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
Learnt from: CR
Repo: davidjuarezdev/streamrip_RipDL PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-18T03:25:38.225Z
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
📚 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:

  • .jules/bolt.md
📚 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: 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:

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

51-52: Good use of asyncio.to_thread() to prevent event loop blocking.

The pattern correctly offloads the synchronous login_via_arl call to a background thread, consistent with the existing pattern in other methods like get_track() and get_album().

One consideration: since self.client (the deezer.Deezer() instance) is now accessed concurrently from multiple thread pool workers, confirm that the deezer-python library is thread-safe. If multiple async tasks invoke different methods (e.g., one logging in while another searches), they'll run simultaneously in separate threads sharing the same client instance.

[approve_code_changes, request_verification]

Is deezer-python library thread-safe for concurrent API calls?

134-135: LGTM!

The synchronous search function is correctly offloaded to a background thread. The # type: ignore comment is reasonable here since search_function is dynamically retrieved via getattr.


153-154: LGTM!

Correctly offloads the synchronous gw.get_track call to a background thread.


167-167: No concerns with whitespace adjustment.


193-194: LGTM!

The synchronous get_track_url call is correctly offloaded to a background thread, and the existing exception handling for deezer.WrongLicense and deezer.WrongGeolocation is preserved.

.jules/bolt.md (1)

5-7: Clear and accurate documentation of the learning.

The documented learning and action accurately reflect the code changes and provide valuable context for future maintainers.


📝 Walkthrough

Summary by CodeRabbit

  • Refactor

    • Optimized Deezer client operations to execute asynchronously in background threads, improving application responsiveness during login, search, and track availability checks.
  • Documentation

    • Updated technical documentation regarding async handling patterns for API calls.

Walkthrough

Documentation was added describing how to offload synchronous Deezer API calls made within asynchronous functions to background threads. The DeezerClient implementation was updated to apply this pattern across three methods: login, search, and get_downloadable now use asyncio.to_thread() to prevent blocking the event loop.

Changes

Cohort / File(s) Summary
Documentation
.jules/bolt.md
Added learning entry documenting the issue of synchronous deezer-python API calls blocking the asyncio event loop and the solution of using await asyncio.to_thread() for background execution.
Deezer Client Async Offloading
streamrip/client/deezer.py
Three methods modified to offload synchronous Deezer API calls to background threads: login offloads self.client.login_via_arl(arl), search offloads the selected search function, and get_downloadable offloads self.client.gw.get_track() and self.client.get_track_url(). Exception handling and response logic preserved.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Poem

🐰 Hop, hop—threads multiply!
Sync calls no longer tie up the sky,
In background burrows they dance and play,
While async loops hop away!

🚥 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 summarizes the main change: preventing the Deezer client from blocking the async event loop by using asyncio.to_thread for synchronous calls.
Description check ✅ Passed The description is directly related to the changeset, explaining what was changed (synchronous Deezer calls wrapped in asyncio.to_thread), why it matters (prevents event loop blocking), and the expected impact on performance.

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

Important

Merge conflicts detected (Beta)

  • Resolve merge conflict in branch bolt/deezer-asyncio-to-thread-6835358796840750250
✨ 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-6835358796840750250
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch bolt/deezer-asyncio-to-thread-6835358796840750250

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.

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

sourcery-ai Bot commented Mar 21, 2026

Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

Wraps blocking deezer-python client calls in asyncio.to_thread within the async Deezer client to avoid blocking the event loop, plus documents this optimization in the Bolt notes.

Sequence diagram for async Deezer download using asyncio.to_thread

sequenceDiagram
    actor User
    participant Downloader
    participant DeezerClientAsync
    participant AsyncioThreadPool
    participant DeezerPythonClient
    participant DeezerAPI

    User->>Downloader: request_download(item_id, quality)
    Downloader->>DeezerClientAsync: get_downloadable(item_id, quality)

    DeezerClientAsync->>AsyncioThreadPool: asyncio.to_thread(gw_get_track, item_id)
    AsyncioThreadPool->>DeezerPythonClient: gw.get_track(item_id)
    DeezerPythonClient->>DeezerAPI: HTTP get_track
    DeezerAPI-->>DeezerPythonClient: track_info
    DeezerPythonClient-->>AsyncioThreadPool: track_info
    AsyncioThreadPool-->>DeezerClientAsync: track_info

    DeezerClientAsync->>AsyncioThreadPool: asyncio.to_thread(get_track_url, token, format_str)
    AsyncioThreadPool->>DeezerPythonClient: get_track_url(token, format_str)
    DeezerPythonClient->>DeezerAPI: HTTP get_track_url
    DeezerAPI-->>DeezerPythonClient: url
    DeezerPythonClient-->>AsyncioThreadPool: url
    AsyncioThreadPool-->>DeezerClientAsync: url

    DeezerClientAsync-->>Downloader: dl_info_with_url
    Downloader-->>User: start_download(url)
Loading

Updated class diagram for async Deezer client wrapping sync deezer-python

classDiagram
    class DeezerClientAsync {
        - config
        - client DeezerPythonClient
        - logged_in bool
        + async login()
        + async search(media_type, query, limit)
        + async get_downloadable(item_id, quality)
    }

    class DeezerPythonClient {
        + login_via_arl(arl)
        + get_track_url(token, format_str)
        + gw GwClient
    }

    class GwClient {
        + get_track(item_id)
    }

    DeezerClientAsync --> DeezerPythonClient : wraps_sync_calls
    DeezerPythonClient --> GwClient : uses
Loading

File-Level Changes

Change Details Files
Offload synchronous Deezer client authentication and search calls to background threads so async functions do not block the event loop.
  • Wrap login_via_arl call in await asyncio.to_thread(...) within the async login method
  • Wrap the dynamic search_function invocation in await asyncio.to_thread(...) within the async search method
streamrip/client/deezer.py
Offload synchronous track metadata and URL retrieval to background threads during download preparation.
  • Wrap client.gw.get_track(...) in await asyncio.to_thread(...) when building downloadable track info
  • Wrap client.get_track_url(...) in await asyncio.to_thread(...) when resolving the streaming URL
  • Minor whitespace cleanup in get_downloadable implementation
streamrip/client/deezer.py
Document the new learning about synchronous Deezer API calls blocking the event loop and the chosen mitigation pattern.
  • Add a new Bolt note describing the blocking behavior of deezer-python calls inside async functions
  • Record the standard pattern of using await asyncio.to_thread(sync_function, *args) for such calls
.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

@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 found 1 issue, and left some high level feedback:

  • Since the same pattern of await asyncio.to_thread(...) is now used for multiple Deezer client calls, consider extracting a small helper (e.g. _run_in_thread(self, func, *args, **kwargs)) to centralize this behavior and keep future Deezer API usages consistent and easier to update.
  • If Deezer calls can be very high-volume (e.g. large playlist downloads), it may be worth explicitly managing a dedicated ThreadPoolExecutor for these to_thread calls to avoid contention with other default-loop threadpool tasks and to allow tuning max_workers.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Since the same pattern of `await asyncio.to_thread(...)` is now used for multiple Deezer client calls, consider extracting a small helper (e.g. `_run_in_thread(self, func, *args, **kwargs)`) to centralize this behavior and keep future Deezer API usages consistent and easier to update.
- If Deezer calls can be very high-volume (e.g. large playlist downloads), it may be worth explicitly managing a dedicated ThreadPoolExecutor for these `to_thread` calls to avoid contention with other default-loop threadpool tasks and to allow tuning max_workers.

## Individual Comments

### Comment 1
<location path="streamrip/client/deezer.py" line_range="51-52" />
<code_context>
         if not arl:
             raise MissingCredentialsError
-        success = self.client.login_via_arl(arl)
+        # ⚡ Bolt: Offload synchronous deezer API call to background thread to prevent blocking async event loop
+        success = await asyncio.to_thread(self.client.login_via_arl, arl)
         if not success:
             raise AuthenticationError
</code_context>
<issue_to_address>
**issue (bug_risk):** Consider potential thread-safety issues when calling the Deezer client from a background thread.

Moving `login_via_arl` to a background thread means `self.client` may now be used from multiple threads. If the client (or its dependencies) isn’t thread-safe, this can introduce race conditions. Consider protecting login-related calls with a lock, or ensuring all client interactions happen on the same thread/executor to avoid cross-thread state access.
</issue_to_address>

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.

Comment on lines +51 to +52
# ⚡ Bolt: Offload synchronous deezer API call to background thread to prevent blocking async event loop
success = await asyncio.to_thread(self.client.login_via_arl, arl)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): Consider potential thread-safety issues when calling the Deezer client from a background thread.

Moving login_via_arl to a background thread means self.client may now be used from multiple threads. If the client (or its dependencies) isn’t thread-safe, this can introduce race conditions. Consider protecting login-related calls with a lock, or ensuring all client interactions happen on the same thread/executor to avoid cross-thread state access.

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 improves Deezer download concurrency by ensuring synchronous deezer-python API calls do not block the asyncio event loop, aligning login, search, and get_downloadable with the existing non-blocking approach already used elsewhere in DeezerClient.

Changes:

  • Offloaded login_via_arl to a background thread via asyncio.to_thread.
  • Offloaded Deezer search and gw.get_track / get_track_url calls via asyncio.to_thread to avoid event-loop blocking.
  • Documented the “Bolt” learning/action note about blocking synchronous API calls in .jules/bolt.md.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
streamrip/client/deezer.py Wraps remaining synchronous Deezer API calls with asyncio.to_thread inside async methods to prevent event-loop blocking.
.jules/bolt.md Adds an internal note capturing the event-loop blocking lesson and recommended mitigation.

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

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