Skip to content

Break result sets after failed or cancelled fetches and drop the cross-thread close guards #1206

Closed

Description

@laughingman7743

Part of #1205 (step 2; see R2 and model 2 there).

Problem

A result set's fetches and close() can run at the same time. Each backend guards this differently, or not at all:

  1. 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).
  2. 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).
  3. 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 ().
  4. 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.
  5. 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.

Environment

  • PyAthena master at b2d7aa2
  • Python 3.15

Proposed fix (optional)

Use one operation guard in the base result set, replacing the per-backend guards (#1182, #1190, #1197, #1187):

  • Each fetch and read runs in an operation scope that serializes fetches.
  • close() marks the result set closed, signals the stop from step 1, and defers releasing resources until the running operation leaves its scope.
  • Operations that start or complete after close() return no rows. Paths that cannot return nothing raise one ProgrammingError for the closed result set.
  • Page state advances only after its rows are converted.

Validate with unit tests for each backend, sync and asyncio.

Activity

  1. laughingman7743 commented on Oct 16, 2026

    @laughingman7743
    MemberAuthor

    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().

  2. laughingman7743 commented on Oct 17, 2026

    @laughingman7743
    MemberAuthor

    #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.

  3. laughingman7743 commented on Oct 17, 2026

    @laughingman7743
    MemberAuthor

    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 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:

    What remains of the problems above:

    1. 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.
    2. to 4. These need a close() from another thread and are out of scope.
    3. A conversion error drops a page. With Define the behavior of result set fetches after an error #1225 option (c), a failed fetch breaks the result set, so no later fetch can skip the page.

    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.

  4. 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions