SWF timeouts can happen on different objects (workflow executions,
        activity tasks, decision tasks) and should be processed in order.

        A specific timeout can change the workflow execution state and have an
        impact on other timeouts: for instance, if the workflow execution
        timeouts, subsequent timeouts on activity or decision tasks are
        irrelevant ; if an activity task timeouts, other timeouts on this task
        are irrelevant, and a new decision is fired, which could well timeout
        before the end of the workflow.

        So the idea here is to find the earliest timeout that would have been
        triggered, process it, then make the workflow state progress and repeat
        the whole process.

def _process_timeouts(self):
        """
        SWF timeouts can happen on different objects (workflow executions,
        activity tasks, decision tasks) and should be processed in order.

        A specific timeout can change the workflow execution state and have an
        impact on other timeouts: for instance, if the workflow execution
        timeouts, subsequent timeouts on activity or decision tasks are
        irrelevant ; if an activity task timeouts, other timeouts on this task
        are irrelevant, and a new decision is fired, which could well timeout
        before the end of the workflow.

        So the idea here is to find the earliest timeout that would have been
        triggered, process it, then make the workflow state progress and repeat
        the whole process.
        """
        timeout_candidates = []

        # workflow execution timeout
        timeout_candidates.append(self.first_timeout())

        # decision tasks timeouts
        for task in self.decision_tasks:
            timeout_candidates.append(task.first_timeout())

        # activity tasks timeouts
        for task in self.activity_tasks:
            timeout_candidates.append(task.first_timeout())

        # remove blank values (foo.first_timeout() is a Timeout or None)
        timeout_candidates = list(filter(None, timeout_candidates))

        # now find the first timeout to process
        first_timeout = None
        if timeout_candidates:
            first_timeout = min(
                timeout_candidates,
                key=lambda t: t.timestamp
            )

        if first_timeout:
            should_schedule_decision_next = False
            if isinstance(first_timeout.obj, WorkflowExecution):
                self.timeout(first_timeout)
            elif isinstance(first_timeout.obj, DecisionTask):
                self.timeout_decision_task(first_timeout)
                should_schedule_decision_next = True
            elif isinstance(first_timeout.obj, ActivityTask):
                self.timeout_activity_task(first_timeout)
                should_schedule_decision_next = True
            else:
                raise NotImplementedError("Unhandled timeout object")

            # schedule decision task if needed
            if should_schedule_decision_next:
                self.schedule_decision_task()

            # the workflow execution progressed, let's see if another
            # timeout should be processed
            self._process_timeouts()