(self, runner: _run.Runner)
| 158 | sys.set_asyncgen_hooks(firstiter=firstiter, finalizer=finalizer) # type: ignore[arg-type] # Finalizer doesn't use AsyncGeneratorType |
| 159 | |
| 160 | async def finalize_remaining(self, runner: _run.Runner) -> None: |
| 161 | # This is called from init after shutting down the system nursery. |
| 162 | # The only tasks running at this point are init and |
| 163 | # the run_sync_soon task, and since the system nursery is closed, |
| 164 | # there's no way for user code to spawn more. |
| 165 | assert _core.current_task() is runner.init_task |
| 166 | assert len(runner.tasks) == 2 |
| 167 | |
| 168 | # To make async generator finalization easier to reason |
| 169 | # about, we'll shut down asyncgen garbage collection by turning |
| 170 | # the alive WeakSet into a regular set. |
| 171 | self.alive = set(self.alive) |
| 172 | |
| 173 | # Process all pending run_sync_soon callbacks, in case one of |
| 174 | # them was an asyncgen finalizer that snuck in under the wire. |
| 175 | runner.entry_queue.run_sync_soon(runner.reschedule, runner.init_task) |
| 176 | await _core.wait_task_rescheduled( |
| 177 | lambda _: _core.Abort.FAILED, # pragma: no cover |
| 178 | ) |
| 179 | self.alive.update(self.trailing_needs_finalize) |
| 180 | self.trailing_needs_finalize.clear() |
| 181 | |
| 182 | # None of the still-living tasks use async generators, so |
| 183 | # every async generator must be suspended at a yield point -- |
| 184 | # there's no one to be doing the iteration. That's good, |
| 185 | # because aclose() only works on an asyncgen that's suspended |
| 186 | # at a yield point. (If it's suspended at an event loop trap, |
| 187 | # because someone is in the middle of iterating it, then you |
| 188 | # get a RuntimeError on 3.8+, and a nasty surprise on earlier |
| 189 | # versions due to https://bugs.python.org/issue32526.) |
| 190 | # |
| 191 | # However, once we start aclose() of one async generator, it |
| 192 | # might start fetching the next value from another, thus |
| 193 | # preventing us from closing that other (at least until |
| 194 | # aclose() of the first one is complete). This constraint |
| 195 | # effectively requires us to finalize the remaining asyncgens |
| 196 | # in arbitrary order, rather than doing all of them at the |
| 197 | # same time. On 3.8+ we could defer any generator with |
| 198 | # ag_running=True to a later batch, but that only catches |
| 199 | # the case where our aclose() starts after the user's |
| 200 | # asend()/etc. If our aclose() starts first, then the |
| 201 | # user's asend()/etc will raise RuntimeError, since they're |
| 202 | # probably not checking ag_running. |
| 203 | # |
| 204 | # It might be possible to allow some parallelized cleanup if |
| 205 | # we can determine that a certain set of asyncgens have no |
| 206 | # interdependencies, using gc.get_referents() and such. |
| 207 | # But just doing one at a time will typically work well enough |
| 208 | # (since each aclose() executes in a cancelled scope) and |
| 209 | # is much easier to reason about. |
| 210 | |
| 211 | # It's possible that that cleanup code will itself create |
| 212 | # more async generators, so we iterate repeatedly until |
| 213 | # all are gone. |
| 214 | while self.alive: |
| 215 | batch = self.alive |
| 216 | self.alive = _ASYNC_GEN_SET() |
| 217 | for agen in batch: |
no test coverage detected