Repository: Qiskit/qiskit

Issue:
`ConstrainedReschedule` Pass causes RecurssionError for large enough input circuit
### Environment

- **Qiskit Terra version**: 0.24.0rc1
- **Python version**: 3.9
- **Operating system**:  Linux


### What is happening?

Running the constrained reschedule pass with a sufficiently large backend can cause a recursion error like:
```
RecursionError: maximum recursion depth exceeded while calling a Python object
```

This seems to be caused by the internal recursion used to push back gates that need to be aligned. It looks like for large circuits it's just exceeding the max stack depth allowed causing an error.

### How can we reproduce the issue?

```python
from qiskit.transpiler.passes import ALAPScheduleAnalysis, TimeUnitConversion, InstructionDurationCheck, ConstrainedReschedule, PadDelay
from qiskit.transpiler.target import Target, InstructionProperties
from qiskit import transpile
from qiskit.transpiler import PassManager
from qiskit.circuit.random import random_circuit
from qiskit.circuit.library.standard_gates import *
from qiskit.circuit import Parameter, Measure, Delay
import rustworkx as rx
import numpy as np

rng = np.random.default_rng(seed=12345678942)

g = rx.generators.directed_heavy_hex_graph(13)
target = Target(
    num_qubits=len(g),
    min_length=16,
    pulse_alignment=8,
    acquire_alignment=16,
    granularity=8
)
rz_props = {}
x_props = {}
sx_props = {}
measure_props = {}
delay_props = {}
id_props = {}
for i in range(len(g)):
    qarg = (i,)
    rz_props[qarg] = InstructionProperties(error=0.0, duration=0.0)
    x_props[qarg] = InstructionProperties(
        error=rng.uniform(1e-6, 1e-5), duration=rng.uniform(1e-8, 9e-7)
    )
    sx_props[qarg] = InstructionProperties(
        error=rng.uniform(1e-6, 1e-5), duration=rng.uniform(1e-8, 9e-7)
    )
    measure_props[qarg] = InstructionProperties(
        error=rng.uniform(1e-6, 1e-5), duration=rng.uniform(1e-8, 9e-7)
    )
    delay_props[qarg] = None
    id_props[qarg] = None

target.add_instruction(XGate(), x_props)
target.add_instruction(SXGate(), sx_props)
target.add_instruction(RZGate(Parameter("theta")), rz_props)
target.add_instruction(Measure(), measure_props)
target.add_instruction(Delay(Parameter("t")), delay_props)
target.add_instruction(IGate(), id_props, name="id")
target.add_instruction(
    ECRGate(),
    {
        edge: InstructionProperties(
            error=rng.uniform(1e-6, 5e-5), duration=rng.uniform(1e-8, 9e-7)
        )
        for edge in g.edge_list()
    }
)

durations = target.durations()

schedule = PassManager([
    TimeUnitConversion(durations),
    ALAPScheduleAnalysis(durations),
    InstructionDurationCheck(target.acquire_alignment, target.pulse_alignment),
    ConstrainedReschedule(target.acquire_alignment, target.pulse_alignment),
    PadDelay(),
])

qc = random_circuit(100, 20, measure=True, seed=202342)
tqc = transpile(qc, target=target)
schedule.run(tqc)
```

### What should happen?

The pass shouldn't error and should function as expected.

### Any suggestions?

Remove the recursion and rewrite the internals to be iterative


Discussion:

Resolving patch:
diff --git a/qiskit/transpiler/passes/scheduling/alignments/reschedule.py b/qiskit/transpiler/passes/scheduling/alignments/reschedule.py
--- a/qiskit/transpiler/passes/scheduling/alignments/reschedule.py
+++ b/qiskit/transpiler/passes/scheduling/alignments/reschedule.py
@@ -88,13 +88,9 @@ def _get_next_gate(cls, dag: DAGCircuit, node: DAGOpNode) -> List[DAGOpNode]:
         Returns:
             A list of non-delay successors.
         """
-        op_nodes = []
         for next_node in dag.successors(node):
-            if isinstance(next_node, DAGOutNode):
-                continue
-            op_nodes.append(next_node)
-
-        return op_nodes
+            if not isinstance(next_node, DAGOutNode):
+                yield next_node
 
     def _push_node_back(self, dag: DAGCircuit, node: DAGOpNode, shift: int):
         """Update start time of current node. Successors are also shifted to avoid overlap.
@@ -114,57 +110,63 @@ def _push_node_back(self, dag: DAGCircuit, node: DAGOpNode, shift: int):
         conditional_latency = self.property_set.get("conditional_latency", 0)
         clbit_write_latency = self.property_set.get("clbit_write_latency", 0)
 
-        # Compute shifted t1 of this node separately for qreg and creg
-        this_t0 = node_start_time[node]
-        new_t1q = this_t0 + node.op.duration + shift
-        this_qubits = set(node.qargs)
-        if isinstance(node.op, Measure):
-            # creg access ends at the end of instruction
-            new_t1c = new_t1q
-            this_clbits = set(node.cargs)
-        else:
-            if node.op.condition_bits:
-                # conditional access ends at the beginning of node start time
-                new_t1c = this_t0 + shift
-                this_clbits = set(node.op.condition_bits)
-            else:
-                new_t1c = None
-                this_clbits = set()
-
-        # Check successors for overlap
-        for next_node in self._get_next_gate(dag, node):
-            # Compute next node start time separately for qreg and creg
-            next_t0q = node_start_time[next_node]
-            next_qubits = set(next_node.qargs)
-            if isinstance(next_node.op, Measure):
-                # creg access starts after write latency
-                next_t0c = next_t0q + clbit_write_latency
-                next_clbits = set(next_node.cargs)
+        nodes_with_overlap = [(node, shift)]
+        shift_stack = []
+        while nodes_with_overlap:
+            node, shift = nodes_with_overlap.pop()
+            shift_stack.append((node, shift))
+            # Compute shifted t1 of this node separately for qreg and creg
+            this_t0 = node_start_time[node]
+            new_t1q = this_t0 + node.op.duration + shift
+            this_qubits = set(node.qargs)
+            if isinstance(node.op, Measure):
+                # creg access ends at the end of instruction
+                new_t1c = new_t1q
+                this_clbits = set(node.cargs)
             else:
-                if next_node.op.condition_bits:
-                    # conditional access starts before node start time
-                    next_t0c = next_t0q - conditional_latency
-                    next_clbits = set(next_node.op.condition_bits)
+                if node.op.condition_bits:
+                    # conditional access ends at the beginning of node start time
+                    new_t1c = this_t0 + shift
+                    this_clbits = set(node.op.condition_bits)
                 else:
-                    next_t0c = None
-                    next_clbits = set()
-            # Compute overlap if there is qubits overlap
-            if any(this_qubits & next_qubits):
-                qreg_overlap = new_t1q - next_t0q
-            else:
-                qreg_overlap = 0
-            # Compute overlap if there is clbits overlap
-            if any(this_clbits & next_clbits):
-                creg_overlap = new_t1c - next_t0c
-            else:
-                creg_overlap = 0
-            # Shift next node if there is finite overlap in either in qubits or clbits
-            overlap = max(qreg_overlap, creg_overlap)
-            if overlap > 0:
-                self._push_node_back(dag, next_node, overlap)
-
+                    new_t1c = None
+                    this_clbits = set()
+
+            # Check successors for overlap
+            for next_node in self._get_next_gate(dag, node):
+                # Compute next node start time separately for qreg and creg
+                next_t0q = node_start_time[next_node]
+                next_qubits = set(next_node.qargs)
+                if isinstance(next_node.op, Measure):
+                    # creg access starts after write latency
+                    next_t0c = next_t0q + clbit_write_latency
+                    next_clbits = set(next_node.cargs)
+                else:
+                    if next_node.op.condition_bits:
+                        # conditional access starts before node start time
+                        next_t0c = next_t0q - conditional_latency
+                        next_clbits = set(next_node.op.condition_bits)
+                    else:
+                        next_t0c = None
+                        next_clbits = set()
+                # Compute overlap if there is qubits overlap
+                if any(this_qubits & next_qubits):
+                    qreg_overlap = new_t1q - next_t0q
+                else:
+                    qreg_overlap = 0
+                # Compute overlap if there is clbits overlap
+                if any(this_clbits & next_clbits):
+                    creg_overlap = new_t1c - next_t0c
+                else:
+                    creg_overlap = 0
+                # Shift next node if there is finite overlap in either in qubits or clbits
+                overlap = max(qreg_overlap, creg_overlap)
+                if overlap > 0:
+                    nodes_with_overlap.append((next_node, overlap))
         # Update start time of this node after all overlaps are resolved
-        node_start_time[node] += shift
+        while shift_stack:
+            node, shift = shift_stack.pop()
+            node_start_time[node] += shift
 
     def run(self, dag: DAGCircuit):
         """Run rescheduler.


Test patch: