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
Closing a subscription leaves a distinct source iterator open #283
On current main (d913a61), map_async_iterable registers cleanup on the async iterable object, while async iteration consumes the object returned by its aiter method. A valid wrapper can return a separate async generator. Closing the mapped stream then leaves that generator open, so its finally block does not run. The same behavior is reachable through subscribe().
Minimal reproducer:
importasynciofromgraphql.execution.async_iterablesimportmap_async_iterableasyncdefmain():
closed=Falseasyncdefevents():
nonlocalclosedtry:
yield1yield2finally:
closed=Trueiterator=events()
classEvents:
def__aiter__(self):
returniteratorasyncdefidentity(value):
returnvaluemapped=map_async_iterable(Events(), identity)
try:
assertawaitanext(mapped) ==1awaitmapped.aclose()
assertclosed# Fails: the source iterator is still open.finally:
awaititerator.aclose()
asyncio.run(main())
Expected: closing the mapped stream awaits cleanup of the iterator actually supplying events. Actual: cleanup is attempted on the wrapper; the generator remains open and can yield the second event.
A narrow local fix acquires the iterator before registering cleanup (aclosing(aiter(iterable))). Four new regressions fail on the base and pass with that change, including one through the public subscription API. The full local suites pass on Python 3.10 and 3.14. This differs from the broader AnyIO/cancellation work in #165 and #166; it does not change pending-next cancellation behavior.
Does closing the consumed iterator match the intended port contract? I can link the focused patch after discussion, following the PR template. AI assistance was used to investigate and validate this report.
Thanks for the report and the clear reproducer, @AryanRG. Yes, closing the iterator that is actually consumed is the intended contract. This is a regression in 3.3: in 3.2, MapAsyncIterator called iterable.__aiter__() and closed that iterator. GraphQL.js does the same in mapAsyncIterable: it calls return() on iterable[Symbol.asyncIterator](), not on the iterable itself.
So aclosing(aiter(iterable)) is the right fix, and aiter() is fine since we require Python 3.10. Please go ahead and open a PR. Keep it focused, with the tests in tests/execution/test_map_async_iterable.py plus the one going through subscribe(). The fix will then be part of 3.3.1.
Activity
Cito commentedon Oct 19, 2026
Thanks for the report and the clear reproducer, @AryanRG. Yes, closing the iterator that is actually consumed is the intended contract. This is a regression in 3.3: in 3.2,
MapAsyncIteratorcallediterable.__aiter__()and closed that iterator. GraphQL.js does the same inmapAsyncIterable: it callsreturn()oniterable[Symbol.asyncIterator](), not on the iterable itself.So
aclosing(aiter(iterable))is the right fix, andaiter()is fine since we require Python 3.10. Please go ahead and open a PR. Keep it focused, with the tests intests/execution/test_map_async_iterable.pyplus the one going throughsubscribe(). The fix will then be part of 3.3.1.AryanRG commentedon Oct 19, 2026
Thanks @Cito, I've opened #286 with the iterator cleanup fix and regression tests, including the
subscribe()case.