You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Break result sets after failed or cancelled fetches and drop the cross-thread close guards #1206
A result set's fetches and close() can run at the same time. Each backend guards this differently, or not at all:
Concurrent fetches. Cancelling an asyncio fetch that runs in asyncio.to_thread() (pyathena/aio/common.py:807, 831, 851) leaves its thread fetching, and the next fetch runs alongside it. For pandas and Polars, the second fetch raises ValueError: generator already executing (pyathena/pandas/result_set.py:982, 996; pyathena/polars/result_set.py:637, 651).
S3FS close during a read. A close() during a fetch in another thread makes that fetch fail with TypeError / AttributeError / ValueError: I/O operation on closed file. Alternatively, the fetch returns a row of the closed result set (pyathena/s3fs/result_set.py:220-262, pyathena/s3fs/reader.py:163-165). The pandas and Polars result sets defer the close instead (pyathena/pandas/result_set.py:338-416, pyathena/polars/result_set.py:101-171).
Arrow and S3FS refill after close. Their _fetch() overrides lack the base check after the page is stored (pyathena/result_set.py:478-482). Rows read during close() are returned by later fetches on the closed result set: Arrow returns them unconverted (JSON as text), and S3FS returns ().
Closed semantics differ by path. Page fetches return nothing. _iter_all_row_pages() raises ProgrammingError("AthenaResultSet is closed."). _get_content_length() / _read_data_manifest() raise OutputLocation is none or empty. / DataManifestLocation is none or empty., and a close() during HeadObject still returns the length.
A conversion error drops a page._fetch() advances _next_token before converting the rows (pyathena/result_set.py:475-477), so any exception during conversion, including KeyboardInterrupt, loses that page.
Reproduction
The stub reproductions are summarized in #1205. They use no AWS calls: a stubbed GetQueryResults, local CSV files, and threads ordered with threading.Event.
Carried over from #1202 (PR #1223), where the maintainer decided to leave these fetches unstopped:
The fetchone() / fetchmany() / fetchall() of AioPandasCursor, AioArrowCursor, AioPolarsCursor, and AioS3FSCursor (WithAsyncFetch in pyathena/aio/common.py) still run in asyncio.to_thread(), so a cancellation does not stop their retries. Their retries are mostly S3 reads of a reader that later fetches continue. Stopping such a read leaves the reader part-way:
The CSV reader of S3FS fails in the middle of a record. csv.reader resets its record state on the next call, so a later fetch can start mid-record.
The itertuples() of pandas and Polars ends after an exception (documented), so later fetches return no rows.
The same happens today when a read exhausts its retries. The operation guard should therefore define what a cancelled fetch leaves behind. For example, it could invalidate the reader, so that later fetches raise instead of returning shifted or missing rows. Once that is defined, the fetches can be stopped with pyathena.aio.util.run_in_thread().
#1225 is decided as option (c) (see #1225 (comment)) and is folded into this issue's PR: an exception that leaves the operation scope marks the result set broken, and later fetches raise ProgrammingError before any request. This also covers problem 1 here, because a fetch after a cancelled asyncio fetch raises at once instead of running alongside the leftover thread.
PyAthena declares DB API threadsafety = 2: threads may share the module and connections, but not cursors. Using one cursor or result set from several threads, including a close() from another thread during a fetch, is therefore unsupported, and this issue no longer adds an operation guard for it. The documentation will state the limitation instead.
The existing code that exists only for that case is removed:
stop_when=lambda: self.is_closed in the GetQueryResults retries, the conversion of a failure during a concurrent close() into the closed error, and the close() check in _iter_all_row_pages(), from Stop the retries of closed result sets and cancelled asyncio calls #1223. A cancellation of an asyncio fetch still stops its retries through run_in_thread().
What remains of the problems above:
Cancelled asyncio fetch. This happens within one task, for example with asyncio.wait_for(). The fetches of WithAsyncFetch move to pyathena.aio.util.run_in_thread(), and a cancellation breaks the result set (Define the behavior of result set fetches after an error #1225 option (c)). The next fetch then raises ProgrammingError instead of running alongside the leftover thread.
to 4. These need a close() from another thread and are out of scope.
The #1225 implementation (a broken flag checked at the start of each fetch and set when a fetch raises, across the base, asyncio, pandas, Polars, Arrow, and S3FS result sets and the pandas and Polars chunk iterators) lands in the same PR.
changed the title [-]Coordinate result set operations with close() and with each other[/-][+]Break result sets after failed or cancelled fetches and drop the cross-thread close guards[/+]on Oct 17, 2026
Activity
Keep the retries of cleanup requests and of the cursors' fetches
laughingman7743 commentedon Oct 16, 2026
Carried over from #1202 (PR #1223), where the maintainer decided to leave these fetches unstopped:
The
fetchone()/fetchmany()/fetchall()ofAioPandasCursor,AioArrowCursor,AioPolarsCursor, andAioS3FSCursor(WithAsyncFetchinpyathena/aio/common.py) still run inasyncio.to_thread(), so a cancellation does not stop their retries. Their retries are mostly S3 reads of a reader that later fetches continue. Stopping such a read leaves the reader part-way:csv.readerresets its record state on the next call, so a later fetch can start mid-record.itertuples()of pandas and Polars ends after an exception (documented), so later fetches return no rows.The same happens today when a read exhausts its retries. The operation guard should therefore define what a cancelled fetch leaves behind. For example, it could invalidate the reader, so that later fetches raise instead of returning shifted or missing rows. Once that is defined, the fetches can be stopped with
pyathena.aio.util.run_in_thread().laughingman7743 commentedon Oct 17, 2026
#1225 is decided as option (c) (see #1225 (comment)) and is folded into this issue's PR: an exception that leaves the operation scope marks the result set broken, and later fetches raise
ProgrammingErrorbefore any request. This also covers problem 1 here, because a fetch after a cancelled asyncio fetch raises at once instead of running alongside the leftover thread.laughingman7743 commentedon Oct 17, 2026
Scope change: no operation guard
PyAthena declares DB API
threadsafety = 2: threads may share the module and connections, but not cursors. Using one cursor or result set from several threads, including aclose()from another thread during a fetch, is therefore unsupported, and this issue no longer adds an operation guard for it. The documentation will state the limitation instead.The existing code that exists only for that case is removed:
_lock,_readers,_close_requested,_read()), from Defer closing a chunk reader while another thread reads it #1182.close()from another thread before the first read).is_closedcheck after a page is stored and the page discard in_fetch()/_async_fetch(), from Discard the page of a request that close() interrupts #1197._take_buffered_rows()and the single read ofrownumberin_count_rows(), from Take buffered rows in bulk in fetchmany() and fetchall() #1187.stop_when=lambda: self.is_closedin theGetQueryResultsretries, the conversion of a failure during a concurrentclose()into the closed error, and theclose()check in_iter_all_row_pages(), from Stop the retries of closed result sets and cancelled asyncio calls #1223. A cancellation of an asyncio fetch still stops its retries throughrun_in_thread().What remains of the problems above:
asyncio.wait_for(). The fetches ofWithAsyncFetchmove topyathena.aio.util.run_in_thread(), and a cancellation breaks the result set (Define the behavior of result set fetches after an error #1225 option (c)). The next fetch then raisesProgrammingErrorinstead of running alongside the leftover thread.close()from another thread and are out of scope.The #1225 implementation (a broken flag checked at the start of each fetch and set when a fetch raises, across the base, asyncio, pandas, Polars, Arrow, and S3FS result sets and the pandas and Polars chunk iterators) lands in the same PR.
[-]Coordinate result set operations with close() and with each other[/-][+]Break result sets after failed or cancelled fetches and drop the cross-thread close guards[/+]Remove the stop_when condition of retry_api_call()
Close the result set of a cancelled fetch without the event loop
Simplify the failed-fetch and cancelled-close code
Describe how the asyncio cursors run fetches in threads
Break the pandas and Polars result sets on an interrupt while counting
Expect ProgrammingError after a failed binary CSV chunk read
Break result sets after a failed fetch and drop the cross-thread guards
Remove the stop_when condition of retry_api_call()
Close the result set of a cancelled fetch without the event loop
Simplify the failed-fetch and cancelled-close code
Describe how the asyncio cursors run fetches in threads
Break the pandas and Polars result sets on an interrupt while counting
Expect ProgrammingError after a failed binary CSV chunk read