Open bswck opened 2 weeks ago
Here's a short proof that I wrote for fun and to illustrate the problem with more depth and room for interaction. It's not meant to be very realistic, but just to show why blocking calls are bad.
Imagine there is a task group with 10000 async tasks that first load the newest config from the disk and then sleep for 0 seconds (and then check how much they actually slept). All those tasks are inserted to the task group one by one, letting the event loop run them concurrently:
If you run that, you'll see that some of the late tasks which use the executor make the event loop maximally stretch the time needed to wait for await sleep(0)
completion for around ~0.05 secs (a negligible delay), while without the executor await sleep(0)
in a late task can even lose about ~1.5 secs! (At least on my personal machine). And that's due to blocking calls still being performed in the preceding concurrent tasks!
Thanks @bswck for the issue and explanation.
Let's document it at the end of our docs before In-place reloading
This is how I load/reload my settings in an asynchronous app:
The reason why I'm doing it this way is because the
Settings
settings model hastoml_file
specified in its model config which causes the initializer to directly interact with the blocking disk I/O when resolving config values: https://github.com/pydantic/pydantic-settings/blob/6fe3bd1e35ff3854808f57e099484317f7063238/pydantic_settings/sources.py#L1993-L1996Calls to
open()
are blocking and halt the entire event loop for possibly longer than regular statements betweenawait
s in typical coroutines, which can lead to unsound effects.Let's create an API for loading configuration asynchronously or hint the users in the docs (possibly here and here) to use
asyncio.to_thread
/loop.run_in_executor
in order to use a worker thread that can make the entire I/O code non-blocking, wrapped in a future and correctly awaited in a coroutine.The big question is whether loading the config from sources is currently thread-safe (I'm guessing so).