support for running coroutines in parallel with staggered start times all staggeredrace import contextlib import typing from import events from import exceptions as exceptionsmod from import locks from import tasks async def staggeredrace corofns typing iterabletyping callable typing awaitable delay typing optionalfloat loop events abstracteventloop none typing tuple typing any typing optionalint typing listtyping optionalexception do work undo partially completed work todo when we have aiter and anext allow async iterables in corofns loop loop or events getrunningloop enumcorofns enumeratecorofns winnerresult none winnerindex none exceptions runningtasks async def runonecoro previousfailed typing optionallocks event none wait for the previous task to finish or for delay seconds if previousfailed is not none with contextlib suppressexceptionsmod timeouterror use asyncio waitfor instead of asyncio wait here so that if we get cancelled at this point event wait is also cancelled otherwise there will be a task destroyed but it is pending later await tasks waitforpreviousfailed wait delay get the next coroutine to run try thisindex corofn nextenumcorofns except stopiteration return start task that will run the next coroutine thisfailed locks event nexttask loop createtaskrunonecorothisfailed runningtasks appendnexttask assert lenrunningtasks thisindex 2 prepare place to put this coroutine s exceptions if not won exceptions appendnone assert lenexceptions thisindex 1 try result await corofn except systemexit keyboardinterrupt raise except baseexception as e exceptionsthisindex e thisfailed set kickstart the next coroutine else store winner s results nonlocal winnerindex winnerresult assert winnerindex is none winnerindex thisindex winnerresult result cancel all other tasks we take care to not cancel the current task as well if we do so then since there is no await after here and cancellederror are usually thrown at one we will encounter a curious corner case where the current task will end up as done true cancelled false exception asyncio cancellederror this behavior is specified in https bugs python orgissue30048 for i t in enumeraterunningtasks if i thisindex t cancel firsttask loop createtaskrunonecoronone runningtasks appendfirsttask try wait for a growing list of tasks to all finish poor man s version of curio s taskgroup or trio s nursery donecount 0 while donecount lenrunningtasks done await tasks waitrunningtasks donecount lendone if runonecoro raises an unhandled exception it s probably a programming error and i want to see it if debug for d in done if d done and not d cancelled and d exception raise d exception return winnerresult winnerindex exceptions finally make sure no tasks are left running if we leave this function for t in runningtasks t cancel run coroutines with staggered start times and take the first to finish this method takes an iterable of coroutine functions the first one is started immediately from then on whenever the immediately preceding one fails raises an exception or when delay seconds has passed the next coroutine is started this continues until one of the coroutines complete successfully in which case all others are cancelled or until all coroutines fail the coroutines provided should be well behaved in the following way they should only return if completed successfully they should always raise an exception if they did not complete successfully in particular if they handle cancellation they should probably reraise like this try do work except asyncio cancellederror undo partially completed work raise args coro_fns an iterable of coroutine functions i e callables that return a coroutine object when called use functools partial or lambdas to pass arguments delay amount of time in seconds between starting coroutines if none the coroutines will run sequentially loop the event loop to use returns tuple winner_result winner_index exceptions where winner_result the result of the winning coroutine or none if no coroutines won winner_index the index of the winning coroutine in coro_fns or none if no coroutines won if the winning coroutine may return none on success winner_index can be used to definitively determine whether any coroutine won exceptions list of exceptions returned by the coroutines len exceptions is equal to the number of coroutines actually started and the order is the same as in coro_fns the winning coroutine s entry is none todo when we have aiter and anext allow async iterables in coro_fns wait for the previous task to finish or for delay seconds use asyncio wait_for instead of asyncio wait here so that if we get cancelled at this point event wait is also cancelled otherwise there will be a task destroyed but it is pending later get the next coroutine to run start task that will run the next coroutine prepare place to put this coroutine s exceptions if not won kickstart the next coroutine store winner s results cancel all other tasks we take care to not cancel the current task as well if we do so then since there is no await after here and cancellederror are usually thrown at one we will encounter a curious corner case where the current task will end up as done true cancelled false exception asyncio cancellederror this behavior is specified in https bugs python org issue30048 wait for a growing list of tasks to all finish poor man s version of curio s taskgroup or trio s nursery if run_one_coro raises an unhandled exception it s probably a programming error and i want to see it make sure no tasks are left running if we leave this function
__all__ = 'staggered_race',

import contextlib
import typing

from . import events
from . import exceptions as exceptions_mod
from . import locks
from . import tasks


async def staggered_race(
        coro_fns: typing.Iterable[typing.Callable[[], typing.Awaitable]],
        delay: typing.Optional[float],
        *,
        loop: events.AbstractEventLoop = None,
) -> typing.Tuple[
    typing.Any,
    typing.Optional[int],
    typing.List[typing.Optional[Exception]]
]:
    
    
    loop = loop or events.get_running_loop()
    enum_coro_fns = enumerate(coro_fns)
    winner_result = None
    winner_index = None
    exceptions = []
    running_tasks = []

    async def run_one_coro(
            previous_failed: typing.Optional[locks.Event]) -> None:
        
        if previous_failed is not None:
            with contextlib.suppress(exceptions_mod.TimeoutError):
                
                
                
                
                await tasks.wait_for(previous_failed.wait(), delay)
        
        try:
            this_index, coro_fn = next(enum_coro_fns)
        except StopIteration:
            return
        
        this_failed = locks.Event()
        next_task = loop.create_task(run_one_coro(this_failed))
        running_tasks.append(next_task)
        assert len(running_tasks) == this_index + 2
        
        exceptions.append(None)
        assert len(exceptions) == this_index + 1

        try:
            result = await coro_fn()
        except (SystemExit, KeyboardInterrupt):
            raise
        except BaseException as e:
            exceptions[this_index] = e
            this_failed.set()  
        else:
            
            nonlocal winner_index, winner_result
            assert winner_index is None
            winner_index = this_index
            winner_result = result
            
            
            
            
            
            
            
            for i, t in enumerate(running_tasks):
                if i != this_index:
                    t.cancel()

    first_task = loop.create_task(run_one_coro(None))
    running_tasks.append(first_task)
    try:
        
        
        done_count = 0
        while done_count != len(running_tasks):
            done, _ = await tasks.wait(running_tasks)
            done_count = len(done)
            
            
            if __debug__:
                for d in done:
                    if d.done() and not d.cancelled() and d.exception():
                        raise d.exception()
        return winner_result, winner_index, exceptions
    finally:
        
        for t in running_tasks:
            t.cancel()