Skip to content

Conversation

@thehesiod
Copy link
Collaborator

Description of Change

Added the ability to configure a specific executor in AioConfig for handling file load events. This enhancement enables greater flexibility and control for users managing asynchronous tasks.

Assumptions

No additional assumptions were made.

Checklist for All Submissions

  • I have added change info to CHANGES.rst
  • If this is resolving an issue (needed so future developers can determine if change is still necessary and under what conditions) (can be provided via link to issue with these details):
    • Detailed description of issue
    • Alternative methods considered (if any)
    • How issue is being resolved
    • How issue can be reproduced
  • If this is providing a new feature (can be provided via link to issue with these details):
    • Detailed description of new feature
    • Why needed
    • Alternatives methods considered (if any)

@gemini-code-assist
Copy link

Important

Installation incomplete: to start using Gemini Code Assist, please ask the organization owner(s) to visit the Gemini Code Assist Admin Console and sign the Terms of Services.

@thehesiod thehesiod linked an issue Dec 9, 2025 that may be closed by this pull request
6 tasks
@thehesiod

This comment was marked as outdated.

@bdraco
Copy link
Member

bdraco commented Dec 9, 2025

Sure looks like it will solve the issue. Nice work!

@chemelli74 and @zweckj will need to confirm since they have the test cases.

@codecov
Copy link

codecov bot commented Dec 9, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.64%. Comparing base (41e938e) to head (104ab9b).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1451      +/-   ##
==========================================
+ Coverage   91.60%   91.64%   +0.04%     
==========================================
  Files          76       77       +1     
  Lines        8078     8118      +40     
==========================================
+ Hits         7400     7440      +40     
  Misses        678      678              
Flag Coverage Δ
no-httpx 88.52% <100.00%> (+0.05%) ⬆️
os-ubuntu-24.04 91.64% <100.00%> (+0.04%) ⬆️
os-ubuntu-24.04-arm 89.66% <100.00%> (+0.05%) ⬆️
python-3.10 89.62% <100.00%> (+0.05%) ⬆️
python-3.11 89.62% <100.00%> (+0.05%) ⬆️
python-3.12 89.62% <100.00%> (+0.05%) ⬆️
python-3.13 89.62% <100.00%> (+0.05%) ⬆️
python-3.14 91.60% <100.00%> (+0.04%) ⬆️
python-3.9 89.63% <100.00%> (+0.05%) ⬆️
unittests 91.64% <100.00%> (+0.04%) ⬆️
with-awscrt 91.27% <100.00%> (+0.04%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@jakob-keller
Copy link
Collaborator

jakob-keller commented Dec 10, 2025

Thank you for taking the initiative!

I would like to clarify the proposed design, before providing an in-depth review.

In my understanding, this PR:

  1. Introduces load_executor in AioConfig
  2. Provides optionally_run_in_executor() to run a (blocking) function in an executor, if provided
  3. Uses 1. and 2. to avoid blocking calls in AioClientCreator.create_client()

Pros:

  • May be used by HA community to resolve their reported issues
  • Only affects client creation - no impact on making API calls

Cons:

  1. No comprehensive solution to blocking loaders: Does not remove all blocking codepaths from create_client() (e.g. _register_retries() is potentially blocking). Leaves majority of affected codepaths untouched (e.g. waiters, paginators, ...)
  2. Does not leverage instance caches already in place in botocore.loaders: executor invocations could be avoided for any subsequent cache hits
  3. Relies on substantial modification of create_client() - will not be simple to extend to further blocking codepaths without incurring undue drift from upstream and violating DRY principle
  4. Requires explicit configuration - likely will not benefit the majority of users

Alternative proposal:

UPDATE: #1452 is provided as proof-of-concept of the following, alternative design proposal.

I was thinking of a cache-aware wrapper for blocking loader functions, such as:

# to be placed in aiobotocore/loaders.py
async def call_cached(loader, func, *args, **kwargs):
    # key building logic taken from botocore.loaders
    key = (func.__name__,) + args
    for pair in sorted(kwargs.items()):
        key += pair

    if key in loader._cache:
        return loader._cache[key]

    return await asyncio.to_thread(func, loader, *args, **kwargs)

This could be used in light-weight patches wherever we decide to provide non-blocking behavior (everywhere!?), e.g.:

class AioClientCreator(ClientCreator):
    # ...
    async def _load_service_model(self, service_name, api_version=None):
        json_model = await call_cached(
            self._loader,
            self._loader.load_service_model,
            service_name,
            'service-2',
            api_version=api_version,
        )
        service_model = ServiceModel(json_model, service_name=service_name)
        return service_model

I believe this would provide more potential to deliver full non-blocking loaders. Also note that no client configuration would be needed, if we agree that a cache-aware implementation would eliminate the need for user opt-in and use the recommended, high-level asyncio.to_thread() API.

Nitpick 1: I welcome introducing type hints such as those enabled by _ConnectorArgsType and _HttpSessionTypes. Could we separate that work out into a separate PR since it appears to be unrelated to unblocking loaders? This could also be related to #1450.

Nitpick 2: botocore provides support for Python 3.9 until 2026-04-29. Are we sure we want to drop support before then? If so, it would simplify this PR if we do this in a separate PR. I'd be happy to prepare that, just let me know.

@thehesiod
Copy link
Collaborator Author

thehesiod commented Dec 10, 2025

ya you're right, we should support 3.9 until then, will remove that part, not as bad given we can use the type_extensions

@thehesiod
Copy link
Collaborator Author

Responses for Cons

  1. I think for now we only want to solve for things that potentially block due to loading large number of file operations as those are not async. I don't think we need to solve for places where small files may be loaded as that would be a much larger change. waiters and paginators don't load large number of files to my understanding (if any).
  2. this does not bypass caches already in place in botocore. It calls the existing caching code, just in another thread. The only thing is that the cache will be managed from a separate thread now.
  3. I'm not sure there are other, and if so very few, places where large number of files are loaded. If you think about it it's before the client is created that a large number of files need to be read. I don't understand how wrapping two function calls in a wrapper is a large change to a function we already override. It's rather minimal in fact
  4. to my understanding for most users this is not an issue as they do an upfront load of the client and have it stick around for awhile, so it seems to make sense to me that you'd need to opt-in for better behavior. I think long term we need to just scrap relying on botocore as a foundation so we can better manage issues like these without dramatically increasing our footprint. Another option I suppose is pre-warming all these caches ourselves in dedicated threadpool that we throw away. Actually thinking about this I think that's what we want to do, is manage these caches ourselves via a temporarily thread pool we throw away, then it's ok for everyone. I'll investigate this a bit in my PR if you want to investigate this in yours too.

@jakob-keller
Copy link
Collaborator

  1. I think for now we only want to solve for things that potentially block due to loading large number of file operations as those are not async. I don't think we need to solve for places where small files may be loaded as that would be a much larger change. waiters and paginators don't load large number of files to my understanding (if any).

I don't know what those operations are and was working under the assumption that we need to wrap all file IO in a thread, at least eventually. All the more reason to clarify the goal. @chemelli74 and @zweckj: Could the HA community weigh in on what exactly constitutes unacceptable blocking from their point of view?
Waiters and paginators do perform additional file IO on first use and AFTER client creation. Those are just examples and there might be more blocking uses of Loader than that.

  1. this does not bypass caches already in place in botocore. It calls the existing caching code, just in another thread. The only thing is that the cache will be managed from a separate thread now.

Correct, but being aware of the cache could be used to avoid paying the overhead of running code in a separate thread, as demonstrated by #1452. This is not a big issue in create_client(), but could be useful for further codepaths that need to be non-blocking. I am thinking of a massively concurrent application (number of asynchronous jobs making API calls >> number of threads available to executor). As long as this behavior is opt-in we might be good, but it would be better if a design allowed for out-of-the-box non-blocking behavior.

  1. to my understanding for most users this is not an issue as they do an upfront load of the client and have it stick around for awhile, so it seems to make sense to me that you'd need to opt-in for better behavior. I think long term we need to just scrap relying on botocore as a foundation so we can better manage issues like these without dramatically increasing our footprint. Another option I suppose is pre-warming all these caches ourselves in dedicated threadpool that we throw away. Actually thinking about this I think that's what we want to do, is manage these caches ourselves via a temporarily thread pool we throw away, then it's ok for everyone. I'll investigate this a bit in my PR if you want to investigate this in yours too.

aiobotocore / botocore already supports passing in a custom loader. As I had originally proposed, HA could "simply" initialize and pre-warm a loader (in a thread to prevent blocking) and then have their clients use that without having to worrying about blocking. If that is not feasible, we could provide a utility function to initialize and pre-warm a loader for such a purpose.

@thehesiod
Copy link
Collaborator Author

the loader case is interesting, since you have context could you post a little pseudo code of how that could work?

@jakob-keller
Copy link
Collaborator

the loader case is interesting, since you have context could you post a little pseudo code of how that could work?

import asyncio

import aiobotocore.session

from botocore.loaders import create_loader


def pre_warm_loader(
    loader,
    /,
    service_name,
    api_version=None,
):
    # from session.py
    loader.load_data_with_path('endpoints')
    loader.load_data('sdk-default-configuration')
    loader.load_service_model(
        service_name, 'waiters-2', api_version
    )
    loader.load_service_model(
        service_name, 'paginators-1', api_version
    )
    loader.load_service_model(
        service_name, type_name='service-2', api_version=api_version
    )
    loader.list_available_services(
        type_name='service-2'
    )

    # from client.py
    loader.load_data('partitions')
    loader.load_service_model(
        service_name, 'service-2', api_version=api_version
    )
    loader.load_service_model(
        service_name, 'endpoint-rule-set-1', api_version=api_version
    )
    loader.load_data('_retry')

    # from docs/service.py
    loader.load_service_model(
        service_name, 'examples-1', api_version
    )


async def main():
    loader = create_loader()
    await asyncio.to_thread(pre_warm_loader, loader, "s3")

    session = aiobotocore.session.get_session()
    session.register_component("data_loader", loader)

    async with session.create_client("s3") as client:  # should not block
        await client.list_buckets()

@jakob-keller
Copy link
Collaborator

It should be feasible to perform such pre-warming steps optionally in session.create_client():

async with session.create_client("s3", pre_load=True) as client:

The default would initially be pre_load=False to preserve current behavior.

AioConfig could be used to influence behavior if the pre_load argument is not passed to create_client() and could also be used to specify a custom executor to be used, if that's a requirement.

If that turns out to be useful and performant, pre_load=True could become the default behavior in aiobotocore 4.0.0.

@thehesiod
Copy link
Collaborator Author

@jakob-keller sorry for delays, really want to get back to this, will try to care out some time to think about this some more

@chemelli74
Copy link

Did a quick test, and get:

  File "/workspaces/core/homeassistant/components/aws/__init__.py", line 8, in <module>
    from aiobotocore.config import AioConfig
  File "/home/vscode/.local/ha-venv/lib/python3.13/site-packages/aiobotocore/config.py", line 12, in <module>
    from .endpoint import DEFAULT_HTTP_SESSION_CLS
  File "/home/vscode/.local/ha-venv/lib/python3.13/site-packages/aiobotocore/endpoint.py", line 17, in <module>
    from aiobotocore.httpchecksum import handle_checksum_body
  File "/home/vscode/.local/ha-venv/lib/python3.13/site-packages/aiobotocore/httpchecksum.py", line 3, in <module>
    from botocore.httpchecksum import (
    ...<9 lines>...
    )
ImportError: cannot import name '_register_checksum_algorithm_feature_id' from 'botocore.httpchecksum' (/home/vscode/.local/ha-venv/lib/python3.13/site-packages/botocore/httpchecksum.py)

even if def _register_checksum_algorithm_feature_id(algorithm): is there in botocore/httpchecksum.py.
Using v1.42.1 according to pip show

@thehesiod
Copy link
Collaborator Author

@chemelli74 this is most likely your botocore does not match the aiobotocore version requirement

@thehesiod
Copy link
Collaborator Author

ok did some research with AI and it seems to think along my lines that 1452 puts its fingers on too much internals when we have a solution that should work at a high level for the problem at hand and is opt-in so it can provide valuable feedback. Also the slowdown would be minimal given they'll be passing in their own executor which could be pre-spawned. Also I did some research in that there's no docs stating that thread workers are GC'd, and, it keeps around 5 threads already once you touch the default work pool (as I previously saw). It will re-use existing threads if they exist but that's just the definition of a pool. Also, I think we want to give the option for clients to restrict how many threads are spawned across multiple sessions and clients if they so please so I think this is would be a great enhancement. I've gone ahead and reverted the 3.9 changes so I think this PR should be good to go. thoughts?

@jakob-keller
Copy link
Collaborator

Nitpick 1: I welcome introducing type hints such as those enabled by _ConnectorArgsType and _HttpSessionTypes. Could we separate that work out into a separate PR since it appears to be unrelated to unblocking loaders? This could also be related to #1450.

I took the liberty and added type annotations to AioConfig as part of #1454

@chemelli74
Copy link

@chemelli74 this is most likely your botocore does not match the aiobotocore version requirement

As I wrote is v1.42.1 according to pip show and as far as I can tell zhould be fine.
Isn't it ?

@thehesiod
Copy link
Collaborator Author

@chemelli74 yes but you didn't say what version of aiobotocore, works fine for me:

$ python3
Python 3.11.11 (main, Jan 14 2025, 23:36:41) [Clang 19.1.6 ] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from aiobotocore.config import AioConfig
>>> import aiobotocore
>>> aiobotocore.__version__
'3.1.0'
>>> import botocore
>>> botocore.__version__
'1.42.19'
>>> 

@thehesiod
Copy link
Collaborator Author

@jakob-keller ok updated and merged in your PR

Copy link
Collaborator

Choose a reason for hiding this comment

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

These may be dropped from the PR.

ssl_context: NotRequired[ssl.SSLContext]
resolver: NotRequired[AbstractResolver]
socket_factory: NotRequired[Optional[SocketFactoryType]]
resolver: NotRequired[AbstractResolver]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Duplicate of line 36.

connector_args: _ConnectorArgs,
http_session_cls: type[_HttpSessionType],
) -> None:
if connector_args is None:
Copy link
Collaborator

Choose a reason for hiding this comment

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

Are changes from #1454 reverted on purpose? Why?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

validate before assignment

Copy link
Collaborator

Choose a reason for hiding this comment

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

if connector_args: would be slightly more efficient


self._validate_connector_args(connector_args, http_session_cls)

if load_executor and not isinstance(load_executor, ThreadPoolExecutor):
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe move to end of method?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I want to validate before assigning any variables


async def optionally_run_in_executor(
loop: AbstractEventLoop,
executor: Optional[ThreadPoolExecutor],
Copy link
Collaborator

@jakob-keller jakob-keller Jan 2, 2026

Choose a reason for hiding this comment

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

run_in_executor() accepts instances of concurrent.futures.Executor and uses the default executor if executor is None. It might reduce friction, if we follow the standard library's approach.

Then again, the function would be reduced to line 29 and could be dropped entirely.

self,
connector_args: Optional[_ConnectorArgs] = None,
http_session_cls: type[_HttpSessionType] = DEFAULT_HTTP_SESSION_CLS,
load_executor: Optional[ThreadPoolExecutor] = None,
Copy link
Collaborator

@jakob-keller jakob-keller Jan 2, 2026

Choose a reason for hiding this comment

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

load_executor: Union[bool, concurrent.futures.Executor] = False,

Would be more flexible:

  • False (default): keep current behavior
  • True: run in default executor, i.e. using run_in_executor(None, ...) or asyncio.to_thread(...)
  • instance of Executor: use run_in_executor(executor, ...)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

added support for default executor via sentinal

Copy link
Collaborator

Choose a reason for hiding this comment

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

How about remaining blocking file I/O? Is the scope of this PR limited to _load_service_model() and _load_service_endpoints_ruleset() on purpose?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

These are the main pain points to my understanding, we can always expand upon this later

@jakob-keller
Copy link
Collaborator

It should be feasible to perform such pre-warming steps optionally in session.create_client():

async with session.create_client("s3", pre_load=True) as client:

The default would initially be pre_load=False to preserve current behavior.

AioConfig could be used to influence behavior if the pre_load argument is not passed to create_client() and could also be used to specify a custom executor to be used, if that's a requirement.

If that turns out to be useful and performant, pre_load=True could become the default behavior in aiobotocore 4.0.0.

#1462 sketches this alternative design. I think it is cleaner and more comprehensive. What are your thoughts?

@thehesiod
Copy link
Collaborator Author

It should be feasible to perform such pre-warming steps optionally in session.create_client():
async with session.create_client("s3", pre_load=True) as client:
The default would initially be pre_load=False to preserve current behavior.
AioConfig could be used to influence behavior if the pre_load argument is not passed to create_client() and could also be used to specify a custom executor to be used, if that's a requirement.
If that turns out to be useful and performant, pre_load=True could become the default behavior in aiobotocore 4.0.0.

#1462 sketches this alternative design. I think it is cleaner and more comprehensive. What are your thoughts?

we should get feedback from community, @bdraco @chemelli74 . No reason both can't exist too I think. The warm-up one seems like something people can do on their own tho

@jakob-keller
Copy link
Collaborator

jakob-keller commented Jan 2, 2026

The warm-up one seems like something people can do on their own tho

I agree, but they seemed to have struggled with it, probably since it's not at all obvious how proper cache warming would need to be performed. My PR is aiming to provide a smooth experience for users who wish to do so.

@chemelli74
Copy link

@chemelli74 yes but you didn't say what version of aiobotocore, works fine for me:

Obviously this PR code so:
pip install "git+https://github.com/aio-libs/aiobotocore.git@refs/pull/1451/head#aiobotocore==3.1.0"

@thehesiod
Copy link
Collaborator Author

thehesiod commented Jan 5, 2026

@chemelli74 yes but you didn't say what version of aiobotocore, works fine for me:

Obviously this PR code so: pip install "git+https://github.com/aio-libs/aiobotocore.git@refs/pull/1451/head#aiobotocore==3.1.0"

still works for me with that version of botocore:

>>> from aiobotocore.config import AioConfig
>>> import aiobotocore
>>> import botocore
>>> aiobotocore.__version__
'3.1.0'
>>> botocore.__version__
'1.42.1'

you should do a runtime version check, may have a pathing issue. And not obvious, you could always apply this patch to older versions

@thehesiod
Copy link
Collaborator Author

lets wait until we hear back from @bdraco @chemelli74

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.

Detected blocking call inside the event loop

5 participants