Repository: PrefectHQ/prefect

Issue:
Add additional FlowRunner schedule check
Similar to the `TaskRunner` check which determines if a scheduled task is running after its scheduled time, we should add a scheduling check to the `FlowRunner` to ensure the flow run doesn't begin until on or after its scheduled time.


Discussion:

Resolving patch:
diff --git a/src/prefect/engine/flow_runner.py b/src/prefect/engine/flow_runner.py
--- a/src/prefect/engine/flow_runner.py
+++ b/src/prefect/engine/flow_runner.py
@@ -25,6 +25,7 @@
     Pending,
     Retrying,
     Running,
+    Scheduled,
     State,
     Success,
 )
@@ -236,6 +237,7 @@ def run(
 
             with prefect.context(context):
                 state = self.check_flow_is_pending_or_running(state)
+                state = self.check_flow_reached_start_time(state)
                 state = self.set_flow_to_running(state)
                 state = self.get_flow_run_state(
                     state,
@@ -264,6 +266,31 @@ def run(
 
         return state
 
+    @call_state_handlers
+    def check_flow_reached_start_time(self, state: State) -> State:
+        """
+        Checks if the Flow is in a Scheduled state and, if it is, ensures that the scheduled
+        time has been reached.
+
+        Args:
+            - state (State): the current state of this Flow
+
+        Returns:
+            - State: the state of the flow after performing the check
+
+        Raises:
+            - ENDRUN: if the flow is Scheduled with a future scheduled time
+        """
+        if isinstance(state, Scheduled):
+            if state.start_time and state.start_time > pendulum.now("utc"):
+                self.logger.debug(
+                    "Flow '{name}': start_time has not been reached; ending run.".format(
+                        name=self.flow.name
+                    )
+                )
+                raise ENDRUN(state)
+        return state
+
     @call_state_handlers
     def check_flow_is_pending_or_running(self, state: State) -> State:
         """
diff --git a/src/prefect/engine/task_runner.py b/src/prefect/engine/task_runner.py
--- a/src/prefect/engine/task_runner.py
+++ b/src/prefect/engine/task_runner.py
@@ -517,7 +517,7 @@ def check_task_reached_start_time(self, state: State) -> State:
             - state (State): the current state of this task
 
         Returns:
-            - State: the state of the task after running the task
+            - State: the state of the task after performing the check
 
         Raises:
             - ENDRUN: if the task is not a start task and Scheduled with a future


Test patch:
diff --git a/tests/engine/test_flow_runner.py b/tests/engine/test_flow_runner.py
--- a/tests/engine/test_flow_runner.py
+++ b/tests/engine/test_flow_runner.py
@@ -476,6 +476,36 @@ def test_not_pending_or_running_raise_endrun(self, state):
             FlowRunner(flow=flow).check_flow_is_pending_or_running(state=state)
 
 
+class TestCheckScheduledStep:
+    @pytest.mark.parametrize("state", [Failed(), Pending(), Running(), Success()])
+    def test_non_scheduled_states(self, state):
+        assert (
+            FlowRunner(flow=Flow()).check_flow_reached_start_time(state=state) is state
+        )
+
+    def test_scheduled_states_without_start_time(self):
+        state = Scheduled(start_time=None)
+        assert (
+            FlowRunner(flow=Flow()).check_flow_reached_start_time(state=state) is state
+        )
+
+    def test_scheduled_states_with_future_start_time(self):
+        state = Scheduled(
+            start_time=pendulum.now("utc") + datetime.timedelta(minutes=10)
+        )
+        with pytest.raises(ENDRUN) as exc:
+            FlowRunner(flow=Flow()).check_flow_reached_start_time(state=state)
+        assert exc.value.state is state
+
+    def test_scheduled_states_with_past_start_time(self):
+        state = Scheduled(
+            start_time=pendulum.now("utc") - datetime.timedelta(minutes=1)
+        )
+        assert (
+            FlowRunner(flow=Flow()).check_flow_reached_start_time(state=state) is state
+        )
+
+
 class TestSetFlowToRunning:
     @pytest.mark.parametrize("state", [Pending(), Retrying()])
     def test_pending_becomes_running(self, state):