Timeout
A Timeout limits how long the code in a with-statement may run. Entering the
context makes it the current timeout of the calling thread - as a WarningCollector is
made the current collector - so code anywhere below in the call hierarchy asks for the time left, without the timeout
being passed along as a parameter.
from pyTooling.Timeout import Timeout
with Timeout(20.0):
for package in packages:
Timeout.RaiseIfExpired("Scanning the dependencies took longer than 20 seconds.")
response = session.get(package.URL, timeout=Timeout.Remaining())
Important
The timeout is cooperative. Python can’t stop a running thread from outside, so nothing interrupts the code in
the with block when the deadline passes. A timeout takes effect where the code checks it - a loop aborting
itself when IsExpired() turns True - and where the time left is handed to a
blocking call as its own timeout.
Asking for the Time Left
Three classmethods answer for the current timeout of the calling thread:
Remaining()The seconds left as a
float,0.0once the timeout expired, andNonewhen no timeout is current.IsExpired()Trueonce the timeout expired;Falsebefore, and when no timeout is current.Current()The current timeout itself - like
threading.current_thread()returns the thread - orNone. ItsSecondsandParenttell how it was set up.
A timeout is entered once at a time: entering it again inside its own with block raises
TimeoutUsageError. After its block, it can be entered again.
The time left is what a blocking call gets as its own timeout - a request (requests: timeout=, aiohttp:
ClientTimeout(total=...)), threading.Thread.join(), concurrent.futures.Future.result(),
asyncio.timeout() or asyncio.wait_for(). A loop calls RaiseIfExpired()
once per iteration and so aborts itself with a TimeoutExpiredError, which is a
TimeoutError too. The error carries the configured timeout
(Seconds) and by how many seconds the deadline was overrun
(Overrun) - how long the last iteration ran past it. A loop that needs to
clean up first asks IsExpired() and raises itself.
The time runs from entering the context; the deadline is a time.perf_counter_ns() value, so a change of the
system’s clock doesn’t shorten or extend it.
Nesting
A timeout entered while another one is current can only shorten the time left, never extend it: its deadline is the earlier of its own and the outer one’s. Leaving the inner context makes the outer timeout current again.
with Timeout(20.0): # the whole dependency scan
with Timeout(5.0): # one package: at most 5 seconds, and never beyond the 20
...
with Timeout(60.0): # still ends with the outer 20 seconds
...
Threads and Tasks
The current timeout is stored per thread.
asynciotasks run in the thread of their event loop, so they see the timeout of that thread.A new thread starts without a current timeout. Hand the time left over to it: wait for it no longer than the time left, or let it enter a timeout of its own with the seconds read before starting it.
from threading import Thread
def worker(remaining: float) -> None:
with Timeout(remaining):
...
with Timeout(20.0):
thread = Thread(target=worker, args=(Timeout.Remaining(), ))
thread.start()
thread.join(timeout=Timeout.Remaining())