SingleInstanceTask has distinct steps for creation of the object, and
starting the task. If a different coroutine is waiting on the
SingleInstanceTask, it isn't safe to directly call
SingleInstanceTask.wait() as the task may or may not have been created
yet.
Existing code usage of SingleInstanceTask is in 4 categories, with
reguards to SingleInstanceTask.wait():
1) using SingleInstanceTask without using SingleInstanceTask.wait().
This is unchanged.
2) using SingleInstanceTask.wait without a check on task is not None.
This may be safe now, but is fragile in the face of innocent-looking
refactors around the SingleInstanceTask.
3) using SingleInstanceTask.wait after confirming that the task is not
None. This is fine but a leaky abstraction.
4) directly waiting on the SingleInstanceTask.task. Another leaky
abstraction, but it's solving a cancellation problem. Leaving this
alone.
By enhancing SingleInstanceTask.wait(), cases 2 and 3 are improved. The
code not checking the task today is made safer, and the code checking
the task today can be simplified.
calling asyncio.create_task(...) without storing a reference to the
result can lead to the task being garbage collected before it actually
executed.
https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task
The documentation gives an example of a reliable way to run
fire-and-forget background tasks.
This patch adds an helper to do exactly that.
Signed-off-by: Olivier Gayot <olivier.gayot@canonical.com>
The behavior of asyncio.get_event_loop() will change in a future Python
version. It is deprecated starting Python 3.10.
The functions that we can use instead are:
* asyncio.new_event_loop() - which creates a new event loop
* asyncio.get_running_loop() - which returns the event loop only if it
is already running
Signed-off-by: Olivier Gayot <olivier.gayot@canonical.com>
cancel_restart is a mode for SingleInstanceTask that changes the
behavior when starting the task - if the task is already running, do not
cancel it to start another.
There is a common problem in concurrent / asynchronous code of what to
do with unhandled exceptions. If a (conceptual) thread of execution
fails, there's no guarantee (and no way of telling) if there's anything
listening. By default, I chose to have a failing task propagate the
exception up to the run loop for two reasons:
1) Unhandled exceptions are generally bad
2) urwid.ExitMainLoop needs to be propagated to the run loop to have
any effect
But this means that tasks that are expected to fail (and have this
failure handled) like block probing crash the process, which is
obviously a Bad Thing. This branch adds a way to turn off exception
propagation per-task, which is a bit hackish but works ok it seems.