Repository: PrefectHQ/prefect

Issue:
Unify on- and off- schedule run pipelines
Currently, `flow.run()` has two logic branches:
- if a flow doesn't have a schedule, we create a flowrunner and call it's run() method.
- if a flow does have a schedule, we call the `_run_on_schedule` method which does a number of additional logic.

I would like to combine these into a single branch, because having two different pipelines leads to the potential for different behaviors depending on how you're running a flow (an issue that plagues Airflow). For example in #1429 I realized that tasks results are returned when running on schedule, but not returned when running off schedule.

A solution would be to simply create a "one-off" schedule and call `_run_on_schedule()` no matter what, with the caveat that the schedule would need to be removed so it didn't overwrite the original flow schedule.


Discussion:
> For example in #1429 I realized that tasks results are returned when running on schedule, but not returned when running off schedule.

Something about this is strange to me, as I have been using `flow.run()` for schedule-less flows and getting task results back (see, for example, my most recent PR'ed tutorial).  But I see in the code you changed how it could occur 🤔 

Resolving patch:
diff --git a/src/prefect/core/flow.py b/src/prefect/core/flow.py
--- a/src/prefect/core/flow.py
+++ b/src/prefect/core/flow.py
@@ -822,15 +822,19 @@ def set_dependencies(
 
     # Execution  ---------------------------------------------------------------
 
-    def _run_on_schedule(
-        self, parameters: Dict[str, Any], runner_cls: type, **kwargs: Any
+    def _run(
+        self,
+        parameters: Dict[str, Any],
+        runner_cls: type,
+        run_on_schedule: bool = True,
+        **kwargs: Any
     ) -> "prefect.engine.state.State":
 
         base_parameters = parameters or dict()
 
         ## determine time of first run
         try:
-            if self.schedule is not None:
+            if run_on_schedule and self.schedule is not None:
                 next_run_event = self.schedule.next(1, return_events=True)[0]
                 next_run_time = next_run_event.start_time  # type: ignore
                 parameters = base_parameters.copy()
@@ -925,7 +929,7 @@ def _run_on_schedule(
                         if s.cached_result_expiration > now
                     ]
                     prefect.context.caches[t.cache_key or t.name] = fresh_states
-                if self.schedule is not None:
+                if run_on_schedule and self.schedule is not None:
                     next_run_event = self.schedule.next(1, return_events=True)[0]
                     next_run_time = next_run_event.start_time  # type: ignore
                     parameters = base_parameters.copy()
@@ -1016,13 +1020,13 @@ def run(
 
         if run_on_schedule is None:
             run_on_schedule = cast(bool, prefect.config.flows.run_on_schedule)
-        if run_on_schedule is False:
-            runner = runner_cls(flow=self)
-            state = runner.run(parameters=parameters, return_tasks=self.tasks, **kwargs)
-        else:
-            state = self._run_on_schedule(
-                parameters=parameters, runner_cls=runner_cls, **kwargs
-            )
+
+        state = self._run(
+            parameters=parameters,
+            runner_cls=runner_cls,
+            run_on_schedule=run_on_schedule,
+            **kwargs
+        )
 
         # state always should return a dict of tasks. If it's NoResult (meaning the run was
         # interrupted before any tasks were executed), we set the dict manually.


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
@@ -1389,29 +1389,18 @@ def return_scheduled_start_time():
             res.result[return_scheduled_start_time].result, datetime.datetime
         )
 
-    def test_flow_runner_does_override_scheduled_start_time_when_running_off_schedule(
-        self,
-    ):
-        @prefect.task
-        def return_scheduled_start_time():
-            return prefect.context.get("scheduled_start_time")
-
-        f = Flow(name="test", tasks=[return_scheduled_start_time])
-        res = f.run(context=dict(scheduled_start_time=42), run_on_schedule=False)
-
-        assert res.is_successful()
-        assert res.result[return_scheduled_start_time].is_successful()
-        assert res.result[return_scheduled_start_time].result == 42
-
+    @pytest.mark.parametrize("run_on_schedule", [True, False])
     def test_flow_runner_doesnt_override_scheduled_start_time_when_running_on_schedule(
-        self,
+        self, run_on_schedule
     ):
         @prefect.task
         def return_scheduled_start_time():
             return prefect.context.get("scheduled_start_time")
 
         f = Flow(name="test", tasks=[return_scheduled_start_time])
-        res = f.run(context=dict(scheduled_start_time=42), run_on_schedule=True)
+        res = f.run(
+            context=dict(scheduled_start_time=42), run_on_schedule=run_on_schedule
+        )
 
         assert res.is_successful()
         assert res.result[return_scheduled_start_time].result != 42