Repository navigation
Own the AioS3FileSystem pool and keep aio DataFrame reads off the event loop #1209
Copy link
Copy link
Closed
Labels
Description
Part of #1205 (step 5; see R5 and model 5 there).
Problem
AioS3FileSystem._thread_pool(pyathena/filesystem/s3_async.py:145) is never shut down. A cached instance keeps its threads for the life of the process.AioPandasCursor.as_pandas()returns the synchronousPandasDataFrameIterator(pyathena/aio/pandas/cursor.py:239-248). Its chunk reads, and with automatic chunks the whole read, run on the event loop thread.AioPolarsCursor.as_polars()/as_arrow()follow the same pattern (pyathena/aio/polars/cursor.py:214-234, source inspection only).
Reproduction
The stub reproductions are summarized in #1205. They use no AWS calls.
Environment
- PyAthena master at b2d7aa2
- Python 3.15
Proposed fix (optional)
- Give
AioS3FileSysteman owned pool lifecycle, through aclose()or a release at garbage collection, coordinated with the executor cleanup in Free the data of failed S3 requests and clean up async copies at loop shutdown #1192. - Make the aio cursors'
as_pandas(),as_polars(), andas_arrow()read through threads, or document that the returned iterator blocks.
Decisions (2026-10-18)
Agreed with the maintainer after design consultations with Codex and claude-fable-5-1. Documentation alone was ruled out: docs/aio.md already describes the blocking.
AioS3FileSystem.close()releases the threads of its pool, without ending the filesystem.S3AioThreadPooldrops itsThreadPoolExecutor(shutdown(wait=False)), and the next call creates a new one, as after a fork. Calls that were submitted before still run.- There is no closed state: the filesystem stays usable, with no error after
close(), and it stays in fsspec's instance cache, so other holders of a cached instance are not broken. - The cleanup of a multipart copy that is running at
close()(the case in the comment above) still schedules its completion and abort. withandasync withcallclose()on exit.- Rejected: a terminal
close(), which would have raisedRuntimeErrorafterwards and evicted the instance from the cache. It would break other holders of a shared cached instance and needs closed checks at every I/O entry point.weakref.finalizewas rejected too: collection already releases the threads of an uncached instance, and a cached one is never collected.
- Async reads on the aio cursors; the synchronous
as_*methods stay unchanged.AioPandasCursor.iter_chunks()andAioPolarsCursor.iter_chunks()return async generators (async for df in cursor.iter_chunks()). They advance one result-set chunk iterator in threads, bound to the result set at the start. A cancellation follows the fetch methods.await AioPandasCursor.read_pandas()andawait AioPolarsCursor.read_polars()/read_arrow()return the whole remaining result. They run the existing collectors, including the categorical restoration of pandas, in a thread.AioArrowCursoris unchanged: itsas_arrow()never reads S3.- Rejected: making
as_*coroutines, which is breaking and still leaves a synchronous chunk iterator. Also rejected: documentingpd.concat/pl.concatoveriter_chunks()as the whole-read path. It fails on an empty result, concatenates on the event loop, and skips the categorical restoration.
Revisions during the review of #1286 (2026-10-18)
- Pool release.
S3AioThreadPool.release()keeps the executor until no submitted call runs or waits, and then shuts it down. Dropping a busy executor at once would stack executors beyondmax_workerswhen a shared filesystem is closed repeatedly. - Whole reads.
read_pandas()/read_polars()/read_arrow()are dropped. Theas_*()methods of the aio cursors become coroutines instead, which follows the aio naming of PyAthena: the same name as the synchronous method, awaited. This coversAioPandasCursor.as_pandas(),AioPolarsCursor.as_polars()/as_arrow(), andAioArrowCursor.as_arrow()/as_polars(). No base class defines them, and nothing in PyAthena calls them.AioPandasCursor.as_pandas()always returns a DataFrame. With chunks, it joins the chunks that have not yet been read.- This is a breaking change for 4.2.0.
- Chunk iterators after close. The synchronous chunk iterators of
AthenaPandasResultSetandAthenaPolarsResultSetused to end silently once their result set was closed. They now raiseProgrammingError, matching the aio iterator, and this replaces the behavior documented indocs/usage.md. The iterators share a closed flag with their result set, so they hold no reference to it. - Async
iter_chunks(). It binds the result set when called. Leaving the loop early keeps the unread chunks with the result set.
Reactions are currently unavailable
Activity
laughingman7743 commentedon Oct 18, 2026
A related case for the pool ownership in item 1, found in the review of #1275 (#1230). It predates #1275: master at 7030514 behaves the same way.
Once
AioS3FileSystem._thread_poolhas been shut down, the multipart copy cannot clean up its upload.AioS3FileSystem._copy_object_with_multipart_upload()runs its cleanup withawait self._thread_pool.run_shielded(_abort)in itsexcept BaseExceptionbranch.S3AioThreadPool.submit()then raisesRuntimeError: cannot schedule new futures after shutdown. As a result:RuntimeErrorreplaces theCancelledError.RuntimeError.Nothing in PyAthena shuts this pool down today: item 1 notes that it is never shut down. The case arises only once the pool gets a lifecycle, through a
close()or a release at garbage collection, or when a caller shuts it down. That lifecycle should keep the cleanup of a running copy able to run, or run it outside the pool.Reproduction with stubbed requests (no AWS calls):
Output, on master at 7030514 and on the #1275 branch at aaed141:
Release the pool threads when idle and bind iter_chunks() at its call (…
Share the idle release step of S3AioThreadPool and trim docstrings (G…
Describe the idle release and the aio chunk iterator after close (GH-…
Track pending futures for the idle release and correct the aio read d…
Check that close() does not wait for a running copy part (GH-1209)
Make the aio as_*() methods coroutines and raise from closed chunk it…
Prune the pending futures only in release() and correct close-time do…
Refer to the pool weakly from future callbacks and narrow close-time …
Drop the retry claim of a cancelled chunk read and settle the pool GC…
Merge pull request #1286 from pyathena-dev/feat/1209-aio-pool-close-a…