Skip to content

fix: run sync document store methods in a worker thread - #13381

Merged
davidsbatista merged 5 commits into
deepset-ai:mainfrom
Starchaser1024:fix/async-retriever-docstore-guard
Oct 23, 2026
Merged

davidsbatista merged 5 commits into
deepset-ai:mainfrom
Starchaser1024:fix/async-retriever-docstore-guard

Conversation

@Starchaser1024

@Starchaser1024 Starchaser1024 commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #13380.

What changed

FilterRetriever, AutoMergingRetriever, SentenceWindowRetriever, CacheChecker and DocumentWriter all called the optional <method>_async APIs unconditionally. A document store that implements only the synchronous protocol therefore failed with AttributeError: 'X' object has no attribute 'filter_documents_async' — an error that names a missing attribute rather than the real situation, and that made these five components unusable from run_async on a store that works fine in a synchronous pipeline.

run_async now calls a small private helper added to haystack/utils/async_utils.py:

async def _call_with_async_fallback(instance: Any, method_name: str, /, **kwargs: Any) -> Any:
    async_method: Callable[..., Awaitable[Any]] | None = getattr(instance, f"{method_name}_async", None)
    if callable(async_method):
        return await async_method(**kwargs)
    return await asyncio.to_thread(getattr(instance, method_name), **kwargs)

It awaits the async method when the store provides one and otherwise runs the synchronous method in a worker thread — the same fallback Pipeline.run_async applies to components that do not implement run_async (haystack/utils/async_utils.py::_execute_component_async), and the shape haystack/tools/tool.py:309, haystack/components/evaluators/llm_evaluator.py:344 and haystack/document_stores/in_memory/document_store.py:968 already use. The three retrievers, CacheChecker (filter_documents) and DocumentWriter (write_documents) go through it, so the five components now behave the same way. The attribute guards and the :raises TypeError: entries in their docstrings are gone.

filter_retriever.py keeps the comment about the typing quirk: the DocumentStore protocol does not declare the async methods. The # type: ignore[attr-defined] itself is no longer needed, because the helper resolves the method with getattr instead of touching the attribute directly; the same note sits in the other two retrievers.

Behaviour

A store that implements both APIs is unaffected: the async method is awaited exactly as before, and InMemoryDocumentStore still runs its own async path. A sync-only store now returns results instead of raising, with the synchronous call executed off the event loop.

One test changed shape rather than intent: test_sentence_window_retriever_async.py::test_run_async_queries_document_store_once_for_all_retrieved_documents asserted the filter was passed positionally (call_args.args[0]). Calls now pass it by keyword, so the assertion reads call_args.kwargs["filters"]; what it checks — one store query for all retrieved documents, two conditions — is unchanged.

Tests

The three retriever tests and the CacheChecker/DocumentWriter ones now build a sync-only store with document_store_class(...) and assert the calls return results. With the previous guard in place all five fail with TypeError: Document store MockedDocumentStore does not provide async support.; with this change:

$ pytest test/components/retrievers test/components/caching test/components/writers -q
286 passed, 11 skipped in 3.43s

ruff check and ruff format --check (0.16.0, the rev pinned in .pre-commit-config.yaml) are clean on the touched files. The release note is reworded: the components now work with document stores that implement only the synchronous API, instead of documenting a TypeError.

…tore

FilterRetriever, AutoMergingRetriever and SentenceWindowRetriever call
filter_documents_async unconditionally. The async methods are optional in
the DocumentStore protocol, so a store that implements only the sync API
failed with AttributeError: 'X' object has no attribute
'filter_documents_async' instead of the TypeError DocumentWriter and
CacheChecker already raise for the same situation.

Add the same hasattr guard at the top of each run_async and one test per
retriever using document_store_class(), the helper the DocumentWriter and
CacheChecker async tests already use for this case.
@Starchaser1024
Starchaser1024 requested a review from a team as a code owner October 22, 2026 02:28
@Starchaser1024
Starchaser1024 requested review from davidsbatista and removed request for a team October 22, 2026 02:28
@vercel

vercel Bot commented Oct 22, 2026

Copy link
Copy Markdown
Contributor

@Starchaser1024 is attempting to deploy a commit to the deepset Team on Vercel.

A member of the Team first needs to authorize it.

@CLAassistant

CLAassistant commented Oct 22, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@HaystackBot

Copy link
Copy Markdown
Contributor

Hi @Starchaser1024, thanks a lot for your contribution! 🙏

We noticed that the Contributor License Agreement (CLA) check (license/cla) hasn't passed yet, so we've temporarily moved this PR to draft and paused the review assignment.

To get your PR reviewed, please sign the CLA via the link in the license/cla check below (or in the CLA bot comment). As soon as the check turns green, this PR will automatically be marked ready for review again and a reviewer will be re-assigned.

@HaystackBot
HaystackBot removed the request for review from davidsbatista October 22, 2026 03:34
@HaystackBot HaystackBot added the cla-pending PR is in draft until the contributor signs the CLA label Oct 22, 2026
@HaystackBot
HaystackBot marked this pull request as draft October 22, 2026 03:34
@HaystackBot
HaystackBot marked this pull request as ready for review October 22, 2026 04:12
@HaystackBot HaystackBot removed the cla-pending PR is in draft until the contributor signs the CLA label Oct 22, 2026
@HaystackBot

Copy link
Copy Markdown
Contributor

Thanks for signing the CLA, @Starchaser1024! 🎉 This PR is now ready for review again and the reviewer has been re-assigned.

@davidsbatista

Copy link
Copy Markdown
Contributor

Thanks for the PR, @Starchaser1024!

The guard is consistent with DocumentWriter and CacheChecker, but it only replaces one error with another. The component still can't run against a sync-only store in an async pipeline.

I'd rather fall back to the sync method in a worker thread when the async one is missing:

if hasattr(self.document_store, "filter_documents_async"):
    docs = await self.document_store.filter_documents_async(filters=filters)  # type: ignore[attr-defined]
else:
    docs = await asyncio.to_thread(self.document_store.filter_documents, filters=filters)

This is what we already do elsewhere for sync-only code under async:

  • Pipeline.run_async() runs components without run_async via asyncio.to_thread (haystack/utils/async_utils.py)
  • Tool.invoke_async falls back to asyncio.to_thread(self.invoke, ...)
  • LLMEvaluator and ToolResultOffloading wrap sync calls the same way
  • InMemoryDocumentStore's own *_async methods are sync calls run in an executor

Could you:

  1. Replace the TypeError guard with this fallback in the three retrievers, ideally through a small shared private helper.
  2. Apply the same change to CacheChecker.run_async (filter_documents) and DocumentWriter.run_async (write_documents), so all five components behave the same.
  3. Update the tests to check that a sync-only store (document_store_class(...)) returns results instead of raising.
  4. Reword the release note: these components now work with document stores that don't implement the async API.

Also, please keep the comment in filter_retriever.py that explains the # type: ignore[attr-defined].

FilterRetriever, AutoMergingRetriever, SentenceWindowRetriever, CacheChecker and
DocumentWriter called the optional <method>_async APIs unconditionally, so a
document store that implements only the synchronous protocol failed with
AttributeError: 'X' object has no attribute 'filter_documents_async'.

run_async now goes through a shared helper that awaits the async method when the
store provides one and otherwise runs the synchronous method in a worker thread,
the same fallback Pipeline.run_async applies to components without run_async.
The attribute guards and their TypeError docstrings are gone; the tests use
document_store_class() for a sync-only store and assert the calls return results.
@Starchaser1024 Starchaser1024 changed the title fix: raise TypeError when async retrievers get a sync-only document store fix: run sync document store methods in a worker thread Oct 22, 2026
@Starchaser1024

Copy link
Copy Markdown
Contributor Author

All four points are in, and the two components that already had the guard now share the same path.

The three retrievers no longer raise. They call a small private helper in haystack/utils/async_utils.py, _call_with_async_fallback(store, "filter_documents", ...): it awaits <method>_async when the store provides it, and otherwise runs the sync method through asyncio.to_thread — the same fallback Pipeline.run_async uses for components without run_async. CacheChecker (filter_documents) and DocumentWriter (write_documents) go through the same helper, so all five behave the same.

On the # type: ignore[attr-defined] comment: the helper resolves the async method with getattr, so there is no attribute access left to ignore. I kept the explanatory comment in filter_retriever.py — and the equivalent note in the other two retrievers — now saying what the Protocol does not declare and what the helper does about it. If you would rather keep the direct call and the ignore there, I can switch that one back.

The tests for all five components now build a sync-only store with document_store_class(...) and assert results. With the guard still in place they fail with the TypeError this PR used to raise; with the change they pass. pytest test/components/retrievers test/components/caching test/components/writers -q → 286 passed, 11 skipped; ruff check and ruff format --check (0.16.0) are clean on the touched files. The release note is reworded as you asked.

@github-actions github-actions Bot added the type:documentation Improvements on the docs label Oct 22, 2026
@davidsbatista

Copy link
Copy Markdown
Contributor

Thanks, looks good! A few nits:

  1. The release note says AttributeError, but DocumentWriter/CacheChecker raised TypeError. Please reword it and rename the file (there's no guard anymore).
  2. Without the type: ignore, the retriever comments can be one line or removed. In filter_retriever.py the comment is above the wrong line.
  3. Drop the logger.debug in the helper. It fires once per item in CacheChecker.

@HaystackBot
HaystackBot removed the request for review from davidsbatista October 22, 2026 10:30
@HaystackBot HaystackBot added the cla-pending PR is in draft until the contributor signs the CLA label Oct 22, 2026
@HaystackBot
HaystackBot marked this pull request as draft October 22, 2026 10:30
@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  haystack/components/caching
  cache_checker.py
  haystack/components/retrievers
  auto_merging_retriever.py
  filter_retriever.py
  sentence_window_retriever.py
  haystack/components/writers
  document_writer.py
  haystack/tools
  tool.py
  haystack/utils
  async_utils.py
Project Total  

This report was generated by python-coverage-comment-action

@davidsbatista
davidsbatista self-requested a review October 22, 2026 10:49
@davidsbatista davidsbatista removed the cla-pending PR is in draft until the contributor signs the CLA label Oct 22, 2026
@davidsbatista
davidsbatista marked this pull request as ready for review October 22, 2026 10:50
@HaystackBot
HaystackBot removed the request for review from davidsbatista October 22, 2026 10:53
@HaystackBot HaystackBot added the cla-pending PR is in draft until the contributor signs the CLA label Oct 22, 2026
@HaystackBot
HaystackBot marked this pull request as draft October 22, 2026 10:53
- Rename and reword the release note: the components no longer raise a
  specific error, they run the sync method in a worker thread.
- Shorten the retriever comments and move the FilterRetriever one above
  the call it describes.
- Drop the per-call debug log from _call_with_async_fallback; CacheChecker
  would emit it once per item.
@Starchaser1024
Starchaser1024 marked this pull request as ready for review October 22, 2026 11:10
@Starchaser1024

Copy link
Copy Markdown
Contributor Author

All three are in, de63990:

  • release note renamed to async-retriever-sync-store-thread-8f178e15ef2d7883.yaml and reworded: it now says the components fall back to a worker thread instead of naming an error type. You were right that AttributeError did not fit DocumentWriter and CacheChecker.
  • the retriever comments are one line each now, and the FilterRetriever one sits above the _call_with_async_fallback call instead of above resolved_filters.
  • dropped the logger.debug from _call_with_async_fallback; nothing else relied on it.

pytest test/components/retrievers test/components/caching test/components/writers -q gives 286 passed, 11 skipped, and test/utils/test_async_utils.py gives 5 passed. pre-commit run --files on the five touched files passes (ruff 0.16.0, release-note-backticks, codespell).

@HaystackBot HaystackBot removed the cla-pending PR is in draft until the contributor signs the CLA label Oct 22, 2026
@HaystackBot

Copy link
Copy Markdown
Contributor

Thanks for signing the CLA, @Starchaser1024! 🎉 This PR is now ready for review again and the reviewer has been re-assigned.

@vercel

vercel Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
haystack-docs Ignored Ignored Preview Oct 23, 2026 10:13am UTC

Request Review

@davidsbatista
davidsbatista merged commit 76fc2d3 into deepset-ai:main Oct 23, 2026
30 of 31 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

topic:tests type:documentation Improvements on the docs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: async retrievers raise AttributeError instead of TypeError for a document store without async support

4 participants