PrivateLoop. A backend may make one for each job
and close it as the job ends, or keep one for job after job and close it as the process that ran
them stops; each run is one job’s, and PrivateLoop.close ends the loop the same way for both.
Every run of the loop makes it the thread’s current event loop while it runs and leaves the thread
with none after, as asyncio.Runner does, and runs it until the future it waits for is done and
the callbacks that end scheduled have run. A task that raises a SystemExit or
KeyboardInterrupt ends with it, and asyncio throws the same exception out of the call that runs
the loop as well; the loop keeps a record of its tasks and reads the exit a task ended with, so the
run tells a task’s exit, after which the loop runs on and the exit reaches the job it belongs to by
the ordinary await, from an exception raised in the loop’s own code by no task (a signal handler, a
pool’s time limit raised into the job’s thread, loop.stop(), a done callback raising a task’s
exit again, the loop failing to run at all).
An exception raised by no task ends the job: the job’s task is cancelled and run to its end, its
cleanup included, and the job then raises the exception. A further exception while that task ends
(a signal in the wind-down’s own code, an exception the loop raises on every run) ends that and is
what the job ends with, the first exception as its context; the loop so left with its work unended
is closed at once (PrivateLoop.close) and serves no further job.
The kit keeps its loop-bound resources (pooled clients, checkpoint savers, stores) per loop, and the
loop’s close ends the loop in the order asyncio.Runner does, with the kit’s release as the
application’s own clean-up that runs inside the run, before the runner’s close: after the mark below, the
release runs on the loop itself, while every task left on it is still alive, since a resource may own
tasks of its own (a connection pool’s workers) that its close awaits to their end. Every task left
on the loop is then cancelled and run to its end, round after round until no task is left, so a
task made while the others end (one a task’s cleanup starts, one the release leaves running, one a
thread hands the loop) is ended as well; the async generators left suspended on it are then closed
and the tasks their close made ended the same way, and the loop closes. Before the release, the close
marks the loop closing (tai42_kit.loop_closing): a task that leases a pooled client or a registry from
then on (one still running, or one in its cleanup) gets a RuntimeError naming the loop as
closing, as a closed connection pool refuses a new connection, so nothing is opened that the release
would not close; a lease the task held from before keeps working until it is released. Work that
starts itself anew each time it is cancelled keeps the close running, as a task that ignores its
cancel keeps asyncio.run waiting.
The loop’s default executor (the threads of asyncio.to_thread and run_in_executor(None, ...))
is shut down as the loop closes without waiting for its threads, where asyncio.run joins them:
a thread cannot be cancelled, so a job that awaited one is not held by it once the exception that
ends the job has reached the loop, and the thread ends on its own, its result unread. When that
exception reaches a loop waiting for a thread is the pool’s to say: a signal handler’s interrupts
the loop’s wait at once, while an exception raised into the job’s thread from another thread lands
only once the loop runs Python code again, which a loop waiting for its I/O does not do until the
wait ends (with the thread’s end, at the latest). A thread that hands the loop work after its close
fails at once with asyncio’s RuntimeError.
Pure Python cannot guard every instruction against an asynchronous exception (a signal handler’s,
or one a pool raises into the thread), so an exception that lands in this module’s own code is
raised where it lands: on the job’s thread the job raises it, and in close, close raises it.
Inside a callback the loop runs, asyncio itself catches it: a SystemExit or
KeyboardInterrupt it throws out of the loop, which ends the job as above; any other exception it
logs at ERROR and runs on, so the job does not raise it and what that callback was to do stays
undone. The callback by which this module ends a run stops the loop even when such an exception
lands in its loop.stop() call, so the run still ends, as its work did.
Two windows stay open. An exception that lands at the first instruction of that callback, or in
asyncio’s own scheduling of it as the work ends, is logged before the loop is stopped, and the run
waits on until the process is ended from outside, as a run of asyncio.run does in the same
case. A thread’s hand-off that reaches the loop from the start of the close’s last pass over it until
its closing is dropped with the loop’s pending callbacks, and a thread that waits for that
hand-off’s result waits on.
PrivateLoop
tai42_kit.backend.private_loop.PrivateLoop
close.
Members
loop
tai42_kit.backend.private_loop.PrivateLoop.loop
closed
tai42_kit.backend.private_loop.PrivateLoop.closed
close, by a job whose work on it could not be ended, or directly.
run
tai42_kit.backend.private_loop.PrivateLoop.run
coroutine to completion on the loop as one job, and return its result.
The job raises what the coroutine’s task ends with, a SystemExit or KeyboardInterrupt
included: the loop runs on after a task’s exit, so an exit from any task of the coroutine
reaches its awaiter as any exception does, and the loop serves the next job.
An exception raised by no task (a signal handler, a time limit raised into the job’s thread,
loop.stop(), a done callback raising a task’s exit again) cancels the coroutine’s task
and runs the loop until that task has ended, its cleanup included, and is then raised; an
error the task ends with after that cancel is logged at ERROR with its traceback, and so is
one the task had already ended with when the exception came, the log saying it came before.
A further exception while the task is cancelled and ends ends that: the job raises it, the
first as its context, and the loop is closed (close). A job that holds no task of
its coroutine closes the coroutine: the loop is closed, its task factory was replaced, or an
exception landed as the task was made, before the job held it. In that last case the task
stays on the loop, and the loop’s next run runs it: it fails at once on the closed
coroutine, which asyncio logs at ERROR as an exception never retrieved, and that run’s own
job is unaffected.
Parameters
close
tai42_kit.backend.private_loop.PrivateLoop.close
RuntimeError. Every task on the loop is then cancelled and the loop run
until each has ended, round after round until no task is left, as asyncio.run ends its
tasks on its way out; an error a task ends with then is logged at ERROR with its traceback.
The async generators left suspended on the loop are then closed, followed by the same end of
the tasks their close made. A release that fails raises once all of that has run. A step
that raises ends the steps after it, and the loop still closes. A loop running (on another
thread) cannot be closed: the close raises RuntimeError, leaves the loop running and
unmarked, and releases nothing. The loop’s default executor is shut down without waiting
for its threads (the module docstring tells why). A loop already closed is left as it is.
