Skip to content

fix(backend): stop the async scanner counting awaited calls as blocking - #12060

Merged
kodjima33 merged 2 commits into
BasedHardware:mainfrom
aryanorastar:fix/async-scanner-awaited-calls
Aug 23, 2026
Merged

fix(backend): stop the async scanner counting awaited calls as blocking#12060
kodjima33 merged 2 commits into
BasedHardware:mainfrom
aryanorastar:fix/async-scanner-awaited-calls

Conversation

@aryanorastar

@aryanorastar aryanorastar commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Summary

scan_async_blockers classified every database.* import called inside an async def as a synchronous DB call, without checking whether it was awaited.

await get_async_redis_client() is the correct way to reach the shared async client. The scanner reported it as a blocking helper:

async_helpers_with_blocking: backend/routers/listen/registry.py:49
  | proactive_message_dispatcher | get_async_redis_client:58

Why it needed fixing rather than working around

backend-async-blockers is a blocking pre-push gate, and this scanner has no allowlist, no inline waiver, and no way to record that a finding is wrong. So a correct change becomes unpushable, and the only routes past it are to restructure working code around the linter or to bypass the gate.

Found while routing the proactive dispatcher onto the publisher's shared Redis client — the dispatcher was subscribing to a different server than the publisher writes to, so no message ever arrived.

The change

Calls that are the direct operand of await are skipped. Keyed on AST node identity, not line number — one line can hold an awaited call and a synchronous one, and only the awaited half is safe.

The rule does not widen: an unawaited database.* call inside an async def is still reported, pinned by its own test.

Verification

python3 -m pytest backend/tests/unit/test_scan_async_blockers.py
  -> 27 passed  (25 pre-existing + 2 new)

Guard proven by removing it: with the awaited-call skip deleted, exactly one test fails — the awaited-accessor case. A test that has never failed is not evidence.

Product invariants affected

none

Failure class (fixes)

Failure-Class: none

Review in cubic

`_scan_function_body` classified every `database.*` import called inside an
`async def` as a synchronous DB call, without checking whether it was awaited.
`await get_async_redis_client()` is the correct way to reach the shared async
client, and the scanner reported it as a blocking helper.

That made a correct change unpushable: `backend-async-blockers` is a blocking
pre-push gate and this scanner has no allowlist, no inline waiver, and no way to
record that a finding is wrong. The only routes past it were to restructure
working code around the linter or to bypass the gate.

Awaited calls are now skipped, keyed on AST node identity rather than line
number — one line can hold an awaited call and a synchronous one, and only the
awaited half is safe.

The rule does not widen: an unawaited `database.*` call inside an `async def` is
still reported, with a test pinning that.

Verification:
  python3 -m pytest backend/tests/unit/test_scan_async_blockers.py -> 27 passed
  (25 pre-existing + 2 new)

  Guard proven by removing it: with the awaited-call skip deleted, exactly one
  test fails — the awaited-accessor case.

Failure-Class: none
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@aryanorastar

Copy link
Copy Markdown
Contributor Author

@Git-on-my-level small one, and it's blocking another PR.

scan_async_blockers counts awaited calls as blocking, so await get_async_redis_client() — the correct way to reach the shared async client — fails backend-async-blockers. No allowlist or inline waiver exists, so the only routes past it are restructuring working code around the linter or bypassing the gate.

Fix skips calls that are the direct operand of await, keyed on AST node identity. Doesn't widen: an unawaited database.* call in an async def is still reported, pinned by its own test.

27 tests pass (25 existing + 2 new). Proven by deleting the guard — exactly one test fails.

Checks green. #11807 can't push until this lands.

@kodjima33 kodjima33 left a comment

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.

Confidence 5/5: root cause explained, scoped diff (60 lines, 2 files), CI green, new regression tests (added + removed to prove they catch the bug). Author aryanorastar is a trusted backend contributor per prior history. No abuse signals (no workflow file changes).

@kodjima33
kodjima33 merged commit fe32dd3 into BasedHardware:main Aug 23, 2026
29 checks passed
formed2forge pushed a commit to formed2forge/omi that referenced this pull request Aug 24, 2026
…ng (BasedHardware#12060)

`_scan_function_body` classified every `database.*` import called inside an
`async def` as a synchronous DB call, without checking whether it was awaited.
`await get_async_redis_client()` is the correct way to reach the shared async
client, and the scanner reported it as a blocking helper.

That made a correct change unpushable: `backend-async-blockers` is a blocking
pre-push gate and this scanner has no allowlist, no inline waiver, and no way to
record that a finding is wrong. The only routes past it were to restructure
working code around the linter or to bypass the gate.

Awaited calls are now skipped, keyed on AST node identity rather than line
number — one line can hold an awaited call and a synchronous one, and only the
awaited half is safe.

The rule does not widen: an unawaited `database.*` call inside an `async def` is
still reported, with a test pinning that.

Verification:
  python3 -m pytest backend/tests/unit/test_scan_async_blockers.py -> 27 passed
  (25 pre-existing + 2 new)

  Guard proven by removing it: with the awaited-call skip deleted, exactly one
  test fails — the awaited-accessor case.

Failure-Class: none
kodjima33 pushed a commit that referenced this pull request Aug 24, 2026
…yncio (#12115)

#12060 taught `_scan_function_body` that `await get_async_redis_client()` is
not a blocking DB call. The skip is keyed on the Call node being the direct
operand of an `await`, so it only covers the single-coroutine form.

Awaiting two accessors at once, or one with a deadline, is spelled
`asyncio.gather(...)`, `asyncio.create_task(...)`, `asyncio.wait_for(...)`.
The inner call then stops being that operand and the same accessor #12060
cleared is reported again the moment a second one is awaited beside it.
Calling an `async def` only builds a coroutine object; whether the event loop
receives it directly or through asyncio, the calling frame does not block.

`backend-async-blockers` is a blocking pre-push gate with no allowlist and no
inline waiver, so every false positive costs a correct change.

Arguments written at the call site are covered — directly, unpacked with `*`,
or as elements of a literal list/tuple/set. A name built elsewhere and passed
in stays outside the analysis, like the rest of this scanner.

The rule does not widen: an unawaited `database.*` call is still reported,
including on a line that also carries a handoff.

Failure-Class: none
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