Repository: PrefectHQ/prefect

Issue:
Add test for whether heartbeats stay alive post-run
This could be implemented by mocking `self.cache_result` for the TaskRunner and having it sleep; if the heartbeat interval is short enough, then if heartbeats are still ongoing we would expect calls to be made.  We expect and _want_ heartbeats to end the second the `run` method is complete.  **cc:** @jlowin 


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
@@ -202,11 +202,13 @@ def run(
 
         # All other exceptions are trapped and turned into Failed states
         except Exception as exc:
-            self.logger.info("Unexpected error while running flow.")
+            self.logger.info(
+                "Unexpected error while running flow: {}".format(repr(exc))
+            )
             if raise_on_exception:
                 raise exc
             return Failed(
-                message="Unexpected error while running flow: {}".format(str(exc)),
+                message="Unexpected error while running flow: {}".format(repr(exc)),
                 result=exc,
             )
 
diff --git a/src/prefect/engine/runner.py b/src/prefect/engine/runner.py
--- a/src/prefect/engine/runner.py
+++ b/src/prefect/engine/runner.py
@@ -70,10 +70,11 @@ def inner(self: "Runner", state: State, *args: Any, **kwargs: Any) -> State:
             new_state = exc.state
 
         except Exception as exc:
-            self.logger.info("Unexpected error.")
+            formatted = "Unexpected error: {}".format(repr(exc))
+            self.logger.info(formatted)
             if raise_on_exception:
                 raise exc
-            new_state = Failed("Unexpected error.", result=exc)
+            new_state = Failed(formatted, result=exc)
 
         if new_state is not state:
             new_state = self.handle_state_change(old_state=state, new_state=new_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
@@ -300,7 +300,7 @@ def run(
                 raise exc
 
         except Exception as exc:
-            msg = "Unexpected error while running task: {}".format(str(exc))
+            msg = "Unexpected error while running task: {}".format(repr(exc))
             self.logger.info(msg)
             state = Failed(message=msg, result=exc)
             raise_on_exception = prefect.context.get("raise_on_exception", False)
@@ -443,15 +443,18 @@ def check_task_trigger(
         # Exceptions are trapped and turned into TriggerFailed states
         except Exception as exc:
             self.logger.info(
-                "Unexpected error while evaluating task trigger '{}'.".format(
-                    self.task.name
+                "Unexpected error while evaluating task trigger for '{name}': {exc}".format(
+                    exc=repr(exc), name=self.task.name
                 )
             )
             if prefect.context.get("raise_on_exception"):
                 raise exc
             raise ENDRUN(
                 TriggerFailed(
-                    "Unexpected error while checking task trigger.", result=exc
+                    "Unexpected error while checking task trigger: {}".format(
+                        repr(exc)
+                    ),
+                    result=exc,
                 )
             )
 


Test patch:
diff --git a/tests/engine/cloud/test_cloud_task_runner.py b/tests/engine/cloud/test_cloud_task_runner.py
--- a/tests/engine/cloud/test_cloud_task_runner.py
+++ b/tests/engine/cloud/test_cloud_task_runner.py
@@ -244,6 +244,25 @@ def sleeper():
         assert client.update_task_run_heartbeat.called
         assert client.update_task_run_heartbeat.call_count >= 2
 
+    @pytest.mark.parametrize(
+        "executor", ["local", "sync", "mproc", "mthread"], indirect=True
+    )
+    def test_task_runner_has_a_heartbeat_only_during_execution(
+        self, executor, monkeypatch
+    ):
+        client = MagicMock()
+        monkeypatch.setattr(
+            "prefect.engine.cloud.task_runner.Client", MagicMock(return_value=client)
+        )
+
+        with set_temporary_config({"cloud.heartbeat_interval": 0.05}):
+            runner = CloudTaskRunner(task=Task())
+            runner.cache_result = lambda *args, **kwargs: time.sleep(0.2)
+            res = runner.run(executor=executor)
+
+        assert client.update_task_run_heartbeat.called
+        assert client.update_task_run_heartbeat.call_count == 1
+
     @pytest.mark.parametrize(
         "executor", ["local", "sync", "mproc", "mthread"], indirect=True
     )