Skip to content

fix race condition - library can become not ready before the event.wait() returns#740

Open
eliska-n wants to merge 1 commit intomasterfrom
feature/wait-for-ready
Open

fix race condition - library can become not ready before the event.wait() returns#740
eliska-n wants to merge 1 commit intomasterfrom
feature/wait-for-ready

Conversation

@eliska-n
Copy link
Copy Markdown
Collaborator

@eliska-n eliska-n commented Mar 23, 2026

Mithun showed me there is a race condition in the waiting mechanism.
I believe it. It is just still quite difficult to me to understand it fully. Robot summarizes:

wait_for_library_ready() waited only on LibraryReadyEvent, which behaves like a doorbell: it signals that readiness happened at some point (set() on the not-ready → ready edge). open() / list() need the stronger guarantee: the library is ready right now — like checking through the peephole before you act.
Without a check after the await, another task could move the library back to not-ready between “the bell rang” and “this coroutine continues,” so callers could proceed on a stale observation.

Summary by CodeRabbit

  • Bug Fixes
    • Library readiness detection now short-circuits when already ready and performs additional validation after readiness events fire, ensuring consistent behavior in edge cases.

@eliska-n eliska-n self-assigned this Mar 23, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 23, 2026

📝 Walkthrough

Walkthrough

The LibraryService.wait_for_library_ready() method is enhanced to avoid unnecessary waiting when the library is already ready, and adds verification via _ensure_ready() after receiving the ready event to handle race conditions where readiness state may change unexpectedly.

Changes

Cohort / File(s) Summary
LibraryService Ready-State Handling
asab/library/service.py
Added early return if library is already ready; replaced assumption that LibraryReadyEvent guarantees readiness with explicit _ensure_ready() call; updated docstring to clarify LibraryNotReadyError behavior.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Possibly related PRs

Suggested reviewers

  • mejroslav
  • ateska

Poem

🐰 A rabbit hops through ready states with care,
No waiting when the library's already there!
But when events fire, we double-check with glee—
Ensuring readiness is what we guarantee! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: fixing a race condition where the library can become not ready between the event firing and the coroutine resuming.

✏️ 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 feature/wait-for-ready

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

@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)
asab/library/service.py (1)

189-190: Add exception chaining to preserve the original timeout error in tracebacks.

The current code raises a different exception type without chaining, which obscures the original asyncio.TimeoutError in debugging context. Use raise ... from err to maintain exception causality:

-		except asyncio.TimeoutError:
-			raise LibraryNotReadyError("Library is not ready yet.")
+		except asyncio.TimeoutError as err:
+			raise LibraryNotReadyError("Library is not ready yet.") from err
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@asab/library/service.py` around lines 189 - 190, Catch the original
asyncio.TimeoutError as a variable and re-raise LibraryNotReadyError using
exception chaining so the original timeout is preserved in tracebacks: modify
the except block handling asyncio.TimeoutError to use "except
asyncio.TimeoutError as err" and then "raise LibraryNotReadyError(... ) from
err" referencing the asyncio.TimeoutError and LibraryNotReadyError symbols.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@asab/library/service.py`:
- Around line 189-190: Catch the original asyncio.TimeoutError as a variable and
re-raise LibraryNotReadyError using exception chaining so the original timeout
is preserved in tracebacks: modify the except block handling
asyncio.TimeoutError to use "except asyncio.TimeoutError as err" and then "raise
LibraryNotReadyError(... ) from err" referencing the asyncio.TimeoutError and
LibraryNotReadyError symbols.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4eaf1b10-1840-4222-89d5-85e240acc448

📥 Commits

Reviewing files that changed from the base of the PR and between 130951b and 0625970.

📒 Files selected for processing (1)
  • asab/library/service.py

"""
if timeout is None:
timeout = self.LibraryReadyTimeout
if self.is_ready():
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Fast path is good. It avoids waiting when readiness already holds at entry itself.

await asyncio.wait_for(self.LibraryReadyEvent.wait(), timeout=timeout)
except asyncio.TimeoutError:
raise LibraryNotReadyError("Library is not ready yet.")
self._ensure_ready()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This still feels like a one-time wait. We wake on the first ready event and then decide immediately from that one wake-up.

If the goal is still “wait until ready or timeout”, I think we probably need to keep waiting when that wake turns out to be stale, instead of treating it as the final truth for readiness.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I would probably do something like this instead, so we keep waiting until self.is_ready() is actually true, while still respecting the original timeout.


async def wait_for_library_ready(self, timeout: int = None):
    if timeout is None:
        timeout = self.LibraryReadyTimeout

    loop = asyncio.get_running_loop()
    deadline = loop.time() + timeout

    while True:
        if self.is_ready():
            return

        remaining = deadline - loop.time()
        if remaining <= 0:
            raise LibraryNotReadyError("Library is not ready yet.")

        try:
            await asyncio.wait_for(self.LibraryReadyEvent.wait(), timeout=remaining)
        except asyncio.TimeoutError:
            raise LibraryNotReadyError("Library is not ready yet.")

await asyncio.wait_for(self.LibraryReadyEvent.wait(), timeout=timeout)
except asyncio.TimeoutError:
raise LibraryNotReadyError("Library is not ready yet.")
self._ensure_ready()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I would probably do something like this instead, so we keep waiting until self.is_ready() is actually true, while still respecting the original timeout.


async def wait_for_library_ready(self, timeout: int = None):
    if timeout is None:
        timeout = self.LibraryReadyTimeout

    loop = asyncio.get_running_loop()
    deadline = loop.time() + timeout

    while True:
        if self.is_ready():
            return

        remaining = deadline - loop.time()
        if remaining <= 0:
            raise LibraryNotReadyError("Library is not ready yet.")

        try:
            await asyncio.wait_for(self.LibraryReadyEvent.wait(), timeout=remaining)
        except asyncio.TimeoutError:
            raise LibraryNotReadyError("Library is not ready yet.")

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